From 4003519deb636a575064ca53a34b58ef2abe24e1 Mon Sep 17 00:00:00 2001 From: 446564 Date: Tue, 20 Jan 2026 15:26:41 -0800 Subject: [PATCH 001/421] add community to hashtag channel name brings behavior in line with community public channels and prefixes the community name this allows users to use the same radio with multiple clients and be able to tell which hashtag channel they are using i.e. Scouts #leaders, where previous it was just a private chanel named #leaders. --- lib/screens/channels_screen.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 1cb66ab..22fb768 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -917,10 +917,11 @@ class _ChannelsScreenState extends State if (hashtag.startsWith('#')) { hashtag = hashtag.substring(1); } - final channelName = '#$hashtag'; + final String channelName; final Uint8List psk; if (isRegularHashtag) { + channelName = '#$hashtag'; // Regular hashtag - public derivation using SHA256 psk = Channel.derivePskFromHashtag(hashtag); } else { @@ -931,6 +932,7 @@ class _ChannelsScreenState extends State ); return; } + channelName = '${selectedCommunity!.name} #$hashtag'; psk = selectedCommunity!.deriveCommunityHashtagPsk(hashtag); // Track in community's hashtag list await _communityStore.addHashtagChannel(selectedCommunity!.id, hashtag); From 30bcbedf5efeeb186d3079f2082795df560edaf8 Mon Sep 17 00:00:00 2001 From: 446564 Date: Tue, 20 Jan 2026 17:21:44 -0800 Subject: [PATCH 002/421] update tooltips add missing tooltip: - channels, add channel button - map, filter nodes button --- lib/screens/channels_screen.dart | 1 + lib/screens/map_screen.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 1cb66ab..101828b 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -299,6 +299,7 @@ class _ChannelsScreenState extends State ), floatingActionButton: FloatingActionButton( onPressed: () => _showAddChannelDialog(context), + tooltip: context.l10n.channels_addChannel, child: const Icon(Icons.add), ), bottomNavigationBar: SafeArea( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 5b804eb..74e5cf9 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -354,6 +354,7 @@ class _MapScreenState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () => _showFilterDialog(context, settingsService), + tooltip: context.l10n.map_filterNodes, child: const Icon(Icons.filter_list), ), ), From 26d9029538f375240d9e438a207672b189c57722 Mon Sep 17 00:00:00 2001 From: 446564 Date: Tue, 20 Jan 2026 17:35:14 -0800 Subject: [PATCH 003/421] remove msg notify prefix when preview avail this removes the 'Received new message: ' prefix from notications when there is a message preview available --- lib/services/notification_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 09039f0..cefeb2a 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -206,7 +206,7 @@ class NotificationService { final preview = _truncateMessage(message, 30); final body = preview.isEmpty ? 'Received new message' - : 'Received new message: $preview'; + : preview; await _notifications.show( channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, From a0be63b2e74ed33a03ba57b9d1c6f2c569c0ad86 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 20 Jan 2026 21:42:54 -0700 Subject: [PATCH 004/421] feat: integrate link handling in chat screen with linkify support - Added flutter_linkify package to auto-detect and linkify URLs in chat messages. - Implemented LinkHandler class to manage link tap confirmations and URL launching. - Updated chat_screen.dart to use Linkify for displaying message text with links. - Registered url_launcher plugin for handling URL launches across platforms. - Updated pubspec.yaml and pubspec.lock to include new dependencies. - Cleaned up untranslated.json by removing unused translations. --- android/app/src/main/AndroidManifest.xml | 9 + ios/Runner/Info.plist | 5 + lib/helpers/link_handler.dart | 76 ++ lib/l10n/app_bg.arb | 53 +- lib/l10n/app_de.arb | 53 +- lib/l10n/app_en.arb | 10 + lib/l10n/app_es.arb | 53 +- lib/l10n/app_fr.arb | 53 +- lib/l10n/app_it.arb | 53 +- lib/l10n/app_localizations.dart | 30 + lib/l10n/app_localizations_bg.dart | 32 +- lib/l10n/app_localizations_de.dart | 32 +- lib/l10n/app_localizations_en.dart | 18 + lib/l10n/app_localizations_es.dart | 32 +- lib/l10n/app_localizations_fr.dart | 32 +- lib/l10n/app_localizations_it.dart | 32 +- lib/l10n/app_localizations_nl.dart | 32 +- lib/l10n/app_localizations_pl.dart | 32 +- lib/l10n/app_localizations_pt.dart | 32 +- lib/l10n/app_localizations_sk.dart | 32 +- lib/l10n/app_localizations_sl.dart | 32 +- lib/l10n/app_localizations_sv.dart | 32 +- lib/l10n/app_localizations_zh.dart | 31 +- lib/l10n/app_nl.arb | 53 +- lib/l10n/app_pl.arb | 53 +- lib/l10n/app_pt.arb | 53 +- lib/l10n/app_sk.arb | 53 +- lib/l10n/app_sl.arb | 53 +- lib/l10n/app_sv.arb | 53 +- lib/l10n/app_zh.arb | 53 +- lib/screens/channel_chat_screen.dart | 17 +- lib/screens/channels_screen.dart | 815 +++++++++--------- lib/screens/chat_screen.dart | 16 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 84 +- pubspec.yaml | 2 + untranslated.json | 122 +-- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 41 files changed, 1615 insertions(+), 619 deletions(-) create mode 100644 lib/helpers/link_handler.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 43cacc9..b8dd623 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -67,5 +67,14 @@ + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index b4e35ed..92ebc46 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -55,5 +55,10 @@ This app uses Bluetooth to communicate with MeshCore devices. NSCameraUsageDescription This app uses the camera to scan QR codes for joining communities. + LSApplicationQueriesSchemes + + http + https + diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart new file mode 100644 index 0000000..fa8e5ff --- /dev/null +++ b/lib/helpers/link_handler.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../l10n/l10n.dart'; + +class LinkHandler { + static Future handleLinkTap(BuildContext context, String url) async { + // Show confirmation dialog + final shouldOpen = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.chat_openLink), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.chat_openLinkConfirmation, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: SelectableText( + url, + style: const TextStyle( + fontSize: 12, + fontFamily: 'monospace', + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(context.l10n.common_cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.chat_open), + ), + ], + ), + ); + + if (shouldOpen != true) return; + + // Launch URL + try { + final uri = Uri.parse(url); + if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_couldNotOpenLink(url)), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_invalidLink), + backgroundColor: Colors.red, + ), + ); + } + } + } +} diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 1b5e5de..e5f40f3 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Отваряне на връзката?", + "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?", + "chat_open": "Отвори", + "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Невалиден формат на връзката", "map_title": "Карта на възлите", "map_noNodesWithLocation": "Няма възли с данни за местоположение.", "map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Остави общността \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)", "community_communityHashtag": "Общностен хаштаг", "community_communityHashtagDesc": "Само за членове на общността", - "community_forCommunity": "За {name}" + "community_forCommunity": "За {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.", + "community_secretRegenerated": "Секретно презареждане за \"{name}\"", + "community_regenerateSecret": "Регенерейрай секрет", + "community_regenerate": "Регенерация", + "community_updateSecret": "Актуализирай тайна", + "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"", + "community_secretUpdated": "Секретно обновено за \"{name}\"" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 07f395a..0bb17c8 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Link öffnen?", + "chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?", + "chat_open": "Öffnen", + "chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Ungültiges Link-Format", "map_title": "Karte", "map_noNodesWithLocation": "Keine Knoten mit Standortdaten", "map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Community \"{name}\" verlassen", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)", "community_communityHashtagDesc": "Nur für Mitglieder der Community", "community_forCommunity": "Für {name}", - "community_communityHashtag": "Community Hashtag" + "community_communityHashtag": "Community Hashtag", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerate": "Neu generieren", + "community_secretRegenerated": "Geheime Wiederherstellung für \"{name}\" erfolgreich", + "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.", + "community_regenerateSecret": "Neu generieren Sie das Geheimnis", + "community_secretUpdated": "Geheime für \"{name}\" aktualisiert", + "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", + "community_updateSecret": "Aktualisieren Sie das Geheimnis" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1c1ee51..56cb1cc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -550,6 +550,16 @@ "count": {"type": "int"} } }, + "chat_openLink": "Open Link?", + "chat_openLinkConfirmation": "Do you want to open this link in your browser?", + "chat_open": "Open", + "chat_couldNotOpenLink": "Could not open link: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": {"type": "String"} + } + }, + "chat_invalidLink": "Invalid link format", "map_title": "Node Map", "map_noNodesWithLocation": "No nodes with location data", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index b406e94..4b6b526 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "¿Abrir enlace?", + "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?", + "chat_open": "Abrir", + "chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Formato de enlace no válido", "map_title": "Mapa de Nodos", "map_noNodesWithLocation": "No hay nodos con datos de ubicación", "map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Has salido de la comunidad \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)", "community_communityHashtag": "Hashtag de la Comunidad", "community_communityHashtagDesc": "Exclusivo para miembros de la comunidad", - "community_forCommunity": "Para {name}" + "community_forCommunity": "Para {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "Regenerar Contraseña Secreta", + "community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.", + "community_secretRegenerated": "Código secreto regenerado para \"{name}\"", + "community_regenerate": "Regenerar", + "community_secretUpdated": "Confidencialidad actualizada para \"{name}\"", + "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"", + "community_updateSecret": "Actualizar Contraseña" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 785ee37..1b8d35d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Ouvrir le lien ?", + "chat_openLinkConfirmation": "Voulez-vous ouvrir ce lien dans votre navigateur ?", + "chat_open": "Ouvrir", + "chat_couldNotOpenLink": "Impossible d'ouvrir le lien : {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Format de lien invalide", "map_title": "Carte des nœuds", "map_noNodesWithLocation": "Aucun nœud avec des données de localisation", "map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Communauté \"{name}\" quittée", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)", "community_communityHashtag": "Hashtag de la communauté", "community_communityHashtagDesc": "Exclusif aux membres de la communauté", - "community_forCommunity": "Pour {name}" + "community_forCommunity": "Pour {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "Régénérer le secret", + "community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.", + "community_regenerate": "Régénérer", + "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"", + "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"", + "community_updateSecret": "Mettre à jour le secret", + "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b0c13a0..cd031fb 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Aprire il link?", + "chat_openLinkConfirmation": "Vuoi aprire questo link nel tuo browser?", + "chat_open": "Apri", + "chat_couldNotOpenLink": "Impossibile aprire il link: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Formato di link non valido", "map_title": "Mappa Nodi", "map_noNodesWithLocation": "Nessun nodo con dati di posizione", "map_nodesNeedGps": "I nodi devono condividere le loro coordinate GPS\nper apparire sulla mappa", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Hai lasciato la comunità \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)", "community_communityHashtag": "Hashtag della Comunità", "community_communityHashtagDesc": "Visibile solo ai membri della comunità", - "community_forCommunity": "Per {name}" + "community_forCommunity": "Per {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecretConfirm": "Regenera la chiave segreta per \"{name}\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.", + "community_regenerateSecret": "Ri genera la chiave segreta", + "community_regenerate": "Rigenera", + "community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"", + "community_updateSecret": "Aggiorna Segreto", + "community_secretUpdated": "Segreto aggiornato per \"{name}\"", + "community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index fe4fc01..d52830c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2226,6 +2226,36 @@ abstract class AppLocalizations { /// **'Unread: {count}'** String chat_unread(int count); + /// No description provided for @chat_openLink. + /// + /// In en, this message translates to: + /// **'Open Link?'** + String get chat_openLink; + + /// No description provided for @chat_openLinkConfirmation. + /// + /// In en, this message translates to: + /// **'Do you want to open this link in your browser?'** + String get chat_openLinkConfirmation; + + /// No description provided for @chat_open. + /// + /// In en, this message translates to: + /// **'Open'** + String get chat_open; + + /// No description provided for @chat_couldNotOpenLink. + /// + /// In en, this message translates to: + /// **'Could not open link: {url}'** + String chat_couldNotOpenLink(String url); + + /// No description provided for @chat_invalidLink. + /// + /// In en, this message translates to: + /// **'Invalid link format'** + String get chat_invalidLink; + /// No description provided for @map_title. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 314e702..9b70d9c 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1207,6 +1207,24 @@ class AppLocalizationsBg extends AppLocalizations { return 'Непрочетени: $count'; } + @override + String get chat_openLink => 'Отваряне на връзката?'; + + @override + String get chat_openLinkConfirmation => + 'Искате ли да отворите тази връзка в браузъра си?'; + + @override + String get chat_open => 'Отвори'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Не можа да се отвори връзката: $url'; + } + + @override + String get chat_invalidLink => 'Невалиден формат на връзката'; + @override String get map_title => 'Карта на възлите'; @@ -2567,32 +2585,32 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Регенерейрай секрет'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Регенерация'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Секретно презареждане за \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Актуализирай тайна'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Секретно обновено за \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b884f3c..9bab237 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1206,6 +1206,24 @@ class AppLocalizationsDe extends AppLocalizations { return 'Ungelesen: $count'; } + @override + String get chat_openLink => 'Link öffnen?'; + + @override + String get chat_openLinkConfirmation => + 'Möchten Sie diesen Link in Ihrem Browser öffnen?'; + + @override + String get chat_open => 'Öffnen'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Link konnte nicht geöffnet werden: $url'; + } + + @override + String get chat_invalidLink => 'Ungültiges Link-Format'; + @override String get map_title => 'Karte'; @@ -2570,32 +2588,32 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Neu generieren Sie das Geheimnis'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Neu generieren'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Geheime Wiederherstellung für \"$name\" erfolgreich'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Aktualisieren Sie das Geheimnis'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Geheime für \"$name\" aktualisiert'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.'; } @override diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 96ba1b9..86f18ba 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1186,6 +1186,24 @@ class AppLocalizationsEn extends AppLocalizations { return 'Unread: $count'; } + @override + String get chat_openLink => 'Open Link?'; + + @override + String get chat_openLinkConfirmation => + 'Do you want to open this link in your browser?'; + + @override + String get chat_open => 'Open'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Could not open link: $url'; + } + + @override + String get chat_invalidLink => 'Invalid link format'; + @override String get map_title => 'Node Map'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 029ed11..908c88c 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1204,6 +1204,24 @@ class AppLocalizationsEs extends AppLocalizations { return 'Sin leer: $count'; } + @override + String get chat_openLink => '¿Abrir enlace?'; + + @override + String get chat_openLinkConfirmation => + '¿Quiere abrir este enlace en su navegador?'; + + @override + String get chat_open => 'Abrir'; + + @override + String chat_couldNotOpenLink(String url) { + return 'No se pudo abrir el enlace: $url'; + } + + @override + String get chat_invalidLink => 'Formato de enlace no válido'; + @override String get map_title => 'Mapa de Nodos'; @@ -2565,32 +2583,32 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regenerar Contraseña Secreta'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regenerar'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Código secreto regenerado para \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Actualizar Contraseña'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Confidencialidad actualizada para \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1dce57f..48a6ac4 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1209,6 +1209,24 @@ class AppLocalizationsFr extends AppLocalizations { return 'Non lu : $count'; } + @override + String get chat_openLink => 'Ouvrir le lien ?'; + + @override + String get chat_openLinkConfirmation => + 'Voulez-vous ouvrir ce lien dans votre navigateur ?'; + + @override + String get chat_open => 'Ouvrir'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Impossible d\'ouvrir le lien : $url'; + } + + @override + String get chat_invalidLink => 'Format de lien invalide'; + @override String get map_title => 'Carte des nœuds'; @@ -2581,32 +2599,32 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Régénérer le secret'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Régénérer'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Mot de passe secret régénéré pour \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Mettre à jour le secret'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Modification secrète mise à jour pour \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 20df619..83010d8 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1203,6 +1203,24 @@ class AppLocalizationsIt extends AppLocalizations { return 'Non letti: $count'; } + @override + String get chat_openLink => 'Aprire il link?'; + + @override + String get chat_openLinkConfirmation => + 'Vuoi aprire questo link nel tuo browser?'; + + @override + String get chat_open => 'Apri'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Impossibile aprire il link: $url'; + } + + @override + String get chat_invalidLink => 'Formato di link non valido'; + @override String get map_title => 'Mappa Nodi'; @@ -2565,32 +2583,32 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Ri genera la chiave segreta'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Regenera la chiave segreta per \"$name\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Rigenera'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Codice segreto rigenerato per \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Aggiorna Segreto'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Segreto aggiornato per \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 50f5744..ce60a8f 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1199,6 +1199,24 @@ class AppLocalizationsNl extends AppLocalizations { return 'Nieuw: $count'; } + @override + String get chat_openLink => 'Link openen?'; + + @override + String get chat_openLinkConfirmation => + 'Wilt u deze link in uw browser openen?'; + + @override + String get chat_open => 'Openen'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Kan link niet openen: $url'; + } + + @override + String get chat_invalidLink => 'Ongeldig linkformaat'; + @override String get map_title => 'Node Map'; @@ -2556,32 +2574,32 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regeneer Geheimwoord'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Regeneere de geheime sleutel voor \"$name\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regeneer'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Geheim hersteld voor \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Bijwerken Geheime'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Geheim gewijzigd voor \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scan de nieuwe QR-code om het geheim voor \"$name\" bij te werken'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 378858a..13fbeb0 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1205,6 +1205,24 @@ class AppLocalizationsPl extends AppLocalizations { return 'Niezgłoszone: $count'; } + @override + String get chat_openLink => 'Otworzyć link?'; + + @override + String get chat_openLinkConfirmation => + 'Czy chcesz otworzyć ten link w przeglądarce?'; + + @override + String get chat_open => 'Otwórz'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Nie można otworzyć linku: $url'; + } + + @override + String get chat_invalidLink => 'Nieprawidłowy format linku'; + @override String get map_title => 'Mapa węzłów'; @@ -2564,32 +2582,32 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Zregeneruj sekret'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Zregeneruj'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Hasło ponownie wygenerowane dla \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Zaktualizuj tajny klucz'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Hasło zaktualizowane dla \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ae02aff..3f54001 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1204,6 +1204,24 @@ class AppLocalizationsPt extends AppLocalizations { return 'Não lido: $count'; } + @override + String get chat_openLink => 'Abrir link?'; + + @override + String get chat_openLinkConfirmation => + 'Deseja abrir este link no seu navegador?'; + + @override + String get chat_open => 'Abrir'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Não foi possível abrir o link: $url'; + } + + @override + String get chat_invalidLink => 'Formato de link inválido'; + @override String get map_title => 'Mapa de Nós'; @@ -2567,32 +2585,32 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regenerar Senha Segura'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regenerar'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Senha secreta regenerada para \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Atualizar Segredo'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Segredo atualizado para \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 81bf16a..75d4654 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1200,6 +1200,24 @@ class AppLocalizationsSk extends AppLocalizations { return 'Nezriadené: $count'; } + @override + String get chat_openLink => 'Otvoriť odkaz?'; + + @override + String get chat_openLinkConfirmation => + 'Chcete otvoriť tento odkaz v prehliadači?'; + + @override + String get chat_open => 'Otvoriť'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Nepodarilo sa otvoriť odkaz: $url'; + } + + @override + String get chat_invalidLink => 'Neplatný formát odkazu'; + @override String get map_title => 'Mapa uzlov'; @@ -2553,32 +2571,32 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Zobraziť nový tajný kód'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Znovu vygenerovať'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Záznam pre \"$name\" bol regenerovaný tajne'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Aktualizovať tajné heslo'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Zmena tajnej slova pre \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index cdcd23c..be9556f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1197,6 +1197,24 @@ class AppLocalizationsSl extends AppLocalizations { return 'Nerešeno: $count'; } + @override + String get chat_openLink => 'Odpreti povezavo?'; + + @override + String get chat_openLinkConfirmation => + 'Ali želite odpreti to povezavo v brskalniku?'; + + @override + String get chat_open => 'Odpri'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Povezave ni bilo mogoče odpreti: $url'; + } + + @override + String get chat_invalidLink => 'Neveljavna oblika povezave'; + @override String get map_title => 'Mapa omrežja'; @@ -2557,32 +2575,32 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Preberi nov tajni kôd'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Preberi znova'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Tajna za \"$name\" ponovno ustvarjena'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Ažurniraj tajno'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Skrivnostno spremembo za \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skeniraj nov kôd QR za posodabljanje tajne za $name'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 8b36b97..34b54b4 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1192,6 +1192,24 @@ class AppLocalizationsSv extends AppLocalizations { return 'Olästa: $count'; } + @override + String get chat_openLink => 'Öppna länk?'; + + @override + String get chat_openLinkConfirmation => + 'Vill du öppna den här länken i din webbläsare?'; + + @override + String get chat_open => 'Öppna'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Kunde inte öppna länken: $url'; + } + + @override + String get chat_invalidLink => 'Ogiltigt länkformat'; + @override String get map_title => 'Nodkarta'; @@ -2541,32 +2559,32 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => 'Regenerera hemlig kod'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => 'Regenerera'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return 'Lösenord återskapad för \"$name\"'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => 'Uppdatera hemlighet'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return 'Hemlighet uppdaterad för \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 3d7bd06..cd9c3be 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1148,6 +1148,23 @@ class AppLocalizationsZh extends AppLocalizations { return '未读:$count'; } + @override + String get chat_openLink => '打开链接?'; + + @override + String get chat_openLinkConfirmation => '您想在浏览器中打开此链接吗?'; + + @override + String get chat_open => '打开'; + + @override + String chat_couldNotOpenLink(String url) { + return '无法打开链接:$url'; + } + + @override + String get chat_invalidLink => '链接格式无效'; + @override String get map_title => '节点地图'; @@ -2425,32 +2442,32 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerate Secret'; + String get community_regenerateSecret => '重新生成密钥'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.'; + return '重新生成“$name”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。'; } @override - String get community_regenerate => 'Regenerate'; + String get community_regenerate => '重新生成'; @override String community_secretRegenerated(String name) { - return 'Secret regenerated for \"$name\"'; + return '密码已重置为“$name”'; } @override - String get community_updateSecret => 'Update Secret'; + String get community_updateSecret => '更新密钥'; @override String community_secretUpdated(String name) { - return 'Secret updated for \"$name\"'; + return '密码已更新为“$name”'; } @override String community_scanToUpdateSecret(String name) { - return 'Scan the new QR code to update the secret for \"$name\"'; + return '扫描新的二维码更新\"$name\"的密码'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 75a6cc0..48ef3dd 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Link openen?", + "chat_openLinkConfirmation": "Wilt u deze link in uw browser openen?", + "chat_open": "Openen", + "chat_couldNotOpenLink": "Kan link niet openen: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Ongeldig linkformaat", "map_title": "Node Map", "map_noNodesWithLocation": "Geen nodes met locatiegegevens", "map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Community \"{name}\" verlaten", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)", "community_communityHashtag": "Gemeenschappelijk Hashtag", "community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community", - "community_forCommunity": "Voor {name}" + "community_forCommunity": "Voor {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_secretRegenerated": "Geheim hersteld voor \"{name}\"", + "community_regenerateSecret": "Regeneer Geheimwoord", + "community_regenerateSecretConfirm": "Regeneere de geheime sleutel voor \"{name}\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.", + "community_regenerate": "Regeneer", + "community_updateSecret": "Bijwerken Geheime", + "community_secretUpdated": "Geheim gewijzigd voor \"{name}\"", + "community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 50732d1..823bba1 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Otworzyć link?", + "chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?", + "chat_open": "Otwórz", + "chat_couldNotOpenLink": "Nie można otworzyć linku: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Nieprawidłowy format linku", "map_title": "Mapa węzłów", "map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi", "map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Opuszczono społeczność \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)", "community_communityHashtag": "Hashtag Społeczności", "community_communityHashtagDesc": "Dostępne tylko dla członków społeczności", - "community_forCommunity": "Dla {name}" + "community_forCommunity": "Dla {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerate": "Zregeneruj", + "community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"", + "community_regenerateSecret": "Zregeneruj sekret", + "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.", + "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"", + "community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"", + "community_updateSecret": "Zaktualizuj tajny klucz" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 34797be..b48db37 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Abrir link?", + "chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?", + "chat_open": "Abrir", + "chat_couldNotOpenLink": "Não foi possível abrir o link: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Formato de link inválido", "map_title": "Mapa de Nós", "map_noNodesWithLocation": "Não existem nós com dados de localização.", "map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Saiu da comunidade \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)", "community_communityHashtag": "Hashtag da Comunidade", "community_communityHashtagDesc": "Apenas para membros da comunidade", - "community_forCommunity": "Para {name}" + "community_forCommunity": "Para {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.", + "community_regenerateSecret": "Regenerar Senha Segura", + "community_secretRegenerated": "Senha secreta regenerada para \"{name}\"", + "community_regenerate": "Regenerar", + "community_secretUpdated": "Segredo atualizado para \"{name}\"", + "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++", + "community_updateSecret": "Atualizar Segredo" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d6ea7d8..71871d1 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Otvoriť odkaz?", + "chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?", + "chat_open": "Otvoriť", + "chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Neplatný formát odkazu", "map_title": "Mapa uzlov", "map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe", "map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Opustená komunita \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)", "community_communityHashtag": "Komunitný Hashtag", "community_communityHashtagDesc": "Špecifické pre členov komunity", - "community_forCommunity": "Pre {name}" + "community_forCommunity": "Pre {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne", + "community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.", + "community_regenerate": "Znovu vygenerovať", + "community_regenerateSecret": "Zobraziť nový tajný kód", + "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"", + "community_updateSecret": "Aktualizovať tajné heslo", + "community_secretUpdated": "Zmena tajnej slova pre \"{name}\"" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 09ee0bc..977f29c 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Odpreti povezavo?", + "chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?", + "chat_open": "Odpri", + "chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Neveljavna oblika povezave", "map_title": "Mapa omrežja", "map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.", "map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Zapustil skupnost \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)", "community_communityHashtag": "Skupnostni hashtag", "community_communityHashtagDesc": "Izključeno za uporabnike skupnosti", - "community_forCommunity": "Za {name}" + "community_forCommunity": "Za {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_secretRegenerated": "Tajna za \"{name}\" ponovno ustvarjena", + "community_regenerateSecret": "Preberi nov tajni kôd", + "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.", + "community_regenerate": "Preberi znova", + "community_scanToUpdateSecret": "Skeniraj nov kôd QR za posodabljanje tajne za {name}", + "community_updateSecret": "Ažurniraj tajno", + "community_secretUpdated": "Skrivnostno spremembo za \"{name}\"" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 4d302a5..f1da7c8 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "Öppna länk?", + "chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?", + "chat_open": "Öppna", + "chat_couldNotOpenLink": "Kunde inte öppna länken: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Ogiltigt länkformat", "map_title": "Nodkarta", "map_noNodesWithLocation": "Inga noder med platsinformation", "map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Lämnade community \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)", "community_communityHashtagDesc": "Endast för medlemmar", "community_forCommunity": "För {name}", - "community_communityHashtag": "Community Hashtag" + "community_communityHashtag": "Community Hashtag", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerate": "Regenerera", + "community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.", + "community_secretRegenerated": "Lösenord återskapad för \"{name}\"", + "community_regenerateSecret": "Regenerera hemlig kod", + "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"", + "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"", + "community_updateSecret": "Uppdatera hemlighet" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c070414..ae10f60 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -604,6 +604,18 @@ } } }, + "chat_openLink": "打开链接?", + "chat_openLinkConfirmation": "您想在浏览器中打开此链接吗?", + "chat_open": "打开", + "chat_couldNotOpenLink": "无法打开链接:{url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "链接格式无效", "map_title": "节点地图", "map_noNodesWithLocation": "没有具有位置数据的节点", "map_nodesNeedGps": "节点需要共享它们的 GPS 坐标\n才能在地图上显示", @@ -1473,7 +1485,9 @@ "community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "已退出社区 \"{name}\"", @@ -1484,5 +1498,40 @@ "community_regularHashtagDesc": "公共话题(任何人都可以加入)", "community_communityHashtag": "社区标签", "community_communityHashtagDesc": "仅限社区成员使用", - "community_forCommunity": "对于 {name}" + "community_forCommunity": "对于 {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "重新生成密钥", + "community_secretRegenerated": "密码已重置为“{name}”", + "community_regenerate": "重新生成", + "community_regenerateSecretConfirm": "重新生成“{name}”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。", + "community_scanToUpdateSecret": "扫描新的二维码更新\"{name}\"的密码", + "community_updateSecret": "更新密钥", + "community_secretUpdated": "密码已更新为“{name}”" } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 4664843..84aeb0a 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -3,11 +3,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; @@ -280,9 +282,20 @@ class _ChannelChatScreenState extends State { : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), ) else - Text( - message.text, + Linkify( + text: message.text, style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => LinkHandler.handleLinkTap(context, link.url), ), if (displayPath.isNotEmpty) ...[ const SizedBox(height: 4), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 1cb66ab..a120a0d 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -18,7 +18,6 @@ import '../widgets/battery_indicator.dart'; import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; import '../widgets/qr_code_display.dart'; -import '../widgets/qr_scanner_widget.dart'; import '../widgets/quick_switch_bar.dart'; import '../widgets/unread_badge.dart'; import 'channel_chat_screen.dart'; @@ -27,20 +26,12 @@ import 'contacts_screen.dart'; import 'map_screen.dart'; import 'settings_screen.dart'; -enum ChannelSortOption { - manual, - name, - latestMessages, - unread, -} +enum ChannelSortOption { manual, name, latestMessages, unread } class ChannelsScreen extends StatefulWidget { final bool hideBackButton; - const ChannelsScreen({ - super.key, - this.hideBackButton = false, - }); + const ChannelsScreen({super.key, this.hideBackButton = false}); @override State createState() => _ChannelsScreenState(); @@ -54,7 +45,7 @@ class _ChannelsScreenState extends State Timer? _searchDebounce; ChannelSortOption _sortOption = ChannelSortOption.manual; List _communities = []; - + // Cache of PSK hex -> Community for quick lookup final Map _pskToCommunity = {}; @@ -66,7 +57,7 @@ class _ChannelsScreenState extends State _loadCommunities(); }); } - + Future _loadCommunities() async { final communities = await _communityStore.loadCommunities(); if (mounted) { @@ -76,14 +67,14 @@ class _ChannelsScreenState extends State }); } } - + void _buildPskCommunityMap() { _pskToCommunity.clear(); for (final community in _communities) { // Map the community public channel PSK final publicPsk = community.deriveCommunityPublicPsk(); _pskToCommunity[Channel.formatPskHex(publicPsk)] = community; - + // Map all known hashtag channel PSKs for (final hashtag in community.hashtagChannels) { final hashtagPsk = community.deriveCommunityHashtagPsk(hashtag); @@ -91,12 +82,12 @@ class _ChannelsScreenState extends State } } } - + /// Returns the community this channel belongs to, or null if not a community channel Community? _getCommunityForChannel(Channel channel) { return _pskToCommunity[channel.pskHex]; } - + /// Returns true if this is the community's public channel bool _isCommunityPublicChannel(Channel channel, Community community) { final publicPsk = community.deriveCommunityPublicPsk(); @@ -181,7 +172,10 @@ class _ChannelsScreenState extends State ); } - final filteredChannels = _filterAndSortChannels(channels, connector); + final filteredChannels = _filterAndSortChannels( + channels, + connector, + ); return Column( children: [ @@ -211,17 +205,22 @@ class _ChannelsScreenState extends State border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), ), onChanged: (value) { _searchDebounce?.cancel(); - _searchDebounce = Timer(const Duration(milliseconds: 300), () { - if (!mounted) return; - setState(() { - _searchQuery = value.toLowerCase(); - }); - }); + _searchDebounce = Timer( + const Duration(milliseconds: 300), + () { + if (!mounted) return; + setState(() { + _searchQuery = value.toLowerCase(); + }); + }, + ); }, ), ), @@ -235,11 +234,18 @@ class _ChannelsScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey[400]), + Icon( + Icons.search_off, + size: 64, + color: Colors.grey[400], + ), const SizedBox(height: 16), Text( context.l10n.channels_noChannelsFound, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), ), ], ), @@ -247,51 +253,58 @@ class _ChannelsScreenState extends State ), ], ) - : (_sortOption == ChannelSortOption.manual && _searchQuery.isEmpty) - ? ReorderableListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, + : (_sortOption == ChannelSortOption.manual && + _searchQuery.isEmpty) + ? ReorderableListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, + ), + buildDefaultDragHandles: false, + itemCount: filteredChannels.length, + onReorder: (oldIndex, newIndex) { + if (newIndex > oldIndex) newIndex -= 1; + final reordered = List.from( + filteredChannels, + ); + final item = reordered.removeAt(oldIndex); + reordered.insert(newIndex, item); + unawaited( + connector.setChannelOrder( + reordered.map((c) => c.index).toList(), ), - buildDefaultDragHandles: false, - itemCount: filteredChannels.length, - onReorder: (oldIndex, newIndex) { - if (newIndex > oldIndex) newIndex -= 1; - final reordered = List.from(filteredChannels); - final item = reordered.removeAt(oldIndex); - reordered.insert(newIndex, item); - unawaited( - connector.setChannelOrder( - reordered.map((c) => c.index).toList(), - ), - ); - }, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile( - context, - connector, - channel, - showDragHandle: true, - dragIndex: index, - ); - }, - ) - : ListView.builder( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 88, - ), - itemCount: filteredChannels.length, - itemBuilder: (context, index) { - final channel = filteredChannels[index]; - return _buildChannelTile(context, connector, channel); - }, - ), + ); + }, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channel, + showDragHandle: true, + dragIndex: index, + ); + }, + ) + : ListView.builder( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 88, + ), + itemCount: filteredChannels.length, + itemBuilder: (context, index) { + final channel = filteredChannels[index]; + return _buildChannelTile( + context, + connector, + channel, + ); + }, + ), ), ], ); @@ -305,7 +318,8 @@ class _ChannelsScreenState extends State top: false, child: QuickSwitchBar( selectedIndex: 1, - onDestinationSelected: (index) => _handleQuickSwitch(index, context), + onDestinationSelected: (index) => + _handleQuickSwitch(index, context), ), ), ), @@ -315,33 +329,34 @@ class _ChannelsScreenState extends State Widget _buildChannelTile( BuildContext context, MeshCoreConnector connector, - Channel channel, - { + Channel channel, { bool showDragHandle = false, int? dragIndex, - } - ) { + }) { final unreadCount = connector.getUnreadCountForChannel(channel); final community = _getCommunityForChannel(channel); final isCommunityChannel = community != null; - final isCommunityPublic = isCommunityChannel && _isCommunityPublicChannel(channel, community); - + final isCommunityPublic = + isCommunityChannel && _isCommunityPublicChannel(channel, community); + // Determine icon and colors based on channel type IconData icon; Color iconColor; Color bgColor; String subtitle; - + if (isCommunityChannel) { // Community channel styling iconColor = Colors.purple; bgColor = Colors.purple.withValues(alpha: 0.2); if (isCommunityPublic) { icon = Icons.groups; - subtitle = '${context.l10n.community_publicChannel} • ${community.name}'; + subtitle = + '${context.l10n.community_publicChannel} • ${community.name}'; } else { icon = Icons.tag; - subtitle = '${context.l10n.community_hashtagChannel} • ${community.name}'; + subtitle = + '${context.l10n.community_hashtagChannel} • ${community.name}'; } } else if (channel.isPublicChannel) { icon = Icons.public; @@ -359,7 +374,7 @@ class _ChannelsScreenState extends State bgColor = Colors.blue.withValues(alpha: 0.2); subtitle = context.l10n.channels_privateChannel; } - + return Card( key: ValueKey('channel_${channel.index}'), margin: const EdgeInsets.only(bottom: 12), @@ -389,24 +404,18 @@ class _ChannelsScreenState extends State width: 2, ), ), - child: const Icon( - Icons.people, - size: 8, - color: Colors.white, - ), + child: const Icon(Icons.people, size: 8, color: Colors.white), ), ), ], ), title: Text( - channel.name.isEmpty ? context.l10n.channels_channelIndex(channel.index) : channel.name, + channel.name.isEmpty + ? context.l10n.channels_channelIndex(channel.index) + : channel.name, style: const TextStyle(fontWeight: FontWeight.w500), ), - subtitle: Text( - subtitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -465,7 +474,10 @@ class _ChannelsScreenState extends State ), ListTile( leading: const Icon(Icons.delete_outline, color: Colors.red), - title: Text(context.l10n.channels_deleteChannel, style: const TextStyle(color: Colors.red)), + title: Text( + context.l10n.channels_deleteChannel, + style: const TextStyle(color: Colors.red), + ), onTap: () async { Navigator.pop(context); await Future.delayed(const Duration(milliseconds: 100)); @@ -486,17 +498,13 @@ class _ChannelsScreenState extends State case 0: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const ContactsScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)), ); break; case 2: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const MapScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const MapScreen(hideBackButton: true)), ); break; } @@ -587,8 +595,12 @@ class _ChannelsScreenState extends State filtered.sort((a, b) { final aMessages = connector.getChannelMessages(a); final bMessages = connector.getChannelMessages(b); - final aLast = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp; - final bLast = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp; + final aLast = aMessages.isEmpty + ? DateTime(1970) + : aMessages.last.timestamp; + final bLast = bMessages.isEmpty + ? DateTime(1970) + : bMessages.last.timestamp; final timeCompare = bLast.compareTo(aLast); if (timeCompare != 0) return timeCompare; return compareByName(a, b); @@ -612,7 +624,9 @@ class _ChannelsScreenState extends State } String _normalizeChannelName(Channel channel) { - if (channel.name.isEmpty) return 'Channel ${channel.index}'; // Fallback for sorting + if (channel.name.isEmpty) { + return 'Channel ${channel.index}'; // Fallback for sorting + } final trimmed = channel.name.trim(); if (trimmed.startsWith('#') && trimmed.length > 1) { return trimmed.substring(1); @@ -622,7 +636,10 @@ class _ChannelsScreenState extends State void _showAddChannelDialog(BuildContext context) { final connector = context.read(); - final nextIndex = _findNextAvailableIndex(connector.channels, connector.maxChannels); + final nextIndex = _findNextAvailableIndex( + connector.channels, + connector.maxChannels, + ); final hasPublicChannel = connector.channels.any((c) => c.isPublicChannel); int? selectedOption; final nameController = TextEditingController(); @@ -647,12 +664,16 @@ class _ChannelsScreenState extends State return ListTile( leading: CircleAvatar( backgroundColor: enabled - ? (isSelected ? Theme.of(dialogContext).colorScheme.primaryContainer : null) + ? (isSelected + ? Theme.of(dialogContext).colorScheme.primaryContainer + : null) : Colors.grey.withValues(alpha: 0.2), child: Icon( icon, color: enabled - ? (isSelected ? Theme.of(dialogContext).colorScheme.primary : null) + ? (isSelected + ? Theme.of(dialogContext).colorScheme.primary + : null) : Colors.grey, ), ), @@ -685,7 +706,10 @@ class _ChannelsScreenState extends State return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: nameController, decoration: InputDecoration( @@ -704,8 +728,16 @@ class _ChannelsScreenState extends State onPressed: () { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_enterChannelName, + ), + ), ); return; } @@ -718,7 +750,13 @@ class _ChannelsScreenState extends State connector.setChannel(nextIndex, name, psk); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelAdded(name))), + SnackBar( + content: Text( + context.l10n.channels_channelAdded( + name, + ), + ), + ), ); } }, @@ -735,7 +773,10 @@ class _ChannelsScreenState extends State return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: nameController, decoration: InputDecoration( @@ -746,7 +787,10 @@ class _ChannelsScreenState extends State ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: pskController, decoration: InputDecoration( @@ -765,8 +809,16 @@ class _ChannelsScreenState extends State final name = nameController.text.trim(); final pskHex = pskController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_enterChannelName, + ), + ), ); return; } @@ -774,8 +826,16 @@ class _ChannelsScreenState extends State try { psk = Channel.parsePskHex(pskHex); } on FormatException { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_pskMustBe32Hex, + ), + ), ); return; } @@ -783,7 +843,13 @@ class _ChannelsScreenState extends State connector.setChannel(nextIndex, name, psk); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelAdded(name))), + SnackBar( + content: Text( + context.l10n.channels_channelAdded( + name, + ), + ), + ), ); } }, @@ -798,18 +864,27 @@ class _ChannelsScreenState extends State case 2: // Join Public Channel return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: Row( children: [ Expanded( child: FilledButton( onPressed: () { - final psk = Channel.parsePskHex(Channel.publicChannelPsk); + final psk = Channel.parsePskHex( + Channel.publicChannelPsk, + ); Navigator.pop(dialogContext); connector.setChannel(nextIndex, 'Public', psk); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_publicChannelAdded)), + SnackBar( + content: Text( + context.l10n.channels_publicChannelAdded, + ), + ), ); } }, @@ -825,55 +900,76 @@ class _ChannelsScreenState extends State children: [ // Only show type selection if user has communities if (_communities.isNotEmpty) ...[ - RadioGroup( + RadioListTile( + value: true, groupValue: isRegularHashtag, onChanged: (v) => setDialogState(() { - if (v != null) { - isRegularHashtag = v; - if (isRegularHashtag) { - selectedCommunity = null; - } else if (selectedCommunity == null && _communities.isNotEmpty) { - selectedCommunity = _communities.first; - } + isRegularHashtag = v!; + if (isRegularHashtag) { + selectedCommunity = null; } }), - child: Column( - children: [ - RadioListTile( - value: true, - title: Text(dialogContext.l10n.community_regularHashtag), - subtitle: Text(dialogContext.l10n.community_regularHashtagDesc), - dense: true, - ), - RadioListTile( - value: false, - title: Text(dialogContext.l10n.community_communityHashtag), - subtitle: Text(dialogContext.l10n.community_communityHashtagDesc), - dense: true, - ), - ], + title: Text( + dialogContext.l10n.community_regularHashtag, ), + subtitle: Text( + dialogContext.l10n.community_regularHashtagDesc, + ), + dense: true, + ), + RadioListTile( + value: false, + groupValue: isRegularHashtag, + onChanged: (v) => setDialogState(() { + isRegularHashtag = v!; + if (!isRegularHashtag && + selectedCommunity == null && + _communities.isNotEmpty) { + selectedCommunity = _communities.first; + } + }), + title: Text( + dialogContext.l10n.community_communityHashtag, + ), + subtitle: Text( + dialogContext.l10n.community_communityHashtagDesc, + ), + dense: true, ), ], // Community dropdown (only if community hashtag selected) if (!isRegularHashtag && _communities.isNotEmpty) Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: DropdownMenu( - initialSelection: selectedCommunity, - dropdownMenuEntries: _communities.map((c) => DropdownMenuEntry( - value: c, - label: c.name, - )).toList(), - onSelected: (c) => setDialogState(() => selectedCommunity = c), - label: Text(dialogContext.l10n.community_selectCommunity), - leadingIcon: const Icon(Icons.groups), - expandedInsets: EdgeInsets.zero, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: DropdownButtonFormField( + initialValue: selectedCommunity, + items: _communities + .map( + (c) => DropdownMenuItem( + value: c, + child: Text(c.name), + ), + ) + .toList(), + onChanged: (c) => + setDialogState(() => selectedCommunity = c), + decoration: InputDecoration( + labelText: + dialogContext.l10n.community_selectCommunity, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.groups), + ), ), ), // Hashtag name input Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: hashtagController, decoration: InputDecoration( @@ -899,7 +995,10 @@ class _ChannelsScreenState extends State ), ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: Row( children: [ Expanded( @@ -907,18 +1006,26 @@ class _ChannelsScreenState extends State onPressed: () async { var hashtag = hashtagController.text.trim(); if (hashtag.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .channels_enterChannelName, + ), + ), ); return; } - + // Normalize hashtag name (remove leading # if present) if (hashtag.startsWith('#')) { hashtag = hashtag.substring(1); } final channelName = '#$hashtag'; - + final Uint8List psk; if (isRegularHashtag) { // Regular hashtag - public derivation using SHA256 @@ -926,24 +1033,46 @@ class _ChannelsScreenState extends State } else { // Community hashtag - HMAC derivation from community secret if (selectedCommunity == null) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.community_selectCommunity)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext + .l10n + .community_selectCommunity, + ), + ), ); return; } - psk = selectedCommunity!.deriveCommunityHashtagPsk(hashtag); + psk = selectedCommunity! + .deriveCommunityHashtagPsk(hashtag); // Track in community's hashtag list - await _communityStore.addHashtagChannel(selectedCommunity!.id, hashtag); + await _communityStore.addHashtagChannel( + selectedCommunity!.id, + hashtag, + ); _loadCommunities(); } - + if (dialogContext.mounted) { Navigator.pop(dialogContext); } - connector.setChannel(nextIndex, channelName, psk); + connector.setChannel( + nextIndex, + channelName, + psk, + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelAdded(channelName))), + SnackBar( + content: Text( + context.l10n.channels_channelAdded( + channelName, + ), + ), + ), ); } }, @@ -958,7 +1087,10 @@ class _ChannelsScreenState extends State case 4: // Scan Community QR return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: Row( children: [ Expanded( @@ -966,15 +1098,16 @@ class _ChannelsScreenState extends State onPressed: () async { Navigator.pop(dialogContext); if (context.mounted) { - await Navigator.push( + final result = await Navigator.push( context, MaterialPageRoute( - builder: (context) => const CommunityQrScannerScreen(), + builder: (context) => + const CommunityQrScannerScreen(), ), ); - // Refresh communities list when returning from scanner - if (context.mounted) { - _loadCommunities(); + // Result handled by scanner screen + if (result != null && context.mounted) { + // Community was joined, refresh might be needed } } }, @@ -990,7 +1123,10 @@ class _ChannelsScreenState extends State return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: TextField( controller: nameController, decoration: InputDecoration( @@ -1009,10 +1145,16 @@ class _ChannelsScreenState extends State addPublicChannel = value ?? true; }); }, - title: Text(dialogContext.l10n.community_addPublicChannel), - subtitle: Text(dialogContext.l10n.community_addPublicChannelHint), + title: Text( + dialogContext.l10n.community_addPublicChannel, + ), + subtitle: Text( + dialogContext.l10n.community_addPublicChannelHint, + ), controlAffinity: ListTileControlAffinity.leading, - contentPadding: const EdgeInsets.symmetric(horizontal: 16), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -1023,47 +1165,68 @@ class _ChannelsScreenState extends State onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { - ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.community_enterName)), + ScaffoldMessenger.of( + dialogContext, + ).showSnackBar( + SnackBar( + content: Text( + dialogContext.l10n.community_enterName, + ), + ), ); return; } - + // Create community with random secret final community = Community.create( id: const Uuid().v4(), name: name, ); - + // Save to store await _communityStore.addCommunity(community); - + // Optionally add the community public channel to the device if (addPublicChannel) { - final psk = community.deriveCommunityPublicPsk(); - final channelName = '${community.name} Public'; - connector.setChannel(nextIndex, channelName, psk); + final psk = community + .deriveCommunityPublicPsk(); + final channelName = + '${community.name} Public'; + connector.setChannel( + nextIndex, + channelName, + psk, + ); } - + if (dialogContext.mounted) { Navigator.pop(dialogContext); } - + // Refresh communities list _loadCommunities(); - + if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.community_created(name))), + SnackBar( + content: Text( + context.l10n.community_created(name), + ), + ), ); - + // Show QR code dialog await QrCodeShareDialog.show( context: context, data: community.toQrJson(), title: context.l10n.community_qrTitle, - instructions: context.l10n.community_qrInstructions(name), - embeddedImage: Image.asset('assets/images/mesh-icon.png', width: 40, height: 40), + instructions: context.l10n + .community_qrInstructions(name), + embeddedImage: Image.asset( + 'assets/images/mesh-icon.png', + width: 40, + height: 40, + ), ); } }, @@ -1094,7 +1257,8 @@ class _ChannelsScreenState extends State optionIndex: 0, icon: Icons.add, title: dialogContext.l10n.channels_createPrivateChannel, - subtitle: dialogContext.l10n.channels_createPrivateChannelDesc, + subtitle: + dialogContext.l10n.channels_createPrivateChannelDesc, ), if (selectedOption == 0) buildExpandedContent()!, const Divider(height: 1), @@ -1102,7 +1266,8 @@ class _ChannelsScreenState extends State optionIndex: 1, icon: Icons.lock, title: dialogContext.l10n.channels_joinPrivateChannel, - subtitle: dialogContext.l10n.channels_joinPrivateChannelDesc, + subtitle: + dialogContext.l10n.channels_joinPrivateChannelDesc, ), if (selectedOption == 1) buildExpandedContent()!, if (!hasPublicChannel) ...[ @@ -1111,7 +1276,8 @@ class _ChannelsScreenState extends State optionIndex: 2, icon: Icons.public, title: dialogContext.l10n.channels_joinPublicChannel, - subtitle: dialogContext.l10n.channels_joinPublicChannelDesc, + subtitle: + dialogContext.l10n.channels_joinPublicChannelDesc, ), if (selectedOption == 2) buildExpandedContent()!, ], @@ -1120,7 +1286,8 @@ class _ChannelsScreenState extends State optionIndex: 3, icon: Icons.tag, title: dialogContext.l10n.channels_joinHashtagChannel, - subtitle: dialogContext.l10n.channels_joinHashtagChannelDesc, + subtitle: + dialogContext.l10n.channels_joinHashtagChannelDesc, ), if (selectedOption == 3) buildExpandedContent()!, const Divider(height: 1), @@ -1168,7 +1335,9 @@ class _ChannelsScreenState extends State context: context, builder: (dialogContext) => StatefulBuilder( builder: (dialogContext, setState) => AlertDialog( - title: Text(dialogContext.l10n.channels_editChannelTitle(channel.index)), + title: Text( + dialogContext.l10n.channels_editChannelTitle(channel.index), + ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -1226,7 +1395,9 @@ class _ChannelsScreenState extends State psk = Channel.parsePskHex(pskHex); } on FormatException { ScaffoldMessenger.of(dialogContext).showSnackBar( - SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)), + SnackBar( + content: Text(dialogContext.l10n.channels_pskMustBe32Hex), + ), ); return; } @@ -1235,7 +1406,9 @@ class _ChannelsScreenState extends State connector.setChannel(channel.index, name, psk); connector.setChannelSmazEnabled(channel.index, smazEnabled); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelUpdated(name))), + SnackBar( + content: Text(context.l10n.channels_channelUpdated(name)), + ), ); }, child: Text(dialogContext.l10n.common_save), @@ -1255,7 +1428,9 @@ class _ChannelsScreenState extends State context: context, builder: (dialogContext) => AlertDialog( title: Text(dialogContext.l10n.channels_deleteChannel), - content: Text(dialogContext.l10n.channels_deleteChannelConfirm(channel.name)), + content: Text( + dialogContext.l10n.channels_deleteChannelConfirm(channel.name), + ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), @@ -1266,10 +1441,17 @@ class _ChannelsScreenState extends State Navigator.pop(dialogContext); connector.deleteChannel(channel.index); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.channels_channelDeleted(channel.name))), + SnackBar( + content: Text( + context.l10n.channels_channelDeleted(channel.name), + ), + ), ); }, - child: Text(dialogContext.l10n.common_delete, style: const TextStyle(color: Colors.red)), + child: Text( + dialogContext.l10n.common_delete, + style: const TextStyle(color: Colors.red), + ), ), ], ), @@ -1323,16 +1505,26 @@ class _ChannelsScreenState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.groups_outlined, size: 64, color: Colors.grey[400]), + Icon( + Icons.groups_outlined, + size: 64, + color: Colors.grey[400], + ), const SizedBox(height: 16), Text( context.l10n.community_noCommunities, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), ), const SizedBox(height: 8), Text( context.l10n.community_scanOrCreate, - style: TextStyle(fontSize: 14, color: Colors.grey[500]), + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + ), textAlign: TextAlign.center, ), ], @@ -1345,8 +1537,13 @@ class _ChannelsScreenState extends State final community = _communities[index]; return ListTile( leading: CircleAvatar( - backgroundColor: Colors.purple.withValues(alpha: 0.2), - child: const Icon(Icons.groups, color: Colors.purple), + backgroundColor: Colors.purple.withValues( + alpha: 0.2, + ), + child: const Icon( + Icons.groups, + color: Colors.purple, + ), ), title: Text(community.name), subtitle: Text( @@ -1361,10 +1558,6 @@ class _ChannelsScreenState extends State Navigator.pop(sheetContext); if (value == 'share') { _showCommunityQrDialog(context, community); - } else if (value == 'regenerate') { - _regenerateCommunitySecret(context, community); - } else if (value == 'update') { - _updateCommunitySecret(context, community); } else if (value == 'leave') { _confirmLeaveCommunity(context, community); } @@ -1380,32 +1573,14 @@ class _ChannelsScreenState extends State ], ), ), - PopupMenuItem( - value: 'regenerate', - child: Row( - children: [ - const Icon(Icons.refresh), - const SizedBox(width: 12), - Text(context.l10n.community_regenerateSecret), - ], - ), - ), - PopupMenuItem( - value: 'update', - child: Row( - children: [ - const Icon(Icons.qr_code_scanner), - const SizedBox(width: 12), - Text(context.l10n.community_updateSecret), - ], - ), - ), - const PopupMenuDivider(), PopupMenuItem( value: 'leave', child: Row( children: [ - const Icon(Icons.exit_to_app, color: Colors.red), + const Icon( + Icons.exit_to_app, + color: Colors.red, + ), const SizedBox(width: 12), Text( context.l10n.community_delete, @@ -1436,128 +1611,23 @@ class _ChannelsScreenState extends State data: community.toQrJson(), title: context.l10n.community_qrTitle, instructions: context.l10n.community_qrInstructions(community.name), - embeddedImage: Image.asset('assets/images/mesh-icon.png', width: 40, height: 40), - ); - } - - /// Regenerate the community secret and update all associated channels - void _regenerateCommunitySecret(BuildContext context, Community community) { - showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: Text(dialogContext.l10n.community_regenerateSecret), - content: Text(dialogContext.l10n.community_regenerateSecretConfirm(community.name)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(dialogContext), - child: Text(dialogContext.l10n.common_cancel), - ), - FilledButton( - onPressed: () async { - Navigator.pop(dialogContext); - - final connector = context.read(); - final newCommunity = community.withRegeneratedSecret(); - - // Update channel PSKs - await _updateCommunityChannelPsks(connector, community, newCommunity); - - // Save updated community - await _communityStore.updateCommunity(newCommunity); - _loadCommunities(); - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.community_secretRegenerated(community.name))), - ); - - // Show the new QR code - _showCommunityQrDialog(context, newCommunity); - } - }, - child: Text(dialogContext.l10n.community_regenerate), - ), - ], + embeddedImage: Image.asset( + 'assets/images/mesh-icon.png', + width: 40, + height: 40, ), ); } - /// Update community secret from a scanned QR code - void _updateCommunitySecret(BuildContext context, Community community) async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => _CommunitySecretScannerScreen( - communityName: community.name, - ), - ), - ); - - if (result == null || !context.mounted) return; - - final newSecret = Community.extractSecretFromQrData(result); - if (newSecret == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.community_invalidQrCode)), - ); - return; - } - - final connector = context.read(); - final newCommunity = community.withNewSecret(newSecret); - - // Update channel PSKs - await _updateCommunityChannelPsks(connector, community, newCommunity); - - // Save updated community - await _communityStore.updateCommunity(newCommunity); - _loadCommunities(); - - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.community_secretUpdated(community.name))), - ); - } - } - - /// Update PSKs for all channels belonging to a community - Future _updateCommunityChannelPsks( - MeshCoreConnector connector, - Community oldCommunity, - Community newCommunity, - ) async { - // Find and update the public channel - final oldPublicPskHex = Channel.formatPskHex(oldCommunity.deriveCommunityPublicPsk()); - final newPublicPsk = newCommunity.deriveCommunityPublicPsk(); - - for (final channel in connector.channels) { - if (channel.pskHex == oldPublicPskHex) { - await connector.setChannel(channel.index, channel.name, newPublicPsk); - break; - } - } - - // Find and update hashtag channels - for (final hashtag in oldCommunity.hashtagChannels) { - final oldHashtagPskHex = Channel.formatPskHex(oldCommunity.deriveCommunityHashtagPsk(hashtag)); - final newHashtagPsk = newCommunity.deriveCommunityHashtagPsk(hashtag); - - for (final channel in connector.channels) { - if (channel.pskHex == oldHashtagPskHex) { - await connector.setChannel(channel.index, channel.name, newHashtagPsk); - break; - } - } - } - } - void _confirmLeaveCommunity(BuildContext context, Community community) { final connector = context.read(); - + // Find all channels that belong to this community List communityChannels = []; - final publicPskHex = Channel.formatPskHex(community.deriveCommunityPublicPsk()); - + final publicPskHex = Channel.formatPskHex( + community.deriveCommunityPublicPsk(), + ); + for (final channel in connector.channels) { // Check if it's the public channel if (channel.pskHex == publicPskHex) { @@ -1566,16 +1636,18 @@ class _ChannelsScreenState extends State } // Check if it's a hashtag channel for (final hashtag in community.hashtagChannels) { - final hashtagPskHex = Channel.formatPskHex(community.deriveCommunityHashtagPsk(hashtag)); + final hashtagPskHex = Channel.formatPskHex( + community.deriveCommunityHashtagPsk(hashtag), + ); if (channel.pskHex == hashtagPskHex) { communityChannels.add(channel); break; } } } - + final channelCount = communityChannels.length; - + showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -1593,19 +1665,23 @@ class _ChannelsScreenState extends State TextButton( onPressed: () async { Navigator.pop(dialogContext); - + // Delete all community channels from the device for (final channel in communityChannels) { await connector.deleteChannel(channel.index); } - + // Remove community from store await _communityStore.removeCommunity(community.id); _loadCommunities(); - + if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.community_deleted(community.name))), + SnackBar( + content: Text( + context.l10n.community_deleted(community.name), + ), + ), ); } }, @@ -1619,32 +1695,3 @@ class _ChannelsScreenState extends State ); } } - -/// Simple scanner screen for updating community secret -class _CommunitySecretScannerScreen extends StatelessWidget { - final String communityName; - - const _CommunitySecretScannerScreen({required this.communityName}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.community_updateSecret), - ), - body: QrScannerWidget( - onScanned: (data) { - Navigator.pop(context, data); - }, - validator: (data) => Community.isValidQrData(data), - onValidationFailed: (data) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.community_invalidQrCode)), - ); - }, - instructions: context.l10n.community_scanToUpdateSecret(communityName), - overlay: const ScannerCornerOverlay(), - ), - ); - } -} diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 5764bc8..a1707b9 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,11 +5,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:provider/provider.dart'; import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; @@ -988,11 +990,21 @@ class _MessageBubble extends StatelessWidget { fallbackTextColor: textColor.withValues(alpha: 0.7), ) else - Text( - messageText, + Linkify( + text: messageText, style: TextStyle( color: textColor, ), + linkStyle: const TextStyle( + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => LinkHandler.handleLinkTap(context, link.url), ), if (isOutgoing && message.retryCount > 0) ...[ const SizedBox(height: 4), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index fdb93ad..31428df 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,6 +12,7 @@ import package_info_plus import path_provider_foundation import shared_preferences_foundation import sqflite_darwin +import url_launcher_macos import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -22,5 +23,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index de12f54..2e2b746 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -262,6 +262,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.1" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_lints: dependency: "direct dev" description: @@ -397,6 +405,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" lints: dependency: transitive description: @@ -818,6 +834,70 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" uuid: dependency: "direct main" description: @@ -907,5 +987,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.2 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index 490c83d..c9e9120 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,8 @@ dependencies: package_info_plus: ^8.0.0 mobile_scanner: ^6.0.0 # QR/barcode scanning qr_flutter: ^4.1.0 # QR code generation + url_launcher: ^6.3.0 # Launch URLs in system browser + flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text dev_dependencies: flutter_test: diff --git a/untranslated.json b/untranslated.json index 2138a62..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,121 +1 @@ -{ - "bg": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "de": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "es": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "fr": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "it": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "nl": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "pl": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "pt": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "sk": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "sl": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "sv": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ], - - "zh": [ - "community_regenerateSecret", - "community_regenerateSecretConfirm", - "community_regenerate", - "community_secretRegenerated", - "community_updateSecret", - "community_secretUpdated", - "community_scanToUpdateSecret" - ] -} +{} \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c158b14..eeb548f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterBluePlusPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterBluePlusPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 905321a..68825d8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_blue_plus_winrt + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST From 537384ea5baf0fc153f9e5e683d6e86b22511741 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 20 Jan 2026 21:50:35 -0700 Subject: [PATCH 005/421] fix: add safety margin to text message overhead calculations --- lib/connector/meshcore_protocol.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 8469d61..f9241e8 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -195,8 +195,8 @@ const int maxFrameSize = 172; const int appProtocolVersion = 3; // Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE). const int maxTextPayloadBytes = 160; -const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1; -const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1; +const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin +const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin int maxContactMessageBytes() { final byFrame = maxFrameSize - _sendTextMsgOverheadBytes; From 20171c491f228ce31b0ef1a09bbe12bcc64d024f Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 20 Jan 2026 22:28:37 -0700 Subject: [PATCH 006/421] fix: update iOS platform version and enable sentence capitalization in chat input fields --- ios/Podfile | 2 +- lib/screens/channel_chat_screen.dart | 1 + lib/screens/chat_screen.dart | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/Podfile b/ios/Podfile index 69ed111..1cecf97 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,4 +1,4 @@ -platform :ios, '12.0' +platform :ios, '15.5' ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 84aeb0a..380c7ce 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -727,6 +727,7 @@ class _ChannelChatScreenState extends State { inputFormatters: [ Utf8LengthLimitingTextInputFormatter(maxBytes), ], + textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, border: OutlineInputBorder( diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index a1707b9..079f25d 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -281,6 +281,7 @@ class _ChatScreenState extends State { inputFormatters: [ Utf8LengthLimitingTextInputFormatter(maxBytes), ], + textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, border: const OutlineInputBorder(), From 297e609b3e3a12ba719e5cce3d66a273b93e6efd Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 20 Jan 2026 22:40:42 -0700 Subject: [PATCH 007/421] fix: replace RadioListTile with RadioGroup for better state management in community selection --- lib/screens/channels_screen.dart | 53 ++++++++++++++++---------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index a120a0d..30a99f0 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -900,41 +900,42 @@ class _ChannelsScreenState extends State children: [ // Only show type selection if user has communities if (_communities.isNotEmpty) ...[ - RadioListTile( - value: true, + RadioGroup( groupValue: isRegularHashtag, onChanged: (v) => setDialogState(() { - isRegularHashtag = v!; + if (v == null) return; + isRegularHashtag = v; if (isRegularHashtag) { selectedCommunity = null; - } - }), - title: Text( - dialogContext.l10n.community_regularHashtag, - ), - subtitle: Text( - dialogContext.l10n.community_regularHashtagDesc, - ), - dense: true, - ), - RadioListTile( - value: false, - groupValue: isRegularHashtag, - onChanged: (v) => setDialogState(() { - isRegularHashtag = v!; - if (!isRegularHashtag && - selectedCommunity == null && + } else if (selectedCommunity == null && _communities.isNotEmpty) { selectedCommunity = _communities.first; } }), - title: Text( - dialogContext.l10n.community_communityHashtag, + child: Column( + children: [ + RadioListTile( + value: true, + title: Text( + dialogContext.l10n.community_regularHashtag, + ), + subtitle: Text( + dialogContext.l10n.community_regularHashtagDesc, + ), + dense: true, + ), + RadioListTile( + value: false, + title: Text( + dialogContext.l10n.community_communityHashtag, + ), + subtitle: Text( + dialogContext.l10n.community_communityHashtagDesc, + ), + dense: true, + ), + ], ), - subtitle: Text( - dialogContext.l10n.community_communityHashtagDesc, - ), - dense: true, ), ], // Community dropdown (only if community hashtag selected) From dff037535dae6d581312f6904fab0a0f8bdad2a4 Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:13:24 +0100 Subject: [PATCH 008/421] More french translation updates --- lib/l10n/app_fr.arb | 40 +++++++++++++++--------------- lib/l10n/app_localizations_fr.dart | 40 +++++++++++++++--------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1b8d35d..f3015cc 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -279,7 +279,7 @@ } } }, - "contacts_newGroup": "Nouvelle Groupe", + "contacts_newGroup": "Nouveau Groupe", "contacts_groupName": "Nom du groupe", "contacts_groupNameRequired": "Le nom du groupe est obligatoire.", "contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.", @@ -293,8 +293,8 @@ "contacts_filterContacts": "Filtrer les contacts...", "contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.", "contacts_noMembers": "Aucun membre", - "contacts_lastSeenNow": "Dernière fois vu maintenant", - "contacts_lastSeenMinsAgo": "Dernière fois vu il y a {minutes} minutes.", + "contacts_lastSeenNow": "Vu maintenant", + "contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -302,8 +302,8 @@ } } }, - "contacts_lastSeenHourAgo": "Dernière fois vu il y a 1 heure.", - "contacts_lastSeenHoursAgo": "Dernière fois vu il y a {hours} heures.", + "contacts_lastSeenHourAgo": "Vu il y a 1 heure", + "contacts_lastSeenHoursAgo": "Vu il y a {hours} heures", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -311,8 +311,8 @@ } } }, - "contacts_lastSeenDayAgo": "Dernière fois vu il y a 1 jour", - "contacts_lastSeenDaysAgo": "Dernière activité il y a {days} jours", + "contacts_lastSeenDayAgo": "Vu il y a 1 jour", + "contacts_lastSeenDaysAgo": "Vu il y a {days} jours", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -394,7 +394,7 @@ "channels_sortBy": "Trier par", "channels_sortManual": "Manuel", "channels_sortAZ": "A à Z", - "channels_sortLatestMessages": "Dernières messages", + "channels_sortLatestMessages": "Derniers messages", "channels_sortUnread": "Non lu", "chat_noMessages": "Aucun message pour le moment.", "chat_sendMessageToStart": "Envoyer un message pour commencer", @@ -436,7 +436,7 @@ "chat_messageCopied": "Message copié", "chat_messageDeleted": "Message supprimé", "chat_retryingMessage": "Tentative de récupération.", - "chat_retryCount": "Réessayer {current}/{max}", + "chat_retryCount": "Essai {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -699,7 +699,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Tiles mis en cache ({downloaded}) ({failed} ratés)", + "mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -746,7 +746,7 @@ } } }, - "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", + "mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -763,7 +763,7 @@ } } }, - "time_justNow": "Il y a tout juste maintenant", + "time_justNow": "Maintenant", "time_minutesAgo": "{minutes} minutes auparavant", "@time_minutesAgo": { "placeholders": { @@ -911,7 +911,7 @@ "repeater_packetStatistics": "Statistiques des paquets", "repeater_sent": "Envoyé", "repeater_received": "Reçu", - "repeater_duplicates": "Dupliques", + "repeater_duplicates": "Doublons", "repeater_daysHoursMinsSecs": "{days} jours {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { @@ -1120,7 +1120,7 @@ "repeater_cliHelpSetAf": "Définit le facteur de temps d'air.", "repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).", "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.", - "repeater_cliHelpSetAllowReadOnly": "(Serveur de pièce) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", + "repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", "repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).", "repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.", "repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.", @@ -1339,16 +1339,16 @@ "channelPath_unknownRepeater": "Répéteur Inconnu", "listFilter_tooltip": "Filtrer et trier", "listFilter_sortBy": "Trier par", - "listFilter_latestMessages": "Dernières messages", + "listFilter_latestMessages": "Derniers messages", "listFilter_heardRecently": "Écoute récemment", "listFilter_az": "A à Z", "listFilter_filters": "Filtres", "listFilter_all": "Tout", "listFilter_users": "Utilisateurs", "listFilter_repeaters": "Répéteurs", - "listFilter_roomServers": "Serveurs de pièce", + "listFilter_roomServers": "Rooms servers", "listFilter_unreadOnly": "Messages non lus seulement", - "listFilter_newGroup": "Nouvelle groupe", + "listFilter_newGroup": "Nouveau groupe", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1374,7 +1374,7 @@ "channels_scanQrCode": "Scanner un code QR", "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", - "channels_hashtagHint": "ex. #équipe", + "channels_hashtagHint": "ex. #equipe", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1395,7 +1395,7 @@ "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", "settings_locationIntervalSec": "Intervalo pour GPS (Segundos)", "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.", - "contacts_manageRoom": "Gestionar Servidor de Habitación", + "contacts_manageRoom": "Gérer le Room Server", "room_management": "Administración del Servidor de Habitación", "@community_joinConfirmation": { "placeholders": { @@ -1492,7 +1492,7 @@ }, "community_deleted": "Communauté \"{name}\" quittée", "community_addHashtagChannel": "Ajouter un Hashtag Communauté", - "community_addHashtagChannelDesc": "Ajouter un canal hachage pour cette communauté", + "community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté", "community_selectCommunity": "Sélectionner Communauté", "community_regularHashtag": "Hashtag régulier", "community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)", diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 48a6ac4..105997d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -667,7 +667,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_manageRepeater => 'Gérer le répétiteur'; @override - String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; + String get contacts_manageRoom => 'Gérer le Room Server'; @override String get contacts_roomLogin => 'Connexion Salle'; @@ -687,7 +687,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get contacts_newGroup => 'Nouvelle Groupe'; + String get contacts_newGroup => 'Nouveau Groupe'; @override String get contacts_groupName => 'Nom du groupe'; @@ -711,27 +711,27 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_noMembers => 'Aucun membre'; @override - String get contacts_lastSeenNow => 'Dernière fois vu maintenant'; + String get contacts_lastSeenNow => 'Vu maintenant'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Dernière fois vu il y a $minutes minutes.'; + return 'Vu il y a $minutes minutes'; } @override - String get contacts_lastSeenHourAgo => 'Dernière fois vu il y a 1 heure.'; + String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Dernière fois vu il y a $hours heures.'; + return 'Vu il y a $hours heures'; } @override - String get contacts_lastSeenDayAgo => 'Dernière fois vu il y a 1 jour'; + String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Dernière activité il y a $days jours'; + return 'Vu il y a $days jours'; } @override @@ -845,7 +845,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_sortAZ => 'A à Z'; @override - String get channels_sortLatestMessages => 'Dernières messages'; + String get channels_sortLatestMessages => 'Derniers messages'; @override String get channels_sortUnread => 'Non lu'; @@ -888,7 +888,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_enterHashtag => 'Entrez le hashtag'; @override - String get channels_hashtagHint => 'ex. #équipe'; + String get channels_hashtagHint => 'ex. #equipe'; @override String get chat_noMessages => 'Aucun message pour le moment.'; @@ -936,7 +936,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String chat_retryCount(int current, int max) { - return 'Réessayer $current/$max'; + return 'Essai $current/$max'; } @override @@ -1389,7 +1389,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Tiles mis en cache ($downloaded) ($failed ratés)'; + return 'Tuiles mis en cache ($downloaded) ($failed ratés)'; } @override @@ -1443,11 +1443,11 @@ class AppLocalizationsFr extends AppLocalizations { String east, String west, ) { - return 'N $north, S $south, E $east, W $west'; + return 'N $north, S $south, E $east, O $west'; } @override - String get time_justNow => 'Il y a tout juste maintenant'; + String get time_justNow => 'Maintenant'; @override String time_minutesAgo(int minutes) { @@ -1743,7 +1743,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_received => 'Reçu'; @override - String get repeater_duplicates => 'Dupliques'; + String get repeater_duplicates => 'Doublons'; @override String repeater_daysHoursMinsSecs( @@ -2088,7 +2088,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpSetAllowReadOnly => - '(Serveur de pièce) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)'; + '(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)'; @override String get repeater_cliHelpSetFloodMax => @@ -2632,7 +2632,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get community_addHashtagChannelDesc => - 'Ajouter un canal hachage pour cette communauté'; + 'Ajouter un canal hashtag pour cette communauté'; @override String get community_selectCommunity => 'Sélectionner Communauté'; @@ -2663,7 +2663,7 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_sortBy => 'Trier par'; @override - String get listFilter_latestMessages => 'Dernières messages'; + String get listFilter_latestMessages => 'Derniers messages'; @override String get listFilter_heardRecently => 'Écoute récemment'; @@ -2684,11 +2684,11 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_repeaters => 'Répéteurs'; @override - String get listFilter_roomServers => 'Serveurs de pièce'; + String get listFilter_roomServers => 'Rooms servers'; @override String get listFilter_unreadOnly => 'Messages non lus seulement'; @override - String get listFilter_newGroup => 'Nouvelle groupe'; + String get listFilter_newGroup => 'Nouveau groupe'; } From 2a2275ec3115e00044895be1bf6ff6ed9b8c7129 Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:16:58 +0100 Subject: [PATCH 009/421] More french translation updates2 --- lib/l10n/app_fr.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f3015cc..e50fb82 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1391,8 +1391,8 @@ }, "neighbors_unknownContact": "Clé publique inconnue {pubkey}", "neighbors_heardAgo": "Écouté : {time} auparavant", - "settings_locationGPSEnable": "Habilita GPS", - "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", + "settings_locationGPSEnable": "Activer le GPS", + "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS", "settings_locationIntervalSec": "Intervalo pour GPS (Segundos)", "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.", "contacts_manageRoom": "Gérer le Room Server", From 72216e2cf7fa772dbc515adad3c61dd37b78d911 Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:21:09 +0100 Subject: [PATCH 010/421] More french translation updates3 --- lib/l10n/app_localizations_fr.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 105997d..74587b2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -204,11 +204,11 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_locationInvalid => 'Latitude ou longitude invalide.'; @override - String get settings_locationGPSEnable => 'Habilita GPS'; + String get settings_locationGPSEnable => 'Activer le GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Habilita la actualización automática de la ubicación mediante GPS.'; + 'Activer la mise à jour automatique de la position via GPS'; @override String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)'; From d6794bc8d76e7ba43569a75d77c19bcdf1308c15 Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Thu, 22 Jan 2026 08:45:54 +0100 Subject: [PATCH 011/421] More french translation updates4 --- lib/l10n/app_fr.arb | 4 ++-- lib/l10n/app_localizations_fr.dart | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e50fb82..8abb536 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1393,8 +1393,8 @@ "neighbors_heardAgo": "Écouté : {time} auparavant", "settings_locationGPSEnable": "Activer le GPS", "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS", - "settings_locationIntervalSec": "Intervalo pour GPS (Segundos)", - "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.", + "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", + "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", "contacts_manageRoom": "Gérer le Room Server", "room_management": "Administración del Servidor de Habitación", "@community_joinConfirmation": { diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 74587b2..918cee6 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -211,11 +211,12 @@ class AppLocalizationsFr extends AppLocalizations { 'Activer la mise à jour automatique de la position via GPS'; @override - String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)'; + String get settings_locationIntervalSec => + 'Intervalle de mise-à-jour du GPS (Secondes)'; @override String get settings_locationIntervalInvalid => - 'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.'; + 'L\'intervalle doit être compris entre 60 et 86400 secondes.'; @override String get settings_latitude => 'Latitude'; From e2b9b58d7d2085eb72c3509946df113d1512bc43 Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:25:42 +0100 Subject: [PATCH 012/421] More french translation updates5 --- lib/l10n/app_fr.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 8abb536..43cf4ec 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -104,7 +104,7 @@ "settings_timeSynchronized": "Synchronisation temporelle", "settings_refreshContacts": "Rafraîchir les Contacts", "settings_refreshContactsSubtitle": "Recharger la liste des contacts depuis l'appareil", - "settings_rebootDevice": "Réinitialiser l'appareil", + "settings_rebootDevice": "Redémarrer l'appareil", "settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore", "settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.", "settings_debug": "Déboguer", @@ -1024,7 +1024,7 @@ } }, "repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées", - "repeater_dangerZone": "Zone d'alerte", + "repeater_dangerZone": "Zone dangereuse", "repeater_rebootRepeater": "Redémarrer Répéteur", "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur", "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?", From c43df67fac91fe635f657d93dd05b39d8b9d0fe5 Mon Sep 17 00:00:00 2001 From: megadimich <127159274+megadimich@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:08:42 +0000 Subject: [PATCH 013/421] Ukrainian localization files --- lib/l10n/app_localizations_uk.dart | 2686 ++++++++++++++++++++++++++++ lib/l10n/app_uk.arb | 1538 ++++++++++++++++ 2 files changed, 4224 insertions(+) create mode 100644 lib/l10n/app_localizations_uk.dart create mode 100644 lib/l10n/app_uk.arb diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart new file mode 100644 index 0000000..a58d554 --- /dev/null +++ b/lib/l10n/app_localizations_uk.dart @@ -0,0 +1,2686 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Ukrainian (`uk`). +class AppLocalizationsUk extends AppLocalizations { + AppLocalizationsUk([String locale = 'uk']) : super(locale); + + @override + String get appTitle => 'MeshCore Open'; + + @override + String get nav_contacts => 'Контакти'; + + @override + String get nav_channels => 'Канали'; + + @override + String get nav_map => 'Карта'; + + @override + String get common_cancel => 'Скасувати'; + + @override + String get common_ok => 'ОК'; + + @override + String get common_connect => 'Підключити'; + + @override + String get common_unknownDevice => 'Невідомий пристрій'; + + @override + String get common_save => 'Зберегти'; + + @override + String get common_delete => 'Видалити'; + + @override + String get common_close => 'Закрити'; + + @override + String get common_edit => 'Редагувати'; + + @override + String get common_add => 'Додати'; + + @override + String get common_settings => 'Налаштування'; + + @override + String get common_disconnect => 'Відключити'; + + @override + String get common_connected => 'Підключено'; + + @override + String get common_disconnected => 'Відключено'; + + @override + String get common_create => 'Створити'; + + @override + String get common_continue => 'Продовжити'; + + @override + String get common_share => 'Поділитися'; + + @override + String get common_copy => 'Копіювати'; + + @override + String get common_retry => 'Повторити'; + + @override + String get common_hide => 'Приховати'; + + @override + String get common_remove => 'Прибрати'; + + @override + String get common_enable => 'Увімкнути'; + + @override + String get common_disable => 'Вимкнути'; + + @override + String get common_reboot => 'Перезавантажити'; + + @override + String get common_loading => 'Завантаження...'; + + @override + String get common_notAvailable => '—'; + + @override + String common_voltageValue(String volts) { + return '$volts В'; + } + + @override + String common_percentValue(int percent) { + return '$percent%'; + } + + @override + String get scanner_title => 'MeshCore Open'; + + @override + String get scanner_scanning => 'Пошук пристроїв...'; + + @override + String get scanner_connecting => 'Підключення...'; + + @override + String get scanner_disconnecting => 'Відключення...'; + + @override + String get scanner_notConnected => 'Не підключено'; + + @override + String scanner_connectedTo(String deviceName) { + return 'Підключено до $deviceName'; + } + + @override + String get scanner_searchingDevices => 'Пошук пристроїв MeshCore...'; + + @override + String get scanner_tapToScan => + 'Натисніть «Сканувати», щоб знайти пристрої MeshCore'; + + @override + String scanner_connectionFailed(String error) { + return 'Помилка підключення: $error'; + } + + @override + String get scanner_stop => 'Стоп'; + + @override + String get scanner_scan => 'Сканувати'; + + @override + String get device_quickSwitch => 'Швидке перемикання'; + + @override + String get device_meshcore => 'MeshCore'; + + @override + String get settings_title => 'Налаштування'; + + @override + String get settings_deviceInfo => 'Інформація про пристрій'; + + @override + String get settings_appSettings => 'Налаштування програми'; + + @override + String get settings_appSettingsSubtitle => + 'Сповіщення, повідомлення та налаштування карти'; + + @override + String get settings_nodeSettings => 'Налаштування вузла'; + + @override + String get settings_nodeName => 'Ім\'я вузла'; + + @override + String get settings_nodeNameNotSet => 'Не встановлено'; + + @override + String get settings_nodeNameHint => 'Введіть ім\'я вузла'; + + @override + String get settings_nodeNameUpdated => 'Ім\'я оновлено'; + + @override + String get settings_radioSettings => 'Налаштування радіо'; + + @override + String get settings_radioSettingsSubtitle => + 'Частота, потужність, коефіцієнт розширення'; + + @override + String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено'; + + @override + String get settings_location => 'Розташування'; + + @override + String get settings_locationSubtitle => 'GPS координати'; + + @override + String get settings_locationUpdated => 'Розташування оновлено'; + + @override + String get settings_locationBothRequired => 'Введіть широту та довготу.'; + + @override + String get settings_locationInvalid => 'Некоректна широта або довгота.'; + + @override + String get settings_locationGPSEnable => 'Увімкнути GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Вмикає автоматичне оновлення місцезнаходження через GPS.'; + + @override + String get settings_locationIntervalSec => 'Інтервал для GPS (Секунди)'; + + @override + String get settings_locationIntervalInvalid => + 'Інтервал має бути не менше 60 секунд і менше 86400 секунд.'; + + @override + String get settings_latitude => 'Широта'; + + @override + String get settings_longitude => 'Довгота'; + + @override + String get settings_privacyMode => 'Режим приватності'; + + @override + String get settings_privacyModeSubtitle => + 'Приховати ім\'я/розташування в оголошеннях'; + + @override + String get settings_privacyModeToggle => + 'Увімкніть режим приватності, щоб приховати своє ім\'я та місцезнаходження в оголошеннях.'; + + @override + String get settings_privacyModeEnabled => 'Режим приватності увімкнено'; + + @override + String get settings_privacyModeDisabled => 'Режим приватності вимкнено'; + + @override + String get settings_actions => 'Дії'; + + @override + String get settings_sendAdvertisement => 'Оголосити себе'; + + @override + String get settings_sendAdvertisementSubtitle => + 'Транслювати присутність зараз'; + + @override + String get settings_advertisementSent => 'Оголошення надіслано'; + + @override + String get settings_syncTime => 'Синхронізація часу'; + + @override + String get settings_syncTimeSubtitle => + 'Встановити час пристрою відповідно до часу телефону.'; + + @override + String get settings_timeSynchronized => 'Час синхронізовано'; + + @override + String get settings_refreshContacts => 'Оновити контакти'; + + @override + String get settings_refreshContactsSubtitle => + 'Перезавантажити список контактів з пристрою'; + + @override + String get settings_rebootDevice => 'Перезавантажити пристрій'; + + @override + String get settings_rebootDeviceSubtitle => 'Перезавантажити пристрій MeshCore'; + + @override + String get settings_rebootDeviceConfirm => + 'Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.'; + + @override + String get settings_debug => 'Налагодження'; + + @override + String get settings_bleDebugLog => 'Журнал налагодження BLE'; + + @override + String get settings_bleDebugLogSubtitle => + 'Команди BLE, відповіді та необроблені дані'; + + @override + String get settings_appDebugLog => 'Журнал налагодження програми'; + + @override + String get settings_appDebugLogSubtitle => + 'Повідомлення налагодження програми'; + + @override + String get settings_about => 'Про програму'; + + @override + String settings_aboutVersion(String version) { + return 'MeshCore Open v$version'; + } + + @override + String get settings_aboutLegalese => 'Проєкт MeshCore Open Source 2026'; + + @override + String get settings_aboutDescription => + 'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.'; + + @override + String get settings_infoName => 'Ім\'я'; + + @override + String get settings_infoId => 'ID'; + + @override + String get settings_infoStatus => 'Статус'; + + @override + String get settings_infoBattery => 'Батарея'; + + @override + String get settings_infoPublicKey => 'Відкритий ключ'; + + @override + String get settings_infoContactsCount => 'Кількість контактів'; + + @override + String get settings_infoChannelCount => 'Кількість каналів'; + + @override + String get settings_presets => 'Попередні налаштування'; + + @override + String get settings_preset915Mhz => '915 МГц'; + + @override + String get settings_preset868Mhz => '868 МГц'; + + @override + String get settings_preset433Mhz => '433 МГц'; + + @override + String get settings_frequency => 'Частота (МГц)'; + + @override + String get settings_frequencyHelper => '300.0 - 2500.0'; + + @override + String get settings_frequencyInvalid => 'Некоректна частота (300-2500 МГц)'; + + @override + String get settings_bandwidth => 'Смуга пропускання'; + + @override + String get settings_spreadingFactor => 'Коефіцієнт розширення'; + + @override + String get settings_codingRate => 'Швидкість кодування'; + + @override + String get settings_txPower => 'Потужність TX (дБм)'; + + @override + String get settings_txPowerHelper => '0 - 22'; + + @override + String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)'; + + @override + String get settings_longRange => 'Дальній діапазон'; + + @override + String get settings_fastSpeed => 'Висока швидкість'; + + @override + String settings_error(String message) { + return 'Помилка: $message'; + } + + @override + String get appSettings_title => 'Налаштування програми'; + + @override + String get appSettings_appearance => 'Вигляд'; + + @override + String get appSettings_theme => 'Тема'; + + @override + String get appSettings_themeSystem => 'Системна'; + + @override + String get appSettings_themeLight => 'Світла'; + + @override + String get appSettings_themeDark => 'Темна'; + + @override + String get appSettings_language => 'Мова'; + + @override + String get appSettings_languageSystem => 'Як у системі'; + + @override + String get appSettings_languageEn => 'English'; + + @override + String get appSettings_languageFr => 'Français'; + + @override + String get appSettings_languageEs => 'Español'; + + @override + String get appSettings_languageDe => 'Deutsch'; + + @override + String get appSettings_languagePl => 'Polski'; + + @override + String get appSettings_languageSl => 'Slovenščina'; + + @override + String get appSettings_languagePt => 'Português'; + + @override + String get appSettings_languageIt => 'Italiano'; + + @override + String get appSettings_languageZh => '中文'; + + @override + String get appSettings_languageSv => 'Svenska'; + + @override + String get appSettings_languageNl => 'Nederlands'; + + @override + String get appSettings_languageSk => 'Slovenčina'; + + @override + String get appSettings_languageBg => 'Български'; + + @override + String get appSettings_languageUk => 'Українська'; + + @override + String get appSettings_notifications => 'Сповіщення'; + + @override + String get appSettings_enableNotifications => 'Увімкнути сповіщення'; + + @override + String get appSettings_enableNotificationsSubtitle => + 'Отримувати сповіщення про повідомлення та оголошення'; + + @override + String get appSettings_notificationPermissionDenied => + 'У доступі до сповіщень відмовлено'; + + @override + String get appSettings_notificationsEnabled => 'Сповіщення увімкнено'; + + @override + String get appSettings_notificationsDisabled => 'Сповіщення вимкнено'; + + @override + String get appSettings_messageNotifications => 'Сповіщення про повідомлення'; + + @override + String get appSettings_messageNotificationsSubtitle => + 'Показувати сповіщення при отриманні нових повідомлень'; + + @override + String get appSettings_channelMessageNotifications => 'Сповіщення каналів'; + + @override + String get appSettings_channelMessageNotificationsSubtitle => + 'Показувати сповіщення при отриманні повідомлень каналу'; + + @override + String get appSettings_advertisementNotifications => + 'Сповіщення про оголошення'; + + @override + String get appSettings_advertisementNotificationsSubtitle => + 'Показувати сповіщення при виявленні нових вузлів'; + + @override + String get appSettings_messaging => 'Обмін повідомленнями'; + + @override + String get appSettings_clearPathOnMaxRetry => + 'Очищати шлях після макс. спроб'; + + @override + String get appSettings_clearPathOnMaxRetrySubtitle => + 'Скидати шлях до контакту після 5 невдалих спроб надсилання'; + + @override + String get appSettings_pathsWillBeCleared => + 'Шляхи будуть очищені після 5 невдалих спроб.'; + + @override + String get appSettings_pathsWillNotBeCleared => + 'Шляхи не будуть очищатися автоматично.'; + + @override + String get appSettings_autoRouteRotation => 'Авторотація маршруту'; + + @override + String get appSettings_autoRouteRotationSubtitle => + 'Чергувати найкращі шляхи та режим «на всю мережу» (flood)'; + + @override + String get appSettings_autoRouteRotationEnabled => + 'Авторотація маршрутизації увімкнена'; + + @override + String get appSettings_autoRouteRotationDisabled => + 'Авторотація маршрутизації вимкнена'; + + @override + String get appSettings_battery => 'Батарея'; + + @override + String get appSettings_batteryChemistry => 'Хімія батареї'; + + @override + String appSettings_batteryChemistryPerDevice(String deviceName) { + return 'Встановити для пристрою ($deviceName)'; + } + + @override + String get appSettings_batteryChemistryConnectFirst => + 'Підключіть пристрій, щоб вибрати'; + + @override + String get appSettings_batteryNmc => '18650 NMC (3.0-4.2В)'; + + @override + String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65В)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)'; + + @override + String get appSettings_mapDisplay => 'Відображення карти'; + + @override + String get appSettings_showRepeaters => 'Показувати ретранслятори'; + + @override + String get appSettings_showRepeatersSubtitle => + 'Відображати вузли-ретранслятори на карті'; + + @override + String get appSettings_showChatNodes => 'Показувати вузли чату'; + + @override + String get appSettings_showChatNodesSubtitle => + 'Відображати вузли чату на карті'; + + @override + String get appSettings_showOtherNodes => 'Показувати інші вузли'; + + @override + String get appSettings_showOtherNodesSubtitle => + 'Відображати інші типи вузлів на карті'; + + @override + String get appSettings_timeFilter => 'Фільтр часу'; + + @override + String get appSettings_timeFilterShowAll => 'Показати всі вузли'; + + @override + String appSettings_timeFilterShowLast(int hours) { + return 'Показати вузли за останні $hours год'; + } + + @override + String get appSettings_mapTimeFilter => 'Фільтр часу карти'; + + @override + String get appSettings_showNodesDiscoveredWithin => + 'Показувати вузли, виявлені за:'; + + @override + String get appSettings_allTime => 'Весь час'; + + @override + String get appSettings_lastHour => 'Останню годину'; + + @override + String get appSettings_last6Hours => 'Останні 6 годин'; + + @override + String get appSettings_last24Hours => 'Останні 24 години'; + + @override + String get appSettings_lastWeek => 'Минулий тиждень'; + + @override + String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; + + @override + String get appSettings_noAreaSelected => 'Область не вибрано'; + + @override + String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { + return 'Вибрана область (зум $minZoom-$maxZoom)'; + } + + @override + String get appSettings_debugCard => 'Налагодження'; + + @override + String get appSettings_appDebugLogging => 'Логування налагодження програми'; + + @override + String get appSettings_appDebugLoggingSubtitle => + 'Записувати повідомлення налагодження програми в лог для усунення несправностей.'; + + @override + String get appSettings_appDebugLoggingEnabled => + 'Логування налагодження програми увімкнено'; + + @override + String get appSettings_appDebugLoggingDisabled => + 'Налагодження програми вимкнено.'; + + @override + String get contacts_title => 'Контакти'; + + @override + String get contacts_noContacts => 'Контактів не знайдено.'; + + @override + String get contacts_contactsWillAppear => + 'Контакти з\'являться, коли пристрої надішлють оголошення.'; + + @override + String get contacts_searchContacts => 'Пошук контактів...'; + + @override + String get contacts_noUnreadContacts => 'Немає непрочитаних контактів'; + + @override + String get contacts_noContactsFound => 'Контактів або груп не знайдено.'; + + @override + String get contacts_deleteContact => 'Видалити контакт'; + + @override + String contacts_removeConfirm(String contactName) { + return 'Видалити $contactName з контактів?'; + } + + @override + String get contacts_manageRepeater => 'Керувати ретранслятором'; + + @override + String get contacts_manageRoom => 'Керувати сервером кімнати'; + + @override + String get contacts_roomLogin => 'Вхід у кімнату'; + + @override + String get contacts_openChat => 'Відкрити чат'; + + @override + String get contacts_editGroup => 'Редагувати групу'; + + @override + String get contacts_deleteGroup => 'Видалити групу'; + + @override + String contacts_deleteGroupConfirm(String groupName) { + return 'Видалити $groupName?'; + } + + @override + String get contacts_newGroup => 'Нова група'; + + @override + String get contacts_groupName => 'Назва групи'; + + @override + String get contacts_groupNameRequired => 'Назва групи обов\'язкова.'; + + @override + String contacts_groupAlreadyExists(String name) { + return 'Група \"$name\" вже існує.'; + } + + @override + String get contacts_filterContacts => 'Фільтрувати контакти...'; + + @override + String get contacts_noContactsMatchFilter => + 'Жоден контакт не відповідає фільтру.'; + + @override + String get contacts_noMembers => 'Немає учасників'; + + @override + String get contacts_lastSeenNow => 'В мережі'; + + @override + String contacts_lastSeenMinsAgo(int minutes) { + return 'В мережі $minutes хв. тому'; + } + + @override + String get contacts_lastSeenHourAgo => 'В мережі 1 годину тому'; + + @override + String contacts_lastSeenHoursAgo(int hours) { + return 'В мережі $hours год. тому'; + } + + @override + String get contacts_lastSeenDayAgo => 'В мережі 1 день тому'; + + @override + String contacts_lastSeenDaysAgo(int days) { + return 'В мережі $days дн. тому'; + } + + @override + String get channels_title => 'Канали'; + + @override + String get channels_noChannelsConfigured => 'Канали не налаштовані'; + + @override + String get channels_addPublicChannel => 'Додати публічний канал'; + + @override + String get channels_searchChannels => 'Пошук каналів...'; + + @override + String get channels_noChannelsFound => 'Каналів не знайдено'; + + @override + String channels_channelIndex(int index) { + return 'Канал $index'; + } + + @override + String get channels_hashtagChannel => 'Канал з хештегом'; + + @override + String get channels_public => 'Публічний'; + + @override + String get channels_private => 'Приватний'; + + @override + String get channels_publicChannel => 'Публічний канал'; + + @override + String get channels_privateChannel => 'Приватний канал'; + + @override + String get channels_editChannel => 'Редагувати канал'; + + @override + String get channels_deleteChannel => 'Видалити канал'; + + @override + String channels_deleteChannelConfirm(String name) { + return 'Видалити $name? Це не можна скасувати.'; + } + + @override + String channels_channelDeleted(String name) { + return 'Канал \"$name\" видалено'; + } + + @override + String get channels_addChannel => 'Додати канал'; + + @override + String get channels_channelIndexLabel => 'Індекс каналу'; + + @override + String get channels_channelName => 'Назва каналу'; + + @override + String get channels_usePublicChannel => 'Використовувати публічний канал'; + + @override + String get channels_standardPublicPsk => 'Стандартний публічний PSK'; + + @override + String get channels_pskHex => 'PSK (Hex)'; + + @override + String get channels_generateRandomPsk => 'Згенерувати випадковий ключ PSK'; + + @override + String get channels_enterChannelName => 'Будь ласка, введіть назву каналу'; + + @override + String get channels_pskMustBe32Hex => + 'PSK має складатися з 32 шістнадцяткових символів.'; + + @override + String channels_channelAdded(String name) { + return 'Канал \"$name\" додано'; + } + + @override + String channels_editChannelTitle(int index) { + return 'Редагувати канал $index'; + } + + @override + String get channels_smazCompression => 'Стиснення SMAZ'; + + @override + String channels_channelUpdated(String name) { + return 'Канал \"$name\" оновлено'; + } + + @override + String get channels_publicChannelAdded => 'Публічний канал додано'; + + @override + String get channels_sortBy => 'Сортувати за'; + + @override + String get channels_sortManual => 'Вручну'; + + @override + String get channels_sortAZ => 'А-Я'; + + @override + String get channels_sortLatestMessages => 'Останні повідомлення'; + + @override + String get channels_sortUnread => 'Непрочитані'; + + @override + String get channels_createPrivateChannel => 'Створити приватний канал'; + + @override + String get channels_createPrivateChannelDesc => 'Захищено секретним ключем.'; + + @override + String get channels_joinPrivateChannel => 'Приєднатися до приватного каналу'; + + @override + String get channels_joinPrivateChannelDesc => 'Ввести секретний ключ вручну.'; + + @override + String get channels_joinPublicChannel => 'Приєднатися до публічного каналу'; + + @override + String get channels_joinPublicChannelDesc => + 'Будь-хто може приєднатися до цього каналу.'; + + @override + String get channels_joinHashtagChannel => 'Приєднатися до каналу з хештегом'; + + @override + String get channels_joinHashtagChannelDesc => + 'Будь-хто може приєднатися до каналів #hashtag.'; + + @override + String get channels_scanQrCode => 'Сканувати QR-код'; + + @override + String get channels_scanQrCodeComingSoon => 'Скоро буде'; + + @override + String get channels_enterHashtag => 'Введіть хештег'; + + @override + String get channels_hashtagHint => 'напр. #команда'; + + @override + String get chat_noMessages => 'Поки немає повідомлень.'; + + @override + String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати'; + + @override + String get chat_originalMessageNotFound => + 'Оригінальне повідомлення не знайдено'; + + @override + String chat_replyingTo(String name) { + return 'Відповідь $name'; + } + + @override + String chat_replyTo(String name) { + return 'Відповісти $name'; + } + + @override + String get chat_location => 'Розташування'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Надіслати повідомлення $contactName'; + } + + @override + String get chat_typeMessage => 'Введіть повідомлення...'; + + @override + String chat_messageTooLong(int maxBytes) { + return 'Повідомлення занадто довге (макс. $maxBytes байт).'; + } + + @override + String get chat_messageCopied => 'Повідомлення скопійовано'; + + @override + String get chat_messageDeleted => 'Повідомлення видалено'; + + @override + String get chat_retryingMessage => 'Спроба відновлення.'; + + @override + String chat_retryCount(int current, int max) { + return 'Повторна спроба $current/$max'; + } + + @override + String get chat_sendGif => 'Надіслати GIF'; + + @override + String get chat_reply => 'Відповісти'; + + @override + String get chat_addReaction => 'Додати реакцію'; + + @override + String get chat_me => 'Я'; + + @override + String get emojiCategorySmileys => 'Емодзі'; + + @override + String get emojiCategoryGestures => 'Жести'; + + @override + String get emojiCategoryHearts => 'Серця'; + + @override + String get emojiCategoryObjects => 'Об\'єкти'; + + @override + String get gifPicker_title => 'Вибрати GIF'; + + @override + String get gifPicker_searchHint => 'Пошук GIF...'; + + @override + String get gifPicker_poweredBy => 'На базі GIPHY'; + + @override + String get gifPicker_noGifsFound => 'GIF не знайдено'; + + @override + String get gifPicker_failedLoad => 'Не вдалося завантажити GIF-файли'; + + @override + String get gifPicker_failedSearch => 'Пошук GIF не вдався'; + + @override + String get gifPicker_noInternet => 'Немає інтернет-з\'єднання'; + + @override + String get debugLog_appTitle => 'Журнал налагодження програми'; + + @override + String get debugLog_bleTitle => 'Журнал налагодження BLE'; + + @override + String get debugLog_copyLog => 'Копіювати журнал'; + + @override + String get debugLog_clearLog => 'Очистити журнал'; + + @override + String get debugLog_copied => 'Журнал налагодження скопійовано'; + + @override + String get debugLog_bleCopied => 'Журнал BLE скопійовано'; + + @override + String get debugLog_noEntries => 'Поки що немає записів журналу налагодження.'; + + @override + String get debugLog_enableInSettings => + 'Увімкніть налагодження програми в налаштуваннях'; + + @override + String get debugLog_frames => 'Кадри'; + + @override + String get debugLog_rawLogRx => 'Необроблений лог - RX'; + + @override + String get debugLog_noBleActivity => 'Поки що немає активності BLE.'; + + @override + String debugFrame_length(int count) { + return 'Довжина кадру: $count байт'; + } + + @override + String debugFrame_command(String value) { + return 'Команда: 0x$value'; + } + + @override + String get debugFrame_textMessageHeader => 'Повідомлення:'; + + @override + String debugFrame_destinationPubKey(String pubKey) { + return '- PubKey призначення: $pubKey'; + } + + @override + String debugFrame_timestamp(int timestamp) { + return '- Мітка часу: $timestamp'; + } + + @override + String debugFrame_flags(String value) { + return '- Прапорці: 0x$value'; + } + + @override + String debugFrame_textType(int type, String label) { + return '- Тип тексту: $type ($label)'; + } + + @override + String get debugFrame_textTypeCli => 'CLI'; + + @override + String get debugFrame_textTypePlain => 'Звичайний'; + + @override + String debugFrame_text(String text) { + return '- Текст: \"$text\"'; + } + + @override + String get debugFrame_hexDump => 'Дамп Hex:'; + + @override + String get chat_pathManagement => 'Керування шляхами'; + + @override + String get chat_routingMode => 'Режим маршрутизації'; + + @override + String get chat_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; + + @override + String get chat_forceFloodMode => 'Примусово на всю мережу'; + + @override + String get chat_recentAckPaths => + 'Недавні шляхи ACK (натисніть, щоб використати):'; + + @override + String get chat_pathHistoryFull => + 'Історія шляхів заповнена. Видаліть записи, щоб додати нові.'; + + @override + String get chat_hopSingular => 'Стрибок'; + + @override + String get chat_hopPlural => 'стрибків'; + + @override + String chat_hopsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'стрибків', + many: 'стрибків', + few: 'стрибки', + one: 'стрибок', + ); + return '$count $_temp0'; + } + + @override + String get chat_successes => 'Успішно'; + + @override + String get chat_removePath => 'Видалити шлях'; + + @override + String get chat_noPathHistoryYet => + 'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.'; + + @override + String get chat_pathActions => 'Дії зі шляхом:'; + + @override + String get chat_setCustomPath => 'Встановити власний шлях'; + + @override + String get chat_setCustomPathSubtitle => 'Вказати шлях маршрутизації вручну'; + + @override + String get chat_clearPath => 'Очистити шлях'; + + @override + String get chat_clearPathSubtitle => + 'Примусово повторити пошук при наступному надсиланні'; + + @override + String get chat_pathCleared => + 'Шлях очищено. Наступне повідомлення оновить маршрут.'; + + @override + String get chat_floodModeSubtitle => + 'Використовувати перемикач маршрутизації в панелі програми'; + + @override + String get chat_floodModeEnabled => + 'Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; + + @override + String get chat_fullPath => 'Повний шлях'; + + @override + String get chat_pathDetailsNotAvailable => + 'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.'; + + @override + String chat_pathSetHops(int hopCount, String status) { + String _temp0 = intl.Intl.pluralLogic( + hopCount, + locale: localeName, + other: 'стрибків', + many: 'стрибків', + few: 'стрибки', + one: 'стрибок', + ); + return 'Шлях встановлено: $hopCount $_temp0 - $status'; + } + + @override + String get chat_pathSavedLocally => + 'Збережено локально. Підключіться для синхронізації.'; + + @override + String get chat_pathDeviceConfirmed => 'Пристрій підтверджено.'; + + @override + String get chat_pathDeviceNotConfirmed => 'Пристрій ще не підтверджено.'; + + @override + String get chat_type => 'Ввід'; + + @override + String get chat_path => 'Шлях'; + + @override + String get chat_publicKey => 'Відкритий ключ'; + + @override + String get chat_compressOutgoingMessages => 'Стискати вихідні повідомлення'; + + @override + String get chat_floodForced => 'На всю мережу (примусово)'; + + @override + String get chat_directForced => 'Прямий (примусово)'; + + @override + String chat_hopsForced(int count) { + return '$count стрибків (примусово)'; + } + + @override + String get chat_floodAuto => 'На всю мережу (авто)'; + + @override + String get chat_direct => 'Прямий'; + + @override + String get chat_poiShared => 'Точкою інтересу поділилися'; + + @override + String chat_unread(int count) { + return 'Непрочитано: $count'; + } + + @override + String get chat_openLink => 'Відкрити посилання?'; + + @override + String get chat_openLinkConfirmation => + 'Ви хочете відкрити це посилання у браузері?'; + + @override + String get chat_open => 'Відкрити'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Не вдалося відкрити посилання: $url'; + } + + @override + String get chat_invalidLink => 'Невірний формат посилання'; + + @override + String get map_title => 'Карта вузлів'; + + @override + String get map_noNodesWithLocation => 'Немає вузлів з даними про розташування'; + + @override + String get map_nodesNeedGps => + 'Вузли повинні надавати свої GPS координати,\nщоб з\'явитися на карті.'; + + @override + String map_nodesCount(int count) { + return 'Вузли: $count'; + } + + @override + String map_pinsCount(int count) { + return 'Мітки: $count'; + } + + @override + String get map_chat => 'Чат'; + + @override + String get map_repeater => 'Ретранслятор'; + + @override + String get map_room => 'Кімната'; + + @override + String get map_sensor => 'Сенсор'; + + @override + String get map_pinDm => 'Ключ (DM)'; + + @override + String get map_pinPrivate => 'Замок (Приватний)'; + + @override + String get map_pinPublic => 'Ключ (Публічний)'; + + @override + String get map_lastSeen => 'Останній раз бачили'; + + @override + String get map_disconnectConfirm => + 'Ви впевнені, що хочете відключитися від цього пристрою?'; + + @override + String get map_from => 'Від'; + + @override + String get map_source => 'Джерело'; + + @override + String get map_flags => 'Прапорці'; + + @override + String get map_shareMarkerHere => 'Поділитися маркером тут'; + + @override + String get map_pinLabel => 'Мітка піна'; + + @override + String get map_label => 'Мітка'; + + @override + String get map_pointOfInterest => 'Точка інтересу'; + + @override + String get map_sendToContact => 'Надіслати контакту'; + + @override + String get map_sendToChannel => 'Надіслати в канал'; + + @override + String get map_noChannelsAvailable => 'Немає доступних каналів'; + + @override + String get map_publicLocationShare => 'Поділитися в публічному місці'; + + @override + String map_publicLocationShareConfirm(String channelLabel) { + return 'Ви збираєтеся поділитися розташуванням у $channelLabel. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.'; + } + + @override + String get map_connectToShareMarkers => + 'Підключіться до пристрою, щоб поділитися маркерами'; + + @override + String get map_filterNodes => 'Фільтрувати вузли'; + + @override + String get map_nodeTypes => 'Типи вузлів'; + + @override + String get map_chatNodes => 'Вузли чату'; + + @override + String get map_repeaters => 'Ретранслятори'; + + @override + String get map_otherNodes => 'Інші вузли'; + + @override + String get map_keyPrefix => 'Префікс ключа'; + + @override + String get map_filterByKeyPrefix => 'Фільтрувати за префіксом ключа'; + + @override + String get map_publicKeyPrefix => 'Префікс відкритого ключа'; + + @override + String get map_markers => 'Маркери'; + + @override + String get map_showSharedMarkers => 'Показувати спільні маркери'; + + @override + String get map_lastSeenTime => 'Час останньої активності'; + + @override + String get map_sharedPin => 'Спільний пін'; + + @override + String get map_joinRoom => 'Приєднатися до кімнати'; + + @override + String get map_manageRepeater => 'Керувати ретранслятором'; + + @override + String get mapCache_title => 'Офлайн-кеш карти'; + + @override + String get mapCache_selectAreaFirst => 'Спершу виберіть область для кешування'; + + @override + String get mapCache_noTilesToDownload => + 'Немає плиток для завантаження в цій області.'; + + @override + String get mapCache_downloadTilesTitle => 'Завантажити плитки'; + + @override + String mapCache_downloadTilesPrompt(int count) { + return 'Завантажити $count плиток для використання офлайн?'; + } + + @override + String get mapCache_downloadAction => 'Завантажити'; + + @override + String mapCache_cachedTiles(int count) { + return 'Закешовано $count плиток'; + } + + @override + String mapCache_cachedTilesWithFailed(int downloaded, int failed) { + return 'Плитки в кеші ($downloaded) ($failed помилок)'; + } + + @override + String get mapCache_clearOfflineCacheTitle => 'Очистити офлайн-кеш'; + + @override + String get mapCache_clearOfflineCachePrompt => + 'Видалити всі закешовані плитки карти?'; + + @override + String get mapCache_offlineCacheCleared => 'Офлайн-кеш очищено.'; + + @override + String get mapCache_noAreaSelected => 'Область не вибрано'; + + @override + String get mapCache_cacheArea => 'Область кешування'; + + @override + String get mapCache_useCurrentView => 'Використати поточний вигляд'; + + @override + String get mapCache_zoomRange => 'Діапазон масштабування'; + + @override + String mapCache_estimatedTiles(int count) { + return 'Оцінка плиток: $count'; + } + + @override + String mapCache_downloadedTiles(int completed, int total) { + return 'Завантажено $completed / $total'; + } + + @override + String get mapCache_downloadTilesButton => 'Завантажити плитки'; + + @override + String get mapCache_clearCacheButton => 'Очистити кеш'; + + @override + String mapCache_failedDownloads(int count) { + return 'Невдалі завантаження: $count'; + } + + @override + String mapCache_boundsLabel( + String north, + String south, + String east, + String west, + ) { + return 'Пн $north, Пд $south, Сх $east, Зх $west'; + } + + @override + String get time_justNow => 'Тільки що'; + + @override + String time_minutesAgo(int minutes) { + return '$minutes хв. тому'; + } + + @override + String time_hoursAgo(int hours) { + return '$hours год. тому'; + } + + @override + String time_daysAgo(int days) { + return '$days дн. тому'; + } + + @override + String get time_hour => 'година'; + + @override + String get time_hours => 'годин'; + + @override + String get time_day => 'день'; + + @override + String get time_days => 'днів'; + + @override + String get time_week => 'тиждень'; + + @override + String get time_weeks => 'тижнів'; + + @override + String get time_month => 'місяць'; + + @override + String get time_months => 'місяців'; + + @override + String get time_minutes => 'хвилин'; + + @override + String get time_allTime => 'Весь час'; + + @override + String get dialog_disconnect => 'Відключити'; + + @override + String get dialog_disconnectConfirm => + 'Ви впевнені, що хочете відключитися від цього пристрою?'; + + @override + String get login_repeaterLogin => 'Вхід у ретранслятор'; + + @override + String get login_roomLogin => 'Вхід у кімнату'; + + @override + String get login_password => 'Пароль'; + + @override + String get login_enterPassword => 'Введіть пароль'; + + @override + String get login_savePassword => 'Зберегти пароль'; + + @override + String get login_savePasswordSubtitle => + 'Пароль буде надійно збережено на цьому пристрої.'; + + @override + String get login_repeaterDescription => + 'Введіть пароль ретранслятора для доступу до налаштувань та статусу.'; + + @override + String get login_roomDescription => + 'Введіть пароль кімнати для доступу до налаштувань та статусу.'; + + @override + String get login_routing => 'Маршрутизація'; + + @override + String get login_routingMode => 'Режим маршрутизації'; + + @override + String get login_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; + + @override + String get login_forceFloodMode => 'Примусово на всю мережу'; + + @override + String get login_managePaths => 'Керувати шляхами'; + + @override + String get login_login => 'Вхід'; + + @override + String login_attempt(int current, int max) { + return 'Спроба $current/$max'; + } + + @override + String login_failed(String error) { + return 'Вхід не вдався: $error'; + } + + @override + String get login_failedMessage => + 'Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.'; + + @override + String get common_reload => 'Перезавантажити'; + + @override + String get common_clear => 'Очистити'; + + @override + String path_currentPath(String path) { + return 'Поточний шлях: $path'; + } + + @override + String path_usingHopsPath(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'стрибками', + many: 'стрибками', + few: 'стрибками', + one: 'стрибком', + ); + return 'Використання шляху з $count $_temp0'; + } + + @override + String get path_enterCustomPath => 'Ввести власний шлях'; + + @override + String get path_currentPathLabel => 'Поточний шлях'; + + @override + String get path_hexPrefixInstructions => + 'Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.'; + + @override + String get path_hexPrefixExample => + 'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).'; + + @override + String get path_labelHexPrefixes => 'Hex-префікси'; + + @override + String get path_helperMaxHops => + 'Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)'; + + @override + String get path_selectFromContacts => 'Вибрати з контактів:'; + + @override + String get path_noRepeatersFound => + 'Ретрансляторів або серверів кімнат не знайдено.'; + + @override + String get path_customPathsRequire => + 'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.'; + + @override + String path_invalidHexPrefixes(String prefixes) { + return 'Некоректні hex-префікси: $prefixes'; + } + + @override + String get path_tooLong => 'Шлях занадто довгий. Максимум 64 стрибки.'; + + @override + String get path_setPath => 'Встановити шлях'; + + @override + String get repeater_management => 'Керування ретранслятором'; + + @override + String get room_management => 'Адміністрування сервера кімнати'; + + @override + String get repeater_managementTools => 'Інструменти керування'; + + @override + String get repeater_status => 'Статус'; + + @override + String get repeater_statusSubtitle => + 'Показати статус, статистику та сусідів ретранслятора'; + + @override + String get repeater_telemetry => 'Телеметрія'; + + @override + String get repeater_telemetrySubtitle => + 'Показати телеметрію сенсорів та статистику системи'; + + @override + String get repeater_cli => 'CLI'; + + @override + String get repeater_cliSubtitle => 'Надіслати команди ретранслятору'; + + @override + String get repeater_neighbours => 'Сусіди'; + + @override + String get repeater_neighboursSubtitle => 'Показати сусідів нульового стрибка.'; + + @override + String get repeater_settings => 'Налаштування'; + + @override + String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора'; + + @override + String get repeater_statusTitle => 'Статус ретранслятора'; + + @override + String get repeater_routingMode => 'Режим маршрутизації'; + + @override + String get repeater_autoUseSavedPath => + 'Авто (використовувати збережений шлях)'; + + @override + String get repeater_forceFloodMode => 'Примусово на всю мережу'; + + @override + String get repeater_pathManagement => 'Керування шляхами'; + + @override + String get repeater_refresh => 'Оновити'; + + @override + String get repeater_statusRequestTimeout => + 'Час очікування запиту статусу вичерпано.'; + + @override + String repeater_errorLoadingStatus(String error) { + return 'Помилка завантаження статусу: $error'; + } + + @override + String get repeater_systemInformation => 'Системна інформація'; + + @override + String get repeater_battery => 'Батарея'; + + @override + String get repeater_clockAtLogin => 'Годинник (при вході)'; + + @override + String get repeater_uptime => 'Час роботи'; + + @override + String get repeater_queueLength => 'Довжина черги'; + + @override + String get repeater_debugFlags => 'Прапорці налагодження'; + + @override + String get repeater_radioStatistics => 'Статистика радіо'; + + @override + String get repeater_lastRssi => 'Останній RSSI'; + + @override + String get repeater_lastSnr => 'Останній SNR'; + + @override + String get repeater_noiseFloor => 'Рівень шуму'; + + @override + String get repeater_txAirtime => 'Ефірний час TX'; + + @override + String get repeater_rxAirtime => 'Ефірний час RX'; + + @override + String get repeater_packetStatistics => 'Статистика пакетів'; + + @override + String get repeater_sent => 'Надіслано'; + + @override + String get repeater_received => 'Отримано'; + + @override + String get repeater_duplicates => 'Дублікати'; + + @override + String repeater_daysHoursMinsSecs( + int days, + int hours, + int minutes, + int seconds, + ) { + return '$days дн. ${hours} год $minutes хв $seconds с'; + } + + @override + String repeater_packetTxTotal(int total, String flood, String direct) { + return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + } + + @override + String repeater_packetRxTotal(int total, String flood, String direct) { + return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + } + + @override + String repeater_duplicatesFloodDirect(String flood, String direct) { + return 'На всю мережу: $flood, Прямі: $direct'; + } + + @override + String repeater_duplicatesTotal(int total) { + return 'Всього: $total'; + } + + @override + String get repeater_settingsTitle => 'Налаштування ретранслятора'; + + @override + String get repeater_basicSettings => 'Основні налаштування'; + + @override + String get repeater_repeaterName => 'Ім\'я ретранслятора'; + + @override + String get repeater_repeaterNameHelper => 'Показати ім\'я цього ретранслятора'; + + @override + String get repeater_adminPassword => 'Пароль адміністратора'; + + @override + String get repeater_adminPasswordHelper => 'Пароль повного доступу'; + + @override + String get repeater_guestPassword => 'Гостьовий пароль'; + + @override + String get repeater_guestPasswordHelper => + 'Доступ лише для читання з паролем'; + + @override + String get repeater_radioSettings => 'Налаштування радіо'; + + @override + String get repeater_frequencyMhz => 'Частота (МГц)'; + + @override + String get repeater_frequencyHelper => '300-2500 МГц'; + + @override + String get repeater_txPower => 'Потужність TX'; + + @override + String get repeater_txPowerHelper => '1-30 дБм'; + + @override + String get repeater_bandwidth => 'Смуга пропускання'; + + @override + String get repeater_spreadingFactor => 'Коефіцієнт розширення'; + + @override + String get repeater_codingRate => 'Швидкість кодування'; + + @override + String get repeater_locationSettings => 'Налаштування розташування'; + + @override + String get repeater_latitude => 'Широта'; + + @override + String get repeater_latitudeHelper => + 'Десяткові градуси (наприклад, 37.7749)'; + + @override + String get repeater_longitude => 'Довгота'; + + @override + String get repeater_longitudeHelper => + 'Десяткові градуси (наприклад, -122.4194)'; + + @override + String get repeater_features => 'Функції'; + + @override + String get repeater_packetForwarding => 'Пересилання пакетів'; + + @override + String get repeater_packetForwardingSubtitle => + 'Дозволити ретранслятору пересилати пакети'; + + @override + String get repeater_guestAccess => 'Гостьовий доступ'; + + @override + String get repeater_guestAccessSubtitle => + 'Дозволити гостьовий доступ лише для читання'; + + @override + String get repeater_privacyMode => 'Режим приватності'; + + @override + String get repeater_privacyModeSubtitle => + 'Приховати ім\'я/розташування в оголошеннях'; + + @override + String get repeater_advertisementSettings => 'Налаштування оголошень'; + + @override + String get repeater_localAdvertInterval => + 'Інтервал локальних оголошень (0 стрибків)'; + + @override + String repeater_localAdvertIntervalMinutes(int minutes) { + return '$minutes хвилин'; + } + + @override + String get repeater_floodAdvertInterval => + 'Інтервал оголошень на всю мережу (flood)'; + + @override + String repeater_floodAdvertIntervalHours(int hours) { + return '$hours годин'; + } + + @override + String get repeater_encryptedAdvertInterval => + 'Інтервал зашифрованих оголошень'; + + @override + String get repeater_dangerZone => 'Небезпечна зона'; + + @override + String get repeater_rebootRepeater => 'Перезавантажити ретранслятор'; + + @override + String get repeater_rebootRepeaterSubtitle => 'Скинути пристрій ретранслятора'; + + @override + String get repeater_rebootRepeaterConfirm => + 'Ви впевнені, що хочете перезавантажити цей ретранслятор?'; + + @override + String get repeater_regenerateIdentityKey => 'Перегенерувати ключ ідентичності'; + + @override + String get repeater_regenerateIdentityKeySubtitle => + 'Згенерувати нову пару ключів (публічний/приватний)'; + + @override + String get repeater_regenerateIdentityKeyConfirm => + 'Це створить нову ідентичність для ретранслятора. Продовжити?'; + + @override + String get repeater_eraseFileSystem => 'Очистити файлову систему'; + + @override + String get repeater_eraseFileSystemSubtitle => + 'Відформатувати файлову систему ретранслятора'; + + @override + String get repeater_eraseFileSystemConfirm => + 'УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!'; + + @override + String get repeater_eraseSerialOnly => + 'Очищення доступне лише через послідовну консоль.'; + + @override + String repeater_commandSent(String command) { + return 'Команда надіслана: $command'; + } + + @override + String repeater_errorSendingCommand(String error) { + return 'Помилка надсилання команди: $error'; + } + + @override + String get repeater_confirm => 'Підтвердити'; + + @override + String get repeater_settingsSaved => 'Налаштування успішно збережено.'; + + @override + String repeater_errorSavingSettings(String error) { + return 'Помилка збереження налаштувань: $error'; + } + + @override + String get repeater_refreshBasicSettings => 'Оновити основні налаштування'; + + @override + String get repeater_refreshRadioSettings => 'Оновити налаштування радіо'; + + @override + String get repeater_refreshTxPower => 'Оновити потужність TX'; + + @override + String get repeater_refreshLocationSettings => + 'Оновити налаштування розташування'; + + @override + String get repeater_refreshPacketForwarding => 'Оновити пересилання пакетів'; + + @override + String get repeater_refreshGuestAccess => 'Оновити гостьовий доступ'; + + @override + String get repeater_refreshPrivacyMode => 'Оновити режим приватності'; + + @override + String get repeater_refreshAdvertisementSettings => + 'Оновити налаштування оголошень'; + + @override + String repeater_refreshed(String label) { + return '$label оновлено'; + } + + @override + String repeater_errorRefreshing(String label) { + return 'Помилка оновлення $label'; + } + + @override + String get repeater_cliTitle => 'Ретранслятор CLI'; + + @override + String get repeater_debugNextCommand => 'Налагодити наступну команду'; + + @override + String get repeater_commandHelp => 'Довідка'; + + @override + String get repeater_clearHistory => 'Очистити історію'; + + @override + String get repeater_noCommandsSent => 'Команди ще не надсилалися.'; + + @override + String get repeater_typeCommandOrUseQuick => + 'Введіть команду нижче або використовуйте швидкі команди'; + + @override + String get repeater_enterCommandHint => 'Введіть команду...'; + + @override + String get repeater_previousCommand => 'Попередня команда'; + + @override + String get repeater_nextCommand => 'Наступна команда'; + + @override + String get repeater_enterCommandFirst => 'Спершу введіть команду'; + + @override + String get repeater_cliCommandFrameTitle => 'Фрейм команди CLI'; + + @override + String repeater_cliCommandError(String error) { + return 'Помилка: $error'; + } + + @override + String get repeater_cliQuickGetName => 'Отримати ім\'я'; + + @override + String get repeater_cliQuickGetRadio => 'Отримати Радіо'; + + @override + String get repeater_cliQuickGetTx => 'Отримати TX'; + + @override + String get repeater_cliQuickNeighbors => 'Сусіди'; + + @override + String get repeater_cliQuickVersion => 'Версія'; + + @override + String get repeater_cliQuickAdvertise => 'Оголосити'; + + @override + String get repeater_cliQuickClock => 'Годинник'; + + @override + String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; + + @override + String get repeater_cliHelpReboot => + 'Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що є нормальним)'; + + @override + String get repeater_cliHelpClock => + 'Відображає поточний час за годинником кожного пристрою.'; + + @override + String get repeater_cliHelpPassword => + 'Встановлює новий пароль адміністратора для пристрою.'; + + @override + String get repeater_cliHelpVersion => + 'Відображає версію пристрою та дату збірки прошивки.'; + + @override + String get repeater_cliHelpClearStats => + 'Скидає різні лічильники статистики до нуля.'; + + @override + String get repeater_cliHelpSetAf => 'Встановлює коефіцієнт ефірного часу.'; + + @override + String get repeater_cliHelpSetTx => + 'Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).'; + + @override + String get repeater_cliHelpSetRepeat => + 'Вмикає або вимикає роль ретранслятора для цього вузла.'; + + @override + String get repeater_cliHelpSetAllowReadOnly => + '(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)'; + + @override + String get repeater_cliHelpSetFloodMax => + 'Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).'; + + @override + String get repeater_cliHelpSetIntThresh => + 'Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.'; + + @override + String get repeater_cliHelpSetAgcResetInterval => + 'Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.'; + + @override + String get repeater_cliHelpSetMultiAcks => + 'Вмикає або вимикає функціональність подвійних ACK.'; + + @override + String get repeater_cliHelpSetAdvertInterval => + 'Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.'; + + @override + String get repeater_cliHelpSetFloodAdvertInterval => + 'Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.'; + + @override + String get repeater_cliHelpSetGuestPassword => + 'Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)'; + + @override + String get repeater_cliHelpSetName => 'Встановлює ім\'я для оголошення.'; + + @override + String get repeater_cliHelpSetLat => + 'Встановлює широту для карти оголошень. (десяткові градуси)'; + + @override + String get repeater_cliHelpSetLon => + 'Встановлює довготу для карти оголошень. (десяткові градуси)'; + + @override + String get repeater_cliHelpSetRadio => + 'Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.'; + + @override + String get repeater_cliHelpSetRxDelay => + 'Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.'; + + @override + String get repeater_cliHelpSetTxDelay => + 'Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).'; + + @override + String get repeater_cliHelpSetDirectTxDelay => + 'Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.'; + + @override + String get repeater_cliHelpSetBridgeEnabled => 'Увімкнути/Вимкнути міст.'; + + @override + String get repeater_cliHelpSetBridgeDelay => + 'Встановити затримку перед пересиланням пакетів.'; + + @override + String get repeater_cliHelpSetBridgeSource => + 'Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.'; + + @override + String get repeater_cliHelpSetBridgeBaud => + 'Встановити швидкість послідовного зв\'язку для мостів Rs232.'; + + @override + String get repeater_cliHelpSetBridgeSecret => + 'Встановити секрет мосту для мостів espnow.'; + + @override + String get repeater_cliHelpSetAdcMultiplier => + 'Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).'; + + @override + String get repeater_cliHelpTempRadio => + 'Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).'; + + @override + String get repeater_cliHelpSetPerm => + 'Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний і його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).'; + + @override + String get repeater_cliHelpGetBridgeType => + 'Отримати тип мосту: немає, rs232, espnow'; + + @override + String get repeater_cliHelpLogStart => + 'Починає запис пакетів у файлову систему.'; + + @override + String get repeater_cliHelpLogStop => + 'Зупиняє запис пакетів у файлову систему.'; + + @override + String get repeater_cliHelpLogErase => + 'Видаляє журнали пакетів з файлової системи.'; + + @override + String get repeater_cliHelpNeighbors => + 'Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4'; + + @override + String get repeater_cliHelpNeighborRemove => + 'Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.'; + + @override + String get repeater_cliHelpRegion => + '(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).'; + + @override + String get repeater_cliHelpRegionLoad => + 'ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.'; + + @override + String get repeater_cliHelpRegionGet => + 'Шукає регіон із заданим префіксом назви (або \"\" для глобальної області). Відповідає: \"-> ім\'я-регіону (ім\'я-батька) \'F\'\"'; + + @override + String get repeater_cliHelpRegionPut => + 'Додає або оновлює визначення регіону з заданою назвою.'; + + @override + String get repeater_cliHelpRegionRemove => + 'Видаляє визначення регіону з заданою назвою.'; + + @override + String get repeater_cliHelpRegionAllowf => + 'Встановлює дозвіл «Flood» для заданого регіону. (\'\' для глобальної/успадкованої області)'; + + @override + String get repeater_cliHelpRegionDenyf => + 'Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )'; + + @override + String get repeater_cliHelpRegionHome => + 'Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)'; + + @override + String get repeater_cliHelpRegionHomeSet => 'Встановлює «домашній» регіон.'; + + @override + String get repeater_cliHelpRegionSave => + 'Зберігає список/карту регіонів у сховищі.'; + + @override + String get repeater_cliHelpGps => + 'Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.'; + + @override + String get repeater_cliHelpGpsOnOff => 'Увімкнути/вимкнути GPS.'; + + @override + String get repeater_cliHelpGpsSync => + 'Синхронізує час вузла з годинником GPS.'; + + @override + String get repeater_cliHelpGpsSetLoc => + 'Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.'; + + @override + String get repeater_cliHelpGpsAdvert => + 'Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях'; + + @override + String get repeater_cliHelpGpsAdvertSet => + 'Встановлює конфігурацію оголошення розташування.'; + + @override + String get repeater_commandsListTitle => 'Список команд'; + + @override + String get repeater_commandsListNote => + 'ПРИМІТКА: для різних команд «set»... також існує команда «get»...'; + + @override + String get repeater_general => 'Загальні'; + + @override + String get repeater_settingsCategory => 'Налаштування'; + + @override + String get repeater_bridge => 'Міст'; + + @override + String get repeater_logging => 'Логування'; + + @override + String get repeater_neighborsRepeaterOnly => + 'Сусіди (Тільки ретранслятор)'; + + @override + String get repeater_regionManagementRepeaterOnly => + 'Керування регіонами (Тільки ретранслятор)'; + + @override + String get repeater_regionNote => + 'Команди регіонів були введені для керування визначеннями та дозволами регіонів.'; + + @override + String get repeater_gpsManagement => 'Керування GPS'; + + @override + String get repeater_gpsNote => + 'Команда GPS була введена для керування питаннями, пов\'язаними з локацією.'; + + @override + String get telemetry_receivedData => 'Дані телеметрії отримано'; + + @override + String get telemetry_requestTimeout => 'Час запиту телеметрії вичерпано.'; + + @override + String telemetry_errorLoading(String error) { + return 'Помилка завантаження телеметрії: $error'; + } + + @override + String get telemetry_noData => 'Дані телеметрії недоступні.'; + + @override + String telemetry_channelTitle(int channel) { + return 'Канал $channel'; + } + + @override + String get telemetry_batteryLabel => 'Батарея'; + + @override + String get telemetry_voltageLabel => 'Напруга'; + + @override + String get telemetry_mcuTemperatureLabel => 'Температура MCU'; + + @override + String get telemetry_temperatureLabel => 'Температура'; + + @override + String get telemetry_currentLabel => 'Поточний струм'; + + @override + String telemetry_batteryValue(int percent, String volts) { + return '$percent% / ${volts}В'; + } + + @override + String telemetry_voltageValue(String volts) { + return '${volts}В'; + } + + @override + String telemetry_currentValue(String amps) { + return '${amps}А'; + } + + @override + String telemetry_temperatureValue(String celsius, String fahrenheit) { + return '$celsius°C / $fahrenheit°F'; + } + + @override + String get neighbors_receivedData => 'Дані сусідів отримано'; + + @override + String get neighbors_requestTimedOut => 'Час запиту сусідів вичерпано.'; + + @override + String neighbors_errorLoading(String error) { + return 'Помилка завантаження сусідів: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди'; + + @override + String get neighbors_noData => 'Дані про сусідів недоступні.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Невідомий відкритий ключ $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Почуто: $time тому'; + } + + @override + String get channelPath_title => 'Шлях пакету'; + + @override + String get channelPath_viewMap => 'Показати карту'; + + @override + String get channelPath_otherObservedPaths => 'Інші спостережувані шляхи'; + + @override + String get channelPath_repeaterHops => 'Стрибки ретранслятора'; + + @override + String get channelPath_noHopDetails => + 'Деталі відправки не надані для цього пакету.'; + + @override + String get channelPath_messageDetails => 'Деталі повідомлення'; + + @override + String get channelPath_senderLabel => 'Відправник'; + + @override + String get channelPath_timeLabel => 'Час'; + + @override + String get channelPath_repeatsLabel => 'Повторення'; + + @override + String channelPath_pathLabel(int index) { + return 'Шлях $index'; + } + + @override + String get channelPath_observedLabel => 'Спостережено'; + + @override + String channelPath_observedPathTitle(int index, String hops) { + return 'Спостережуваний шлях $index • $hops'; + } + + @override + String get channelPath_noLocationData => 'Немає даних про розташування'; + + @override + String channelPath_timeWithDate(int day, int month, String time) { + return '$day/$month $time'; + } + + @override + String channelPath_timeOnly(String time) { + return '$time'; + } + + @override + String get channelPath_unknownPath => 'Невідомий'; + + @override + String get channelPath_floodPath => 'На всю мережу'; + + @override + String get channelPath_directPath => 'Прямий'; + + @override + String channelPath_observedZeroOf(int total) { + return '0 з $total стрибків'; + } + + @override + String channelPath_observedSomeOf(int observed, int total) { + return '$observed з $total стрибків'; + } + + @override + String get channelPath_mapTitle => 'Карта шляху'; + + @override + String get channelPath_noRepeaterLocations => + 'Позиції ретрансляторів недоступні для цього шляху.'; + + @override + String channelPath_primaryPath(int index) { + return 'Шлях $index (Основний)'; + } + + @override + String get channelPath_pathLabelTitle => 'Шлях'; + + @override + String get channelPath_observedPathHeader => 'Спостережуваний шлях'; + + @override + String channelPath_selectedPathLabel(String label, String prefixes) { + return '$label • $prefixes'; + } + + @override + String get channelPath_noHopDetailsAvailable => + 'Деталі стрибків недоступні для цього пакету.'; + + @override + String get channelPath_unknownRepeater => 'Невідомий ретранслятор'; + + @override + String get community_title => 'Спільнота'; + + @override + String get community_create => 'Створити спільноту'; + + @override + String get community_createDesc => + 'Створити нову спільноту та поділитися через QR-код.'; + + @override + String get community_join => 'Приєднатися'; + + @override + String get community_joinTitle => 'Приєднатися до спільноти'; + + @override + String community_joinConfirmation(String name) { + return 'Ви бажаєте приєднатися до спільноти \"$name\"?'; + } + + @override + String get community_scanQr => 'Сканувати QR спільноти'; + + @override + String get community_scanInstructions => + 'Наведіть камеру на QR-код спільноти.'; + + @override + String get community_showQr => 'Показати QR-код'; + + @override + String get community_publicChannel => 'Публічна спільнота'; + + @override + String get community_hashtagChannel => 'Хештег спільноти'; + + @override + String get community_name => 'Назва спільноти'; + + @override + String get community_enterName => 'Введіть назву спільноти'; + + @override + String community_created(String name) { + return 'Спільноту \"$name\" створено'; + } + + @override + String community_joined(String name) { + return 'Приєднався до спільноти \"$name\"'; + } + + @override + String get community_qrTitle => 'Поділитися спільнотою'; + + @override + String community_qrInstructions(String name) { + return 'Відскануйте цей QR-код, щоб приєднатися до $name'; + } + + @override + String get community_hashtagPrivacyHint => + 'Канали хештегів спільноти доступні лише членам спільноти'; + + @override + String get community_invalidQrCode => 'Недійсний QR-код спільноти'; + + @override + String get community_alreadyMember => 'Вже учасник'; + + @override + String community_alreadyMemberMessage(String name) { + return 'Ви вже є учасником \"$name\".'; + } + + @override + String get community_addPublicChannel => 'Додати публічний канал спільноти'; + + @override + String get community_addPublicChannelHint => + 'Автоматично додати публічний канал для цієї спільноти'; + + @override + String get community_noCommunities => + 'Поки не приєднано до жодної групи.'; + + @override + String get community_scanOrCreate => + 'Відскануйте QR-код або створіть спільноту, щоб почати'; + + @override + String get community_manageCommunities => 'Керувати спільнотами'; + + @override + String get community_delete => 'Покинути спільноту'; + + @override + String community_deleteConfirm(String name) { + return 'Покинути \"$name\"?'; + } + + @override + String community_deleteChannelsWarning(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'каналів', + many: 'каналів', + few: 'канали', + one: 'канал', + ); + return 'Це також видалить $count $_temp0 та їх повідомлення.'; + } + + @override + String community_deleted(String name) { + return 'Спільноту \"$name\" покинуто'; + } + + @override + String get community_regenerateSecret => 'Перегенерувати секрет'; + + @override + String community_regenerateSecretConfirm(String name) { + return 'Перегенерувати секретний ключ для \"$name\"? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; + } + + @override + String get community_regenerate => 'Перегенерувати'; + + @override + String community_secretRegenerated(String name) { + return 'Секретний пароль для \"$name\" перегенеровано'; + } + + @override + String get community_updateSecret => 'Оновити секрет'; + + @override + String community_secretUpdated(String name) { + return 'Зміну секрету для \"$name\" оновлено'; + } + + @override + String community_scanToUpdateSecret(String name) { + return 'Відскануйте новий QR-код, щоб оновити пароль для \"$name\"'; + } + + @override + String get community_addHashtagChannel => 'Додати хештег спільноти'; + + @override + String get community_addHashtagChannelDesc => + 'Додати канал хештегу для цієї спільноти'; + + @override + String get community_selectCommunity => 'Вибрати спільноту'; + + @override + String get community_regularHashtag => 'Звичайний хештег'; + + @override + String get community_regularHashtagDesc => + 'Публічний хештег (будь-хто може приєднатися)'; + + @override + String get community_communityHashtag => 'Хештег спільноти'; + + @override + String get community_communityHashtagDesc => + 'Ексклюзивно для членів спільноти'; + + @override + String community_forCommunity(String name) { + return 'Для $name'; + } + + @override + String get listFilter_tooltip => 'Фільтр та сортування'; + + @override + String get listFilter_sortBy => 'Сортувати за'; + + @override + String get listFilter_latestMessages => 'Останні повідомлення'; + + @override + String get listFilter_heardRecently => 'Нещодавно чули'; + + @override + String get listFilter_az => 'А-Я'; + + @override + String get listFilter_filters => 'Фільтри'; + + @override + String get listFilter_all => 'Все'; + + @override + String get listFilter_users => 'Користувачі'; + + @override + String get listFilter_repeaters => 'Ретранслятори'; + + @override + String get listFilter_roomServers => 'Сервери кімнат'; + + @override + String get listFilter_unreadOnly => 'Тільки непрочитані повідомлення'; + + @override + String get listFilter_newGroup => 'Нова група'; +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb new file mode 100644 index 0000000..492805e --- /dev/null +++ b/lib/l10n/app_uk.arb @@ -0,0 +1,1538 @@ +{ + "@@locale": "uk", + "appTitle": "MeshCore Open", + "nav_contacts": "Контакти", + "nav_channels": "Канали", + "nav_map": "Карта", + "common_cancel": "Скасувати", + "common_connect": "Підключити", + "common_unknownDevice": "Невідомий пристрій", + "common_save": "Зберегти", + "common_delete": "Видалити", + "common_close": "Закрити", + "common_edit": "Редагувати", + "common_add": "Додати", + "common_settings": "Налаштування", + "common_disconnect": "Відключити", + "common_connected": "Підключено", + "common_disconnected": "Відключено", + "common_create": "Створити", + "common_continue": "Продовжити", + "common_share": "Поділитися", + "common_copy": "Копіювати", + "common_retry": "Повторити", + "common_hide": "Приховати", + "common_remove": "Прибрати", + "common_enable": "Увімкнути", + "common_disable": "Вимкнути", + "common_reboot": "Перезавантажити", + "common_loading": "Завантаження...", + "common_notAvailable": "—", + "common_voltageValue": "{volts} В", + "@common_voltageValue": { + "placeholders": { + "volts": { + "type": "String" + } + } + }, + "common_percentValue": "{percent}%", + "@common_percentValue": { + "placeholders": { + "percent": { + "type": "int" + } + } + }, + "scanner_title": "MeshCore Open", + "scanner_scanning": "Пошук пристроїв...", + "scanner_connecting": "Підключення...", + "scanner_disconnecting": "Відключення...", + "scanner_notConnected": "Не підключено", + "scanner_connectedTo": "Підключено до {deviceName}", + "@scanner_connectedTo": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "scanner_searchingDevices": "Пошук пристроїв MeshCore...", + "scanner_tapToScan": "Натисніть «Сканувати», щоб знайти пристрої MeshCore", + "scanner_connectionFailed": "Помилка підключення: {error}", + "@scanner_connectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "scanner_stop": "Стоп", + "scanner_scan": "Сканувати", + "device_quickSwitch": "Швидке перемикання", + "device_meshcore": "MeshCore", + "settings_title": "Налаштування", + "settings_deviceInfo": "Інформація про пристрій", + "settings_appSettings": "Налаштування програми", + "settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти", + "settings_nodeSettings": "Налаштування вузла", + "settings_nodeName": "Ім'я вузла", + "settings_nodeNameNotSet": "Не встановлено", + "settings_nodeNameHint": "Введіть ім'я вузла", + "settings_nodeNameUpdated": "Ім'я оновлено", + "settings_radioSettings": "Налаштування радіо", + "settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення", + "settings_radioSettingsUpdated": "Налаштування радіо оновлено", + "settings_location": "Розташування", + "settings_locationSubtitle": "GPS координати", + "settings_locationUpdated": "Розташування оновлено", + "settings_locationBothRequired": "Введіть широту та довготу.", + "settings_locationInvalid": "Некоректна широта або довгота.", + "settings_latitude": "Широта", + "settings_longitude": "Довгота", + "settings_privacyMode": "Режим приватності", + "settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", + "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.", + "settings_privacyModeEnabled": "Режим приватності увімкнено", + "settings_privacyModeDisabled": "Режим приватності вимкнено", + "settings_actions": "Дії", + "settings_sendAdvertisement": "Оголосити себе", + "settings_sendAdvertisementSubtitle": "Транслювати присутність зараз", + "settings_advertisementSent": "Оголошення надіслано", + "settings_syncTime": "Синхронізація часу", + "settings_syncTimeSubtitle": "Встановити час пристрою відповідно до часу телефону.", + "settings_timeSynchronized": "Час синхронізовано", + "settings_refreshContacts": "Оновити контакти", + "settings_refreshContactsSubtitle": "Перезавантажити список контактів з пристрою", + "settings_rebootDevice": "Перезавантажити пристрій", + "settings_rebootDeviceSubtitle": "Перезавантажити пристрій MeshCore", + "settings_rebootDeviceConfirm": "Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.", + "settings_debug": "Налагодження", + "settings_bleDebugLog": "Журнал налагодження BLE", + "settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані", + "settings_appDebugLog": "Журнал налагодження програми", + "settings_appDebugLogSubtitle": "Повідомлення налагодження програми", + "settings_about": "Про програму", + "settings_aboutVersion": "MeshCore Open v{version}", + "@settings_aboutVersion": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "settings_aboutLegalese": "Проєкт MeshCore Open Source 2026", + "settings_aboutDescription": "Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.", + "settings_infoName": "Ім'я", + "settings_infoId": "ID", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батарея", + "settings_infoPublicKey": "Відкритий ключ", + "settings_infoContactsCount": "Кількість контактів", + "settings_infoChannelCount": "Кількість каналів", + "settings_presets": "Попередні налаштування", + "settings_preset915Mhz": "915 МГц", + "settings_preset868Mhz": "868 МГц", + "settings_preset433Mhz": "433 МГц", + "settings_frequency": "Частота (МГц)", + "settings_frequencyHelper": "300.0 - 2500.0", + "settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)", + "settings_bandwidth": "Смуга пропускання", + "settings_spreadingFactor": "Коефіцієнт розширення", + "settings_codingRate": "Швидкість кодування", + "settings_txPower": "Потужність TX (дБм)", + "settings_txPowerHelper": "0 - 22", + "settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)", + "settings_longRange": "Дальній діапазон", + "settings_fastSpeed": "Висока швидкість", + "settings_error": "Помилка: {message}", + "@settings_error": { + "placeholders": { + "message": { + "type": "String" + } + } + }, + "appSettings_title": "Налаштування програми", + "appSettings_appearance": "Вигляд", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Системна", + "appSettings_themeLight": "Світла", + "appSettings_themeDark": "Темна", + "appSettings_language": "Мова", + "appSettings_languageSystem": "Як у системі", + "appSettings_languageEn": "English", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", + "appSettings_languageDe": "Deutsch", + "appSettings_languagePl": "Polski", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", + "appSettings_languageIt": "Italiano", + "appSettings_languageZh": "中文", + "appSettings_languageSv": "Svenska", + "appSettings_languageNl": "Nederlands", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_languageUk": "Українська", + "appSettings_notifications": "Сповіщення", + "appSettings_enableNotifications": "Увімкнути сповіщення", + "appSettings_enableNotificationsSubtitle": "Отримувати сповіщення про повідомлення та оголошення", + "appSettings_notificationPermissionDenied": "У доступі до сповіщень відмовлено", + "appSettings_notificationsEnabled": "Сповіщення увімкнено", + "appSettings_notificationsDisabled": "Сповіщення вимкнено", + "appSettings_messageNotifications": "Сповіщення про повідомлення", + "appSettings_messageNotificationsSubtitle": "Показувати сповіщення при отриманні нових повідомлень", + "appSettings_channelMessageNotifications": "Сповіщення каналів", + "appSettings_channelMessageNotificationsSubtitle": "Показувати сповіщення при отриманні повідомлень каналу", + "appSettings_advertisementNotifications": "Сповіщення про оголошення", + "appSettings_advertisementNotificationsSubtitle": "Показувати сповіщення при виявленні нових вузлів", + "appSettings_messaging": "Обмін повідомленнями", + "appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб", + "appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання", + "appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.", + "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.", + "appSettings_autoRouteRotation": "Авторотація маршруту", + "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)", + "appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена", + "appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена", + "appSettings_battery": "Батарея", + "appSettings_batteryChemistry": "Хімія батареї", + "appSettings_batteryChemistryPerDevice": "Встановити для пристрою ({deviceName})", + "@appSettings_batteryChemistryPerDevice": { + "placeholders": { + "deviceName": { + "type": "String" + } + } + }, + "appSettings_batteryChemistryConnectFirst": "Підключіть пристрій, щоб вибрати", + "appSettings_batteryNmc": "18650 NMC (3.0-4.2В)", + "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65В)", + "appSettings_batteryLipo": "LiPo (3.0-4.2В)", + "appSettings_mapDisplay": "Відображення карти", + "appSettings_showRepeaters": "Показувати ретранслятори", + "appSettings_showRepeatersSubtitle": "Відображати вузли-ретранслятори на карті", + "appSettings_showChatNodes": "Показувати вузли чату", + "appSettings_showChatNodesSubtitle": "Відображати вузли чату на карті", + "appSettings_showOtherNodes": "Показувати інші вузли", + "appSettings_showOtherNodesSubtitle": "Відображати інші типи вузлів на карті", + "appSettings_timeFilter": "Фільтр часу", + "appSettings_timeFilterShowAll": "Показати всі вузли", + "appSettings_timeFilterShowLast": "Показати вузли за останні {hours} год", + "@appSettings_timeFilterShowLast": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "appSettings_mapTimeFilter": "Фільтр часу карти", + "appSettings_showNodesDiscoveredWithin": "Показувати вузли, виявлені за:", + "appSettings_allTime": "Весь час", + "appSettings_lastHour": "Останню годину", + "appSettings_last6Hours": "Останні 6 годин", + "appSettings_last24Hours": "Останні 24 години", + "appSettings_lastWeek": "Минулий тиждень", + "appSettings_offlineMapCache": "Офлайн-кеш карти", + "appSettings_noAreaSelected": "Область не вибрано", + "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", + "@appSettings_areaSelectedZoom": { + "placeholders": { + "minZoom": { + "type": "int" + }, + "maxZoom": { + "type": "int" + } + } + }, + "appSettings_debugCard": "Налагодження", + "appSettings_appDebugLogging": "Логування налагодження програми", + "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.", + "appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено", + "appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.", + "contacts_title": "Контакти", + "contacts_noContacts": "Контактів не знайдено.", + "contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.", + "contacts_searchContacts": "Пошук контактів...", + "contacts_noUnreadContacts": "Немає непрочитаних контактів", + "contacts_noContactsFound": "Контактів або груп не знайдено.", + "contacts_deleteContact": "Видалити контакт", + "contacts_removeConfirm": "Видалити {contactName} з контактів?", + "@contacts_removeConfirm": { + "placeholders": { + "contactName": { + "type": "String" + } + } + }, + "contacts_manageRepeater": "Керувати ретранслятором", + "contacts_roomLogin": "Вхід у кімнату", + "contacts_openChat": "Відкрити чат", + "contacts_editGroup": "Редагувати групу", + "contacts_deleteGroup": "Видалити групу", + "contacts_deleteGroupConfirm": "Видалити {groupName}?", + "@contacts_deleteGroupConfirm": { + "placeholders": { + "groupName": { + "type": "String" + } + } + }, + "contacts_newGroup": "Нова група", + "contacts_groupName": "Назва групи", + "contacts_groupNameRequired": "Назва групи обов'язкова.", + "contacts_groupAlreadyExists": "Група «{name}» вже існує.", + "@contacts_groupAlreadyExists": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "contacts_filterContacts": "Фільтрувати контакти...", + "contacts_noContactsMatchFilter": "Жоден контакт не відповідає фільтру.", + "contacts_noMembers": "Немає учасників", + "contacts_lastSeenNow": "В мережі", + "contacts_lastSeenMinsAgo": "В мережі {minutes} хв. тому", + "@contacts_lastSeenMinsAgo": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "contacts_lastSeenHourAgo": "В мережі 1 годину тому", + "contacts_lastSeenHoursAgo": "В мережі {hours} год. тому", + "@contacts_lastSeenHoursAgo": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "contacts_lastSeenDayAgo": "В мережі 1 день тому", + "contacts_lastSeenDaysAgo": "В мережі {days} дн. тому", + "@contacts_lastSeenDaysAgo": { + "placeholders": { + "days": { + "type": "int" + } + } + }, + "channels_title": "Канали", + "channels_noChannelsConfigured": "Канали не налаштовані", + "channels_addPublicChannel": "Додати публічний канал", + "channels_searchChannels": "Пошук каналів...", + "channels_noChannelsFound": "Каналів не знайдено", + "channels_channelIndex": "Канал {index}", + "@channels_channelIndex": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "channels_hashtagChannel": "Канал з хештегом", + "channels_public": "Публічний", + "channels_private": "Приватний", + "channels_publicChannel": "Публічний канал", + "channels_privateChannel": "Приватний канал", + "channels_editChannel": "Редагувати канал", + "channels_deleteChannel": "Видалити канал", + "channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.", + "@channels_deleteChannelConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "channels_channelDeleted": "Канал «{name}» видалено", + "@channels_channelDeleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "channels_addChannel": "Додати канал", + "channels_channelIndexLabel": "Індекс каналу", + "channels_channelName": "Назва каналу", + "channels_usePublicChannel": "Використовувати публічний канал", + "channels_standardPublicPsk": "Стандартний публічний PSK", + "channels_pskHex": "PSK (Hex)", + "channels_generateRandomPsk": "Згенерувати випадковий ключ PSK", + "channels_enterChannelName": "Будь ласка, введіть назву каналу", + "channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.", + "channels_channelAdded": "Канал «{name}» додано", + "@channels_channelAdded": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "channels_editChannelTitle": "Редагувати канал {index}", + "@channels_editChannelTitle": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "channels_smazCompression": "Стиснення SMAZ", + "channels_channelUpdated": "Канал «{name}» оновлено", + "@channels_channelUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "channels_publicChannelAdded": "Публічний канал додано", + "channels_sortBy": "Сортувати за", + "channels_sortManual": "Вручну", + "channels_sortAZ": "А-Я", + "channels_sortLatestMessages": "Останні повідомлення", + "channels_sortUnread": "Непрочитані", + "chat_noMessages": "Поки немає повідомлень.", + "chat_sendMessageToStart": "Надішліть повідомлення, щоб почати", + "chat_originalMessageNotFound": "Оригінальне повідомлення не знайдено", + "chat_replyingTo": "Відповідь {name}", + "@chat_replyingTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chat_replyTo": "Відповісти {name}", + "@chat_replyTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "chat_location": "Розташування", + "chat_sendMessageTo": "Надіслати повідомлення {contactName}", + "@chat_sendMessageTo": { + "placeholders": { + "contactName": { + "type": "String" + } + } + }, + "chat_typeMessage": "Введіть повідомлення...", + "chat_messageTooLong": "Повідомлення занадто довге (макс. {maxBytes} байт).", + "@chat_messageTooLong": { + "placeholders": { + "maxBytes": { + "type": "int" + } + } + }, + "chat_messageCopied": "Повідомлення скопійовано", + "chat_messageDeleted": "Повідомлення видалено", + "chat_retryingMessage": "Спроба відновлення.", + "chat_retryCount": "Повторна спроба {current}/{max}", + "@chat_retryCount": { + "placeholders": { + "current": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "chat_sendGif": "Надіслати GIF", + "chat_reply": "Відповісти", + "chat_addReaction": "Додати реакцію", + "chat_me": "Я", + "emojiCategorySmileys": "Емодзі", + "emojiCategoryGestures": "Жести", + "emojiCategoryHearts": "Серця", + "emojiCategoryObjects": "Об'єкти", + "gifPicker_title": "Вибрати GIF", + "gifPicker_searchHint": "Пошук GIF...", + "gifPicker_poweredBy": "На базі GIPHY", + "gifPicker_noGifsFound": "GIF не знайдено", + "gifPicker_failedLoad": "Не вдалося завантажити GIF-файли", + "gifPicker_failedSearch": "Пошук GIF не вдався", + "gifPicker_noInternet": "Немає інтернет-з'єднання", + "debugLog_appTitle": "Журнал налагодження програми", + "debugLog_bleTitle": "Журнал налагодження BLE", + "debugLog_copyLog": "Копіювати журнал", + "debugLog_clearLog": "Очистити журнал", + "debugLog_copied": "Журнал налагодження скопійовано", + "debugLog_bleCopied": "Журнал BLE скопійовано", + "debugLog_noEntries": "Поки що немає записів журналу налагодження.", + "debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях", + "debugLog_frames": "Кадри", + "debugLog_rawLogRx": "Необроблений лог - RX", + "debugLog_noBleActivity": "Поки що немає активності BLE.", + "debugFrame_length": "Довжина кадру: {count} байт", + "@debugFrame_length": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "debugFrame_command": "Команда: 0x{value}", + "@debugFrame_command": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "debugFrame_textMessageHeader": "Повідомлення:", + "debugFrame_destinationPubKey": "- PubKey призначення: {pubKey}", + "@debugFrame_destinationPubKey": { + "placeholders": { + "pubKey": { + "type": "String" + } + } + }, + "debugFrame_timestamp": "- Мітка часу: {timestamp}", + "@debugFrame_timestamp": { + "placeholders": { + "timestamp": { + "type": "int" + } + } + }, + "debugFrame_flags": "- Прапорці: 0x{value}", + "@debugFrame_flags": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "debugFrame_textType": "- Тип тексту: {type} ({label})", + "@debugFrame_textType": { + "placeholders": { + "type": { + "type": "int" + }, + "label": { + "type": "String" + } + } + }, + "debugFrame_textTypeCli": "CLI", + "debugFrame_textTypePlain": "Звичайний", + "debugFrame_text": "- Текст: \"{text}\"", + "@debugFrame_text": { + "placeholders": { + "text": { + "type": "String" + } + } + }, + "debugFrame_hexDump": "Дамп Hex:", + "chat_pathManagement": "Керування шляхами", + "chat_routingMode": "Режим маршрутизації", + "chat_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "chat_forceFloodMode": "Примусово на всю мережу", + "chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):", + "chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.", + "chat_hopSingular": "Стрибок", + "chat_hopPlural": "стрибків", + "chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}", + "@chat_hopsCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "chat_successes": "Успішно", + "chat_removePath": "Видалити шлях", + "chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.", + "chat_pathActions": "Дії зі шляхом:", + "chat_setCustomPath": "Встановити власний шлях", + "chat_setCustomPathSubtitle": "Вказати шлях маршрутизації вручну", + "chat_clearPath": "Очистити шлях", + "chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні", + "chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.", + "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми", + "chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", + "chat_fullPath": "Повний шлях", + "chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.", + "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}", + "@chat_pathSetHops": { + "placeholders": { + "hopCount": { + "type": "int" + }, + "status": { + "type": "String" + } + } + }, + "chat_pathSavedLocally": "Збережено локально. Підключіться для синхронізації.", + "chat_pathDeviceConfirmed": "Пристрій підтверджено.", + "chat_pathDeviceNotConfirmed": "Пристрій ще не підтверджено.", + "chat_type": "Ввід", + "chat_path": "Шлях", + "chat_publicKey": "Відкритий ключ", + "chat_compressOutgoingMessages": "Стискати вихідні повідомлення", + "chat_floodForced": "На всю мережу (примусово)", + "chat_directForced": "Прямий (примусово)", + "chat_hopsForced": "{count} стрибків (примусово)", + "@chat_hopsForced": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "chat_floodAuto": "На всю мережу (авто)", + "chat_direct": "Прямий", + "chat_poiShared": "Точкою інтересу поділилися", + "chat_unread": "Непрочитано: {count}", + "@chat_unread": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "chat_openLink": "Відкрити посилання?", + "chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?", + "chat_open": "Відкрити", + "chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "chat_invalidLink": "Невірний формат посилання", + "map_title": "Карта вузлів", + "map_noNodesWithLocation": "Немає вузлів з даними про розташування", + "map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.", + "map_nodesCount": "Вузли: {count}", + "@map_nodesCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_pinsCount": "Мітки: {count}", + "@map_pinsCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "map_chat": "Чат", + "map_repeater": "Ретранслятор", + "map_room": "Кімната", + "map_sensor": "Сенсор", + "map_pinDm": "Ключ (DM)", + "map_pinPrivate": "Замок (Приватний)", + "map_pinPublic": "Ключ (Публічний)", + "map_lastSeen": "Останній раз бачили", + "map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "map_from": "Від", + "map_source": "Джерело", + "map_flags": "Прапорці", + "map_shareMarkerHere": "Поділитися маркером тут", + "map_pinLabel": "Мітка піна", + "map_label": "Мітка", + "map_pointOfInterest": "Точка інтересу", + "map_sendToContact": "Надіслати контакту", + "map_sendToChannel": "Надіслати в канал", + "map_noChannelsAvailable": "Немає доступних каналів", + "map_publicLocationShare": "Поділитися в публічному місці", + "map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.", + "@map_publicLocationShareConfirm": { + "placeholders": { + "channelLabel": { + "type": "String" + } + } + }, + "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами", + "map_filterNodes": "Фільтрувати вузли", + "map_nodeTypes": "Типи вузлів", + "map_chatNodes": "Вузли чату", + "map_repeaters": "Ретранслятори", + "map_otherNodes": "Інші вузли", + "map_keyPrefix": "Префікс ключа", + "map_filterByKeyPrefix": "Фільтрувати за префіксом ключа", + "map_publicKeyPrefix": "Префікс відкритого ключа", + "map_markers": "Маркери", + "map_showSharedMarkers": "Показувати спільні маркери", + "map_lastSeenTime": "Час останньої активності", + "map_sharedPin": "Спільний пін", + "map_joinRoom": "Приєднатися до кімнати", + "map_manageRepeater": "Керувати ретранслятором", + "mapCache_title": "Офлайн-кеш карти", + "mapCache_selectAreaFirst": "Спершу виберіть область для кешування", + "mapCache_noTilesToDownload": "Немає плиток для завантаження в цій області.", + "mapCache_downloadTilesTitle": "Завантажити плитки", + "mapCache_downloadTilesPrompt": "Завантажити {count} плиток для використання офлайн?", + "@mapCache_downloadTilesPrompt": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_downloadAction": "Завантажити", + "mapCache_cachedTiles": "Закешовано {count} плиток", + "@mapCache_cachedTiles": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_cachedTilesWithFailed": "Плитки в кеші ({downloaded}) ({failed} помилок)", + "@mapCache_cachedTilesWithFailed": { + "placeholders": { + "downloaded": { + "type": "int" + }, + "failed": { + "type": "int" + } + } + }, + "mapCache_clearOfflineCacheTitle": "Очистити офлайн-кеш", + "mapCache_clearOfflineCachePrompt": "Видалити всі закешовані плитки карти?", + "mapCache_offlineCacheCleared": "Офлайн-кеш очищено.", + "mapCache_noAreaSelected": "Область не вибрано", + "mapCache_cacheArea": "Область кешування", + "mapCache_useCurrentView": "Використати поточний вигляд", + "mapCache_zoomRange": "Діапазон масштабування", + "mapCache_estimatedTiles": "Оцінка плиток: {count}", + "@mapCache_estimatedTiles": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_downloadedTiles": "Завантажено {completed} / {total}", + "@mapCache_downloadedTiles": { + "placeholders": { + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "mapCache_downloadTilesButton": "Завантажити плитки", + "mapCache_clearCacheButton": "Очистити кеш", + "mapCache_failedDownloads": "Невдалі завантаження: {count}", + "@mapCache_failedDownloads": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", + "@mapCache_boundsLabel": { + "placeholders": { + "north": { + "type": "String" + }, + "south": { + "type": "String" + }, + "east": { + "type": "String" + }, + "west": { + "type": "String" + } + } + }, + "time_justNow": "Тільки що", + "time_minutesAgo": "{minutes} хв. тому", + "@time_minutesAgo": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "time_hoursAgo": "{hours} год. тому", + "@time_hoursAgo": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "time_daysAgo": "{days} дн. тому", + "@time_daysAgo": { + "placeholders": { + "days": { + "type": "int" + } + } + }, + "time_hour": "година", + "time_hours": "годин", + "time_day": "день", + "time_days": "днів", + "time_week": "тиждень", + "time_weeks": "тижнів", + "time_month": "місяць", + "time_months": "місяців", + "time_minutes": "хвилин", + "time_allTime": "Весь час", + "dialog_disconnect": "Відключити", + "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "login_repeaterLogin": "Вхід у ретранслятор", + "login_roomLogin": "Вхід у кімнату", + "login_password": "Пароль", + "login_enterPassword": "Введіть пароль", + "login_savePassword": "Зберегти пароль", + "login_savePasswordSubtitle": "Пароль буде надійно збережено на цьому пристрої.", + "login_repeaterDescription": "Введіть пароль ретранслятора для доступу до налаштувань та статусу.", + "login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.", + "login_routing": "Маршрутизація", + "login_routingMode": "Режим маршрутизації", + "login_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "login_forceFloodMode": "Примусово на всю мережу", + "login_managePaths": "Керувати шляхами", + "login_login": "Вхід", + "login_attempt": "Спроба {current}/{max}", + "@login_attempt": { + "placeholders": { + "current": { + "type": "int" + }, + "max": { + "type": "int" + } + } + }, + "login_failed": "Вхід не вдався: {error}", + "@login_failed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "login_failedMessage": "Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.", + "common_reload": "Перезавантажити", + "common_clear": "Очистити", + "path_currentPath": "Поточний шлях: {path}", + "@path_currentPath": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}", + "@path_usingHopsPath": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "path_enterCustomPath": "Ввести власний шлях", + "path_currentPathLabel": "Поточний шлях", + "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.", + "path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).", + "path_labelHexPrefixes": "Hex-префікси", + "path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)", + "path_selectFromContacts": "Вибрати з контактів:", + "path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.", + "path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.", + "path_invalidHexPrefixes": "Некоректні hex-префікси: {prefixes}", + "@path_invalidHexPrefixes": { + "placeholders": { + "prefixes": { + "type": "String" + } + } + }, + "path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.", + "path_setPath": "Встановити шлях", + "repeater_management": "Керування ретранслятором", + "repeater_managementTools": "Інструменти керування", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Показати статус, статистику та сусідів ретранслятора", + "repeater_telemetry": "Телеметрія", + "repeater_telemetrySubtitle": "Показати телеметрію сенсорів та статистику системи", + "repeater_cli": "CLI", + "repeater_cliSubtitle": "Надіслати команди ретранслятору", + "repeater_settings": "Налаштування", + "repeater_settingsSubtitle": "Налаштувати параметри ретранслятора", + "repeater_statusTitle": "Статус ретранслятора", + "repeater_routingMode": "Режим маршрутизації", + "repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "repeater_forceFloodMode": "Примусово на всю мережу", + "repeater_pathManagement": "Керування шляхами", + "repeater_refresh": "Оновити", + "repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.", + "repeater_errorLoadingStatus": "Помилка завантаження статусу: {error}", + "@repeater_errorLoadingStatus": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_systemInformation": "Системна інформація", + "repeater_battery": "Батарея", + "repeater_clockAtLogin": "Годинник (при вході)", + "repeater_uptime": "Час роботи", + "repeater_queueLength": "Довжина черги", + "repeater_debugFlags": "Прапорці налагодження", + "repeater_radioStatistics": "Статистика радіо", + "repeater_lastRssi": "Останній RSSI", + "repeater_lastSnr": "Останній SNR", + "repeater_noiseFloor": "Рівень шуму", + "repeater_txAirtime": "Ефірний час TX", + "repeater_rxAirtime": "Ефірний час RX", + "repeater_packetStatistics": "Статистика пакетів", + "repeater_sent": "Надіслано", + "repeater_received": "Отримано", + "repeater_duplicates": "Дублікати", + "repeater_daysHoursMinsSecs": "{days} дн. {hours} год {minutes} хв {seconds} с", + "@repeater_daysHoursMinsSecs": { + "placeholders": { + "days": { + "type": "int" + }, + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + }, + "seconds": { + "type": "int" + } + } + }, + "repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "@repeater_packetTxTotal": { + "placeholders": { + "total": { + "type": "int" + }, + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } + } + }, + "repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "@repeater_packetRxTotal": { + "placeholders": { + "total": { + "type": "int" + }, + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } + } + }, + "repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}", + "@repeater_duplicatesFloodDirect": { + "placeholders": { + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } + } + }, + "repeater_duplicatesTotal": "Всього: {total}", + "@repeater_duplicatesTotal": { + "placeholders": { + "total": { + "type": "int" + } + } + }, + "repeater_settingsTitle": "Налаштування ретранслятора", + "repeater_basicSettings": "Основні налаштування", + "repeater_repeaterName": "Ім'я ретранслятора", + "repeater_repeaterNameHelper": "Показати ім'я цього ретранслятора", + "repeater_adminPassword": "Пароль адміністратора", + "repeater_adminPasswordHelper": "Пароль повного доступу", + "repeater_guestPassword": "Гостьовий пароль", + "repeater_guestPasswordHelper": "Доступ лише для читання з паролем", + "repeater_radioSettings": "Налаштування радіо", + "repeater_frequencyMhz": "Частота (МГц)", + "repeater_frequencyHelper": "300-2500 МГц", + "repeater_txPower": "Потужність TX", + "repeater_txPowerHelper": "1-30 дБм", + "repeater_bandwidth": "Смуга пропускання", + "repeater_spreadingFactor": "Коефіцієнт розширення", + "repeater_codingRate": "Швидкість кодування", + "repeater_locationSettings": "Налаштування розташування", + "repeater_latitude": "Широта", + "repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)", + "repeater_longitude": "Довгота", + "repeater_longitudeHelper": "Десяткові градуси (наприклад, -122.4194)", + "repeater_features": "Функції", + "repeater_packetForwarding": "Пересилання пакетів", + "repeater_packetForwardingSubtitle": "Дозволити ретранслятору пересилати пакети", + "repeater_guestAccess": "Гостьовий доступ", + "repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання", + "repeater_privacyMode": "Режим приватності", + "repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", + "repeater_advertisementSettings": "Налаштування оголошень", + "repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)", + "repeater_localAdvertIntervalMinutes": "{minutes} хвилин", + "@repeater_localAdvertIntervalMinutes": { + "placeholders": { + "minutes": { + "type": "int" + } + } + }, + "repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)", + "repeater_floodAdvertIntervalHours": "{hours} годин", + "@repeater_floodAdvertIntervalHours": { + "placeholders": { + "hours": { + "type": "int" + } + } + }, + "repeater_encryptedAdvertInterval": "Інтервал зашифрованих оголошень", + "repeater_dangerZone": "Небезпечна зона", + "repeater_rebootRepeater": "Перезавантажити ретранслятор", + "repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора", + "repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?", + "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності", + "repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)", + "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?", + "repeater_eraseFileSystem": "Очистити файлову систему", + "repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора", + "repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!", + "repeater_eraseSerialOnly": "Очищення доступне лише через послідовну консоль.", + "repeater_commandSent": "Команда надіслана: {command}", + "@repeater_commandSent": { + "placeholders": { + "command": { + "type": "String" + } + } + }, + "repeater_errorSendingCommand": "Помилка надсилання команди: {error}", + "@repeater_errorSendingCommand": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_confirm": "Підтвердити", + "repeater_settingsSaved": "Налаштування успішно збережено.", + "repeater_errorSavingSettings": "Помилка збереження налаштувань: {error}", + "@repeater_errorSavingSettings": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_refreshBasicSettings": "Оновити основні налаштування", + "repeater_refreshRadioSettings": "Оновити налаштування радіо", + "repeater_refreshTxPower": "Оновити потужність TX", + "repeater_refreshLocationSettings": "Оновити налаштування розташування", + "repeater_refreshPacketForwarding": "Оновити пересилання пакетів", + "repeater_refreshGuestAccess": "Оновити гостьовий доступ", + "repeater_refreshPrivacyMode": "Оновити режим приватності", + "repeater_refreshAdvertisementSettings": "Оновити налаштування оголошень", + "repeater_refreshed": "{label} оновлено", + "@repeater_refreshed": { + "placeholders": { + "label": { + "type": "String" + } + } + }, + "repeater_errorRefreshing": "Помилка оновлення {label}", + "@repeater_errorRefreshing": { + "placeholders": { + "label": { + "type": "String" + } + } + }, + "repeater_cliTitle": "Ретранслятор CLI", + "repeater_debugNextCommand": "Налагодити наступну команду", + "repeater_commandHelp": "Довідка", + "repeater_clearHistory": "Очистити історію", + "repeater_noCommandsSent": "Команди ще не надсилалися.", + "repeater_typeCommandOrUseQuick": "Введіть команду нижче або використовуйте швидкі команди", + "repeater_enterCommandHint": "Введіть команду...", + "repeater_previousCommand": "Попередня команда", + "repeater_nextCommand": "Наступна команда", + "repeater_enterCommandFirst": "Спершу введіть команду", + "repeater_cliCommandFrameTitle": "Фрейм команди CLI", + "repeater_cliCommandError": "Помилка: {error}", + "@repeater_cliCommandError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_cliQuickGetName": "Отримати ім'я", + "repeater_cliQuickGetRadio": "Отримати Радіо", + "repeater_cliQuickGetTx": "Отримати TX", + "repeater_cliQuickNeighbors": "Сусіди", + "repeater_cliQuickVersion": "Версія", + "repeater_cliQuickAdvertise": "Оголосити", + "repeater_cliQuickClock": "Годинник", + "repeater_cliHelpAdvert": "Надсилає пакет оголошення", + "repeater_cliHelpReboot": "Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що є нормальним)", + "repeater_cliHelpClock": "Відображає поточний час за годинником кожного пристрою.", + "repeater_cliHelpPassword": "Встановлює новий пароль адміністратора для пристрою.", + "repeater_cliHelpVersion": "Відображає версію пристрою та дату збірки прошивки.", + "repeater_cliHelpClearStats": "Скидає різні лічильники статистики до нуля.", + "repeater_cliHelpSetAf": "Встановлює коефіцієнт ефірного часу.", + "repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).", + "repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.", + "repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)", + "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).", + "repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.", + "repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.", + "repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.", + "repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.", + "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.", + "repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)", + "repeater_cliHelpSetName": "Встановлює ім'я для оголошення.", + "repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)", + "repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)", + "repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.", + "repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.", + "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", + "repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.", + "repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.", + "repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.", + "repeater_cliHelpSetBridgeSource": "Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.", + "repeater_cliHelpSetBridgeBaud": "Встановити швидкість послідовного зв'язку для мостів Rs232.", + "repeater_cliHelpSetBridgeSecret": "Встановити секрет мосту для мостів espnow.", + "repeater_cliHelpSetAdcMultiplier": "Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).", + "repeater_cliHelpTempRadio": "Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).", + "repeater_cliHelpSetPerm": "Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний і його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).", + "repeater_cliHelpGetBridgeType": "Отримати тип мосту: немає, rs232, espnow", + "repeater_cliHelpLogStart": "Починає запис пакетів у файлову систему.", + "repeater_cliHelpLogStop": "Зупиняє запис пакетів у файлову систему.", + "repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.", + "repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4", + "repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.", + "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).", + "repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.", + "repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»", + "repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.", + "repeater_cliHelpRegionRemove": "Видаляє визначення регіону з заданою назвою.", + "repeater_cliHelpRegionAllowf": "Встановлює дозвіл «Flood» для заданого регіону. ('' для глобальної/успадкованої області)", + "repeater_cliHelpRegionDenyf": "Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )", + "repeater_cliHelpRegionHome": "Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)", + "repeater_cliHelpRegionHomeSet": "Встановлює «домашній» регіон.", + "repeater_cliHelpRegionSave": "Зберігає список/карту регіонів у сховищі.", + "repeater_cliHelpGps": "Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.", + "repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.", + "repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.", + "repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.", + "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях", + "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.", + "repeater_commandsListTitle": "Список команд", + "repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...", + "repeater_general": "Загальні", + "repeater_settingsCategory": "Налаштування", + "repeater_bridge": "Міст", + "repeater_logging": "Логування", + "repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)", + "repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)", + "repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.", + "repeater_gpsManagement": "Керування GPS", + "repeater_gpsNote": "Команда GPS була введена для керування питаннями, пов'язаними з локацією.", + "telemetry_receivedData": "Дані телеметрії отримано", + "telemetry_requestTimeout": "Час запиту телеметрії вичерпано.", + "telemetry_errorLoading": "Помилка завантаження телеметрії: {error}", + "@telemetry_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "telemetry_noData": "Дані телеметрії недоступні.", + "telemetry_channelTitle": "Канал {channel}", + "@telemetry_channelTitle": { + "placeholders": { + "channel": { + "type": "int" + } + } + }, + "telemetry_batteryLabel": "Батарея", + "telemetry_voltageLabel": "Напруга", + "telemetry_mcuTemperatureLabel": "Температура MCU", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Поточний струм", + "telemetry_batteryValue": "{percent}% / {volts}В", + "@telemetry_batteryValue": { + "placeholders": { + "percent": { + "type": "int" + }, + "volts": { + "type": "String" + } + } + }, + "telemetry_voltageValue": "{volts}В", + "@telemetry_voltageValue": { + "placeholders": { + "volts": { + "type": "String" + } + } + }, + "telemetry_currentValue": "{amps}А", + "@telemetry_currentValue": { + "placeholders": { + "amps": { + "type": "String" + } + } + }, + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "@telemetry_temperatureValue": { + "placeholders": { + "celsius": { + "type": "String" + }, + "fahrenheit": { + "type": "String" + } + } + }, + "channelPath_title": "Шлях пакету", + "channelPath_viewMap": "Показати карту", + "channelPath_otherObservedPaths": "Інші спостережувані шляхи", + "channelPath_repeaterHops": "Стрибки ретранслятора", + "channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.", + "channelPath_messageDetails": "Деталі повідомлення", + "channelPath_senderLabel": "Відправник", + "channelPath_timeLabel": "Час", + "channelPath_repeatsLabel": "Повторення", + "channelPath_pathLabel": "Шлях {index}", + "channelPath_observedLabel": "Спостережено", + "channelPath_observedPathTitle": "Спостережуваний шлях {index} • {hops}", + "@channelPath_observedPathTitle": { + "placeholders": { + "index": { + "type": "int" + }, + "hops": { + "type": "String" + } + } + }, + "channelPath_noLocationData": "Немає даних про розташування", + "channelPath_timeWithDate": "{day}/{month} {time}", + "@channelPath_timeWithDate": { + "placeholders": { + "day": { + "type": "int" + }, + "month": { + "type": "int" + }, + "time": { + "type": "String" + } + } + }, + "channelPath_timeOnly": "{time}", + "@channelPath_timeOnly": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "channelPath_unknownPath": "Невідомий", + "channelPath_floodPath": "На всю мережу", + "channelPath_directPath": "Прямий", + "channelPath_observedZeroOf": "0 з {total} стрибків", + "@channelPath_observedZeroOf": { + "placeholders": { + "total": { + "type": "int" + } + } + }, + "channelPath_observedSomeOf": "{observed} з {total} стрибків", + "@channelPath_observedSomeOf": { + "placeholders": { + "observed": { + "type": "int" + }, + "total": { + "type": "int" + } + } + }, + "channelPath_mapTitle": "Карта шляху", + "channelPath_noRepeaterLocations": "Позиції ретрансляторів недоступні для цього шляху.", + "channelPath_primaryPath": "Шлях {index} (Основний)", + "@channelPath_primaryPath": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "@channelPath_pathLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "channelPath_pathLabelTitle": "Шлях", + "channelPath_observedPathHeader": "Спостережуваний шлях", + "channelPath_selectedPathLabel": "{label} • {prefixes}", + "@channelPath_selectedPathLabel": { + "placeholders": { + "label": { + "type": "String" + }, + "prefixes": { + "type": "String" + } + } + }, + "channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.", + "channelPath_unknownRepeater": "Невідомий ретранслятор", + "listFilter_tooltip": "Фільтр та сортування", + "listFilter_sortBy": "Сортувати за", + "listFilter_latestMessages": "Останні повідомлення", + "listFilter_heardRecently": "Нещодавно чули", + "listFilter_az": "А-Я", + "listFilter_filters": "Фільтри", + "listFilter_all": "Все", + "listFilter_users": "Користувачі", + "listFilter_repeaters": "Ретранслятори", + "listFilter_roomServers": "Сервери кімнат", + "listFilter_unreadOnly": "Тільки непрочитані повідомлення", + "listFilter_newGroup": "Нова група", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Сусіди", + "repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.", + "neighbors_receivedData": "Дані сусідів отримано", + "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", + "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", + "neighbors_repeatersNeighbours": "Ретранслятори-сусіди", + "neighbors_noData": "Дані про сусідів недоступні.", + "channels_createPrivateChannelDesc": "Захищено секретним ключем.", + "channels_joinPrivateChannel": "Приєднатися до приватного каналу", + "channels_createPrivateChannel": "Створити приватний канал", + "channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.", + "channels_joinPublicChannel": "Приєднатися до публічного каналу", + "channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.", + "channels_joinHashtagChannel": "Приєднатися до каналу з хештегом", + "channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.", + "channels_scanQrCode": "Сканувати QR-код", + "channels_scanQrCodeComingSoon": "Скоро буде", + "channels_enterHashtag": "Введіть хештег", + "channels_hashtagHint": "напр. #команда", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}", + "neighbors_heardAgo": "Почуто: {time} тому", + "settings_locationGPSEnable": "Увімкнути GPS", + "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.", + "settings_locationIntervalSec": "Інтервал для GPS (Секунди)", + "settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.", + "contacts_manageRoom": "Керувати сервером кімнати", + "room_management": "Адміністрування сервера кімнати", + "@community_joinConfirmation": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_created": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_joined": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_qrInstructions": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_alreadyMemberMessage": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_deleteConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_deleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_forCommunity": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "common_ok": "ОК", + "community_title": "Спільнота", + "community_create": "Створити спільноту", + "community_createDesc": "Створити нову спільноту та поділитися через QR-код.", + "community_join": "Приєднатися", + "community_joinTitle": "Приєднатися до спільноти", + "community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?", + "community_scanQr": "Сканувати QR спільноти", + "community_scanInstructions": "Наведіть камеру на QR-код спільноти.", + "community_showQr": "Показати QR-код", + "community_publicChannel": "Публічна спільнота", + "community_hashtagChannel": "Хештег спільноти", + "community_name": "Назва спільноти", + "community_enterName": "Введіть назву спільноти", + "community_created": "Спільноту «{name}» створено", + "community_joined": "Приєднався до спільноти «{name}»", + "community_qrTitle": "Поділитися спільнотою", + "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}", + "community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти", + "community_invalidQrCode": "Недійсний QR-код спільноти", + "community_alreadyMember": "Вже учасник", + "community_alreadyMemberMessage": "Ви вже є учасником «{name}».", + "community_addPublicChannel": "Додати публічний канал спільноти", + "community_addPublicChannelHint": "Автоматично додати публічний канал для цієї спільноти", + "community_noCommunities": "Поки не приєднано до жодної групи.", + "community_scanOrCreate": "Відскануйте QR-код або створіть спільноту, щоб почати", + "community_manageCommunities": "Керувати спільнотами", + "community_delete": "Покинути спільноту", + "community_deleteConfirm": "Покинути «{name}»?", + "community_deleteChannelsWarning": "Це також видалить {count} {count, plural, =1{канал} few{канали} many{каналів} other{каналів}} та їх повідомлення.", + "@community_deleteChannelsWarning": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "community_deleted": "Спільноту «{name}» покинуто", + "community_addHashtagChannel": "Додати хештег спільноти", + "community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти", + "community_selectCommunity": "Вибрати спільноту", + "community_regularHashtag": "Звичайний хештег", + "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)", + "community_communityHashtag": "Хештег спільноти", + "community_communityHashtagDesc": "Ексклюзивно для членів спільноти", + "community_forCommunity": "Для {name}", + "@community_regenerateSecretConfirm": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretRegenerated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_secretUpdated": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "@community_scanToUpdateSecret": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "Перегенерувати секрет", + "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.", + "community_regenerate": "Перегенерувати", + "community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", + "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", + "community_updateSecret": "Оновити секрет", + "community_secretUpdated": "Зміну секрету для «{name}» оновлено" +} \ No newline at end of file From 2089613696641f1bf1ab299c1e8327f1070a61bf Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 22 Jan 2026 23:42:10 -0800 Subject: [PATCH 014/421] Added the basics for path tracing --- lib/connector/meshcore_protocol.dart | 17 ++++++++++++++ lib/screens/contacts_screen.dart | 30 +++++++++++++++++++++++-- lib/services/ble_debug_log_service.dart | 4 ++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index f9241e8..a4faf0e 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -127,6 +127,7 @@ const int cmdSendStatusReq = 27; const int cmdGetContactByKey = 30; const int cmdGetChannel = 31; const int cmdSetChannel = 32; +const int cmdSendTracePath = 36; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; @@ -176,6 +177,7 @@ const int pushCodeLoginSuccess = 0x85; const int pushCodeLoginFail = 0x86; const int pushCodeStatusResponse = 0x87; const int pushCodeLogRxData = 0x88; +const int pushCodeTraceData = 0x89; const int pushCodeNewAdvert = 0x8A; const int pushCodeTelemetryResponse = 0x8B; const int pushCodeBinaryResponse = 0x8C; @@ -708,3 +710,18 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { } return writer.toBytes(); } + +//Build a trace request frame +//[cmd][tag x4][auth x4][flag][payload] +Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) +{ + final writer = BufferWriter(); + writer.writeByte(cmdSendTracePath); + writer.writeUInt32LE(tag); + writer.writeUInt32LE(auth); + writer.writeByte(flag); + if (payload != null && payload.isNotEmpty) { + writer.writeBytes(payload); + } + return writer.toBytes(); +} \ No newline at end of file diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index e91cd94..02faff5 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -752,7 +752,20 @@ class _ContactsScreenState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (isRepeater) + if (isRepeater) ...[ + ListTile( + leading: const Icon(Icons.radar, color: Colors.green), + title: Text("Ping"), + onTap: () async { + final frame = buildTraceReq( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + 0, + 0, + payload: contact.publicKey.sublist(0,1), + ); + await connector.sendFrame(frame); + } + ), ListTile( leading: const Icon(Icons.cell_tower, color: Colors.orange), title: Text(context.l10n.contacts_manageRepeater), @@ -761,7 +774,20 @@ class _ContactsScreenState extends State _showRepeaterLogin(context, contact); }, ) - else if (isRoom) ...[ + ]else if (isRoom) ...[ + ListTile( + leading: const Icon(Icons.radar, color: Colors.green), + title: Text("Ping"), + onTap: () async { + final frame = buildTraceReq( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + 0, + 0, + payload: contact.publicKey.sublist(0,1), + ); + await connector.sendFrame(frame); + } + ), ListTile( leading: const Icon(Icons.room, color: Colors.blue), title: Text(context.l10n.contacts_roomLogin), diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index a53ad5d..07ac689 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -156,6 +156,8 @@ class BleDebugLogService extends ChangeNotifier { return 'CMD_GET_RADIO_SETTINGS'; case cmdSetCustomVar: return 'CMD_SET_CUSTOM_VAR'; + case cmdSendTracePath: + return 'CMD_SEND_TRACE_PATH'; default: return null; } @@ -195,6 +197,8 @@ class BleDebugLogService extends ChangeNotifier { return 'RESP_CODE_CHANNEL_INFO'; case respCodeRadioSettings: return 'RESP_CODE_RADIO_SETTINGS'; + case pushCodeTraceData: + return 'PUSH_CODE_TRACE_DATA'; default: return null; } From 75356fe20d8c079accc0e5ea6b02dab3a41ba010 Mon Sep 17 00:00:00 2001 From: anupoh <41981106+anupoh@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:58:16 +0700 Subject: [PATCH 015/421] Russian translation for the app I've prepared the Russian localization files for the app. It would be great if localization were included in the app. Thanx a lot! --- lib/l10n/app_localizations_ru.dart | 2626 ++++++++++++++++++++++++++++ lib/l10n/app_ru.arb | 761 ++++++++ 2 files changed, 3387 insertions(+) create mode 100644 lib/l10n/app_localizations_ru.dart create mode 100644 lib/l10n/app_ru.arb diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart new file mode 100644 index 0000000..b1e6c1f --- /dev/null +++ b/lib/l10n/app_localizations_ru.dart @@ -0,0 +1,2626 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; +// ignore_for_file: type=lint +/// The translations for Russian (`ru`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get appTitle => 'MeshCore Open'; + + @override + String get nav_contacts => 'Контакты'; + + @override + String get nav_channels => 'Каналы'; + + @override + String get nav_map => 'Карта'; + + @override + String get common_cancel => 'Отмена'; + + @override + String get common_ok => 'OK'; + + @override + String get common_connect => 'Коннект'; + + @override + String get common_unknownDevice => 'Неизвестное устройство'; + + @override + String get common_save => 'Сохранить'; + + @override + String get common_delete => 'Удалить'; + + @override + String get common_close => 'Закрыть'; + + @override + String get common_edit => 'Изменить'; + + @override + String get common_add => 'Добавить'; + + @override + String get common_settings => 'Настройки'; + + @override + String get common_disconnect => 'Отключить'; + + @override + String get common_connected => 'Подключено'; + + @override + String get common_disconnected => 'Отключено'; + + @override + String get common_create => 'Создать'; + + @override + String get common_continue => 'Продолжить'; + + @override + String get common_share => 'Поделиться'; + + @override + String get common_copy => 'Копировать'; + + @override + String get common_retry => 'Повторить'; + + @override + String get common_hide => 'Скрыть'; + + @override + String get common_remove => 'Убрать'; + + @override + String get common_enable => 'Включить'; + + @override + String get common_disable => 'Выключить'; + + @override + String get common_reboot => 'Перезагрузить'; + + @override + String get common_loading => 'Загрузка...'; + + @override + String get common_notAvailable => '—'; + + @override + String common_voltageValue(String volts) { + return '$volts В'; +} + + @override + String common_percentValue(int percent) { + return '$percent%'; +} + + @override + String get scanner_title => 'MeshCore Open'; + + @override + String get scanner_scanning => 'Поиск устройств...'; + + @override + String get scanner_connecting => 'Подключение...'; + + @override + String get scanner_disconnecting => 'Отключение...'; + + @override + String get scanner_notConnected => 'Не подключено'; + + @override + String scanner_connectedTo(String deviceName) { + return 'Подключено к $deviceName'; +} + + @override + String get scanner_searchingDevices => 'Поиск устройств MeshCore...'; + + @override + String get scanner_tapToScan => 'Нажмите для поиска MeshCore устройств'; + + @override + String scanner_connectionFailed(String error) { + return 'Подключение не удалось: $error'; +} + + @override + String get scanner_stop => 'Стоп'; + + @override + String get scanner_scan => 'Сканирование'; + + @override + String get device_quickSwitch => 'Быстрое переключение'; + + @override + String get device_meshcore => 'MeshCore'; + + @override + String get settings_title => 'Настройки'; + + @override + String get settings_deviceInfo => 'Информация об устройстве'; + + @override + String get settings_appSettings => 'Настройки приложения'; + + @override + String get settings_appSettingsSubtitle => + 'Уведомления, сообщения и настройки карты'; + + @override + String get settings_nodeSettings => 'Настройки ноды'; + + @override + String get settings_nodeName => 'Имя ноды'; + + @override + String get settings_nodeNameNotSet => 'Не установлено'; + + @override + String get settings_nodeNameHint => 'Введите имя ноды'; + + @override + String get settings_nodeNameUpdated => 'Имя обновлено'; + + @override + String get settings_radioSettings => 'Настройки радио'; + + @override + String get settings_radioSettingsSubtitle => + 'Частота, мощность и коэффициент распространения'; + + @override + String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; + + @override + String get settings_location => 'Позиция'; + + @override + String get settings_locationSubtitle => 'Координаты GPS'; + + @override + String get settings_locationUpdated => 'Позиция и настройки GPS обновлены'; + + @override + String get settings_locationBothRequired => + 'Введите широту и долготу.'; + + @override + String get settings_locationInvalid => 'Неверная широта или долгота.'; + + @override + String get settings_locationGPSEnable => 'Включить GPS'; + + @override + String get settings_locationGPSEnableSubtitle => + 'Включение GPS для автоматического обновления позиции.'; + + @override + String get settings_locationIntervalSec => 'Интервал для позиционирования GPS (секунды)'; + + @override + String get settings_locationIntervalInvalid => + 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; + + @override + String get settings_latitude => 'Широта'; + + @override + String get settings_longitude => 'Долгота'; + + @override + String get settings_privacyMode => 'Режим конфиденциальности'; + + @override + String get settings_privacyModeSubtitle => + 'Скрыть имя/позицию в анонсировании'; + + @override + String get settings_privacyModeToggle => + 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; + + @override + String get settings_privacyModeEnabled => 'Режим конфиденциальности включен'; + + @override + String get settings_privacyModeDisabled => 'Режим конфиденциальности выключен'; + + @override + String get settings_actions => 'Действия'; + + @override + String get settings_sendAdvertisement => 'Отправить анонсирование'; + + @override + String get settings_sendAdvertisementSubtitle => 'Отправить анонсирование о присутствии сейчас'; + + @override + String get settings_advertisementSent => 'Анонсирование отправлено'; + + @override + String get settings_syncTime => 'Синхронизация времени'; + + @override + String get settings_syncTimeSubtitle => 'Синхронизировать время с телефоном'; + + @override + String get settings_timeSynchronized => 'Время синхронизировано'; + + @override + String get settings_refreshContacts => 'Обновить контакты'; + + @override + String get settings_refreshContactsSubtitle => + 'Перезагрузить список контактов с устройства'; + + @override + String get settings_rebootDevice => 'Перезагрузить устройство'; + + @override + String get settings_rebootDeviceSubtitle => 'Перезапустить устройство MeshCore'; + + @override + String get settings_rebootDeviceConfirm => + 'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.'; + + @override + String get settings_debug => 'Отладка'; + + @override + String get settings_bleDebugLog => 'Журнал отладки BLE'; + + @override + String get settings_bleDebugLogSubtitle => + 'Команды BLE, ответы и сырые данные'; + + @override + String get settings_appDebugLog => 'Журнал отладки приложения'; + + @override + String get settings_appDebugLogSubtitle => 'Сообщения отладки приложения'; + + @override + String get settings_about => 'О программе'; + + @override + String settings_aboutVersion(String version) { + return 'MeshCore Open v$version'; +} + + @override + String get settings_aboutLegalese => '2026 MeshCore Open Source Project'; + + @override + String get settings_aboutDescription => + 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; + + @override + String get settings_infoName => 'Имя'; + + @override + String get settings_infoId => 'ID'; + + @override + String get settings_infoStatus => 'Статус'; + + @override + String get settings_infoBattery => 'Батарея'; + + @override + String get settings_infoPublicKey => 'Публичный ключ'; + + @override + String get settings_infoContactsCount => 'Количество контактов'; + + @override + String get settings_infoChannelCount => 'Количество каналов'; + + @override + String get settings_presets => 'Пресеты'; + + @override + String get settings_preset915Mhz => '915 МГц'; + + @override + String get settings_preset868Mhz => '868 МГц'; + + @override + String get settings_preset433Mhz => '433 МГц'; + + @override + String get settings_frequency => 'Частота (МГц)'; + + @override + String get settings_frequencyHelper => '300.0 – 2500.0'; + + @override + String get settings_frequencyInvalid => 'Недопустимая частота (300–2500 МГц)'; + + @override + String get settings_bandwidth => 'Полоса пропускания'; + + @override + String get settings_spreadingFactor => 'Коэффициент расширения'; + + @override + String get settings_codingRate => 'Коэффициент кодирования'; + + @override + String get settings_txPower => 'Мощность передачи (дБм)'; + + @override + String get settings_txPowerHelper => '0 – 22'; + + @override + String get settings_txPowerInvalid => 'Недопустимая мощность передачи (0–22 дБм)'; + + @override + String get settings_longRange => 'Дальний радиус'; + + @override + String get settings_fastSpeed => 'Высокая скорость'; + + @override + String settings_error(String message) { + return 'Ошибка: $message'; +} + + @override + String get appSettings_title => 'Настройки приложения'; + + @override + String get appSettings_appearance => 'Внешний вид'; + + @override + String get appSettings_theme => 'Тема'; + + @override + String get appSettings_themeSystem => 'Как в системе'; + + @override + String get appSettings_themeLight => 'Светлая'; + + @override + String get appSettings_themeDark => 'Тёмная'; + + @override + String get appSettings_language => 'Язык'; + + @override + String get appSettings_languageSystem => 'Как в системе'; + + @override + String get appSettings_languageEn => 'Английский'; + + @override + String get appSettings_languageFr => 'Французский'; + + @override + String get appSettings_languageEs => 'Испанский'; + + @override + String get appSettings_languageDe => 'Немецкий'; + + @override + String get appSettings_languagePl => 'Польский'; + + @override + String get appSettings_languageSl => 'Словенский'; + + @override + String get appSettings_languagePt => 'Португальский'; + + @override + String get appSettings_languageIt => 'Итальянский'; + + @override + String get appSettings_languageZh => 'Китайский'; + + @override + String get appSettings_languageSv => 'Шведский'; + + @override + String get appSettings_languageNl => 'Нидерландский'; + + @override + String get appSettings_languageSk => 'Словацкий'; + + @override + String get appSettings_languageBg => 'Болгарский'; + + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_notifications => 'Уведомления'; + + @override + String get appSettings_enableNotifications => 'Включить уведомления'; + + @override + String get appSettings_enableNotificationsSubtitle => + 'Получать уведомления о сообщениях и оповещениях'; + + @override + String get appSettings_notificationPermissionDenied => + 'Разрешение на уведомления отклонено'; + + @override + String get appSettings_notificationsEnabled => 'Уведомления включены'; + + @override + String get appSettings_notificationsDisabled => 'Уведомления отключены'; + + @override + String get appSettings_messageNotifications => 'Уведомления о сообщениях'; + + @override + String get appSettings_messageNotificationsSubtitle => + 'Показывать уведомление при получении новых сообщений'; + + @override + String get appSettings_channelMessageNotifications => + 'Уведомления о сообщениях в каналах'; + + @override + String get appSettings_channelMessageNotificationsSubtitle => + 'Показывать уведомление при получении сообщений в каналах'; + + @override + String get appSettings_advertisementNotifications => + 'Уведомления об анонсированиях'; + + @override + String get appSettings_advertisementNotificationsSubtitle => + 'Показывать уведомление при обнаружении новых нод'; + + @override + String get appSettings_messaging => 'Обмен сообщениями'; + + @override + String get appSettings_clearPathOnMaxRetry => 'Сбросить маршрут после максимального числа попыток'; + + @override + String get appSettings_clearPathOnMaxRetrySubtitle => + 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; + + @override + String get appSettings_pathsWillBeCleared => + 'Маршруты будут сброшены после 5 неудачных попыток'; + + @override + String get appSettings_pathsWillNotBeCleared => + 'Маршруты не будут автоматически сбрасываться'; + + @override + String get appSettings_autoRouteRotation => 'Автоматическое переключение маршрутов'; + + @override + String get appSettings_autoRouteRotationSubtitle => + 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; + + @override + String get appSettings_autoRouteRotationEnabled => + 'Автоматическое переключение маршрутов включено'; + + @override + String get appSettings_autoRouteRotationDisabled => + 'Автоматическое переключение маршрутов отключено'; + + @override + String get appSettings_battery => 'Батарея'; + + @override + String get appSettings_batteryChemistry => 'Химия батареи'; + + @override + String appSettings_batteryChemistryPerDevice(String deviceName) { + return 'Установить для устройства ($deviceName)'; +} + + @override + String get appSettings_batteryChemistryConnectFirst => + 'Подключитесь к устройству, чтобы выбрать'; + + @override + String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 В)'; + + @override + String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 В)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; + + @override + String get appSettings_mapDisplay => 'Отображение карты'; + + @override + String get appSettings_showRepeaters => 'Показывать репитеры'; + + @override + String get appSettings_showRepeatersSubtitle => + 'Отображать репитеры на карте'; + + @override + String get appSettings_showChatNodes => 'Показывать чат-ноды'; + + @override + String get appSettings_showChatNodesSubtitle => + 'Отображать чат-ноды на карте'; + + @override + String get appSettings_showOtherNodes => 'Показывать другие ноды'; + + @override + String get appSettings_showOtherNodesSubtitle => + 'Отображать другие типы нод на карте'; + + @override + String get appSettings_timeFilter => 'Фильтр по времени'; + + @override + String get appSettings_timeFilterShowAll => 'Показывать все ноды'; + + @override + String appSettings_timeFilterShowLast(int hours) { + return 'Показывать ноды за последние $hours ч'; +} + + @override + String get appSettings_mapTimeFilter => 'Временной фильтр карты'; + + @override + String get appSettings_showNodesDiscoveredWithin => + 'Показывать ноды, обнаруженные за:'; + + @override + String get appSettings_allTime => 'Всё время'; + + @override + String get appSettings_lastHour => 'Последний час'; + + @override + String get appSettings_last6Hours => 'Последние 6 часов'; + + @override + String get appSettings_last24Hours => 'Последние 24 часа'; + + @override + String get appSettings_lastWeek => 'Последнюю неделю'; + + @override + String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; + + @override + String get appSettings_noAreaSelected => 'Область не выбрана'; + + @override + String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { + return 'Область выбрана (масштаб $minZoom–$maxZoom)'; +} + + @override + String get appSettings_debugCard => 'Отладка'; + + @override + String get appSettings_appDebugLogging => 'Журнал отладки приложения'; + + @override + String get appSettings_appDebugLoggingSubtitle => + 'Записывать отладочные сообщения приложения для диагностики'; + + @override + String get appSettings_appDebugLoggingEnabled => 'Журнал отладки приложения включён'; + + @override + String get appSettings_appDebugLoggingDisabled => + 'Журнал отладки приложения отключён'; + + @override + String get contacts_title => 'Контакты'; + + @override + String get contacts_noContacts => 'Контактов пока нет'; + + @override + String get contacts_contactsWillAppear => + 'Контакты появятся, когда устройства начнут рассылать оповещения'; + + @override + String get contacts_searchContacts => 'Поиск контактов...'; + + @override + String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; + + @override + String get contacts_noContactsFound => 'Контакты или группы не найдены'; + + @override + String get contacts_deleteContact => 'Удалить контакт'; + + @override + String contacts_removeConfirm(String contactName) { + return 'Удалить $contactName из контактов?'; +} + + @override + String get contacts_manageRepeater => 'Управление репитером'; + + @override + String get contacts_manageRoom => 'Управление сервером комнат'; + + @override + String get contacts_roomLogin => 'Вход на сервер комнат'; + + @override + String get contacts_openChat => 'Открыть чат'; + + @override + String get contacts_editGroup => 'Изменить группу'; + + @override + String get contacts_deleteGroup => 'Удалить группу'; + + @override + String contacts_deleteGroupConfirm(String groupName) { + return 'Удалить \"$groupName\"?'; +} + + @override + String get contacts_newGroup => 'Новая группа'; + + @override + String get contacts_groupName => 'Имя группы'; + + @override + String get contacts_groupNameRequired => 'Имя группы обязательно'; + + @override + String contacts_groupAlreadyExists(String name) { + return 'Группа \"$name\" уже существует'; +} + + @override + String get contacts_filterContacts => 'Фильтр контактов...'; + + @override + String get contacts_noContactsMatchFilter => 'Нет контактов, соответствующих фильтру'; + + @override + String get contacts_noMembers => 'Нет участников'; + + @override + String get contacts_lastSeenNow => 'Видели только что'; + + @override + String contacts_lastSeenMinsAgo(int minutes) { + return 'Видели $minutes мин назад'; +} + + @override + String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; + + @override + String contacts_lastSeenHoursAgo(int hours) { + return 'Видели $hours ч назад'; +} + + @override + String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; + + @override + String contacts_lastSeenDaysAgo(int days) { + return 'Видели $days дн. назад'; +} + + @override + String get channels_title => 'Каналы'; + + @override + String get channels_noChannelsConfigured => 'Каналы не настроены'; + + @override + String get channels_addPublicChannel => 'Добавить публичный канал'; + + @override + String get channels_searchChannels => 'Поиск каналов...'; + + @override + String get channels_noChannelsFound => 'Каналы не найдены'; + + @override + String channels_channelIndex(int index) { + return 'Канал $index'; +} + + @override + String get channels_hashtagChannel => 'Хэштег-канал'; + + @override + String get channels_public => 'Публичный'; + + @override + String get channels_private => 'Приватный'; + + @override + String get channels_publicChannel => 'Публичный канал'; + + @override + String get channels_privateChannel => 'Приватный канал'; + + @override + String get channels_editChannel => 'Изменить канал'; + + @override + String get channels_deleteChannel => 'Удалить канал'; + + @override + String channels_deleteChannelConfirm(String name) { + return 'Удалить \"$name\"? Это действие нельзя отменить.'; +} + + @override + String channels_channelDeleted(String name) { + return 'Канал \"$name\" удалён'; +} + + @override + String get channels_addChannel => 'Добавить канал'; + + @override + String get channels_channelIndexLabel => 'Индекс канала'; + + @override + String get channels_channelName => 'Имя канала'; + + @override + String get channels_usePublicChannel => 'Использовать публичный канал'; + + @override + String get channels_standardPublicPsk => 'Стандартный публичный PSK'; + + @override + String get channels_pskHex => 'PSK (Hex)'; + + @override + String get channels_generateRandomPsk => 'Сгенерировать случайный PSK'; + + @override + String get channels_enterChannelName => 'Введите имя канала'; + + @override + String get channels_pskMustBe32Hex => 'PSK должен содержать 32 шестнадцатеричных символа'; + + @override + String channels_channelAdded(String name) { + return 'Канал \"$name\" добавлен'; +} + + @override + String channels_editChannelTitle(int index) { + return 'Изменить канал $index'; +} + + @override + String get channels_smazCompression => 'Сжатие SMAZ'; + + @override + String channels_channelUpdated(String name) { + return 'Канал \"$name\" обновлён'; +} + + @override + String get channels_publicChannelAdded => 'Публичный канал добавлен'; + + @override + String get channels_sortBy => 'Сортировка'; + + @override + String get channels_sortManual => 'Вручную'; + + @override + String get channels_sortAZ => 'По алфавиту'; + + @override + String get channels_sortLatestMessages => 'По последним сообщениям'; + + @override + String get channels_sortUnread => 'По непрочитанным'; + + @override + String get channels_createPrivateChannel => 'Создать приватный канал'; + + @override + String get channels_createPrivateChannelDesc => 'Защищён секретным ключом.'; + + @override + String get channels_joinPrivateChannel => 'Присоединиться к приватному каналу'; + + @override + String get channels_joinPrivateChannelDesc => 'Введите секретный ключ вручную.'; + + @override + String get channels_joinPublicChannel => 'Присоединиться к публичному каналу'; + + @override + String get channels_joinPublicChannelDesc => 'К этому каналу может присоединиться любой.'; + + @override + String get channels_joinHashtagChannel => 'Присоединиться к хэштег-каналу'; + + @override + String get channels_joinHashtagChannelDesc => + 'К хэштег-каналам может присоединиться любой.'; + + @override + String get channels_scanQrCode => 'Сканировать QR-код'; + + @override + String get channels_scanQrCodeComingSoon => 'Скоро будет'; + + @override + String get channels_enterHashtag => 'Введите хэштег'; + + @override + String get channels_hashtagHint => 'например, #команда'; + + @override + String get chat_noMessages => 'Сообщений пока нет'; + + @override + String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; + + @override + String get chat_originalMessageNotFound => 'Исходное сообщение не найдено'; + + @override + String chat_replyingTo(String name) { + return 'Ответ для $name'; +} + + @override + String chat_replyTo(String name) { + return 'Ответить $name'; +} + + @override + String get chat_location => 'Местоположение'; + + @override + String chat_sendMessageTo(String contactName) { + return 'Отправить сообщение $contactName'; +} + + @override + String get chat_typeMessage => 'Напишите сообщение...'; + + @override + String chat_messageTooLong(int maxBytes) { + return 'Сообщение слишком длинное (макс. $maxBytes байт).'; +} + + @override + String get chat_messageCopied => 'Сообщение скопировано'; + + @override + String get chat_messageDeleted => 'Сообщение удалено'; + + @override + String get chat_retryingMessage => 'Повтор отправки сообщения'; + + @override + String chat_retryCount(int current, int max) { + return 'Попытка $current/$max'; +} + + @override + String get chat_sendGif => 'Отправить GIF'; + + @override + String get chat_reply => 'Ответить'; + + @override + String get chat_addReaction => 'Добавить реакцию'; + + @override + String get chat_me => 'Я'; + + @override + String get emojiCategorySmileys => 'Смайлы'; + + @override + String get emojiCategoryGestures => 'Жесты'; + + @override + String get emojiCategoryHearts => 'Сердечки'; + + @override + String get emojiCategoryObjects => 'Предметы'; + + @override + String get gifPicker_title => 'Выберите GIF'; + + @override + String get gifPicker_searchHint => 'Поиск GIF...'; + + @override + String get gifPicker_poweredBy => 'Работает на GIPHY'; + + @override + String get gifPicker_noGifsFound => 'GIF не найдены'; + + @override + String get gifPicker_failedLoad => 'Не удалось загрузить GIF'; + + @override + String get gifPicker_failedSearch => 'Не удалось выполнить поиск GIF'; + + @override + String get gifPicker_noInternet => 'Нет подключения к интернету'; + + @override + String get debugLog_appTitle => 'Журнал отладки приложения'; + + @override + String get debugLog_bleTitle => 'Журнал отладки BLE'; + + @override + String get debugLog_copyLog => 'Копировать журнал'; + + @override + String get debugLog_clearLog => 'Очистить журнал'; + + @override + String get debugLog_copied => 'Журнал отладки скопирован'; + + @override + String get debugLog_bleCopied => 'Журнал BLE скопирован'; + + @override + String get debugLog_noEntries => 'Журнал отладки пока пуст'; + + @override + String get debugLog_enableInSettings => + 'Включите запись журнала отладки в настройках'; + + @override + String get debugLog_frames => 'Фреймы'; + + @override + String get debugLog_rawLogRx => 'Сырой журнал приёма'; + + @override + String get debugLog_noBleActivity => 'Активность BLE пока отсутствует'; + + @override + String debugFrame_length(int count) { + return 'Длина фрейма: $count байт'; +} + + @override + String debugFrame_command(String value) { + return 'Команда: 0x$value'; +} + + @override + String get debugFrame_textMessageHeader => 'Фрейм текстового сообщения:'; + + @override + String debugFrame_destinationPubKey(String pubKey) { + return '- Публичный ключ получателя: $pubKey'; +} + + @override + String debugFrame_timestamp(int timestamp) { + return '- Временная метка: $timestamp'; +} + + @override + String debugFrame_flags(String value) { + return '- Флаги: 0x$value'; +} + + @override + String debugFrame_textType(int type, String label) { + return '- Тип текста: $type ($label)'; +} + + @override + String get debugFrame_textTypeCli => 'CLI'; + + @override + String get debugFrame_textTypePlain => 'Обычный'; + + @override + String debugFrame_text(String text) { + return '- Текст: \"$text\"'; +} + + @override + String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; + + @override + String get chat_pathManagement => 'Управление маршрутами'; + + @override + String get chat_routingMode => 'Режим маршрутизации'; + + @override + String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; + + @override + String get chat_forceFloodMode => 'Принудительный режим рассылки'; + + @override + String get chat_recentAckPaths => 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; + + @override + String get chat_pathHistoryFull => + 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; + + @override + String get chat_hopSingular => 'хоп'; + + @override + String get chat_hopPlural => 'хопов'; + + @override + String chat_hopsCount(int count) { + String _temp0 = intl.Intl.pluralLogic( +count, +locale: localeName, +other: 'хопов', +one: 'хоп', +); + return '$count $_temp0'; +} + + @override + String get chat_successes => 'успешно'; + + @override + String get chat_removePath => 'Удалить маршрут'; + + @override + String get chat_noPathHistoryYet => + 'История маршрутов пока пуста. +Отправьте сообщение, чтобы обнаружить маршруты.'; + + @override + String get chat_pathActions => 'Действия с маршрутом:'; + + @override + String get chat_setCustomPath => 'Указать маршрут вручную'; + + @override + String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи'; + + @override + String get chat_clearPath => 'Очистить маршрут'; + + @override + String get chat_clearPathSubtitle => 'Принудительно обновить маршрут при следующей отправке'; + + @override + String get chat_pathCleared => + 'Маршрут очищен. Следующее сообщение обновит маршрут.'; + + @override + String get chat_floodModeSubtitle => 'Используйте переключатель маршрутизации в панели приложения'; + + @override + String get chat_floodModeEnabled => + 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; + + @override + String get chat_fullPath => 'Полный маршрут'; + + @override + String get chat_pathDetailsNotAvailable => + 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; + + @override + String chat_pathSetHops(int hopCount, String status) { + String _temp0 = intl.Intl.pluralLogic( +hopCount, +locale: localeName, +other: 'хопов', +one: 'хоп', +); + return 'Маршрут установлен: $hopCount $_temp0 — $status'; +} + + @override + String get chat_pathSavedLocally => 'Сохранено локально. Подключитесь для синхронизации.'; + + @override + String get chat_pathDeviceConfirmed => 'Подтверждено устройством.'; + + @override + String get chat_pathDeviceNotConfirmed => 'Ещё не подтверждено устройством.'; + + @override + String get chat_type => 'Тип'; + + @override + String get chat_path => 'Маршрут'; + + @override + String get chat_publicKey => 'Публичный ключ'; + + @override + String get chat_compressOutgoingMessages => 'Сжимать исходящие сообщения'; + + @override + String get chat_floodForced => 'Рассылка (принудительно)'; + + @override + String get chat_directForced => 'Прямой (принудительно)'; + + @override + String chat_hopsForced(int count) { + return '$count хоп(ов) (принудительно)'; +} + + @override + String get chat_floodAuto => 'Рассылка (авто)'; + + @override + String get chat_direct => 'Прямой'; + + @override + String get chat_poiShared => 'Точка интереса отправлена'; + + @override + String chat_unread(int count) { + return 'Непрочитанных: $count'; +} + + @override + String get map_title => 'Карта нод'; + + @override + String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; + + @override + String get map_nodesNeedGps => + 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; + + @override + String map_nodesCount(int count) { + return 'Нод: $count'; +} + + @override + String map_pinsCount(int count) { + return 'Меток: $count'; +} + + @override + String get map_chat => 'Чат'; + + @override + String get map_repeater => 'Репитер'; + + @override + String get map_room => 'Комната'; + + @override + String get map_sensor => 'Сенсор'; + + @override + String get map_pinDm => 'Метка (ЛС)'; + + @override + String get map_pinPrivate => 'Метка (Приватная)'; + + @override + String get map_pinPublic => 'Метка (Публичная)'; + + @override + String get map_lastSeen => 'Последнее появление'; + + @override + String get map_disconnectConfirm => + 'Вы уверены, что хотите отключиться от этого устройства?'; + + @override + String get map_from => 'От'; + + @override + String get map_source => 'Источник'; + + @override + String get map_flags => 'Флаги'; + + @override + String get map_shareMarkerHere => 'Поделиться меткой здесь'; + + @override + String get map_pinLabel => 'Метка'; + + @override + String get map_label => 'Подпись'; + + @override + String get map_pointOfInterest => 'Точка интереса'; + + @override + String get map_sendToContact => 'Отправить контакту'; + + @override + String get map_sendToChannel => 'Отправить в канал'; + + @override + String get map_noChannelsAvailable => 'Нет доступных каналов'; + + @override + String get map_publicLocationShare => 'Публичная передача местоположения'; + + @override + String map_publicLocationShareConfirm(String channelLabel) { + return 'Вы собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; +} + + @override + String get map_connectToShareMarkers => + 'Подключитесь к устройству, чтобы делиться метками'; + + @override + String get map_filterNodes => 'Фильтр нод'; + + @override + String get map_nodeTypes => 'Типы нод'; + + @override + String get map_chatNodes => 'Чат-ноды'; + + @override + String get map_repeaters => 'Репитеры'; + + @override + String get map_otherNodes => 'Другие ноды'; + + @override + String get map_keyPrefix => 'Префикс ключа'; + + @override + String get map_filterByKeyPrefix => 'Фильтр по префиксу ключа'; + + @override + String get map_publicKeyPrefix => 'Префикс публичного ключа'; + + @override + String get map_markers => 'Метки'; + + @override + String get map_showSharedMarkers => 'Показывать общие метки'; + + @override + String get map_lastSeenTime => 'Время последнего появления'; + + @override + String get map_sharedPin => 'Общая метка'; + + @override + String get map_joinRoom => 'Присоединиться к комнате'; + + @override + String get map_manageRepeater => 'Управление репитером'; + + @override + String get mapCache_title => 'Кэш офлайн-карты'; + + @override + String get mapCache_selectAreaFirst => 'Сначала выберите область для кэширования'; + + @override + String get mapCache_noTilesToDownload => 'Нет плиток для загрузки в этой области'; + + @override + String get mapCache_downloadTilesTitle => 'Загрузить плитки'; + + @override + String mapCache_downloadTilesPrompt(int count) { + return 'Загрузить $count плиток для офлайн-использования?'; +} + + @override + String get mapCache_downloadAction => 'Загрузить'; + + @override + String mapCache_cachedTiles(int count) { + return 'Закэшировано $count плиток'; +} + + @override + String mapCache_cachedTilesWithFailed(int downloaded, int failed) { + return 'Закэшировано $downloaded плиток ($failed не загружено)'; +} + + @override + String get mapCache_clearOfflineCacheTitle => 'Очистить офлайн-кэш'; + + @override + String get mapCache_clearOfflineCachePrompt => 'Удалить все закэшированные плитки карты?'; + + @override + String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; + + @override + String get mapCache_noAreaSelected => 'Область не выбрана'; + + @override + String get mapCache_cacheArea => 'Область кэширования'; + + @override + String get mapCache_useCurrentView => 'Использовать текущий вид'; + + @override + String get mapCache_zoomRange => 'Диапазон масштаба'; + + @override + String mapCache_estimatedTiles(int count) { + return 'Оценочное количество плиток: $count'; +} + + @override + String mapCache_downloadedTiles(int completed, int total) { + return 'Загружено $completed из $total'; +} + + @override + String get mapCache_downloadTilesButton => 'Загрузить плитки'; + + @override + String get mapCache_clearCacheButton => 'Очистить кэш'; + + @override + String mapCache_failedDownloads(int count) { + return 'Неудачных загрузок: $count'; +} + + @override + String mapCache_boundsLabel( + String north, + String south, + String east, + String west, +) { + return 'С $north, Ю $south, В $east, З $west'; +} + + @override + String get time_justNow => 'Только что'; + + @override + String time_minutesAgo(int minutes) { + return '${minutes} мин назад'; +} + + @override + String time_hoursAgo(int hours) { + return '${hours} ч назад'; +} + + @override + String time_daysAgo(int days) { + return '${days} дн. назад'; +} + + @override + String get time_hour => 'час'; + + @override + String get time_hours => 'часов'; + + @override + String get time_day => 'день'; + + @override + String get time_days => 'дней'; + + @override + String get time_week => 'неделя'; + + @override + String get time_weeks => 'недель'; + + @override + String get time_month => 'месяц'; + + @override + String get time_months => 'месяцев'; + + @override + String get time_minutes => 'минут'; + + @override + String get time_allTime => 'Всё время'; + + @override + String get dialog_disconnect => 'Отключиться'; + + @override + String get dialog_disconnectConfirm => + 'Вы уверены, что хотите отключиться от этого устройства?'; + + @override + String get login_repeaterLogin => 'Вход в репитер'; + + @override + String get login_roomLogin => 'Вход на сервер комнат'; + + @override + String get login_password => 'Пароль'; + + @override + String get login_enterPassword => 'Введите пароль'; + + @override + String get login_savePassword => 'Сохранить пароль'; + + @override + String get login_savePasswordSubtitle => + 'Пароль будет надёжно сохранён на этом устройстве'; + + @override + String get login_repeaterDescription => + 'Введите пароль репитера для доступа к настройкам и статусу.'; + + @override + String get login_roomDescription => + 'Введите пароль комнаты для доступа к настройкам и статусу.'; + + @override + String get login_routing => 'Маршрутизация'; + + @override + String get login_routingMode => 'Режим маршрутизации'; + + @override + String get login_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; + + @override + String get login_forceFloodMode => 'Принудительный режим рассылки'; + + @override + String get login_managePaths => 'Управление маршрутами'; + + @override + String get login_login => 'Войти'; + + @override + String login_attempt(int current, int max) { + return 'Попытка $current/$max'; +} + + @override + String login_failed(String error) { + return 'Ошибка входа: $error'; +} + + @override + String get login_failedMessage => + 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; + + @override + String get common_reload => 'Обновить'; + + @override + String get common_clear => 'Очистить'; + + @override + String path_currentPath(String path) { + return 'Текущий маршрут: $path'; +} + + @override + String path_usingHopsPath(int count) { + String _temp0 = intl.Intl.pluralLogic( +count, +locale: localeName, +other: 'хопов', +one: 'хоп', +); + return 'Используется маршрут из $count $_temp0'; +} + + @override + String get path_enterCustomPath => 'Введите маршрут вручную'; + + @override + String get path_currentPathLabel => 'Текущий маршрут'; + + @override + String get path_hexPrefixInstructions => + 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; + + @override + String get path_hexPrefixExample => + 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; + + @override + String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)'; + + @override + String get path_helperMaxHops => + 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; + + @override + String get path_selectFromContacts => 'Или выберите из контактов:'; + + @override + String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; + + @override + String get path_customPathsRequire => + 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; + + @override + String path_invalidHexPrefixes(String prefixes) { + return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; +} + + @override + String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.'; + + @override + String get path_setPath => 'Установить маршрут'; + + @override + String get repeater_management => 'Управление репитером'; + + @override + String get room_management => 'Управление сервером комнат'; + + @override + String get repeater_managementTools => 'Инструменты управления'; + + @override + String get repeater_status => 'Статус'; + + @override + String get repeater_statusSubtitle => + 'Просмотр статуса, статистики и соседей репитера'; + + @override + String get repeater_telemetry => 'Телеметрия'; + + @override + String get repeater_telemetrySubtitle => + 'Просмотр телеметрии датчиков и системной статистики'; + + @override + String get repeater_cli => 'CLI'; + + @override + String get repeater_cliSubtitle => 'Отправка команд репитеру'; + + @override + String get repeater_neighbours => 'Соседи'; + + @override + String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.'; + + @override + String get repeater_settings => 'Настройки'; + + @override + String get repeater_settingsSubtitle => 'Настройка параметров репитера'; + + @override + String get repeater_statusTitle => 'Статус репитера'; + + @override + String get repeater_routingMode => 'Режим маршрутизации'; + + @override + String get repeater_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; + + @override + String get repeater_forceFloodMode => 'Принудительный режим рассылки'; + + @override + String get repeater_pathManagement => 'Управление маршрутами'; + + @override + String get repeater_refresh => 'Обновить'; + + @override + String get repeater_statusRequestTimeout => 'Время ожидания статуса истекло.'; + + @override + String repeater_errorLoadingStatus(String error) { + return 'Ошибка загрузки статуса: $error'; +} + + @override + String get repeater_systemInformation => 'Системная информация'; + + @override + String get repeater_battery => 'Батарея'; + + @override + String get repeater_clockAtLogin => 'Время (при входе)'; + + @override + String get repeater_uptime => 'Время работы'; + + @override + String get repeater_queueLength => 'Длина очереди'; + + @override + String get repeater_debugFlags => 'Флаги отладки'; + + @override + String get repeater_radioStatistics => 'Радиостатистика'; + + @override + String get repeater_lastRssi => 'Последний RSSI'; + + @override + String get repeater_lastSnr => 'Последний SNR'; + + @override + String get repeater_noiseFloor => 'Уровень шума'; + + @override + String get repeater_txAirtime => 'Время эфира (передача)'; + + @override + String get repeater_rxAirtime => 'Время эфира (приём)'; + + @override + String get repeater_packetStatistics => 'Статистика пакетов'; + + @override + String get repeater_sent => 'Отправлено'; + + @override + String get repeater_received => 'Получено'; + + @override + String get repeater_duplicates => 'Дубликаты'; + + @override + String repeater_daysHoursMinsSecs( +int days, +int hours, +int minutes, +int seconds, +) { + return '$days дн. ${hours}ч ${minutes}м ${seconds}с'; +} + + @override + String repeater_packetTxTotal(int total, String flood, String direct) { + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; +} + + @override + String repeater_packetRxTotal(int total, String flood, String direct) { + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; +} + + @override + String repeater_duplicatesFloodDirect(String flood, String direct) { + return 'Рассылка: $flood, Прямые: $direct'; +} + + @override + String repeater_duplicatesTotal(int total) { + return 'Всего: $total'; +} + + @override + String get repeater_settingsTitle => 'Настройки репитера'; + + @override + String get repeater_basicSettings => 'Основные настройки'; + + @override + String get repeater_repeaterName => 'Имя репитера'; + + @override + String get repeater_repeaterNameHelper => 'Отображаемое имя этого репитера'; + + @override + String get repeater_adminPassword => 'Пароль администратора'; + + @override + String get repeater_adminPasswordHelper => 'Пароль с полным доступом'; + + @override + String get repeater_guestPassword => 'Гостевой пароль'; + + @override + String get repeater_guestPasswordHelper => 'Пароль для доступа только для чтения'; + + @override + String get repeater_radioSettings => 'Настройки радио'; + + @override + String get repeater_frequencyMhz => 'Частота (МГц)'; + + @override + String get repeater_frequencyHelper => '300–2500 МГц'; + + @override + String get repeater_txPower => 'Мощность передачи'; + + @override + String get repeater_txPowerHelper => '1–30 дБм'; + + @override + String get repeater_bandwidth => 'Полоса пропускания'; + + @override + String get repeater_spreadingFactor => 'Коэффициент расширения'; + + @override + String get repeater_codingRate => 'Коэффициент кодирования'; + + @override + String get repeater_locationSettings => 'Настройки местоположения'; + + @override + String get repeater_latitude => 'Широта'; + + @override + String get repeater_latitudeHelper => 'В десятичных градусах (напр., 37.7749)'; + + @override + String get repeater_longitude => 'Долгота'; + + @override + String get repeater_longitudeHelper => 'В десятичных градусах (напр., -122.4194)'; + + @override + String get repeater_features => 'Функции'; + + @override + String get repeater_packetForwarding => 'Пересылка пакетов'; + + @override + String get repeater_packetForwardingSubtitle => + 'Разрешить репитеру пересылать пакеты'; + + @override + String get repeater_guestAccess => 'Гостевой доступ'; + + @override + String get repeater_guestAccessSubtitle => 'Разрешить гостевой доступ только для чтения'; + + @override + String get repeater_privacyMode => 'Режим конфиденциальности'; + + @override + String get repeater_privacyModeSubtitle => + 'Скрывать имя/местоположение в оповещениях'; + + @override + String get repeater_advertisementSettings => 'Настройки анонсирования'; + + @override + String get repeater_localAdvertInterval => 'Интервал локальных анонсирований'; + + @override + String repeater_localAdvertIntervalMinutes(int minutes) { + return '$minutes минут'; +} + + @override + String get repeater_floodAdvertInterval => 'Интервал анонсирований рассылкой (flood)'; + + @override + String repeater_floodAdvertIntervalHours(int hours) { + return '$hours часов'; +} + + @override + String get repeater_encryptedAdvertInterval => + 'Интервал зашифрованных анонсирований'; + + @override + String get repeater_dangerZone => 'Опасная зона'; + + @override + String get repeater_rebootRepeater => 'Перезагрузить репитер'; + + @override + String get repeater_rebootRepeaterSubtitle => 'Перезапустить устройство репитера'; + + @override + String get repeater_rebootRepeaterConfirm => + 'Вы уверены, что хотите перезагрузить этот репитер?'; + + @override + String get repeater_regenerateIdentityKey => 'Пересоздать ключ идентификации'; + + @override + String get repeater_regenerateIdentityKeySubtitle => + 'Сгенерировать новую пару публичного/приватного ключей'; + + @override + String get repeater_regenerateIdentityKeyConfirm => + 'Это создаст новую идентичность для репитера. Продолжить?'; + + @override + String get repeater_eraseFileSystem => 'Стереть файловую систему'; + + @override + String get repeater_eraseFileSystemSubtitle => + 'Отформатировать файловую систему репитера'; + + @override + String get repeater_eraseFileSystemConfirm => + 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; + + @override + String get repeater_eraseSerialOnly => + 'Очистка доступна только через последовательную консоль.'; + + @override + String repeater_commandSent(String command) { + return 'Команда отправлена: $command'; +} + + @override + String repeater_errorSendingCommand(String error) { + return 'Ошибка отправки команды: $error'; +} + + @override + String get repeater_confirm => 'Подтвердить'; + + @override + String get repeater_settingsSaved => 'Настройки успешно сохранены'; + + @override + String repeater_errorSavingSettings(String error) { + return 'Ошибка сохранения настроек: $error'; +} + + @override + String get repeater_refreshBasicSettings => 'Обновить основные настройки'; + + @override + String get repeater_refreshRadioSettings => 'Обновить настройки радио'; + + @override + String get repeater_refreshTxPower => 'Обновить мощность передачи'; + + @override + String get repeater_refreshLocationSettings => 'Обновить настройки местоположения'; + + @override + String get repeater_refreshPacketForwarding => 'Обновить пересылку пакетов'; + + @override + String get repeater_refreshGuestAccess => 'Обновить гостевой доступ'; + + @override + String get repeater_refreshPrivacyMode => 'Обновить режим конфиденциальности'; + + @override + String get repeater_refreshAdvertisementSettings => + 'Обновить настройки анонсирований'; + + @override + String repeater_refreshed(String label) { + return '$label обновлён'; +} + + @override + String repeater_errorRefreshing(String label) { + return 'Ошибка обновления $label'; +} + + @override + String get repeater_cliTitle => 'CLI репитера'; + + @override + String get repeater_debugNextCommand => 'Отладка следующей команды'; + + @override + String get repeater_commandHelp => 'Справка по командам'; + + @override + String get repeater_clearHistory => 'Очистить историю'; + + @override + String get repeater_noCommandsSent => 'Команды ещё не отправлялись'; + + @override + String get repeater_typeCommandOrUseQuick => + 'Введите команду ниже или используйте быстрые команды'; + + @override + String get repeater_enterCommandHint => 'Введите команду...'; + + @override + String get repeater_previousCommand => 'Предыдущая команда'; + + @override + String get repeater_nextCommand => 'Следующая команда'; + + @override + String get repeater_enterCommandFirst => 'Сначала введите команду'; + + @override + String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; + + @override + String repeater_cliCommandError(String error) { + return 'Ошибка: $error'; +} + + @override + String get repeater_cliQuickGetName => 'Получить имя'; + + @override + String get repeater_cliQuickGetRadio => 'Получить радио'; + + @override + String get repeater_cliQuickGetTx => 'Получить TX'; + + @override + String get repeater_cliQuickNeighbors => 'Соседи'; + + @override + String get repeater_cliQuickVersion => 'Версия'; + + @override + String get repeater_cliQuickAdvertise => 'Анонсировать'; + + @override + String get repeater_cliQuickClock => 'Время'; + + @override + String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; + + @override + String get repeater_cliHelpReboot => + 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; + + @override + String get repeater_cliHelpClock => + 'Показывает текущее время по часам устройства.'; + + @override + String get repeater_cliHelpPassword => + 'Устанавливает новый пароль администратора для устройства.'; + + @override + String get repeater_cliHelpVersion => + 'Показывает версию устройства и дату сборки прошивки.'; + + @override + String get repeater_cliHelpClearStats => + 'Сбрасывает различные счётчики статистики в ноль.'; + + @override + String get repeater_cliHelpSetAf => 'Устанавливает коэффициент времени в эфире.'; + + @override + String get repeater_cliHelpSetTx => + 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; + + @override + String get repeater_cliHelpSetRepeat => + 'Включает или отключает роль репитера для этой ноды.'; + + @override + String get repeater_cliHelpSetAllowReadOnly => + '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; + + @override + String get repeater_cliHelpSetFloodMax => + 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; + + @override + String get repeater_cliHelpSetIntThresh => + 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; + + @override + String get repeater_cliHelpSetAgcResetInterval => + 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetMultiAcks => + 'Включает или отключает функцию «двойных ACK».'; + + @override + String get repeater_cliHelpSetAdvertInterval => + 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetFloodAdvertInterval => + 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetGuestPassword => + 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; + + @override + String get repeater_cliHelpSetName => 'Устанавливает имя в оповещениях.'; + + @override + String get repeater_cliHelpSetLat => + 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; + + @override + String get repeater_cliHelpSetLon => + 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; + + @override + String get repeater_cliHelpSetRadio => + 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; + + @override + String get repeater_cliHelpSetRxDelay => + 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; + + @override + String get repeater_cliHelpSetTxDelay => + 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; + + @override + String get repeater_cliHelpSetDirectTxDelay => + 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; + + @override + String get repeater_cliHelpSetBridgeEnabled => 'Включить/выключить мост.'; + + @override + String get repeater_cliHelpSetBridgeDelay => + 'Установить задержку перед ретрансляцией пакетов.'; + + @override + String get repeater_cliHelpSetBridgeSource => + 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; + + @override + String get repeater_cliHelpSetBridgeBaud => + 'Установить скорость последовательного соединения для мостов RS232.'; + + @override + String get repeater_cliHelpSetBridgeSecret => + 'Установить секрет моста для мостов ESP-NOW.'; + + @override + String get repeater_cliHelpSetAdcMultiplier => + 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; + + @override + String get repeater_cliHelpTempRadio => + 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; + + @override + String get repeater_cliHelpSetPerm => + 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; + + @override + String get repeater_cliHelpGetBridgeType => + 'Получает тип моста: none, rs232, espnow'; + + @override + String get repeater_cliHelpLogStart => + 'Начинает запись пакетов в файловую систему.'; + + @override + String get repeater_cliHelpLogStop => 'Останавливает запись пакетов в файловую систему.'; + + @override + String get repeater_cliHelpLogErase => + 'Удаляет журналы пакетов из файловой системы.'; + + @override + String get repeater_cliHelpNeighbors => + 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; + + @override + String get repeater_cliHelpNeighborRemove => + 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; + + @override + String get repeater_cliHelpRegion => + '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; + + @override + String get repeater_cliHelpRegionLoad => + 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; + + @override + String get repeater_cliHelpRegionGet => + 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; + + @override + String get repeater_cliHelpRegionPut => + 'Добавляет или обновляет определение региона с заданным именем.'; + + @override + String get repeater_cliHelpRegionRemove => + 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; + + @override + String get repeater_cliHelpRegionAllowf => + 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; + + @override + String get repeater_cliHelpRegionDenyf => + 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; + + @override + String get repeater_cliHelpRegionHome => + 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; + + @override + String get repeater_cliHelpRegionHomeSet => 'Устанавливает «домашний» регион.'; + + @override + String get repeater_cliHelpRegionSave => + 'Сохраняет список/карту регионов в память.'; + + @override + String get repeater_cliHelpGps => + 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; + + @override + String get repeater_cliHelpGpsOnOff => 'Переключает состояние питания GPS.'; + + @override + String get repeater_cliHelpGpsSync => 'Синхронизирует время ноды с часами GPS.'; + + @override + String get repeater_cliHelpGpsSetLoc => + 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; + + @override + String get repeater_cliHelpGpsAdvert => + 'Показывает конфигурацию передачи местоположения в анонсированиях: + - none: не включать местоположение + - share: передавать GPS-координаты (из SensorManager) + - prefs: передавать координаты из настроек'; + + @override + String get repeater_cliHelpGpsAdvertSet => + 'Устанавливает конфигурацию передачи местоположения.'; + + @override + String get repeater_commandsListTitle => 'Список команд'; + + @override + String get repeater_commandsListNote => + 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; + + @override + String get repeater_general => 'Общие'; + + @override + String get repeater_settingsCategory => 'Настройки'; + + @override + String get repeater_bridge => 'Мост'; + + @override + String get repeater_logging => 'Журналирование'; + + @override + String get repeater_neighborsRepeaterOnly => 'Соседи (только для репитеров)'; + + @override + String get repeater_regionManagementRepeaterOnly => + 'Управление регионами (только для репитеров)'; + + @override + String get repeater_regionNote => + 'Команды регионов введены для управления определениями регионов и правами доступа.'; + + @override + String get repeater_gpsManagement => 'Управление GPS'; + + @override + String get repeater_gpsNote => + 'Команда gps введена для управления параметрами, связанными с местоположением.'; + + @override + String get telemetry_receivedData => 'Полученные телеметрические данные'; + + @override + String get telemetry_requestTimeout => 'Время ожидания телеметрии истекло.'; + + @override + String telemetry_errorLoading(String error) { + return 'Ошибка загрузки телеметрии: $error'; +} + + @override + String get telemetry_noData => 'Данные телеметрии недоступны.'; + + @override + String telemetry_channelTitle(int channel) { + return 'Канал $channel'; +} + + @override + String get telemetry_batteryLabel => 'Батарея'; + + @override + String get telemetry_voltageLabel => 'Напряжение'; + + @override + String get telemetry_mcuTemperatureLabel => 'Температура МК'; + + @override + String get telemetry_temperatureLabel => 'Температура'; + + @override + String get telemetry_currentLabel => 'Ток'; + + @override + String telemetry_batteryValue(int percent, String volts) { + return '$percent% / ${volts}В'; +} + + @override + String telemetry_voltageValue(String volts) { + return '${volts}В'; +} + + @override + String telemetry_currentValue(String amps) { + return '${amps}А'; +} + + @override + String telemetry_temperatureValue(String celsius, String fahrenheit) { + return '$celsius°C / $fahrenheit°F'; +} + + @override + String get neighbors_receivedData => 'Полученные данные о соседях'; + + @override + String get neighbors_requestTimedOut => 'Время ожидания данных о соседях истекло.'; + + @override + String neighbors_errorLoading(String error) { + return 'Ошибка загрузки соседей: $error'; +} + + @override + String get neighbors_repeatersNeighbours => 'Соседи репитеров'; + + @override + String get neighbors_noData => 'Данные о соседях недоступны.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Неизвестный $pubkey'; +} + + @override + String neighbors_heardAgo(String time) { + return 'Слышали: $time назад'; +} + + @override + String get channelPath_title => 'Путь пакета'; + + @override + String get channelPath_viewMap => 'Посмотреть на карте'; + + @override + String get channelPath_otherObservedPaths => 'Другие наблюдаемые пути'; + + @override + String get channelPath_repeaterHops => 'Хопы через репитеры'; + + @override + String get channelPath_noHopDetails => + 'Детали хопов для этого пакета не предоставлены.'; + + @override + String get channelPath_messageDetails => 'Детали сообщения'; + + @override + String get channelPath_senderLabel => 'Отправитель'; + + @override + String get channelPath_timeLabel => 'Время'; + + @override + String get channelPath_repeatsLabel => 'Повторы'; + + @override + String channelPath_pathLabel(int index) { + return 'Путь $index'; +} + + @override + String get channelPath_observedLabel => 'Наблюдаемый'; + + @override + String channelPath_observedPathTitle(int index, String hops) { + return 'Наблюдаемый путь $index • $hops'; +} + + @override + String get channelPath_noLocationData => 'Нет данных о местоположении'; + + @override + String channelPath_timeWithDate(int day, int month, String time) { + return '$day/$month $time'; +} + + @override + String channelPath_timeOnly(String time) { + return '$time'; +} + + @override + String get channelPath_unknownPath => 'Неизвестный'; + + @override + String get channelPath_floodPath => 'Рассылка'; + + @override + String get channelPath_directPath => 'Прямой'; + + @override + String channelPath_observedZeroOf(int total) { + return '0 из $total хопов'; +} + + @override + String channelPath_observedSomeOf(int observed, int total) { + return '$observed из $total хопов'; +} + + @override + String get channelPath_mapTitle => 'Карта пути'; + + @override + String get channelPath_noRepeaterLocations => + 'Нет данных о местоположении репитеров для этого пути.'; + + @override + String channelPath_primaryPath(int index) { + return 'Путь $index (Основной)'; +} + + @override + String get channelPath_pathLabelTitle => 'Путь'; + + @override + String get channelPath_observedPathHeader => 'Наблюдаемый путь'; + + @override + String channelPath_selectedPathLabel(String label, String prefixes) { + return '$label • $prefixes'; +} + + @override + String get channelPath_noHopDetailsAvailable => + 'Детали хопов для этого пакета недоступны.'; + + @override + String get channelPath_unknownRepeater => 'Неизвестный репитер'; + + @override + String get community_title => 'Сообщество'; + + @override + String get community_create => 'Создать сообщество'; + + @override + String get community_createDesc => + 'Создать новое сообщество и поделиться через QR-код.'; + + @override + String get community_join => 'Присоединиться'; + + @override + String get community_joinTitle => 'Присоединиться к сообществу'; + + @override + String community_joinConfirmation(String name) { + return 'Вы хотите присоединиться к сообществу \"$name\"?'; +} + + @override + String get community_scanQr => 'Сканировать QR-код сообщества'; + + @override + String get community_scanInstructions => + 'Наведите камеру на QR-код сообщества'; + + @override + String get community_showQr => 'Показать QR-код'; + + @override + String get community_publicChannel => 'Публичный канал сообщества'; + + @override + String get community_hashtagChannel => 'Хэштег-канал сообщества'; + + @override + String get community_name => 'Имя сообщества'; + + @override + String get community_enterName => 'Введите имя сообщества'; + + @override + String community_created(String name) { + return 'Сообщество \"$name\" создано'; +} + + @override + String community_joined(String name) { + return 'Присоединились к сообществу \"$name\"'; +} + + @override + String get community_qrTitle => 'Поделиться сообществом'; + + @override + String community_qrInstructions(String name) { + return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; +} + + @override + String get community_hashtagPrivacyHint => + 'Хэштег-каналы сообщества доступны только его участникам'; + + @override + String get community_invalidQrCode => 'Недопустимый QR-код сообщества'; + + @override + String get community_alreadyMember => 'Уже участник'; + + @override + String community_alreadyMemberMessage(String name) { + return 'Вы уже участник сообщества \"$name\".'; +} + + @override + String get community_addPublicChannel => 'Добавить публичный канал сообщества'; + + @override + String get community_addPublicChannelHint => + 'Автоматически добавить публичный канал для этого сообщества'; + + @override + String get community_noCommunities => 'Вы ещё не присоединились ни к одному сообществу'; + + @override + String get community_scanOrCreate => + 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; + + @override + String get community_manageCommunities => 'Управление сообществами'; + + @override + String get community_delete => 'Покинуть сообщество'; + + @override + String community_deleteConfirm(String name) { + return 'Покинуть \"$name\"?'; +} + + @override + String community_deleteChannelsWarning(int count) { + return 'Это также удалит $count канал(ов) и их сообщения.'; +} + + @override + String community_deleted(String name) { + return 'Покинули сообщество \"$name\"'; +} + + @override + String get community_regenerateSecret => 'Пересоздать секрет'; + + @override + String community_regenerateSecretConfirm(String name) { + return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; +} + + @override + String get community_regenerate => 'Пересоздать'; + + @override + String community_secretRegenerated(String name) { + return 'Секрет пересоздан для \"$name\"'; +} + + @override + String get community_updateSecret => 'Обновить секрет'; + + @override + String community_secretUpdated(String name) { + return 'Секрет обновлён для \"$name\"'; +} + + @override + String community_scanToUpdateSecret(String name) { + return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; +} + + @override + String get community_addHashtagChannel => 'Добавить хэштег-канал сообщества'; + + @override + String get community_addHashtagChannelDesc => + 'Добавить хэштег-канал для этого сообщества'; + + @override + String get community_selectCommunity => 'Выбрать сообщество'; + + @override + String get community_regularHashtag => 'Обычный хэштег'; + + @override + String get community_regularHashtagDesc => 'Публичный хэштег (любой может присоединиться)'; + + @override + String get community_communityHashtag => 'Хэштег сообщества'; + + @override + String get community_communityHashtagDesc => 'Доступен только участникам сообщества'; + + @override + String community_forCommunity(String name) { + return 'Для $name'; +} + + @override + String get listFilter_tooltip => 'Фильтр и сортировка'; + + @override + String get listFilter_sortBy => 'Сортировка по'; + + @override + String get listFilter_latestMessages => 'Последние сообщения'; + + @override + String get listFilter_heardRecently => 'Слышали недавно'; + + @override + String get listFilter_az => 'По алфавиту'; + + @override + String get listFilter_filters => 'Фильтры'; + + @override + String get listFilter_all => 'Все'; + + @override + String get listFilter_users => 'Пользователи'; + + @override + String get listFilter_repeaters => 'Репитеры'; + + @override + String get listFilter_roomServers => 'Серверы комнат'; + + @override + String get listFilter_unreadOnly => 'Только непрочитанные'; + + @override + String get listFilter_newGroup => 'Новая группа'; +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..7ac1e85 --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,761 @@ +{ + "@@locale": "ru", + + "appTitle": "MeshCore Open", + + "nav_contacts": "Контакты", + "nav_channels": "Каналы", + "nav_map": "Карта", + + "common_cancel": "Отмена", + "common_ok": "OK", + "common_connect": "Коннект", + "common_unknownDevice": "Неизвестное устройство", + "common_save": "Сохранить", + "common_delete": "Удалить", + "common_close": "Закрыть", + "common_edit": "Изменить", + "common_add": "Добавить", + "common_settings": "Настройки", + "common_disconnect": "Отключить", + "common_connected": "Подключено", + "common_disconnected": "Отключено", + "common_create": "Создать", + "common_continue": "Продолжить", + "common_share": "Поделиться", + "common_copy": "Копировать", + "common_retry": "Повторить", + "common_hide": "Скрыть", + "common_remove": "Убрать", + "common_enable": "Включить", + "common_disable": "Выключить", + "common_reboot": "Перезагрузить", + "common_loading": "Загрузка...", + "common_notAvailable": "—", + "common_voltageValue": "{volts} В", + "common_percentValue": "{percent}%", + "scanner_title": "MeshCore Open", + "scanner_scanning": "Поиск устройств...", + "scanner_connecting": "Подключение...", + "scanner_disconnecting": "Отключение...", + "scanner_notConnected": "Не подключено", + "scanner_connectedTo": "Подключено к {deviceName}", + "scanner_searchingDevices": "Поиск устройств MeshCore...", + "scanner_tapToScan": "Нажмите для поиска MeshCore устройств", + "scanner_connectionFailed": "Подключение не удалось: {error}", + "scanner_stop": "Стоп", + "scanner_scan": "Сканирование", + "device_quickSwitch": "Быстрое переключение", + "device_meshcore": "MeshCore", + "settings_title": "Настройки", + "settings_deviceInfo": "Информация об устройстве", + "settings_appSettings": "Настройки приложения", + "settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты", + "settings_nodeSettings": "Настройки ноды", + "settings_nodeName": "Имя ноды", + "settings_nodeNameNotSet": "Не установлено", + "settings_nodeNameHint": "Введите имя ноды", + "settings_nodeNameUpdated": "Имя обновлено", + "settings_radioSettings": "Настройки радио", + "settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения", + "settings_radioSettingsUpdated": "Настройки радио обновлены", + "settings_location": "Позиция", + "settings_locationSubtitle": "Координаты GPS", + "settings_locationUpdated": "Позиция и настройки GPS обновлены", + "settings_locationBothRequired": "Введите широту и долготу.", + "settings_locationInvalid": "Неверная широта или долгота.", + "settings_locationGPSEnable": "Включить GPS", + "settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.", + "settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)", + "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", + "settings_latitude": "Широта", + "settings_longitude": "Долгота", + "settings_privacyMode": "Режим конфиденциальности", + "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", + "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", + "settings_privacyModeEnabled": "Режим конфиденциальности включен", + "settings_privacyModeDisabled": "Режим конфиденциальности выключен", + "settings_actions": "Действия", + "settings_sendAdvertisement": "Отправить анонсирование", + "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", + "settings_advertisementSent": "Анонсирование отправлено", + "settings_syncTime": "Синхронизация времени", + "settings_syncTimeSubtitle": "Синхронизировать время с телефоном", + "settings_timeSynchronized": "Время синхронизировано", + "settings_refreshContacts": "Обновить контакты", + "settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства", + "settings_rebootDevice": "Перезагрузить устройство", + "settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore", + "settings_rebootDeviceConfirm": "Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.", + "settings_debug": "Отладка", + "settings_bleDebugLog": "Журнал отладки BLE", + "settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные", + "settings_appDebugLog": "Журнал отладки приложения", + "settings_appDebugLogSubtitle": "Сообщения отладки приложения", + "settings_about": "О программе", + "settings_aboutVersion": "MeshCore Open v{version}", + "settings_aboutLegalese": "2026 MeshCore Open Source Project", + "settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.", + "settings_infoName": "Имя", + "settings_infoId": "ID", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батарея", + "settings_infoPublicKey": "Публичный ключ", + "settings_infoContactsCount": "Количество контактов", + "settings_infoChannelCount": "Количество каналов", + "settings_presets": "Пресеты", + "settings_preset915Mhz": "915 МГц", + "settings_preset868Mhz": "868 МГц", + "settings_preset433Mhz": "433 МГц", + "settings_frequency": "Частота (МГц)", + "settings_frequencyHelper": "300.0 – 2500.0", + "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", + "settings_bandwidth": "Полоса пропускания", + "settings_spreadingFactor": "Коэффициент расширения", + "settings_codingRate": "Коэффициент кодирования", + "settings_txPower": "Мощность передачи (дБм)", + "settings_txPowerHelper": "0 – 22", + "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", + "settings_longRange": "Дальний радиус", + "settings_fastSpeed": "Высокая скорость", + "settings_error": "Ошибка: {message}", + "appSettings_title": "Настройки приложения", + "appSettings_appearance": "Внешний вид", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Как в системе", + "appSettings_themeLight": "Светлая", + "appSettings_themeDark": "Тёмная", + "appSettings_language": "Язык", + "appSettings_languageSystem": "Как в системе", + "appSettings_languageEn": "Английский", + "appSettings_languageFr": "Французский", + "appSettings_languageEs": "Испанский", + "appSettings_languageDe": "Немецкий", + "appSettings_languagePl": "Польский", + "appSettings_languageSl": "Словенский", + "appSettings_languagePt": "Португальский", + "appSettings_languageIt": "Итальянский", + "appSettings_languageZh": "Китайский", + "appSettings_languageSv": "Шведский", + "appSettings_languageNl": "Нидерландский", + "appSettings_languageSk": "Словацкий", + "appSettings_languageBg": "Болгарский", + "appSettings_languageRu": "Русский", + "appSettings_notifications": "Уведомления", + "appSettings_enableNotifications": "Включить уведомления", + "appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях", + "appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено", + "appSettings_notificationsEnabled": "Уведомления включены", + "appSettings_notificationsDisabled": "Уведомления отключены", + "appSettings_messageNotifications": "Уведомления о сообщениях", + "appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений", + "appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах", + "appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах", + "appSettings_advertisementNotifications": "Уведомления об анонсированиях", + "appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод", + "appSettings_messaging": "Обмен сообщениями", + "appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток", + "appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки", + "appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток", + "appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться", + "appSettings_autoRouteRotation": "Автоматическое переключение маршрутов", + "appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки", + "appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено", + "appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено", + "appSettings_battery": "Батарея", + "appSettings_batteryChemistry": "Химия батареи", + "appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})", + "appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать", + "appSettings_batteryNmc": "18650 NMC (3.0–4.2 В)", + "appSettings_batteryLifepo4": "LiFePO4 (2.6–3.65 В)", + "appSettings_batteryLipo": "LiPo (3.0–4.2 В)", + "appSettings_mapDisplay": "Отображение карты", + "appSettings_showRepeaters": "Показывать репитеры", + "appSettings_showRepeatersSubtitle": "Отображать репитеры на карте", + "appSettings_showChatNodes": "Показывать чат-ноды", + "appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте", + "appSettings_showOtherNodes": "Показывать другие ноды", + "appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте", + "appSettings_timeFilter": "Фильтр по времени", + "appSettings_timeFilterShowAll": "Показывать все ноды", + "appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч", + "appSettings_mapTimeFilter": "Временной фильтр карты", + "appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:", + "appSettings_allTime": "Всё время", + "appSettings_lastHour": "Последний час", + "appSettings_last6Hours": "Последние 6 часов", + "appSettings_last24Hours": "Последние 24 часа", + "appSettings_lastWeek": "Последнюю неделю", + "appSettings_offlineMapCache": "Кэш офлайн-карты", + "appSettings_noAreaSelected": "Область не выбрана", + "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", + "appSettings_debugCard": "Отладка", + "appSettings_appDebugLogging": "Журнал отладки приложения", + "appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики", + "appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён", + "appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён", + "contacts_title": "Контакты", + "contacts_noContacts": "Контактов пока нет", + "contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения", + "contacts_searchContacts": "Поиск контактов...", + "contacts_noUnreadContacts": "Нет непрочитанных контактов", + "contacts_noContactsFound": "Контакты или группы не найдены", + "contacts_deleteContact": "Удалить контакт", + "contacts_removeConfirm": "Удалить {contactName} из контактов?", + "contacts_manageRepeater": "Управление репитером", + "contacts_manageRoom": "Управление сервером комнат", + "contacts_roomLogin": "Вход на сервер комнат", + "contacts_openChat": "Открыть чат", + "contacts_editGroup": "Изменить группу", + "contacts_deleteGroup": "Удалить группу", + "contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?", + "contacts_newGroup": "Новая группа", + "contacts_groupName": "Имя группы", + "contacts_groupNameRequired": "Имя группы обязательно", + "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", + "contacts_filterContacts": "Фильтр контактов...", + "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", + "contacts_noMembers": "Нет участников", + "contacts_lastSeenNow": "Видели только что", + "contacts_lastSeenMinsAgo": "Видели {minutes} мин назад", + "contacts_lastSeenHourAgo": "Видели 1 час назад", + "contacts_lastSeenHoursAgo": "Видели {hours} ч назад", + "contacts_lastSeenDayAgo": "Видели 1 день назад", + "contacts_lastSeenDaysAgo": "Видели {days} дн. назад", + "channels_title": "Каналы", + "channels_noChannelsConfigured": "Каналы не настроены", + "channels_addPublicChannel": "Добавить публичный канал", + "channels_searchChannels": "Поиск каналов...", + "channels_noChannelsFound": "Каналы не найдены", + "channels_channelIndex": "Канал {index}", + "channels_hashtagChannel": "Хэштег-канал", + "channels_public": "Публичный", + "channels_private": "Приватный", + "channels_publicChannel": "Публичный канал", + "channels_privateChannel": "Приватный канал", + "channels_editChannel": "Изменить канал", + "channels_deleteChannel": "Удалить канал", + "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", + "channels_channelDeleted": "Канал \"{name}\" удалён", + "channels_addChannel": "Добавить канал", + "channels_channelIndexLabel": "Индекс канала", + "channels_channelName": "Имя канала", + "channels_usePublicChannel": "Использовать публичный канал", + "channels_standardPublicPsk": "Стандартный публичный PSK", + "channels_pskHex": "PSK (Hex)", + "channels_generateRandomPsk": "Сгенерировать случайный PSK", + "channels_enterChannelName": "Введите имя канала", + "channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа", + "channels_channelAdded": "Канал \"{name}\" добавлен", + "channels_editChannelTitle": "Изменить канал {index}", + "channels_smazCompression": "Сжатие SMAZ", + "channels_channelUpdated": "Канал \"{name}\" обновлён", + "channels_publicChannelAdded": "Публичный канал добавлен", + "channels_sortBy": "Сортировка", + "channels_sortManual": "Вручную", + "channels_sortAZ": "По алфавиту", + "channels_sortLatestMessages": "По последним сообщениям", + "channels_sortUnread": "По непрочитанным", + "channels_createPrivateChannel": "Создать приватный канал", + "channels_createPrivateChannelDesc": "Защищён секретным ключом.", + "channels_joinPrivateChannel": "Присоединиться к приватному каналу", + "channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.", + "channels_joinPublicChannel": "Присоединиться к публичному каналу", + "channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.", + "channels_joinHashtagChannel": "Присоединиться к хэштег-каналу", + "channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.", + "channels_scanQrCode": "Сканировать QR-код", + "channels_scanQrCodeComingSoon": "Скоро будет", + "channels_enterHashtag": "Введите хэштег", + "channels_hashtagHint": "например, #команда", + "chat_noMessages": "Сообщений пока нет", + "chat_sendMessageToStart": "Отправьте сообщение, чтобы начать", + "chat_originalMessageNotFound": "Исходное сообщение не найдено", + "chat_replyingTo": "Ответ для {name}", + "chat_replyTo": "Ответить {name}", + "chat_location": "Местоположение", + "chat_sendMessageTo": "Отправить сообщение {contactName}", + "chat_typeMessage": "Напишите сообщение...", + "chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).", + "chat_messageCopied": "Сообщение скопировано", + "chat_messageDeleted": "Сообщение удалено", + "chat_retryingMessage": "Повтор отправки сообщения", + "chat_retryCount": "Попытка {current}/{max}", + "chat_sendGif": "Отправить GIF", + "chat_reply": "Ответить", + "chat_addReaction": "Добавить реакцию", + "chat_me": "Я", + "emojiCategorySmileys": "Смайлы", + "emojiCategoryGestures": "Жесты", + "emojiCategoryHearts": "Сердечки", + "emojiCategoryObjects": "Предметы", + "gifPicker_title": "Выберите GIF", + "gifPicker_searchHint": "Поиск GIF...", + "gifPicker_poweredBy": "Работает на GIPHY", + "gifPicker_noGifsFound": "GIF не найдены", + "gifPicker_failedLoad": "Не удалось загрузить GIF", + "gifPicker_failedSearch": "Не удалось выполнить поиск GIF", + "gifPicker_noInternet": "Нет подключения к интернету", + "debugLog_appTitle": "Журнал отладки приложения", + "debugLog_bleTitle": "Журнал отладки BLE", + "debugLog_copyLog": "Копировать журнал", + "debugLog_clearLog": "Очистить журнал", + "debugLog_copied": "Журнал отладки скопирован", + "debugLog_bleCopied": "Журнал BLE скопирован", + "debugLog_noEntries": "Журнал отладки пока пуст", + "debugLog_enableInSettings": "Включите запись журнала отладки в настройках", + "debugLog_frames": "Фреймы", + "debugLog_rawLogRx": "Сырой журнал приёма", + "debugLog_noBleActivity": "Активность BLE пока отсутствует", + "debugFrame_length": "Длина фрейма: {count} байт", + "debugFrame_command": "Команда: 0x{value}", + "debugFrame_textMessageHeader": "Фрейм текстового сообщения:", + "debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}", + "debugFrame_timestamp": "- Временная метка: {timestamp}", + "debugFrame_flags": "- Флаги: 0x{value}", + "debugFrame_textType": "- Тип текста: {type} ({label})", + "debugFrame_textTypeCli": "CLI", + "debugFrame_textTypePlain": "Обычный", + "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_hexDump": "Шестнадцатеричный дамп:", + "chat_pathManagement": "Управление маршрутами", + "chat_routingMode": "Режим маршрутизации", + "chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "chat_forceFloodMode": "Принудительный режим рассылки", + "chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):", + "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", + "chat_hopSingular": "хоп", + "chat_hopPlural": "хопов", + "chat_hopsCount": "{count} {plural, select, one {хоп} other {хопов}}", + "chat_successes": "успешно", + "chat_removePath": "Удалить маршрут", + "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", + "chat_pathActions": "Действия с маршрутом:", + "chat_setCustomPath": "Указать маршрут вручную", + "chat_setCustomPathSubtitle": "Вручную задать маршрут передачи", + "chat_clearPath": "Очистить маршрут", + "chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке", + "chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.", + "chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения", + "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", + "chat_fullPath": "Полный маршрут", + "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", + "chat_pathSetHops": "Маршрут установлен: {hopCount} {plural, select, one {хоп} other {хопов}} — {status}", + "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", + "chat_pathDeviceConfirmed": "Подтверждено устройством.", + "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", + "chat_type": "Тип", + "chat_path": "Маршрут", + "chat_publicKey": "Публичный ключ", + "chat_compressOutgoingMessages": "Сжимать исходящие сообщения", + "chat_floodForced": "Рассылка (принудительно)", + "chat_directForced": "Прямой (принудительно)", + "chat_hopsForced": "{count} хоп(ов) (принудительно)", + "chat_floodAuto": "Рассылка (авто)", + "chat_direct": "Прямой", + "chat_poiShared": "Точка интереса отправлена", + "chat_unread": "Непрочитанных: {count}", + "map_title": "Карта нод", + "map_noNodesWithLocation": "Нет нод с данными о местоположении", + "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте", + "map_nodesCount": "Нод: {count}", + "map_pinsCount": "Меток: {count}", + "map_chat": "Чат", + "map_repeater": "Репитер", + "map_room": "Комната", + "map_sensor": "Сенсор", + "map_pinDm": "Метка (ЛС)", + "map_pinPrivate": "Метка (Приватная)", + "map_pinPublic": "Метка (Публичная)", + "map_lastSeen": "Последнее появление", + "map_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", + "map_from": "От", + "map_source": "Источник", + "map_flags": "Флаги", + "map_shareMarkerHere": "Поделиться меткой здесь", + "map_pinLabel": "Метка", + "map_label": "Подпись", + "map_pointOfInterest": "Точка интереса", + "map_sendToContact": "Отправить контакту", + "map_sendToChannel": "Отправить в канал", + "map_noChannelsAvailable": "Нет доступных каналов", + "map_publicLocationShare": "Публичная передача местоположения", + "map_publicLocationShareConfirm": "Вы собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.", + "map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками", + "map_filterNodes": "Фильтр нод", + "map_nodeTypes": "Типы нод", + "map_chatNodes": "Чат-ноды", + "map_repeaters": "Репитеры", + "map_otherNodes": "Другие ноды", + "map_keyPrefix": "Префикс ключа", + "map_filterByKeyPrefix": "Фильтр по префиксу ключа", + "map_publicKeyPrefix": "Префикс публичного ключа", + "map_markers": "Метки", + "map_showSharedMarkers": "Показывать общие метки", + "map_lastSeenTime": "Время последнего появления", + "map_sharedPin": "Общая метка", + "map_joinRoom": "Присоединиться к комнате", + "map_manageRepeater": "Управление репитером", + "mapCache_title": "Кэш офлайн-карты", + "mapCache_selectAreaFirst": "Сначала выберите область для кэширования", + "mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области", + "mapCache_downloadTilesTitle": "Загрузить плитки", + "mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?", + "mapCache_downloadAction": "Загрузить", + "mapCache_cachedTiles": "Закэшировано {count} плиток", + "mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)", + "mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш", + "mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?", + "mapCache_offlineCacheCleared": "Офлайн-кэш очищен", + "mapCache_noAreaSelected": "Область не выбрана", + "mapCache_cacheArea": "Область кэширования", + "mapCache_useCurrentView": "Использовать текущий вид", + "mapCache_zoomRange": "Диапазон масштаба", + "mapCache_estimatedTiles": "Оценочное количество плиток: {count}", + "mapCache_downloadedTiles": "Загружено {completed} из {total}", + "mapCache_downloadTilesButton": "Загрузить плитки", + "mapCache_clearCacheButton": "Очистить кэш", + "mapCache_failedDownloads": "Неудачных загрузок: {count}", + "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}", + "time_justNow": "Только что", + "time_minutesAgo": "{minutes} мин назад", + "time_hoursAgo": "{hours} ч назад", + "time_daysAgo": "{days} дн. назад", + "time_hour": "час", + "time_hours": "часов", + "time_day": "день", + "time_days": "дней", + "time_week": "неделя", + "time_weeks": "недель", + "time_month": "месяц", + "time_months": "месяцев", + "time_minutes": "минут", + "time_allTime": "Всё время", + "dialog_disconnect": "Отключиться", + "dialog_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", + "login_repeaterLogin": "Вход в репитер", + "login_roomLogin": "Вход на сервер комнат", + "login_password": "Пароль", + "login_enterPassword": "Введите пароль", + "login_savePassword": "Сохранить пароль", + "login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве", + "login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.", + "login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.", + "login_routing": "Маршрутизация", + "login_routingMode": "Режим маршрутизации", + "login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "login_forceFloodMode": "Принудительный режим рассылки", + "login_managePaths": "Управление маршрутами", + "login_login": "Войти", + "login_attempt": "Попытка {current}/{max}", + "login_failed": "Ошибка входа: {error}", + "login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.", + "common_reload": "Обновить", + "common_clear": "Очистить", + "path_currentPath": "Текущий маршрут: {path}", + "path_usingHopsPath": "Используется маршрут из {count} {plural, select, one {хоп} other {хопов}}", + "path_enterCustomPath": "Введите маршрут вручную", + "path_currentPathLabel": "Текущий маршрут", + "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", + "path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)", + "path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)", + "path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)", + "path_selectFromContacts": "Или выберите из контактов:", + "path_noRepeatersFound": "Репитеры или серверы комнат не найдены.", + "path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.", + "path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}", + "path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.", + "path_setPath": "Установить маршрут", + "repeater_management": "Управление репитером", + "room_management": "Управление сервером комнат", + "repeater_managementTools": "Инструменты управления", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера", + "repeater_telemetry": "Телеметрия", + "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", + "repeater_cli": "CLI", + "repeater_cliSubtitle": "Отправка команд репитеру", + "repeater_neighbours": "Соседи", + "repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.", + "repeater_settings": "Настройки", + "repeater_settingsSubtitle": "Настройка параметров репитера", + "repeater_statusTitle": "Статус репитера", + "repeater_routingMode": "Режим маршрутизации", + "repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "repeater_forceFloodMode": "Принудительный режим рассылки", + "repeater_pathManagement": "Управление маршрутами", + "repeater_refresh": "Обновить", + "repeater_statusRequestTimeout": "Время ожидания статуса истекло.", + "repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}", + "repeater_systemInformation": "Системная информация", + "repeater_battery": "Батарея", + "repeater_clockAtLogin": "Время (при входе)", + "repeater_uptime": "Время работы", + "repeater_queueLength": "Длина очереди", + "repeater_debugFlags": "Флаги отладки", + "repeater_radioStatistics": "Радиостатистика", + "repeater_lastRssi": "Последний RSSI", + "repeater_lastSnr": "Последний SNR", + "repeater_noiseFloor": "Уровень шума", + "repeater_txAirtime": "Время эфира (передача)", + "repeater_rxAirtime": "Время эфира (приём)", + "repeater_packetStatistics": "Статистика пакетов", + "repeater_sent": "Отправлено", + "repeater_received": "Получено", + "repeater_duplicates": "Дубликаты", + "repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с", + "repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesTotal": "Всего: {total}", + "repeater_settingsTitle": "Настройки репитера", + "repeater_basicSettings": "Основные настройки", + "repeater_repeaterName": "Имя репитера", + "repeater_repeaterNameHelper": "Отображаемое имя этого репитера", + "repeater_adminPassword": "Пароль администратора", + "repeater_adminPasswordHelper": "Пароль с полным доступом", + "repeater_guestPassword": "Гостевой пароль", + "repeater_guestPasswordHelper": "Пароль для доступа только для чтения", + "repeater_radioSettings": "Настройки радио", + "repeater_frequencyMhz": "Частота (МГц)", + "repeater_frequencyHelper": "300–2500 МГц", + "repeater_txPower": "Мощность передачи", + "repeater_txPowerHelper": "1–30 дБм", + "repeater_bandwidth": "Полоса пропускания", + "repeater_spreadingFactor": "Коэффициент расширения", + "repeater_codingRate": "Коэффициент кодирования", + "repeater_locationSettings": "Настройки местоположения", + "repeater_latitude": "Широта", + "repeater_latitudeHelper": "В десятичных градусах (напр., 37.7749)", + "repeater_longitude": "Долгота", + "repeater_longitudeHelper": "В десятичных градусах (напр., -122.4194)", + "repeater_features": "Функции", + "repeater_packetForwarding": "Пересылка пакетов", + "repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты", + "repeater_guestAccess": "Гостевой доступ", + "repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения", + "repeater_privacyMode": "Режим конфиденциальности", + "repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях", + "repeater_advertisementSettings": "Настройки анонсирования", + "repeater_localAdvertInterval": "Интервал локальных анонсирований", + "repeater_localAdvertIntervalMinutes": "{minutes} минут", + "repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)", + "repeater_floodAdvertIntervalHours": "{hours} часов", + "repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований", + "repeater_dangerZone": "Опасная зона", + "repeater_rebootRepeater": "Перезагрузить репитер", + "repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера", + "repeater_rebootRepeaterConfirm": "Вы уверены, что хотите перезагрузить этот репитер?", + "repeater_regenerateIdentityKey": "Пересоздать ключ идентификации", + "repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей", + "repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?", + "repeater_eraseFileSystem": "Стереть файловую систему", + "repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера", + "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!", + "repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.", + "repeater_commandSent": "Команда отправлена: {command}", + "repeater_errorSendingCommand": "Ошибка отправки команды: {error}", + "repeater_confirm": "Подтвердить", + "repeater_settingsSaved": "Настройки успешно сохранены", + "repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}", + "repeater_refreshBasicSettings": "Обновить основные настройки", + "repeater_refreshRadioSettings": "Обновить настройки радио", + "repeater_refreshTxPower": "Обновить мощность передачи", + "repeater_refreshLocationSettings": "Обновить настройки местоположения", + "repeater_refreshPacketForwarding": "Обновить пересылку пакетов", + "repeater_refreshGuestAccess": "Обновить гостевой доступ", + "repeater_refreshPrivacyMode": "Обновить режим конфиденциальности", + "repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований", + "repeater_refreshed": "{label} обновлён", + "repeater_errorRefreshing": "Ошибка обновления {label}", + "repeater_cliTitle": "CLI репитера", + "repeater_debugNextCommand": "Отладка следующей команды", + "repeater_commandHelp": "Справка по командам", + "repeater_clearHistory": "Очистить историю", + "repeater_noCommandsSent": "Команды ещё не отправлялись", + "repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды", + "repeater_enterCommandHint": "Введите команду...", + "repeater_previousCommand": "Предыдущая команда", + "repeater_nextCommand": "Следующая команда", + "repeater_enterCommandFirst": "Сначала введите команду", + "repeater_cliCommandFrameTitle": "Фрейм CLI-команды", + "repeater_cliCommandError": "Ошибка: {error}", + "repeater_cliQuickGetName": "Получить имя", + "repeater_cliQuickGetRadio": "Получить радио", + "repeater_cliQuickGetTx": "Получить TX", + "repeater_cliQuickNeighbors": "Соседи", + "repeater_cliQuickVersion": "Версия", + "repeater_cliQuickAdvertise": "Анонсировать", + "repeater_cliQuickClock": "Время", + "repeater_cliHelpAdvert": "Отправляет пакет анонсирования", + "repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)", + "repeater_cliHelpClock": "Показывает текущее время по часам устройства.", + "repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.", + "repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.", + "repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.", + "repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.", + "repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)", + "repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.", + "repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)", + "repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)", + "repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.", + "repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.", + "repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».", + "repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.", + "repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.", + "repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)", + "repeater_cliHelpSetName": "Устанавливает имя в оповещениях.", + "repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.", + "repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.", + "repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).", + "repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.", + "repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.", + "repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.", + "repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.", + "repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.", + "repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.", + "repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).", + "repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).", + "repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)", + "repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow", + "repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.", + "repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.", + "repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.", + "repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4", + "repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.", + "repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.", + "repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.", + "repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»", + "repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.", + "repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)", + "repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)", + "repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)", + "repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)", + "repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.", + "repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.", + "repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.", + "repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.", + "repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.", + "repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.", + "repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек", + "repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.", + "repeater_commandsListTitle": "Список команд", + "repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».", + "repeater_general": "Общие", + "repeater_settingsCategory": "Настройки", + "repeater_bridge": "Мост", + "repeater_logging": "Журналирование", + "repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)", + "repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)", + "repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.", + "repeater_gpsManagement": "Управление GPS", + "repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.", + "telemetry_receivedData": "Полученные телеметрические данные", + "telemetry_requestTimeout": "Время ожидания телеметрии истекло.", + "telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}", + "telemetry_noData": "Данные телеметрии недоступны.", + "telemetry_channelTitle": "Канал {channel}", + "telemetry_batteryLabel": "Батарея", + "telemetry_voltageLabel": "Напряжение", + "telemetry_mcuTemperatureLabel": "Температура МК", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Ток", + "telemetry_batteryValue": "{percent}% / {volts}В", + "telemetry_voltageValue": "{volts}В", + "telemetry_currentValue": "{amps}А", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "neighbors_receivedData": "Полученные данные о соседях", + "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", + "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", + "neighbors_repeatersNeighbours": "Соседи репитеров", + "neighbors_noData": "Данные о соседях недоступны.", + "neighbors_unknownContact": "Неизвестный {pubkey}", + "neighbors_heardA ago": "Слышали: {time} назад", + "channelPath_title": "Путь пакета", + "channelPath_viewMap": "Посмотреть на карте", + "channelPath_otherObservedPaths": "Другие наблюдаемые пути", + "channelPath_repeaterHops": "Хопы через репитеры", + "channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.", + "channelPath_messageDetails": "Детали сообщения", + "channelPath_senderLabel": "Отправитель", + "channelPath_timeLabel": "Время", + "channelPath_repeatsLabel": "Повторы", + "channelPath_pathLabel": "Путь {index}", + "channelPath_observedLabel": "Наблюдаемый", + "channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}", + "channelPath_noLocationData": "Нет данных о местоположении", + "channelPath_timeWithDate": "{day}/{month} {time}", + "channelPath_timeOnly": "{time}", + "channelPath_unknownPath": "Неизвестный", + "channelPath_floodPath": "Рассылка", + "channelPath_directPath": "Прямой", + "channelPath_observedZeroOf": "0 из {total} хопов", + "channelPath_observedSomeOf": "{observed} из {total} хопов", + "channelPath_mapTitle": "Карта пути", + "channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.", + "channelPath_primaryPath": "Путь {index} (Основной)", + "channelPath_pathLabelTitle": "Путь", + "channelPath_observedPathHeader": "Наблюдаемый путь", + "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.", + "channelPath_unknownRepeater": "Неизвестный репитер", + "community_title": "Сообщество", + "community_create": "Создать сообщество", + "community_createDesc": "Создать новое сообщество и поделиться через QR-код.", + "community_join": "Присоединиться", + "community_joinTitle": "Присоединиться к сообществу", + "community_joinConfirmation": "Вы хотите присоединиться к сообществу \"{name}\"?", + "community_scanQr": "Сканировать QR-код сообщества", + "community_scanInstructions": "Наведите камеру на QR-код сообщества", + "community_showQr": "Показать QR-код", + "community_publicChannel": "Публичный канал сообщества", + "community_hashtagChannel": "Хэштег-канал сообщества", + "community_name": "Имя сообщества", + "community_enterName": "Введите имя сообщества", + "community_created": "Сообщество \"{name}\" создано", + "community_joined": "Присоединились к сообществу \"{name}\"", + "community_qrTitle": "Поделиться сообществом", + "community_qrInstructions": "Отсканируйте этот QR-код, чтобы присоединиться к \"{name}\"", + "community_hashtagPrivacyHint": "Хэштег-каналы сообщества доступны только его участникам", + "community_invalidQrCode": "Недопустимый QR-код сообщества", + "community_alreadyMember": "Уже участник", + "community_alreadyMemberMessage": "Вы уже участник сообщества \"{name}\".", + "community_addPublicChannel": "Добавить публичный канал сообщества", + "community_addPublicChannelHint": "Автоматически добавить публичный канал для этого сообщества", + "community_noCommunities": "Вы ещё не присоединились ни к одному сообществу", + "community_scanOrCreate": "Отсканируйте QR-код или создайте сообщество, чтобы начать", + "community_manageCommunities": "Управление сообществами", + "community_delete": "Покинуть сообщество", + "community_deleteConfirm": "Покинуть \"{name}\"?", + "community_deleteChannelsWarning": "Это также удалит {count} канал(ов) и их сообщения.", + "community_deleted": "Покинули сообщество \"{name}\"", + "community_regenerateSecret": "Пересоздать секрет", + "community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.", + "community_regenerate": "Пересоздать", + "community_secretRegenerated": "Секрет пересоздан для \"{name}\"", + "community_updateSecret": "Обновить секрет", + "community_secretUpdated": "Секрет обновлён для \"{name}\"", + "community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"", + "community_addHashtagChannel": "Добавить хэштег-канал сообщества", + "community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества", + "community_selectCommunity": "Выбрать сообщество", + "community_regularHashtag": "Обычный хэштег", + "community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)", + "community_communityHashtag": "Хэштег сообщества", + "community_communityHashtagDesc": "Доступен только участникам сообщества", + "community_forCommunity": "Для {name}", + "listFilter_tooltip": "Фильтр и сортировка", + "listFilter_sortBy": "Сортировка по", + "listFilter_latestMessages": "Последние сообщения", + "listFilter_heardRecently": "Слышали недавно", + "listFilter_az": "По алфавиту", + "listFilter_filters": "Фильтры", + "listFilter_all": "Все", + "listFilter_users": "Пользователи", + "listFilter_repeaters": "Репитеры", + "listFilter_roomServers": "Серверы комнат", + "listFilter_unreadOnly": "Только непрочитанные", + "listFilter_newGroup": "Новая группа" +} \ No newline at end of file From cfb51d96ff645175741812da2bdfe0dd8a4904b5 Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:39:49 +0100 Subject: [PATCH 016/421] More french translation updates6 --- lib/l10n/app_fr.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 43cf4ec..1b69540 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1346,7 +1346,7 @@ "listFilter_all": "Tout", "listFilter_users": "Utilisateurs", "listFilter_repeaters": "Répéteurs", - "listFilter_roomServers": "Rooms servers", + "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Messages non lus seulement", "listFilter_newGroup": "Nouveau groupe", "@neighbors_errorLoading": { From 115667a27cbde60d53e8d107434bd56d44bcc230 Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:39:59 +0100 Subject: [PATCH 017/421] More french translation updates6 --- lib/l10n/app_localizations_fr.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 918cee6..07ec4c8 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -273,7 +273,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Recharger la liste des contacts depuis l\'appareil'; @override - String get settings_rebootDevice => 'Réinitialiser l\'appareil'; + String get settings_rebootDevice => 'Redémarrer l\'appareil'; @override String get settings_rebootDeviceSubtitle => 'Redémarrer l\'appareil MeshCore'; @@ -1892,7 +1892,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Intervalle d\'annonces cryptées'; @override - String get repeater_dangerZone => 'Zone d\'alerte'; + String get repeater_dangerZone => 'Zone dangereuse'; @override String get repeater_rebootRepeater => 'Redémarrer Répéteur'; @@ -2685,7 +2685,7 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_repeaters => 'Répéteurs'; @override - String get listFilter_roomServers => 'Rooms servers'; + String get listFilter_roomServers => 'Room servers'; @override String get listFilter_unreadOnly => 'Messages non lus seulement'; From fa514533eb921770a5610534a4a8e7a61791a276 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Fri, 23 Jan 2026 17:56:06 -0700 Subject: [PATCH 018/421] feat: add ChatScrollController and JumpToBottomButton for improved chat scrolling experience - Implemented ChatScrollController to manage scroll behavior and visibility of jump-to-bottom button. - Added functionality to automatically scroll to the bottom when the keyboard opens. - Created JumpToBottomButton widget that appears when the user scrolls up, allowing quick navigation back to the bottom of the chat. --- .../reports/problems/problems-report.html | 663 ++++++++++++++++++ lib/connector/meshcore_connector.dart | 5 + lib/helpers/chat_scroll_controller.dart | 68 ++ lib/screens/channel_chat_screen.dart | 244 ++++--- lib/screens/chat_screen.dart | 211 ++++-- lib/widgets/gif_message.dart | 67 +- lib/widgets/jump_to_bottom_button.dart | 29 + 7 files changed, 1097 insertions(+), 190 deletions(-) create mode 100644 android/build/reports/problems/problems-report.html create mode 100644 lib/helpers/chat_scroll_controller.dart create mode 100644 lib/widgets/jump_to_bottom_button.dart diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..2220133 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 29f92af..28b1082 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -146,6 +146,7 @@ class MeshCoreConnector extends ChangeNotifier { final Set _knownContactKeys = {}; final Map _contactLastReadMs = {}; final Map _channelLastReadMs = {}; + bool _unreadStateLoaded = false; final Map _pendingRepeaterAcks = {}; String? _activeContactKey; int? _activeChannelIndex; @@ -317,6 +318,7 @@ class MeshCoreConnector extends ChangeNotifier { } int getUnreadCountForContactKey(String contactKeyHex) { + if (!_unreadStateLoaded) return 0; if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return 0; final messages = _conversations[contactKeyHex]; if (messages == null || messages.isEmpty) return 0; @@ -336,6 +338,7 @@ class MeshCoreConnector extends ChangeNotifier { } int getUnreadCountForChannelIndex(int channelIndex) { + if (!_unreadStateLoaded) return 0; final messages = _channelMessages[channelIndex]; if (messages == null || messages.isEmpty) return 0; final lastReadMs = _channelLastReadMs[channelIndex] ?? 0; @@ -350,6 +353,7 @@ class MeshCoreConnector extends ChangeNotifier { } int getTotalUnreadCount() { + if (!_unreadStateLoaded) return 0; var total = 0; // Count unread contact messages for (final contact in _contacts) { @@ -381,6 +385,7 @@ class MeshCoreConnector extends ChangeNotifier { _channelLastReadMs ..clear() ..addAll(await _unreadStore.loadChannelLastRead()); + _unreadStateLoaded = true; notifyListeners(); } diff --git a/lib/helpers/chat_scroll_controller.dart b/lib/helpers/chat_scroll_controller.dart new file mode 100644 index 0000000..d2c73fb --- /dev/null +++ b/lib/helpers/chat_scroll_controller.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class ChatScrollController extends ScrollController { + final ValueNotifier showJumpToBottom = ValueNotifier(false); + VoidCallback? onScrollNearTop; + + static const _bottomThreshold = 100.0; + static const _topThreshold = 50.0; + + ChatScrollController() { + addListener(_handleScroll); + } + + void _handleScroll() { + if (!hasClients) return; + final pos = position; + + // With reverse: true, position 0 is bottom, maxScrollExtent is top + // Show jump button when scrolled away from bottom (position > threshold) + final isAtBottom = pos.pixels <= _bottomThreshold; + if (showJumpToBottom.value == isAtBottom) { + showJumpToBottom.value = !isAtBottom; + } + + // Pagination trigger when scrolled near top (maxScrollExtent) + if (pos.pixels >= pos.maxScrollExtent - _topThreshold) { + onScrollNearTop?.call(); + } + } + + void jumpToBottom() { + if (hasClients && position.maxScrollExtent > 0) { + animateTo( + 0, // With reverse: true, position 0 is bottom + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } + + void handleKeyboardOpen() { + // Simple: just scroll to bottom when keyboard opens + if (hasClients) { + animateTo( + 0, // With reverse: true, position 0 is bottom + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + } + + void scrollToBottomIfAtBottom() { + // Only scroll if jump button is NOT showing (i.e., already at bottom) + if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) { + animateTo( + 0, // With reverse: true, position 0 is bottom + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + } + + @override + void dispose() { + showJumpToBottom.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 380c7ce..f45ed34 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -8,6 +8,7 @@ import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; @@ -17,6 +18,7 @@ import '../models/channel_message.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; +import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -35,42 +37,51 @@ class ChannelChatScreen extends StatefulWidget { class _ChannelChatScreenState extends State { final TextEditingController _textController = TextEditingController(); - final ScrollController _scrollController = ScrollController(); + final ChatScrollController _scrollController = ChatScrollController(); + final FocusNode _textFieldFocusNode = FocusNode(); ChannelMessage? _replyingToMessage; final Map _messageKeys = {}; + bool _isLoadingOlder = false; @override void initState() { super.initState(); + _textFieldFocusNode.addListener(_onTextFieldFocusChange); + _scrollController.onScrollNearTop = _loadOlderMessages; SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; context.read().setActiveChannel(widget.channel.index); - - // Scroll to bottom when opening channel chat - use SchedulerBinding for next frame - if (_scrollController.hasClients) { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - } }); } + void _onTextFieldFocusChange() { + if (_textFieldFocusNode.hasFocus && mounted) { + _scrollController.handleKeyboardOpen(); + } + } + + Future _loadOlderMessages() async { + if (_isLoadingOlder) return; + setState(() => _isLoadingOlder = true); + + final connector = context.read(); + await connector.loadOlderChannelMessages(widget.channel.index); + + if (mounted) { + setState(() => _isLoadingOlder = false); + } + } + @override void dispose() { context.read().setActiveChannel(null); + _textFieldFocusNode.removeListener(_onTextFieldFocusChange); + _textFieldFocusNode.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); } - void _scrollToBottom() { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - } - void _setReplyingTo(ChannelMessage message) { setState(() { _replyingToMessage = message; @@ -155,10 +166,6 @@ class _ChannelChatScreenState extends State { builder: (context, connector, child) { final messages = connector.getChannelMessages(widget.channel); - SchedulerBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - if (messages.isEmpty) { return Center( child: Column( @@ -192,20 +199,51 @@ class _ChannelChatScreenState extends State { ); } - return ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.all(8), - itemCount: messages.length, - itemBuilder: (context, index) { - final message = messages[index]; - if (!_messageKeys.containsKey(message.messageId)) { - _messageKeys[message.messageId] = GlobalKey(); - } - return Container( - key: _messageKeys[message.messageId]!, - child: _buildMessageBubble(message), - ); - }, + // Reverse messages so newest appear at bottom with reverse: true + final reversedMessages = messages.reversed.toList(); + final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); + + // Auto-scroll to bottom if user is already at bottom + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.scrollToBottomIfAtBottom(); + }); + + return Stack( + children: [ + ListView.builder( + reverse: true, // List grows from bottom up + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: itemCount, + itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ); + } + final messageIndex = index; + final message = reversedMessages[messageIndex]; + if (!_messageKeys.containsKey(message.messageId)) { + _messageKeys[message.messageId] = GlobalKey(); + } + return Container( + key: _messageKeys[message.messageId]!, + child: _buildMessageBubble(message), + ); + }, + ), + JumpToBottomButton( + scrollController: _scrollController, + ), + ], ); }, ), @@ -243,7 +281,9 @@ class _ChannelChatScreenState extends State { onTap: () => _showMessagePathInfo(message), onLongPress: () => _showMessageActions(message), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.65, ), @@ -257,15 +297,20 @@ class _ChannelChatScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOutgoing) ...[ - Text( - message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, top: 4, bottom: 4) + : EdgeInsets.zero, + child: Text( + message.senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), ), ), - const SizedBox(height: 4), + if (gifId == null) const SizedBox(height: 4), ], if (message.replyToMessageId != null) ...[ _buildReplyPreview(message), @@ -274,12 +319,15 @@ class _ChannelChatScreenState extends State { if (poi != null) _buildPoiMessage(context, poi, isOutgoing) else if (gifId != null) - GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, - fallbackTextColor: isOutgoing - ? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ), ) else Linkify( @@ -299,46 +347,56 @@ class _ChannelChatScreenState extends State { ), if (displayPath.isNotEmpty) ...[ const SizedBox(height: 4), - Text( - 'via ${_formatPathPrefixes(displayPath)}', - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', + style: TextStyle(fontSize: 11, color: Colors.grey[600]), + ), ), ], const SizedBox(height: 4), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon(Icons.repeat, size: 12, color: Colors.grey[600]), - const SizedBox(width: 2), + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, right: 8, bottom: 4) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - '${message.repeatCount}', - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), ), + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon(Icons.repeat, size: 12, color: Colors.grey[600]), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle(fontSize: 11, color: Colors.grey[600]), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], + ), ), ], ), @@ -377,8 +435,7 @@ class _ChannelChatScreenState extends State { url: 'https://media.giphy.com/media/$gifId/giphy.gif', backgroundColor: colorScheme.surfaceContainerHighest, fallbackTextColor: previewTextColor, - width: 120, - height: 80, + maxSize: 80, ), ); } else if (poi != null) { @@ -703,14 +760,16 @@ class _ChannelChatScreenState extends State { return Row( children: [ Expanded( - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, - fallbackTextColor: - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - width: 160, - height: 110, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerHighest, + fallbackTextColor: + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), const SizedBox(width: 8), @@ -724,6 +783,7 @@ class _ChannelChatScreenState extends State { return TextField( controller: _textController, + focusNode: _textFieldFocusNode, inputFormatters: [ Utf8LengthLimitingTextInputFormatter(maxBytes), ], diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 079f25d..efc3537 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -11,6 +11,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; @@ -22,6 +23,7 @@ import 'map_screen.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; +import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; import '../widgets/path_selection_dialog.dart'; import '../utils/app_logger.dart'; @@ -38,25 +40,44 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final _textController = TextEditingController(); - final _scrollController = ScrollController(); + final _scrollController = ChatScrollController(); + final _textFieldFocusNode = FocusNode(); + bool _isLoadingOlder = false; @override void initState() { super.initState(); + _textFieldFocusNode.addListener(_onTextFieldFocusChange); + _scrollController.onScrollNearTop = _loadOlderMessages; SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; context.read().setActiveContact(widget.contact.publicKeyHex); - - // Scroll to bottom when opening chat use SchedulerBinding for next frame - if (_scrollController.hasClients) { - _scrollController.jumpTo(_scrollController.position.maxScrollExtent); - } }); } + void _onTextFieldFocusChange() { + if (_textFieldFocusNode.hasFocus && mounted) { + _scrollController.handleKeyboardOpen(); + } + } + + Future _loadOlderMessages() async { + if (_isLoadingOlder) return; + setState(() => _isLoadingOlder = true); + + final connector = context.read(); + await connector.loadOlderMessages(widget.contact.publicKeyHex); + + if (mounted) { + setState(() => _isLoadingOlder = false); + } + } + @override void dispose() { context.read().setActiveContact(null); + _textFieldFocusNode.removeListener(_onTextFieldFocusChange); + _textFieldFocusNode.dispose(); _textController.dispose(); _scrollController.dispose(); super.dispose(); @@ -169,9 +190,16 @@ class _ChatScreenState extends State { return Column( children: [ Expanded( - child: messages.isEmpty - ? _buildEmptyState() - : _buildMessageList(messages, connector), + child: Stack( + children: [ + messages.isEmpty + ? _buildEmptyState() + : _buildMessageList(messages, connector), + JumpToBottomButton( + scrollController: _scrollController, + ), + ], + ), ), _buildInputBar(connector), ], @@ -203,13 +231,37 @@ class _ChatScreenState extends State { } Widget _buildMessageList(List messages, MeshCoreConnector connector) { + // Reverse messages so newest appear at bottom with reverse: true + final reversedMessages = messages.reversed.toList(); + final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); + + // Auto-scroll to bottom if user is already at bottom + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.scrollToBottomIfAtBottom(); + }); + return ListView.builder( + reverse: true, // List grows from bottom up controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - itemCount: messages.length, + itemCount: itemCount, itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ); + } + final messageIndex = index; Contact contact = widget.contact; - final message = messages[index]; + final message = reversedMessages[messageIndex]; String fourByteHex = ''; if (widget.contact.type == advTypeRoom) { contact = _resolveContactFrom4Bytes( @@ -258,13 +310,15 @@ class _ChatScreenState extends State { return Row( children: [ Expanded( - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: colorScheme.surfaceContainerHighest, - fallbackTextColor: - colorScheme.onSurface.withValues(alpha: 0.6), - width: 160, - height: 110, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: colorScheme.surfaceContainerHighest, + fallbackTextColor: + colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), const SizedBox(width: 8), @@ -278,6 +332,7 @@ class _ChatScreenState extends State { return TextField( controller: _textController, + focusNode: _textFieldFocusNode, inputFormatters: [ Utf8LengthLimitingTextInputFormatter(maxBytes), ], @@ -339,16 +394,6 @@ class _ChatScreenState extends State { text, ); _textController.clear(); - - Future.delayed(const Duration(milliseconds: 100), () { - if (_scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - ); - } - }); } @@ -960,7 +1005,9 @@ class _MessageBubble extends StatelessWidget { ], Flexible( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.65, ), @@ -972,23 +1019,31 @@ class _MessageBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOutgoing) ...[ - Text( - senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.primary, + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, top: 4, bottom: 4) + : EdgeInsets.zero, + child: Text( + senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), ), ), - const SizedBox(height: 4), + if (gifId == null) const SizedBox(height: 4), ], if (poi != null) _buildPoiMessage(context, poi, textColor, metaColor) else if (gifId != null) - GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: bubbleColor, - fallbackTextColor: textColor.withValues(alpha: 0.7), + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues(alpha: 0.7), + ), ) else Linkify( @@ -1009,48 +1064,58 @@ class _MessageBubble extends StatelessWidget { ), if (isOutgoing && message.retryCount > 0) ...[ const SizedBox(height: 4), - Text( - context.l10n.chat_retryCount(message.retryCount, 4), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount(message.retryCount, 4), + style: TextStyle( + fontSize: 10, + color: metaColor, + fontWeight: FontWeight.w500, + ), ), ), ], const SizedBox(height: 4), - Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, - ), - ), - if (isOutgoing) ...[ - const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing ? metaColor : Colors.green[700], - ), + Padding( + padding: gifId != null + ? const EdgeInsets.only(left: 8, right: 8, bottom: 4) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + _formatTime(message.timestamp), style: TextStyle( - fontSize: 9, - color: isOutgoing ? metaColor : Colors.green[700], + fontSize: 10, + color: metaColor, ), ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + _buildStatusIcon(metaColor), + ], + if (message.tripTimeMs != null && + message.status == MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10, + color: isOutgoing ? metaColor : Colors.green[700], + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9, + color: isOutgoing ? metaColor : Colors.green[700], + ), + ), + ], ], - ], + ), ), ], ), diff --git a/lib/widgets/gif_message.dart b/lib/widgets/gif_message.dart index 402565f..b98bdc6 100644 --- a/lib/widgets/gif_message.dart +++ b/lib/widgets/gif_message.dart @@ -6,16 +6,14 @@ class GifMessage extends StatefulWidget { final String url; final Color backgroundColor; final Color fallbackTextColor; - final double width; - final double height; + final double maxSize; const GifMessage({ super.key, required this.url, required this.backgroundColor, required this.fallbackTextColor, - this.width = 200, - this.height = 140, + this.maxSize = 200, }); @override @@ -122,6 +120,28 @@ class _GifMessageState extends State { @override Widget build(BuildContext context) { + // Calculate display size based on image aspect ratio + // Use 4:3 placeholder aspect ratio during loading to minimize layout shifts + double displayWidth = widget.maxSize; + double displayHeight = widget.maxSize * 0.75; + + if (_image != null) { + final imageWidth = _image!.width.toDouble(); + final imageHeight = _image!.height.toDouble(); + final aspectRatio = imageWidth / imageHeight; + + // Fit within maxSize, calculating dimensions from aspect ratio + if (aspectRatio >= 1) { + // Wider than tall: constrain by width + displayWidth = widget.maxSize; + displayHeight = displayWidth / aspectRatio; + } else { + // Taller than wide: constrain by height + displayHeight = widget.maxSize; + displayWidth = displayHeight * aspectRatio; + } + } + Widget content; if (_error != null) { @@ -151,33 +171,30 @@ class _GifMessageState extends State { } else { content = RawImage( image: _image, - fit: BoxFit.cover, - width: widget.width, - height: widget.height, + fit: BoxFit.contain, + width: displayWidth, + height: displayHeight, ); } return GestureDetector( onTap: _togglePause, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Container( - color: widget.backgroundColor, - width: widget.width, - height: widget.height, - child: Stack( - fit: StackFit.expand, - children: [ - content, - if (_isPaused && _image != null) - Container( - color: Colors.black.withValues(alpha: 0.2), - child: const Center( - child: Icon(Icons.pause, color: Colors.white70, size: 28), - ), + child: Container( + color: widget.backgroundColor, + width: displayWidth, + height: displayHeight, + child: Stack( + fit: StackFit.expand, + children: [ + content, + if (_isPaused && _image != null) + Container( + color: Colors.black.withValues(alpha: 0.2), + child: const Center( + child: Icon(Icons.pause, color: Colors.white70, size: 28), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/widgets/jump_to_bottom_button.dart b/lib/widgets/jump_to_bottom_button.dart new file mode 100644 index 0000000..08614f3 --- /dev/null +++ b/lib/widgets/jump_to_bottom_button.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import '../helpers/chat_scroll_controller.dart'; + +class JumpToBottomButton extends StatelessWidget { + final ChatScrollController scrollController; + + const JumpToBottomButton({ + super.key, + required this.scrollController, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: scrollController.showJumpToBottom, + builder: (context, show, _) { + if (!show) return const SizedBox.shrink(); + return Positioned( + right: 16, + bottom: 16, + child: FloatingActionButton.small( + onPressed: scrollController.jumpToBottom, + child: const Icon(Icons.keyboard_arrow_down), + ), + ); + }, + ); + } +} From 09e1cd2b8dba49f92c877f2b5fcf23b8b78d42e5 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 24 Jan 2026 00:17:18 -0700 Subject: [PATCH 019/421] fix: improve BLE scanning reliability and filter out own node from contacts list improve text scaling --- lib/connector/meshcore_connector.dart | 11 +++++++ lib/screens/contacts_screen.dart | 45 ++++++++++++++++++--------- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 28b1082..0d5b4b1 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -625,6 +625,17 @@ class MeshCoreConnector extends ChangeNotifier { _scanResults.clear(); _setState(MeshCoreConnectionState.scanning); + // Ensure any previous scan is fully stopped + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); + + // On iOS/macOS, add a small delay to allow BLE stack to reset + // This prevents cached results from interfering with new scans + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + await Future.delayed(const Duration(milliseconds: 300)); + } + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { _scanResults.clear(); for (var result in results) { diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index e91cd94..54f819c 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -313,6 +313,14 @@ class _ContactsScreenState extends State return matchesContactQuery(contact, _searchQuery); }).toList(); + // Filter out own node from the list + if (connector.selfPublicKey != null) { + final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!); + filtered = filtered.where((contact) { + return contact.publicKeyHex != selfPubKeyHex; + }).toList(); + } + if (_typeFilter != ContactTypeFilter.all) { filtered = filtered.where(_matchesTypeFilter).toList(); } @@ -863,21 +871,30 @@ class _ContactTile extends StatelessWidget { subtitle: Text( '${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey', ), - trailing: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(height: 4), - ], - Text( - _formatLastSeen(context, lastSeen), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + // Clamp text scaling in trailing section to prevent overflow while + // maintaining accessibility. Primary content (title/subtitle) scales normally. + trailing: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), - ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (unreadCount > 0) ...[ + UnreadBadge(count: unreadCount), + const SizedBox(height: 4), + ], + Text( + _formatLastSeen(context, lastSeen), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + ], + ), ), onTap: onTap, onLongPress: onLongPress, From f0d34f7503eb7775e778a3e63b671946047d454a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 24 Jan 2026 00:27:45 -0700 Subject: [PATCH 020/421] Update Russian localization for improved pluralization and add new chat link handling messages - Enhanced pluralization rules for "hops" in various contexts to better reflect Russian grammar. - Added new localization strings for chat link handling, including error messages and confirmation prompts. - Ensured consistency in the use of plural forms across the application. --- lib/l10n/app_localizations.dart | 5 + lib/l10n/app_localizations_ru.dart | 1962 ++++++++++++++-------------- lib/l10n/app_ru.arb | 33 +- 3 files changed, 1039 insertions(+), 961 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d52830c..5e2d8fe 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -14,6 +14,7 @@ import 'app_localizations_it.dart'; import 'app_localizations_nl.dart'; import 'app_localizations_pl.dart'; import 'app_localizations_pt.dart'; +import 'app_localizations_ru.dart'; import 'app_localizations_sk.dart'; import 'app_localizations_sl.dart'; import 'app_localizations_sv.dart'; @@ -114,6 +115,7 @@ abstract class AppLocalizations { Locale('nl'), Locale('pl'), Locale('pt'), + Locale('ru'), Locale('sk'), Locale('sl'), Locale('sv'), @@ -4705,6 +4707,7 @@ class _AppLocalizationsDelegate 'nl', 'pl', 'pt', + 'ru', 'sk', 'sl', 'sv', @@ -4736,6 +4739,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) { return AppLocalizationsPl(); case 'pt': return AppLocalizationsPt(); + case 'ru': + return AppLocalizationsRu(); case 'sk': return AppLocalizationsSk(); case 'sl': diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index b1e6c1f..ae784e4 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1,2626 +1,2682 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; + // ignore_for_file: type=lint + /// The translations for Russian (`ru`). -class AppLocalizationsEn extends AppLocalizations { +class AppLocalizationsRu extends AppLocalizations { AppLocalizationsRu([String locale = 'ru']) : super(locale); - + @override String get appTitle => 'MeshCore Open'; - + @override String get nav_contacts => 'Контакты'; - + @override String get nav_channels => 'Каналы'; - + @override String get nav_map => 'Карта'; - + @override String get common_cancel => 'Отмена'; - + @override String get common_ok => 'OK'; - + @override String get common_connect => 'Коннект'; - + @override String get common_unknownDevice => 'Неизвестное устройство'; - + @override String get common_save => 'Сохранить'; - + @override String get common_delete => 'Удалить'; - + @override String get common_close => 'Закрыть'; - + @override String get common_edit => 'Изменить'; - + @override String get common_add => 'Добавить'; - + @override String get common_settings => 'Настройки'; - + @override String get common_disconnect => 'Отключить'; - + @override String get common_connected => 'Подключено'; - + @override String get common_disconnected => 'Отключено'; - + @override String get common_create => 'Создать'; - + @override String get common_continue => 'Продолжить'; - + @override String get common_share => 'Поделиться'; - + @override String get common_copy => 'Копировать'; - + @override String get common_retry => 'Повторить'; - + @override String get common_hide => 'Скрыть'; - + @override String get common_remove => 'Убрать'; - + @override String get common_enable => 'Включить'; - + @override String get common_disable => 'Выключить'; - + @override String get common_reboot => 'Перезагрузить'; - + @override String get common_loading => 'Загрузка...'; - + @override String get common_notAvailable => '—'; - + @override String common_voltageValue(String volts) { return '$volts В'; -} - + } + @override String common_percentValue(int percent) { return '$percent%'; -} - + } + @override String get scanner_title => 'MeshCore Open'; - + @override String get scanner_scanning => 'Поиск устройств...'; - + @override String get scanner_connecting => 'Подключение...'; - + @override String get scanner_disconnecting => 'Отключение...'; - + @override String get scanner_notConnected => 'Не подключено'; - + @override String scanner_connectedTo(String deviceName) { return 'Подключено к $deviceName'; -} - + } + @override String get scanner_searchingDevices => 'Поиск устройств MeshCore...'; - + @override String get scanner_tapToScan => 'Нажмите для поиска MeshCore устройств'; - + @override String scanner_connectionFailed(String error) { return 'Подключение не удалось: $error'; -} - + } + @override String get scanner_stop => 'Стоп'; - + @override String get scanner_scan => 'Сканирование'; - + @override String get device_quickSwitch => 'Быстрое переключение'; - + @override String get device_meshcore => 'MeshCore'; - + @override String get settings_title => 'Настройки'; - + @override String get settings_deviceInfo => 'Информация об устройстве'; - + @override String get settings_appSettings => 'Настройки приложения'; - + @override String get settings_appSettingsSubtitle => 'Уведомления, сообщения и настройки карты'; - + @override String get settings_nodeSettings => 'Настройки ноды'; - + @override String get settings_nodeName => 'Имя ноды'; - + @override String get settings_nodeNameNotSet => 'Не установлено'; - + @override String get settings_nodeNameHint => 'Введите имя ноды'; - + @override String get settings_nodeNameUpdated => 'Имя обновлено'; - + @override String get settings_radioSettings => 'Настройки радио'; - + @override String get settings_radioSettingsSubtitle => 'Частота, мощность и коэффициент распространения'; - + @override String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; - + @override String get settings_location => 'Позиция'; - + @override String get settings_locationSubtitle => 'Координаты GPS'; - + @override String get settings_locationUpdated => 'Позиция и настройки GPS обновлены'; - + @override - String get settings_locationBothRequired => - 'Введите широту и долготу.'; - + String get settings_locationBothRequired => 'Введите широту и долготу.'; + @override String get settings_locationInvalid => 'Неверная широта или долгота.'; - + @override String get settings_locationGPSEnable => 'Включить GPS'; - + @override String get settings_locationGPSEnableSubtitle => 'Включение GPS для автоматического обновления позиции.'; - + @override - String get settings_locationIntervalSec => 'Интервал для позиционирования GPS (секунды)'; - + String get settings_locationIntervalSec => + 'Интервал для позиционирования GPS (секунды)'; + @override String get settings_locationIntervalInvalid => 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; - + @override String get settings_latitude => 'Широта'; - + @override String get settings_longitude => 'Долгота'; - + @override String get settings_privacyMode => 'Режим конфиденциальности'; - + @override String get settings_privacyModeSubtitle => 'Скрыть имя/позицию в анонсировании'; - + @override String get settings_privacyModeToggle => 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; - + @override String get settings_privacyModeEnabled => 'Режим конфиденциальности включен'; - + @override - String get settings_privacyModeDisabled => 'Режим конфиденциальности выключен'; - + String get settings_privacyModeDisabled => + 'Режим конфиденциальности выключен'; + @override String get settings_actions => 'Действия'; - + @override String get settings_sendAdvertisement => 'Отправить анонсирование'; - + @override - String get settings_sendAdvertisementSubtitle => 'Отправить анонсирование о присутствии сейчас'; - + String get settings_sendAdvertisementSubtitle => + 'Отправить анонсирование о присутствии сейчас'; + @override String get settings_advertisementSent => 'Анонсирование отправлено'; - + @override String get settings_syncTime => 'Синхронизация времени'; - + @override String get settings_syncTimeSubtitle => 'Синхронизировать время с телефоном'; - + @override String get settings_timeSynchronized => 'Время синхронизировано'; - + @override String get settings_refreshContacts => 'Обновить контакты'; - + @override String get settings_refreshContactsSubtitle => 'Перезагрузить список контактов с устройства'; - + @override String get settings_rebootDevice => 'Перезагрузить устройство'; - + @override - String get settings_rebootDeviceSubtitle => 'Перезапустить устройство MeshCore'; - + String get settings_rebootDeviceSubtitle => + 'Перезапустить устройство MeshCore'; + @override String get settings_rebootDeviceConfirm => 'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.'; - + @override String get settings_debug => 'Отладка'; - + @override String get settings_bleDebugLog => 'Журнал отладки BLE'; - + @override String get settings_bleDebugLogSubtitle => 'Команды BLE, ответы и сырые данные'; - + @override String get settings_appDebugLog => 'Журнал отладки приложения'; - + @override String get settings_appDebugLogSubtitle => 'Сообщения отладки приложения'; - + @override String get settings_about => 'О программе'; - + @override String settings_aboutVersion(String version) { return 'MeshCore Open v$version'; -} - + } + @override String get settings_aboutLegalese => '2026 MeshCore Open Source Project'; - + @override String get settings_aboutDescription => 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; - + @override String get settings_infoName => 'Имя'; - + @override String get settings_infoId => 'ID'; - + @override String get settings_infoStatus => 'Статус'; - + @override String get settings_infoBattery => 'Батарея'; - + @override String get settings_infoPublicKey => 'Публичный ключ'; - + @override String get settings_infoContactsCount => 'Количество контактов'; - + @override String get settings_infoChannelCount => 'Количество каналов'; - + @override String get settings_presets => 'Пресеты'; - + @override String get settings_preset915Mhz => '915 МГц'; - + @override String get settings_preset868Mhz => '868 МГц'; - + @override String get settings_preset433Mhz => '433 МГц'; - + @override String get settings_frequency => 'Частота (МГц)'; - + @override String get settings_frequencyHelper => '300.0 – 2500.0'; - + @override String get settings_frequencyInvalid => 'Недопустимая частота (300–2500 МГц)'; - + @override String get settings_bandwidth => 'Полоса пропускания'; - + @override String get settings_spreadingFactor => 'Коэффициент расширения'; - + @override String get settings_codingRate => 'Коэффициент кодирования'; - + @override String get settings_txPower => 'Мощность передачи (дБм)'; - + @override String get settings_txPowerHelper => '0 – 22'; - + @override - String get settings_txPowerInvalid => 'Недопустимая мощность передачи (0–22 дБм)'; - + String get settings_txPowerInvalid => + 'Недопустимая мощность передачи (0–22 дБм)'; + @override String get settings_longRange => 'Дальний радиус'; - + @override String get settings_fastSpeed => 'Высокая скорость'; - + @override String settings_error(String message) { return 'Ошибка: $message'; -} - + } + @override String get appSettings_title => 'Настройки приложения'; - + @override String get appSettings_appearance => 'Внешний вид'; - + @override String get appSettings_theme => 'Тема'; - + @override String get appSettings_themeSystem => 'Как в системе'; - + @override String get appSettings_themeLight => 'Светлая'; - + @override String get appSettings_themeDark => 'Тёмная'; - + @override String get appSettings_language => 'Язык'; - + @override String get appSettings_languageSystem => 'Как в системе'; - + @override String get appSettings_languageEn => 'Английский'; - + @override String get appSettings_languageFr => 'Французский'; - + @override String get appSettings_languageEs => 'Испанский'; - + @override String get appSettings_languageDe => 'Немецкий'; - + @override String get appSettings_languagePl => 'Польский'; - + @override String get appSettings_languageSl => 'Словенский'; - + @override String get appSettings_languagePt => 'Португальский'; - + @override String get appSettings_languageIt => 'Итальянский'; - + @override String get appSettings_languageZh => 'Китайский'; - + @override String get appSettings_languageSv => 'Шведский'; - + @override String get appSettings_languageNl => 'Нидерландский'; - + @override String get appSettings_languageSk => 'Словацкий'; - + @override String get appSettings_languageBg => 'Болгарский'; - - @override - String get appSettings_languageRu => 'Русский'; - + @override String get appSettings_notifications => 'Уведомления'; - + @override String get appSettings_enableNotifications => 'Включить уведомления'; - + @override String get appSettings_enableNotificationsSubtitle => 'Получать уведомления о сообщениях и оповещениях'; - + @override String get appSettings_notificationPermissionDenied => - 'Разрешение на уведомления отклонено'; - + 'Разрешение на уведомления отклонено'; + @override String get appSettings_notificationsEnabled => 'Уведомления включены'; - + @override String get appSettings_notificationsDisabled => 'Уведомления отключены'; - + @override String get appSettings_messageNotifications => 'Уведомления о сообщениях'; - + @override String get appSettings_messageNotificationsSubtitle => 'Показывать уведомление при получении новых сообщений'; - + @override String get appSettings_channelMessageNotifications => 'Уведомления о сообщениях в каналах'; - + @override String get appSettings_channelMessageNotificationsSubtitle => 'Показывать уведомление при получении сообщений в каналах'; - + @override String get appSettings_advertisementNotifications => 'Уведомления об анонсированиях'; - + @override String get appSettings_advertisementNotificationsSubtitle => 'Показывать уведомление при обнаружении новых нод'; - + @override String get appSettings_messaging => 'Обмен сообщениями'; - + @override - String get appSettings_clearPathOnMaxRetry => 'Сбросить маршрут после максимального числа попыток'; - + String get appSettings_clearPathOnMaxRetry => + 'Сбросить маршрут после максимального числа попыток'; + @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; - + 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; + @override String get appSettings_pathsWillBeCleared => 'Маршруты будут сброшены после 5 неудачных попыток'; - + @override String get appSettings_pathsWillNotBeCleared => 'Маршруты не будут автоматически сбрасываться'; - + @override - String get appSettings_autoRouteRotation => 'Автоматическое переключение маршрутов'; - + String get appSettings_autoRouteRotation => + 'Автоматическое переключение маршрутов'; + @override String get appSettings_autoRouteRotationSubtitle => 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; - + @override String get appSettings_autoRouteRotationEnabled => 'Автоматическое переключение маршрутов включено'; - + @override String get appSettings_autoRouteRotationDisabled => 'Автоматическое переключение маршрутов отключено'; - + @override String get appSettings_battery => 'Батарея'; - + @override String get appSettings_batteryChemistry => 'Химия батареи'; - + @override String appSettings_batteryChemistryPerDevice(String deviceName) { return 'Установить для устройства ($deviceName)'; -} - + } + @override String get appSettings_batteryChemistryConnectFirst => 'Подключитесь к устройству, чтобы выбрать'; - + @override String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 В)'; - + @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 В)'; - + @override String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; - + @override String get appSettings_mapDisplay => 'Отображение карты'; - + @override String get appSettings_showRepeaters => 'Показывать репитеры'; - + @override String get appSettings_showRepeatersSubtitle => 'Отображать репитеры на карте'; - + @override String get appSettings_showChatNodes => 'Показывать чат-ноды'; - + @override String get appSettings_showChatNodesSubtitle => 'Отображать чат-ноды на карте'; - + @override String get appSettings_showOtherNodes => 'Показывать другие ноды'; - + @override String get appSettings_showOtherNodesSubtitle => 'Отображать другие типы нод на карте'; - + @override String get appSettings_timeFilter => 'Фильтр по времени'; - + @override String get appSettings_timeFilterShowAll => 'Показывать все ноды'; - + @override String appSettings_timeFilterShowLast(int hours) { return 'Показывать ноды за последние $hours ч'; -} - + } + @override String get appSettings_mapTimeFilter => 'Временной фильтр карты'; - + @override String get appSettings_showNodesDiscoveredWithin => 'Показывать ноды, обнаруженные за:'; - + @override String get appSettings_allTime => 'Всё время'; - + @override String get appSettings_lastHour => 'Последний час'; - + @override String get appSettings_last6Hours => 'Последние 6 часов'; - + @override String get appSettings_last24Hours => 'Последние 24 часа'; - + @override String get appSettings_lastWeek => 'Последнюю неделю'; - + @override String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; - + @override String get appSettings_noAreaSelected => 'Область не выбрана'; - + @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { return 'Область выбрана (масштаб $minZoom–$maxZoom)'; -} - + } + @override String get appSettings_debugCard => 'Отладка'; - + @override String get appSettings_appDebugLogging => 'Журнал отладки приложения'; - + @override String get appSettings_appDebugLoggingSubtitle => 'Записывать отладочные сообщения приложения для диагностики'; - + @override - String get appSettings_appDebugLoggingEnabled => 'Журнал отладки приложения включён'; - + String get appSettings_appDebugLoggingEnabled => + 'Журнал отладки приложения включён'; + @override String get appSettings_appDebugLoggingDisabled => 'Журнал отладки приложения отключён'; - + @override String get contacts_title => 'Контакты'; - + @override String get contacts_noContacts => 'Контактов пока нет'; - + @override String get contacts_contactsWillAppear => 'Контакты появятся, когда устройства начнут рассылать оповещения'; - + @override String get contacts_searchContacts => 'Поиск контактов...'; - + @override String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; - + @override String get contacts_noContactsFound => 'Контакты или группы не найдены'; - + @override String get contacts_deleteContact => 'Удалить контакт'; - + @override String contacts_removeConfirm(String contactName) { return 'Удалить $contactName из контактов?'; -} - + } + @override String get contacts_manageRepeater => 'Управление репитером'; - + @override String get contacts_manageRoom => 'Управление сервером комнат'; - + @override String get contacts_roomLogin => 'Вход на сервер комнат'; - + @override String get contacts_openChat => 'Открыть чат'; - + @override String get contacts_editGroup => 'Изменить группу'; - + @override String get contacts_deleteGroup => 'Удалить группу'; - + @override String contacts_deleteGroupConfirm(String groupName) { return 'Удалить \"$groupName\"?'; -} - + } + @override String get contacts_newGroup => 'Новая группа'; - + @override String get contacts_groupName => 'Имя группы'; - + @override String get contacts_groupNameRequired => 'Имя группы обязательно'; - + @override String contacts_groupAlreadyExists(String name) { return 'Группа \"$name\" уже существует'; -} - + } + @override String get contacts_filterContacts => 'Фильтр контактов...'; - + @override - String get contacts_noContactsMatchFilter => 'Нет контактов, соответствующих фильтру'; - + String get contacts_noContactsMatchFilter => + 'Нет контактов, соответствующих фильтру'; + @override String get contacts_noMembers => 'Нет участников'; - + @override String get contacts_lastSeenNow => 'Видели только что'; - + @override String contacts_lastSeenMinsAgo(int minutes) { return 'Видели $minutes мин назад'; -} - + } + @override String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; - + @override String contacts_lastSeenHoursAgo(int hours) { return 'Видели $hours ч назад'; -} - + } + @override String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; - + @override String contacts_lastSeenDaysAgo(int days) { return 'Видели $days дн. назад'; -} - + } + @override String get channels_title => 'Каналы'; - + @override String get channels_noChannelsConfigured => 'Каналы не настроены'; - + @override String get channels_addPublicChannel => 'Добавить публичный канал'; - + @override String get channels_searchChannels => 'Поиск каналов...'; - + @override String get channels_noChannelsFound => 'Каналы не найдены'; - + @override String channels_channelIndex(int index) { return 'Канал $index'; -} - + } + @override String get channels_hashtagChannel => 'Хэштег-канал'; - + @override String get channels_public => 'Публичный'; - + @override String get channels_private => 'Приватный'; - + @override String get channels_publicChannel => 'Публичный канал'; - + @override String get channels_privateChannel => 'Приватный канал'; - + @override String get channels_editChannel => 'Изменить канал'; - + @override String get channels_deleteChannel => 'Удалить канал'; - + @override String channels_deleteChannelConfirm(String name) { return 'Удалить \"$name\"? Это действие нельзя отменить.'; -} - + } + @override String channels_channelDeleted(String name) { return 'Канал \"$name\" удалён'; -} - + } + @override String get channels_addChannel => 'Добавить канал'; - + @override String get channels_channelIndexLabel => 'Индекс канала'; - + @override String get channels_channelName => 'Имя канала'; - + @override String get channels_usePublicChannel => 'Использовать публичный канал'; - + @override String get channels_standardPublicPsk => 'Стандартный публичный PSK'; - + @override String get channels_pskHex => 'PSK (Hex)'; - + @override String get channels_generateRandomPsk => 'Сгенерировать случайный PSK'; - + @override String get channels_enterChannelName => 'Введите имя канала'; - + @override - String get channels_pskMustBe32Hex => 'PSK должен содержать 32 шестнадцатеричных символа'; - + String get channels_pskMustBe32Hex => + 'PSK должен содержать 32 шестнадцатеричных символа'; + @override String channels_channelAdded(String name) { return 'Канал \"$name\" добавлен'; -} - + } + @override String channels_editChannelTitle(int index) { return 'Изменить канал $index'; -} - + } + @override String get channels_smazCompression => 'Сжатие SMAZ'; - + @override String channels_channelUpdated(String name) { return 'Канал \"$name\" обновлён'; -} - + } + @override String get channels_publicChannelAdded => 'Публичный канал добавлен'; - + @override String get channels_sortBy => 'Сортировка'; - + @override String get channels_sortManual => 'Вручную'; - + @override String get channels_sortAZ => 'По алфавиту'; - + @override String get channels_sortLatestMessages => 'По последним сообщениям'; - + @override String get channels_sortUnread => 'По непрочитанным'; - + @override String get channels_createPrivateChannel => 'Создать приватный канал'; - + @override String get channels_createPrivateChannelDesc => 'Защищён секретным ключом.'; - + @override - String get channels_joinPrivateChannel => 'Присоединиться к приватному каналу'; - + String get channels_joinPrivateChannel => + 'Присоединиться к приватному каналу'; + @override - String get channels_joinPrivateChannelDesc => 'Введите секретный ключ вручную.'; - + String get channels_joinPrivateChannelDesc => + 'Введите секретный ключ вручную.'; + @override String get channels_joinPublicChannel => 'Присоединиться к публичному каналу'; - + @override - String get channels_joinPublicChannelDesc => 'К этому каналу может присоединиться любой.'; - + String get channels_joinPublicChannelDesc => + 'К этому каналу может присоединиться любой.'; + @override String get channels_joinHashtagChannel => 'Присоединиться к хэштег-каналу'; - + @override String get channels_joinHashtagChannelDesc => 'К хэштег-каналам может присоединиться любой.'; - + @override String get channels_scanQrCode => 'Сканировать QR-код'; - + @override String get channels_scanQrCodeComingSoon => 'Скоро будет'; - + @override String get channels_enterHashtag => 'Введите хэштег'; - + @override String get channels_hashtagHint => 'например, #команда'; - + @override String get chat_noMessages => 'Сообщений пока нет'; - + @override String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; - + @override String get chat_originalMessageNotFound => 'Исходное сообщение не найдено'; - + @override String chat_replyingTo(String name) { return 'Ответ для $name'; -} - + } + @override String chat_replyTo(String name) { return 'Ответить $name'; -} - + } + @override String get chat_location => 'Местоположение'; - + @override String chat_sendMessageTo(String contactName) { return 'Отправить сообщение $contactName'; -} - + } + @override String get chat_typeMessage => 'Напишите сообщение...'; - + @override String chat_messageTooLong(int maxBytes) { return 'Сообщение слишком длинное (макс. $maxBytes байт).'; -} - + } + @override String get chat_messageCopied => 'Сообщение скопировано'; - + @override String get chat_messageDeleted => 'Сообщение удалено'; - + @override String get chat_retryingMessage => 'Повтор отправки сообщения'; - + @override String chat_retryCount(int current, int max) { return 'Попытка $current/$max'; -} - + } + @override String get chat_sendGif => 'Отправить GIF'; - + @override String get chat_reply => 'Ответить'; - + @override String get chat_addReaction => 'Добавить реакцию'; - + @override String get chat_me => 'Я'; - + @override String get emojiCategorySmileys => 'Смайлы'; - + @override String get emojiCategoryGestures => 'Жесты'; - + @override String get emojiCategoryHearts => 'Сердечки'; - + @override String get emojiCategoryObjects => 'Предметы'; - + @override String get gifPicker_title => 'Выберите GIF'; - + @override String get gifPicker_searchHint => 'Поиск GIF...'; - + @override String get gifPicker_poweredBy => 'Работает на GIPHY'; - + @override String get gifPicker_noGifsFound => 'GIF не найдены'; - + @override String get gifPicker_failedLoad => 'Не удалось загрузить GIF'; - + @override String get gifPicker_failedSearch => 'Не удалось выполнить поиск GIF'; - + @override String get gifPicker_noInternet => 'Нет подключения к интернету'; - + @override String get debugLog_appTitle => 'Журнал отладки приложения'; - + @override String get debugLog_bleTitle => 'Журнал отладки BLE'; - + @override String get debugLog_copyLog => 'Копировать журнал'; - + @override String get debugLog_clearLog => 'Очистить журнал'; - + @override String get debugLog_copied => 'Журнал отладки скопирован'; - + @override String get debugLog_bleCopied => 'Журнал BLE скопирован'; - + @override String get debugLog_noEntries => 'Журнал отладки пока пуст'; - + @override String get debugLog_enableInSettings => 'Включите запись журнала отладки в настройках'; - + @override String get debugLog_frames => 'Фреймы'; - + @override String get debugLog_rawLogRx => 'Сырой журнал приёма'; - + @override String get debugLog_noBleActivity => 'Активность BLE пока отсутствует'; - + @override String debugFrame_length(int count) { return 'Длина фрейма: $count байт'; -} - + } + @override String debugFrame_command(String value) { return 'Команда: 0x$value'; -} - + } + @override String get debugFrame_textMessageHeader => 'Фрейм текстового сообщения:'; - + @override String debugFrame_destinationPubKey(String pubKey) { return '- Публичный ключ получателя: $pubKey'; -} - + } + @override String debugFrame_timestamp(int timestamp) { return '- Временная метка: $timestamp'; -} - + } + @override String debugFrame_flags(String value) { return '- Флаги: 0x$value'; -} - + } + @override - String debugFrame_textType(int type, String label) { + String debugFrame_textType(int type, String label) { return '- Тип текста: $type ($label)'; -} - + } + @override String get debugFrame_textTypeCli => 'CLI'; - + @override String get debugFrame_textTypePlain => 'Обычный'; - + @override String debugFrame_text(String text) { return '- Текст: \"$text\"'; -} - + } + @override String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; - + @override String get chat_pathManagement => 'Управление маршрутами'; - + @override String get chat_routingMode => 'Режим маршрутизации'; - + @override String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; - + @override String get chat_forceFloodMode => 'Принудительный режим рассылки'; - + @override - String get chat_recentAckPaths => 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; - + String get chat_recentAckPaths => + 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; + @override String get chat_pathHistoryFull => 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; - + @override String get chat_hopSingular => 'хоп'; - + @override String get chat_hopPlural => 'хопов'; - + @override String chat_hopsCount(int count) { - String _temp0 = intl.Intl.pluralLogic( -count, -locale: localeName, -other: 'хопов', -one: 'хоп', -); + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', + ); return '$count $_temp0'; -} - + } + @override String get chat_successes => 'успешно'; - + @override String get chat_removePath => 'Удалить маршрут'; - + @override String get chat_noPathHistoryYet => - 'История маршрутов пока пуста. -Отправьте сообщение, чтобы обнаружить маршруты.'; - + 'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; + @override String get chat_pathActions => 'Действия с маршрутом:'; - + @override String get chat_setCustomPath => 'Указать маршрут вручную'; - + @override String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи'; - + @override String get chat_clearPath => 'Очистить маршрут'; - + @override - String get chat_clearPathSubtitle => 'Принудительно обновить маршрут при следующей отправке'; - + String get chat_clearPathSubtitle => + 'Принудительно обновить маршрут при следующей отправке'; + @override String get chat_pathCleared => 'Маршрут очищен. Следующее сообщение обновит маршрут.'; - + @override - String get chat_floodModeSubtitle => 'Используйте переключатель маршрутизации в панели приложения'; - + String get chat_floodModeSubtitle => + 'Используйте переключатель маршрутизации в панели приложения'; + @override String get chat_floodModeEnabled => 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; - + @override String get chat_fullPath => 'Полный маршрут'; - + @override String get chat_pathDetailsNotAvailable => 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; - + @override - String chat_pathSetHops(int hopCount, String status) { - String _temp0 = intl.Intl.pluralLogic( -hopCount, -locale: localeName, -other: 'хопов', -one: 'хоп', -); + String chat_pathSetHops(int hopCount, String status) { + String _temp0 = intl.Intl.pluralLogic( + hopCount, + locale: localeName, + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', + ); return 'Маршрут установлен: $hopCount $_temp0 — $status'; -} - + } + @override - String get chat_pathSavedLocally => 'Сохранено локально. Подключитесь для синхронизации.'; - + String get chat_pathSavedLocally => + 'Сохранено локально. Подключитесь для синхронизации.'; + @override String get chat_pathDeviceConfirmed => 'Подтверждено устройством.'; - + @override String get chat_pathDeviceNotConfirmed => 'Ещё не подтверждено устройством.'; - + @override String get chat_type => 'Тип'; - + @override String get chat_path => 'Маршрут'; - + @override String get chat_publicKey => 'Публичный ключ'; - + @override String get chat_compressOutgoingMessages => 'Сжимать исходящие сообщения'; - + @override String get chat_floodForced => 'Рассылка (принудительно)'; - + @override String get chat_directForced => 'Прямой (принудительно)'; - + @override String chat_hopsForced(int count) { return '$count хоп(ов) (принудительно)'; -} - + } + @override String get chat_floodAuto => 'Рассылка (авто)'; - + @override String get chat_direct => 'Прямой'; - + @override String get chat_poiShared => 'Точка интереса отправлена'; - + @override String chat_unread(int count) { return 'Непрочитанных: $count'; -} - + } + + @override + String get chat_openLink => 'Открыть ссылку?'; + + @override + String get chat_openLinkConfirmation => + 'Хотите открыть эту ссылку в вашем браузере?'; + + @override + String get chat_open => 'Открыть'; + + @override + String chat_couldNotOpenLink(String url) { + return 'Не удалось открыть ссылку: $url'; + } + + @override + String get chat_invalidLink => 'Неправильный формат ссылки'; + @override String get map_title => 'Карта нод'; - + @override String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; - + @override String get map_nodesNeedGps => 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; - + @override String map_nodesCount(int count) { return 'Нод: $count'; -} - + } + @override String map_pinsCount(int count) { return 'Меток: $count'; -} - + } + @override String get map_chat => 'Чат'; - + @override String get map_repeater => 'Репитер'; - + @override String get map_room => 'Комната'; - + @override String get map_sensor => 'Сенсор'; - + @override String get map_pinDm => 'Метка (ЛС)'; - + @override String get map_pinPrivate => 'Метка (Приватная)'; - + @override String get map_pinPublic => 'Метка (Публичная)'; - + @override String get map_lastSeen => 'Последнее появление'; - + @override String get map_disconnectConfirm => 'Вы уверены, что хотите отключиться от этого устройства?'; - + @override String get map_from => 'От'; - + @override String get map_source => 'Источник'; - + @override String get map_flags => 'Флаги'; - + @override String get map_shareMarkerHere => 'Поделиться меткой здесь'; - + @override String get map_pinLabel => 'Метка'; - + @override String get map_label => 'Подпись'; - + @override String get map_pointOfInterest => 'Точка интереса'; - + @override String get map_sendToContact => 'Отправить контакту'; - + @override String get map_sendToChannel => 'Отправить в канал'; - + @override String get map_noChannelsAvailable => 'Нет доступных каналов'; - + @override String get map_publicLocationShare => 'Публичная передача местоположения'; - + @override String map_publicLocationShareConfirm(String channelLabel) { return 'Вы собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; -} - + } + @override String get map_connectToShareMarkers => 'Подключитесь к устройству, чтобы делиться метками'; - + @override String get map_filterNodes => 'Фильтр нод'; - + @override String get map_nodeTypes => 'Типы нод'; - + @override String get map_chatNodes => 'Чат-ноды'; - + @override String get map_repeaters => 'Репитеры'; - + @override String get map_otherNodes => 'Другие ноды'; - + @override String get map_keyPrefix => 'Префикс ключа'; - + @override String get map_filterByKeyPrefix => 'Фильтр по префиксу ключа'; - + @override String get map_publicKeyPrefix => 'Префикс публичного ключа'; - + @override String get map_markers => 'Метки'; - + @override String get map_showSharedMarkers => 'Показывать общие метки'; - + @override String get map_lastSeenTime => 'Время последнего появления'; - + @override String get map_sharedPin => 'Общая метка'; - + @override String get map_joinRoom => 'Присоединиться к комнате'; - + @override String get map_manageRepeater => 'Управление репитером'; - + @override String get mapCache_title => 'Кэш офлайн-карты'; - + @override - String get mapCache_selectAreaFirst => 'Сначала выберите область для кэширования'; - + String get mapCache_selectAreaFirst => + 'Сначала выберите область для кэширования'; + @override - String get mapCache_noTilesToDownload => 'Нет плиток для загрузки в этой области'; - + String get mapCache_noTilesToDownload => + 'Нет плиток для загрузки в этой области'; + @override String get mapCache_downloadTilesTitle => 'Загрузить плитки'; - + @override String mapCache_downloadTilesPrompt(int count) { return 'Загрузить $count плиток для офлайн-использования?'; -} - + } + @override String get mapCache_downloadAction => 'Загрузить'; - + @override String mapCache_cachedTiles(int count) { return 'Закэшировано $count плиток'; -} - + } + @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { return 'Закэшировано $downloaded плиток ($failed не загружено)'; -} - + } + @override String get mapCache_clearOfflineCacheTitle => 'Очистить офлайн-кэш'; - + @override - String get mapCache_clearOfflineCachePrompt => 'Удалить все закэшированные плитки карты?'; - + String get mapCache_clearOfflineCachePrompt => + 'Удалить все закэшированные плитки карты?'; + @override String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; - + @override String get mapCache_noAreaSelected => 'Область не выбрана'; - + @override String get mapCache_cacheArea => 'Область кэширования'; - + @override String get mapCache_useCurrentView => 'Использовать текущий вид'; - + @override String get mapCache_zoomRange => 'Диапазон масштаба'; - + @override String mapCache_estimatedTiles(int count) { return 'Оценочное количество плиток: $count'; -} - + } + @override String mapCache_downloadedTiles(int completed, int total) { return 'Загружено $completed из $total'; -} - + } + @override String get mapCache_downloadTilesButton => 'Загрузить плитки'; - + @override String get mapCache_clearCacheButton => 'Очистить кэш'; - + @override String mapCache_failedDownloads(int count) { return 'Неудачных загрузок: $count'; -} - + } + @override String mapCache_boundsLabel( - String north, - String south, - String east, - String west, -) { + String north, + String south, + String east, + String west, + ) { return 'С $north, Ю $south, В $east, З $west'; -} - + } + @override String get time_justNow => 'Только что'; - + @override String time_minutesAgo(int minutes) { - return '${minutes} мин назад'; -} - + return '$minutes мин назад'; + } + @override String time_hoursAgo(int hours) { - return '${hours} ч назад'; -} - + return '$hours ч назад'; + } + @override String time_daysAgo(int days) { - return '${days} дн. назад'; -} - + return '$days дн. назад'; + } + @override String get time_hour => 'час'; - + @override String get time_hours => 'часов'; - + @override String get time_day => 'день'; - + @override String get time_days => 'дней'; - + @override String get time_week => 'неделя'; - + @override String get time_weeks => 'недель'; - + @override String get time_month => 'месяц'; - + @override String get time_months => 'месяцев'; - + @override String get time_minutes => 'минут'; - + @override String get time_allTime => 'Всё время'; - + @override String get dialog_disconnect => 'Отключиться'; - + @override String get dialog_disconnectConfirm => 'Вы уверены, что хотите отключиться от этого устройства?'; - + @override String get login_repeaterLogin => 'Вход в репитер'; - + @override String get login_roomLogin => 'Вход на сервер комнат'; - + @override String get login_password => 'Пароль'; - + @override String get login_enterPassword => 'Введите пароль'; - + @override String get login_savePassword => 'Сохранить пароль'; - + @override String get login_savePasswordSubtitle => 'Пароль будет надёжно сохранён на этом устройстве'; - + @override String get login_repeaterDescription => 'Введите пароль репитера для доступа к настройкам и статусу.'; - + @override String get login_roomDescription => 'Введите пароль комнаты для доступа к настройкам и статусу.'; - + @override String get login_routing => 'Маршрутизация'; - + @override String get login_routingMode => 'Режим маршрутизации'; - + @override - String get login_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; - + String get login_autoUseSavedPath => + 'Авто (использовать сохранённый маршрут)'; + @override String get login_forceFloodMode => 'Принудительный режим рассылки'; - + @override String get login_managePaths => 'Управление маршрутами'; - + @override String get login_login => 'Войти'; - + @override String login_attempt(int current, int max) { return 'Попытка $current/$max'; -} - + } + @override String login_failed(String error) { return 'Ошибка входа: $error'; -} - + } + @override String get login_failedMessage => 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; - + @override String get common_reload => 'Обновить'; - + @override String get common_clear => 'Очистить'; - + @override String path_currentPath(String path) { return 'Текущий маршрут: $path'; -} - + } + @override String path_usingHopsPath(int count) { - String _temp0 = intl.Intl.pluralLogic( -count, -locale: localeName, -other: 'хопов', -one: 'хоп', -); + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', + ); return 'Используется маршрут из $count $_temp0'; -} - + } + @override String get path_enterCustomPath => 'Введите маршрут вручную'; - + @override String get path_currentPathLabel => 'Текущий маршрут'; - + @override String get path_hexPrefixInstructions => 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; - + @override String get path_hexPrefixExample => 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; - + @override String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)'; - + @override String get path_helperMaxHops => 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; - + @override String get path_selectFromContacts => 'Или выберите из контактов:'; - + @override String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; - + @override String get path_customPathsRequire => 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; - + @override String path_invalidHexPrefixes(String prefixes) { return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; -} - + } + @override String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.'; - + @override String get path_setPath => 'Установить маршрут'; - + @override String get repeater_management => 'Управление репитером'; - + @override String get room_management => 'Управление сервером комнат'; - + @override String get repeater_managementTools => 'Инструменты управления'; - + @override String get repeater_status => 'Статус'; - + @override String get repeater_statusSubtitle => 'Просмотр статуса, статистики и соседей репитера'; - + @override String get repeater_telemetry => 'Телеметрия'; - + @override String get repeater_telemetrySubtitle => 'Просмотр телеметрии датчиков и системной статистики'; - + @override String get repeater_cli => 'CLI'; - + @override String get repeater_cliSubtitle => 'Отправка команд репитеру'; - + @override String get repeater_neighbours => 'Соседи'; - + @override String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.'; - + @override String get repeater_settings => 'Настройки'; - + @override String get repeater_settingsSubtitle => 'Настройка параметров репитера'; - + @override String get repeater_statusTitle => 'Статус репитера'; - + @override String get repeater_routingMode => 'Режим маршрутизации'; - + @override - String get repeater_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; - + String get repeater_autoUseSavedPath => + 'Авто (использовать сохранённый маршрут)'; + @override String get repeater_forceFloodMode => 'Принудительный режим рассылки'; - + @override String get repeater_pathManagement => 'Управление маршрутами'; - + @override String get repeater_refresh => 'Обновить'; - + @override String get repeater_statusRequestTimeout => 'Время ожидания статуса истекло.'; - + @override String repeater_errorLoadingStatus(String error) { return 'Ошибка загрузки статуса: $error'; -} - + } + @override String get repeater_systemInformation => 'Системная информация'; - + @override String get repeater_battery => 'Батарея'; - + @override String get repeater_clockAtLogin => 'Время (при входе)'; - + @override String get repeater_uptime => 'Время работы'; - + @override String get repeater_queueLength => 'Длина очереди'; - + @override String get repeater_debugFlags => 'Флаги отладки'; - + @override String get repeater_radioStatistics => 'Радиостатистика'; - + @override String get repeater_lastRssi => 'Последний RSSI'; - + @override String get repeater_lastSnr => 'Последний SNR'; - + @override String get repeater_noiseFloor => 'Уровень шума'; - + @override String get repeater_txAirtime => 'Время эфира (передача)'; - + @override String get repeater_rxAirtime => 'Время эфира (приём)'; - + @override String get repeater_packetStatistics => 'Статистика пакетов'; - + @override String get repeater_sent => 'Отправлено'; - + @override String get repeater_received => 'Получено'; - + @override String get repeater_duplicates => 'Дубликаты'; - + @override String repeater_daysHoursMinsSecs( -int days, -int hours, -int minutes, -int seconds, -) { - return '$days дн. ${hours}ч ${minutes}м ${seconds}с'; -} - + int days, + int hours, + int minutes, + int seconds, + ) { + return '$days дн. $hoursч $minutesм $secondsс'; + } + @override - String repeater_packetTxTotal(int total, String flood, String direct) { + String repeater_packetTxTotal(int total, String flood, String direct) { return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; -} - + } + @override - String repeater_packetRxTotal(int total, String flood, String direct) { + String repeater_packetRxTotal(int total, String flood, String direct) { return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; -} - + } + @override - String repeater_duplicatesFloodDirect(String flood, String direct) { + String repeater_duplicatesFloodDirect(String flood, String direct) { return 'Рассылка: $flood, Прямые: $direct'; -} - + } + @override String repeater_duplicatesTotal(int total) { return 'Всего: $total'; -} - + } + @override String get repeater_settingsTitle => 'Настройки репитера'; - + @override String get repeater_basicSettings => 'Основные настройки'; - + @override String get repeater_repeaterName => 'Имя репитера'; - + @override String get repeater_repeaterNameHelper => 'Отображаемое имя этого репитера'; - + @override String get repeater_adminPassword => 'Пароль администратора'; - + @override String get repeater_adminPasswordHelper => 'Пароль с полным доступом'; - + @override String get repeater_guestPassword => 'Гостевой пароль'; - + @override - String get repeater_guestPasswordHelper => 'Пароль для доступа только для чтения'; - + String get repeater_guestPasswordHelper => + 'Пароль для доступа только для чтения'; + @override String get repeater_radioSettings => 'Настройки радио'; - + @override String get repeater_frequencyMhz => 'Частота (МГц)'; - + @override String get repeater_frequencyHelper => '300–2500 МГц'; - + @override String get repeater_txPower => 'Мощность передачи'; - + @override String get repeater_txPowerHelper => '1–30 дБм'; - + @override String get repeater_bandwidth => 'Полоса пропускания'; - + @override String get repeater_spreadingFactor => 'Коэффициент расширения'; - + @override String get repeater_codingRate => 'Коэффициент кодирования'; - + @override String get repeater_locationSettings => 'Настройки местоположения'; - + @override String get repeater_latitude => 'Широта'; - + @override - String get repeater_latitudeHelper => 'В десятичных градусах (напр., 37.7749)'; - + String get repeater_latitudeHelper => + 'В десятичных градусах (напр., 37.7749)'; + @override String get repeater_longitude => 'Долгота'; - + @override - String get repeater_longitudeHelper => 'В десятичных градусах (напр., -122.4194)'; - + String get repeater_longitudeHelper => + 'В десятичных градусах (напр., -122.4194)'; + @override String get repeater_features => 'Функции'; - + @override String get repeater_packetForwarding => 'Пересылка пакетов'; - + @override String get repeater_packetForwardingSubtitle => 'Разрешить репитеру пересылать пакеты'; - + @override String get repeater_guestAccess => 'Гостевой доступ'; - + @override - String get repeater_guestAccessSubtitle => 'Разрешить гостевой доступ только для чтения'; - + String get repeater_guestAccessSubtitle => + 'Разрешить гостевой доступ только для чтения'; + @override String get repeater_privacyMode => 'Режим конфиденциальности'; - + @override String get repeater_privacyModeSubtitle => 'Скрывать имя/местоположение в оповещениях'; - + @override String get repeater_advertisementSettings => 'Настройки анонсирования'; - + @override String get repeater_localAdvertInterval => 'Интервал локальных анонсирований'; - + @override String repeater_localAdvertIntervalMinutes(int minutes) { return '$minutes минут'; -} - + } + @override - String get repeater_floodAdvertInterval => 'Интервал анонсирований рассылкой (flood)'; - + String get repeater_floodAdvertInterval => + 'Интервал анонсирований рассылкой (flood)'; + @override String repeater_floodAdvertIntervalHours(int hours) { return '$hours часов'; -} - + } + @override String get repeater_encryptedAdvertInterval => 'Интервал зашифрованных анонсирований'; - + @override String get repeater_dangerZone => 'Опасная зона'; - + @override String get repeater_rebootRepeater => 'Перезагрузить репитер'; - + @override - String get repeater_rebootRepeaterSubtitle => 'Перезапустить устройство репитера'; - + String get repeater_rebootRepeaterSubtitle => + 'Перезапустить устройство репитера'; + @override String get repeater_rebootRepeaterConfirm => 'Вы уверены, что хотите перезагрузить этот репитер?'; - + @override String get repeater_regenerateIdentityKey => 'Пересоздать ключ идентификации'; - + @override String get repeater_regenerateIdentityKeySubtitle => 'Сгенерировать новую пару публичного/приватного ключей'; - + @override String get repeater_regenerateIdentityKeyConfirm => 'Это создаст новую идентичность для репитера. Продолжить?'; - + @override String get repeater_eraseFileSystem => 'Стереть файловую систему'; - + @override String get repeater_eraseFileSystemSubtitle => 'Отформатировать файловую систему репитера'; - + @override String get repeater_eraseFileSystemConfirm => 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; - + @override String get repeater_eraseSerialOnly => 'Очистка доступна только через последовательную консоль.'; - + @override String repeater_commandSent(String command) { return 'Команда отправлена: $command'; -} - + } + @override String repeater_errorSendingCommand(String error) { return 'Ошибка отправки команды: $error'; -} - + } + @override String get repeater_confirm => 'Подтвердить'; - + @override String get repeater_settingsSaved => 'Настройки успешно сохранены'; - + @override String repeater_errorSavingSettings(String error) { return 'Ошибка сохранения настроек: $error'; -} - + } + @override String get repeater_refreshBasicSettings => 'Обновить основные настройки'; - + @override String get repeater_refreshRadioSettings => 'Обновить настройки радио'; - + @override String get repeater_refreshTxPower => 'Обновить мощность передачи'; - + @override - String get repeater_refreshLocationSettings => 'Обновить настройки местоположения'; - + String get repeater_refreshLocationSettings => + 'Обновить настройки местоположения'; + @override String get repeater_refreshPacketForwarding => 'Обновить пересылку пакетов'; - + @override String get repeater_refreshGuestAccess => 'Обновить гостевой доступ'; - + @override String get repeater_refreshPrivacyMode => 'Обновить режим конфиденциальности'; - + @override String get repeater_refreshAdvertisementSettings => 'Обновить настройки анонсирований'; - + @override String repeater_refreshed(String label) { return '$label обновлён'; -} - + } + @override String repeater_errorRefreshing(String label) { return 'Ошибка обновления $label'; -} - + } + @override String get repeater_cliTitle => 'CLI репитера'; - + @override String get repeater_debugNextCommand => 'Отладка следующей команды'; - + @override String get repeater_commandHelp => 'Справка по командам'; - + @override String get repeater_clearHistory => 'Очистить историю'; - + @override String get repeater_noCommandsSent => 'Команды ещё не отправлялись'; - + @override String get repeater_typeCommandOrUseQuick => 'Введите команду ниже или используйте быстрые команды'; - + @override String get repeater_enterCommandHint => 'Введите команду...'; - + @override String get repeater_previousCommand => 'Предыдущая команда'; - + @override String get repeater_nextCommand => 'Следующая команда'; - + @override String get repeater_enterCommandFirst => 'Сначала введите команду'; - + @override String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; - + @override String repeater_cliCommandError(String error) { return 'Ошибка: $error'; -} - + } + @override String get repeater_cliQuickGetName => 'Получить имя'; - + @override String get repeater_cliQuickGetRadio => 'Получить радио'; - + @override String get repeater_cliQuickGetTx => 'Получить TX'; - + @override String get repeater_cliQuickNeighbors => 'Соседи'; - + @override String get repeater_cliQuickVersion => 'Версия'; - + @override String get repeater_cliQuickAdvertise => 'Анонсировать'; - + @override String get repeater_cliQuickClock => 'Время'; - + @override String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; - + @override String get repeater_cliHelpReboot => 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; - + @override String get repeater_cliHelpClock => 'Показывает текущее время по часам устройства.'; - + @override String get repeater_cliHelpPassword => 'Устанавливает новый пароль администратора для устройства.'; - + @override String get repeater_cliHelpVersion => 'Показывает версию устройства и дату сборки прошивки.'; - + @override String get repeater_cliHelpClearStats => 'Сбрасывает различные счётчики статистики в ноль.'; - + @override - String get repeater_cliHelpSetAf => 'Устанавливает коэффициент времени в эфире.'; - + String get repeater_cliHelpSetAf => + 'Устанавливает коэффициент времени в эфире.'; + @override String get repeater_cliHelpSetTx => 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; - + @override String get repeater_cliHelpSetRepeat => 'Включает или отключает роль репитера для этой ноды.'; - + @override String get repeater_cliHelpSetAllowReadOnly => '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; - + @override String get repeater_cliHelpSetFloodMax => 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; - + @override String get repeater_cliHelpSetIntThresh => 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; - + @override String get repeater_cliHelpSetAgcResetInterval => 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetMultiAcks => 'Включает или отключает функцию «двойных ACK».'; - + @override String get repeater_cliHelpSetAdvertInterval => 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetFloodAdvertInterval => 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetGuestPassword => 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; - + @override String get repeater_cliHelpSetName => 'Устанавливает имя в оповещениях.'; - + @override String get repeater_cliHelpSetLat => 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; - + @override String get repeater_cliHelpSetLon => 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; - + @override String get repeater_cliHelpSetRadio => 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; - + @override String get repeater_cliHelpSetRxDelay => 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; - + @override String get repeater_cliHelpSetTxDelay => 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; - + @override String get repeater_cliHelpSetDirectTxDelay => 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; - + @override String get repeater_cliHelpSetBridgeEnabled => 'Включить/выключить мост.'; - + @override String get repeater_cliHelpSetBridgeDelay => 'Установить задержку перед ретрансляцией пакетов.'; - + @override String get repeater_cliHelpSetBridgeSource => 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; - + @override String get repeater_cliHelpSetBridgeBaud => 'Установить скорость последовательного соединения для мостов RS232.'; - + @override String get repeater_cliHelpSetBridgeSecret => 'Установить секрет моста для мостов ESP-NOW.'; - + @override String get repeater_cliHelpSetAdcMultiplier => 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; - + @override String get repeater_cliHelpTempRadio => 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; - + @override String get repeater_cliHelpSetPerm => 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; - + @override String get repeater_cliHelpGetBridgeType => 'Получает тип моста: none, rs232, espnow'; - + @override String get repeater_cliHelpLogStart => 'Начинает запись пакетов в файловую систему.'; - + @override - String get repeater_cliHelpLogStop => 'Останавливает запись пакетов в файловую систему.'; - + String get repeater_cliHelpLogStop => + 'Останавливает запись пакетов в файловую систему.'; + @override String get repeater_cliHelpLogErase => 'Удаляет журналы пакетов из файловой системы.'; - + @override String get repeater_cliHelpNeighbors => 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; - + @override String get repeater_cliHelpNeighborRemove => 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; - + @override String get repeater_cliHelpRegion => '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; - + @override String get repeater_cliHelpRegionLoad => 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; - + @override String get repeater_cliHelpRegionGet => 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; - + @override String get repeater_cliHelpRegionPut => 'Добавляет или обновляет определение региона с заданным именем.'; - + @override String get repeater_cliHelpRegionRemove => 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; - + @override String get repeater_cliHelpRegionAllowf => 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; - + @override String get repeater_cliHelpRegionDenyf => 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; - + @override String get repeater_cliHelpRegionHome => 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; - + @override - String get repeater_cliHelpRegionHomeSet => 'Устанавливает «домашний» регион.'; - + String get repeater_cliHelpRegionHomeSet => + 'Устанавливает «домашний» регион.'; + @override String get repeater_cliHelpRegionSave => 'Сохраняет список/карту регионов в память.'; - + @override String get repeater_cliHelpGps => 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; - + @override String get repeater_cliHelpGpsOnOff => 'Переключает состояние питания GPS.'; - + @override - String get repeater_cliHelpGpsSync => 'Синхронизирует время ноды с часами GPS.'; - + String get repeater_cliHelpGpsSync => + 'Синхронизирует время ноды с часами GPS.'; + @override String get repeater_cliHelpGpsSetLoc => 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; - + @override String get repeater_cliHelpGpsAdvert => - 'Показывает конфигурацию передачи местоположения в анонсированиях: - - none: не включать местоположение - - share: передавать GPS-координаты (из SensorManager) - - prefs: передавать координаты из настроек'; - + 'Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек'; + @override String get repeater_cliHelpGpsAdvertSet => 'Устанавливает конфигурацию передачи местоположения.'; - + @override String get repeater_commandsListTitle => 'Список команд'; - + @override String get repeater_commandsListNote => 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; - + @override String get repeater_general => 'Общие'; - + @override String get repeater_settingsCategory => 'Настройки'; - + @override String get repeater_bridge => 'Мост'; - + @override String get repeater_logging => 'Журналирование'; - + @override String get repeater_neighborsRepeaterOnly => 'Соседи (только для репитеров)'; - + @override String get repeater_regionManagementRepeaterOnly => 'Управление регионами (только для репитеров)'; - + @override String get repeater_regionNote => 'Команды регионов введены для управления определениями регионов и правами доступа.'; - + @override String get repeater_gpsManagement => 'Управление GPS'; - + @override String get repeater_gpsNote => 'Команда gps введена для управления параметрами, связанными с местоположением.'; - + @override String get telemetry_receivedData => 'Полученные телеметрические данные'; - + @override String get telemetry_requestTimeout => 'Время ожидания телеметрии истекло.'; - + @override String telemetry_errorLoading(String error) { return 'Ошибка загрузки телеметрии: $error'; -} - + } + @override String get telemetry_noData => 'Данные телеметрии недоступны.'; - + @override String telemetry_channelTitle(int channel) { return 'Канал $channel'; -} - + } + @override String get telemetry_batteryLabel => 'Батарея'; - + @override String get telemetry_voltageLabel => 'Напряжение'; - + @override String get telemetry_mcuTemperatureLabel => 'Температура МК'; - + @override String get telemetry_temperatureLabel => 'Температура'; - + @override String get telemetry_currentLabel => 'Ток'; - + @override - String telemetry_batteryValue(int percent, String volts) { - return '$percent% / ${volts}В'; -} - + String telemetry_batteryValue(int percent, String volts) { + return '$percent% / $voltsВ'; + } + @override String telemetry_voltageValue(String volts) { - return '${volts}В'; -} - + return '$voltsВ'; + } + @override String telemetry_currentValue(String amps) { - return '${amps}А'; -} - + return '$ampsА'; + } + @override - String telemetry_temperatureValue(String celsius, String fahrenheit) { + String telemetry_temperatureValue(String celsius, String fahrenheit) { return '$celsius°C / $fahrenheit°F'; -} - + } + @override String get neighbors_receivedData => 'Полученные данные о соседях'; - + @override - String get neighbors_requestTimedOut => 'Время ожидания данных о соседях истекло.'; - + String get neighbors_requestTimedOut => + 'Время ожидания данных о соседях истекло.'; + @override String neighbors_errorLoading(String error) { return 'Ошибка загрузки соседей: $error'; -} - + } + @override String get neighbors_repeatersNeighbours => 'Соседи репитеров'; - + @override String get neighbors_noData => 'Данные о соседях недоступны.'; - + @override String neighbors_unknownContact(String pubkey) { return 'Неизвестный $pubkey'; -} - + } + @override String neighbors_heardAgo(String time) { - return 'Слышали: $time назад'; -} - + return 'Слушал(а): $time назад'; + } + @override String get channelPath_title => 'Путь пакета'; - + @override String get channelPath_viewMap => 'Посмотреть на карте'; - + @override String get channelPath_otherObservedPaths => 'Другие наблюдаемые пути'; - + @override String get channelPath_repeaterHops => 'Хопы через репитеры'; - + @override String get channelPath_noHopDetails => 'Детали хопов для этого пакета не предоставлены.'; - + @override String get channelPath_messageDetails => 'Детали сообщения'; - + @override String get channelPath_senderLabel => 'Отправитель'; - + @override String get channelPath_timeLabel => 'Время'; - + @override String get channelPath_repeatsLabel => 'Повторы'; - + @override String channelPath_pathLabel(int index) { return 'Путь $index'; -} - + } + @override String get channelPath_observedLabel => 'Наблюдаемый'; - + @override - String channelPath_observedPathTitle(int index, String hops) { + String channelPath_observedPathTitle(int index, String hops) { return 'Наблюдаемый путь $index • $hops'; -} - + } + @override String get channelPath_noLocationData => 'Нет данных о местоположении'; - + @override - String channelPath_timeWithDate(int day, int month, String time) { + String channelPath_timeWithDate(int day, int month, String time) { return '$day/$month $time'; -} - + } + @override String channelPath_timeOnly(String time) { return '$time'; -} - + } + @override String get channelPath_unknownPath => 'Неизвестный'; - + @override String get channelPath_floodPath => 'Рассылка'; - + @override String get channelPath_directPath => 'Прямой'; - + @override String channelPath_observedZeroOf(int total) { return '0 из $total хопов'; -} - + } + @override String channelPath_observedSomeOf(int observed, int total) { return '$observed из $total хопов'; -} - + } + @override String get channelPath_mapTitle => 'Карта пути'; - + @override String get channelPath_noRepeaterLocations => 'Нет данных о местоположении репитеров для этого пути.'; - + @override String channelPath_primaryPath(int index) { return 'Путь $index (Основной)'; -} - + } + @override String get channelPath_pathLabelTitle => 'Путь'; - + @override String get channelPath_observedPathHeader => 'Наблюдаемый путь'; - + @override - String channelPath_selectedPathLabel(String label, String prefixes) { + String channelPath_selectedPathLabel(String label, String prefixes) { return '$label • $prefixes'; -} - + } + @override String get channelPath_noHopDetailsAvailable => 'Детали хопов для этого пакета недоступны.'; - + @override String get channelPath_unknownRepeater => 'Неизвестный репитер'; - + @override String get community_title => 'Сообщество'; - + @override String get community_create => 'Создать сообщество'; - + @override String get community_createDesc => 'Создать новое сообщество и поделиться через QR-код.'; - + @override String get community_join => 'Присоединиться'; - + @override String get community_joinTitle => 'Присоединиться к сообществу'; - + @override String community_joinConfirmation(String name) { return 'Вы хотите присоединиться к сообществу \"$name\"?'; -} - + } + @override String get community_scanQr => 'Сканировать QR-код сообщества'; - + @override String get community_scanInstructions => 'Наведите камеру на QR-код сообщества'; - + @override String get community_showQr => 'Показать QR-код'; - + @override String get community_publicChannel => 'Публичный канал сообщества'; - + @override String get community_hashtagChannel => 'Хэштег-канал сообщества'; - + @override String get community_name => 'Имя сообщества'; - + @override String get community_enterName => 'Введите имя сообщества'; - + @override String community_created(String name) { return 'Сообщество \"$name\" создано'; -} - + } + @override String community_joined(String name) { return 'Присоединились к сообществу \"$name\"'; -} - + } + @override String get community_qrTitle => 'Поделиться сообществом'; - + @override String community_qrInstructions(String name) { return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; -} - + } + @override String get community_hashtagPrivacyHint => 'Хэштег-каналы сообщества доступны только его участникам'; - + @override String get community_invalidQrCode => 'Недопустимый QR-код сообщества'; - + @override String get community_alreadyMember => 'Уже участник'; - + @override String community_alreadyMemberMessage(String name) { return 'Вы уже участник сообщества \"$name\".'; -} - + } + @override - String get community_addPublicChannel => 'Добавить публичный канал сообщества'; - + String get community_addPublicChannel => + 'Добавить публичный канал сообщества'; + @override String get community_addPublicChannelHint => 'Автоматически добавить публичный канал для этого сообщества'; - + @override - String get community_noCommunities => 'Вы ещё не присоединились ни к одному сообществу'; - + String get community_noCommunities => + 'Вы ещё не присоединились ни к одному сообществу'; + @override String get community_scanOrCreate => 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; - + @override String get community_manageCommunities => 'Управление сообществами'; - + @override String get community_delete => 'Покинуть сообщество'; - + @override String community_deleteConfirm(String name) { return 'Покинуть \"$name\"?'; -} - + } + @override String community_deleteChannelsWarning(int count) { return 'Это также удалит $count канал(ов) и их сообщения.'; -} - + } + @override String community_deleted(String name) { return 'Покинули сообщество \"$name\"'; -} - + } + @override String get community_regenerateSecret => 'Пересоздать секрет'; - + @override String community_regenerateSecretConfirm(String name) { return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; -} - + } + @override String get community_regenerate => 'Пересоздать'; - + @override String community_secretRegenerated(String name) { return 'Секрет пересоздан для \"$name\"'; -} - + } + @override String get community_updateSecret => 'Обновить секрет'; - + @override String community_secretUpdated(String name) { return 'Секрет обновлён для \"$name\"'; -} - + } + @override String community_scanToUpdateSecret(String name) { return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; -} - + } + @override String get community_addHashtagChannel => 'Добавить хэштег-канал сообщества'; - + @override String get community_addHashtagChannelDesc => 'Добавить хэштег-канал для этого сообщества'; - + @override String get community_selectCommunity => 'Выбрать сообщество'; - + @override String get community_regularHashtag => 'Обычный хэштег'; - + @override - String get community_regularHashtagDesc => 'Публичный хэштег (любой может присоединиться)'; - + String get community_regularHashtagDesc => + 'Публичный хэштег (любой может присоединиться)'; + @override String get community_communityHashtag => 'Хэштег сообщества'; - + @override - String get community_communityHashtagDesc => 'Доступен только участникам сообщества'; - + String get community_communityHashtagDesc => + 'Доступен только участникам сообщества'; + @override String community_forCommunity(String name) { - return 'Для $name'; -} - + return 'Для $name'; + } + @override String get listFilter_tooltip => 'Фильтр и сортировка'; - + @override String get listFilter_sortBy => 'Сортировка по'; - + @override String get listFilter_latestMessages => 'Последние сообщения'; - + @override String get listFilter_heardRecently => 'Слышали недавно'; - + @override String get listFilter_az => 'По алфавиту'; - + @override String get listFilter_filters => 'Фильтры'; - + @override String get listFilter_all => 'Все'; - + @override String get listFilter_users => 'Пользователи'; - + @override String get listFilter_repeaters => 'Репитеры'; - + @override String get listFilter_roomServers => 'Серверы комнат'; - + @override String get listFilter_unreadOnly => 'Только непрочитанные'; - + @override String get listFilter_newGroup => 'Новая группа'; -} \ No newline at end of file +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 7ac1e85..e0c2cbe 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,12 +1,9 @@ { "@@locale": "ru", - "appTitle": "MeshCore Open", - "nav_contacts": "Контакты", "nav_channels": "Каналы", "nav_map": "Карта", - "common_cancel": "Отмена", "common_ok": "OK", "common_connect": "Коннект", @@ -326,7 +323,7 @@ "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", "chat_hopSingular": "хоп", "chat_hopPlural": "хопов", - "chat_hopsCount": "{count} {plural, select, one {хоп} other {хопов}}", + "chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", "chat_successes": "успешно", "chat_removePath": "Удалить маршрут", "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", @@ -340,7 +337,7 @@ "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", "chat_fullPath": "Полный маршрут", "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", - "chat_pathSetHops": "Маршрут установлен: {hopCount} {plural, select, one {хоп} other {хопов}} — {status}", + "chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}", "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", "chat_pathDeviceConfirmed": "Подтверждено устройством.", "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", @@ -453,7 +450,7 @@ "common_reload": "Обновить", "common_clear": "Очистить", "path_currentPath": "Текущий маршрут: {path}", - "path_usingHopsPath": "Используется маршрут из {count} {plural, select, one {хоп} other {хопов}}", + "path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", "path_enterCustomPath": "Введите маршрут вручную", "path_currentPathLabel": "Текущий маршрут", "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", @@ -757,5 +754,25 @@ "listFilter_repeaters": "Репитеры", "listFilter_roomServers": "Серверы комнат", "listFilter_unreadOnly": "Только непрочитанные", - "listFilter_newGroup": "Новая группа" -} \ No newline at end of file + "listFilter_newGroup": "Новая группа", + "@chat_couldNotOpenLink": { + "placeholders": { + "url": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "chat_open": "Открыть", + "chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}", + "chat_openLink": "Открыть ссылку?", + "chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?", + "neighbors_heardAgo": "Слушал(а): {time} назад", + "chat_invalidLink": "Неправильный формат ссылки" +} From e95a55e4f0610b64411ab4b914ca2c727ba158e6 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 24 Jan 2026 00:45:01 -0700 Subject: [PATCH 021/421] feat: add Ukrainian localization support and improve string formatting --- lib/l10n/app_localizations.dart | 5 ++ lib/l10n/app_localizations_uk.dart | 73 ++++++++++++++++-------------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5e2d8fe..d40c791 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -18,6 +18,7 @@ import 'app_localizations_ru.dart'; import 'app_localizations_sk.dart'; import 'app_localizations_sl.dart'; import 'app_localizations_sv.dart'; +import 'app_localizations_uk.dart'; import 'app_localizations_zh.dart'; // ignore_for_file: type=lint @@ -119,6 +120,7 @@ abstract class AppLocalizations { Locale('sk'), Locale('sl'), Locale('sv'), + Locale('uk'), Locale('zh'), ]; @@ -4711,6 +4713,7 @@ class _AppLocalizationsDelegate 'sk', 'sl', 'sv', + 'uk', 'zh', ].contains(locale.languageCode); @@ -4747,6 +4750,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) { return AppLocalizationsSl(); case 'sv': return AppLocalizationsSv(); + case 'uk': + return AppLocalizationsUk(); case 'zh': return AppLocalizationsZh(); } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index a58d554..bc431ea 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -273,7 +273,8 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_rebootDevice => 'Перезавантажити пристрій'; @override - String get settings_rebootDeviceSubtitle => 'Перезавантажити пристрій MeshCore'; + String get settings_rebootDeviceSubtitle => + 'Перезавантажити пристрій MeshCore'; @override String get settings_rebootDeviceConfirm => @@ -445,9 +446,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; - @override - String get appSettings_languageUk => 'Українська'; - @override String get appSettings_notifications => 'Сповіщення'; @@ -695,7 +693,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String contacts_groupAlreadyExists(String name) { - return 'Група \"$name\" вже існує.'; + return 'Група «$name» вже існує.'; } @override @@ -780,7 +778,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String channels_channelDeleted(String name) { - return 'Канал \"$name\" видалено'; + return 'Канал «$name» видалено'; } @override @@ -813,7 +811,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String channels_channelAdded(String name) { - return 'Канал \"$name\" додано'; + return 'Канал «$name» додано'; } @override @@ -826,7 +824,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String channels_channelUpdated(String name) { - return 'Канал \"$name\" оновлено'; + return 'Канал «$name» оновлено'; } @override @@ -999,7 +997,8 @@ class AppLocalizationsUk extends AppLocalizations { String get debugLog_bleCopied => 'Журнал BLE скопійовано'; @override - String get debugLog_noEntries => 'Поки що немає записів журналу налагодження.'; + String get debugLog_noEntries => + 'Поки що немає записів журналу налагодження.'; @override String get debugLog_enableInSettings => @@ -1227,7 +1226,8 @@ class AppLocalizationsUk extends AppLocalizations { String get map_title => 'Карта вузлів'; @override - String get map_noNodesWithLocation => 'Немає вузлів з даними про розташування'; + String get map_noNodesWithLocation => + 'Немає вузлів з даними про розташування'; @override String get map_nodesNeedGps => @@ -1359,7 +1359,8 @@ class AppLocalizationsUk extends AppLocalizations { String get mapCache_title => 'Офлайн-кеш карти'; @override - String get mapCache_selectAreaFirst => 'Спершу виберіть область для кешування'; + String get mapCache_selectAreaFirst => + 'Спершу виберіть область для кешування'; @override String get mapCache_noTilesToDownload => @@ -1653,7 +1654,8 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_neighbours => 'Сусіди'; @override - String get repeater_neighboursSubtitle => 'Показати сусідів нульового стрибка.'; + String get repeater_neighboursSubtitle => + 'Показати сусідів нульового стрибка.'; @override String get repeater_settings => 'Налаштування'; @@ -1744,7 +1746,7 @@ class AppLocalizationsUk extends AppLocalizations { int minutes, int seconds, ) { - return '$days дн. ${hours} год $minutes хв $seconds с'; + return '$days дн. $hours год $minutes хв $seconds с'; } @override @@ -1777,7 +1779,8 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_repeaterName => 'Ім\'я ретранслятора'; @override - String get repeater_repeaterNameHelper => 'Показати ім\'я цього ретранслятора'; + String get repeater_repeaterNameHelper => + 'Показати ім\'я цього ретранслятора'; @override String get repeater_adminPassword => 'Пароль адміністратора'; @@ -1889,14 +1892,16 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_rebootRepeater => 'Перезавантажити ретранслятор'; @override - String get repeater_rebootRepeaterSubtitle => 'Скинути пристрій ретранслятора'; + String get repeater_rebootRepeaterSubtitle => + 'Скинути пристрій ретранслятора'; @override String get repeater_rebootRepeaterConfirm => 'Ви впевнені, що хочете перезавантажити цей ретранслятор?'; @override - String get repeater_regenerateIdentityKey => 'Перегенерувати ключ ідентичності'; + String get repeater_regenerateIdentityKey => + 'Перегенерувати ключ ідентичності'; @override String get repeater_regenerateIdentityKeySubtitle => @@ -2196,7 +2201,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_cliHelpRegionGet => - 'Шукає регіон із заданим префіксом назви (або \"\" для глобальної області). Відповідає: \"-> ім\'я-регіону (ім\'я-батька) \'F\'\"'; + 'Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім\'я-регіону (ім\'я-батька) \'F\'»'; @override String get repeater_cliHelpRegionPut => @@ -2268,8 +2273,7 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_logging => 'Логування'; @override - String get repeater_neighborsRepeaterOnly => - 'Сусіди (Тільки ретранслятор)'; + String get repeater_neighborsRepeaterOnly => 'Сусіди (Тільки ретранслятор)'; @override String get repeater_regionManagementRepeaterOnly => @@ -2322,17 +2326,17 @@ class AppLocalizationsUk extends AppLocalizations { @override String telemetry_batteryValue(int percent, String volts) { - return '$percent% / ${volts}В'; + return '$percent% / $voltsВ'; } @override String telemetry_voltageValue(String volts) { - return '${volts}В'; + return '$voltsВ'; } @override String telemetry_currentValue(String amps) { - return '${amps}А'; + return '$ampsА'; } @override @@ -2488,7 +2492,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_joinConfirmation(String name) { - return 'Ви бажаєте приєднатися до спільноти \"$name\"?'; + return 'Ви бажаєте приєднатися до спільноти «$name»?'; } @override @@ -2515,12 +2519,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_created(String name) { - return 'Спільноту \"$name\" створено'; + return 'Спільноту «$name» створено'; } @override String community_joined(String name) { - return 'Приєднався до спільноти \"$name\"'; + return 'Приєднався до спільноти «$name»'; } @override @@ -2543,7 +2547,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_alreadyMemberMessage(String name) { - return 'Ви вже є учасником \"$name\".'; + return 'Ви вже є учасником «$name».'; } @override @@ -2554,8 +2558,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Автоматично додати публічний канал для цієї спільноти'; @override - String get community_noCommunities => - 'Поки не приєднано до жодної групи.'; + String get community_noCommunities => 'Поки не приєднано до жодної групи.'; @override String get community_scanOrCreate => @@ -2569,7 +2572,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_deleteConfirm(String name) { - return 'Покинути \"$name\"?'; + return 'Покинути «$name»?'; } @override @@ -2587,7 +2590,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_deleted(String name) { - return 'Спільноту \"$name\" покинуто'; + return 'Спільноту «$name» покинуто'; } @override @@ -2595,7 +2598,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Перегенерувати секретний ключ для \"$name\"? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; + return 'Перегенерувати секретний ключ для «$name»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; } @override @@ -2603,7 +2606,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Секретний пароль для \"$name\" перегенеровано'; + return 'Секретний пароль для «$name» перегенеровано'; } @override @@ -2611,12 +2614,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String community_secretUpdated(String name) { - return 'Зміну секрету для \"$name\" оновлено'; + return 'Зміну секрету для «$name» оновлено'; } @override String community_scanToUpdateSecret(String name) { - return 'Відскануйте новий QR-код, щоб оновити пароль для \"$name\"'; + return 'Відскануйте новий QR-код, щоб оновити пароль для «$name»'; } @override @@ -2683,4 +2686,4 @@ class AppLocalizationsUk extends AppLocalizations { @override String get listFilter_newGroup => 'Нова група'; -} \ No newline at end of file +} From fee4cd13be0ca61fa63d3fd1bef9cfe4702fe74b Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 24 Jan 2026 00:52:15 -0700 Subject: [PATCH 022/421] chore: update version to 0.4.5+4 in pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index c9e9120..56556e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.4.0+4 +version: 0.4.5+4 environment: sdk: ^3.9.2 From c56cf9c3ed6ca5e95ef46b9de222f3e7d25941e9 Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 24 Jan 2026 01:07:18 -0700 Subject: [PATCH 023/421] feat: add CocoaPods support for macOS and iOS, including necessary configurations and dependencies --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile.lock | 146 ++++++++++++++++++ lib/connector/meshcore_connector.dart | 19 ++- macos/Flutter/Flutter-Debug.xcconfig | 1 + macos/Flutter/Flutter-Release.xcconfig | 1 + macos/Podfile | 42 +++++ macos/Podfile.lock | 74 +++++++++ macos/Runner.xcodeproj/project.pbxproj | 98 +++++++++++- .../contents.xcworkspacedata | 3 + 10 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 ios/Podfile.lock create mode 100644 macos/Podfile create mode 100644 macos/Podfile.lock diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..5f67292 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,146 @@ +PODS: + - Flutter (1.0.0) + - flutter_blue_plus_darwin (0.0.2): + - Flutter + - FlutterMacOS + - flutter_foreground_task (0.0.1): + - Flutter + - flutter_local_notifications (0.0.1): + - Flutter + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMLKit/BarcodeScanning (7.0.0): + - GoogleMLKit/MLKitCore + - MLKitBarcodeScanning (~> 6.0.0) + - GoogleMLKit/MLKitCore (7.0.0): + - MLKitCommon (~> 12.0.0) + - GoogleToolboxForMac/Defines (4.2.1) + - GoogleToolboxForMac/Logger (4.2.1): + - GoogleToolboxForMac/Defines (= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (4.2.1)": + - GoogleToolboxForMac/Defines (= 4.2.1) + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GTMSessionFetcher/Core (3.5.0) + - MLImage (1.0.0-beta6) + - MLKitBarcodeScanning (6.0.0): + - MLKitCommon (~> 12.0) + - MLKitVision (~> 8.0) + - MLKitCommon (12.0.0): + - GoogleDataTransport (~> 10.0) + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GoogleUtilities/Logger (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLKitVision (8.0.0): + - GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1) + - "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)" + - GTMSessionFetcher/Core (< 4.0, >= 3.3.2) + - MLImage (= 1.0.0-beta6) + - MLKitCommon (~> 12.0) + - mobile_scanner (6.0.2): + - Flutter + - GoogleMLKit/BarcodeScanning (~> 7.0.0) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PromisesObjC (2.4.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + +SPEC REPOS: + trunk: + - GoogleDataTransport + - GoogleMLKit + - GoogleToolboxForMac + - GoogleUtilities + - GTMSessionFetcher + - MLImage + - MLKitBarcodeScanning + - MLKitCommon + - MLKitVision + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_blue_plus_darwin: + :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" + flutter_foreground_task: + :path: ".symlinks/plugins/flutter_foreground_task/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + +SPEC CHECKSUMS: + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 + flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 + GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56 + MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2 + MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d + MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e + mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + +PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5 + +COCOAPODS: 1.16.2 diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 0d5b4b1..de39d7e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -629,10 +629,25 @@ class MeshCoreConnector extends ChangeNotifier { await FlutterBluePlus.stopScan(); await _scanSubscription?.cancel(); - // On iOS/macOS, add a small delay to allow BLE stack to reset - // This prevents cached results from interfering with new scans + // On iOS/macOS, wait for Bluetooth to be powered on before scanning if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { + // Wait for adapter state to be powered on + final adapterState = await FlutterBluePlus.adapterState.first; + if (adapterState != BluetoothAdapterState.on) { + // Wait for the adapter to turn on, with timeout + await FlutterBluePlus.adapterState + .firstWhere((state) => state == BluetoothAdapterState.on) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + _setState(MeshCoreConnectionState.disconnected); + throw Exception('Bluetooth adapter not available'); + }, + ); + } + + // Add a small delay to allow BLE stack to fully initialize await Future.delayed(const Duration(milliseconds: 300)); } diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..a87b4cf --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,74 @@ +PODS: + - flutter_blue_plus_darwin (0.0.2): + - Flutter + - FlutterMacOS + - flutter_local_notifications (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - mobile_scanner (6.0.2): + - FlutterMacOS + - package_info_plus (0.0.1): + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + +EXTERNAL SOURCES: + flutter_blue_plus_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin + flutter_local_notifications: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos + FlutterMacOS: + :path: Flutter/ephemeral + mobile_scanner: + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + +SPEC CHECKSUMS: + flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 + package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 6a85646..defe693 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 99C5B380294D2DE19A818101 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */; }; + D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 00F1FE94A1827B8A00BD3DB9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* meshcore_open.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "meshcore_open.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* meshcore_open.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = meshcore_open.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +80,14 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 4172BCCDFD1E1404F7155426 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 96DE804777D5630B2C6952B5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BEFF4DDC60AFB628205F8E82 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 99C5B380294D2DE19A818101 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D7DDCBD47F2955423D77927D /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + 73DBB8BFF247FD65EEC878CC /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + 73DBB8BFF247FD65EEC878CC /* Pods */ = { + isa = PBXGroup; + children = ( + BEFF4DDC60AFB628205F8E82 /* Pods-Runner.debug.xcconfig */, + 4172BCCDFD1E1404F7155426 /* Pods-Runner.release.xcconfig */, + 00F1FE94A1827B8A00BD3DB9 /* Pods-Runner.profile.xcconfig */, + 96DE804777D5630B2C6952B5 /* Pods-RunnerTests.debug.xcconfig */, + EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */, + D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 0F985DDB6BE5BEB6B545DE9A /* Pods_Runner.framework */, + B665683D805EE21638F484F2 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 7DEC542F9A4811B2EEDCB8C1 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 79D67F01E273245A9C69C0B6 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 306490712F2EAA29CA421662 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -291,6 +323,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 306490712F2EAA29CA421662 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -329,6 +378,50 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 79D67F01E273245A9C69C0B6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 7DEC542F9A4811B2EEDCB8C1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 96DE804777D5630B2C6952B5 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EA5A89F8C49904B995EFAA24 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D99E941424F19B7B9AA1B968 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + From 2c495349558868983998a55a7196efffa7f17de2 Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 24 Jan 2026 01:24:56 -0700 Subject: [PATCH 024/421] feat: add url_launcher_ios dependency and update project configuration --- ios/Podfile.lock | 6 ++ ios/Runner.xcodeproj/project.pbxproj | 68 +++++++++++++++++++ .../contents.xcworkspacedata | 3 + 3 files changed, 77 insertions(+) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5f67292..aef2502 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -67,6 +67,8 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter - wakelock_plus (0.0.1): - Flutter @@ -80,6 +82,7 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) SPEC REPOS: @@ -115,6 +118,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" @@ -139,6 +144,7 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 09c8350..4d5727a 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4268181FCF3E12817B700E9C /* libPods-Runner.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,9 +43,13 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 4268181FCF3E12817B700E9C /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -62,6 +67,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,6 +100,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + DEE6F094D3B70E76087722E1 /* Pods */, + DAE613E34DF694C2E33B64C7 /* Frameworks */, ); sourceTree = ""; }; @@ -121,6 +129,25 @@ path = Runner; sourceTree = ""; }; + DAE613E34DF694C2E33B64C7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4268181FCF3E12817B700E9C /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + DEE6F094D3B70E76087722E1 /* Pods */ = { + isa = PBXGroup; + children = ( + 40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */, + 24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */, + 718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -145,12 +172,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -253,6 +282,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + From 45d914de5786c915dbef7a6d5d0367adeeb0d28c Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 24 Jan 2026 01:17:12 -0700 Subject: [PATCH 025/421] chore: update version to 5.0.0+5 in pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 56556e4..8b1415f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.4.5+4 +version: 5.0.0+5 environment: sdk: ^3.9.2 From 8b0bdd9a46ab74c7af89c1b5b588c9949b8d28fc Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 24 Jan 2026 01:37:19 -0700 Subject: [PATCH 026/421] fix: update PRODUCT_BUNDLE_IDENTIFIER to com.monitormx.meshcoreopen --- ios/Runner.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4d5727a..48d9fff 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -436,7 +436,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen; + PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -452,7 +452,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -469,7 +469,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -484,7 +484,7 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; @@ -615,7 +615,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen; + PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -637,7 +637,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen; + PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; From 90f90ad7cf5dcd97543ffdcb28ebc850e0f95d5d Mon Sep 17 00:00:00 2001 From: erikklavora <46493001+erikklavora@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:05:01 +0100 Subject: [PATCH 027/421] Updated Slovenian lang --- lib/l10n/app_localizations_sl.dart | 340 ++++++++++++++--------------- lib/l10n/app_sl.arb | 284 ++++++++++++------------ 2 files changed, 312 insertions(+), 312 deletions(-) diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index be9556f..38bfe3d 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -12,7 +12,7 @@ class AppLocalizationsSl extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Kontakti'; + String get nav_contacts => 'Stiki'; @override String get nav_channels => 'Kanali'; @@ -30,13 +30,13 @@ class AppLocalizationsSl extends AppLocalizations { String get common_connect => 'Poveži se'; @override - String get common_unknownDevice => 'Nepoznano naprave'; + String get common_unknownDevice => 'Nepoznane naprave'; @override String get common_save => 'Shrani'; @override - String get common_delete => 'Izbrisati'; + String get common_delete => 'Izbriši'; @override String get common_close => 'Zapri'; @@ -51,7 +51,7 @@ class AppLocalizationsSl extends AppLocalizations { String get common_settings => 'Nastavitve'; @override - String get common_disconnect => 'Odklopiti'; + String get common_disconnect => 'Odklopi'; @override String get common_connected => 'Povezano'; @@ -66,31 +66,31 @@ class AppLocalizationsSl extends AppLocalizations { String get common_continue => 'Poudarki'; @override - String get common_share => 'Deliti'; + String get common_share => 'Deli'; @override String get common_copy => 'Kopiraj'; @override - String get common_retry => 'Ponoviti'; + String get common_retry => 'Ponovi'; @override String get common_hide => 'Skrita'; @override - String get common_remove => 'Izbrisati'; + String get common_remove => 'Izbriši'; @override String get common_enable => 'Omogoči'; @override - String get common_disable => 'Izklopiti'; + String get common_disable => 'Izklopi'; @override - String get common_reboot => 'Ponoviti'; + String get common_reboot => 'Ponovno zaženi'; @override - String get common_loading => 'Naložanje...'; + String get common_loading => 'Nalaganje...'; @override String get common_notAvailable => '—'; @@ -109,7 +109,7 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_title => 'MeshCore Open'; @override - String get scanner_scanning => 'Skeniram za naprave...'; + String get scanner_scanning => 'Iščem naprave...'; @override String get scanner_connecting => 'Povezujem se...'; @@ -118,7 +118,7 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_disconnecting => 'Odklapljam se...'; @override - String get scanner_notConnected => 'Nezavezan'; + String get scanner_notConnected => 'Ni povezave'; @override String scanner_connectedTo(String deviceName) { @@ -134,7 +134,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Pošlo je z povezavo: $error'; + return 'Napaka pri povezavi: $error'; } @override @@ -144,7 +144,7 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_scan => 'Skeniraj'; @override - String get device_quickSwitch => 'Hitro preklopiti'; + String get device_quickSwitch => 'Hitri preklop'; @override String get device_meshcore => 'MeshCore'; @@ -153,7 +153,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_title => 'Nastavitve'; @override - String get settings_deviceInfo => 'Informacije o napravei'; + String get settings_deviceInfo => 'Informacije o napravi'; @override String get settings_appSettings => 'Nastavitve aplikacije'; @@ -163,16 +163,16 @@ class AppLocalizationsSl extends AppLocalizations { 'Obveščanja, sporoščanje in zemljevidi.'; @override - String get settings_nodeSettings => 'Nastavitve časa'; + String get settings_nodeSettings => 'Nastavitev časa'; @override - String get settings_nodeName => 'Ime omrežno mesto'; + String get settings_nodeName => 'Ime node-a'; @override - String get settings_nodeNameNotSet => 'Nezavedeno'; + String get settings_nodeNameNotSet => 'Ni nastavljeno'; @override - String get settings_nodeNameHint => 'Vnesite ime časa'; + String get settings_nodeNameHint => 'Vnesite ime node-a'; @override String get settings_nodeNameUpdated => 'Ime posodobljeno'; @@ -182,7 +182,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_radioSettingsSubtitle => - 'Frekvenca, moč, razširni faktor'; + 'Frekvenca, moč, razširitveni faktor'; @override String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene'; @@ -201,21 +201,21 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_locationInvalid => - 'Neveljna zemeljska širina ali dolžina.'; + 'Neveljavna zemeljska širina ali dolžina.'; @override String get settings_locationGPSEnable => 'Omogoči GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Omogoči samodejno posodabljanje lokacije z GPS-jem.'; + 'Omogoči samodejno posodabljanje lokacije z GPS-om.'; @override - String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)'; + String get settings_locationIntervalSec => 'Interval za GPS (sekunde)'; @override String get settings_locationIntervalInvalid => - 'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.'; + 'Interval mora biti med 60 in 86400 sekund.'; @override String get settings_latitude => 'Širina'; @@ -224,7 +224,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_longitude => 'Dolžina'; @override - String get settings_privacyMode => 'Mod podjetja'; + String get settings_privacyMode => 'Zasebnost'; @override String get settings_privacyModeSubtitle => 'Skrita imena/lokacije v oglasih'; @@ -234,16 +234,16 @@ class AppLocalizationsSl extends AppLocalizations { 'Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.'; @override - String get settings_privacyModeEnabled => 'Privatni režim je omogočen.'; + String get settings_privacyModeEnabled => 'Privatni način je omogočen.'; @override - String get settings_privacyModeDisabled => 'Privatni režim je onemogočen.'; + String get settings_privacyModeDisabled => 'Privatni način je onemogočen.'; @override String get settings_actions => 'Akcije'; @override - String get settings_sendAdvertisement => 'Pošlji Oglas'; + String get settings_sendAdvertisement => 'Pošlji oglas'; @override String get settings_sendAdvertisementSubtitle => @@ -253,50 +253,50 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_advertisementSent => 'Oglas poslan'; @override - String get settings_syncTime => 'Ugasniti čas'; + String get settings_syncTime => 'Nastavi uro'; @override - String get settings_syncTimeSubtitle => 'Nastavi uro naprave v čas telefona'; + String get settings_syncTimeSubtitle => 'Nastavi uro naprave na čas telefona'; @override - String get settings_timeSynchronized => 'Sinhronizirano po času'; + String get settings_timeSynchronized => 'Ura sinhronizirana'; @override - String get settings_refreshContacts => 'Ponovno obišči kontakte'; + String get settings_refreshContacts => 'Osveži stike'; @override String get settings_refreshContactsSubtitle => - 'Ponovno naloži seznam kontaktov iz naprave'; + 'Ponovno naloži seznam stikov v napravi'; @override - String get settings_rebootDevice => 'Restart Naprave'; + String get settings_rebootDevice => 'Ponovni zagon naprave'; @override String get settings_rebootDeviceSubtitle => - 'Ponovite zažetek naprave MeshCore'; + 'Ponovno zaženi MeshCore napravo'; @override String get settings_rebootDeviceConfirm => - 'Ste prepričani, da želite ponovno zagon napravke? Boste odvisni od omrežja.'; + 'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.'; @override - String get settings_debug => 'Napravi popravek'; + String get settings_debug => 'Debug'; @override - String get settings_bleDebugLog => 'Logarjev zapis BLE'; + String get settings_bleDebugLog => 'BLE debug log (razhroščevanje)'; @override String get settings_bleDebugLogSubtitle => - 'Navodila BLE, odgovori in surovo podatkovno'; + 'BLE ukazi, odgovori in surovi podatki'; @override - String get settings_appDebugLog => 'Log zapiske aplikacije'; + String get settings_appDebugLog => 'Logi aplikacije'; @override - String get settings_appDebugLogSubtitle => 'Prijavni sporočila aplikacije'; + String get settings_appDebugLogSubtitle => 'Debug sporočila aplikacije'; @override - String get settings_about => 'Oglejte si'; + String get settings_about => 'O aplikaciji'; @override String settings_aboutVersion(String version) { @@ -304,11 +304,11 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get settings_aboutLegalese => 'MeshCore Odprtokodni Projekt 2024'; + String get settings_aboutLegalese => 'Odprtokodni projekt MeshCore 2024'; @override String get settings_aboutDescription => - 'Odprtokodni Flutter kličnik za naprave za LoRa mrežo MeshCore.'; + 'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.'; @override String get settings_infoName => 'Ime'; @@ -323,10 +323,10 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_infoBattery => 'Baterija'; @override - String get settings_infoPublicKey => 'Ključ javnega tipa'; + String get settings_infoPublicKey => 'Javni ključ'; @override - String get settings_infoContactsCount => 'Število kontaktov'; + String get settings_infoContactsCount => 'Število stikov'; @override String get settings_infoChannelCount => 'Število kanalov'; @@ -350,7 +350,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_frequencyHelper => '300,00 - 2500,00'; @override - String get settings_frequencyInvalid => 'Neveljčna frekvenca (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Neveljavna frekvenca (300-2500 MHz)'; @override String get settings_bandwidth => 'Pasovna širina'; @@ -359,7 +359,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_spreadingFactor => 'Razširitveni faktor'; @override - String get settings_codingRate => 'Programska hitrost'; + String get settings_codingRate => 'Programska hitrost (CR)'; @override String get settings_txPower => 'TX Moč (dBm)'; @@ -368,13 +368,13 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Neveljaven TX moč (0-22 dBm)'; + String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; @override - String get settings_longRange => 'Dolenje območje'; + String get settings_longRange => 'Dolg doseg'; @override - String get settings_fastSpeed => 'Hitra hitrost'; + String get settings_fastSpeed => 'Visoka hitrost'; @override String settings_error(String message) { @@ -391,10 +391,10 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_theme => 'Tema'; @override - String get appSettings_themeSystem => 'Predpomnilnik sistema'; + String get appSettings_themeSystem => 'Sistemska tema'; @override - String get appSettings_themeLight => 'Luč'; + String get appSettings_themeLight => 'Svetlo'; @override String get appSettings_themeDark => 'Temno'; @@ -445,10 +445,10 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_notifications => 'Obveščanja'; + String get appSettings_notifications => 'Obvestila'; @override - String get appSettings_enableNotifications => 'Omogoči obveščanje'; + String get appSettings_enableNotifications => 'Omogoči obvestila'; @override String get appSettings_enableNotificationsSubtitle => @@ -484,7 +484,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'Pokaži obvestilo, ko so novi vozlišči odkrivljeni.'; + 'Pokaži obvestilo, ko so najdene nove naprave.'; @override String get appSettings_messaging => 'Komuniciranje'; @@ -499,18 +499,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_pathsWillBeCleared => - 'Potnice bodo očiščene po 5 neuspešnih poskusih.'; + 'Počisti pot po 5 neuspešnih poskusih.'; @override String get appSettings_pathsWillNotBeCleared => - 'Potniški poti ne bodo samodejno čiščeni.'; + 'Poti ne bodo samodejno čiščene.'; @override - String get appSettings_autoRouteRotation => 'Avtomatsko Občutke in Rotacije'; + String get appSettings_autoRouteRotation => 'Avtomatsko rotacija prenosne poti'; @override String get appSettings_autoRouteRotationSubtitle => - 'Med spreminjanjem med najboljšimi potmi in plovilnim načinom'; + 'Menjaj med boljšo potjo in flood načinom'; @override String get appSettings_autoRouteRotationEnabled => @@ -524,16 +524,16 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_battery => 'Baterija'; @override - String get appSettings_batteryChemistry => 'Razem z možnostmi'; + String get appSettings_batteryChemistry => 'Kemija baterije'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Nastavitve za naprave ($deviceName)'; + return 'Nastavitev za napravo ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Povežite se z napravo za izbiro'; + 'Za izbiro se poveži z napravo'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; @@ -545,52 +545,52 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override - String get appSettings_mapDisplay => 'Prikaz zemljevide'; + String get appSettings_mapDisplay => 'Prikaz zemljevida'; @override - String get appSettings_showRepeaters => 'Prikaži ponovitve'; + String get appSettings_showRepeaters => 'Prikaži repetitorje'; @override String get appSettings_showRepeatersSubtitle => - 'Prikaži ponovljalne notranjosti na zemljeploscu'; + 'Prikaži repetitorje na mapi'; @override - String get appSettings_showChatNodes => 'Prikaži čakalne notranjosti'; + String get appSettings_showChatNodes => 'Prikaži naprave za klepet'; @override String get appSettings_showChatNodesSubtitle => - 'Prikaži pogovorni pike na zemljeploscu'; + 'Prikaži naprave na zemljevidu'; @override - String get appSettings_showOtherNodes => 'Pokaži druge vozlišča'; + String get appSettings_showOtherNodes => 'Pokaži druge naprave'; @override String get appSettings_showOtherNodesSubtitle => - 'Pokaži druge vrste notranjih elementov na zemljevalu.'; + 'Pokaži druge vrste naprav na zemljevidu.'; @override - String get appSettings_timeFilter => 'Filtri po času'; + String get appSettings_timeFilter => 'Filter po času'; @override - String get appSettings_timeFilterShowAll => 'Pokaži vse notranje elemente'; + String get appSettings_timeFilterShowAll => 'Pokaži vse naprave'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Pokaži notranjosti iz zadnjih $hours ur'; + return 'Pokaži naprave v zadnjih $hours urah'; } @override - String get appSettings_mapTimeFilter => 'Filtri časa zemljevida'; + String get appSettings_mapTimeFilter => 'Filter časa na zemljevidu'; @override String get appSettings_showNodesDiscoveredWithin => - 'Pokaži notranje čepke, odkrivene v:'; + 'Pokaži naprave odkrite v:'; @override - String get appSettings_allTime => 'Vse čase'; + String get appSettings_allTime => 'Brez omejitev'; @override - String get appSettings_lastHour => 'Minuto nazaj'; + String get appSettings_lastHour => 'V zadnji uri'; @override String get appSettings_last6Hours => 'Zadnjih 6 ur'; @@ -599,13 +599,13 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_last24Hours => 'Zadnjih 24 ur'; @override - String get appSettings_lastWeek => 'Lepošno'; + String get appSettings_lastWeek => 'Prejšnji teden'; @override - String get appSettings_offlineMapCache => 'Omrezni Poudni Arhiv'; + String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; @override - String get appSettings_noAreaSelected => 'Nizkana označena površina'; + String get appSettings_noAreaSelected => 'Območje ni izbrano'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { @@ -613,79 +613,79 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get appSettings_debugCard => 'Napravi popravek'; + String get appSettings_debugCard => 'Razhroščevanje'; @override - String get appSettings_appDebugLogging => 'Programski Log'; + String get appSettings_appDebugLogging => 'Programski dnevnik'; @override String get appSettings_appDebugLoggingSubtitle => - 'Log aplikacijske debug sporočila za odpravljanje težav'; + 'Dnevnik debug sporočil za odpravljanje težav'; @override String get appSettings_appDebugLoggingEnabled => - 'Omogočeno zaznamovanje napak v aplikaciji'; + 'Beleženje napak v aplikaciji omogočeno'; @override String get appSettings_appDebugLoggingDisabled => - 'Programski logi aplikacije so onemogočeni.'; + 'Beleženje napak v aplikacije onemogočeno.'; @override - String get contacts_title => 'Kontakti'; + String get contacts_title => 'Stiki'; @override - String get contacts_noContacts => 'Še ni kontaktov.'; + String get contacts_noContacts => 'Ni stikov.'; @override String get contacts_contactsWillAppear => - 'Kontakti se bodo prikazali, ko naprave oglasijo.'; + 'Stiki se bodo prikazali takoj, ko se naprave oglasijo.'; @override - String get contacts_searchContacts => 'Iskanje kontaktov...'; + String get contacts_searchContacts => 'Iskanje stikov...'; @override - String get contacts_noUnreadContacts => 'Nerešeno kontaktov.'; + String get contacts_noUnreadContacts => 'Ne prebrani stiki.'; @override String get contacts_noContactsFound => - 'Niti ena oseba ali skupine ni najdena.'; + 'Stiki niso najdeni.'; @override - String get contacts_deleteContact => 'Izbrisati Kontakt'; + String get contacts_deleteContact => 'Izbriši stik'; @override String contacts_removeConfirm(String contactName) { - return 'Izbrisati $contactName iz kontaktov?'; + return 'Izbrišem $contactName iz stikov?'; } @override - String get contacts_manageRepeater => 'Upravljajte Ponovitve'; + String get contacts_manageRepeater => 'Upravljanje repetitorjev'; @override - String get contacts_manageRoom => 'Upravljajte strežnik sobe'; + String get contacts_manageRoom => 'Upravljanje strežniške sobe'; @override - String get contacts_roomLogin => 'Vnos v sobo'; + String get contacts_roomLogin => 'Prijava v sobo'; @override - String get contacts_openChat => 'Odprta kleta'; + String get contacts_openChat => 'Odpri klepet'; @override - String get contacts_editGroup => 'Uredi Skupino'; + String get contacts_editGroup => 'Uredi skupino'; @override - String get contacts_deleteGroup => 'Izbrisati Skupino'; + String get contacts_deleteGroup => 'Izbriši skupino'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Odpovedati $groupName?'; + return 'Izbriši $groupName?'; } @override - String get contacts_newGroup => 'Novo skupino'; + String get contacts_newGroup => 'Nova skupina'; @override - String get contacts_groupName => 'Skupina imena'; + String get contacts_groupName => 'Ime skupine'; @override String get contacts_groupNameRequired => 'Ime skupine je obvezno.'; @@ -696,53 +696,53 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get contacts_filterContacts => 'Filtri kontakt\\,...'; + String get contacts_filterContacts => 'Filtriraj stik\\,...'; @override String get contacts_noContactsMatchFilter => - 'Niti ena oseba ne ustreza vašemu kriteriju.'; + 'Noben stik ne ustreza vašemu kriteriju.'; @override - String get contacts_noMembers => 'Nič članov.'; + String get contacts_noMembers => 'Ni članov.'; @override - String get contacts_lastSeenNow => 'Datum zadnjega vpisa zdaj'; + String get contacts_lastSeenNow => 'Nazadnje viden zdaj'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Zadnjič videti $minutes minut nazaj'; + return 'Zadnjič viden pred $minutes minutami'; } @override - String get contacts_lastSeenHourAgo => 'Zadnjič ogledan pred 1 uro.'; + String get contacts_lastSeenHourAgo => 'Zadnjič viden pred 1 uro.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Zadnjič videti $hours ur nazaj'; + return 'Zadnjič viden pred $hours urami'; } @override - String get contacts_lastSeenDayAgo => 'Zadnjič ogledan pred 1 dnem'; + String get contacts_lastSeenDayAgo => 'Zadnjič viden pred 1 dnem'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Zadnjič videti $days dni nazaj'; + return 'Zadnjič viden pred $days dnem'; } @override String get channels_title => 'Kanali'; @override - String get channels_noChannelsConfigured => 'Nekonfigurirane kanale'; + String get channels_noChannelsConfigured => 'Kanali še niso konfigurirani'; @override - String get channels_addPublicChannel => 'Dodaj Objavni Kanal'; + String get channels_addPublicChannel => 'Dodaj javni kanal'; @override String get channels_searchChannels => 'Poišči kanale...'; @override - String get channels_noChannelsFound => 'Niti kanalov najti ni.'; + String get channels_noChannelsFound => 'Ne najdem kanalov.'; @override String channels_channelIndex(int index) { @@ -753,16 +753,16 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_hashtagChannel => 'Hashtag kanal'; @override - String get channels_public => 'javno'; + String get channels_public => 'Javni'; @override - String get channels_private => 'Zasebno'; + String get channels_private => 'Zasebni'; @override - String get channels_publicChannel => 'Ogljišna skupina'; + String get channels_publicChannel => 'Javni kanal'; @override - String get channels_privateChannel => 'Zatemniščen kanal'; + String get channels_privateChannel => 'Zasebni kanal'; @override String get channels_editChannel => 'Uredi kanal'; @@ -772,7 +772,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return 'Izbrisati \"$name\"? To se ne da povrniti.'; + return 'Izbrišem \"$name\"? To se ne da povrniti.'; } @override @@ -862,20 +862,20 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channels_joinPublicChannelDesc => - 'Kdor karkoli je, lahko se pridruži tej skupini.'; + 'Kdorkoli se lahko pridruži tej skupini.'; @override - String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom'; + String get channels_joinHashtagChannel => 'Pridružite se kanalu s hashtagom'; @override String get channels_joinHashtagChannelDesc => - 'Kdor karkoli, lahko se pridruži hashtag kanalom.'; + 'Kdorkoli se lahko pridruži hashtag kanalom.'; @override String get channels_scanQrCode => 'Skeniraj QR kodo'; @override - String get channels_scanQrCodeComingSoon => 'Prihajajoča'; + String get channels_scanQrCodeComingSoon => 'Prihaja kmalu'; @override String get channels_enterHashtag => 'Vnesite hashtag'; @@ -895,7 +895,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String chat_replyingTo(String name) { - return 'Odgovarjanje $name'; + return 'Odgovori $name'; } @override @@ -912,35 +912,35 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get chat_typeMessage => 'Vnesite sporočilo...'; + String get chat_typeMessage => 'Vnesi sporočilo...'; @override String chat_messageTooLong(int maxBytes) { - return 'Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes bajt).'; + return 'Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes byte-ov).'; } @override - String get chat_messageCopied => 'Pošljeno sporočilo'; + String get chat_messageCopied => 'Sporočilo poslano'; @override - String get chat_messageDeleted => 'Pošiljanje sporočila izbrisano'; + String get chat_messageDeleted => 'Sporočilo izbrisano'; @override - String get chat_retryingMessage => 'Ponovna poskus.'; + String get chat_retryingMessage => 'Ponovni poskus.'; @override String chat_retryCount(int current, int max) { - return 'Ponovit $current/$max'; + return 'Ponovitev $current/$max'; } @override String get chat_sendGif => 'Pošlji GIF'; @override - String get chat_reply => 'Odpošlji'; + String get chat_reply => 'Odgovori'; @override - String get chat_addReaction => 'Dodaj Reakcijo'; + String get chat_addReaction => 'Dodaj reakcijo'; @override String get chat_me => 'jaz'; @@ -961,19 +961,19 @@ class AppLocalizationsSl extends AppLocalizations { String get gifPicker_title => 'Izberi GIF'; @override - String get gifPicker_searchHint => 'Iskalite GIF-e...'; + String get gifPicker_searchHint => 'Išči GIF-e...'; @override - String get gifPicker_poweredBy => 'Naprodno z GIPHY'; + String get gifPicker_poweredBy => 'Napredno z GIPHY'; @override - String get gifPicker_noGifsFound => 'Niti GIF-jev najti ni.'; + String get gifPicker_noGifsFound => 'Ne najdem GIF-ov.'; @override - String get gifPicker_failedLoad => 'Neuspešno je naložilo GIF-e'; + String get gifPicker_failedLoad => 'Neuspešno nalaganje GIF-a'; @override - String get gifPicker_failedSearch => 'Posodobit neuspešno.'; + String get gifPicker_failedSearch => 'Iskanje neuspešno.'; @override String get gifPicker_noInternet => 'Ni internetne povezave'; @@ -982,35 +982,35 @@ class AppLocalizationsSl extends AppLocalizations { String get debugLog_appTitle => 'Log zapiske aplikacije'; @override - String get debugLog_bleTitle => 'Logarjev zapis BLE'; + String get debugLog_bleTitle => 'Log zapis BLE'; @override - String get debugLog_copyLog => 'Kopiraj zapiske'; + String get debugLog_copyLog => 'Kopiraj dnevnik'; @override - String get debugLog_clearLog => 'Pasters log'; + String get debugLog_clearLog => 'Briši log'; @override - String get debugLog_copied => 'Kopirana belež poteka.'; + String get debugLog_copied => 'Beležka kopirana.'; @override - String get debugLog_bleCopied => 'Kopirana beležke iz BLE'; + String get debugLog_bleCopied => 'Kopirana beležka iz BLE'; @override - String get debugLog_noEntries => 'Še ni ustvarjenih debug zapisov.'; + String get debugLog_noEntries => 'Ni debug zapisov.'; @override String get debugLog_enableInSettings => - 'Omogoči beleženje napak v aplikaciji v nastavitvah'; + 'Omogoči beleženje napak v nastavitvah aplikacije'; @override - String get debugLog_frames => 'Okna'; + String get debugLog_frames => 'Okvirji'; @override String get debugLog_rawLogRx => 'Svež Log-RX'; @override - String get debugLog_noBleActivity => 'Šele začnite z aktivnostjo BLE.'; + String get debugLog_noBleActivity => 'Ni BLE aktivnosti.'; @override String debugFrame_length(int count) { @@ -1019,7 +1019,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String debugFrame_command(String value) { - return 'Navodilo: 0x$value'; + return 'Ukaz: 0x$value'; } @override @@ -1079,10 +1079,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.'; @override - String get chat_hopSingular => 'skoč'; + String get chat_hopSingular => 'skok'; @override - String get chat_hopPlural => 'škrabec'; + String get chat_hopPlural => 'skokov'; @override String chat_hopsCount(int count) { @@ -1103,7 +1103,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Še ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.'; + 'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.'; @override String get chat_pathActions => 'Potni ukazi:'; @@ -1115,7 +1115,7 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_setCustomPathSubtitle => 'Ročno določite potniško pot.'; @override - String get chat_clearPath => 'Čista pot'; + String get chat_clearPath => 'Počisti pot'; @override String get chat_clearPathSubtitle => 'Ob naslednji pošiljanju znova zbrati.'; @@ -1133,7 +1133,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.'; @override - String get chat_fullPath => 'Polni pot'; + String get chat_fullPath => 'Polna pot'; @override String get chat_pathDetailsNotAvailable => @@ -1152,7 +1152,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_pathSavedLocally => - 'Shrano lokalno. Povežite se za sinhronizacijo.'; + 'Shranjeno lokalno. Povežite se za sinhronizacijo.'; @override String get chat_pathDeviceConfirmed => 'Naprave potrjeno.'; @@ -2012,13 +2012,13 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'Dobiti ime'; + String get repeater_cliQuickGetName => 'Pridobi ime'; @override String get repeater_cliQuickGetRadio => 'Dobiti Radiopravo'; @override - String get repeater_cliQuickGetTx => 'Dobiti TX'; + String get repeater_cliQuickGetTx => 'Pridobi TX'; @override String get repeater_cliQuickNeighbors => 'Sosedi'; @@ -2030,7 +2030,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_cliQuickAdvertise => 'Oglasite'; @override - String get repeater_cliQuickClock => 'Urnik'; + String get repeater_cliQuickClock => 'Ura'; @override String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; @@ -2153,7 +2153,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpSetPerm => - 'Modificira ACL. Odstrani ustreznu vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).'; + 'Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).'; @override String get repeater_cliHelpGetBridgeType => @@ -2261,11 +2261,11 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_logging => 'Logiranje'; @override - String get repeater_neighborsRepeaterOnly => 'Sosedi (le za ponovitelja)'; + String get repeater_neighborsRepeaterOnly => 'Sosedi (le za repetitorje)'; @override String get repeater_regionManagementRepeaterOnly => - 'Upravljanje regij (zgolj za ponovitve)'; + 'Upravljanje regij (zgolj za repetitorje)'; @override String get repeater_regionNote => @@ -2380,13 +2380,13 @@ class AppLocalizationsSl extends AppLocalizations { String get channelPath_messageDetails => 'Podrobnosti sporočila'; @override - String get channelPath_senderLabel => 'Pošiljalec'; + String get channelPath_senderLabel => 'Pošiljatelj'; @override - String get channelPath_timeLabel => 'Čas'; + String get channelPath_timeLabel => 'Ura'; @override - String get channelPath_repeatsLabel => 'Ponovi'; + String get channelPath_repeatsLabel => 'Ponovitve'; @override String channelPath_pathLabel(int index) { @@ -2551,10 +2551,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get community_scanOrCreate => - 'Skenirajte QR kodo ali ustvarite skupnost za začetek.'; + 'Skeniraj QR kodo ali ustvari skupnost za začetek.'; @override - String get community_manageCommunities => 'Upravljajte skupnosti'; + String get community_manageCommunities => 'Upravljanje skupnosti'; @override String get community_delete => 'Opusti skupnost'; @@ -2604,7 +2604,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get community_addHashtagChannel => 'Dodaj Oznako Obštnine'; + String get community_addHashtagChannel => 'Dodaj hashtag kanal'; @override String get community_addHashtagChannelDesc => @@ -2618,7 +2618,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get community_regularHashtagDesc => - 'javna oznaka (kateri koli lahko sodelujejo)'; + 'javna oznaka (kdorkoli lahko sodelujeje)'; @override String get community_communityHashtag => 'Skupnostni hashtag'; @@ -2633,7 +2633,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get listFilter_tooltip => 'Filtri in vrstiči'; + String get listFilter_tooltip => 'Filtri in sortiranje'; @override String get listFilter_sortBy => 'Sortiraj po'; @@ -2657,13 +2657,13 @@ class AppLocalizationsSl extends AppLocalizations { String get listFilter_users => 'Uporabniki'; @override - String get listFilter_repeaters => 'Ponovitve'; + String get listFilter_repeaters => 'Samo repetirorji'; @override - String get listFilter_roomServers => 'Smeti za prostore'; + String get listFilter_roomServers => 'Samo room serverji'; @override - String get listFilter_unreadOnly => 'Nezbrani samo'; + String get listFilter_unreadOnly => 'Samo neprebrani'; @override String get listFilter_newGroup => 'Nova skupina'; diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 977f29c..346cdaa 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,7 +1,7 @@ { "@@locale": "sl", "appTitle": "MeshCore Open", - "nav_contacts": "Kontakti", + "nav_contacts": "Stiki", "nav_channels": "Kanali", "nav_map": "Karta", "common_cancel": "Prekliči", @@ -69,49 +69,49 @@ }, "scanner_stop": "Prekliči", "scanner_scan": "Skeniraj", - "device_quickSwitch": "Hitro preklopiti", + "device_quickSwitch": "Hitro preklop", "device_meshcore": "MeshCore", "settings_title": "Nastavitve", "settings_deviceInfo": "Informacije o napravei", "settings_appSettings": "Nastavitve aplikacije", "settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.", - "settings_nodeSettings": "Nastavitve časa", - "settings_nodeName": "Ime omrežno mesto", - "settings_nodeNameNotSet": "Nezavedeno", - "settings_nodeNameHint": "Vnesite ime časa", + "settings_nodeSettings": "Nastavitev časa", + "settings_nodeName": "Ime node-a", + "settings_nodeNameNotSet": "Ni nastavljeno", + "settings_nodeNameHint": "Vnesite ime node-a", "settings_nodeNameUpdated": "Ime posodobljeno", "settings_radioSettings": "Nastavitve radija", - "settings_radioSettingsSubtitle": "Frekvenca, moč, razširni faktor", + "settings_radioSettingsSubtitle": "Frekvenca, moč, razširitveni faktor", "settings_radioSettingsUpdated": "Radio nastavitve posodobljene", "settings_location": "Lokacija", "settings_locationSubtitle": "GPS koordinate", "settings_locationUpdated": "Lokacija posodobljena", "settings_locationBothRequired": "Vnesite širino in dolžino.", - "settings_locationInvalid": "Neveljna zemeljska širina ali dolžina.", + "settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.", "settings_latitude": "Širina", "settings_longitude": "Dolžina", - "settings_privacyMode": "Mod podjetja", + "settings_privacyMode": "Zasebnost", "settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", "settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.", - "settings_privacyModeEnabled": "Privatni režim je omogočen.", - "settings_privacyModeDisabled": "Privatni režim je onemogočen.", + "settings_privacyModeEnabled": "Privatni način je omogočen.", + "settings_privacyModeDisabled": "Privatni način je onemogočen.", "settings_actions": "Akcije", "settings_sendAdvertisement": "Pošlji Oglas", "settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah", "settings_advertisementSent": "Oglas poslan", - "settings_syncTime": "Ugasniti čas", - "settings_syncTimeSubtitle": "Nastavi uro naprave v čas telefona", - "settings_timeSynchronized": "Sinhronizirano po času", + "settings_syncTime": "Nastavi uro", + "settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona", + "settings_timeSynchronized": "Ura sinhronizirana", "settings_refreshContacts": "Ponovno obišči kontakte", - "settings_refreshContactsSubtitle": "Ponovno naloži seznam kontaktov iz naprave", - "settings_rebootDevice": "Restart Naprave", - "settings_rebootDeviceSubtitle": "Ponovite zažetek naprave MeshCore", - "settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagon napravke? Boste odvisni od omrežja.", - "settings_debug": "Napravi popravek", - "settings_bleDebugLog": "Logarjev zapis BLE", - "settings_bleDebugLogSubtitle": "Navodila BLE, odgovori in surovo podatkovno", - "settings_appDebugLog": "Log zapiske aplikacije", - "settings_appDebugLogSubtitle": "Prijavni sporočila aplikacije", + "settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi", + "settings_rebootDevice": "Ponovni zagon naprave", + "settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo", + "settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.", + "settings_debug": "Debug", + "settings_bleDebugLog": "BLE debug log (razhroščevanje)", + "settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki", + "settings_appDebugLog": "Logi aplikacije", + "settings_appDebugLogSubtitle": "Debug sporočila aplikacije", "settings_about": "Oglejte si", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -121,14 +121,14 @@ } } }, - "settings_aboutLegalese": "MeshCore Odprtokodni Projekt 2024", - "settings_aboutDescription": "Odprtokodni Flutter kličnik za naprave za LoRa mrežo MeshCore.", + "settings_aboutLegalese": "Odprtokodni projekt MeshCore 2024", + "settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.", "settings_infoName": "Ime", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Baterija", - "settings_infoPublicKey": "Ključ javnega tipa", - "settings_infoContactsCount": "Število kontaktov", + "settings_infoPublicKey": "Javni ključ", + "settings_infoContactsCount": "Število stikov", "settings_infoChannelCount": "Število kanalov", "settings_presets": "Prednastavitve", "settings_preset915Mhz": "915 MHz", @@ -136,15 +136,15 @@ "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frekvenca (MHz)", "settings_frequencyHelper": "300,00 - 2500,00", - "settings_frequencyInvalid": "Neveljčna frekvenca (300-2500 MHz)", + "settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)", "settings_bandwidth": "Pasovna širina", "settings_spreadingFactor": "Razširitveni faktor", "settings_codingRate": "Programska hitrost", "settings_txPower": "TX Moč (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Neveljaven TX moč (0-22 dBm)", - "settings_longRange": "Dolenje območje", - "settings_fastSpeed": "Hitra hitrost", + "settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)", + "settings_longRange": "DDolg doseg", + "settings_fastSpeed": "Visoka hitrost", "settings_error": "Napaka: {message}", "@settings_error": { "placeholders": { @@ -156,8 +156,8 @@ "appSettings_title": "Nastavitve aplikacije", "appSettings_appearance": "Prikaži", "appSettings_theme": "Tema", - "appSettings_themeSystem": "Predpomnilnik sistema", - "appSettings_themeLight": "Luč", + "appSettings_themeSystem": "Sistemska tema", + "appSettings_themeLight": "Svetlo", "appSettings_themeDark": "Temno", "appSettings_language": "Jezik", "appSettings_languageSystem": "Sistemska privzeta vrednost", @@ -174,8 +174,8 @@ "appSettings_languageNl": "Nederlands", "appSettings_languageSk": "Slovenčina", "appSettings_languageBg": "Български", - "appSettings_notifications": "Obveščanja", - "appSettings_enableNotifications": "Omogoči obveščanje", + "appSettings_notifications": "Obvestila", + "appSettings_enableNotifications": "Omogoči obvestila", "appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih", "appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena", "appSettings_notificationsEnabled": "Obvestila omogočena", @@ -185,19 +185,19 @@ "appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala", "appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala", "appSettings_advertisementNotifications": "Opozorila o oglasih", - "appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so novi vozlišči odkrivljeni.", + "appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.", "appSettings_messaging": "Komuniciranje", "appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju", "appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja", - "appSettings_pathsWillBeCleared": "Potnice bodo očiščene po 5 neuspešnih poskusih.", - "appSettings_pathsWillNotBeCleared": "Potniški poti ne bodo samodejno čiščeni.", - "appSettings_autoRouteRotation": "Avtomatsko Občutke in Rotacije", - "appSettings_autoRouteRotationSubtitle": "Med spreminjanjem med najboljšimi potmi in plovilnim načinom", + "appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspešnih poskusih.", + "appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.", + "appSettings_autoRouteRotation": "Avtomatsko rotacija prenosne poti", + "appSettings_autoRouteRotationSubtitle": "Menjaj med boljšo potjo in flood načinom", "appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno", "appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno", "appSettings_battery": "Baterija", - "appSettings_batteryChemistry": "Razem z možnostmi", - "appSettings_batteryChemistryPerDevice": "Nastavitve za naprave ({deviceName})", + "appSettings_batteryChemistry": "Kemija baterije", + "appSettings_batteryChemistryPerDevice": "Nastavitev za napravo ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -205,20 +205,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Povežite se z napravo za izbiro", + "appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo", "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", - "appSettings_mapDisplay": "Prikaz zemljevide", - "appSettings_showRepeaters": "Prikaži ponovitve", - "appSettings_showRepeatersSubtitle": "Prikaži ponovljalne notranjosti na zemljeploscu", - "appSettings_showChatNodes": "Prikaži čakalne notranjosti", - "appSettings_showChatNodesSubtitle": "Prikaži pogovorni pike na zemljeploscu", - "appSettings_showOtherNodes": "Pokaži druge vozlišča", - "appSettings_showOtherNodesSubtitle": "Pokaži druge vrste notranjih elementov na zemljevalu.", - "appSettings_timeFilter": "Filtri po času", - "appSettings_timeFilterShowAll": "Pokaži vse notranje elemente", - "appSettings_timeFilterShowLast": "Pokaži notranjosti iz zadnjih {hours} ur", + "appSettings_mapDisplay": "Prikaz zemljevida", + "appSettings_showRepeaters": "Prikaži repetitorje", + "appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi", + "appSettings_showChatNodes": "Prikaži naprave za klepet", + "appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu", + "appSettings_showOtherNodes": "Pokaži druge naprave", + "appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.", + "appSettings_timeFilter": "Filter po času", + "appSettings_timeFilterShowAll": "Pokaži vse naprave", + "appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -226,15 +226,15 @@ } } }, - "appSettings_mapTimeFilter": "Filtri časa zemljevida", - "appSettings_showNodesDiscoveredWithin": "Pokaži notranje čepke, odkrivene v:", - "appSettings_allTime": "Vse čase", - "appSettings_lastHour": "Minuto nazaj", + "appSettings_mapTimeFilter": "Filter časa na zemljevidu", + "appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:", + "appSettings_allTime": "Brez omejitev", + "appSettings_lastHour": "V zadnji uri", "appSettings_last6Hours": "Zadnjih 6 ur", "appSettings_last24Hours": "Zadnjih 24 ur", - "appSettings_lastWeek": "Lepošno", - "appSettings_offlineMapCache": "Omrezni Poudni Arhiv", - "appSettings_noAreaSelected": "Nizkana označena površina", + "appSettings_lastWeek": "Prejšnji teden", + "appSettings_offlineMapCache": "Shramba zemljevidov brez povezave", + "appSettings_noAreaSelected": "Območje ni izbrano", "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { @@ -246,19 +246,19 @@ } } }, - "appSettings_debugCard": "Napravi popravek", - "appSettings_appDebugLogging": "Programski Log", - "appSettings_appDebugLoggingSubtitle": "Log aplikacijske debug sporočila za odpravljanje težav", - "appSettings_appDebugLoggingEnabled": "Omogočeno zaznamovanje napak v aplikaciji", - "appSettings_appDebugLoggingDisabled": "Programski logi aplikacije so onemogočeni.", - "contacts_title": "Kontakti", - "contacts_noContacts": "Še ni kontaktov.", - "contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.", - "contacts_searchContacts": "Iskanje kontaktov...", - "contacts_noUnreadContacts": "Nerešeno kontaktov.", - "contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.", - "contacts_deleteContact": "Izbrisati Kontakt", - "contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?", + "appSettings_debugCard": "Razhroščevanje", + "appSettings_appDebugLogging": "Programski dnevnik", + "appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav", + "appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno", + "appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.", + "contacts_title": "Stiki", + "contacts_noContacts": "Ni stikov.", + "contacts_contactsWillAppear": "Stiki se bodo prikazali, ko se naprave oglasijo.", + "contacts_searchContacts": "Iskanje stikov...", + "contacts_noUnreadContacts": "Ne prebrani stiki.", + "contacts_noContactsFound": "Stiki niso najdeni.", + "contacts_deleteContact": "Izbriši stik", + "contacts_removeConfirm": "Izbrišem {contactName} iz stikov?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -266,12 +266,12 @@ } } }, - "contacts_manageRepeater": "Upravljajte Ponovitve", - "contacts_roomLogin": "Vnos v sobo", - "contacts_openChat": "Odprta kleta", - "contacts_editGroup": "Uredi Skupino", - "contacts_deleteGroup": "Izbrisati Skupino", - "contacts_deleteGroupConfirm": "Odpovedati {groupName}?", + "contacts_manageRepeater": "Upravljaj Ponovitve", + "contacts_roomLogin": "Prijava v sobo", + "contacts_openChat": "Odpri klepet", + "contacts_editGroup": "Uredi skupino", + "contacts_deleteGroup": "Izbriši skupino", + "contacts_deleteGroupConfirm": "Izbriši {groupName}?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -279,8 +279,8 @@ } } }, - "contacts_newGroup": "Novo skupino", - "contacts_groupName": "Skupina imena", + "contacts_newGroup": "Nova skupina", + "contacts_groupName": "Ime skupine", "contacts_groupNameRequired": "Ime skupine je obvezno.", "contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja", "@contacts_groupAlreadyExists": { @@ -290,11 +290,11 @@ } } }, - "contacts_filterContacts": "Filtri kontakt\\,...", - "contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.", - "contacts_noMembers": "Nič članov.", - "contacts_lastSeenNow": "Datum zadnjega vpisa zdaj", - "contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj", + "contacts_filterContacts": "Filtriraj stik\\,...", + "contacts_noContactsMatchFilter": "Noben stik ne ustreza vašemu kriteriju.", + "contacts_noMembers": "Ni članov.", + "contacts_lastSeenNow": "Nazadnje viden zdaj", + "contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -302,8 +302,8 @@ } } }, - "contacts_lastSeenHourAgo": "Zadnjič ogledan pred 1 uro.", - "contacts_lastSeenHoursAgo": "Zadnjič videti {hours} ur nazaj", + "contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.", + "contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -311,8 +311,8 @@ } } }, - "contacts_lastSeenDayAgo": "Zadnjič ogledan pred 1 dnem", - "contacts_lastSeenDaysAgo": "Zadnjič videti {days} dni nazaj", + "contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem", + "contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -321,10 +321,10 @@ } }, "channels_title": "Kanali", - "channels_noChannelsConfigured": "Nekonfigurirane kanale", - "channels_addPublicChannel": "Dodaj Objavni Kanal", + "channels_noChannelsConfigured": "Kanali še niso konfigurirani", + "channels_addPublicChannel": "Dodaj javni kanal", "channels_searchChannels": "Poišči kanale...", - "channels_noChannelsFound": "Niti kanalov najti ni.", + "channels_noChannelsFound": "Ne najdem kanalov.", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { "placeholders": { @@ -334,13 +334,13 @@ } }, "channels_hashtagChannel": "Hashtag kanal", - "channels_public": "javno", - "channels_private": "Zasebno", - "channels_publicChannel": "Ogljišna skupina", - "channels_privateChannel": "Zatemniščen kanal", + "channels_public": "Javni", + "channels_private": "Zasebni", + "channels_publicChannel": "Javni kanal", + "channels_privateChannel": "Zasebni kanal", "channels_editChannel": "Uredi kanal", "channels_deleteChannel": "Pošlji kanal", - "channels_deleteChannelConfirm": "Izbrisati \"{name}\"? To se ne da povrniti.", + "channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -424,8 +424,8 @@ } } }, - "chat_typeMessage": "Vnesite sporočilo...", - "chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} bajt).", + "chat_typeMessage": "Vnesi sporočilo...", + "chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -433,9 +433,9 @@ } } }, - "chat_messageCopied": "Pošljeno sporočilo", - "chat_messageDeleted": "Pošiljanje sporočila izbrisano", - "chat_retryingMessage": "Ponovna poskus.", + "chat_messageCopied": "Sporočilo poslano", + "chat_messageDeleted": "Sporočilo izbrisano", + "chat_retryingMessage": "Ponovni poskus.", "chat_retryCount": "Ponovit {current}/{max}", "@chat_retryCount": { "placeholders": { @@ -448,31 +448,31 @@ } }, "chat_sendGif": "Pošlji GIF", - "chat_reply": "Odpošlji", - "chat_addReaction": "Dodaj Reakcijo", + "chat_reply": "Odgovori", + "chat_addReaction": "Dodaj reakcijo", "chat_me": "jaz", "emojiCategorySmileys": "Emoji", "emojiCategoryGestures": "Gestikulacije", "emojiCategoryHearts": "Srce", "emojiCategoryObjects": "Predmeti", "gifPicker_title": "Izberi GIF", - "gifPicker_searchHint": "Iskalite GIF-e...", - "gifPicker_poweredBy": "Naprodno z GIPHY", - "gifPicker_noGifsFound": "Niti GIF-jev najti ni.", - "gifPicker_failedLoad": "Neuspešno je naložilo GIF-e", - "gifPicker_failedSearch": "Posodobit neuspešno.", + "gifPicker_searchHint": "Išči GIF-e...", + "gifPicker_poweredBy": "Napredno z GIPHY", + "gifPicker_noGifsFound": "Ne najdem GIF-ov.", + "gifPicker_failedLoad": "Neuspešno nalaganje GIF-a", + "gifPicker_failedSearch": "Iskanje neuspešno.", "gifPicker_noInternet": "Ni internetne povezave", "debugLog_appTitle": "Log zapiske aplikacije", - "debugLog_bleTitle": "Logarjev zapis BLE", - "debugLog_copyLog": "Kopiraj zapiske", - "debugLog_clearLog": "Pasters log", - "debugLog_copied": "Kopirana belež poteka.", - "debugLog_bleCopied": "Kopirana beležke iz BLE", - "debugLog_noEntries": "Še ni ustvarjenih debug zapisov.", - "debugLog_enableInSettings": "Omogoči beleženje napak v aplikaciji v nastavitvah", - "debugLog_frames": "Okna", + "debugLog_bleTitle": "Log zapis BLE", + "debugLog_copyLog": "Kopiraj dnevnik", + "debugLog_clearLog": "Briši log", + "debugLog_copied": "Beležka kopirana.", + "debugLog_bleCopied": "Kopirana beležka iz BLE", + "debugLog_noEntries": "Ni ustvarjenih debug zapisov.", + "debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije", + "debugLog_frames": "Okvirji", "debugLog_rawLogRx": "Svež Log-RX", - "debugLog_noBleActivity": "Šele začnite z aktivnostjo BLE.", + "debugLog_noBleActivity": "Ni BLE aktivnosti.", "debugFrame_length": "Izhodni rob: {count} bajtov", "@debugFrame_length": { "placeholders": { @@ -542,8 +542,8 @@ "chat_forceFloodMode": "Nasilje obvezati v način", "chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):", "chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.", - "chat_hopSingular": "skoč", - "chat_hopPlural": "škrabec", + "chat_hopSingular": "skok", + "chat_hopPlural": "skokov", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { @@ -554,16 +554,16 @@ }, "chat_successes": "Uspešni", "chat_removePath": "Izbriši pot", - "chat_noPathHistoryYet": "Še ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.", + "chat_noPathHistoryYet": "Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.", "chat_pathActions": "Potni ukazi:", "chat_setCustomPath": "Nastavi Prilozeno Pot", "chat_setCustomPathSubtitle": "Ročno določite potniško pot.", - "chat_clearPath": "Čista pot", + "chat_clearPath": "Počisti pot", "chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.", "chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.", "chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.", "chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.", - "chat_fullPath": "Polni pot", + "chat_fullPath": "Polna pot", "chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.", "chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { @@ -1104,13 +1104,13 @@ } } }, - "repeater_cliQuickGetName": "Dobiti ime", + "repeater_cliQuickGetName": "Pridobi ime", "repeater_cliQuickGetRadio": "Dobiti Radiopravo", - "repeater_cliQuickGetTx": "Dobiti TX", + "repeater_cliQuickGetTx": "Pridobi TX", "repeater_cliQuickNeighbors": "Sosedi", "repeater_cliQuickVersion": "Različica", "repeater_cliQuickAdvertise": "Oglasite", - "repeater_cliQuickClock": "Urnik", + "repeater_cliQuickClock": "Ura", "repeater_cliHelpAdvert": "Pošlje paket oglasov", "repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)", "repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.", @@ -1142,7 +1142,7 @@ "repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.", "repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).", "repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).", - "repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustreznu vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).", + "repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).", "repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow", "repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.", "repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.", @@ -1171,8 +1171,8 @@ "repeater_settingsCategory": "Nastavitve", "repeater_bridge": "Most", "repeater_logging": "Logiranje", - "repeater_neighborsRepeaterOnly": "Sosedi (le za ponovitelja)", - "repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za ponovitve)", + "repeater_neighborsRepeaterOnly": "Sosedi (le za repetitorje)", + "repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za repetitorje)", "repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.", "repeater_gpsManagement": "Upravljanje GPS", "repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.", @@ -1244,9 +1244,9 @@ "channelPath_repeaterHops": "Skoki ponovitelja", "channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.", "channelPath_messageDetails": "Podrobnosti sporočila", - "channelPath_senderLabel": "Pošiljalec", - "channelPath_timeLabel": "Čas", - "channelPath_repeatsLabel": "Ponovi", + "channelPath_senderLabel": "Pošiljatelj", + "channelPath_timeLabel": "Ura", + "channelPath_repeatsLabel": "Ponovitve", "channelPath_pathLabel": "Pot {index}", "channelPath_observedLabel": "Opazovani", "channelPath_observedPathTitle": "Opazovana pot {index} • {hops}", @@ -1478,10 +1478,10 @@ "community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja", "community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.", "community_noCommunities": "Še nobena skupnost se ni pridružila.", - "community_scanOrCreate": "Skenirajte QR kodo ali ustvarite skupnost za začetek.", - "community_manageCommunities": "Upravljajte skupnosti", + "community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.", + "community_manageCommunities": "Upravljanje skupnosti", "community_delete": "Opusti skupnost", - "community_deleteConfirm": "Zapustiti \"{name}\"?", + "community_deleteConfirm": "Zapusti \"{name}\"?", "community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.", "@community_deleteChannelsWarning": { "placeholders": { @@ -1491,11 +1491,11 @@ } }, "community_deleted": "Zapustil skupnost \"{name}\"", - "community_addHashtagChannel": "Dodaj Oznako Obštnine", + "community_addHashtagChannel": "Dodaj hashtag kanal", "community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.", "community_selectCommunity": "Izberi skupnost", "community_regularHashtag": "Oznaka s hashtagom", - "community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)", + "community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)", "community_communityHashtag": "Skupnostni hashtag", "community_communityHashtagDesc": "Izključeno za uporabnike skupnosti", "community_forCommunity": "Za {name}", @@ -1527,11 +1527,11 @@ } } }, - "community_secretRegenerated": "Tajna za \"{name}\" ponovno ustvarjena", - "community_regenerateSecret": "Preberi nov tajni kôd", + "community_secretRegenerated": "Geslo za \"{name}\" ponovno ustvarjeno", + "community_regenerateSecret": "Ponovno ustvari geslo", "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.", "community_regenerate": "Preberi znova", - "community_scanToUpdateSecret": "Skeniraj nov kôd QR za posodabljanje tajne za {name}", - "community_updateSecret": "Ažurniraj tajno", + "community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}", + "community_updateSecret": "Ažuriraj ključ", "community_secretUpdated": "Skrivnostno spremembo za \"{name}\"" } From 88aa104ae51a956ddbd3fda0ba823f7f17788820 Mon Sep 17 00:00:00 2001 From: ericz Date: Sat, 24 Jan 2026 18:05:10 +0100 Subject: [PATCH 028/421] further translation fixes for german --- lib/l10n/app_de.arb | 22 +++++++++++----------- lib/l10n/app_localizations_de.dart | 23 +++++++++++------------ 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0bb17c8..3aba6e5 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -74,7 +74,7 @@ "settings_title": "Einstellungen", "settings_deviceInfo": "Geräteinformationen", "settings_appSettings": "App-Einstellungen", - "settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmungen", + "settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung", "settings_nodeSettings": "Knoten-Einstellungen", "settings_nodeName": "Knotenname", "settings_nodeNameNotSet": "Nicht festgelegt", @@ -266,7 +266,7 @@ } } }, - "contacts_manageRepeater": "Wiederholungen verwalten", + "contacts_manageRepeater": "Repeater verwalten", "contacts_roomLogin": "Raum-Login", "contacts_openChat": "Öffne Chat", "contacts_editGroup": "Gruppe bearbeiten", @@ -360,7 +360,7 @@ "channels_channelIndexLabel": "Kanalindex", "channels_channelName": "Kanalname", "channels_usePublicChannel": "Verwende öffentlichen Kanal", - "channels_standardPublicPsk": "Standard-Öffentliche PSK", + "channels_standardPublicPsk": "Öffentliche Standard PSK", "channels_pskHex": "PSK (Hex)", "channels_generateRandomPsk": "Zufällige PSK generieren", "channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.", @@ -489,8 +489,8 @@ } } }, - "debugFrame_textMessageHeader": "Textnachricht-Frame:", - "debugFrame_destinationPubKey": "- Ziel-Pub-Schlüssel: {pubKey}", + "debugFrame_textMessageHeader": "Textnachrichten Frame:", + "debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -1026,7 +1026,7 @@ "repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung", "repeater_dangerZone": "Gefahrenzone", "repeater_rebootRepeater": "Neustart Repeater", - "repeater_rebootRepeaterSubtitle": "Wiederholen Sie das Repeater-Gerät.", + "repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.", "repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?", "repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung", "repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren", @@ -1361,7 +1361,7 @@ "neighbors_receivedData": "Empfangene Nachbarendaten", "neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", - "neighbors_repeatersNeighbours": "Wiederholer Nachbarn", + "neighbors_repeatersNeighbours": "Nachbarn", "neighbors_noData": "Keine Nachbardaten verfügbar.", "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", @@ -1528,10 +1528,10 @@ } }, "community_regenerate": "Neu generieren", - "community_secretRegenerated": "Geheime Wiederherstellung für \"{name}\" erfolgreich", + "community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich", "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.", - "community_regenerateSecret": "Neu generieren Sie das Geheimnis", - "community_secretUpdated": "Geheime für \"{name}\" aktualisiert", + "community_regenerateSecret": "Neugenerierung des Schlüssels", + "community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert", "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", - "community_updateSecret": "Aktualisieren Sie das Geheimnis" + "community_updateSecret": "Aktualisieren Sie den Schlüssel" } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9bab237..53bec5e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -160,7 +160,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_appSettingsSubtitle => - 'Benachrichtigungen, Messaging und Kartenwahrnehmungen'; + 'Benachrichtigungen, Messaging und Kartenwahrnehmung'; @override String get settings_nodeSettings => 'Knoten-Einstellungen'; @@ -662,7 +662,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Wiederholungen verwalten'; + String get contacts_manageRepeater => 'Repeater verwalten'; @override String get contacts_manageRoom => 'Raum-Server verwalten'; @@ -796,7 +796,7 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_usePublicChannel => 'Verwende öffentlichen Kanal'; @override - String get channels_standardPublicPsk => 'Standard-Öffentliche PSK'; + String get channels_standardPublicPsk => 'Öffentliche Standard PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @@ -1029,11 +1029,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => 'Textnachricht-Frame:'; + String get debugFrame_textMessageHeader => 'Textnachrichten Frame:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Ziel-Pub-Schlüssel: $pubKey'; + return '- Ziel-Public-Schlüssel: $pubKey'; } @override @@ -1888,8 +1888,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_rebootRepeater => 'Neustart Repeater'; @override - String get repeater_rebootRepeaterSubtitle => - 'Wiederholen Sie das Repeater-Gerät.'; + String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.'; @override String get repeater_rebootRepeaterConfirm => @@ -2357,7 +2356,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Wiederholer Nachbarn'; + String get neighbors_repeatersNeighbours => 'Nachbarn'; @override String get neighbors_noData => 'Keine Nachbardaten verfügbar.'; @@ -2588,7 +2587,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get community_regenerateSecret => 'Neu generieren Sie das Geheimnis'; + String get community_regenerateSecret => 'Neugenerierung des Schlüssels'; @override String community_regenerateSecretConfirm(String name) { @@ -2600,15 +2599,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Geheime Wiederherstellung für \"$name\" erfolgreich'; + return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich'; } @override - String get community_updateSecret => 'Aktualisieren Sie das Geheimnis'; + String get community_updateSecret => 'Aktualisieren Sie den Schlüssel'; @override String community_secretUpdated(String name) { - return 'Geheime für \"$name\" aktualisiert'; + return 'Schlüssel für \"$name\" aktualisiert'; } @override From fcf741b20acfc2a923e309ff23bc49c667f577cb Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 24 Jan 2026 20:36:14 -0800 Subject: [PATCH 029/421] Got the basic path tracing working. --- lib/connector/meshcore_protocol.dart | 5 +++ lib/screens/contacts_screen.dart | 49 ++++++++++++++++++++++++-- lib/widgets/path_trace_dialog.dart | 52 ++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 lib/widgets/path_trace_dialog.dart diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index a4faf0e..f987a62 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:ffi'; import 'dart:typed_data'; // Buffer Reader - sequential binary data reader with pointer tracking @@ -18,6 +19,10 @@ class BufferReader { return data; } + void skipBytes(int count) { + _pointer += count; + } + Uint8List readRemainingBytes() => readBytes(remaining); String readString() => diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 02faff5..01e777a 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/widgets/path_trace_dialog.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -51,11 +54,14 @@ class _ContactsScreenState extends State final ContactGroupStore _groupStore = ContactGroupStore(); List _groups = []; Timer? _searchDebounce; - + StreamSubscription? _frameSubscription; + Uint8List _tagData = Uint8List(4); + @override void initState() { super.initState(); _loadGroups(); + _setupFrameListener(); } @override @@ -65,6 +71,44 @@ class _ContactsScreenState extends State super.dispose(); } + void _setupFrameListener() { + final connector = Provider.of(context, listen: false); + + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + + if (frame[0] == respCodeSent) { + _tagData = frame.sublist(2, 6); + print("Stored tag data: $_tagData"); + } + + // Check if it's a binary response + if (frame[0] == pushCodeTraceData && listEquals(frame.sublist(4, 8), _tagData)) { + if (!mounted) return; + _handleTraceResponse(frame); + } + }); + } + + Future _handleTraceResponse(Uint8List frame)async { + final buffer = BufferReader(frame); + buffer.skipBytes(2); // Skip push code and reserved byte + int pathLength = buffer.readUInt8(); + buffer.skipBytes(5); // Skip Flag byte and tag data + buffer.skipBytes(4); // Skip auth code + Uint8List pathData = buffer.readBytes(pathLength); + Uint8List snrData = buffer.readRemainingBytes(); + print("Received path data length: $pathLength, SNR data length: ${snrData.length}"); + showDialog( + context: context, + builder: (context) => PathTraceDialog( + pathData: pathData, + snrData: snrData, + ), + ); + } + Future _loadGroups() async { final groups = await _groupStore.loadGroups(); if (!mounted) return; @@ -757,11 +801,12 @@ class _ContactsScreenState extends State leading: const Icon(Icons.radar, color: Colors.green), title: Text("Ping"), onTap: () async { + Navigator.pop(sheetContext); final frame = buildTraceReq( DateTime.now().millisecondsSinceEpoch ~/ 1000, 0, 0, - payload: contact.publicKey.sublist(0,1), + payload: Uint8List.fromList([0x85,0x91,0x07,0x91,0x85]) //contact.publicKey.sublist(0,1), ); await connector.sendFrame(frame); } diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart new file mode 100644 index 0000000..c5e4cad --- /dev/null +++ b/lib/widgets/path_trace_dialog.dart @@ -0,0 +1,52 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:meshcore_open/widgets/snr_indicator.dart'; + +class PathTraceDialog extends StatefulWidget { + + const PathTraceDialog({ + super.key, + required this.pathData, + required this.snrData, + }); + + final Uint8List pathData; + final Uint8List snrData; + + @override + State createState() => _PathTraceDialogState(); +} + +class _PathTraceDialogState extends State { + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Path Trace'), + content: SizedBox( + width: double.maxFinite, + child: ListView.builder( + itemCount: widget.snrData.length, + itemBuilder: (context, index) { + return ListTile( + leading: index >= widget.snrData.length / 2 ? Icon(Icons.arrow_circle_left) : Icon(Icons.arrow_circle_right), + title: index == 0 || index == widget.snrData.length - 1 ? ( index == 0 ? Text('You to 0x${widget.pathData[0].toRadixString(16).toUpperCase()}') : Text('0x${widget.pathData[widget.pathData.length - 1].toRadixString(16).toUpperCase()} to You')) : Text('0x${widget.pathData[index-1].toRadixString(16).toUpperCase()} to 0x${widget.pathData[index].toRadixString(16).toUpperCase()}'), + trailing: SNRIcon(snr: widget.snrData[index] / 4.0), + onTap: () { + // Handle item tap + }, + + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ); + } +} From bb18038f603374399f739c2fff8201811d1e0400 Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 25 Jan 2026 11:40:02 +0100 Subject: [PATCH 030/421] removed truncation of notification as in Issue #107 --- lib/services/notification_service.dart | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index cefeb2a..1d25f92 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -112,7 +112,7 @@ class NotificationService { await _notifications.show( contactId?.hashCode ?? 0, 'New message from $contactName', - message.length > 100 ? '${message.substring(0, 100)}...' : message, + message, notificationDetails, payload: 'message:$contactId', ); @@ -203,7 +203,7 @@ class NotificationService { macOS: macDetails, ); - final preview = _truncateMessage(message, 30); + final preview = message.trim(); final body = preview.isEmpty ? 'Received new message' : preview; @@ -217,12 +217,6 @@ class NotificationService { ); } - String _truncateMessage(String message, int maxLength) { - final trimmed = message.trim(); - if (trimmed.length <= maxLength) return trimmed; - return '${trimmed.substring(0, maxLength)}...'; - } - void _onNotificationTapped(NotificationResponse response) { final payload = response.payload; if (payload != null) { From 0ebd6887873e9d39e9093cb239c867812d92a59a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 25 Jan 2026 10:53:28 -0800 Subject: [PATCH 031/421] Added shortPubKeyHex and added a trace route builder traceRouteBytes --- lib/models/contact.dart | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 364deff..831c1ec 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -102,6 +102,47 @@ class Contact { return parts.join(','); } + String get shortPubKeyHex { + return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; + } + + Uint8List? get traceRouteBytes { + final pathBytes = _pathBytesForDisplay; + Uint8List? traceBytes; + + if(pathLength <= 0) { + traceBytes = Uint8List(1); + traceBytes[0] = publicKey[0]; + return traceBytes; + } + + if(type == advTypeRepeater || type == advTypeRoom) { + final len = (pathBytes.length + pathBytes.length + 1); + traceBytes = Uint8List(len); + traceBytes[pathBytes.length] = publicKey[0]; + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length) { + traceBytes[len-1-i] = pathBytes[i]; + } + } + } else { + if(pathBytes.length < 2) { + return pathBytes[0] == 0 ? null : pathBytes; + } + final len = (pathBytes.length + pathBytes.length-1); + traceBytes = Uint8List(len); + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length-1) { + traceBytes[len-1-i] = pathBytes[i]; + } + } + } + print(traceBytes); + return traceBytes; + } + Uint8List get _pathBytesForDisplay { if (pathOverride != null) { if (pathOverride! < 0) return Uint8List(0); From cacb9bc67769ed884e8eea9d2cb1b8b53514f35b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 25 Jan 2026 10:55:42 -0800 Subject: [PATCH 032/421] Moved all the path tracing logic to the dialog. refactored repeater hub along with contacts screen to use shortPubKeyHex. Added localization strings for path tracing, english only. --- lib/l10n/app_en.arb | 21 ++- lib/screens/contacts_screen.dart | 96 ++++-------- lib/screens/repeater_hub_screen.dart | 2 +- lib/widgets/path_trace_dialog.dart | 217 ++++++++++++++++++++++++--- 4 files changed, 247 insertions(+), 89 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 56cb1cc..d191370 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1308,5 +1308,24 @@ "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", - "listFilter_newGroup": "New group" + "listFilter_newGroup": "New group", + + "pathTrace_you": "You", + "pathTrace_failed": "Path trace failed.", + "pathTrace_notAvailable": "Path trace not available.", + "pathTrace_refreshTooltip": "Refresh Path Trace.", + "contacts_pathTrace": "Path Trace", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Path trace to repeater", + "contacts_repeaterPing": "Ping repeater", + "contacts_roomPathTrace": "Path trace to room server", + "contacts_roomPing": "Ping room server", + "contacts_chatTraceRoute": "Path trace route", + "contacts_pathTraceTo": "Trace route to {name}", + "@contacts_pathTraceTo": { + "placeholders": { + "name": {"type": "String"} + } + } + } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 01e777a..d3fcaa0 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -54,14 +54,11 @@ class _ContactsScreenState extends State final ContactGroupStore _groupStore = ContactGroupStore(); List _groups = []; Timer? _searchDebounce; - StreamSubscription? _frameSubscription; - Uint8List _tagData = Uint8List(4); @override void initState() { super.initState(); _loadGroups(); - _setupFrameListener(); } @override @@ -71,44 +68,6 @@ class _ContactsScreenState extends State super.dispose(); } - void _setupFrameListener() { - final connector = Provider.of(context, listen: false); - - // Listen for incoming text messages from the repeater - _frameSubscription = connector.receivedFrames.listen((frame) { - if (frame.isEmpty) return; - - if (frame[0] == respCodeSent) { - _tagData = frame.sublist(2, 6); - print("Stored tag data: $_tagData"); - } - - // Check if it's a binary response - if (frame[0] == pushCodeTraceData && listEquals(frame.sublist(4, 8), _tagData)) { - if (!mounted) return; - _handleTraceResponse(frame); - } - }); - } - - Future _handleTraceResponse(Uint8List frame)async { - final buffer = BufferReader(frame); - buffer.skipBytes(2); // Skip push code and reserved byte - int pathLength = buffer.readUInt8(); - buffer.skipBytes(5); // Skip Flag byte and tag data - buffer.skipBytes(4); // Skip auth code - Uint8List pathData = buffer.readBytes(pathLength); - Uint8List snrData = buffer.readRemainingBytes(); - print("Received path data length: $pathLength, SNR data length: ${snrData.length}"); - showDialog( - context: context, - builder: (context) => PathTraceDialog( - pathData: pathData, - snrData: snrData, - ), - ); - } - Future _loadGroups() async { final groups = await _groupStore.loadGroups(); if (!mounted) return; @@ -799,16 +758,14 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: Text("Ping"), - onTap: () async { - Navigator.pop(sheetContext); - final frame = buildTraceReq( - DateTime.now().millisecondsSinceEpoch ~/ 1000, - 0, - 0, - payload: Uint8List.fromList([0x85,0x91,0x07,0x91,0x85]) //contact.publicKey.sublist(0,1), - ); - await connector.sendFrame(frame); + title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), + onTap: () { + showDialog(context: context, builder: (context) { + return PathTraceDialog( + title: contact.pathLength > 0 ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing, + path: contact.traceRouteBytes ?? Uint8List(0), + ); + }); } ), ListTile( @@ -822,15 +779,14 @@ class _ContactsScreenState extends State ]else if (isRoom) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: Text("Ping"), - onTap: () async { - final frame = buildTraceReq( - DateTime.now().millisecondsSinceEpoch ~/ 1000, - 0, - 0, - payload: contact.publicKey.sublist(0,1), - ); - await connector.sendFrame(frame); + title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), + onTap: () { + showDialog(context: context, builder: (context) { + return PathTraceDialog( + title: contact.pathLength > 0 ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, + path: contact.traceRouteBytes ?? Uint8List(0), + ); + }); } ), ListTile( @@ -849,7 +805,20 @@ class _ContactsScreenState extends State _showRoomLogin(context, contact, RoomLoginDestination.management); }, ), - ] else + ] else ...[ + if(contact.pathLength > 0) + ListTile( + leading: const Icon(Icons.radar, color: Colors.green), + title: Text(context.l10n.contacts_chatTraceRoute), + onTap: () { + showDialog(context: context, builder: (context) { + return PathTraceDialog( + title: context.l10n.contacts_pathTraceTo(contact.name), + path: contact.traceRouteBytes ?? Uint8List(0), + ); + }); + } + ), ListTile( leading: const Icon(Icons.chat), title: Text(context.l10n.contacts_openChat), @@ -869,6 +838,7 @@ class _ContactsScreenState extends State _confirmDelete(context, connector, contact); }, ), + ], ], ), ), @@ -923,8 +893,6 @@ class _ContactTile extends StatelessWidget { @override Widget build(BuildContext context) { - final shotPublicKey = - "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>"; return ListTile( leading: CircleAvatar( backgroundColor: _getTypeColor(contact.type), @@ -932,7 +900,7 @@ class _ContactTile extends StatelessWidget { ), title: Text(contact.name), subtitle: Text( - '${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey', + '${contact.typeLabel} • ${contact.pathLabel} ${contact.shortPubKeyHex}', ), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 5a545f3..846d0c5 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -73,7 +73,7 @@ class RepeaterHubScreen extends StatelessWidget { ), const SizedBox(height: 8), Text( - '<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>', + '$repeater.shortPubKeyHex', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), const SizedBox(height: 8), diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart index c5e4cad..2ffb110 100644 --- a/lib/widgets/path_trace_dialog.dart +++ b/lib/widgets/path_trace_dialog.dart @@ -1,50 +1,221 @@ +import 'dart:async'; import 'dart:typed_data'; -import 'package:flutter/material.dart'; -import 'package:meshcore_open/widgets/snr_indicator.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../connector/meshcore_connector.dart'; +import '../connector/meshcore_protocol.dart'; +import '../models/contact.dart'; +import '../widgets/snr_indicator.dart'; +import '../l10n/l10n.dart'; class PathTraceDialog extends StatefulWidget { const PathTraceDialog({ super.key, - required this.pathData, - required this.snrData, + required this.title, + required this.path, }); - final Uint8List pathData; - final Uint8List snrData; + final String title; + final Uint8List path; @override State createState() => _PathTraceDialogState(); } class _PathTraceDialogState extends State { + StreamSubscription? _frameSubscription; + Timer? _timeoutTimer; + + bool _isLoading = false; + bool _failed2Loaded = false; + bool _hasData = false; + Uint8List _pathData = Uint8List(0); + Uint8List _snrData = Uint8List(0) ; + Map _pathContacts = {}; + + @override + void initState() { + super.initState(); + _setupFrameListener(); + _doPathTrace(); + } + + @override + void dispose() { + _frameSubscription?.cancel(); + _timeoutTimer?.cancel(); + super.dispose(); + } + + Future _doPathTrace() async { + if(mounted) { + setState(() { + _isLoading = true; + _failed2Loaded = false; + }); + } + + final connector = Provider.of(context, listen: false); + final frame = buildTraceReq( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + 0, //flags + 0, //auth + payload: widget.path, + ); + connector.sendFrame(frame); + } + + void _setupFrameListener() { + final connector = Provider.of(context, listen: false); + Uint8List tagData = Uint8List(4); + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + final frameBuffer = BufferReader(frame); + final code = frameBuffer.readUInt8(); + + if (code == respCodeSent) { + frameBuffer.skipBytes(1); //reserved + tagData = frameBuffer.readBytes(4); + final timeoutSeconds = frameBuffer.readUInt32LE(); + + // Start timeout timer for trace response + _timeoutTimer?.cancel(); + _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () { + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + }); + } + + // Check if it's a binary response + if (code == pushCodeTraceData && listEquals(frame.sublist(4, 8), tagData)) { + _timeoutTimer?.cancel(); + if (!mounted) return; + frameBuffer.skipBytes(3); //reserved + path length + flag + if(listEquals(frameBuffer.readBytes(4), tagData)){ + _handleTraceResponse(frame); + } + } + }); + } + + Future _handleTraceResponse(Uint8List frame)async { + final connector = Provider.of(context, listen: false); + + final buffer = BufferReader(frame); + buffer.skipBytes(2); // Skip push code and reserved byte + int pathLength = buffer.readUInt8(); + buffer.skipBytes(5); // Skip Flag byte and tag data + buffer.skipBytes(4); // Skip auth code + Uint8List pathData = buffer.readBytes(pathLength); + Uint8List snrData = buffer.readRemainingBytes(); + + Map pathContacts = {}; + + connector.contacts.where((c) => c.type != advTypeChat).forEach(( + repeater, + ) { + for (var neighbourData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([neighbourData]), + )) { + pathContacts[neighbourData] = repeater; + } + } + }); + + setState(() { + _isLoading = false; + _hasData = true; + _pathData = pathData; + _snrData = snrData; + _pathContacts = pathContacts; + }); + } + + String formatDirectionText(int index) { + if (index == 0 || index == _snrData.length - 1) { + if (index == 0) { + return context.l10n.pathTrace_you; + } else { + return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}"; + } + } else { + return _pathContacts[_pathData[index-1]]?.name ?? "0x${_pathData[index-1].toRadixString(16).toUpperCase()}"; + } + } + String formatDirectionSubText(int index) { + if (index == 0 || index == _snrData.length - 1) { + if (index == 0) { + return _pathContacts[_pathData[0]]?.name ?? "0x${_pathData[0].toRadixString(16).toUpperCase()}"; + } else { + return context.l10n.pathTrace_you; + } + } else { + return _pathContacts[_pathData[index]]?.name ?? "0x${_pathData[index].toRadixString(16).toUpperCase()}"; + } + } @override Widget build(BuildContext context) { + final l10n = context.l10n; return AlertDialog( - title: const Text('Path Trace'), - content: SizedBox( - width: double.maxFinite, - child: ListView.builder( - itemCount: widget.snrData.length, - itemBuilder: (context, index) { - return ListTile( - leading: index >= widget.snrData.length / 2 ? Icon(Icons.arrow_circle_left) : Icon(Icons.arrow_circle_right), - title: index == 0 || index == widget.snrData.length - 1 ? ( index == 0 ? Text('You to 0x${widget.pathData[0].toRadixString(16).toUpperCase()}') : Text('0x${widget.pathData[widget.pathData.length - 1].toRadixString(16).toUpperCase()} to You')) : Text('0x${widget.pathData[index-1].toRadixString(16).toUpperCase()} to 0x${widget.pathData[index].toRadixString(16).toUpperCase()}'), - trailing: SNRIcon(snr: widget.snrData[index] / 4.0), - onTap: () { - // Handle item tap - }, - - ); - }, + title: Column( children: [ + FittedBox(fit: BoxFit.scaleDown, child: Text(widget.title, style: const TextStyle(fontSize: 24))), + if(_failed2Loaded) + Text(l10n.pathTrace_failed, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.error),), + ], + ), + content: SafeArea( + child: RefreshIndicator( + onRefresh: _doPathTrace, + child: !_hasData + ? Center( + child: Text(l10n.pathTrace_notAvailable), + ) + : ListView.builder( + itemCount: _snrData.length, + itemBuilder: (context, index) { + return ListTile( + leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made), + title: Text( + formatDirectionText(index), style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + formatDirectionSubText(index), + style: const TextStyle(fontSize: 14), + ), + trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0), + onTap: () { + // Handle item tap + }, + ); + }, + ), ), ), actions: [ + IconButton( + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + onPressed: _isLoading ? null : _doPathTrace, + tooltip: l10n.pathTrace_refreshTooltip, + ), TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('Close'), + child: Text(l10n.common_close), ), ], ); From 9c1b5899fb8ce8bd9ca807920b37d378dcc4e163 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 25 Jan 2026 11:55:55 -0800 Subject: [PATCH 033/421] Added scroll view to room server login. Disabled autofocus of password. --- lib/widgets/path_trace_dialog.dart | 31 +++++++++++++++++------------- lib/widgets/room_login_dialog.dart | 6 ++++-- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart index 2ffb110..0e6fd19 100644 --- a/lib/widgets/path_trace_dialog.dart +++ b/lib/widgets/path_trace_dialog.dart @@ -183,19 +183,24 @@ class _PathTraceDialogState extends State { : ListView.builder( itemCount: _snrData.length, itemBuilder: (context, index) { - return ListTile( - leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made), - title: Text( - formatDirectionText(index), style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - formatDirectionSubText(index), - style: const TextStyle(fontSize: 14), - ), - trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0), - onTap: () { - // Handle item tap - }, + return Column( + children: [ + ListTile( + leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made), + title: Text( + formatDirectionText(index), style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + formatDirectionSubText(index), + style: const TextStyle(fontSize: 14), + ), + trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0), + onTap: () { + // Handle item tap + }, + ), + if (index < _snrData.length - 1) const Divider(height: 0.0), + ], ); }, ), diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 838ecf8..69f8dc6 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -261,7 +261,8 @@ class _RoomLoginDialogState extends State { child: CircularProgressIndicator(), ), ) - : Column( + : SingleChildScrollView( + child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -292,7 +293,7 @@ class _RoomLoginDialogState extends State { ), ), onSubmitted: (_) => _handleLogin(), - autofocus: _passwordController.text.isEmpty, + //autofocus: _passwordController.text.isEmpty, ), const SizedBox(height: 12), CheckboxListTile( @@ -382,6 +383,7 @@ class _RoomLoginDialogState extends State { ), ], ), + ), actions: [ TextButton( onPressed: () => Navigator.pop(context), From 749f9d4dfdd7fb8a02f246b82b2be55956fb865b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 25 Jan 2026 12:00:38 -0800 Subject: [PATCH 034/421] cleaned up. --- lib/connector/meshcore_protocol.dart | 1 - lib/models/contact.dart | 1 - lib/screens/contacts_screen.dart | 1 - lib/widgets/path_trace_dialog.dart | 2 -- 4 files changed, 5 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index f987a62..470b795 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:ffi'; import 'dart:typed_data'; // Buffer Reader - sequential binary data reader with pointer tracking diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 831c1ec..c9e40ab 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -139,7 +139,6 @@ class Contact { } } } - print(traceBytes); return traceBytes; } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d3fcaa0..efaacc6 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart index 0e6fd19..958258b 100644 --- a/lib/widgets/path_trace_dialog.dart +++ b/lib/widgets/path_trace_dialog.dart @@ -1,6 +1,4 @@ import 'dart:async'; -import 'dart:typed_data'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; From 898ef1c11c88fd609a53aca91bd4d3f821670670 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 10:40:10 -0800 Subject: [PATCH 035/421] Refactor autofocus logic in login dialogs for better platform handling --- lib/screens/repeater_hub_screen.dart | 2 +- lib/widgets/repeater_login_dialog.dart | 4 +++- lib/widgets/room_login_dialog.dart | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 846d0c5..903f89e 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -73,7 +73,7 @@ class RepeaterHubScreen extends StatelessWidget { ), const SizedBox(height: 8), Text( - '$repeater.shortPubKeyHex', + repeater.shortPubKeyHex, style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), const SizedBox(height: 8), diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 54c0150..1f767f6 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -322,7 +322,9 @@ class _RepeaterLoginDialogState extends State { } }, onSubmitted: (_) => _handleLogin(), - autofocus: _passwordController.text.isEmpty, + autofocus: !(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) && + _passwordController.text.isEmpty, ), const SizedBox(height: 12), CheckboxListTile( diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 69f8dc6..1d2554d 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -293,7 +293,9 @@ class _RoomLoginDialogState extends State { ), ), onSubmitted: (_) => _handleLogin(), - //autofocus: _passwordController.text.isEmpty, + autofocus: !(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) && + _passwordController.text.isEmpty, ), const SizedBox(height: 12), CheckboxListTile( From c37abb63e37b26ba25893f71fa97bcfd90047059 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 11:56:42 -0800 Subject: [PATCH 036/421] add export and import contact frame builders in meshcore_protocol.dart and implement contact export functionality in contacts_screen.dart --- lib/connector/meshcore_protocol.dart | 18 ++++++ lib/screens/contacts_screen.dart | 89 ++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index f9241e8..0e43e46 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -708,3 +708,21 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { } return writer.toBytes(); } + +// Build a export contact frame +// [cmd][pub_key x32 / if empty exports your contact info] +Uint8List buildExportContactFrame(Uint8List pubKey) { + final writer = BufferWriter(); + writer.writeByte(cmdExportContact); + writer.writeBytes(pubKey); + return writer.toBytes(); +} + +// Build a import contact frame +// [cmd][contact_frame x148] +Uint8List buildImportContactFrame(Uint8List contactFrame) { + final writer = BufferWriter(); + writer.writeByte(cmdImportContact); + writer.writeBytes(contactFrame); + return writer.toBytes(); +} \ No newline at end of file diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 54f819c..4c8e396 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -77,6 +78,13 @@ class _ContactsScreenState extends State await _groupStore.saveGroups(_groups); } + Future _contactExport(Uint8List pubKey) async { + final connector = Provider.of(context, listen: false); + final exportContactFrame = buildExportContactFrame(pubKey); + await connector.sendFrame(exportContactFrame); + return; + } + @override Widget build(BuildContext context) { final connector = context.watch(); @@ -96,18 +104,77 @@ class _ContactsScreenState extends State centerTitle: true, automaticallyImplyLeading: false, actions: [ - IconButton( - icon: const Icon(Icons.bluetooth_disabled), - tooltip: context.l10n.common_disconnect, - onPressed: () => _disconnect(context, connector), - ), - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.common_settings, - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), + PopupMenuButton(itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.connect_without_contact), + const SizedBox(width: 8), + Text("Zero Hop Advert"), + ], + ), + onTap: () => { + connector.sendSelfAdvert(flood: false), + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), + }, ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.cell_tower), + const SizedBox(width: 8), + Text("Flood Advert"), + ], + ), + onTap: () => { + connector.sendSelfAdvert(flood: true), + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.copy), + const SizedBox(width: 8), + Text("Copy Advert to Clipboard"), + ], + ), + onTap: () => _contactExport(Uint8List.fromList([])), + ), + ], + icon: const Icon(Icons.connect_without_contact), + ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + const Text('Disconnect'), + ], + ), + onTap: () => _disconnect(context, connector), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 8), + const Text('Settings'), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + ], + icon: const Icon(Icons.more_vert), ), ], ), From 641307a31632574a81dc59ca18f26ff9342eaf1b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 12:19:45 -0800 Subject: [PATCH 037/421] Added response code for exporting contacts and implement frame listener in contacts_screen.dart --- lib/connector/meshcore_protocol.dart | 1 + lib/screens/contacts_screen.dart | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0e43e46..dda07bd 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -159,6 +159,7 @@ const int respCodeContactMsgRecv = 7; const int respCodeChannelMsgRecv = 8; const int respCodeCurrTime = 9; const int respCodeNoMoreMessages = 10; +const int respCodeExportContact = 11; const int respCodeBattAndStorage = 12; const int respCodeDeviceInfo = 13; const int respCodeContactMsgRecvV3 = 16; diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 4c8e396..d5d3377 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -53,16 +54,20 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; + StreamSubscription? _frameSubscription; + @override void initState() { super.initState(); _loadGroups(); + _setupFrameListener(); } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); + _frameSubscription?.cancel(); super.dispose(); } @@ -78,6 +83,22 @@ class _ContactsScreenState extends State await _groupStore.saveGroups(_groups); } + void _setupFrameListener() { + final connector = Provider.of(context, listen: false); + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + final frameBuffer = BufferReader(frame); + final code = frameBuffer.readUInt8(); + + if (code == respCodeExportContact) { + final advertPacket = frameBuffer.readRemainingBytes(); + final hexString = pubKeyToHex(advertPacket); + Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); + } + }); + } + Future _contactExport(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); From eeb8ff34e8959bd060b0ae4aa706fb341d7f6c0a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 16:11:21 -0800 Subject: [PATCH 038/421] Implement contact import functionality from clipboard and add relevant UI options --- lib/connector/meshcore_protocol.dart | 14 +++++-- lib/l10n/app_en.arb | 6 ++- lib/screens/contacts_screen.dart | 60 +++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index dda07bd..0609adb 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -98,6 +98,14 @@ class BufferWriter { } writeBytes(bytes); } + + void writeHex(String hex) { + List result = []; + for (int i = 0; i < hex.length ~/ 2; i++) { + result.add(int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)); + } + writeBytes(Uint8List.fromList(result)); + } } // Command codes (to device) @@ -720,10 +728,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) { } // Build a import contact frame -// [cmd][contact_frame x148] -Uint8List buildImportContactFrame(Uint8List contactFrame) { +// [cmd][contact_frame x98+] +Uint8List buildImportContactFrame(String contactFrame) { final writer = BufferWriter(); writer.writeByte(cmdImportContact); - writer.writeBytes(contactFrame); + writer.writeHex(contactFrame); return writer.toBytes(); } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 56cb1cc..7c8a0a7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1308,5 +1308,9 @@ "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", - "listFilter_newGroup": "New group" + "listFilter_newGroup": "New group", + + "contacts_clipboardEmpty": "Clipboard Is Empty.", + "contacts_invalidAdvertFormat": "Invalid Contact Data", + "contacts_contactAdded": "Contact has been added." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d5d3377..89a07e0 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -106,6 +106,36 @@ class _ContactsScreenState extends State return; } + Future _contactImport() async { + final clipboardData = await Clipboard.getData('text/plain'); + if (clipboardData == null || clipboardData.text == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), + ); + return; + } + final text = clipboardData.text!.trim(); + if (!text.startsWith('meshcore://')) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + return; + } + final hexString = text.substring('meshcore://'.length); + try { + final connector = Provider.of(context, listen: false); + final importContactFrame = buildImportContactFrame(hexString); + await connector.sendFrame(importContactFrame); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text(context.l10n.contacts_contactAdded)), + // ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } + } + @override Widget build(BuildContext context) { final connector = context.watch(); @@ -166,21 +196,21 @@ class _ContactsScreenState extends State ), onTap: () => _contactExport(Uint8List.fromList([])), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.paste), + const SizedBox(width: 8), + Text("Add Contact from Clipboard"), + ], + ), + onTap: () => _contactImport(), + ), ], icon: const Icon(Icons.connect_without_contact), ), PopupMenuButton( itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - const Text('Disconnect'), - ], - ), - onTap: () => _disconnect(context, connector), - ), PopupMenuItem( child: Row( children: [ @@ -194,6 +224,16 @@ class _ContactsScreenState extends State MaterialPageRoute(builder: (context) => const SettingsScreen()), ), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + const Text('Disconnect'), + ], + ), + onTap: () => _disconnect(context, connector), + ) ], icon: const Icon(Icons.more_vert), ), From d0c8fab6fbe451456fe3e98ce3ab5b9150e7b454 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 27 Jan 2026 18:43:59 -0800 Subject: [PATCH 039/421] Add contact import functionality and update UI feedback for import status --- lib/connector/meshcore_connector.dart | 7 +++++ lib/l10n/app_en.arb | 17 ++++++++++- lib/screens/contacts_screen.dart | 44 ++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index de39d7e..a5c5290 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1680,6 +1680,12 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; + case pushCodeNewAdvert: + debugPrint('Got NEW_ADVERT'); + _handleContact(frame); + notifyListeners(); + unawaited(_persistContacts()); + break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1737,6 +1743,7 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); + break; default: debugPrint('Unknown frame code: $code'); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7c8a0a7..3a9383a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1309,8 +1309,23 @@ "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", "listFilter_newGroup": "New group", + + "contacts_pathTrace": "Path Trace", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Path trace to repeater", + "contacts_repeaterPing": "Ping repeater", + "contacts_roomPathTrace": "Path trace to room server", + "contacts_roomPing": "Ping room server", + "contacts_chatTraceRoute": "Path trace route", + "contacts_pathTraceTo": "Trace route to {name}", + "@contacts_pathTraceTo": { + "placeholders": { + "name": {"type": "String"} + } + }, "contacts_clipboardEmpty": "Clipboard Is Empty.", "contacts_invalidAdvertFormat": "Invalid Contact Data", - "contacts_contactAdded": "Contact has been added." + "contacts_contactImported": "Contact has been Imported.", + "contacts_contactImportFailed": "Contact Failed to Imported." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 89a07e0..64c2924 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -54,6 +54,7 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; + bool _imported = false; StreamSubscription? _frameSubscription; @override @@ -96,6 +97,23 @@ class _ContactsScreenState extends State final hexString = pubKeyToHex(advertPacket); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } + + if(code == respCodeOk && _imported) { + // Show a snackbar indicating success + _imported = false; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImported)), + ); + } + + if(code == respCodeErr && _imported) { + // Show a snackbar indicating success + _imported = false; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), + ); + } + }); } @@ -126,9 +144,7 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final importContactFrame = buildImportContactFrame(hexString); await connector.sendFrame(importContactFrame); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text(context.l10n.contacts_contactAdded)), - // ); + _imported = true; } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), @@ -923,6 +939,7 @@ class _ContactsScreenState extends State _openChat(context, contact); }, ), + ], ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: Text( @@ -997,7 +1014,7 @@ class _ContactTile extends StatelessWidget { ), title: Text(contact.name), subtitle: Text( - '${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey', + '${contact.typeLabel}\n${contact.shortPubKeyHex}', ), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. @@ -1019,13 +1036,24 @@ class _ContactTile extends StatelessWidget { _formatLastSeen(context, lastSeen), style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(contact.pathLabel, + style: TextStyle(fontSize: 12, color: Colors.grey[600])), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + ], + ), + Text( + _formatLastSeen(context, lastSeen), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), ), - onTap: onTap, - onLongPress: onLongPress, ); } From 42115bf200e4421c3d914b9c6ebd74345b5c8060 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Wed, 28 Jan 2026 11:04:34 -0800 Subject: [PATCH 040/421] Refactor contact handling and enhance UI with new advert options and localized strings --- lib/connector/meshcore_connector.dart | 7 ---- lib/connector/meshcore_protocol.dart | 21 +++++++++++ lib/l10n/app_en.arb | 6 +++- lib/models/contact.dart | 4 +++ lib/screens/contacts_screen.dart | 51 ++++++++++++--------------- 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a5c5290..de39d7e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1680,12 +1680,6 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; - case pushCodeNewAdvert: - debugPrint('Got NEW_ADVERT'); - _handleContact(frame); - notifyListeners(); - unawaited(_persistContacts()); - break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1743,7 +1737,6 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); - break; default: debugPrint('Unknown frame code: $code'); } diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0609adb..df07758 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -18,6 +18,10 @@ class BufferReader { return data; } + void skipBytes(int count) { + _pointer += count; + } + Uint8List readRemainingBytes() => readBytes(remaining); String readString() => @@ -135,6 +139,7 @@ const int cmdSendStatusReq = 27; const int cmdGetContactByKey = 30; const int cmdGetChannel = 31; const int cmdSetChannel = 32; +const int cmdSendTracePath = 36; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; @@ -185,6 +190,7 @@ const int pushCodeLoginSuccess = 0x85; const int pushCodeLoginFail = 0x86; const int pushCodeStatusResponse = 0x87; const int pushCodeLogRxData = 0x88; +const int pushCodeTraceData = 0x89; const int pushCodeNewAdvert = 0x8A; const int pushCodeTelemetryResponse = 0x8B; const int pushCodeBinaryResponse = 0x8C; @@ -718,6 +724,21 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { return writer.toBytes(); } +//Build a trace request frame +//[cmd][tag x4][auth x4][flag][payload] +Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) +{ + final writer = BufferWriter(); + writer.writeByte(cmdSendTracePath); + writer.writeUInt32LE(tag); + writer.writeUInt32LE(auth); + writer.writeByte(flag); + if (payload != null && payload.isNotEmpty) { + writer.writeBytes(payload); + } + return writer.toBytes(); +} + // Build a export contact frame // [cmd][pub_key x32 / if empty exports your contact info] Uint8List buildExportContactFrame(Uint8List pubKey) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3a9383a..706d361 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1327,5 +1327,9 @@ "contacts_clipboardEmpty": "Clipboard Is Empty.", "contacts_invalidAdvertFormat": "Invalid Contact Data", "contacts_contactImported": "Contact has been Imported.", - "contacts_contactImportFailed": "Contact Failed to Imported." + "contacts_contactImportFailed": "Contact Failed to Imported.", + "contacts_zeroHopAdvert":"Zero Hop Advert", + "contacts_floodAdvert":"Flood Advert", + "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", + "contacts_addContactFromClipboard":"Add Contact from Clipboard" } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 364deff..9599e01 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -102,6 +102,10 @@ class Contact { return parts.join(','); } + String get shortPubKeyHex { + return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; + } + Uint8List get _pathBytesForDisplay { if (pathOverride != null) { if (pathOverride! < 0) return Uint8List(0); diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 64c2924..e9aef26 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -177,7 +177,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.connect_without_contact), const SizedBox(width: 8), - Text("Zero Hop Advert"), + Text(context.l10n.contacts_zeroHopAdvert), ], ), onTap: () => { @@ -192,7 +192,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.cell_tower), const SizedBox(width: 8), - Text("Flood Advert"), + Text(context.l10n.contacts_floodAdvert), ], ), onTap: () => { @@ -207,7 +207,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.copy), const SizedBox(width: 8), - Text("Copy Advert to Clipboard"), + Text(context.l10n.contacts_copyAdvertToClipboard), ], ), onTap: () => _contactExport(Uint8List.fromList([])), @@ -217,7 +217,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.paste), const SizedBox(width: 8), - Text("Add Contact from Clipboard"), + Text(context.l10n.contacts_addContactFromClipboard), ], ), onTap: () => _contactImport(), @@ -227,12 +227,22 @@ class _ContactsScreenState extends State ), PopupMenuButton( itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.common_disconnect), + ], + ), + onTap: () => _disconnect(context, connector), + ), PopupMenuItem( child: Row( children: [ const Icon(Icons.settings), const SizedBox(width: 8), - const Text('Settings'), + Text(context.l10n.settings_title), ], ), onTap: () => Navigator.push( @@ -240,16 +250,6 @@ class _ContactsScreenState extends State MaterialPageRoute(builder: (context) => const SettingsScreen()), ), ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - const Text('Disconnect'), - ], - ), - onTap: () => _disconnect(context, connector), - ) ], icon: const Icon(Icons.more_vert), ), @@ -930,7 +930,7 @@ class _ContactsScreenState extends State _showRoomLogin(context, contact, RoomLoginDestination.management); }, ), - ] else + ] else ...[ ListTile( leading: const Icon(Icons.chat), title: Text(context.l10n.contacts_openChat), @@ -1005,17 +1005,16 @@ class _ContactTile extends StatelessWidget { @override Widget build(BuildContext context) { - final shotPublicKey = - "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>"; return ListTile( leading: CircleAvatar( backgroundColor: _getTypeColor(contact.type), child: _buildContactAvatar(contact), ), title: Text(contact.name), - subtitle: Text( - '${contact.typeLabel}\n${contact.shortPubKeyHex}', - ), + subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(contact.pathLabel), + Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)) + ],), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. trailing: MediaQuery( @@ -1039,21 +1038,15 @@ class _ContactTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Text(contact.pathLabel, - style: TextStyle(fontSize: 12, color: Colors.grey[600])), if (contact.hasLocation) Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), - Text( - _formatLastSeen(context, lastSeen), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), ), + onTap: onTap, + onLongPress: onLongPress, ); } From 34a6b5d895f46d5b6c02345beabdefd6cbb29bda Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Wed, 28 Jan 2026 19:55:08 -0800 Subject: [PATCH 041/421] Added error catching to requestBatteryStatus to call _handleDisconnection when it fails update. Updated ScannerScreen to manage navigation state logic on connection. --- lib/connector/meshcore_connector.dart | 8 +++++- lib/screens/scanner_screen.dart | 37 +++++++++++++++++++-------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index de39d7e..c378bff 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -959,7 +959,13 @@ class MeshCoreConnector extends ChangeNotifier { if (!isConnected) return; if (_batteryRequested && !force) return; _batteryRequested = true; - await sendFrame(buildGetBattAndStorageFrame()); + try { + await sendFrame(buildGetBattAndStorageFrame()); + } catch (e) { + // Reset flag on error to allow retry + _handleDisconnection(); + _batteryRequested = false; + } } void _startBatteryPolling() { diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 63f4a3c..642ce1f 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -8,9 +8,35 @@ import '../widgets/device_tile.dart'; import 'contacts_screen.dart'; /// Screen for scanning and connecting to MeshCore devices -class ScannerScreen extends StatelessWidget { +class ScannerScreen extends StatefulWidget { const ScannerScreen({super.key}); + @override + State createState() => _ScannerScreenState(); +} + +class _ScannerScreenState extends State { + bool changedNavgation = false; + + @override + void initState() { + super.initState(); + final connector = Provider.of(context, listen: false); + + connector.addListener(() { + if (connector.state == MeshCoreConnectionState.disconnected) { + changedNavgation = false; + }else if (connector.state == MeshCoreConnectionState.connected && !changedNavgation) { + changedNavgation = true; + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const ContactsScreen(), + ), + ); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -161,15 +187,6 @@ final l10n = context.l10n; ? result.device.platformName : result.advertisementData.advName; await connector.connect(result.device, displayName: name); - - if (context.mounted && connector.isConnected) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ContactsScreen(), - ), - ); - } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( From 92d2b224e75ad261e4b6e1149c718c32ddac51cf Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 28 Jan 2026 21:28:28 -0700 Subject: [PATCH 042/421] fix: address PR review issues - Fix memory leak by adding dispose() to remove connection listener - Fix typo: changedNavgation -> _changedNavigation - Add mounted check before navigation to prevent errors - Remove overly aggressive _handleDisconnection() call on battery request failure - Only reset battery flag on error to allow retry without disconnecting --- lib/connector/meshcore_connector.dart | 2 +- lib/screens/scanner_screen.dart | 34 ++++++++++++++++++--------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c378bff..191f74e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -963,7 +963,7 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildGetBattAndStorageFrame()); } catch (e) { // Reset flag on error to allow retry - _handleDisconnection(); + // Don't disconnect on battery request failure - it may be transient _batteryRequested = false; } } diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 642ce1f..0d38d98 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -16,25 +16,37 @@ class ScannerScreen extends StatefulWidget { } class _ScannerScreenState extends State { - bool changedNavgation = false; + bool _changedNavigation = false; + late final VoidCallback _connectionListener; @override void initState() { super.initState(); final connector = Provider.of(context, listen: false); - connector.addListener(() { + _connectionListener = () { if (connector.state == MeshCoreConnectionState.disconnected) { - changedNavgation = false; - }else if (connector.state == MeshCoreConnectionState.connected && !changedNavgation) { - changedNavgation = true; - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const ContactsScreen(), - ), - ); + _changedNavigation = false; + } else if (connector.state == MeshCoreConnectionState.connected && !_changedNavigation) { + _changedNavigation = true; + if (mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const ContactsScreen(), + ), + ); + } } - }); + }; + + connector.addListener(_connectionListener); + } + + @override + void dispose() { + final connector = Provider.of(context, listen: false); + connector.removeListener(_connectionListener); + super.dispose(); } @override From 998ff50495f2bb738588da6d760cd538bc3e2a3a Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 28 Jan 2026 21:34:13 -0700 Subject: [PATCH 043/421] fix: restore _handleDisconnection() on battery request failure This was the author's original intent - use battery request failure as a signal that the connection is lost. --- lib/connector/meshcore_connector.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 191f74e..6f22c5e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -962,9 +962,8 @@ class MeshCoreConnector extends ChangeNotifier { try { await sendFrame(buildGetBattAndStorageFrame()); } catch (e) { - // Reset flag on error to allow retry - // Don't disconnect on battery request failure - it may be transient - _batteryRequested = false; + // Connection likely lost - trigger disconnection handling + _handleDisconnection(); } } From 935b7b07ebe956cfe1a93ff92572dc2daf597156 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 28 Jan 2026 22:05:04 -0700 Subject: [PATCH 044/421] Add path trace localizations for all languages - Translate path trace strings to all 14 supported locales - Regenerate localization Dart files - Fix translate.py to also detect empty string values as missing --- lib/l10n/app_bg.arb | 21 ++++- lib/l10n/app_de.arb | 21 ++++- lib/l10n/app_es.arb | 21 ++++- lib/l10n/app_fr.arb | 21 ++++- lib/l10n/app_it.arb | 21 ++++- lib/l10n/app_localizations.dart | 72 +++++++++++++++ lib/l10n/app_localizations_bg.dart | 38 ++++++++ lib/l10n/app_localizations_de.dart | 38 ++++++++ lib/l10n/app_localizations_en.dart | 38 ++++++++ lib/l10n/app_localizations_es.dart | 39 ++++++++ lib/l10n/app_localizations_fr.dart | 39 ++++++++ lib/l10n/app_localizations_it.dart | 40 +++++++++ lib/l10n/app_localizations_nl.dart | 38 ++++++++ lib/l10n/app_localizations_pl.dart | 39 ++++++++ lib/l10n/app_localizations_pt.dart | 38 ++++++++ lib/l10n/app_localizations_ru.dart | 38 ++++++++ lib/l10n/app_localizations_sk.dart | 38 ++++++++ lib/l10n/app_localizations_sl.dart | 138 ++++++++++++++++++----------- lib/l10n/app_localizations_sv.dart | 38 ++++++++ lib/l10n/app_localizations_uk.dart | 38 ++++++++ lib/l10n/app_localizations_zh.dart | 38 ++++++++ lib/l10n/app_nl.arb | 21 ++++- lib/l10n/app_pl.arb | 21 ++++- lib/l10n/app_pt.arb | 21 ++++- lib/l10n/app_ru.arb | 21 ++++- lib/l10n/app_sk.arb | 21 ++++- lib/l10n/app_sl.arb | 21 ++++- lib/l10n/app_sv.arb | 21 ++++- lib/l10n/app_uk.arb | 23 ++++- lib/l10n/app_zh.arb | 21 ++++- tools/translate.py | 5 +- 31 files changed, 981 insertions(+), 67 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index e5f40f3..958d963 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1533,5 +1533,24 @@ "community_regenerate": "Регенерация", "community_updateSecret": "Актуализирай тайна", "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"", - "community_secretUpdated": "Секретно обновено за \"{name}\"" + "community_secretUpdated": "Секретно обновено за \"{name}\"", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Вие", + "pathTrace_notAvailable": "Пътека за проследяване не е достъпна.", + "contacts_pathTrace": "Пътен проследяване", + "pathTrace_refreshTooltip": "Обнови Path Trace.", + "pathTrace_failed": "Пътят за проследяване не успя.", + "contacts_repeaterPing": "Пингване на повторителя", + "contacts_repeaterPathTrace": "Трасировка до повторител", + "contacts_ping": "Пинг", + "contacts_chatTraceRoute": "Трасиране на път", + "contacts_roomPathTrace": "Трасиране на път до съ", + "contacts_roomPing": "Ping на сървъра на стаята", + "contacts_pathTraceTo": "Проследи маршрут към {name}" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0bb17c8..8e45b7c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1533,5 +1533,24 @@ "community_regenerateSecret": "Neu generieren Sie das Geheimnis", "community_secretUpdated": "Geheime für \"{name}\" aktualisiert", "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", - "community_updateSecret": "Aktualisieren Sie das Geheimnis" + "community_updateSecret": "Aktualisieren Sie das Geheimnis", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_refreshTooltip": "Path Trace aktualisieren.", + "pathTrace_you": "Du", + "pathTrace_failed": "Pfadverfolgung fehlgeschlagen.", + "pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.", + "contacts_pathTrace": "Pfadverfolgung", + "contacts_ping": "Pingen", + "contacts_repeaterPathTrace": "Pfadverfolgung zum Repeater", + "contacts_repeaterPing": "Repeater pingen", + "contacts_roomPathTrace": "Pfadverfolgung zum Raumserver", + "contacts_roomPing": "Raumserver anpingen", + "contacts_pathTraceTo": "Route nach {name} verfolgen", + "contacts_chatTraceRoute": "Pfadverfolgungsroute" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4b6b526..1cdfb7b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1533,5 +1533,24 @@ "community_regenerate": "Regenerar", "community_secretUpdated": "Confidencialidad actualizada para \"{name}\"", "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"", - "community_updateSecret": "Actualizar Contraseña" + "community_updateSecret": "Actualizar Contraseña", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Tú", + "pathTrace_failed": "El trazado de ruta falló.", + "pathTrace_refreshTooltip": "Actualizar Path Trace", + "contacts_pathTrace": "Rastreo de caminos", + "contacts_repeaterPathTrace": "Rastrear ruta al repetidor", + "contacts_repeaterPing": "Pingar repetidor", + "contacts_ping": "Ping", + "pathTrace_notAvailable": "El trazado de ruta no está disponible.", + "contacts_roomPing": "Pingar servidor de sala", + "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", + "contacts_pathTraceTo": "Rastrear ruta a {name}", + "contacts_chatTraceRoute": "Ruta de trazado" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1b69540..88c65d6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1533,5 +1533,24 @@ "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"", "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"", "community_updateSecret": "Mettre à jour le secret", - "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"" + "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Vous", + "pathTrace_refreshTooltip": "Actualiser Path Trace", + "pathTrace_failed": "Traçage du chemin échoué.", + "pathTrace_notAvailable": "Tracé de chemin non disponible.", + "contacts_pathTrace": "Traçage de chemin", + "contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur", + "contacts_repeaterPing": "Pinguer le répéteur", + "contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle", + "contacts_chatTraceRoute": "Tracer le chemin", + "contacts_pathTraceTo": "Tracer l'itinéraire vers {name}", + "contacts_ping": "Ping", + "contacts_roomPing": "Pinguer le serveur de la salle" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index cd031fb..acd440b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1533,5 +1533,24 @@ "community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"", "community_updateSecret": "Aggiorna Segreto", "community_secretUpdated": "Segreto aggiornato per \"{name}\"", - "community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"" + "community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_failed": "Tracciamento del percorso fallito.", + "pathTrace_you": "Tu", + "pathTrace_notAvailable": "Tracciamento del percorso non disponibile.", + "pathTrace_refreshTooltip": "Aggiorna Path Trace.", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Traccia percorso al ripetitore", + "contacts_roomPathTrace": "Traccia del percorso al server della stanza", + "contacts_pathTrace": "Traccia Percorso", + "contacts_repeaterPing": "Ripetitore ping", + "contacts_pathTraceTo": "Traccia percorso verso {name}", + "contacts_roomPing": "Ping al server della stanza", + "contacts_chatTraceRoute": "Traccia percorso path" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d40c791..ec047bd 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4687,6 +4687,78 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'New group'** String get listFilter_newGroup; + + /// No description provided for @pathTrace_you. + /// + /// In en, this message translates to: + /// **'You'** + String get pathTrace_you; + + /// No description provided for @pathTrace_failed. + /// + /// In en, this message translates to: + /// **'Path trace failed.'** + String get pathTrace_failed; + + /// No description provided for @pathTrace_notAvailable. + /// + /// In en, this message translates to: + /// **'Path trace not available.'** + String get pathTrace_notAvailable; + + /// No description provided for @pathTrace_refreshTooltip. + /// + /// In en, this message translates to: + /// **'Refresh Path Trace.'** + String get pathTrace_refreshTooltip; + + /// No description provided for @contacts_pathTrace. + /// + /// In en, this message translates to: + /// **'Path Trace'** + String get contacts_pathTrace; + + /// No description provided for @contacts_ping. + /// + /// In en, this message translates to: + /// **'Ping'** + String get contacts_ping; + + /// No description provided for @contacts_repeaterPathTrace. + /// + /// In en, this message translates to: + /// **'Path trace to repeater'** + String get contacts_repeaterPathTrace; + + /// No description provided for @contacts_repeaterPing. + /// + /// In en, this message translates to: + /// **'Ping repeater'** + String get contacts_repeaterPing; + + /// No description provided for @contacts_roomPathTrace. + /// + /// In en, this message translates to: + /// **'Path trace to room server'** + String get contacts_roomPathTrace; + + /// No description provided for @contacts_roomPing. + /// + /// In en, this message translates to: + /// **'Ping room server'** + String get contacts_roomPing; + + /// No description provided for @contacts_chatTraceRoute. + /// + /// In en, this message translates to: + /// **'Path trace route'** + String get contacts_chatTraceRoute; + + /// No description provided for @contacts_pathTraceTo. + /// + /// In en, this message translates to: + /// **'Trace route to {name}'** + String contacts_pathTraceTo(String name); } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 9b70d9c..520b00d 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2676,4 +2676,42 @@ class AppLocalizationsBg extends AppLocalizations { @override String get listFilter_newGroup => 'Нова група'; + + @override + String get pathTrace_you => 'Вие'; + + @override + String get pathTrace_failed => 'Пътят за проследяване не успя.'; + + @override + String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.'; + + @override + String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; + + @override + String get contacts_pathTrace => 'Пътен проследяване'; + + @override + String get contacts_ping => 'Пинг'; + + @override + String get contacts_repeaterPathTrace => 'Трасировка до повторител'; + + @override + String get contacts_repeaterPing => 'Пингване на повторителя'; + + @override + String get contacts_roomPathTrace => 'Трасиране на път до съ'; + + @override + String get contacts_roomPing => 'Ping на сървъра на стаята'; + + @override + String get contacts_chatTraceRoute => 'Трасиране на път'; + + @override + String contacts_pathTraceTo(String name) { + return 'Проследи маршрут към $name'; + } } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9bab237..e0ccd3d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2681,4 +2681,42 @@ class AppLocalizationsDe extends AppLocalizations { @override String get listFilter_newGroup => 'Neue Gruppe'; + + @override + String get pathTrace_you => 'Du'; + + @override + String get pathTrace_failed => 'Pfadverfolgung fehlgeschlagen.'; + + @override + String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.'; + + @override + String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.'; + + @override + String get contacts_pathTrace => 'Pfadverfolgung'; + + @override + String get contacts_ping => 'Pingen'; + + @override + String get contacts_repeaterPathTrace => 'Pfadverfolgung zum Repeater'; + + @override + String get contacts_repeaterPing => 'Repeater pingen'; + + @override + String get contacts_roomPathTrace => 'Pfadverfolgung zum Raumserver'; + + @override + String get contacts_roomPing => 'Raumserver anpingen'; + + @override + String get contacts_chatTraceRoute => 'Pfadverfolgungsroute'; + + @override + String contacts_pathTraceTo(String name) { + return 'Route nach $name verfolgen'; + } } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 86f18ba..9a1634f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2636,4 +2636,42 @@ class AppLocalizationsEn extends AppLocalizations { @override String get listFilter_newGroup => 'New group'; + + @override + String get pathTrace_you => 'You'; + + @override + String get pathTrace_failed => 'Path trace failed.'; + + @override + String get pathTrace_notAvailable => 'Path trace not available.'; + + @override + String get pathTrace_refreshTooltip => 'Refresh Path Trace.'; + + @override + String get contacts_pathTrace => 'Path Trace'; + + @override + String get contacts_ping => 'Ping'; + + @override + String get contacts_repeaterPathTrace => 'Path trace to repeater'; + + @override + String get contacts_repeaterPing => 'Ping repeater'; + + @override + String get contacts_roomPathTrace => 'Path trace to room server'; + + @override + String get contacts_roomPing => 'Ping room server'; + + @override + String get contacts_chatTraceRoute => 'Path trace route'; + + @override + String contacts_pathTraceTo(String name) { + return 'Trace route to $name'; + } } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 908c88c..7f2f489 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2675,4 +2675,43 @@ class AppLocalizationsEs extends AppLocalizations { @override String get listFilter_newGroup => 'Nuevo grupo'; + + @override + String get pathTrace_you => 'Tú'; + + @override + String get pathTrace_failed => 'El trazado de ruta falló.'; + + @override + String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.'; + + @override + String get pathTrace_refreshTooltip => 'Actualizar Path Trace'; + + @override + String get contacts_pathTrace => 'Rastreo de caminos'; + + @override + String get contacts_ping => 'Ping'; + + @override + String get contacts_repeaterPathTrace => 'Rastrear ruta al repetidor'; + + @override + String get contacts_repeaterPing => 'Pingar repetidor'; + + @override + String get contacts_roomPathTrace => + 'Rastreo de ruta al servidor de la habitación'; + + @override + String get contacts_roomPing => 'Pingar servidor de sala'; + + @override + String get contacts_chatTraceRoute => 'Ruta de trazado'; + + @override + String contacts_pathTraceTo(String name) { + return 'Rastrear ruta a $name'; + } } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 07ec4c8..fbc797f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2692,4 +2692,43 @@ class AppLocalizationsFr extends AppLocalizations { @override String get listFilter_newGroup => 'Nouveau groupe'; + + @override + String get pathTrace_you => 'Vous'; + + @override + String get pathTrace_failed => 'Traçage du chemin échoué.'; + + @override + String get pathTrace_notAvailable => 'Tracé de chemin non disponible.'; + + @override + String get pathTrace_refreshTooltip => 'Actualiser Path Trace'; + + @override + String get contacts_pathTrace => 'Traçage de chemin'; + + @override + String get contacts_ping => 'Ping'; + + @override + String get contacts_repeaterPathTrace => 'Tracer le chemin vers le répéteur'; + + @override + String get contacts_repeaterPing => 'Pinguer le répéteur'; + + @override + String get contacts_roomPathTrace => + 'Traçage du chemin vers le serveur de la salle'; + + @override + String get contacts_roomPing => 'Pinguer le serveur de la salle'; + + @override + String get contacts_chatTraceRoute => 'Tracer le chemin'; + + @override + String contacts_pathTraceTo(String name) { + return 'Tracer l\'itinéraire vers $name'; + } } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 83010d8..b5b100a 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2675,4 +2675,44 @@ class AppLocalizationsIt extends AppLocalizations { @override String get listFilter_newGroup => 'Nuovo gruppo'; + + @override + String get pathTrace_you => 'Tu'; + + @override + String get pathTrace_failed => 'Tracciamento del percorso fallito.'; + + @override + String get pathTrace_notAvailable => + 'Tracciamento del percorso non disponibile.'; + + @override + String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.'; + + @override + String get contacts_pathTrace => 'Traccia Percorso'; + + @override + String get contacts_ping => 'Ping'; + + @override + String get contacts_repeaterPathTrace => 'Traccia percorso al ripetitore'; + + @override + String get contacts_repeaterPing => 'Ripetitore ping'; + + @override + String get contacts_roomPathTrace => + 'Traccia del percorso al server della stanza'; + + @override + String get contacts_roomPing => 'Ping al server della stanza'; + + @override + String get contacts_chatTraceRoute => 'Traccia percorso path'; + + @override + String contacts_pathTraceTo(String name) { + return 'Traccia percorso verso $name'; + } } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ce60a8f..3ca198c 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2666,4 +2666,42 @@ class AppLocalizationsNl extends AppLocalizations { @override String get listFilter_newGroup => 'Nieuwe groep'; + + @override + String get pathTrace_you => 'Jij'; + + @override + String get pathTrace_failed => 'Padtrace mislukt.'; + + @override + String get pathTrace_notAvailable => 'Padtrace niet beschikbaar.'; + + @override + String get pathTrace_refreshTooltip => 'Path Trace vernieuwen.'; + + @override + String get contacts_pathTrace => 'Pad Traceren'; + + @override + String get contacts_ping => 'Pingen'; + + @override + String get contacts_repeaterPathTrace => 'Pad traceren naar repeater'; + + @override + String get contacts_repeaterPing => 'Ping repeater'; + + @override + String get contacts_roomPathTrace => 'Padtrace naar room server'; + + @override + String get contacts_roomPing => 'Ping kamer server'; + + @override + String get contacts_chatTraceRoute => 'Route traceren'; + + @override + String contacts_pathTraceTo(String name) { + return 'Trace route to $name'; + } } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 13fbeb0..491f76d 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2674,4 +2674,43 @@ class AppLocalizationsPl extends AppLocalizations { @override String get listFilter_newGroup => 'Nowa grupa'; + + @override + String get pathTrace_you => 'Ty'; + + @override + String get pathTrace_failed => 'Śledzenie ścieżki nie powiodło się.'; + + @override + String get pathTrace_notAvailable => 'Ścieżka śledzenia niedostępna.'; + + @override + String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.'; + + @override + String get contacts_pathTrace => 'Śledzenie Ścieżek'; + + @override + String get contacts_ping => 'Pingować'; + + @override + String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera'; + + @override + String get contacts_repeaterPing => 'Repeater pingowy'; + + @override + String get contacts_roomPathTrace => + 'Śledzenie ścieżki do serwera pokojowego'; + + @override + String get contacts_roomPing => 'Pinguj serwer pokoju'; + + @override + String get contacts_chatTraceRoute => 'Śledź trasę promienia'; + + @override + String contacts_pathTraceTo(String name) { + return 'Śledź trasę do $name'; + } } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 3f54001..f88a497 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2677,4 +2677,42 @@ class AppLocalizationsPt extends AppLocalizations { @override String get listFilter_newGroup => 'Novo grupo'; + + @override + String get pathTrace_you => 'Você'; + + @override + String get pathTrace_failed => 'Falha no rastreamento de caminho.'; + + @override + String get pathTrace_notAvailable => 'Traçado de caminho não disponível.'; + + @override + String get pathTrace_refreshTooltip => 'Atualizar Path Trace.'; + + @override + String get contacts_pathTrace => 'Traçado de Caminho'; + + @override + String get contacts_ping => 'Pingar'; + + @override + String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor'; + + @override + String get contacts_repeaterPing => 'Pingar repetidor'; + + @override + String get contacts_roomPathTrace => 'Traçar caminho para o servidor da sala'; + + @override + String get contacts_roomPing => 'Pingar servidor da sala'; + + @override + String get contacts_chatTraceRoute => 'Rastrear rota do caminho'; + + @override + String contacts_pathTraceTo(String name) { + return 'Rastrear rota para $name'; + } } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ae784e4..72da35c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2679,4 +2679,42 @@ class AppLocalizationsRu extends AppLocalizations { @override String get listFilter_newGroup => 'Новая группа'; + + @override + String get pathTrace_you => 'Вы'; + + @override + String get pathTrace_failed => 'Путь трассировки не выполнен.'; + + @override + String get pathTrace_notAvailable => 'Трассировка пути недоступна.'; + + @override + String get pathTrace_refreshTooltip => 'Обновить Path Trace'; + + @override + String get contacts_pathTrace => 'Трассировка пути'; + + @override + String get contacts_ping => 'Пинговать'; + + @override + String get contacts_repeaterPathTrace => 'Отследить путь к ретранслятору'; + + @override + String get contacts_repeaterPing => 'Пинговать повторитель'; + + @override + String get contacts_roomPathTrace => 'Трассировка пути к серверу комнаты'; + + @override + String get contacts_roomPing => 'Пинговать сервер комнаты'; + + @override + String get contacts_chatTraceRoute => 'Трассировка маршрута'; + + @override + String contacts_pathTraceTo(String name) { + return 'Показать маршрут к $name'; + } } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 75d4654..23e3f1a 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2662,4 +2662,42 @@ class AppLocalizationsSk extends AppLocalizations { @override String get listFilter_newGroup => 'Nová skupina'; + + @override + String get pathTrace_you => 'Vy'; + + @override + String get pathTrace_failed => 'Sledovanie cesty zlyhalo.'; + + @override + String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.'; + + @override + String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.'; + + @override + String get contacts_pathTrace => 'Sledovanie lúčov'; + + @override + String get contacts_ping => 'Pingovať'; + + @override + String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču'; + + @override + String get contacts_repeaterPing => 'Pingovať opakovač'; + + @override + String get contacts_roomPathTrace => 'Sledovanie cesty k serveru miestnosti'; + + @override + String get contacts_roomPing => 'Ping server miestnosti'; + + @override + String get contacts_chatTraceRoute => 'Sledovať trasu lúča'; + + @override + String contacts_pathTraceTo(String name) { + return 'Sledovať trasu k $name'; + } } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 38bfe3d..4ad59e8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -30,13 +30,13 @@ class AppLocalizationsSl extends AppLocalizations { String get common_connect => 'Poveži se'; @override - String get common_unknownDevice => 'Nepoznane naprave'; + String get common_unknownDevice => 'Nepoznano naprave'; @override String get common_save => 'Shrani'; @override - String get common_delete => 'Izbriši'; + String get common_delete => 'Izbrisati'; @override String get common_close => 'Zapri'; @@ -51,7 +51,7 @@ class AppLocalizationsSl extends AppLocalizations { String get common_settings => 'Nastavitve'; @override - String get common_disconnect => 'Odklopi'; + String get common_disconnect => 'Odklopiti'; @override String get common_connected => 'Povezano'; @@ -66,31 +66,31 @@ class AppLocalizationsSl extends AppLocalizations { String get common_continue => 'Poudarki'; @override - String get common_share => 'Deli'; + String get common_share => 'Deliti'; @override String get common_copy => 'Kopiraj'; @override - String get common_retry => 'Ponovi'; + String get common_retry => 'Ponoviti'; @override String get common_hide => 'Skrita'; @override - String get common_remove => 'Izbriši'; + String get common_remove => 'Izbrisati'; @override String get common_enable => 'Omogoči'; @override - String get common_disable => 'Izklopi'; + String get common_disable => 'Izklopiti'; @override - String get common_reboot => 'Ponovno zaženi'; + String get common_reboot => 'Ponoviti'; @override - String get common_loading => 'Nalaganje...'; + String get common_loading => 'Naložanje...'; @override String get common_notAvailable => '—'; @@ -109,7 +109,7 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_title => 'MeshCore Open'; @override - String get scanner_scanning => 'Iščem naprave...'; + String get scanner_scanning => 'Skeniram za naprave...'; @override String get scanner_connecting => 'Povezujem se...'; @@ -118,7 +118,7 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_disconnecting => 'Odklapljam se...'; @override - String get scanner_notConnected => 'Ni povezave'; + String get scanner_notConnected => 'Nezavezan'; @override String scanner_connectedTo(String deviceName) { @@ -134,7 +134,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Napaka pri povezavi: $error'; + return 'Pošlo je z povezavo: $error'; } @override @@ -144,7 +144,7 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_scan => 'Skeniraj'; @override - String get device_quickSwitch => 'Hitri preklop'; + String get device_quickSwitch => 'Hitro preklop'; @override String get device_meshcore => 'MeshCore'; @@ -153,7 +153,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_title => 'Nastavitve'; @override - String get settings_deviceInfo => 'Informacije o napravi'; + String get settings_deviceInfo => 'Informacije o napravei'; @override String get settings_appSettings => 'Nastavitve aplikacije'; @@ -208,14 +208,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Omogoči samodejno posodabljanje lokacije z GPS-om.'; + 'Omogoči samodejno posodabljanje lokacije z GPS-jem.'; @override - String get settings_locationIntervalSec => 'Interval za GPS (sekunde)'; + String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)'; @override String get settings_locationIntervalInvalid => - 'Interval mora biti med 60 in 86400 sekund.'; + 'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.'; @override String get settings_latitude => 'Širina'; @@ -243,7 +243,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_actions => 'Akcije'; @override - String get settings_sendAdvertisement => 'Pošlji oglas'; + String get settings_sendAdvertisement => 'Pošlji Oglas'; @override String get settings_sendAdvertisementSubtitle => @@ -262,7 +262,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_timeSynchronized => 'Ura sinhronizirana'; @override - String get settings_refreshContacts => 'Osveži stike'; + String get settings_refreshContacts => 'Ponovno obišči kontakte'; @override String get settings_refreshContactsSubtitle => @@ -272,8 +272,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_rebootDevice => 'Ponovni zagon naprave'; @override - String get settings_rebootDeviceSubtitle => - 'Ponovno zaženi MeshCore napravo'; + String get settings_rebootDeviceSubtitle => 'Ponovno zaženi MeshCore napravo'; @override String get settings_rebootDeviceConfirm => @@ -296,7 +295,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_appDebugLogSubtitle => 'Debug sporočila aplikacije'; @override - String get settings_about => 'O aplikaciji'; + String get settings_about => 'Oglejte si'; @override String settings_aboutVersion(String version) { @@ -359,7 +358,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_spreadingFactor => 'Razširitveni faktor'; @override - String get settings_codingRate => 'Programska hitrost (CR)'; + String get settings_codingRate => 'Programska hitrost'; @override String get settings_txPower => 'TX Moč (dBm)'; @@ -371,7 +370,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; @override - String get settings_longRange => 'Dolg doseg'; + String get settings_longRange => 'DDolg doseg'; @override String get settings_fastSpeed => 'Visoka hitrost'; @@ -506,7 +505,8 @@ class AppLocalizationsSl extends AppLocalizations { 'Poti ne bodo samodejno čiščene.'; @override - String get appSettings_autoRouteRotation => 'Avtomatsko rotacija prenosne poti'; + String get appSettings_autoRouteRotation => + 'Avtomatsko rotacija prenosne poti'; @override String get appSettings_autoRouteRotationSubtitle => @@ -551,8 +551,7 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_showRepeaters => 'Prikaži repetitorje'; @override - String get appSettings_showRepeatersSubtitle => - 'Prikaži repetitorje na mapi'; + String get appSettings_showRepeatersSubtitle => 'Prikaži repetitorje na mapi'; @override String get appSettings_showChatNodes => 'Prikaži naprave za klepet'; @@ -638,7 +637,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_contactsWillAppear => - 'Stiki se bodo prikazali takoj, ko se naprave oglasijo.'; + 'Stiki se bodo prikazali, ko se naprave oglasijo.'; @override String get contacts_searchContacts => 'Iskanje stikov...'; @@ -647,8 +646,7 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_noUnreadContacts => 'Ne prebrani stiki.'; @override - String get contacts_noContactsFound => - 'Stiki niso najdeni.'; + String get contacts_noContactsFound => 'Stiki niso najdeni.'; @override String get contacts_deleteContact => 'Izbriši stik'; @@ -659,10 +657,10 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Upravljanje repetitorjev'; + String get contacts_manageRepeater => 'Upravljaj Ponovitve'; @override - String get contacts_manageRoom => 'Upravljanje strežniške sobe'; + String get contacts_manageRoom => 'Upravljajte strežnik sobe'; @override String get contacts_roomLogin => 'Prijava v sobo'; @@ -862,20 +860,20 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channels_joinPublicChannelDesc => - 'Kdorkoli se lahko pridruži tej skupini.'; + 'Kdor karkoli je, lahko se pridruži tej skupini.'; @override - String get channels_joinHashtagChannel => 'Pridružite se kanalu s hashtagom'; + String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom'; @override String get channels_joinHashtagChannelDesc => - 'Kdorkoli se lahko pridruži hashtag kanalom.'; + 'Kdor karkoli, lahko se pridruži hashtag kanalom.'; @override String get channels_scanQrCode => 'Skeniraj QR kodo'; @override - String get channels_scanQrCodeComingSoon => 'Prihaja kmalu'; + String get channels_scanQrCodeComingSoon => 'Prihajajoča'; @override String get channels_enterHashtag => 'Vnesite hashtag'; @@ -895,7 +893,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String chat_replyingTo(String name) { - return 'Odgovori $name'; + return 'Odgovarjanje $name'; } @override @@ -930,7 +928,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String chat_retryCount(int current, int max) { - return 'Ponovitev $current/$max'; + return 'Ponovit $current/$max'; } @override @@ -997,7 +995,7 @@ class AppLocalizationsSl extends AppLocalizations { String get debugLog_bleCopied => 'Kopirana beležka iz BLE'; @override - String get debugLog_noEntries => 'Ni debug zapisov.'; + String get debugLog_noEntries => 'Ni ustvarjenih debug zapisov.'; @override String get debugLog_enableInSettings => @@ -1019,7 +1017,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String debugFrame_command(String value) { - return 'Ukaz: 0x$value'; + return 'Navodilo: 0x$value'; } @override @@ -1152,7 +1150,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_pathSavedLocally => - 'Shranjeno lokalno. Povežite se za sinhronizacijo.'; + 'Shrano lokalno. Povežite se za sinhronizacijo.'; @override String get chat_pathDeviceConfirmed => 'Naprave potrjeno.'; @@ -2561,7 +2559,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_deleteConfirm(String name) { - return 'Zapustiti \"$name\"?'; + return 'Zapusti \"$name\"?'; } @override @@ -2575,7 +2573,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get community_regenerateSecret => 'Preberi nov tajni kôd'; + String get community_regenerateSecret => 'Ponovno ustvari geslo'; @override String community_regenerateSecretConfirm(String name) { @@ -2587,11 +2585,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Tajna za \"$name\" ponovno ustvarjena'; + return 'Geslo za \"$name\" ponovno ustvarjeno'; } @override - String get community_updateSecret => 'Ažurniraj tajno'; + String get community_updateSecret => 'Ažuriraj ključ'; @override String community_secretUpdated(String name) { @@ -2600,7 +2598,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Skeniraj nov kôd QR za posodabljanje tajne za $name'; + return 'Skeniraj novo QR kodo za posodabljanje ključa za $name'; } @override @@ -2618,7 +2616,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get community_regularHashtagDesc => - 'javna oznaka (kdorkoli lahko sodelujeje)'; + 'javna oznaka (kdorkoli lahko sodeluje)'; @override String get community_communityHashtag => 'Skupnostni hashtag'; @@ -2633,7 +2631,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get listFilter_tooltip => 'Filtri in sortiranje'; + String get listFilter_tooltip => 'Filtri in vrstiči'; @override String get listFilter_sortBy => 'Sortiraj po'; @@ -2657,14 +2655,52 @@ class AppLocalizationsSl extends AppLocalizations { String get listFilter_users => 'Uporabniki'; @override - String get listFilter_repeaters => 'Samo repetirorji'; + String get listFilter_repeaters => 'Ponovitve'; @override - String get listFilter_roomServers => 'Samo room serverji'; + String get listFilter_roomServers => 'Smeti za prostore'; @override - String get listFilter_unreadOnly => 'Samo neprebrani'; + String get listFilter_unreadOnly => 'Nezbrani samo'; @override String get listFilter_newGroup => 'Nova skupina'; + + @override + String get pathTrace_you => 'Ti'; + + @override + String get pathTrace_failed => 'Sledenje poti ni uspelo.'; + + @override + String get pathTrace_notAvailable => 'Potni sled ni na voljo.'; + + @override + String get pathTrace_refreshTooltip => 'Osveži Path Trace.'; + + @override + String get contacts_pathTrace => 'Sledenje poti'; + + @override + String get contacts_ping => 'Pingati'; + + @override + String get contacts_repeaterPathTrace => 'Sledi poti do ponavljalnika'; + + @override + String get contacts_repeaterPing => 'Pinguj ponavljalnik'; + + @override + String get contacts_roomPathTrace => 'Sledenje poti do strežnika sobe'; + + @override + String get contacts_roomPing => 'Ping strežnik sobe'; + + @override + String get contacts_chatTraceRoute => 'Slediti poti žarkov'; + + @override + String contacts_pathTraceTo(String name) { + return 'Trace route to $name'; + } } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 34b54b4..885d7d6 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2650,4 +2650,42 @@ class AppLocalizationsSv extends AppLocalizations { @override String get listFilter_newGroup => 'Ny grupp'; + + @override + String get pathTrace_you => 'Du'; + + @override + String get pathTrace_failed => 'Sökvägsföljning misslyckades.'; + + @override + String get pathTrace_notAvailable => 'Path trace ej tillgänglig.'; + + @override + String get pathTrace_refreshTooltip => 'Uppdatera Path Trace'; + + @override + String get contacts_pathTrace => 'Path Trace'; + + @override + String get contacts_ping => 'Ping'; + + @override + String get contacts_repeaterPathTrace => 'Vägspårning till repeater'; + + @override + String get contacts_repeaterPing => 'Ping-repeater'; + + @override + String get contacts_roomPathTrace => 'Vägspårning till rumserver'; + + @override + String get contacts_roomPing => 'Ping rumsserver'; + + @override + String get contacts_chatTraceRoute => 'Spåra rutt'; + + @override + String contacts_pathTraceTo(String name) { + return 'Spåra rutt till $name'; + } } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index bc431ea..9f223da 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2686,4 +2686,42 @@ class AppLocalizationsUk extends AppLocalizations { @override String get listFilter_newGroup => 'Нова група'; + + @override + String get pathTrace_you => 'Ви'; + + @override + String get pathTrace_failed => 'Відстеження шляху не вдалося.'; + + @override + String get pathTrace_notAvailable => 'Трасування шляху недоступне.'; + + @override + String get pathTrace_refreshTooltip => 'Оновити Path Trace'; + + @override + String get contacts_pathTrace => 'Трасування шляхів'; + + @override + String get contacts_ping => 'Пінгувати'; + + @override + String get contacts_repeaterPathTrace => 'Трасування шляху до повторювача'; + + @override + String get contacts_repeaterPing => 'Пінгувати повторювач'; + + @override + String get contacts_roomPathTrace => 'Трасування шляху до серверу кімнати'; + + @override + String get contacts_roomPing => 'Пінг сервера кімнати'; + + @override + String get contacts_chatTraceRoute => 'Трасування шляху'; + + @override + String contacts_pathTraceTo(String name) { + return 'Відстежити маршрут до $name'; + } } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index cd9c3be..fc8d78b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2531,4 +2531,42 @@ class AppLocalizationsZh extends AppLocalizations { @override String get listFilter_newGroup => '新组'; + + @override + String get pathTrace_you => '你'; + + @override + String get pathTrace_failed => '路径追踪失败。'; + + @override + String get pathTrace_notAvailable => '路径追踪不可用'; + + @override + String get pathTrace_refreshTooltip => '刷新路径追踪'; + + @override + String get contacts_pathTrace => '路径追踪'; + + @override + String get contacts_ping => 'ping'; + + @override + String get contacts_repeaterPathTrace => '路径追踪到中继器'; + + @override + String get contacts_repeaterPing => 'Ping 中继器'; + + @override + String get contacts_roomPathTrace => '路径追踪至房间服务器'; + + @override + String get contacts_roomPing => 'Ping 房间服务器'; + + @override + String get contacts_chatTraceRoute => '路径追踪'; + + @override + String contacts_pathTraceTo(String name) { + return '追踪路由到 $name'; + } } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 48ef3dd..b28d668 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1533,5 +1533,24 @@ "community_regenerate": "Regeneer", "community_updateSecret": "Bijwerken Geheime", "community_secretUpdated": "Geheim gewijzigd voor \"{name}\"", - "community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken" + "community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Jij", + "pathTrace_failed": "Padtrace mislukt.", + "pathTrace_notAvailable": "Padtrace niet beschikbaar.", + "pathTrace_refreshTooltip": "Path Trace vernieuwen.", + "contacts_pathTrace": "Pad Traceren", + "contacts_ping": "Pingen", + "contacts_repeaterPathTrace": "Pad traceren naar repeater", + "contacts_repeaterPing": "Ping repeater", + "contacts_roomPathTrace": "Padtrace naar room server", + "contacts_roomPing": "Ping kamer server", + "contacts_chatTraceRoute": "Route traceren", + "contacts_pathTraceTo": "Trace route to {name}" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 823bba1..8070ac3 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1533,5 +1533,24 @@ "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.", "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"", "community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"", - "community_updateSecret": "Zaktualizuj tajny klucz" + "community_updateSecret": "Zaktualizuj tajny klucz", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Ty", + "pathTrace_failed": "Śledzenie ścieżki nie powiodło się.", + "pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.", + "contacts_pathTrace": "Śledzenie Ścieżek", + "contacts_ping": "Pingować", + "contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera", + "contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego", + "contacts_roomPing": "Pinguj serwer pokoju", + "pathTrace_refreshTooltip": "Odśwież ścieżkę.", + "contacts_repeaterPing": "Repeater pingowy", + "contacts_pathTraceTo": "Śledź trasę do {name}", + "contacts_chatTraceRoute": "Śledź trasę promienia" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index b48db37..6994bea 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1533,5 +1533,24 @@ "community_regenerate": "Regenerar", "community_secretUpdated": "Segredo atualizado para \"{name}\"", "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++", - "community_updateSecret": "Atualizar Segredo" + "community_updateSecret": "Atualizar Segredo", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Você", + "pathTrace_failed": "Falha no rastreamento de caminho.", + "pathTrace_notAvailable": "Traçado de caminho não disponível.", + "pathTrace_refreshTooltip": "Atualizar Path Trace.", + "contacts_pathTrace": "Traçado de Caminho", + "contacts_ping": "Pingar", + "contacts_repeaterPathTrace": "Traçar caminho para repetidor", + "contacts_repeaterPing": "Pingar repetidor", + "contacts_roomPathTrace": "Traçar caminho para o servidor da sala", + "contacts_roomPing": "Pingar servidor da sala", + "contacts_chatTraceRoute": "Rastrear rota do caminho", + "contacts_pathTraceTo": "Rastrear rota para {name}" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index e0c2cbe..f007aa7 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -774,5 +774,24 @@ "chat_openLink": "Открыть ссылку?", "chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?", "neighbors_heardAgo": "Слушал(а): {time} назад", - "chat_invalidLink": "Неправильный формат ссылки" + "chat_invalidLink": "Неправильный формат ссылки", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Вы", + "pathTrace_failed": "Путь трассировки не выполнен.", + "pathTrace_notAvailable": "Трассировка пути недоступна.", + "pathTrace_refreshTooltip": "Обновить Path Trace", + "contacts_pathTrace": "Трассировка пути", + "contacts_ping": "Пинговать", + "contacts_repeaterPathTrace": "Отследить путь к ретранслятору", + "contacts_repeaterPing": "Пинговать повторитель", + "contacts_roomPathTrace": "Трассировка пути к серверу комнаты", + "contacts_roomPing": "Пинговать сервер комнаты", + "contacts_chatTraceRoute": "Трассировка маршрута", + "contacts_pathTraceTo": "Показать маршрут к {name}" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 71871d1..4e66af0 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1533,5 +1533,24 @@ "community_regenerateSecret": "Zobraziť nový tajný kód", "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"", "community_updateSecret": "Aktualizovať tajné heslo", - "community_secretUpdated": "Zmena tajnej slova pre \"{name}\"" + "community_secretUpdated": "Zmena tajnej slova pre \"{name}\"", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Vy", + "pathTrace_failed": "Sledovanie cesty zlyhalo.", + "pathTrace_notAvailable": "Path trace nie je k dispozícii.", + "pathTrace_refreshTooltip": "Obnoviť Path Trace.", + "contacts_pathTrace": "Sledovanie lúčov", + "contacts_ping": "Pingovať", + "contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču", + "contacts_repeaterPing": "Pingovať opakovač", + "contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti", + "contacts_roomPing": "Ping server miestnosti", + "contacts_chatTraceRoute": "Sledovať trasu lúča", + "contacts_pathTraceTo": "Sledovať trasu k {name}" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 346cdaa..805621b 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1533,5 +1533,24 @@ "community_regenerate": "Preberi znova", "community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}", "community_updateSecret": "Ažuriraj ključ", - "community_secretUpdated": "Skrivnostno spremembo za \"{name}\"" + "community_secretUpdated": "Skrivnostno spremembo za \"{name}\"", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Ti", + "pathTrace_failed": "Sledenje poti ni uspelo.", + "pathTrace_notAvailable": "Potni sled ni na voljo.", + "pathTrace_refreshTooltip": "Osveži Path Trace.", + "contacts_pathTrace": "Sledenje poti", + "contacts_ping": "Pingati", + "contacts_repeaterPathTrace": "Sledi poti do ponavljalnika", + "contacts_repeaterPing": "Pinguj ponavljalnik", + "contacts_roomPathTrace": "Sledenje poti do strežnika sobe", + "contacts_roomPing": "Ping strežnik sobe", + "contacts_chatTraceRoute": "Slediti poti žarkov", + "contacts_pathTraceTo": "Trace route to {name}" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index f1da7c8..da017be 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1533,5 +1533,24 @@ "community_regenerateSecret": "Regenerera hemlig kod", "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"", "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"", - "community_updateSecret": "Uppdatera hemlighet" + "community_updateSecret": "Uppdatera hemlighet", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Du", + "pathTrace_failed": "Sökvägsföljning misslyckades.", + "pathTrace_notAvailable": "Path trace ej tillgänglig.", + "pathTrace_refreshTooltip": "Uppdatera Path Trace", + "contacts_pathTrace": "Path Trace", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Vägspårning till repeater", + "contacts_repeaterPing": "Ping-repeater", + "contacts_roomPathTrace": "Vägspårning till rumserver", + "contacts_roomPing": "Ping rumsserver", + "contacts_chatTraceRoute": "Spåra rutt", + "contacts_pathTraceTo": "Spåra rutt till {name}" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 492805e..85ce4a2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1534,5 +1534,24 @@ "community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", "community_updateSecret": "Оновити секрет", - "community_secretUpdated": "Зміну секрету для «{name}» оновлено" -} \ No newline at end of file + "community_secretUpdated": "Зміну секрету для «{name}» оновлено", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "Ви", + "pathTrace_failed": "Відстеження шляху не вдалося.", + "pathTrace_notAvailable": "Трасування шляху недоступне.", + "pathTrace_refreshTooltip": "Оновити Path Trace", + "contacts_pathTrace": "Трасування шляхів", + "contacts_ping": "Пінгувати", + "contacts_repeaterPathTrace": "Трасування шляху до повторювача", + "contacts_repeaterPing": "Пінгувати повторювач", + "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", + "contacts_roomPing": "Пінг сервера кімнати", + "contacts_chatTraceRoute": "Трасування шляху", + "contacts_pathTraceTo": "Відстежити маршрут до {name}" +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index ae10f60..5f0c797 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1533,5 +1533,24 @@ "community_regenerateSecretConfirm": "重新生成“{name}”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。", "community_scanToUpdateSecret": "扫描新的二维码更新\"{name}\"的密码", "community_updateSecret": "更新密钥", - "community_secretUpdated": "密码已更新为“{name}”" + "community_secretUpdated": "密码已更新为“{name}”", + "@contacts_pathTraceTo": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "pathTrace_you": "你", + "pathTrace_failed": "路径追踪失败。", + "pathTrace_notAvailable": "路径追踪不可用", + "pathTrace_refreshTooltip": "刷新路径追踪", + "contacts_pathTrace": "路径追踪", + "contacts_ping": "ping", + "contacts_repeaterPathTrace": "路径追踪到中继器", + "contacts_repeaterPing": "Ping 中继器", + "contacts_roomPathTrace": "路径追踪至房间服务器", + "contacts_roomPing": "Ping 房间服务器", + "contacts_chatTraceRoute": "路径追踪", + "contacts_pathTraceTo": "追踪路由到 {name}" } diff --git a/tools/translate.py b/tools/translate.py index 06a95f2..84d172a 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -466,7 +466,7 @@ def fmt_duration(seconds: float) -> str: def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]: - """Find keys that are in source but not in target (excluding metadata keys).""" + """Find keys that are in source but not in target, or have empty values (excluding metadata keys).""" missing = [] for key in source_data: if key == "@@locale": @@ -475,6 +475,9 @@ def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) continue if key not in target_data: missing.append(key) + elif isinstance(target_data.get(key), str) and target_data[key].strip() == "": + # Also include keys with empty string values + missing.append(key) return missing From d61ec217fc4aa83c9878b1a02687b648aeb74fdd Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 28 Jan 2026 22:26:14 -0700 Subject: [PATCH 045/421] feat: add Russian and Ukrainian to language selector These languages had translation files but were missing from the settings UI. Adds appSettings_languageRu and appSettings_languageUk strings and corresponding RadioListTile entries. Fixes missing languages in app settings. --- lib/l10n/app_en.arb | 2 + lib/l10n/app_localizations.dart | 12 +++++ lib/l10n/app_localizations_bg.dart | 6 +++ lib/l10n/app_localizations_de.dart | 6 +++ lib/l10n/app_localizations_en.dart | 6 +++ lib/l10n/app_localizations_es.dart | 6 +++ lib/l10n/app_localizations_fr.dart | 6 +++ lib/l10n/app_localizations_it.dart | 6 +++ lib/l10n/app_localizations_nl.dart | 6 +++ lib/l10n/app_localizations_pl.dart | 6 +++ lib/l10n/app_localizations_pt.dart | 6 +++ lib/l10n/app_localizations_ru.dart | 6 +++ lib/l10n/app_localizations_sk.dart | 6 +++ lib/l10n/app_localizations_sl.dart | 6 +++ lib/l10n/app_localizations_sv.dart | 6 +++ lib/l10n/app_localizations_uk.dart | 6 +++ lib/l10n/app_localizations_zh.dart | 6 +++ lib/screens/app_settings_screen.dart | 12 +++++ untranslated.json | 70 +++++++++++++++++++++++++++- 19 files changed, 185 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d191370..cb7b95e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -174,6 +174,8 @@ "appSettings_languageNl": "Nederlands", "appSettings_languageSk": "Slovenčina", "appSettings_languageBg": "Български", + "appSettings_languageRu": "Русский", + "appSettings_languageUk": "Українська", "appSettings_notifications": "Notifications", "appSettings_enableNotifications": "Enable Notifications", "appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ec047bd..ac3eb99 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -946,6 +946,18 @@ abstract class AppLocalizations { /// **'Български'** String get appSettings_languageBg; + /// No description provided for @appSettings_languageRu. + /// + /// In en, this message translates to: + /// **'Русский'** + String get appSettings_languageRu; + + /// No description provided for @appSettings_languageUk. + /// + /// In en, this message translates to: + /// **'Українська'** + String get appSettings_languageUk; + /// No description provided for @appSettings_notifications. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 520b00d..27b2007 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -450,6 +450,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 905792f..69e6a59 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -444,6 +444,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Benachrichtigungen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9a1634f..a609dd8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -442,6 +442,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 7f2f489..28d3e9d 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -447,6 +447,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Notificaciones'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index fbc797f..ce6f6a9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -448,6 +448,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b5b100a..a7ac6a6 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -446,6 +446,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Notifiche'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3ca198c..b55dc41 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -444,6 +444,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Notificaties'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 491f76d..0f7a704 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -448,6 +448,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Powiadomienia'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index f88a497..5c25276 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -448,6 +448,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Notificações'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 72da35c..a944fab 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -446,6 +446,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_languageBg => 'Болгарский'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 23e3f1a..02f2b62 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -444,6 +444,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Upozornenia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 4ad59e8..21d7b6f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -443,6 +443,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Obvestila'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 885d7d6..a96d7dc 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -441,6 +441,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Meddelanden'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 9f223da..6107c5b 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -446,6 +446,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => 'Сповіщення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index fc8d78b..c10a745 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -432,6 +432,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_languageBg => 'Български'; + @override + String get appSettings_languageRu => 'Русский'; + + @override + String get appSettings_languageUk => 'Українська'; + @override String get appSettings_notifications => '通知'; diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 377c39a..ce61231 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -471,6 +471,10 @@ class AppSettingsScreen extends StatelessWidget { return context.l10n.appSettings_languageSk; case 'bg': return context.l10n.appSettings_languageBg; + case 'ru': + return context.l10n.appSettings_languageRu; + case 'uk': + return context.l10n.appSettings_languageUk; default: return context.l10n.appSettings_languageSystem; } @@ -547,6 +551,14 @@ class AppSettingsScreen extends StatelessWidget { title: Text(context.l10n.appSettings_languageBg), value: 'bg', ), + RadioListTile( + title: Text(context.l10n.appSettings_languageRu), + value: 'ru', + ), + RadioListTile( + title: Text(context.l10n.appSettings_languageUk), + value: 'uk', + ), ], ), ), diff --git a/untranslated.json b/untranslated.json index 9e26dfe..b9dadf3 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,69 @@ -{} \ No newline at end of file +{ + "bg": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "de": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "es": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "fr": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "it": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "nl": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "pl": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "pt": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "ru": [ + "appSettings_languageUk" + ], + + "sk": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "sl": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "sv": [ + "appSettings_languageRu", + "appSettings_languageUk" + ], + + "uk": [ + "appSettings_languageRu" + ], + + "zh": [ + "appSettings_languageRu", + "appSettings_languageUk" + ] +} From 90ce46392a04ebf04c237e03916727f35b6e975f Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 28 Jan 2026 23:21:04 -0700 Subject: [PATCH 046/421] feat: optimize reaction message format to reduce airtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce reaction payload from ~44 bytes to 9 bytes (5x smaller) - Use 4-char hex hash (timestamp + sender + first 5 chars) for message ID - Use 2-char hex emoji index instead of multi-byte UTF-8 emoji - Format: r:HASH:INDEX (e.g., r:a1b2:00) - For 1:1 chats, sender is implicit (null) for shorter hash - Prevent users from reacting to their own messages - Add room server reaction support with sender identification - Make emoji lists public in EmojiPicker for shared indexing - Add 💪 and 🚀 emojis to picker - Add comprehensive unit tests for reaction helpers - Update minor dependencies --- lib/connector/meshcore_connector.dart | 112 +++-- lib/helpers/reaction_helper.dart | 85 ++-- lib/screens/channel_chat_screen.dart | 27 +- lib/screens/chat_screen.dart | 39 +- lib/widgets/emoji_picker.dart | 20 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 80 +++- test/reaction_helper_test.dart | 404 ++++++++++++++++++ 8 files changed, 639 insertions(+), 130 deletions(-) create mode 100644 test/reaction_helper_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 6f22c5e..1bab130 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -67,9 +67,9 @@ class MeshCoreConnector extends ChangeNotifier { final Map> _channelMessages = {}; final Set _loadedConversationKeys = {}; final Map> _processedChannelReactions = - {}; // channelIndex -> Set of "reactionKey_emoji" + {}; // channelIndex -> Set of "targetHash_emoji" final Map> _processedContactReactions = - {}; // contactPubKeyHex -> Set of "reactionKey_emoji" + {}; // contactPubKeyHex -> Set of "targetHash_emoji" StreamSubscription>? _scanSubscription; StreamSubscription? _connectionSubscription; @@ -1288,15 +1288,9 @@ class MeshCoreConnector extends ChangeNotifier { if (reactionInfo != null) { // Check if we've already processed this reaction _processedChannelReactions.putIfAbsent(channel.index, () => {}); - final reactionKey = reactionInfo.reactionKey; - final reactionIdentifier = reactionKey != null - ? '${reactionKey}_${reactionInfo.emoji}' - : null; + final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}'; - if (reactionIdentifier != null && - _processedChannelReactions[channel.index]!.contains( - reactionIdentifier, - )) { + if (_processedChannelReactions[channel.index]!.contains(reactionIdentifier)) { // Already processed, don't process again return; } @@ -1310,9 +1304,7 @@ class MeshCoreConnector extends ChangeNotifier { await _channelMessageStore.saveChannelMessages(channel.index, messages); // Mark this reaction as processed - if (reactionIdentifier != null) { - _processedChannelReactions[channel.index]!.add(reactionIdentifier); - } + _processedChannelReactions[channel.index]!.add(reactionIdentifier); notifyListeners(); @@ -2688,26 +2680,20 @@ class MeshCoreConnector extends ChangeNotifier { // Parse reaction info final reactionInfo = Message.parseReaction(message.text); if (reactionInfo != null) { - // Check if we've already processed this exact reaction using lightweight key + // Check if we've already processed this exact reaction _processedContactReactions.putIfAbsent(pubKeyHex, () => {}); - final reactionKey = reactionInfo.reactionKey; - final reactionIdentifier = reactionKey != null - ? '${reactionKey}_${reactionInfo.emoji}' - : null; + final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}'; final isDuplicate = - reactionIdentifier != null && _processedContactReactions[pubKeyHex]!.contains(reactionIdentifier); if (!isDuplicate) { // New reaction - process it - _processContactReaction(messages, reactionInfo); + _processContactReaction(messages, reactionInfo, pubKeyHex); _messageStore.saveMessages(pubKeyHex, messages); // Mark as processed - if (reactionIdentifier != null) { - _processedContactReactions[pubKeyHex]!.add(reactionIdentifier); - } + _processedContactReactions[pubKeyHex]!.add(reactionIdentifier); notifyListeners(); } @@ -2722,15 +2708,51 @@ class MeshCoreConnector extends ChangeNotifier { void _processContactReaction( List messages, ReactionInfo reactionInfo, + String contactPubKeyHex, ) { - // Find target message by messageId - for (int i = 0; i < messages.length; i++) { - if (messages[i].messageId == reactionInfo.targetMessageId) { - final currentReactions = Map.from(messages[i].reactions); + // Find target message by computing hash and comparing + final targetHash = reactionInfo.targetHash; + final contact = _contacts.cast().firstWhere( + (c) => c?.publicKeyHex == contactPubKeyHex, + orElse: () => null, + ); + final isRoomServer = contact?.type == advTypeRoom; + + for (int i = messages.length - 1; i >= 0; i--) { + final msg = messages[i]; + + // For 1:1 chats: contact reacts to my outgoing messages only + // For room servers: any message can be reacted to (multi-user) + if (!isRoomServer && !msg.isOutgoing) continue; + + final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000; + + // For room servers, include sender name (resolve from fourByteRoomContactKey) + // For 1:1 chats, sender is implicit (null) + String? senderName; + if (isRoomServer && !msg.isOutgoing) { + // Resolve sender from the message's fourByteRoomContactKey + final senderContact = _contacts.cast().firstWhere( + (c) => c != null && _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), + orElse: () => null, + ); + senderName = senderContact?.name; + } else if (isRoomServer && msg.isOutgoing) { + senderName = selfName; + } + // For 1:1, senderName stays null + + final msgHash = ReactionHelper.computeReactionHash( + timestampSecs, + senderName, + msg.text, + ); + if (msgHash == targetHash) { + final currentReactions = Map.from(msg.reactions); currentReactions[reactionInfo.emoji] = (currentReactions[reactionInfo.emoji] ?? 0) + 1; - messages[i] = messages[i].copyWith(reactions: currentReactions); + messages[i] = msg.copyWith(reactions: currentReactions); break; } } @@ -2881,18 +2903,12 @@ class MeshCoreConnector extends ChangeNotifier { // Parse reaction info final reactionInfo = ChannelMessage.parseReaction(message.text); if (reactionInfo != null) { - // Check if we've already processed this exact reaction using lightweight key + // Check if we've already processed this exact reaction _processedChannelReactions.putIfAbsent(channelIndex, () => {}); - final reactionKey = reactionInfo.reactionKey; - final reactionIdentifier = reactionKey != null - ? '${reactionKey}_${reactionInfo.emoji}' - : null; + final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}'; final isDuplicate = - reactionIdentifier != null && - _processedChannelReactions[channelIndex]!.contains( - reactionIdentifier, - ); + _processedChannelReactions[channelIndex]!.contains(reactionIdentifier); if (!isDuplicate) { // New reaction - process it @@ -2901,9 +2917,7 @@ class MeshCoreConnector extends ChangeNotifier { _channelMessageStore.saveChannelMessages(channelIndex, messages); // Mark as processed - if (reactionIdentifier != null) { - _processedChannelReactions[channelIndex]!.add(reactionIdentifier); - } + _processedChannelReactions[channelIndex]!.add(reactionIdentifier); } return false; // Don't add reaction as a visible message } @@ -2999,14 +3013,22 @@ class MeshCoreConnector extends ChangeNotifier { List messages, ReactionInfo reactionInfo, ) { - // Find target message by messageId - for (int i = 0; i < messages.length; i++) { - if (messages[i].messageId == reactionInfo.targetMessageId) { - final currentReactions = Map.from(messages[i].reactions); + // Find target message by computing hash and comparing + final targetHash = reactionInfo.targetHash; + for (int i = messages.length - 1; i >= 0; i--) { + final msg = messages[i]; + final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000; + final msgHash = ReactionHelper.computeReactionHash( + timestampSecs, + msg.senderName, + msg.text, + ); + if (msgHash == targetHash) { + final currentReactions = Map.from(msg.reactions); currentReactions[reactionInfo.emoji] = (currentReactions[reactionInfo.emoji] ?? 0) + 1; - messages[i] = messages[i].copyWith(reactions: currentReactions); + messages[i] = msg.copyWith(reactions: currentReactions); notifyListeners(); break; } diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart index 004904b..b75a9fd 100644 --- a/lib/helpers/reaction_helper.dart +++ b/lib/helpers/reaction_helper.dart @@ -1,53 +1,70 @@ +import '../widgets/emoji_picker.dart'; + class ReactionInfo { - final String targetMessageId; + final String targetHash; final String emoji; - final String? reactionKey; // Lightweight key for deduplication: timestamp_senderPrefix ReactionInfo({ - required this.targetMessageId, + required this.targetHash, required this.emoji, - this.reactionKey, }); } class ReactionHelper { - /// Parse reaction format: r:[messageId]:[emoji] - /// Supports both old format (full messageId) and new format (timestamp_senderPrefix) + static List? _cachedEmojis; + + /// Combined list of all reaction emojis in fixed order. + /// Order must stay stable for index compatibility. + static List get reactionEmojis { + return _cachedEmojis ??= [ + ...EmojiPicker.quickEmojis, + ...EmojiPicker.smileys, + ...EmojiPicker.gestures, + ...EmojiPicker.hearts, + ...EmojiPicker.objects, + ]; + } + + /// Convert emoji to 2-char hex index. Returns null if emoji not in list. + static String? emojiToIndex(String emoji) { + final idx = reactionEmojis.indexOf(emoji); + if (idx < 0) return null; + return idx.toRadixString(16).padLeft(2, '0'); + } + + /// Convert 2-char hex index to emoji. Returns null if invalid index. + static String? indexToEmoji(String hexIndex) { + final idx = int.tryParse(hexIndex, radix: 16); + if (idx == null || idx < 0 || idx >= reactionEmojis.length) return null; + return reactionEmojis[idx]; + } + + /// Compute a 4-char hex hash for a message reaction. + /// Hash input: timestampSeconds + [senderName] + first 5 chars of text + /// For 1:1 chats, senderName can be null (sender is implicit). + static String computeReactionHash(int timestampSeconds, String? senderName, String text) { + final first5 = text.length >= 5 ? text.substring(0, 5) : text; + final input = senderName != null + ? '$timestampSeconds$senderName$first5' + : '$timestampSeconds$first5'; + // Use hashCode and take lower 16 bits, format as 4 hex chars + final hash = input.hashCode & 0xFFFF; + return hash.toRadixString(16).padLeft(4, '0'); + } + + /// Parse reaction format: r:HASH:INDEX (where INDEX is 2-char hex emoji index) + /// Returns null if text is not a valid reaction format static ReactionInfo? parseReaction(String text) { - final regex = RegExp(r'^r:([^:]+):(.+)$'); + final regex = RegExp(r'^r:([0-9a-f]{4}):([0-9a-f]{2})$'); final match = regex.firstMatch(text); if (match == null) return null; - final targetId = match.group(1)!; - final emoji = match.group(2)!; - - // Extract reaction key for deduplication - // If targetId is in new format (timestamp_senderPrefix), use it directly - // Otherwise, extract timestamp from old format (timestamp_nameHash_textHash) - String? reactionKey; - if (targetId.contains('_')) { - final parts = targetId.split('_'); - if (parts.length >= 2) { - // New format: timestamp_senderPrefix, or old format with at least timestamp - reactionKey = '${parts[0]}_${parts[1]}'; - } - } + final emoji = indexToEmoji(match.group(2)!); + if (emoji == null) return null; return ReactionInfo( - targetMessageId: targetId, + targetHash: match.group(1)!, emoji: emoji, - reactionKey: reactionKey, ); } - - /// Generate a lightweight reaction key for a message - /// Format: r:[timestamp]_[senderPrefix]:[emoji] - static String buildReactionText(String timestamp, String senderPrefix, String emoji) { - return 'r:${timestamp}_$senderPrefix:$emoji'; - } - - /// Extract sender prefix from public key hex (first 8 chars) - static String getSenderPrefix(String senderKeyHex) { - return senderKeyHex.substring(0, 8); - } } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index f45ed34..083a60b 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart'; import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/link_handler.dart'; +import '../helpers/reaction_helper.dart'; import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; @@ -877,14 +878,16 @@ class _ChannelChatScreenState extends State { _setReplyingTo(message); }, ), - ListTile( - leading: const Icon(Icons.add_reaction_outlined), - title: Text(context.l10n.chat_addReaction), - onTap: () { - Navigator.pop(sheetContext); - _showEmojiPicker(message); - }, - ), + // Can't react to your own messages + if (!message.isOutgoing) + ListTile( + leading: const Icon(Icons.add_reaction_outlined), + title: Text(context.l10n.chat_addReaction), + onTap: () { + Navigator.pop(sheetContext); + _showEmojiPicker(message); + }, + ), ListTile( leading: const Icon(Icons.copy), title: Text(context.l10n.common_copy), @@ -926,9 +929,11 @@ class _ChannelChatScreenState extends State { void _sendReaction(ChannelMessage message, String emoji) { final connector = context.read(); - // Send reaction with full messageId to find target, but parser will extract - // lightweight reactionKey (timestamp_senderPrefix) for deduplication - final reactionText = 'r:${message.messageId}:$emoji'; + final emojiIndex = ReactionHelper.emojiToIndex(emoji); + if (emojiIndex == null) return; // Unknown emoji, skip + final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000; + final hash = ReactionHelper.computeReactionHash(timestampSecs, message.senderName, message.text); + final reactionText = 'r:$hash:$emojiIndex'; connector.sendChannelMessage(widget.channel, reactionText); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index efc3537..cf34381 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -11,6 +11,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/reaction_helper.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; @@ -850,14 +851,16 @@ class _ChatScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - leading: const Icon(Icons.add_reaction_outlined), - title: Text(context.l10n.chat_addReaction), - onTap: () { - Navigator.pop(sheetContext); - _showEmojiPicker(message); - }, - ), + // Can't react to your own messages + if (!message.isOutgoing) + ListTile( + leading: const Icon(Icons.add_reaction_outlined), + title: Text(context.l10n.chat_addReaction), + onTap: () { + Navigator.pop(sheetContext); + _showEmojiPicker(message, contact); + }, + ), ListTile( leading: const Icon(Icons.copy), title: Text(context.l10n.common_copy), @@ -931,25 +934,29 @@ class _ChatScreenState extends State { ); } - void _showEmojiPicker(Message message) { + void _showEmojiPicker(Message message, Contact senderContact) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (context) => EmojiPicker( onEmojiSelected: (emoji) { - _sendReaction(message, emoji); + _sendReaction(message, senderContact, emoji); }, ), ); } - void _sendReaction(Message message, String emoji) { + void _sendReaction(Message message, Contact senderContact, String emoji) { final connector = context.read(); - // Send reaction with messageId if available, otherwise use lightweight format - // Parser will extract reactionKey (timestamp_senderPrefix) for deduplication - final messageId = message.messageId ?? - '${message.timestamp.millisecondsSinceEpoch}_${message.senderKeyHex.substring(0, 8)}'; - final reactionText = 'r:$messageId:$emoji'; + final emojiIndex = ReactionHelper.emojiToIndex(emoji); + if (emojiIndex == null) return; // Unknown emoji, skip + final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000; + + // For room servers, include sender name (like channels) since multiple users + // For 1:1 chats, sender is implicit (null) + final senderName = widget.contact.type == advTypeRoom ? senderContact.name : null; + final hash = ReactionHelper.computeReactionHash(timestampSecs, senderName, message.text); + final reactionText = 'r:$hash:$emojiIndex'; connector.sendMessage(widget.contact, reactionText); } } diff --git a/lib/widgets/emoji_picker.dart b/lib/widgets/emoji_picker.dart index 1a2ffa3..7345eff 100644 --- a/lib/widgets/emoji_picker.dart +++ b/lib/widgets/emoji_picker.dart @@ -12,32 +12,32 @@ class EmojiPicker extends StatelessWidget { static const List quickEmojis = ['👍', '❤️', '😂', '🎉', '👏', '🔥']; - static const List _smileys = [ + static const List smileys = [ '😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥸', '🤩', '🥳', '😏', '😒', '😞', '😔', '😟', '😕', '🙁', '😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', '🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫', '🤥', '😶', ]; - static const List _gestures = [ + static const List gestures = [ '👍', '👎', '👊', '✊', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '🤌', '🤏', '👈', '👉', '👆', - '👇', '☝️', '👋', '🤚', '🖐️', '✋', '🖖', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', + '👇', '☝️', '👋', '🤚', '🖐️', '✋', '🖖', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪', ]; - static const List _hearts = [ + static const List hearts = [ '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❤️‍🔥', '❤️‍🩹', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '💌', '💢', '💥', '💫', '💦', '💨', '🕳️', '💬', '👁️‍🗨️', '🗨️', '🗯️', '💭', ]; - static const List _objects = [ + static const List objects = [ '🎉', '🎊', '🎈', '🎁', '🎀', '🪅', '🪆', '🏆', '🥇', '🥈', '🥉', '⚽', '⚾', '🥎', '🏀', '🏐', '🏈', '🏉', '🎾', '🥏', '🎳', '🏏', '🏑', '🏒', '🥍', '🏓', '🏸', '🥊', '🥋', '🥅', '⛳', '🔥', - '⭐', '🌟', '✨', '⚡', '💡', '🔦', '🏮', '🪔', '📱', '💻', '⌚', '📷', '📺', '📻', '🎵', '🎶', + '⭐', '🌟', '✨', '⚡', '💡', '🔦', '🏮', '🪔', '📱', '💻', '⌚', '📷', '📺', '📻', '🎵', '🎶', '🚀', ]; Map> _emojiCategories(AppLocalizations l10n) { return { - l10n.emojiCategorySmileys: _smileys, - l10n.emojiCategoryGestures: _gestures, - l10n.emojiCategoryHearts: _hearts, - l10n.emojiCategoryObjects: _objects, + l10n.emojiCategorySmileys: smileys, + l10n.emojiCategoryGestures: gestures, + l10n.emojiCategoryHearts: hearts, + l10n.emojiCategoryObjects: objects, }; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 31428df..b4a41dd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus -import path_provider_foundation import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -20,7 +19,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2e2b746..1e275d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -157,10 +165,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" file: dependency: transitive description: @@ -234,10 +242,10 @@ packages: dependency: transitive description: name: flutter_blue_plus_winrt - sha256: "0c87ca5bdf1a110d42847edeca8fbb11a9701738dc8526aefbb2a115bea29aef" + sha256: "34be2d8e23d5881b46accebb0e71025f7d52869d72ea98b5082c20764e06aa80" url: "https://pub.dev" source: hosted - version: "0.0.10" + version: "0.0.16" flutter_cache_manager: dependency: "direct main" description: @@ -325,6 +333,22 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" http: dependency: "direct main" description: @@ -369,10 +393,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" latlong2: dependency: "direct main" description: @@ -437,6 +461,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -477,6 +509,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.11" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" nested: dependency: transitive description: @@ -485,6 +525,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e" + url: "https://pub.dev" + source: hosted + version: "9.2.5" octo_image: dependency: transitive description: @@ -537,10 +585,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -629,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" qr: dependency: transitive description: @@ -665,10 +721,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f url: "https://pub.dev" source: hosted - version: "2.4.18" + version: "2.4.20" shared_preferences_foundation: dependency: transitive description: @@ -987,5 +1043,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/test/reaction_helper_test.dart b/test/reaction_helper_test.dart new file mode 100644 index 0000000..d2c70b5 --- /dev/null +++ b/test/reaction_helper_test.dart @@ -0,0 +1,404 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/helpers/reaction_helper.dart'; +import 'package:meshcore_open/widgets/emoji_picker.dart'; + +void main() { + group('ReactionHelper', () { + group('reactionEmojis', () { + test('should contain all emoji categories', () { + final emojis = ReactionHelper.reactionEmojis; + + // Should contain quickEmojis + for (final emoji in EmojiPicker.quickEmojis) { + expect(emojis.contains(emoji), isTrue, reason: 'Missing quick emoji: $emoji'); + } + + // Should contain smileys + for (final emoji in EmojiPicker.smileys) { + expect(emojis.contains(emoji), isTrue, reason: 'Missing smiley: $emoji'); + } + + // Should contain gestures + for (final emoji in EmojiPicker.gestures) { + expect(emojis.contains(emoji), isTrue, reason: 'Missing gesture: $emoji'); + } + + // Should contain hearts + for (final emoji in EmojiPicker.hearts) { + expect(emojis.contains(emoji), isTrue, reason: 'Missing heart: $emoji'); + } + + // Should contain objects + for (final emoji in EmojiPicker.objects) { + expect(emojis.contains(emoji), isTrue, reason: 'Missing object: $emoji'); + } + }); + + test('should fit in 1 byte (max 256 emojis)', () { + expect(ReactionHelper.reactionEmojis.length, lessThanOrEqualTo(256)); + }); + }); + + group('emojiToIndex', () { + test('should return 2-char hex for valid emoji', () { + // First emoji (thumbs up) should be index 0 + expect(ReactionHelper.emojiToIndex('👍'), equals('00')); + + // Second emoji (heart) should be index 1 + expect(ReactionHelper.emojiToIndex('❤️'), equals('01')); + }); + + test('should return null for unknown emoji', () { + expect(ReactionHelper.emojiToIndex('🦄'), isNull); // Not in list + expect(ReactionHelper.emojiToIndex('invalid'), isNull); + expect(ReactionHelper.emojiToIndex(''), isNull); + }); + + test('should return lowercase hex', () { + final index = ReactionHelper.emojiToIndex('👍'); + expect(index, matches(RegExp(r'^[0-9a-f]{2}$'))); + }); + }); + + group('indexToEmoji', () { + test('should return emoji for valid index', () { + expect(ReactionHelper.indexToEmoji('00'), equals('👍')); + expect(ReactionHelper.indexToEmoji('01'), equals('❤️')); + }); + + test('should return null for invalid index', () { + expect(ReactionHelper.indexToEmoji('ff'), isNull); // Index 255, out of range + expect(ReactionHelper.indexToEmoji('zz'), isNull); // Invalid hex + expect(ReactionHelper.indexToEmoji(''), isNull); // Empty string + // Note: indexToEmoji parses any valid hex; length validation is done by parseReaction's regex + }); + + test('should handle case insensitivity', () { + // Both uppercase and lowercase should work + expect(ReactionHelper.indexToEmoji('0a'), isNotNull); + expect(ReactionHelper.indexToEmoji('0A'), isNotNull); + }); + }); + + group('emoji round-trip', () { + test('all emojis should round-trip correctly', () { + for (int i = 0; i < ReactionHelper.reactionEmojis.length; i++) { + final emoji = ReactionHelper.reactionEmojis[i]; + final index = ReactionHelper.emojiToIndex(emoji); + expect(index, isNotNull, reason: 'emojiToIndex failed for $emoji'); + + final decoded = ReactionHelper.indexToEmoji(index!); + expect(decoded, equals(emoji), reason: 'Round-trip failed for $emoji (index $index)'); + } + }); + }); + + group('computeReactionHash', () { + test('should return 4-char hex hash', () { + final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello world'); + expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); + }); + + test('should be deterministic', () { + final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + expect(hash1, equals(hash2)); + }); + + test('should differ for different inputs', () { + final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Bob', 'Hello'); + final hash3 = ReactionHelper.computeReactionHash(1234567891, 'Alice', 'Hello'); + final hash4 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'World'); + + expect(hash1, isNot(equals(hash2))); // Different sender + expect(hash1, isNot(equals(hash3))); // Different timestamp + expect(hash1, isNot(equals(hash4))); // Different text + }); + + test('should use first 5 chars of text', () { + final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello world'); + final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello there'); + expect(hash1, equals(hash2)); // Same first 5 chars + }); + + test('should handle short text', () { + final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hi'); + expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); + }); + + test('should handle empty text', () { + final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', ''); + expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); + }); + }); + + group('computeReactionHash with null sender (1:1 chats)', () { + test('should return 4-char hex hash', () { + final hash = ReactionHelper.computeReactionHash(1234567890, null, 'Hello world'); + expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); + }); + + test('should be deterministic', () { + final hash1 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); + final hash2 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); + expect(hash1, equals(hash2)); + }); + + test('should differ for different inputs', () { + final hash1 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); + final hash2 = ReactionHelper.computeReactionHash(1234567891, null, 'Hello'); + final hash3 = ReactionHelper.computeReactionHash(1234567890, null, 'World'); + + expect(hash1, isNot(equals(hash2))); // Different timestamp + expect(hash1, isNot(equals(hash3))); // Different text + }); + + test('should differ from hash with sender name', () { + // Null sender hash doesn't include sender, so should differ + final nullSenderHash = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); + final withSenderHash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + expect(nullSenderHash, isNot(equals(withSenderHash))); + }); + + test('1:1 chat flow: sender and receiver compute same hash', () { + // Alice sends "Hello" at timestamp 1234567890 + // Bob receives it and wants to react + // Bob computes hash the same way Alice's app will match it + const timestamp = 1234567890; + const messageText = 'Hello there!'; + + // Bob (sender of reaction) computes hash with null sender + final bobHash = ReactionHelper.computeReactionHash(timestamp, null, messageText); + + // Alice (receiver of reaction) computes hash for her outgoing message + final aliceHash = ReactionHelper.computeReactionHash(timestamp, null, messageText); + + expect(bobHash, equals(aliceHash)); + }); + }); + + group('parseReaction', () { + test('should parse valid reaction format', () { + final info = ReactionHelper.parseReaction('r:a1b2:00'); + expect(info, isNotNull); + expect(info!.targetHash, equals('a1b2')); + expect(info.emoji, equals('👍')); + }); + + test('should return null for invalid format', () { + expect(ReactionHelper.parseReaction('invalid'), isNull); + expect(ReactionHelper.parseReaction('r:abc:00'), isNull); // Hash too short + expect(ReactionHelper.parseReaction('r:abcde:00'), isNull); // Hash too long + expect(ReactionHelper.parseReaction('r:a1b2:0'), isNull); // Index too short + expect(ReactionHelper.parseReaction('r:a1b2:000'), isNull); // Index too long + expect(ReactionHelper.parseReaction('R:a1b2:00'), isNull); // Uppercase R + expect(ReactionHelper.parseReaction('r:A1B2:00'), isNull); // Uppercase hash + expect(ReactionHelper.parseReaction(''), isNull); + }); + + test('should return null for invalid emoji index', () { + // Index ff (255) is likely out of range + expect(ReactionHelper.parseReaction('r:a1b2:ff'), isNull); + }); + + test('should decode emoji correctly', () { + // Encode thumbs up and verify decode + final index = ReactionHelper.emojiToIndex('👍'); + final info = ReactionHelper.parseReaction('r:dead:$index'); + expect(info, isNotNull); + expect(info!.emoji, equals('👍')); + }); + }); + + group('full reaction flow', () { + test('should encode and decode reaction correctly', () { + // Simulate sending a reaction + const timestamp = 1234567890; + const senderName = 'Alice'; + const messageText = 'Hello world!'; + const emoji = '🎉'; + + // Compute hash (sender side) + final hash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + + // Encode emoji (sender side) + final emojiIndex = ReactionHelper.emojiToIndex(emoji); + expect(emojiIndex, isNotNull); + + // Build reaction text (sender side) + final reactionText = 'r:$hash:$emojiIndex'; + + // Parse reaction (receiver side) + final info = ReactionHelper.parseReaction(reactionText); + expect(info, isNotNull); + expect(info!.targetHash, equals(hash)); + expect(info.emoji, equals(emoji)); + + // Verify receiver can match the hash + final receiverHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + expect(receiverHash, equals(info.targetHash)); + }); + + test('reaction text should be 9 bytes', () { + final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + final index = ReactionHelper.emojiToIndex('👍')!; + final reactionText = 'r:$hash:$index'; + + // r: (2) + hash (4) + : (1) + index (2) = 9 bytes + expect(reactionText.length, equals(9)); + }); + + test('1:1 chat: Bob reacts to Alice message', () { + // Alice sends "Hello" to Bob at timestamp 1234567890 + const timestamp = 1234567890; + const aliceName = 'Alice'; + const messageText = 'Hello'; + const emoji = '👍'; + + // On Bob's device: message.isOutgoing = false, so senderName = contact.name = Alice + final bobSideHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); + final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; + final reactionText = 'r:$bobSideHash:$emojiIndex'; + + // Alice receives the reaction + final info = ReactionHelper.parseReaction(reactionText); + expect(info, isNotNull); + + // On Alice's device: message.isOutgoing = true, so senderName = selfName = Alice + final aliceSideHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); + + // Hashes should match! + expect(info!.targetHash, equals(aliceSideHash)); + expect(info.emoji, equals(emoji)); + }); + + test('1:1 chat: Alice reacts to Bob message', () { + // Bob sends "Hi there" to Alice at timestamp 9876543210 + const timestamp = 9876543210; + const bobName = 'Bob'; + const messageText = 'Hi there'; + const emoji = '❤️'; + + // On Alice's device: message.isOutgoing = false, so senderName = contact.name = Bob + final aliceSideHash = ReactionHelper.computeReactionHash(timestamp, bobName, messageText); + final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; + final reactionText = 'r:$aliceSideHash:$emojiIndex'; + + // Bob receives the reaction + final info = ReactionHelper.parseReaction(reactionText); + expect(info, isNotNull); + + // On Bob's device: message.isOutgoing = true, so senderName = selfName = Bob + final bobSideHash = ReactionHelper.computeReactionHash(timestamp, bobName, messageText); + + // Hashes should match! + expect(info!.targetHash, equals(bobSideHash)); + expect(info.emoji, equals(emoji)); + }); + + test('room server: user reacts to message from another user', () { + // In a room server, Charlie sends "Hello room" at timestamp 1111111111 + // Alice wants to react to it + const timestamp = 1111111111; + const charlieName = 'Charlie'; + const messageText = 'Hello room'; + const emoji = '🎉'; + + // Alice computes hash including sender name (room servers are multi-user) + final aliceHash = ReactionHelper.computeReactionHash(timestamp, charlieName, messageText); + final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; + final reactionText = 'r:$aliceHash:$emojiIndex'; + + // Verify format + expect(reactionText.length, equals(9)); + expect(reactionText, matches(RegExp(r'^r:[0-9a-f]{4}:[0-9a-f]{2}$'))); + + // Bob (another user in the room) receives the reaction + final info = ReactionHelper.parseReaction(reactionText); + expect(info, isNotNull); + + // Bob computes hash for Charlie's message the same way + final bobHash = ReactionHelper.computeReactionHash(timestamp, charlieName, messageText); + + // Hashes should match! + expect(info!.targetHash, equals(bobHash)); + expect(info.emoji, equals(emoji)); + }); + + test('room server: hash differs from 1:1 hash for same message content', () { + // Same timestamp and text, but room server includes sender name + const timestamp = 1234567890; + const senderName = 'Dave'; + const messageText = 'Hello'; + + // Room server hash (with sender name) + final roomHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + + // 1:1 hash (without sender name) + final directHash = ReactionHelper.computeReactionHash(timestamp, null, messageText); + + // They should be different! + expect(roomHash, isNot(equals(directHash))); + }); + + test('room server: different senders produce different hashes', () { + // Two users send the exact same message at the same time in a room + const timestamp = 1234567890; + const messageText = 'Hello'; + + final aliceHash = ReactionHelper.computeReactionHash(timestamp, 'Alice', messageText); + final bobHash = ReactionHelper.computeReactionHash(timestamp, 'Bob', messageText); + + // Different senders = different hashes (even with same content) + expect(aliceHash, isNot(equals(bobHash))); + }); + + test('room server: self message reaction works', () { + // Alice sends "My message" at timestamp 2222222222 + // Bob wants to react to it + const timestamp = 2222222222; + const aliceName = 'Alice'; + const messageText = 'My message'; + const emoji = '👍'; + + // Bob computes hash for Alice's message + final bobHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); + final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; + final reactionText = 'r:$bobHash:$emojiIndex'; + + // Alice receives the reaction and matches against her outgoing message + final info = ReactionHelper.parseReaction(reactionText); + expect(info, isNotNull); + + // Alice computes hash using her selfName + final aliceHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); + + // Hashes should match! + expect(info!.targetHash, equals(aliceHash)); + }); + + test('channel: same logic as room server', () { + // Channel messages also use sender name in hash + const timestamp = 3333333333; + const senderName = 'Eve'; + const messageText = 'Channel msg'; + const emoji = '🔥'; + + // Compute hash with sender name + final hash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; + final reactionText = 'r:$hash:$emojiIndex'; + + // Parse and verify + final info = ReactionHelper.parseReaction(reactionText); + expect(info, isNotNull); + expect(info!.emoji, equals(emoji)); + + // Another user computes the same hash + final otherUserHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + expect(info.targetHash, equals(otherUserHash)); + }); + }); + }); +} From 6712088fcda3d0abaf8cefd0f303dd9e6dc3e793 Mon Sep 17 00:00:00 2001 From: 446564 Date: Fri, 30 Jan 2026 08:44:03 -0800 Subject: [PATCH 047/421] add obtainium badge allow users to easily add app to obtainium https://apps.obtainium.imranr.dev --- README.md | 28 ++++++++++++++++++++++++++-- assets/badges/badge_obtainium.png | Bin 0 -> 21824 bytes 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 assets/badges/badge_obtainium.png diff --git a/README.md b/README.md index 2acb390..984e6ba 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices. MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities. + + Get it on Obtainium + + ## Screenshots @@ -21,6 +25,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Features ### Core Functionality + - **Direct Messaging**: Private encrypted conversations with individual contacts - **Public Channels**: Broadcast messages to channel subscribers on the mesh network - **Contact Management**: Organize contacts, track last seen times, and manage conversation history @@ -29,6 +34,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Message Replies**: Thread conversations with inline reply functionality ### Mesh Network + - **Path Visualization**: View routing paths and signal quality for each contact - **Route Management**: Manual path overriding and automatic route rotation - **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking @@ -36,6 +42,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Repeater Support**: Connect to and manage repeater nodes for extended range ### Map & Location + - **Live Map View**: Real-time visualization of mesh network nodes on an interactive map - **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range - **Location Sharing**: Share GPS coordinates and custom markers with contacts @@ -43,12 +50,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **MGRS Coordinates**: Support for Military Grid Reference System coordinate format ### Device Management + - **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth - **Device Settings**: Configure radio parameters, power settings, and network options - **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves - **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon) ### Repeater Hub + - **CLI Access**: Full command-line interface to repeater nodes - **Settings Management**: Configure repeater behavior, power limits, and network settings - **Statistics Dashboard**: View repeater traffic, connected clients, and system health @@ -57,6 +66,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Technical Details ### Architecture + - **Framework**: Flutter 3.38.5 / Dart 3.10.4 - **State Management**: Provider pattern with ChangeNotifier - **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy @@ -64,11 +74,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Encryption**: End-to-end encryption for private messages using the MeshCore protocol ### Platform Support + - ✅ **Android**: Full support (API 21+) - ✅ **iOS**: Full support (iOS 12+) - 🚧 **Desktop**: Limited support (macOS/Linux/Windows) ### Dependencies + | Package | Purpose | |---------|---------| | flutter_blue_plus | Bluetooth Low Energy communication | @@ -84,6 +96,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Getting Started ### Prerequisites + - Flutter SDK 3.38.5 or later - Android Studio / Xcode (for mobile development) - A MeshCore-compatible LoRa device @@ -91,17 +104,20 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Installation 1. **Clone the repository** + ```bash git clone https://github.com/zjs81/meshcore-open.git cd meshcore-open ``` 2. **Install dependencies** + ```bash flutter pub get ``` 3. **Run the app** + ```bash flutter run ``` @@ -109,11 +125,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Building for Release **Android APK:** + ```bash flutter build apk --release ``` **iOS:** + ```bash flutter build ios --release ``` @@ -152,25 +170,30 @@ lib/ ## BLE Protocol ### Nordic UART Service (NUS) + - **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e` - **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device) - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) ### Device Discovery + Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-` ### Message Format + Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions. ## Configuration ### App Settings + - **Theme**: System default, light, or dark mode - **Notifications**: Configurable for messages, channels, and node advertisements - **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types - **Message Retry**: Automatic retry with configurable path clearing ### Device Settings + - **Radio Power**: Transmit power adjustment (10-30 dBm) - **Frequency**: LoRa frequency configuration - **Bandwidth**: Channel bandwidth selection @@ -182,22 +205,23 @@ Messages are transmitted as binary frames using a custom protocol optimized for This is an open-source project. Contributions are welcome! ### Development Guidelines + - Follow the Flutter style guide - Use Material 3 design components - Write clear commit messages - Test on both Android and iOS before submitting PRs ### Code Style + - Prefer `StatelessWidget` with `Consumer` for reactive UI - Use `const` constructors where possible - Keep functions small and focused - Avoid premature abstractions - ## Support For issues, questions, or feature requests, please open an issue on GitHub: -https://github.com/zjs81/meshcore-open/issues + ## Donate diff --git a/assets/badges/badge_obtainium.png b/assets/badges/badge_obtainium.png new file mode 100644 index 0000000000000000000000000000000000000000..cc3a0ed0e00bbae1f0c02d22f449529d1e1fa6f4 GIT binary patch literal 21824 zcmeIabzGEP*DnrIN{C2Er-UFmbV_#!NH@$7LwBPRf)WBMB~nt-(jf>)NlOeN-5t_- z_ISthJn!#4?>X=1bI$LN^LfU5?zv{JeeGC#?G@j(77-e1@_5+f*eEC{c!~-#nkXnY z!oc;}O-%5A(ZjHH@G$_@Q-mohvZCAsUqDM#bQDx@6?Ow${&{@{zTZGaLqS9S4&Hac zHL?xzO^%B3`!f<;Q~Y`T23)iJ*#>lj!i{x4%B+6;(B8c{sSaI3WDsc`hDaVJ?U;go~DkN0^6Cn2!&PkDC6E86kUu zd5xKY`Gwj_OKT`fOVc{LI>Bu1p(rTc?*n4R6j~oqwwW4c)3e{!xkJ#Uk-C{l|6I99 zihwo5Ama&EymC{zqIBH+JWCA;<%M3MaE;)``Yh|s=QOl^x7A*9XwF~9NxdS)k3Qeq zo?h6dnsw4nwDW93eNrCj&O>MARv@)Yj2dZ%V&0?FGrS_hjnTj7flp()Vh z`pL?%RQ0`>eYU{b{ zsj3KDIyrEdTRB-kIlLX5kvoPWD&g&HZV89F(^^2SZ5_qvcj}wyX>F~<=ymy2xm2B{ zp*FS(zOGO$Uo~w@U$~`^6}^Nwwy3u-=)eK$Zcgj%VDIQA>@7zByI)~&jr^IDp7wVW zceoh69$+MCCs!yfF9$CN7rU&tttSt?I5w@Qs})RGQ%3I3F2FZ2dK-6lXJJlGFE1|+ zF9?T|t2HOLkdP237Y`>74?AeV?&jm@Ztl(Q=*EET;*UOLpl+6~w$ARhPL8z5KFuwh zJlw_T>A`*4KRb7}adLNZvvG3%dk9CjKPdrd!uj`YPHqk^&VPOoEK^nWKYz&E-1&EN z2Zw)o!p&XQ6O8`nO8#Y-zn*Z@_Hl-CYC_$dJX|fIvYt>!cZPo+6nXak?g2>)Yg-5B z-_!up`Io`0EdOrj?BQzvyN#74C)6J506KL8^W*-P_U^W@e^B{f=7zlUpRNAy{{N-* zUn}`N>%XZdEbV0PaMonY?#~s^W2bKS%I!B1bBEMR(w!)9xEL5n1V(aMP{f`^kwhmA&cXJ28vfKjP z+=38@5T78I0ECRGO&;P15^1#!I{(}*; zq2BH)=01P~zwi7Z478vwe|-DntG(@SDL_m6kH>yLqGs*@7OHOHZf@&n>*4Tw=Aifg z&=9Yi#j^*z? z`GbAHv6`=9Xh|27l;-HV$O%-ze}6)I^Bm_dwQ5~&nuHU5&4K*@7((X!~;I$Ak- zxjp!cI)6;S%La;Mxc@1S{7Iz$K>vSTnVY#M^gpc2(#G7;8VVH8p91T@TrZ!cIX9G- zhaJWb6Jm!zc)8fYG2>$w!klrua{q#@2@lQ|HFEPxcK>?{N@7e z7M6S#?7ZL$y9JaR!fpiC)+)1Z&BdC)%f4s^>1?hw!GEA@gf>fJtClD9g*B zTqFObf60vpzua+BdP5@ zu{GsmH0&`iv5RmH=VG9(#-dKA&8H=|%Z$r=rBmfO8)sZ+Ql#OadFDYJl;T-fSGr`E z!mjP<{b<9nj^xeb;{0sJ2m$PA`eUJ=Tul$^r|D!_mXIKn5?QbZS?n1b8y%?_#ozod9XUzI@R8ulUr0&^hGHFMcc z*+f8NVJS;XODz|dJx>~%Gzsz3#ds)g7zu1BN7co();}vN%Tk?0m!6(}9k;Tw(u0qW zk5laW;@1ujsln6XVePge!|JPrxjF79i2kWM4;(~^sl1}%Epl>ly|{?*a0O;8EUcQ^ z+GYge9vD$LsOsa#MYxmGj{Hhbl2)(Tn5_8ODg$S>rq6I&8#I0FS8Z*rpz|zB;Mp4U zn}me&5JEr?Mc?D?C~nJkT6+2$S65e=5?YTRyXbQy#P}^IS!0voii{s^O^SW`^hxb- zeMFh9#k9`-R$AcMgTD7dt#K+cGBPke{}X3Xurg9!o7bamBO@ai*r68Re%89Z0#7Rf zx>zbIc0IUv@1DfrR&Q^wf~sm;W5D^qj~^{@%HVmbmiBf`e6fQ&nORxpa5$THab_lc zwRQgohUNY=Q)`bEXp}%!ditzZuZEtMRfOteEE`U)Z3(L)bs03!!Napl$1&GLkABJ5AVxEgv(qD#yf2!$$4dEWd%Lf ziI`LpQAFxRAWL0K`{uE+vAInr^Ed_T2-p+8u>Ab|=&Gu!uJuW6R^n;j4fe&Qr84)s z1O$(BySuwv4V}JVyj5Zxb98b_d!L-V-f1=_S!`7M`IBymN#FY9(dKyY=BE8SCFF2C zD~DN-5EwVqoON|>#;OFcGp{B(5@5}%ydSXjPpPu1&_gPqOm-kSbGfq{YX z+@Y_pue-`|%C6tc2BwtFW5AKnJyIQTG;VEUZ*QfDBmxIwY%DzJa>MkwgKMo(u{JA> zB^PfwU~TAnjIy%wUMs#tDG``*Y@49#;!P!GWe*rBW~--WzBUg&eoXB8yNm>VBfxdn{7PbZcUdAo?Tymxxepf z1kRXzG-Y$Kas6)7^<{s0KyRwZy>jb*N57vxe@52S2)=pqhVub6LxPLO=4gM*gmG;Y7@W}bpqf3gI=}h8Pyg*aSV1aEO zys)mIxO+HXD>gY9TSR0|zObT#XeQv8%JF9nDl;=PE{!I#oAErLfZ8O z^M}AQ+u-SfvN8f7@-S%(pNypXJvK4n&>y-V-vu_3bu34lb#P!H{g+-(c3mAgI5u(p zA|fI<(O()Hse!1ElW_V@zR`5r>r`slaeHhMM`uYUAVBQ%=g%5OM!10RfS4`K&60Qa zy*9_5efq>|Wo1Qr5ePVSr}3OaN=iz@)U@A}OXi!I!n=~(0M5vD0ko^_1+LBjUdk;$ z$?O~28kM0^#3=ZuW-4TLqzvbBbmuVPXk$fbV=a&BCh_9p;*;56eDsXc{QO%5x+QX& znsGYbq@z9458Y#X^B%HNU>P^qtx1=4|@r6m)JFAMLR^kP+AafCyVM z9Q|o1&b_Mov=12{{Pz`8`_lwnqy`YK8>20&Q%-{{&-LL4Dk=PUr>CdSx0Z2*tQwva=rnhCD{qJcxvg2iRNl}) z2_G$N25LR%($zgBHI_t(8G%Hv9ZKbalXk zbmW>&P{dmjqv8^E7&{bDzGYM*mh~<6hc97fzOyuYDq|9NX8CFjO*jD$JT$0KDBUV3 zX>7rw@lXHd0i=xguU-L+Nmx%JsNejn7fi7p!dE&cwVSt%US2lp{K)5+ksa)zX zm?2V0Z6r4B$~V1DrgqRD{H4cJZ(UmKeN z1*XA5^xPp?YeT_js}g}8LyR42WRQ2GC)H=Of)u|sIYzBVcqwBP~=jjU?^98VRO9vCN{R+ zB)`YHw*SRmhp?%xo?iIn`SC)M^#|L@DwhShi{qwiS#xtbCu|`6H9IKQg{T?MRix9s z1dXTyWRgQh;Md=z%_rfsg^!8wLt6qSCdL>#{m3zEV_L@zO&2!8w zSEZxJlwK!Q6Ekv|N$((76eDXaW;VJ?rE_2M|nrhr=~q=Zq|AB_>%fr znxCNBQqZ%)jcY06_(N5fg|)Q@-u|ciOSgm6{rvn6wx=f3(~1fUW!&5#66n4s=jS-o zX(C>Ic?h^#PEoDIgSNIdZ=jX;=3dZLIr!~&F_$?^@(05|M1+&#cF~IWN#EgKBt!RA zmEpvyu1e39_?rJso?2R%NNI#cNdPi4o#VXw?L@Nw>x?D6+VW0o1jTjLCwL**!1wNM zkqa@f1-1j4z9`Q9V#l*!4=%V?2ge`CPIJ@(rEDP+_X zn2J$@vqOiHg_blHvH%PfgJ+K;m9krYWGlPOe~YLhcE-db4*`dLPCe+1(CH46Db6vx zqY`e0CZlTTJ{=O+AEWH;X}LUhA$vyl<+*e10TyNb*{zqKI1LUw_}zVDA7{yt4fCG7 zkhnla%3qvPjA`kBxni;N-mb1@3a17-(p#7N-JF)^M_ZWq&*|tbxb7k&s-lBWg=d;)8QINjNc-x0zMnT3xyO5=VAsK7ry|%4du3RYM>0bakYb zE$@w&TmRm`z;{`OhlB-7BlZTP@|!DgI6P{isI4O!)nL2kBad~ zyP854DWdASvzumpPxTMHzT{3Z#2gAhHVPgOizVxSN*bT8Vt(FWZSiQQOwja%FR*rx zY@OZysS*^LJ?mnvq7eX$zlA5husT3nJjoGL^NtW{E$F^|(~# zt>j(570McacI(q{auUYeMRV%{J zKaO9+Pj=_xdSCkZ_!wNrLV@u|BFgWHk^K7goz$fkQZ}8!N0O3~la|2PK^nacia`2$ zSMO=#J{m4%^roAa7L_;+xPXn_RUy3){Ltuy)`Ho?;~P}X$~kK=t{8#Bta*+&a(dg0 zs+MgnoGh}z0)L@p%=ePD^6F(W-G~-9u2vXF$H7;=)eW9c$~)u0@MQM>nI(@e3M{jS zJvz&iPEEw3)FPe_&yIJT2H@bNaXbDLL{1GD7~X5y3Fwo{JxRf3mL2&^`fdm+~lol7MQWz<_RL)Ci`cX@&M^`i8hi?hz~RC! z>$CtSQUxnZ%ixm}?_d*(d-s@E)5eB}KUUtZD)R(J%)xNEC&shb;I zg4)*e0=)f)StD27Bzz~>&;zct?aT{Hx-}yA0ypsfCv@{_%rpAkX02z7clz^n`r*BKCALo}$E{5*zq(twC46Xx>W% zA?;OX50AER0tPO(WfkBb!M8TrqbPt;2wWG49o5wL?@CC6{DNyeHX-2!a8W*f{8-`O z2A)BBF)I?+WHbK1@UU@k=C-$^f#DEIo;}rc2pmW`HMMB{Cik9H5nZ@54q({~;@D&7 z<^aGPV&d^!-+q}KlzQ;e+DG8teCEt!=MytcDzZ!9?=^>(6}^%`VQQ8+Q?4OclT;xU z;mtB!co{#P>pKwqt#e`2ZP-WjX}f}*c{oSmb{*fdrt2#|(W{e%u}XM?S=>h8ypyuRK4y_zpt;a1+X`OK@X4?i8q+O*O$k0OG}{ucaSc1Rn*Y9^Zfa9 z_r@czlda3WDZRd;Kw9g4wz_qjM!YAmKxUZkBqbXyisr!ZBJjd=?;7SGu{!Xiro;N~ zrh{KHBT?1SA=4{J8V{mqLon6y-e`;fb_7RJ63PyNBw z&UiVNxw$#g&3RlR;Pc)q5QkjR;X9|2~9`~t;Xk9j--UCwsuHjBI*76_uV`9 zE>0F=yjN1aye-4?q)Xcedd`IL0Yz!rI>iZAH^|e=(`HjkN6wrgH1ZfkZ@c3>OFJiB ze0yO(?6OQk(-tlcG28s?5mu-{FDM^>^uYe^yW62Z&KsWi+{IHJF*c!AOBD{?Tj;QC zk0kr8-=W`$PEKy7)Bv^cIyE)QsqqwZZEX!eO;1-1>WesxYEXb#6d#kHnc0$_2Di0s z?w7#neqw8zKVGcr;ZZqy6{XL=4U8-RY1sP}3f0!sN?##NgQ9>uA!$0F+Trrt)GKNX zk+?h{{PE+*>$te*>vosW9YFr)zc^W0c}~26jUF1yATO0s1Nr*$m)YG|q0YiNT2r5; zxA7Sn84LzA)=;Q9@I6R8A~~W>ye$sMBR45r>htwJAF0=`5!Ss+hnR)NNhefU?mSp1 z>G%~tG%#7#6uo8>=>*epx~qflog)B&$B6PTdIvV_-*$aE;%kl0pIW6hNFR z-VHLaoya*@W6{vibS}9L4-e0d78yQ&tt3MP%tL^%Jz}qaKVIXq;QZ?PvfU67h>fND zP3-~;v;KtwCh;DVxmcwGCO-uKu%Z9sLZjXR+BVg0jl$DN(C}b0*0Yhc`36mc-bFBCF ztRqUZvu`232bnbkYzoLy|6{yB^xK|l&&7l4ZoJiV=s*&T_)8!xbU}38cFdckp*2CFsvdE7qNtdcNY8*wiVQkPQ8+5_dHRhXWD}=)m zR8e{k0A=7!PpWOqC)fRcyUYxrN!+BKjRXPo>2P6k^R0}vp+VA#9??=S7T|S7ipMB3 zN+b`fYkZb_>;x%SDo`YS-uNzu4{}dCRmvkk9ES-BMwzedYvRhhK1LV2M1O+G2Ptq5 z;kHap+OntO`mFwDaK%;lnoWC@Bph8(T8G6}9a8QoA^Dchjp^k!Va=1S!A^jp;f5L0 z+RsYAK3*-#N=rX-=bi8E8CV@ce1O4#4c{=hqKVKBRnC@=qV_3#j;-1oRip2N-BFS| ze{yJ?XUhWDm5}^e0b5=_yRrCo(;B&Dc35i84w(ev1Wb{lVa*$;6o?*w>wRmQl}EUq z_^sJ-mU5~6wa-;>kCO(Ej3sqh>~;9E()THg-_mr1X6|ShxMe#?h;11Ytny8D^2KF27xaLwE^i!2@kE&ytROA^(?&FFn_pFii`ZG(RzBW(P4H<0SbzM{M zED?Pn-gg|HhNjK(sV*G8q%WvjO4Ih;Xz=pDlibFNh1Y+C@DOp+w7VAPH&@hmerd!< zDIFyD!Hs>&OinE;m9Y%ue_HVLtzyIL0PAl%X2)FdeH%uSObx`X6FLKqkMlJO8$#gF zj#yn+bVRqu@!YT1=dG<>rtrnGF^@qi>^6C7oEtd?5VtZOEZLFVzt}H93*Q>uM}64t zEJR<9IxDrer}}zovg;nhSz` zNO12qTGT3|v<#X{4|LFuU z;1TIe<`tHUWy@C-$*i%u-+x1>VDrtq)4DK+vDd-&o>yw}y_}HLM2Mf)qkFA;kgf7l z7e}MX#AqQpEn8EP+OQ^xv{PlIwTO}27fa?`N6vt|cQZKcd{v}bmSHSkT|P^k&i!0E zL&+jSsEk<2@p`7M3?K90_ErTPGUkwZYk|`8wTxUvy?TlFdQTztRM0JQo=dhjKl8IUx5i4eieCjAa~`~}fIq!W|{}Z8di4Hc7qDsBDl(F!!y}j+%rMR@+?x=p3 zq6?W^BMMW5U!@%Nt1zjo8_Ek-%*l3&UBPt1GCqJJFh0{Mqd*j zeCp}vWOyh0DZ#kFaYpI7RIc$ouUd%)UyMpiVe(6c7`o5>amj9Zh4t;v|Bx`X4Mf3Q z!)}G6K**vN2M`)jO!OEHX+(5qOE8oyVp+}nL@*+l2H3E9l8PkR-Xsd|>uvI3ys!Ub zA#9OkyR?*>i$CBe>$jY9)Z`gPdw%}>5t%}q8*6xh_Sw-tIyr2g;|u?#Q3ok6Rn%T@ zPcrJcns=csOY?{;KLd#90J-#xwj|;QA%E+ez~`F1J@XkF80mG5r1xIlo;xVItkFCx zmo-W-pT>VQ*s6R7JDaJ7zc{J(aAuzGrR)gv;8>y6pQ3wn7lPXO<;4b>Phovp`tVJ| z!PpM^^NXc36Mw%ss$pIoz2VBW9p92!K|KA#kC(kY>^1s(XgExdnQz^^`8AGFd3JvO zIRIrWSNc*#oq%qs*HvhmnLTokp&C-xm~rP-?Zp#xw@qmamC}_rJ#eA6eFx;VEsUd} zHl$^p&`b67RYXK{h3(Mn=4RyAua8ikHZ@5sx(Wf8)YZ)m6$N0>^1$gyxI`6knMZ#f zT&X)5+hujZXtA`!DI`SZT1$yiYSw&16hzc+;^VLQ5OR}F2LNc4?&~k{L?E^g5v0OY zQAJTyEtku55NjMeb{5JrKbaPtWwY5%ODaTLCv3T%j;Ou8-D0#*KT*&Hd zGgx!W!-!7k#y2}6j}i@fajM?biO$#n>aP&AVuCHpno(1;RL&GXuX0`W^R8(*!W`#jG27ZZ{X!_;&Ip{jDYXxbH|6*Ft(X(VR zcfZD(Gh4B;kdVyxcoUkTvCcQ2m9)d#Juxn5R@0A*yDmnO>zKWe3m4Hq=#4x;wAETm z0A|c~TXgZu&3dMwpubdPqB;lQc9m zIG^mAUtI14?Hp-U|14Tj{bt5up0^@+;6|MTHvaID<;5rF2 z+7w?%y&cRbE#mwz0UtizS)dF_l7#Pm6$=W>wFn*F#eodIt@wV z`SMO6euPyXd>uC17?VVuIF%LK>H3{pOZj*$JXWn)wPd19aF&Yh=+ z8;A|u&{RHQL2+>tYeAzj3k-S&hEwII$QY!-63KPQL_4b(s@g{Y(*XSeRF#(r!*p(c zBCt)zGdSDvjGm4&%(R#tN3>(X68!K?GGyu%$4X(aA~!UR!iw#|P8$E+XNzxmQ^~OL zZoL5FJm`FK<^=S=%F+I~i+rEWZFgr=VE5}TbakZli*#<*V|<;Kl}dSP<>R#>-K?9< zR>Rx-8R3+5HxQyZ3kOC0E?M(6)A0EnZaiQZ;ezoF1V>sKz9lDB_Gf&$5_UA$yXy^( zP$HN4^Hn!nTU%rP7&9Q@`~w0AsEWRPvv79ix_kGoutU_+)2Ap%5eVSWo4B|k0PzOV z)u)rc+LxA>=_Mp+a@Et?L5{II`4TE@mvAe?If z!fI1?!f!h}6#)Ek52#9cfN7$yd`L~5@|>Qmc4{j49?BT;hZ4L)LV^jBNU9>vaBy&3 zHb&VgMLY|qmUw`4W{{7T0U@)C%gf0|o0MN-3JNehtkdsoCGTu^kEmb z0#bKIYL~eug!c9PoMW&Plp4QirLwG7I|Z8NQd7?Lb$>fhyb^)>0=klKZv4zY8KEgm z`+9C^z|iR|xtO-=R=2RoT;%Kp*cbBQvZmjF0vU)8vYf&|n7 zxKTgyBM=o86<8T^K*J;PQ*e^w6BG{i_gkK0;5c8LxD-&R4A;plg2qwUphk<>t zFpwdA^XAQ2n$p`33xJCJ`Wf0C$(j`y7^qk8$*u;J_3;&u2eT)83woty&zdtzD>th- z&bxTNnvG@j{X8Stq4p&qTj9^BIr{`KcCjH4>w2Bjx~~!^hAyqQc%I1 zb#-XH3s1>P)JPI65XO;YG0M?_* zo?q_F3K2Gu9WgPTv3Hz2>$iNgqxfU!U^OvXja>p+PeBOSDW>)v7MZW zOI=0f8`!@|VoY9X7ck%8XGu-p8ZM6}VYTv_$IRT^gX!5)Q9k&6pTYjj*-^0V9o9Bk zP7X@UCQzIY3}_b+BQcIq;@LkS0mv5uXCXSz(gf%4%^RX`ZEbX1 zTz5du0r>CPvuEM2UP&t{VFMVqNc=(pBz>vhPh)%d5Wwr?z~CI>=VHEp|KpZKsMIrH zuY*A7n(s&yap6)N%$6QRd}kWPUGCRSq4Haf*tHalPxHr1N=T zB2cq>RrX1%W=W&F@$cW;51&d~ELw*tuk2r+Lk zyS2EpQ^H3c2-?M^Rrd7sRB?W-`~ER4&2-XQN9?}mu-_dxu1H(&OUPK#A&;ju^DL~* zHiB3w-7PfkC`5`l-Hq|hkpQIiE3p!;Eo`)@m^WSO)h7&$Cmn@VDnL8Eyn3$5`Zkpx zBeDztmdq0m*Orz>wsR5@CVr|Lz7-tT)+!1hQv(O6m)Ljjm`))$Bo80i+ZT?ElEbX5 z%+`nVfz2t9UkC&`Mm&>hT%n?2IV?&xwK^>=E!`d@%$d)%gzD8eXEX0rJN+_2Pbqvq<9-BkA2c%D`u0IKk+Q9^yBE%-GMgj|@kbih=&E}&W zk@6enqB`7~nwoG&3BS?Lt;NH8LLwq5TVk4;nms%ndcY0=SNGu=!gilhqpkYZUV(Yt zzb-YfD7P~)p*HcJ3-vS(2KJ`?RNa$<+PplLQPQMlkesISLB{tt>*ybncM-GcBwj2j zApCZ_QG?ABKy3fpvsXJ79sQiNBG<1((6nWMaEIB$bkq( z%v5<-UDu$(CRYEs3pgECZkfX<_joRTO-^6s14m&%D6WxVrj>Haf)qD@N(o*}zp&}M z`Q4YGw(SK7W#5q25gVN9u?K(*E6~1tXz$BCg9)5g4QP#oO7&K}8V9jRx!f+&-SZQLYr?B>@+&jv`wpX#B zX+|@?$>7YOShE8$ee1=u=#(6Jgto{+M@-X3(6K_)bAb*UH#eW-DX-qTrhxOK2uA_k z-s7q7MrbSU0HGb+p&RLa0b-y%ZJEJiZ;4-AFI}NAerNLqsAEddqzF}%(uzCpG8`5r z1JL6gbi~LZLPO(_0Y^#T02nl$XN~M-T@~fVHO|eMF(sv?1DmF#JP?R%%H7M$%Tr0- z*y2N?k(3XZ!&edN zH>Vn7I|gc(%goP-gl>y%uoNIz_yrId>FYOf!Z3KO#`%nIQwk2ub$HKyMIAg$V9)Ow z)&r@zK+tr6oC5+PqWdRVI>yFRqs+X(wgu@^ROF2j!0ixL)ZgOs_u6w341jaW9jCpL zbgb6Z7$nB_h!PDmyWJdZm?6VU^aWmxT?fNrw_H?Ria)C#mdAScf81}lf zjNx#xvWa_^??uP=F6By$>zV4NCX%B8xKrwX>Z+2&BkLNPpp&e+B;vXG2$-eOR3c$) zy2W~%R_n-QEKHV9jp31QaPH16@sI;QN|TqgTTd}Si07tM$^*Wa$*5`#a)YG-cH1+h zz}X6~8Cw>cjTku6T8eIgh&Z8>TO5lT?H|q@3;&{ZJJw5*tpA?b7{y zg+tqGGx5YV@ZEIb0}p@A70bLny%gPbu|o5&l0fnTM6C>fW#z}g3n-NIJQ#3X_JRD1 zewU0@ipW(6g|t{sYuSqw+K@F*%xqD5G0QI_`>jr$18ODw^mp;W@q!5YV{LPkHsvS`(^fjhYi~Zi`@D8h1!xrg(w?9)LOn7-Dwp3BwRC zCtd;IcA#im7Y1VjCJhMlGLC`#>e$z>&0>RZ09Q{2O-a57B43R*1=23q$*{}I%lD@J zcGeui`n$Sfjg&a}`AGow1I%btPC){)&;Z;b8CzBA#`=?cfC)KS^2E^=l0)!}Gi2;W z2#2IIj?fWaCWbnGpADzk@*fz0Y(MkZ4hm{&$6+76S4V1@dZ`E{`mYhH&Uks8hF;d!$>+mg z;$AMl5y9()vTWwK-P@l2V(5Mj@W_XbbjQHa1UYqN9@Ootk%~Fl(^+4%w%kL2GYjnI zaPfpUZ@zKKBU6Wxe}$fxUSHU~Z>YuLi^`S*kziz+ob!D5sO^qvcKJ?*C;L)rz}u1c zl3K68Vp38?yy!&?{TUO0lV>=%BTzvM7)8aO*1vW+f*L*SdnBzD^?_;bh!>K=PZc#R{C&z{AR-5B>?_BZmoGmj zW=~jHS~9|MBtRx)dBcm65~mkw9lLX_venhaw=ERgV`w<*0zi?%MW`Hd4b&d0#3!c{ z5_?;1d^I|O=)LT{KLYR%`5_cP)CT};bhxYcB#3OOGPnLvEFvZ*!Kl>>SUJFPTHUuK zq7cLTSyY&PF*kyM~T1Lak^pL zerEx&-FR*vC31CGuI}xN6YY2Fm*44?D=yRVqkLjuGO4a;>;@-^gsEv?@b>bTm!^&l zG5ZtU^BDufOy;XVOMnzu!`kdJ*@*e|wC$JYrOSBF0D}h3L*O#@9#I37 z$7m}I94YTZ)whCgoABg0n!-}vN~G(Le6$>|J5qNp5|fR>u8gYI(7tRn_S&kpAAkPw zeYgl5g6qNunwz(q_+8M(MzEOW01FcA&141@Vz0$7_ zE3*KW5VsPakbu}G7_rFeYLg>Ok@|$$WyCOoryraotgfbZn}>%-Ce5(SLbiU}=g}h+ zWR}r193(6LV)=B1(NSF7v1T}_q3@-CXBO897WO-WI#X!(sBHg5{aJE<5=4GKMh4E_ z0#x4X4tpvZNE%l@%2Su^(-%T_=3_P+Y#OrQ2JW!C}&n zSJ`V+=?j*V8K8zd3?TidijCzyGc$z9ARWLxFC7!QmzZXL0oR@q)GlZ-y=KOguIn~p z>;G0(u;EhXEnsJem4|}K3u(=JqW)=}BfX0Iy0~c?81MrWCYhO;yC5%+7Pwxk%Q+z- z=pS=)7eFF+97BvDDBSQkIqSu_$01{4@?rFpn23m!xB?tG5L8JopD|KWda0k27n>|D zr(0x#M2XBz>}1I#BqYE*ey$pf00CKLV1B5Px1oH~;{&^4mBKU*@~)`&z-+&imDT+p zLqlagzhKQ@mRbCHm_o=!?H z&Se2el|%YSmuH8ThzLbTG!+`{`=%P6= z#7unG7$pZ*Z2+2q^f+xGxd{mgeOEDITwZS8K+A*lAsM(4GKd9ga|XM*4Y9^Z?`r54 z8Dy7_BY?*Zq7=UHfHvfYfGkQQ)wburPnd1HXFBZDKO(UJ%snofCQuf{*?2;~!0Pz!)Z0LkB? zqoYeD$2Q#hSxX2QlK0#v=i^6r1-Fy+k%G$PuXyG*@vid5F#tsX$0BdkkAtry|A8qA z(w`dVdj=F|PjSUEk=nG1l-gi!-lwKQ5)_wQlE<#kHzp;9f3`?6pA{Nb-v$+d3Q9^m zei}A-A>bNjcwHkFM9Qq1{L#(5D&sS-WWEuqyN}td|Kauytva(LbaQ)c8)X&05ITEN zIlTfZ1Hz8NJN=Ce*laSy`gV?5s@;#-aKLoP zW&k;Ay=JKjla`JtFD<inYXlSVE4zD9L^X&~^9Rd-OS6KKB z@Nm&55;}k+E5CdA3^O9z5(}@GeX&^A% z;m_SkQ4|8Vt+}kYHs4Whou`*{JIyW&lD{6u&MC6R3-yS?5Y*Zg<|ILe{S6ekth9KN zK2~wl)V58&);i!|wd=oOLr~~@0|i>C-=G0Z8J&=!5siB7;Z#}KDShuw=C8(qG>c-G zRTv$a>u3#+erFW9S3l69mMh!m@)d~+`w|6VUGGcJm+iulMrKeccolq3UTC<9?CWK~ z?^-*+kjPTIKML>uS6}irx~zDJgELM!V8OHL4vE$vfv3}Td}6{5mgI{m4FNosuM_kW zR294vM3y7!CF{#(Yd(RHCP%0+)n{Cd_4YtS3CeP%XHtZ&zu4?C!- zshypj&GG??NB?SJb`}McQ1$joaDzwF(q_aDJN|$$ANqi~Te*PEql@oCpTxwczQ731 zNP1{oci{iz$&=KxDgaQGFM=>cY};TD3;4+cl(UI}`b#pO^PwhnSccV|^0% zwcIZAGVkO7an^YNOxN<}ffo8CD=X_hI3(42Lq2*>xZd+XmExnKdI?$II@dOX%1s7A z!Nlx1Ky6`T5c?uwH@H7`1lLItfqs9(1Q)(%2(Lf62BBn5c^MgRP(52(z-2#R~eqNTzyd@8G~~-hvKt66E1xRwyi-oc();hj^)ca2>p0b7o~# z)h9&6#8j(O_0_99ETD2m(0i9Q@aojssL`K{ot^#RAaa9=C@3g!sYOXavTHLix!Oi0 zF;U*Udv~?Cx#@om=@-qa7sDl|!Kik0&~dEISf=DCXtw;dd= zAFlvyuU85kN)@Yd!iuJSR$95dbHHDu zOhW!$fK)68d;2N?2iBJhf()O>)<7IRMBN>B>laIyrs|U)O?B+MXX-!ay!{i@01>@} zVuwmK&ntlesWL$2wn4tOf! zA&EI)54)JNv$JIotBePg&x&Z6xML%!H_WSVhpjhW9vRjb78dS;)DJOxCnp-*^qADt z)CN$68!h`Du*o-&d(R6}#UG$93h?v8{Jgz4Z{gsGSZxK|1Y7d|&TE_Xnx=~@?sBdQ Tiv|2W2NXqFHJM^5v*-T<+0V|B literal 0 HcmV?d00001 From ede3142d40ed66cadf05c611468241576aebfbb1 Mon Sep 17 00:00:00 2001 From: 446564 Date: Fri, 30 Jan 2026 11:05:57 -0800 Subject: [PATCH 048/421] allow disable repeater adverts Adds checkbox to disable adverts and flood adverts Also updates flood avert range to new max of 168 hours --- lib/screens/repeater_settings_screen.dart | 241 ++++++++++++++++------ 1 file changed, 178 insertions(+), 63 deletions(-) diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index c757404..6c1e85f 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -41,7 +41,8 @@ class _RepeaterSettingsScreenState extends State { // Basic settings final TextEditingController _nameController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _guestPasswordController = TextEditingController(); + final TextEditingController _guestPasswordController = + TextEditingController(); // Radio settings final TextEditingController _freqController = TextEditingController(); @@ -60,7 +61,9 @@ class _RepeaterSettingsScreenState extends State { bool _privacyMode = false; // Advertisement settings + bool _advertEnable = true; int _advertInterval = 120; // minutes/2 + bool _floodAdvertEnable = true; int _floodAdvertInterval = 12; // hours int _privAdvertInterval = 60; // minutes @@ -146,7 +149,10 @@ class _RepeaterSettingsScreenState extends State { if (_fetchedSettings.isEmpty) return; final appLog = Provider.of(context, listen: false); - appLog.info('Updating UI with keys: ${_fetchedSettings.keys.toList()}', tag: 'RadioSettings'); + appLog.info( + 'Updating UI with keys: ${_fetchedSettings.keys.toList()}', + tag: 'RadioSettings', + ); setState(() { // Update name @@ -161,7 +167,10 @@ class _RepeaterSettingsScreenState extends State { final radioStr = _fetchedSettings['radio']!; appLog.info('Raw radio string: "$radioStr"', tag: 'RadioSettings'); final parts = radioStr.split(','); - appLog.info('Split into ${parts.length} parts: $parts', tag: 'RadioSettings'); + appLog.info( + 'Split into ${parts.length} parts: $parts', + tag: 'RadioSettings', + ); if (parts.isNotEmpty) { final freqText = parts[0].replaceAll(RegExp(r'[^0-9.]'), '').trim(); @@ -193,7 +202,10 @@ class _RepeaterSettingsScreenState extends State { appLog.info('CR text: "$crText"', tag: 'RadioSettings'); _codingRate = int.tryParse(crText) ?? _codingRate; } - appLog.info('Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', tag: 'RadioSettings'); + appLog.info( + 'Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', + tag: 'RadioSettings', + ); } if (_fetchedSettings.containsKey('tx')) { @@ -207,11 +219,17 @@ class _RepeaterSettingsScreenState extends State { } if (_fetchedSettings.containsKey('lat')) { - appLog.info('Setting lat to: "${_fetchedSettings['lat']}"', tag: 'RadioSettings'); + appLog.info( + 'Setting lat to: "${_fetchedSettings['lat']}"', + tag: 'RadioSettings', + ); _latController.text = _fetchedSettings['lat']!; } if (_fetchedSettings.containsKey('lon')) { - appLog.info('Setting lon to: "${_fetchedSettings['lon']}"', tag: 'RadioSettings'); + appLog.info( + 'Setting lon to: "${_fetchedSettings['lon']}"', + tag: 'RadioSettings', + ); _lonController.text = _fetchedSettings['lon']!; } @@ -268,7 +286,10 @@ class _RepeaterSettingsScreenState extends State { void _applySettingResponse(String command, String response) { final appLog = Provider.of(context, listen: false); - appLog.info('Command: "$command", Raw response: "$response"', tag: 'RadioSettings'); + appLog.info( + 'Command: "$command", Raw response: "$response"', + tag: 'RadioSettings', + ); final value = _extractCliValue(response); appLog.info('Extracted value: "$value"', tag: 'RadioSettings'); if (value == null) return; @@ -280,7 +301,10 @@ class _RepeaterSettingsScreenState extends State { // Validate response content matches expected format for the command // This prevents mismatched responses over LoRa where order isn't guaranteed if (!_validateResponseForCommand(key, value)) { - appLog.warn('Response "$value" does not match expected format for "$key", ignoring', tag: 'RadioSettings'); + appLog.warn( + 'Response "$value" does not match expected format for "$key", ignoring', + tag: 'RadioSettings', + ); return; } @@ -311,7 +335,9 @@ class _RepeaterSettingsScreenState extends State { // Must have at least 3 commas and start with a frequency-like number final parts = value.split(','); if (parts.length < 4) return false; - final freq = double.tryParse(parts[0].replaceAll(RegExp(r'[^0-9.]'), '')); + final freq = double.tryParse( + parts[0].replaceAll(RegExp(r'[^0-9.]'), ''), + ); // Frequency should be in reasonable LoRa range (300-2500 MHz) return freq != null && freq >= 300 && freq <= 2500; @@ -339,7 +365,16 @@ class _RepeaterSettingsScreenState extends State { case 'privacy': // Boolean values: on/off/true/false/1/0/enabled/disabled final lower = value.toLowerCase().trim(); - return ['on', 'off', 'true', 'false', '1', '0', 'enabled', 'disabled'].contains(lower); + return [ + 'on', + 'off', + 'true', + 'false', + '1', + '0', + 'enabled', + 'disabled', + ].contains(lower); case 'advert.interval': case 'flood.advert.interval': @@ -354,7 +389,8 @@ class _RepeaterSettingsScreenState extends State { if (value.isEmpty) return false; // If it has 3+ commas and looks like numbers, probably radio data final commaCount = ','.allMatches(value).length; - if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) return false; + if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) + return false; return true; default: @@ -551,7 +587,9 @@ class _RepeaterSettingsScreenState extends State { final freqMHz = double.tryParse(_freqController.text); if (freqMHz != null) { final bwKHz = _bandwidth! / 1000; - commands.add('set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate'); + commands.add( + 'set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate', + ); } } @@ -590,7 +628,9 @@ class _RepeaterSettingsScreenState extends State { timestampSeconds: timestampSeconds, ); await connector.sendFrame(frame); - await Future.delayed(const Duration(milliseconds: 200)); // Delay between commands + await Future.delayed( + const Duration(milliseconds: 200), + ); // Delay between commands } setState(() { @@ -614,7 +654,9 @@ class _RepeaterSettingsScreenState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.repeater_errorSavingSettings(e.toString())), + content: Text( + context.l10n.repeater_errorSavingSettings(e.toString()), + ), backgroundColor: Colors.red, ), ); @@ -699,7 +741,10 @@ class _RepeaterSettingsScreenState extends State { Text(l10n.repeater_settingsTitle), Text( repeater.name, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), ), ], ), @@ -723,12 +768,20 @@ class _RepeaterSettingsScreenState extends State { value: 'auto', child: Row( children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_autoUseSavedPath, style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -738,12 +791,20 @@ class _RepeaterSettingsScreenState extends State { value: 'flood', child: Row( children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_forceFloodMode, style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -754,7 +815,8 @@ class _RepeaterSettingsScreenState extends State { IconButton( icon: const Icon(Icons.timeline), tooltip: l10n.repeater_pathManagement, - onPressed: () => PathManagementDialog.show(context, contact: repeater), + onPressed: () => + PathManagementDialog.show(context, contact: repeater), ), if (_hasChanges) TextButton.icon( @@ -865,7 +927,9 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), suffixText: 'MHz', ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), onChanged: (_) => _markChanged(), ), const SizedBox(height: 16), @@ -923,10 +987,7 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), ), items: _spreadingFactorOptions.map((sf) { - return DropdownMenuItem( - value: sf, - child: Text('SF$sf'), - ); + return DropdownMenuItem(value: sf, child: Text('SF$sf')); }).toList(), onChanged: (value) { if (value != null) { @@ -945,10 +1006,7 @@ class _RepeaterSettingsScreenState extends State { border: const OutlineInputBorder(), ), items: _codingRateOptions.map((cr) { - return DropdownMenuItem( - value: cr, - child: Text('4/$cr'), - ); + return DropdownMenuItem(value: cr, child: Text('4/$cr')); }).toList(), onChanged: (value) { if (value != null) { @@ -988,7 +1046,10 @@ class _RepeaterSettingsScreenState extends State { helperText: l10n.repeater_latitudeHelper, border: const OutlineInputBorder(), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), onChanged: (_) => _markChanged(), ), const SizedBox(height: 16), @@ -999,7 +1060,10 @@ class _RepeaterSettingsScreenState extends State { helperText: l10n.repeater_longitudeHelper, border: const OutlineInputBorder(), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), onChanged: (_) => _markChanged(), ), ], @@ -1018,11 +1082,17 @@ class _RepeaterSettingsScreenState extends State { children: [ Row( children: [ - Icon(Icons.toggle_on, color: Theme.of(context).textTheme.headlineSmall?.color), + Icon( + Icons.toggle_on, + color: Theme.of(context).textTheme.headlineSmall?.color, + ), const SizedBox(width: 8), Text( l10n.repeater_features, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ], ), @@ -1102,7 +1172,7 @@ class _RepeaterSettingsScreenState extends State { width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), - ) + ) : const Icon(Icons.refresh, size: 20), onPressed: isRefreshing ? null : onRefresh, tooltip: refreshTooltip, @@ -1130,40 +1200,72 @@ class _RepeaterSettingsScreenState extends State { const Divider(), ListTile( title: Text(l10n.repeater_localAdvertInterval), - subtitle: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)), - trailing: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)), + subtitle: Text( + l10n.repeater_localAdvertIntervalMinutes(_advertInterval), + ), + trailing: Checkbox( + value: _advertEnable, + onChanged: (value) { + setState(() { + _advertInterval = value! ? 60 : 0; + _advertEnable = value; + }); + _markChanged(); + }, + ), ), Slider( - value: _advertInterval.toDouble(), + value: _advertInterval == 0 + ? 60.toDouble() + : _advertInterval.toDouble(), min: 60, max: 240, divisions: 18, label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval), - onChanged: (value) { - setState(() { - _advertInterval = value.toInt(); - }); - _markChanged(); - }, + onChanged: _advertEnable + ? (value) { + setState(() { + _advertInterval = value.toInt(); + }); + _markChanged(); + } + : null, ), const SizedBox(height: 16), ListTile( title: Text(l10n.repeater_floodAdvertInterval), - subtitle: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)), - trailing: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)), + subtitle: Text( + l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), + ), + trailing: Checkbox( + value: _floodAdvertEnable, + onChanged: (value) { + setState(() { + _floodAdvertInterval = value! ? 3 : 0; + _floodAdvertEnable = value; + }); + _markChanged(); + }, + ), ), Slider( - value: _floodAdvertInterval.toDouble(), + value: _floodAdvertInterval == 0 + ? 3.toDouble() + : _floodAdvertInterval.toDouble(), min: 3, - max: 48, - divisions: 45, - label: l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), - onChanged: (value) { - setState(() { - _floodAdvertInterval = value.toInt(); - }); - _markChanged(); - }, + max: 168, + divisions: 165, + label: l10n.repeater_floodAdvertIntervalHours( + _floodAdvertInterval, + ), + onChanged: _floodAdvertEnable + ? (value) { + setState(() { + _floodAdvertInterval = value.toInt(); + }); + _markChanged(); + } + : null, ), // Encrypted advertisement interval - hidden until privacy mode is implemented // if (_privacyMode) ...[ @@ -1220,10 +1322,15 @@ class _RepeaterSettingsScreenState extends State { const Divider(), ListTile( leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer), - title: Text(l10n.repeater_rebootRepeater, style: TextStyle(color: colorScheme.onErrorContainer)), + title: Text( + l10n.repeater_rebootRepeater, + style: TextStyle(color: colorScheme.onErrorContainer), + ), subtitle: Text( l10n.repeater_rebootRepeaterSubtitle, - style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)), + style: TextStyle( + color: colorScheme.onErrorContainer.withValues(alpha: 0.8), + ), ), onTap: () => _confirmAction( l10n.repeater_rebootRepeater, @@ -1246,11 +1353,19 @@ class _RepeaterSettingsScreenState extends State { // ), // ), ListTile( - leading: Icon(Icons.delete_forever, color: colorScheme.onErrorContainer), - title: Text(l10n.repeater_eraseFileSystem, style: TextStyle(color: colorScheme.onErrorContainer)), + leading: Icon( + Icons.delete_forever, + color: colorScheme.onErrorContainer, + ), + title: Text( + l10n.repeater_eraseFileSystem, + style: TextStyle(color: colorScheme.onErrorContainer), + ), subtitle: Text( l10n.repeater_eraseFileSystemSubtitle, - style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)), + style: TextStyle( + color: colorScheme.onErrorContainer.withValues(alpha: 0.8), + ), ), onTap: () => _confirmAction( l10n.repeater_eraseFileSystem, @@ -1272,9 +1387,9 @@ class _RepeaterSettingsScreenState extends State { if (command == 'erase') { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.repeater_eraseSerialOnly)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly))); } return; } From d30e7c4e2c4cb7b12b3d2a76265a460a2027656f Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 14:55:55 -0800 Subject: [PATCH 049/421] Prevent disconnection handling when already disconnected, curing a race condition. --- lib/connector/meshcore_connector.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 1bab130..d5a36e3 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -706,7 +706,7 @@ class MeshCoreConnector extends ChangeNotifier { try { _connectionSubscription = device.connectionState.listen((state) { - if (state == BluetoothConnectionState.disconnected) { + if (state == BluetoothConnectionState.disconnected && isConnected) { _handleDisconnection(); } }); @@ -959,12 +959,7 @@ class MeshCoreConnector extends ChangeNotifier { if (!isConnected) return; if (_batteryRequested && !force) return; _batteryRequested = true; - try { - await sendFrame(buildGetBattAndStorageFrame()); - } catch (e) { - // Connection likely lost - trigger disconnection handling - _handleDisconnection(); - } + await sendFrame(buildGetBattAndStorageFrame()); } void _startBatteryPolling() { From 5115d8bbe350f3ecb6ca93460380eeb03c31c6b2 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 15:00:33 -0800 Subject: [PATCH 050/421] Added zero-hop contact sharing functionality and related UI updates --- lib/connector/meshcore_protocol.dart | 9 +++ lib/l10n/app_en.arb | 8 +- lib/screens/contacts_screen.dart | 105 ++++++++++++++++++++++----- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index df07758..64a9963 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -755,4 +755,13 @@ Uint8List buildImportContactFrame(String contactFrame) { writer.writeByte(cmdImportContact); writer.writeHex(contactFrame); return writer.toBytes(); +} + +// Build a export contact frame +// [cmd][pub_key x32] +Uint8List buildZeroHopContact(Uint8List pubKey) { + final writer = BufferWriter(); + writer.writeByte(cmdShareContact); + writer.writeBytes(pubKey); + return writer.toBytes(); } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1203b20..ba6afc4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1338,5 +1338,11 @@ "contacts_zeroHopAdvert":"Zero Hop Advert", "contacts_floodAdvert":"Flood Advert", "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", - "contacts_addContactFromClipboard":"Add Contact from Clipboard" + "contacts_addContactFromClipboard":"Add Contact from Clipboard", + "contacts_ShareContact": "Copy contact to Clipboard", + "contacts_ShareContactZeroHop": "Share contact by advert", + "contacts_zeroHopContactAdvertSent": "Sent contact by advert.", + "contacts_zeroHopContactAdvertFailed": "Failed to send contact.", + "contacts_contactAdvertCopied": "Advert copied to Clipboard.", + "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 59a8d05..99bad87 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -55,6 +55,9 @@ class _ContactsScreenState extends State Timer? _searchDebounce; bool _imported = false; + bool _zeroHopContact = false; + bool _copyedContact = false; + StreamSubscription? _frameSubscription; @override @@ -98,20 +101,53 @@ class _ContactsScreenState extends State Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } - if(code == respCodeOk && _imported) { + if(code == respCodeOk) { // Show a snackbar indicating success + if(_imported && mounted){ + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImported)), + ); + } + + if(_zeroHopContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertSent)), + ); + } + + if(_copyedContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), + ); + } + + _copyedContact = false; + _zeroHopContact = false; _imported = false; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImported)), - ); } - if(code == respCodeErr && _imported) { - // Show a snackbar indicating success + if(code == respCodeErr) { + // Show a snackbar indicating failure + if(_imported && mounted){ + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), + ); + } + + if(_zeroHopContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertFailed)), + ); + } + if(_copyedContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactAdvertCopyFailed)), + ); + } + + _copyedContact = false; _imported = false; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), - ); + _zeroHopContact = false; } }); @@ -120,35 +156,49 @@ class _ContactsScreenState extends State Future _contactExport(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); + _copyedContact = true; await connector.sendFrame(exportContactFrame); return; } + Future _contactZeroHop(Uint8List pubKey) async { + final connector = Provider.of(context, listen: false); + final exportContactZeroHopFrame = buildZeroHopContact(pubKey); + _zeroHopContact = true; + await connector.sendFrame(exportContactZeroHopFrame); + } + Future _contactImport() async { + final connector = Provider.of(context, listen: false); final clipboardData = await Clipboard.getData('text/plain'); if (clipboardData == null || clipboardData.text == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), - ); + if(mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), + ); + } return; } final text = clipboardData.text!.trim(); if (!text.startsWith('meshcore://')) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), - ); + if(mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } return; } final hexString = text.substring('meshcore://'.length); try { - final connector = Provider.of(context, listen: false); final importContactFrame = buildImportContactFrame(hexString); await connector.sendFrame(importContactFrame); _imported = true; } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), - ); + if(mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } } } @@ -977,6 +1027,22 @@ class _ContactsScreenState extends State }, ), ], + ListTile( + leading: const Icon(Icons.copy), + title: Text(context.l10n.contacts_ShareContact), + onTap: () { + Navigator.pop(sheetContext); + _contactExport(contact.publicKey); + }, + ), + ListTile( + leading: const Icon(Icons.connect_without_contact), + title: Text(context.l10n.contacts_ShareContactZeroHop), + onTap: () { + Navigator.pop(sheetContext); + _contactZeroHop(contact.publicKey); + }, + ), ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: Text( @@ -988,7 +1054,6 @@ class _ContactsScreenState extends State _confirmDelete(context, connector, contact); }, ), - ], ], ), ), From 33680f0cb99e595d077aa3b6d208b2f3bda2a1c8 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 15:25:34 -0800 Subject: [PATCH 051/421] Replace action buttons with a popup menu for better UI/UX on channels and map screens --- lib/screens/channels_screen.dart | 56 ++++++++++++++++++++++---------- lib/screens/map_screen.dart | 39 ++++++++++++++-------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index c302fb3..efd7340 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -121,24 +121,44 @@ class _ChannelsScreenState extends State centerTitle: true, automaticallyImplyLeading: false, actions: [ - if (_communities.isNotEmpty) - IconButton( - icon: const Icon(Icons.groups), - tooltip: context.l10n.community_manageCommunities, - onPressed: () => _showManageCommunitiesDialog(context), - ), - IconButton( - icon: const Icon(Icons.bluetooth_disabled), - tooltip: context.l10n.common_disconnect, - onPressed: () => _disconnect(context), - ), - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.common_settings, - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.common_disconnect), + ], + ), + onTap: () => _disconnect(context), + ), + if (_communities.isNotEmpty) + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.groups), + const SizedBox(width: 8), + Text(context.l10n.community_manageCommunities), + ], + ), + onTap: () => _showManageCommunitiesDialog(context), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 8), + Text(context.l10n.settings_title), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + ], + icon: const Icon(Icons.more_vert), ), ], ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 74e5cf9..edef811 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -245,20 +245,33 @@ class _MapScreenState extends State { centerTitle: true, automaticallyImplyLeading: false, actions: [ - IconButton( - icon: const Icon(Icons.bluetooth_disabled), - tooltip: context.l10n.common_disconnect, - onPressed: () => _disconnect(context, connector), - ), - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.common_settings, - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.common_disconnect), + ], + ), + onTap: () => _disconnect(context, connector), ), - ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 8), + Text(context.l10n.settings_title), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + ], + icon: const Icon(Icons.more_vert), ), ], ), From 6d7d51f0a4cd1c5f44cd7b425540d5602fb4cd91 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 16:03:05 -0800 Subject: [PATCH 052/421] _requestDeviceInfo added isConnected not already _awaitingSelfInfo --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d5a36e3..3a36a92 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -990,6 +990,7 @@ class MeshCoreConnector extends ChangeNotifier { } Future _requestDeviceInfo() async { + if (!isConnected || _awaitingSelfInfo) return; _awaitingSelfInfo = true; await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildAppStartFrame()); From 4f83d87f8c3fefc4775a0ae895760a03f237a0c3 Mon Sep 17 00:00:00 2001 From: 446564 Date: Sat, 31 Jan 2026 17:07:24 -0800 Subject: [PATCH 053/421] use switch for advert enable/disable move style to align with other toggles and use a switch instead of a checkbox --- lib/screens/repeater_settings_screen.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 6c1e85f..018caef 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -1203,11 +1203,11 @@ class _RepeaterSettingsScreenState extends State { subtitle: Text( l10n.repeater_localAdvertIntervalMinutes(_advertInterval), ), - trailing: Checkbox( + trailing: Switch( value: _advertEnable, onChanged: (value) { setState(() { - _advertInterval = value! ? 60 : 0; + _advertInterval = value ? 60 : 0; _advertEnable = value; }); _markChanged(); @@ -1237,11 +1237,11 @@ class _RepeaterSettingsScreenState extends State { subtitle: Text( l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval), ), - trailing: Checkbox( + trailing: Switch( value: _floodAdvertEnable, onChanged: (value) { setState(() { - _floodAdvertInterval = value! ? 3 : 0; + _floodAdvertInterval = value ? 3 : 0; _floodAdvertEnable = value; }); _markChanged(); From 8d8b938878fef633980a1e7cf6893f3b704b9e94 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 22:19:01 -0800 Subject: [PATCH 054/421] Ran translation script --- lib/l10n/app_bg.arb | 18 +++++++++++++++++- lib/l10n/app_de.arb | 18 +++++++++++++++++- lib/l10n/app_es.arb | 18 +++++++++++++++++- lib/l10n/app_fr.arb | 18 +++++++++++++++++- lib/l10n/app_it.arb | 18 +++++++++++++++++- lib/l10n/app_nl.arb | 18 +++++++++++++++++- lib/l10n/app_pl.arb | 18 +++++++++++++++++- lib/l10n/app_pt.arb | 18 +++++++++++++++++- lib/l10n/app_ru.arb | 17 ++++++++++++++++- lib/l10n/app_sk.arb | 18 +++++++++++++++++- lib/l10n/app_sl.arb | 18 +++++++++++++++++- lib/l10n/app_sv.arb | 18 +++++++++++++++++- lib/l10n/app_uk.arb | 17 ++++++++++++++++- lib/l10n/app_zh.arb | 18 +++++++++++++++++- 14 files changed, 236 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 958d963..460bc9d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1552,5 +1552,21 @@ "contacts_chatTraceRoute": "Трасиране на път", "contacts_roomPathTrace": "Трасиране на път до съ", "contacts_roomPing": "Ping на сървъра на стаята", - "contacts_pathTraceTo": "Проследи маршрут към {name}" + "contacts_pathTraceTo": "Проследи маршрут към {name}", + "appSettings_languageUk": "Украински", + "contacts_clipboardEmpty": "Клипборда е празна.", + "contacts_invalidAdvertFormat": "Невалидни данни за контакт", + "appSettings_languageRu": "Руски", + "contacts_contactImported": "Контактът е импортиран.", + "contacts_zeroHopAdvert": "Реклама без скок", + "contacts_contactImportFailed": "Контактът не е успешно импортиран.", + "contacts_floodAdvert": "Потопна реклама", + "contacts_addContactFromClipboard": "Добави контакт от клипборда", + "contacts_copyAdvertToClipboard": "Копирай обявата в клипборда", + "contacts_ShareContact": "Копирай контакт в клипборда", + "contacts_ShareContactZeroHop": "Сподели контакт чрез обява", + "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", + "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", + "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2586877..0a72559 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Pfadverfolgung zum Raumserver", "contacts_roomPing": "Raumserver anpingen", "contacts_pathTraceTo": "Route nach {name} verfolgen", - "contacts_chatTraceRoute": "Pfadverfolgungsroute" + "contacts_chatTraceRoute": "Pfadverfolgungsroute", + "appSettings_languageRu": "Russisch", + "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", + "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", + "appSettings_languageUk": "Ukrainisch", + "contacts_contactImported": "Kontakt wurde importiert.", + "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", + "contacts_zeroHopAdvert": "Zero-Hop-Anzeige", + "contacts_floodAdvert": "Überflutungsanzeige", + "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", + "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", + "contacts_copyAdvertToClipboard": "Werbung in die Zwischenablage kopieren", + "contacts_ShareContact": "Kontakt in die Zwischenablage kopieren", + "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", + "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", + "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen." } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1cdfb7b..c6dad1f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1552,5 +1552,21 @@ "contacts_roomPing": "Pingar servidor de sala", "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", "contacts_pathTraceTo": "Rastrear ruta a {name}", - "contacts_chatTraceRoute": "Ruta de trazado" + "contacts_chatTraceRoute": "Ruta de trazado", + "appSettings_languageUk": "Ucraniano", + "contacts_clipboardEmpty": "El portapapeles está vacío.", + "appSettings_languageRu": "Ruso", + "contacts_invalidAdvertFormat": "Datos de contacto no válidos", + "contacts_floodAdvert": "Anuncio de inundación", + "contacts_contactImported": "El contacto ha sido importado.", + "contacts_contactImportFailed": "Contacto no se importó correctamente.", + "contacts_zeroHopAdvert": "Anuncio de Zero Hop", + "contacts_ShareContactZeroHop": "Compartir contacto por anuncio", + "contacts_ShareContact": "Copiar contacto al Portapapeles", + "contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles", + "contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles", + "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", + "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", + "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 88c65d6..c1157ed 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1552,5 +1552,21 @@ "contacts_chatTraceRoute": "Tracer le chemin", "contacts_pathTraceTo": "Tracer l'itinéraire vers {name}", "contacts_ping": "Ping", - "contacts_roomPing": "Pinguer le serveur de la salle" + "contacts_roomPing": "Pinguer le serveur de la salle", + "contacts_invalidAdvertFormat": "Données de contact non valides", + "appSettings_languageUk": "Ukrainien", + "appSettings_languageRu": "Russe", + "contacts_clipboardEmpty": "Le presse-papiers est vide.", + "contacts_contactImported": "Le contact a été importé.", + "contacts_floodAdvert": "Annonce de crue", + "contacts_contactImportFailed": "Échec de l'importation du contact.", + "contacts_zeroHopAdvert": "Annonce Zero Hop", + "contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers", + "contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers", + "contacts_ShareContact": "Copier le contact dans le presse-papiers", + "contacts_ShareContactZeroHop": "Partager un contact par annonce", + "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", + "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", + "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index acd440b..c32e863 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1552,5 +1552,21 @@ "contacts_repeaterPing": "Ripetitore ping", "contacts_pathTraceTo": "Traccia percorso verso {name}", "contacts_roomPing": "Ping al server della stanza", - "contacts_chatTraceRoute": "Traccia percorso path" + "contacts_chatTraceRoute": "Traccia percorso path", + "appSettings_languageRu": "Russo", + "contacts_invalidAdvertFormat": "Dati di contatto non validi", + "appSettings_languageUk": "Ucraino", + "contacts_zeroHopAdvert": "Annuncio Zero Hop", + "contacts_floodAdvert": "Annuncio alluvionale", + "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", + "contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard", + "contacts_clipboardEmpty": "La clipboard è vuota.", + "contacts_ShareContact": "Copia contatto negli Appunti", + "contacts_contactImported": "Il contatto è stato importato.", + "contacts_contactImportFailed": "Contatto non importato con successo.", + "contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.", + "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", + "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", + "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti." } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index b28d668..e94deb3 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Padtrace naar room server", "contacts_roomPing": "Ping kamer server", "contacts_chatTraceRoute": "Route traceren", - "contacts_pathTraceTo": "Trace route to {name}" + "contacts_pathTraceTo": "Trace route to {name}", + "appSettings_languageUk": "Oekraïens", + "contacts_invalidAdvertFormat": "Ongeldige contactgegevens", + "contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.", + "contacts_zeroHopAdvert": "Zero Hop Reclame", + "contacts_floodAdvert": "Overstromingsadvertentie", + "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", + "appSettings_languageRu": "Russisch", + "contacts_clipboardEmpty": "Knipbord is leeg.", + "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", + "contacts_contactImported": "Contact is geïmporteerd.", + "contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie", + "contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.", + "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", + "contacts_ShareContact": "Kontakt naar Klembord kopiëren", + "contacts_ShareContactZeroHop": "Contact delen via advertentie", + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8070ac3..44552c3 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1552,5 +1552,21 @@ "pathTrace_refreshTooltip": "Odśwież ścieżkę.", "contacts_repeaterPing": "Repeater pingowy", "contacts_pathTraceTo": "Śledź trasę do {name}", - "contacts_chatTraceRoute": "Śledź trasę promienia" + "contacts_chatTraceRoute": "Śledź trasę promienia", + "appSettings_languageRu": "Rosyjski", + "appSettings_languageUk": "Ukraińska", + "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", + "contacts_zeroHopAdvert": "Reklama Zero Hop", + "contacts_floodAdvert": "Reklama powodziowa", + "contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka", + "contacts_clipboardEmpty": "Schowek jest pusty.", + "contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe", + "contacts_addContactFromClipboard": "Dodaj kontakt z schowka", + "contacts_contactImported": "Kontakt został zaimportowany.", + "contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.", + "contacts_contactAdvertCopied": "Reklama skopiowana do schowka.", + "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", + "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", + "contacts_ShareContact": "Kopiuj kontakt do schowka", + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6994bea..56a7f2b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Traçar caminho para o servidor da sala", "contacts_roomPing": "Pingar servidor da sala", "contacts_chatTraceRoute": "Rastrear rota do caminho", - "contacts_pathTraceTo": "Rastrear rota para {name}" + "contacts_pathTraceTo": "Rastrear rota para {name}", + "contacts_invalidAdvertFormat": "Dados de Contato Inválidos", + "contacts_clipboardEmpty": "Área de Transferência Está Vazia.", + "appSettings_languageUk": "Ucraniano", + "contacts_contactImported": "Contato foi importado.", + "contacts_zeroHopAdvert": "Anúncio Zero Hop", + "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", + "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", + "appSettings_languageRu": "Russo", + "contacts_ShareContact": "Copiar contato para Área de Transferência", + "contacts_contactImportFailed": "Contato falhou ao ser importado.", + "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", + "contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.", + "contacts_floodAdvert": "Anúncio de Inundação", + "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", + "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index f007aa7..0bca5ef 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -793,5 +793,20 @@ "contacts_roomPathTrace": "Трассировка пути к серверу комнаты", "contacts_roomPing": "Пинговать сервер комнаты", "contacts_chatTraceRoute": "Трассировка маршрута", - "contacts_pathTraceTo": "Показать маршрут к {name}" + "contacts_pathTraceTo": "Показать маршрут к {name}", + "contacts_contactImported": "Контакт был импортирован", + "contacts_contactImportFailed": "Контакт не удалось импортировать", + "contacts_invalidAdvertFormat": "Недействительные контактные данные", + "contacts_zeroHopAdvert": "Реклама Zero Hop", + "appSettings_languageUk": "Українська", + "contacts_floodAdvert": "Рекламный поток", + "contacts_clipboardEmpty": "Буфер обмена пуст.", + "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", + "contacts_ShareContact": "Копировать контакт в буфер обмена", + "contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.", + "contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.", + "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", + "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", + "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 4e66af0..d61cca6 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti", "contacts_roomPing": "Ping server miestnosti", "contacts_chatTraceRoute": "Sledovať trasu lúča", - "contacts_pathTraceTo": "Sledovať trasu k {name}" + "contacts_pathTraceTo": "Sledovať trasu k {name}", + "contacts_clipboardEmpty": "Schránka je prázdna.", + "appSettings_languageUk": "Ukrajinská", + "contacts_contactImportFailed": "Kontakt sa nepodarilo importovať.", + "contacts_zeroHopAdvert": "Inzerát Zero Hop", + "contacts_floodAdvert": "Inzerát povodní", + "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", + "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", + "appSettings_languageRu": "Ruština", + "contacts_addContactFromClipboard": "Pridať kontakt z schránky", + "contacts_contactImported": "Kontakt bol importovaný.", + "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", + "contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.", + "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", + "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", + "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", + "contacts_ShareContact": "Kopírovať kontakt do schránky" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 805621b..cbc4e3f 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Sledenje poti do strežnika sobe", "contacts_roomPing": "Ping strežnik sobe", "contacts_chatTraceRoute": "Slediti poti žarkov", - "contacts_pathTraceTo": "Trace route to {name}" + "contacts_pathTraceTo": "Trace route to {name}", + "appSettings_languageRu": "Ruščina", + "appSettings_languageUk": "Ukrajinsko", + "contacts_contactImported": "Kontakt je bil uvožen.", + "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", + "contacts_zeroHopAdvert": "Reklama brez posrednikov", + "contacts_floodAdvert": "Poplavna oglás", + "contacts_invalidAdvertFormat": "Neveljavni kontaktne podatke", + "contacts_clipboardEmpty": "Odložišče je prazno.", + "contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče", + "contacts_addContactFromClipboard": "Dodaj stik iz odložišča", + "contacts_zeroHopContactAdvertSent": "Poslano po oglasu.", + "contacts_zeroHopContactAdvertFailed": "Pošiljanje kontakta ni uspelo.", + "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", + "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", + "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", + "contacts_ShareContact": "Kopiraj stik v Odložišče" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index da017be..05f77cb 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Vägspårning till rumserver", "contacts_roomPing": "Ping rumsserver", "contacts_chatTraceRoute": "Spåra rutt", - "contacts_pathTraceTo": "Spåra rutt till {name}" + "contacts_pathTraceTo": "Spåra rutt till {name}", + "contacts_clipboardEmpty": "Urklipp är tomt.", + "appSettings_languageRu": "Ryska", + "contacts_contactImportFailed": "Kontakt kunde inte importeras.", + "contacts_zeroHopAdvert": "Reklam med nollhopp", + "contacts_floodAdvert": "Översvämningsannons", + "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", + "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", + "appSettings_languageUk": "Ukrainska", + "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", + "contacts_contactImported": "Kontakt har importerats.", + "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", + "contacts_contactAdvertCopied": "Annons kopierad till Urklipp.", + "contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.", + "contacts_ShareContact": "Kopiera kontakt till Urklipp", + "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", + "contacts_ShareContactZeroHop": "Dela kontakt via annons" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 85ce4a2..3362d40 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1553,5 +1553,20 @@ "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", "contacts_roomPing": "Пінг сервера кімнати", "contacts_chatTraceRoute": "Трасування шляху", - "contacts_pathTraceTo": "Відстежити маршрут до {name}" + "contacts_pathTraceTo": "Відстежити маршрут до {name}", + "contacts_invalidAdvertFormat": "Недійсні контактні дані", + "contacts_contactImported": "Контакт було імпортовано.", + "contacts_contactImportFailed": "Контакт не вдалося імпортувати", + "contacts_zeroHopAdvert": "Реклама без перехоплення", + "contacts_floodAdvert": "Залив реклами", + "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", + "contacts_clipboardEmpty": "Буфер обміну порожній", + "appSettings_languageRu": "Російська", + "contacts_ShareContact": "Копіювати контакт у буфер обміну", + "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", + "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", + "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", + "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", + "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5f0c797..c118b9d 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "路径追踪至房间服务器", "contacts_roomPing": "Ping 房间服务器", "contacts_chatTraceRoute": "路径追踪", - "contacts_pathTraceTo": "追踪路由到 {name}" + "contacts_pathTraceTo": "追踪路由到 {name}", + "appSettings_languageUk": "乌克兰语", + "appSettings_languageRu": "俄语", + "contacts_contactImported": "联系人已导入", + "contacts_contactImportFailed": "联系人导入失败", + "contacts_zeroHopAdvert": "零跳广告", + "contacts_floodAdvert": "洪水广告", + "contacts_clipboardEmpty": "剪贴板为空。", + "contacts_invalidAdvertFormat": "无效联系人数据", + "contacts_addContactFromClipboard": "从剪贴板添加联系人", + "contacts_zeroHopContactAdvertSent": "通过广告发送的联系人", + "contacts_zeroHopContactAdvertFailed": "发送联系人失败", + "contacts_contactAdvertCopied": "广告已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "复制广告到剪贴板失败。", + "contacts_copyAdvertToClipboard": "复制广告到剪贴板", + "contacts_ShareContactZeroHop": "通过广告分享联系人", + "contacts_ShareContact": "复制联系人到剪贴板" } From 79ffc21bd68a78272adf899663f01f7bf91951fd Mon Sep 17 00:00:00 2001 From: Zach Date: Sun, 1 Feb 2026 16:57:17 -0700 Subject: [PATCH 055/421] fix commit --- ios/Podfile.lock | 7 - lib/connector/meshcore_protocol.dart | 13 +- lib/l10n/app_en.arb | 9 +- lib/l10n/app_localizations.dart | 84 ++ lib/l10n/app_localizations_bg.dart | 50 +- lib/l10n/app_localizations_de.dart | 53 +- lib/l10n/app_localizations_en.dart | 43 + lib/l10n/app_localizations_es.dart | 50 +- lib/l10n/app_localizations_fr.dart | 54 +- lib/l10n/app_localizations_it.dart | 52 +- lib/l10n/app_localizations_nl.dart | 52 +- lib/l10n/app_localizations_pl.dart | 51 +- lib/l10n/app_localizations_pt.dart | 51 +- lib/l10n/app_localizations_ru.dart | 50 ++ lib/l10n/app_localizations_sk.dart | 50 +- lib/l10n/app_localizations_sl.dart | 49 +- lib/l10n/app_localizations_sv.dart | 49 +- lib/l10n/app_localizations_uk.dart | 51 +- lib/l10n/app_localizations_zh.dart | 966 ++++++++++++----------- lib/l10n/app_zh.arb | 1076 +++++++++++++------------- lib/screens/contacts_screen.dart | 53 +- tools/translate.py | 1059 +++++++++---------------- untranslated.json | 70 +- 23 files changed, 2211 insertions(+), 1831 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aef2502..cf8bbca 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,9 +57,6 @@ PODS: - nanopb/encode (3.30910.0) - package_info_plus (0.4.5): - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - PromisesObjC (2.4.0) - shared_preferences_foundation (0.0.1): - Flutter @@ -79,7 +76,6 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -112,8 +108,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/mobile_scanner/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: @@ -140,7 +134,6 @@ SPEC CHECKSUMS: mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 64a9963..dfe787e 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -104,9 +104,18 @@ class BufferWriter { } void writeHex(String hex) { + // Validate hex string length is even and not empty + if (hex.isEmpty || hex.length % 2 != 0) { + throw FormatException('Invalid hex string length: ${hex.length}'); + } List result = []; for (int i = 0; i < hex.length ~/ 2; i++) { - result.add(int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)); + final hexByte = hex.substring(i * 2, i * 2 + 2); + final byte = int.tryParse(hexByte, radix: 16); + if (byte == null) { + throw FormatException('Invalid hex characters at position $i: $hexByte'); + } + result.add(byte); } writeBytes(Uint8List.fromList(result)); } @@ -764,4 +773,4 @@ Uint8List buildZeroHopContact(Uint8List pubKey) { writer.writeByte(cmdShareContact); writer.writeBytes(pubKey); return writer.toBytes(); -} \ No newline at end of file +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ba6afc4..ee5cf7d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1316,7 +1316,6 @@ "pathTrace_failed": "Path trace failed.", "pathTrace_notAvailable": "Path trace not available.", "pathTrace_refreshTooltip": "Refresh Path Trace.", - "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", @@ -1331,10 +1330,10 @@ } }, - "contacts_clipboardEmpty": "Clipboard Is Empty.", - "contacts_invalidAdvertFormat": "Invalid Contact Data", - "contacts_contactImported": "Contact has been Imported.", - "contacts_contactImportFailed": "Contact Failed to Imported.", + "contacts_clipboardEmpty": "Clipboard is empty.", + "contacts_invalidAdvertFormat": "Invalid contact data", + "contacts_contactImported": "Contact has been imported.", + "contacts_contactImportFailed": "Failed to import contact.", "contacts_zeroHopAdvert":"Zero Hop Advert", "contacts_floodAdvert":"Flood Advert", "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ac3eb99..055667f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4771,6 +4771,90 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Trace route to {name}'** String contacts_pathTraceTo(String name); + + /// No description provided for @contacts_clipboardEmpty. + /// + /// In en, this message translates to: + /// **'Clipboard is empty.'** + String get contacts_clipboardEmpty; + + /// No description provided for @contacts_invalidAdvertFormat. + /// + /// In en, this message translates to: + /// **'Invalid contact data'** + String get contacts_invalidAdvertFormat; + + /// No description provided for @contacts_contactImported. + /// + /// In en, this message translates to: + /// **'Contact has been imported.'** + String get contacts_contactImported; + + /// No description provided for @contacts_contactImportFailed. + /// + /// In en, this message translates to: + /// **'Failed to import contact.'** + String get contacts_contactImportFailed; + + /// No description provided for @contacts_zeroHopAdvert. + /// + /// In en, this message translates to: + /// **'Zero Hop Advert'** + String get contacts_zeroHopAdvert; + + /// No description provided for @contacts_floodAdvert. + /// + /// In en, this message translates to: + /// **'Flood Advert'** + String get contacts_floodAdvert; + + /// No description provided for @contacts_copyAdvertToClipboard. + /// + /// In en, this message translates to: + /// **'Copy Advert to Clipboard'** + String get contacts_copyAdvertToClipboard; + + /// No description provided for @contacts_addContactFromClipboard. + /// + /// In en, this message translates to: + /// **'Add Contact from Clipboard'** + String get contacts_addContactFromClipboard; + + /// No description provided for @contacts_ShareContact. + /// + /// In en, this message translates to: + /// **'Copy contact to Clipboard'** + String get contacts_ShareContact; + + /// No description provided for @contacts_ShareContactZeroHop. + /// + /// In en, this message translates to: + /// **'Share contact by advert'** + String get contacts_ShareContactZeroHop; + + /// No description provided for @contacts_zeroHopContactAdvertSent. + /// + /// In en, this message translates to: + /// **'Sent contact by advert.'** + String get contacts_zeroHopContactAdvertSent; + + /// No description provided for @contacts_zeroHopContactAdvertFailed. + /// + /// In en, this message translates to: + /// **'Failed to send contact.'** + String get contacts_zeroHopContactAdvertFailed; + + /// No description provided for @contacts_contactAdvertCopied. + /// + /// In en, this message translates to: + /// **'Advert copied to Clipboard.'** + String get contacts_contactAdvertCopied; + + /// No description provided for @contacts_contactAdvertCopyFailed. + /// + /// In en, this message translates to: + /// **'Copying advert to Clipboard failed.'** + String get contacts_contactAdvertCopyFailed; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 27b2007..701429e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -451,10 +451,10 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Руски'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Украински'; @override String get appSettings_notifications => 'Уведомления'; @@ -2720,4 +2720,50 @@ class AppLocalizationsBg extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Проследи маршрут към $name'; } + + @override + String get contacts_clipboardEmpty => 'Клипборда е празна.'; + + @override + String get contacts_invalidAdvertFormat => 'Невалидни данни за контакт'; + + @override + String get contacts_contactImported => 'Контактът е импортиран.'; + + @override + String get contacts_contactImportFailed => + 'Контактът не е успешно импортиран.'; + + @override + String get contacts_zeroHopAdvert => 'Реклама без скок'; + + @override + String get contacts_floodAdvert => 'Потопна реклама'; + + @override + String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда'; + + @override + String get contacts_addContactFromClipboard => 'Добави контакт от клипборда'; + + @override + String get contacts_ShareContact => 'Копирай контакт в клипборда'; + + @override + String get contacts_ShareContactZeroHop => 'Сподели контакт чрез обява'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Изпратен контакт по обява.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Неуспешно изпращане на контакт.'; + + @override + String get contacts_contactAdvertCopied => + 'Рекламата е копирана в клипборда.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Копирането на обявата в клипборда не успя.'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 69e6a59..514a7a1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -445,10 +445,10 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russisch'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrainisch'; @override String get appSettings_notifications => 'Benachrichtigungen'; @@ -2724,4 +2724,53 @@ class AppLocalizationsDe extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Route nach $name verfolgen'; } + + @override + String get contacts_clipboardEmpty => 'Die Zwischenablage ist leer.'; + + @override + String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten'; + + @override + String get contacts_contactImported => 'Kontakt wurde importiert.'; + + @override + String get contacts_contactImportFailed => + 'Kontakt konnte nicht importiert werden'; + + @override + String get contacts_zeroHopAdvert => 'Zero-Hop-Anzeige'; + + @override + String get contacts_floodAdvert => 'Überflutungsanzeige'; + + @override + String get contacts_copyAdvertToClipboard => + 'Werbung in die Zwischenablage kopieren'; + + @override + String get contacts_addContactFromClipboard => + 'Kontakt aus Zwischenablage hinzufügen'; + + @override + String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren'; + + @override + String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Kontakt über Anzeige gesendet'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Kontakt konnte nicht gesendet werden.'; + + @override + String get contacts_contactAdvertCopied => + 'Anzeige in die Zwischenablage kopiert.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a609dd8..040d809 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2680,4 +2680,47 @@ class AppLocalizationsEn extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Trace route to $name'; } + + @override + String get contacts_clipboardEmpty => 'Clipboard is empty.'; + + @override + String get contacts_invalidAdvertFormat => 'Invalid contact data'; + + @override + String get contacts_contactImported => 'Contact has been imported.'; + + @override + String get contacts_contactImportFailed => 'Failed to import contact.'; + + @override + String get contacts_zeroHopAdvert => 'Zero Hop Advert'; + + @override + String get contacts_floodAdvert => 'Flood Advert'; + + @override + String get contacts_copyAdvertToClipboard => 'Copy Advert to Clipboard'; + + @override + String get contacts_addContactFromClipboard => 'Add Contact from Clipboard'; + + @override + String get contacts_ShareContact => 'Copy contact to Clipboard'; + + @override + String get contacts_ShareContactZeroHop => 'Share contact by advert'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Sent contact by advert.'; + + @override + String get contacts_zeroHopContactAdvertFailed => 'Failed to send contact.'; + + @override + String get contacts_contactAdvertCopied => 'Advert copied to Clipboard.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Copying advert to Clipboard failed.'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 28d3e9d..e65cbcd 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -448,10 +448,10 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ruso'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ucraniano'; @override String get appSettings_notifications => 'Notificaciones'; @@ -2720,4 +2720,50 @@ class AppLocalizationsEs extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Rastrear ruta a $name'; } + + @override + String get contacts_clipboardEmpty => 'El portapapeles está vacío.'; + + @override + String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos'; + + @override + String get contacts_contactImported => 'El contacto ha sido importado.'; + + @override + String get contacts_contactImportFailed => + 'Contacto no se importó correctamente.'; + + @override + String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop'; + + @override + String get contacts_floodAdvert => 'Anuncio de inundación'; + + @override + String get contacts_copyAdvertToClipboard => 'Copiar anuncio al portapapeles'; + + @override + String get contacts_addContactFromClipboard => + 'Agregar contacto desde el portapapeles'; + + @override + String get contacts_ShareContact => 'Copiar contacto al Portapapeles'; + + @override + String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'No se pudo enviar el contacto.'; + + @override + String get contacts_contactAdvertCopied => 'Anuncio copiado al Portapapeles.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Copiar anuncio al Portapapeles ha fallado.'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ce6f6a9..4496fc8 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -449,10 +449,10 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russe'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrainien'; @override String get appSettings_notifications => 'Notifications'; @@ -2737,4 +2737,54 @@ class AppLocalizationsFr extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Tracer l\'itinéraire vers $name'; } + + @override + String get contacts_clipboardEmpty => 'Le presse-papiers est vide.'; + + @override + String get contacts_invalidAdvertFormat => 'Données de contact non valides'; + + @override + String get contacts_contactImported => 'Le contact a été importé.'; + + @override + String get contacts_contactImportFailed => + 'Échec de l\'importation du contact.'; + + @override + String get contacts_zeroHopAdvert => 'Annonce Zero Hop'; + + @override + String get contacts_floodAdvert => 'Annonce de crue'; + + @override + String get contacts_copyAdvertToClipboard => + 'Copier l\'annonce dans le presse-papiers'; + + @override + String get contacts_addContactFromClipboard => + 'Ajouter un contact depuis le presse-papiers'; + + @override + String get contacts_ShareContact => + 'Copier le contact dans le presse-papiers'; + + @override + String get contacts_ShareContactZeroHop => 'Partager un contact par annonce'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Envoyer un contact par annonce.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Échec de l\'envoi du contact.'; + + @override + String get contacts_contactAdvertCopied => + 'Annonce copiée dans le presse-papiers.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'La copie de l\'annonce vers le presse-papiers a échoué.'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a7ac6a6..02345c4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -447,10 +447,10 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russo'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ucraino'; @override String get appSettings_notifications => 'Notifiche'; @@ -2721,4 +2721,52 @@ class AppLocalizationsIt extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Traccia percorso verso $name'; } + + @override + String get contacts_clipboardEmpty => 'La clipboard è vuota.'; + + @override + String get contacts_invalidAdvertFormat => 'Dati di contatto non validi'; + + @override + String get contacts_contactImported => 'Il contatto è stato importato.'; + + @override + String get contacts_contactImportFailed => + 'Contatto non importato con successo.'; + + @override + String get contacts_zeroHopAdvert => 'Annuncio Zero Hop'; + + @override + String get contacts_floodAdvert => 'Annuncio alluvionale'; + + @override + String get contacts_copyAdvertToClipboard => 'Copia Annuncio negli Appunti'; + + @override + String get contacts_addContactFromClipboard => + 'Aggiungere contatto dalla clipboard'; + + @override + String get contacts_ShareContact => 'Copia contatto negli Appunti'; + + @override + String get contacts_ShareContactZeroHop => + 'Condividi contatto tramite annuncio'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Inviato contatto tramite annuncio.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Invio del contatto non riuscito.'; + + @override + String get contacts_contactAdvertCopied => 'Annuncio copiato negli Appunti.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Copia dell\'annuncio nella Clipboard non riuscita.'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index b55dc41..292181f 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -445,10 +445,10 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russisch'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Oekraïens'; @override String get appSettings_notifications => 'Notificaties'; @@ -2710,4 +2710,52 @@ class AppLocalizationsNl extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Trace route to $name'; } + + @override + String get contacts_clipboardEmpty => 'Knipbord is leeg.'; + + @override + String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens'; + + @override + String get contacts_contactImported => 'Contact is geïmporteerd.'; + + @override + String get contacts_contactImportFailed => + 'Contact kon niet geïmporteerd worden.'; + + @override + String get contacts_zeroHopAdvert => 'Zero Hop Reclame'; + + @override + String get contacts_floodAdvert => 'Overstromingsadvertentie'; + + @override + String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren'; + + @override + String get contacts_addContactFromClipboard => + 'Contact uit klembord toevoegen'; + + @override + String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren'; + + @override + String get contacts_ShareContactZeroHop => 'Contact delen via advertentie'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Contact verzonden via advertentie'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Mislukt om contact te verzenden'; + + @override + String get contacts_contactAdvertCopied => + 'Reclame gekopieerd naar Klembord.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiëren van advertentie naar Clipboard is mislukt.'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 0f7a704..0832329 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -449,10 +449,10 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Rosyjski'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukraińska'; @override String get appSettings_notifications => 'Powiadomienia'; @@ -2719,4 +2719,51 @@ class AppLocalizationsPl extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Śledź trasę do $name'; } + + @override + String get contacts_clipboardEmpty => 'Schowek jest pusty.'; + + @override + String get contacts_invalidAdvertFormat => 'Nieprawidłowe dane kontaktowe'; + + @override + String get contacts_contactImported => 'Kontakt został zaimportowany.'; + + @override + String get contacts_contactImportFailed => + 'Kontakt nie został zaimportowany.'; + + @override + String get contacts_zeroHopAdvert => 'Reklama Zero Hop'; + + @override + String get contacts_floodAdvert => 'Reklama powodziowa'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka'; + + @override + String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka'; + + @override + String get contacts_ShareContact => 'Kopiuj kontakt do schowka'; + + @override + String get contacts_ShareContactZeroHop => + 'Udostępnij kontakt przez ogłoszenie'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Wysłano kontakt przez ogłoszenie.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Nie udało się wysłać kontaktu.'; + + @override + String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5c25276..eadea3b 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -449,10 +449,10 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russo'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ucraniano'; @override String get appSettings_notifications => 'Notificações'; @@ -2721,4 +2721,51 @@ class AppLocalizationsPt extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Rastrear rota para $name'; } + + @override + String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.'; + + @override + String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos'; + + @override + String get contacts_contactImported => 'Contato foi importado.'; + + @override + String get contacts_contactImportFailed => 'Contato falhou ao ser importado.'; + + @override + String get contacts_zeroHopAdvert => 'Anúncio Zero Hop'; + + @override + String get contacts_floodAdvert => 'Anúncio de Inundação'; + + @override + String get contacts_copyAdvertToClipboard => + 'Copiar Anúncio para Área de Transferência'; + + @override + String get contacts_addContactFromClipboard => + 'Adicionar Contato da Área de Transferência'; + + @override + String get contacts_ShareContact => + 'Copiar contato para Área de Transferência'; + + @override + String get contacts_ShareContactZeroHop => 'Compartilhar contato por anúncio'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Enviou contato por anúncio.'; + + @override + String get contacts_zeroHopContactAdvertFailed => 'Falha ao enviar contato.'; + + @override + String get contacts_contactAdvertCopied => + 'Anúncio copiado para a Área de Transferência.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Cópia do anúncio para a Área de Transferência falhou.'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index a944fab..ec0f1ba 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2723,4 +2723,54 @@ class AppLocalizationsRu extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Показать маршрут к $name'; } + + @override + String get contacts_clipboardEmpty => 'Буфер обмена пуст.'; + + @override + String get contacts_invalidAdvertFormat => + 'Недействительные контактные данные'; + + @override + String get contacts_contactImported => 'Контакт был импортирован'; + + @override + String get contacts_contactImportFailed => 'Контакт не удалось импортировать'; + + @override + String get contacts_zeroHopAdvert => 'Реклама Zero Hop'; + + @override + String get contacts_floodAdvert => 'Рекламный поток'; + + @override + String get contacts_copyAdvertToClipboard => + 'Копировать рекламу в буфер обмена'; + + @override + String get contacts_addContactFromClipboard => + 'Добавить контакт из буфера обмена'; + + @override + String get contacts_ShareContact => 'Копировать контакт в буфер обмена'; + + @override + String get contacts_ShareContactZeroHop => + 'Поделиться контактом по объявлению'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Отправлено сообщение по объявлению.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Не удалось отправить контакт.'; + + @override + String get contacts_contactAdvertCopied => + 'Реклама скопирована в буфер обмена.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Копирование рекламы в буфер обмена не удалось.'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 02f2b62..346047b 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -445,10 +445,10 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ruština'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrajinská'; @override String get appSettings_notifications => 'Upozornenia'; @@ -2706,4 +2706,50 @@ class AppLocalizationsSk extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Sledovať trasu k $name'; } + + @override + String get contacts_clipboardEmpty => 'Schránka je prázdna.'; + + @override + String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje'; + + @override + String get contacts_contactImported => 'Kontakt bol importovaný.'; + + @override + String get contacts_contactImportFailed => + 'Kontakt sa nepodarilo importovať.'; + + @override + String get contacts_zeroHopAdvert => 'Inzerát Zero Hop'; + + @override + String get contacts_floodAdvert => 'Inzerát povodní'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopírovať reklamu do schránky'; + + @override + String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky'; + + @override + String get contacts_ShareContact => 'Kopírovať kontakt do schránky'; + + @override + String get contacts_ShareContactZeroHop => 'Zdieľať kontakt cez inzerát'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Poslal kontakt cez inzerát.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Zlyhalo odoslanie kontaktu.'; + + @override + String get contacts_contactAdvertCopied => + 'Inzerát bol skopírovaný do schránky.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopírovanie inzerátu do schránky zlyhalo.'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 21d7b6f..ed71122 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -444,10 +444,10 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ruščina'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrajinsko'; @override String get appSettings_notifications => 'Obvestila'; @@ -2709,4 +2709,49 @@ class AppLocalizationsSl extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Trace route to $name'; } + + @override + String get contacts_clipboardEmpty => 'Odložišče je prazno.'; + + @override + String get contacts_invalidAdvertFormat => 'Neveljavni kontaktne podatke'; + + @override + String get contacts_contactImported => 'Kontakt je bil uvožen.'; + + @override + String get contacts_contactImportFailed => 'Kontakt ni bil uspešno uvožen.'; + + @override + String get contacts_zeroHopAdvert => 'Reklama brez posrednikov'; + + @override + String get contacts_floodAdvert => 'Poplavna oglás'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopiraj oglas v odložišče'; + + @override + String get contacts_addContactFromClipboard => 'Dodaj stik iz odložišča'; + + @override + String get contacts_ShareContact => 'Kopiraj stik v Odložišče'; + + @override + String get contacts_ShareContactZeroHop => 'Deliti kontakt prek oglasa'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Poslano po oglasu.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Pošiljanje kontakta ni uspelo.'; + + @override + String get contacts_contactAdvertCopied => + 'Oglas je bil kopiran v odložišče.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiranje oglasa v odložišče je spodletelo.'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a96d7dc..97b849f 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -442,10 +442,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ryska'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrainska'; @override String get appSettings_notifications => 'Meddelanden'; @@ -2694,4 +2694,49 @@ class AppLocalizationsSv extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Spåra rutt till $name'; } + + @override + String get contacts_clipboardEmpty => 'Urklipp är tomt.'; + + @override + String get contacts_invalidAdvertFormat => 'Ogiltiga kontaktuppgifter'; + + @override + String get contacts_contactImported => 'Kontakt har importerats.'; + + @override + String get contacts_contactImportFailed => 'Kontakt kunde inte importeras.'; + + @override + String get contacts_zeroHopAdvert => 'Reklam med nollhopp'; + + @override + String get contacts_floodAdvert => 'Översvämningsannons'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopiera annons till urklipp'; + + @override + String get contacts_addContactFromClipboard => + 'Lägg till kontakt från urklipp'; + + @override + String get contacts_ShareContact => 'Kopiera kontakt till Urklipp'; + + @override + String get contacts_ShareContactZeroHop => 'Dela kontakt via annons'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Skickat kontakt via annons.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Misslyckades med att skicka kontakt.'; + + @override + String get contacts_contactAdvertCopied => 'Annons kopierad till Urklipp.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiering av annons till Urklipp misslyckades.'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 6107c5b..899d540 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -447,7 +447,7 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Російська'; @override String get appSettings_languageUk => 'Українська'; @@ -2730,4 +2730,53 @@ class AppLocalizationsUk extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Відстежити маршрут до $name'; } + + @override + String get contacts_clipboardEmpty => 'Буфер обміну порожній'; + + @override + String get contacts_invalidAdvertFormat => 'Недійсні контактні дані'; + + @override + String get contacts_contactImported => 'Контакт було імпортовано.'; + + @override + String get contacts_contactImportFailed => 'Контакт не вдалося імпортувати'; + + @override + String get contacts_zeroHopAdvert => 'Реклама без перехоплення'; + + @override + String get contacts_floodAdvert => 'Залив реклами'; + + @override + String get contacts_copyAdvertToClipboard => + 'Копіювати оголошення в буфер обміну'; + + @override + String get contacts_addContactFromClipboard => + 'Додати контакт з буфера обміну'; + + @override + String get contacts_ShareContact => 'Копіювати контакт у буфер обміну'; + + @override + String get contacts_ShareContactZeroHop => + 'Поділитися контактом за оголошенням'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Відправлено контакт за оголошенням'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Не вдалося надіслати контакт.'; + + @override + String get contacts_contactAdvertCopied => + 'Рекламу скопійовано до буфера обміну.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Копіювання оголошення в буфер обміну завершилося невдало'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index c10a745..7746792 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -12,7 +12,7 @@ class AppLocalizationsZh extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => '联系人'; + String get nav_contacts => '联系方式'; @override String get nav_channels => '频道'; @@ -54,13 +54,13 @@ class AppLocalizationsZh extends AppLocalizations { String get common_disconnect => '断开'; @override - String get common_connected => '已连接'; + String get common_connected => '连接'; @override String get common_disconnected => '断开'; @override - String get common_create => '创建'; + String get common_create => '创造'; @override String get common_continue => '继续'; @@ -78,7 +78,7 @@ class AppLocalizationsZh extends AppLocalizations { String get common_hide => '隐藏'; @override - String get common_remove => '删除'; + String get common_remove => '移除'; @override String get common_enable => '启用'; @@ -87,7 +87,7 @@ class AppLocalizationsZh extends AppLocalizations { String get common_disable => '禁用'; @override - String get common_reboot => '重启'; + String get common_reboot => '重新启动'; @override String get common_loading => '正在加载...'; @@ -106,34 +106,34 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get scanner_title => 'MeshCore Open'; + String get scanner_title => 'MeshCore 开放'; @override - String get scanner_scanning => '扫描设备…'; + String get scanner_scanning => '正在搜索设备...'; @override - String get scanner_connecting => '连接中...'; + String get scanner_connecting => '正在连接...'; @override - String get scanner_disconnecting => '断开中...'; + String get scanner_disconnecting => '断开连接...'; @override String get scanner_notConnected => '未连接'; @override String scanner_connectedTo(String deviceName) { - return '已连接至 $deviceName'; + return '已连接到 $deviceName'; } @override - String get scanner_searchingDevices => '搜索 MeshCore 设备...'; + String get scanner_searchingDevices => '正在搜索 MeshCore 设备...'; @override - String get scanner_tapToScan => '点击扫描以查找MeshCore设备'; + String get scanner_tapToScan => '点击“扫描”功能,以查找 MeshCore 设备。'; @override String scanner_connectionFailed(String error) { - return '连接失败:$error'; + return 'Connection failed: $error'; } @override @@ -146,7 +146,7 @@ class AppLocalizationsZh extends AppLocalizations { String get device_quickSwitch => '快速切换'; @override - String get device_meshcore => 'MeshCore'; + String get device_meshcore => '网格核心'; @override String get settings_title => '设置'; @@ -176,40 +176,40 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_nodeNameUpdated => '姓名已更新'; @override - String get settings_radioSettings => '无线设置'; + String get settings_radioSettings => '收音机设置'; @override - String get settings_radioSettingsSubtitle => '频率,功率,扩展因子'; + String get settings_radioSettingsSubtitle => '频率、功率、扩频因子'; @override - String get settings_radioSettingsUpdated => '射频设置已更新'; + String get settings_radioSettingsUpdated => '收音机设置已更新'; @override - String get settings_location => '位置'; + String get settings_location => '地点'; @override - String get settings_locationSubtitle => 'GPS坐标'; + String get settings_locationSubtitle => 'GPS 坐标'; @override - String get settings_locationUpdated => '位置已更新'; + String get settings_locationUpdated => '位置和 GPS 设置已更新'; @override - String get settings_locationBothRequired => '请输入纬度和经度。'; + String get settings_locationBothRequired => '请输入经度和纬度。'; @override - String get settings_locationInvalid => '无效的纬度或经度。'; + String get settings_locationInvalid => '无效的经度和纬度。'; @override - String get settings_locationGPSEnable => '启用GPS'; + String get settings_locationGPSEnable => '开启 GPS 功能'; @override - String get settings_locationGPSEnableSubtitle => '启用GPS自动更新位置。'; + String get settings_locationGPSEnableSubtitle => '使 GPS 能够自动更新位置。'; @override - String get settings_locationIntervalSec => 'GPS 间隔(秒)'; + String get settings_locationIntervalSec => 'GPS 间隔时间(秒)'; @override - String get settings_locationIntervalInvalid => '时间间隔必须至少为60秒,且小于86400秒。'; + String get settings_locationIntervalInvalid => '间隔时间必须至少为 60 秒,但不超过 86400 秒。'; @override String get settings_latitude => '纬度'; @@ -221,34 +221,34 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_privacyMode => '隐私模式'; @override - String get settings_privacyModeSubtitle => '隐藏在广告中的姓名/位置'; + String get settings_privacyModeSubtitle => '在广告中隐藏姓名/位置'; @override - String get settings_privacyModeToggle => '开启隐私模式以隐藏您的姓名和位置在广告中的显示。'; + String get settings_privacyModeToggle => '切换隐私模式,以隐藏您的姓名和位置,从而在广告中保护您的个人信息。'; @override String get settings_privacyModeEnabled => '隐私模式已启用'; @override - String get settings_privacyModeDisabled => '隐私模式已禁用'; + String get settings_privacyModeDisabled => '隐私模式已关闭'; @override - String get settings_actions => '操作'; + String get settings_actions => '行动'; @override - String get settings_sendAdvertisement => '发送广告'; + String get settings_sendAdvertisement => '发布广告'; @override - String get settings_sendAdvertisementSubtitle => '现在已广播'; + String get settings_sendAdvertisementSubtitle => '现已开始进行广播节目'; @override - String get settings_advertisementSent => '广告已发送'; + String get settings_advertisementSent => '已发送广告'; @override String get settings_syncTime => '同步时间'; @override - String get settings_syncTimeSubtitle => '将设备时钟设置为手机时间'; + String get settings_syncTimeSubtitle => '将设备时钟设置为与手机时间一致'; @override String get settings_timeSynchronized => '时间同步'; @@ -257,31 +257,31 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_refreshContacts => '刷新联系人'; @override - String get settings_refreshContactsSubtitle => '从设备重新加载联系人列表'; + String get settings_refreshContactsSubtitle => '从设备中重新加载联系人列表'; @override String get settings_rebootDevice => '重启设备'; @override - String get settings_rebootDeviceSubtitle => '重启 MeshCore 设备'; + String get settings_rebootDeviceSubtitle => '重新启动 MeshCore 设备'; @override - String get settings_rebootDeviceConfirm => '您确定要重启设备吗?您将会断开连接。'; + String get settings_rebootDeviceConfirm => '您确定要重启设备吗?这将导致您与设备断开连接。'; @override String get settings_debug => '调试'; @override - String get settings_bleDebugLog => '蓝牙调试日志'; + String get settings_bleDebugLog => 'BLE 调试日志'; @override - String get settings_bleDebugLogSubtitle => '蓝牙命令、响应和原始数据'; + String get settings_bleDebugLogSubtitle => 'BLE 命令、响应和原始数据'; @override - String get settings_appDebugLog => '应用调试日志'; + String get settings_appDebugLog => '应用程序调试日志'; @override - String get settings_appDebugLogSubtitle => '应用调试消息'; + String get settings_appDebugLogSubtitle => '应用程序调试消息'; @override String get settings_about => '关于'; @@ -292,11 +292,11 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get settings_aboutLegalese => '2024 MeshCore 开放源代码项目'; + String get settings_aboutLegalese => '2026 MeshCore 开源项目'; @override String get settings_aboutDescription => - '一个开源的 Flutter 客户端,用于 MeshCore LoRa 网状网络设备。'; + '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; @override String get settings_infoName => '姓名'; @@ -317,19 +317,19 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_infoContactsCount => '联系人数量'; @override - String get settings_infoChannelCount => '频道数量'; + String get settings_infoChannelCount => '通道数量'; @override String get settings_presets => '预设'; @override - String get settings_preset915Mhz => '915 MHz'; + String get settings_preset915Mhz => '915 兆赫'; @override - String get settings_preset868Mhz => '868 MHz'; + String get settings_preset868Mhz => '868 兆赫'; @override - String get settings_preset433Mhz => '433 MHz'; + String get settings_preset433Mhz => '433 兆赫'; @override String get settings_frequency => '频率 (MHz)'; @@ -338,35 +338,35 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => '无效频率 (300-2500 MHz)'; + String get settings_frequencyInvalid => '无效频率(300-2500 MHz)'; @override String get settings_bandwidth => '带宽'; @override - String get settings_spreadingFactor => '扩散因子'; + String get settings_spreadingFactor => '传播系数'; @override String get settings_codingRate => '编码速率'; @override - String get settings_txPower => 'TX Power (dBm)'; + String get settings_txPower => 'TX 功率(dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => '无效的 TX 电功率 (0-22 dBm)'; + String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; @override String get settings_longRange => '远距离'; @override - String get settings_fastSpeed => '快速速度'; + String get settings_fastSpeed => '高速'; @override String settings_error(String message) { - return '错误:$message'; + return '[保存:$message]\n错误:$message'; } @override @@ -379,64 +379,64 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_theme => '主题'; @override - String get appSettings_themeSystem => '系统默认'; + String get appSettings_themeSystem => '系统默认设置'; @override String get appSettings_themeLight => '光'; @override - String get appSettings_themeDark => '深色'; + String get appSettings_themeDark => '黑暗'; @override String get appSettings_language => '语言'; @override - String get appSettings_languageSystem => '系统默认'; + String get appSettings_languageSystem => '系统默认设置'; @override - String get appSettings_languageEn => 'English'; + String get appSettings_languageEn => '英语'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => '法语'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => '西班牙语'; @override - String get appSettings_languageDe => 'Deutsch'; + String get appSettings_languageDe => '德语'; @override - String get appSettings_languagePl => 'Polski'; + String get appSettings_languagePl => '波兰语'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => '斯洛文语'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => '葡萄牙语'; @override - String get appSettings_languageIt => 'Italiano'; + String get appSettings_languageIt => '意大利语'; @override String get appSettings_languageZh => '中文'; @override - String get appSettings_languageSv => 'Svenska'; + String get appSettings_languageSv => '瑞典语'; @override - String get appSettings_languageNl => 'Nederlands'; + String get appSettings_languageNl => '荷兰语'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => '斯洛伐克语'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => '保加利亚'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => '俄语'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => '乌克兰'; @override String get appSettings_notifications => '通知'; @@ -448,7 +448,7 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_enableNotificationsSubtitle => '接收消息和广告的通知'; @override - String get appSettings_notificationPermissionDenied => '通知权限被拒绝'; + String get appSettings_notificationPermissionDenied => '权限被拒绝'; @override String get appSettings_notificationsEnabled => '通知已启用'; @@ -460,40 +460,41 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_messageNotifications => '消息通知'; @override - String get appSettings_messageNotificationsSubtitle => '显示收到新消息时的通知'; + String get appSettings_messageNotificationsSubtitle => '在收到新消息时显示通知'; @override String get appSettings_channelMessageNotifications => '频道消息通知'; @override - String get appSettings_channelMessageNotificationsSubtitle => '显示接收频道消息时的通知'; + String get appSettings_channelMessageNotificationsSubtitle => + '在收到频道消息时,显示通知。'; @override String get appSettings_advertisementNotifications => '广告通知'; @override - String get appSettings_advertisementNotificationsSubtitle => '显示当新节点被发现时通知'; + String get appSettings_advertisementNotificationsSubtitle => '在发现新的节点时,显示通知。'; @override - String get appSettings_messaging => '消息'; + String get appSettings_messaging => '信息传递'; @override - String get appSettings_clearPathOnMaxRetry => '清除最大重试路径'; + String get appSettings_clearPathOnMaxRetry => '关于“最大重试”的清晰说明'; @override - String get appSettings_clearPathOnMaxRetrySubtitle => '重置联系人路径,在5次发送失败尝试后'; + String get appSettings_clearPathOnMaxRetrySubtitle => '在尝试发送失败后 5 次,重置联系路径。'; @override - String get appSettings_pathsWillBeCleared => '路径将在5次失败重试后清除'; + String get appSettings_pathsWillBeCleared => '如果尝试 5 次后仍然失败,则将重新规划路径。'; @override - String get appSettings_pathsWillNotBeCleared => '路径不会自动清理'; + String get appSettings_pathsWillNotBeCleared => '路径不会自动清除。'; @override - String get appSettings_autoRouteRotation => '自动路径旋转'; + String get appSettings_autoRouteRotation => '自动路径轮换'; @override - String get appSettings_autoRouteRotationSubtitle => '在最佳路径和洪水模式之间切换'; + String get appSettings_autoRouteRotationSubtitle => '在最佳路径和防洪模式之间切换'; @override String get appSettings_autoRouteRotationEnabled => '自动路径轮换已启用'; @@ -509,26 +510,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return '设置每个设备 ($deviceName)'; + return '为每个设备设置 ($deviceName)'; } @override - String get appSettings_batteryChemistryConnectFirst => '连接设备以选择'; + String get appSettings_batteryChemistryConnectFirst => '连接到设备以进行选择'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; + String get appSettings_batteryNmc => '18650 型号,NMC 电池(3.0-4.2V)'; @override String get appSettings_batteryLifepo4 => '磷酸铁锂 (2.6-3.65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; + String get appSettings_batteryLipo => '锂离子电池 (3.0-4.2V)'; @override - String get appSettings_mapDisplay => '地图显示'; + String get appSettings_mapDisplay => '地图展示'; @override - String get appSettings_showRepeaters => '显示循环器'; + String get appSettings_showRepeaters => '显示重复'; @override String get appSettings_showRepeatersSubtitle => '在地图上显示重复节点'; @@ -543,36 +544,36 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_showOtherNodes => '显示其他节点'; @override - String get appSettings_showOtherNodesSubtitle => '显示其他节点类型在地图上'; + String get appSettings_showOtherNodesSubtitle => '在地图上显示其他节点类型'; @override - String get appSettings_timeFilter => '时间筛选'; + String get appSettings_timeFilter => '时间过滤器'; @override String get appSettings_timeFilterShowAll => '显示所有节点'; @override String appSettings_timeFilterShowLast(int hours) { - return '显示来自过去 $hours 小时的节点'; + return 'Show nodes from last $hours hours'; } @override String get appSettings_mapTimeFilter => '地图时间筛选'; @override - String get appSettings_showNodesDiscoveredWithin => '显示发现的节点在:'; + String get appSettings_showNodesDiscoveredWithin => '显示在以下范围内发现的节点:'; @override String get appSettings_allTime => '所有时间'; @override - String get appSettings_lastHour => '最后小时'; + String get appSettings_lastHour => '过去一小时'; @override - String get appSettings_last6Hours => '最后6小时'; + String get appSettings_last6Hours => '过去6小时'; @override - String get appSettings_last24Hours => '最后24小时'; + String get appSettings_last24Hours => '过去24小时'; @override String get appSettings_lastWeek => '上周'; @@ -585,38 +586,38 @@ class AppLocalizationsZh extends AppLocalizations { @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return '选中的区域(缩放至 $minZoom - $maxZoom)'; + return '已选择区域(缩放至 $minZoom - $maxZoom)'; } @override String get appSettings_debugCard => '调试'; @override - String get appSettings_appDebugLogging => '应用调试日志'; + String get appSettings_appDebugLogging => '应用程序调试日志'; @override - String get appSettings_appDebugLoggingSubtitle => '记录应用调试消息以供故障排除'; + String get appSettings_appDebugLoggingSubtitle => '用于故障排除的日志应用程序调试消息'; @override - String get appSettings_appDebugLoggingEnabled => '应用调试日志已启用'; + String get appSettings_appDebugLoggingEnabled => '调试日志已启用'; @override - String get appSettings_appDebugLoggingDisabled => '应用调试日志已禁用'; + String get appSettings_appDebugLoggingDisabled => '应用程序调试日志已禁用'; @override - String get contacts_title => '联系人'; + String get contacts_title => '联系方式'; @override - String get contacts_noContacts => '还没有联系人'; + String get contacts_noContacts => '目前还没有联系人'; @override - String get contacts_contactsWillAppear => '设备会广播时,联系人会显示'; + String get contacts_contactsWillAppear => '当设备发布广告时,联系方式会显示。'; @override String get contacts_searchContacts => '搜索联系人...'; @override - String get contacts_noUnreadContacts => '未读联系人'; + String get contacts_noUnreadContacts => '没有未读通讯'; @override String get contacts_noContactsFound => '未找到任何联系人或群组'; @@ -626,26 +627,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String contacts_removeConfirm(String contactName) { - return '从联系人中删除 $contactName 吗?'; + return 'Remove $contactName from contacts?'; } @override - String get contacts_manageRepeater => '管理重复项'; + String get contacts_manageRepeater => '管理重复器'; @override String get contacts_manageRoom => '管理房间服务器'; @override - String get contacts_roomLogin => '房间登录'; + String get contacts_roomLogin => '服务器登录'; @override - String get contacts_openChat => '打开聊天'; + String get contacts_openChat => '开放聊天'; @override - String get contacts_editGroup => '编辑组'; + String get contacts_editGroup => '编辑小组'; @override - String get contacts_deleteGroup => '删除分组'; + String get contacts_deleteGroup => '删除群组'; @override String contacts_deleteGroupConfirm(String groupName) { @@ -653,50 +654,50 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get contacts_newGroup => '新组'; + String get contacts_newGroup => '新的团体'; @override - String get contacts_groupName => '组名'; + String get contacts_groupName => '团体名称'; @override - String get contacts_groupNameRequired => '组名不能为空'; + String get contacts_groupNameRequired => '需要提供组名称'; @override String contacts_groupAlreadyExists(String name) { - return '组“$name”已存在'; + return '名为\"$name\"的组已经存在'; } @override String get contacts_filterContacts => '筛选联系人...'; @override - String get contacts_noContactsMatchFilter => '未找到匹配您的筛选条件的结果'; + String get contacts_noContactsMatchFilter => '未找到符合您筛选条件的联系人'; @override String get contacts_noMembers => '没有会员'; @override - String get contacts_lastSeenNow => '最后一次登录时间现在'; + String get contacts_lastSeenNow => '最后一次被看到的时间'; @override String contacts_lastSeenMinsAgo(int minutes) { - return '最后一次出现 $minutes 分前'; + return 'Last seen $minutes mins ago'; } @override - String get contacts_lastSeenHourAgo => '最后一次出现前1小时'; + String get contacts_lastSeenHourAgo => '最后一次被看到的时间:1小时前'; @override String contacts_lastSeenHoursAgo(int hours) { - return '最后一次出现 $hours 小时前'; + return 'Last seen $hours hours ago'; } @override - String get contacts_lastSeenDayAgo => '最后一次登录前一天'; + String get contacts_lastSeenDayAgo => '最后一次被看到的时间是1天前'; @override String contacts_lastSeenDaysAgo(int days) { - return '最后一次出现 $days 天前'; + return 'Last seen $days days ago'; } @override @@ -706,7 +707,7 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_noChannelsConfigured => '未配置任何频道'; @override - String get channels_addPublicChannel => '添加公开频道'; + String get channels_addPublicChannel => '添加公共频道'; @override String get channels_searchChannels => '搜索频道...'; @@ -720,19 +721,19 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channels_hashtagChannel => '话题频道'; + String get channels_hashtagChannel => '话题标签频道'; @override - String get channels_public => '公开'; + String get channels_public => '公众'; @override - String get channels_private => '私有'; + String get channels_private => '私人'; @override - String get channels_publicChannel => '公开频道'; + String get channels_publicChannel => '公共频道'; @override - String get channels_privateChannel => '私聊频道'; + String get channels_privateChannel => '私密频道'; @override String get channels_editChannel => '编辑频道'; @@ -742,12 +743,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return '删除\"$name\"?此操作无法撤销。'; + return 'Delete \"$name\"? This cannot be undone.'; } @override String channels_channelDeleted(String name) { - return '频道“$name”已删除'; + return '删除频道 \"$name\"'; } @override @@ -763,23 +764,23 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_usePublicChannel => '使用公共频道'; @override - String get channels_standardPublicPsk => '标准公钥共享密钥'; + String get channels_standardPublicPsk => '标准公共PSK'; @override String get channels_pskHex => 'PSK (十六进制)'; @override - String get channels_generateRandomPsk => '生成随机PSK'; + String get channels_generateRandomPsk => '生成随机的PSK(正交相移键控)'; @override - String get channels_enterChannelName => '请输入频道名称'; + String get channels_enterChannelName => '请在此处输入频道名称'; @override - String get channels_pskMustBe32Hex => 'PSK 必须是 32 个十六进制字符'; + String get channels_pskMustBe32Hex => 'PSK 必须包含 32 个十六进制字符。'; @override String channels_channelAdded(String name) { - return '频道“$name”已添加'; + return '添加频道 \"$name\"'; } @override @@ -792,20 +793,20 @@ class AppLocalizationsZh extends AppLocalizations { @override String channels_channelUpdated(String name) { - return '频道“$name”已更新'; + return '频道 \"$name\" 已更新'; } @override - String get channels_publicChannelAdded => '公共频道已添加'; + String get channels_publicChannelAdded => '已添加公共频道'; @override - String get channels_sortBy => '按类型排序'; + String get channels_sortBy => '按排序'; @override - String get channels_sortManual => '手动'; + String get channels_sortManual => '手册'; @override - String get channels_sortAZ => 'A-Z'; + String get channels_sortAZ => 'A 到 Z'; @override String get channels_sortLatestMessages => '最新消息'; @@ -814,10 +815,10 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_sortUnread => '未读'; @override - String get channels_createPrivateChannel => '创建私聊频道'; + String get channels_createPrivateChannel => '创建私密频道'; @override - String get channels_createPrivateChannelDesc => '使用密钥保护。'; + String get channels_createPrivateChannelDesc => '使用秘密密钥进行保护。'; @override String get channels_joinPrivateChannel => '加入私密频道'; @@ -832,48 +833,48 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_joinPublicChannelDesc => '任何人都可以加入这个频道。'; @override - String get channels_joinHashtagChannel => '加入标签频道'; + String get channels_joinHashtagChannel => '加入一个带有特定标签的频道'; @override - String get channels_joinHashtagChannelDesc => '任何人都可以加入话题频道。'; + String get channels_joinHashtagChannelDesc => '任何人都可以加入带有特定标签的频道。'; @override String get channels_scanQrCode => '扫描二维码'; @override - String get channels_scanQrCodeComingSoon => '即将到来'; + String get channels_scanQrCodeComingSoon => '即将发布'; @override String get channels_enterHashtag => '输入标签'; @override - String get channels_hashtagHint => '例如 #团队'; + String get channels_hashtagHint => '例如:#团队'; @override - String get chat_noMessages => '目前还没有消息'; + String get chat_noMessages => '目前还没有收到任何消息。'; @override - String get chat_sendMessageToStart => '发送消息开始'; + String get chat_sendMessageToStart => '发送消息以开始'; @override - String get chat_originalMessageNotFound => '找不到原始消息'; + String get chat_originalMessageNotFound => '无法找到原始消息'; @override String chat_replyingTo(String name) { - return '回复 $name'; + return 'Replying to $name'; } @override String chat_replyTo(String name) { - return '回复 $name'; + return 'Reply to $name'; } @override - String get chat_location => '位置'; + String get chat_location => '地点'; @override String chat_sendMessageTo(String contactName) { - return '向$contactName发送消息'; + return 'Send a message to $contactName'; } @override @@ -881,7 +882,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return '消息太长了(最大 $maxBytes 字节)。'; + return '消息内容过长(最大 $maxBytes 字节)。'; } @override @@ -891,21 +892,21 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_messageDeleted => '消息已删除'; @override - String get chat_retryingMessage => '重试'; + String get chat_retryingMessage => '重试消息'; @override String chat_retryCount(int current, int max) { - return '重试 $current/$max'; + return 'Retry $current/$max'; } @override - String get chat_sendGif => '发送GIF'; + String get chat_sendGif => '发送 GIF 动画'; @override String get chat_reply => '回复'; @override - String get chat_addReaction => '添加反应'; + String get chat_addReaction => '添加评论'; @override String get chat_me => '我'; @@ -917,16 +918,16 @@ class AppLocalizationsZh extends AppLocalizations { String get emojiCategoryGestures => '手势'; @override - String get emojiCategoryHearts => '心'; + String get emojiCategoryHearts => '心脏'; @override - String get emojiCategoryObjects => '对象'; + String get emojiCategoryObjects => '物体'; @override - String get gifPicker_title => '选择一个 GIF'; + String get gifPicker_title => '选择一个 GIF 动画'; @override - String get gifPicker_searchHint => '搜索GIF...'; + String get gifPicker_searchHint => '搜索 GIF 动画...'; @override String get gifPicker_poweredBy => '由 GIPHY 提供支持'; @@ -935,46 +936,46 @@ class AppLocalizationsZh extends AppLocalizations { String get gifPicker_noGifsFound => '未找到 GIF 动画'; @override - String get gifPicker_failedLoad => 'GIF 加载失败'; + String get gifPicker_failedLoad => '无法加载 GIF 动画'; @override - String get gifPicker_failedSearch => '搜索GIF失败'; + String get gifPicker_failedSearch => '未能搜索 GIF 动画'; @override - String get gifPicker_noInternet => '无网络连接'; + String get gifPicker_noInternet => '没有互联网连接'; @override - String get debugLog_appTitle => '应用调试日志'; + String get debugLog_appTitle => '应用程序调试日志'; @override - String get debugLog_bleTitle => '蓝牙调试日志'; + String get debugLog_bleTitle => 'BLE 调试日志'; @override String get debugLog_copyLog => '复制日志'; @override - String get debugLog_clearLog => '清除日志'; + String get debugLog_clearLog => '清晰的日志'; @override String get debugLog_copied => '调试日志已复制'; @override - String get debugLog_bleCopied => '蓝牙日志复制'; + String get debugLog_bleCopied => 'BLE 日志已复制'; @override - String get debugLog_noEntries => '尚未生成调试日志'; + String get debugLog_noEntries => '目前还没有调试日志'; @override - String get debugLog_enableInSettings => '启用应用调试日志记录设置'; + String get debugLog_enableInSettings => '在设置中启用应用程序调试日志功能。'; @override - String get debugLog_frames => '帧'; + String get debugLog_frames => '框架'; @override String get debugLog_rawLogRx => '原始日志-RX'; @override - String get debugLog_noBleActivity => '目前还没有蓝牙活动。'; + String get debugLog_noBleActivity => '目前尚未有蓝牙低功耗(BLE)活动。'; @override String debugFrame_length(int count) { @@ -987,16 +988,16 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => '短信框'; + String get debugFrame_textMessageHeader => '短信模板:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- 目的地公钥:$pubKey'; + return '- 目标公钥:$pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- 时间戳:$timestamp'; + return '- Timestamp: $timestamp'; } @override @@ -1006,22 +1007,22 @@ class AppLocalizationsZh extends AppLocalizations { @override String debugFrame_textType(int type, String label) { - return '- 文本类型:$type ($label)'; + return '- Text Type: $type ($label)'; } @override - String get debugFrame_textTypeCli => 'CLI'; + String get debugFrame_textTypeCli => '命令行界面'; @override - String get debugFrame_textTypePlain => '简洁'; + String get debugFrame_textTypePlain => '简单'; @override String debugFrame_text(String text) { - return '- 文本:\"$text\"'; + return '- 文本:“$text”'; } @override - String get debugFrame_hexDump => '十六进制数据'; + String get debugFrame_hexDump => '十六进制数据:'; @override String get chat_pathManagement => '路径管理'; @@ -1030,30 +1031,30 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_routingMode => '路由模式'; @override - String get chat_autoUseSavedPath => '自动(使用已保存路径)'; + String get chat_autoUseSavedPath => '自动(使用已保存的路径)'; @override String get chat_forceFloodMode => '强制洪水模式'; @override - String get chat_recentAckPaths => '最近的 ACK 路径 (点击以使用):'; + String get chat_recentAckPaths => '最近使用的 ACK 路径(点击使用):'; @override - String get chat_pathHistoryFull => '路径历史已满。删除条目以添加新条目。'; + String get chat_pathHistoryFull => '路径历史已满。删除条目以添加新的条目。'; @override - String get chat_hopSingular => '跳转'; + String get chat_hopSingular => '跳跃'; @override - String get chat_hopPlural => '跳跃'; + String get chat_hopPlural => '啤酒花'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '跳跃', - one: '跳跃', + other: 'hops', + one: 'hop', ); return '$count $_temp0'; } @@ -1065,7 +1066,7 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_removePath => '删除路径'; @override - String get chat_noPathHistoryYet => '还没有历史记录。\n发送消息以发现路径。'; + String get chat_noPathHistoryYet => '目前还没有历史记录。\n发送消息以查找路径。'; @override String get chat_pathActions => '路径操作:'; @@ -1077,25 +1078,25 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_setCustomPathSubtitle => '手动指定路由路径'; @override - String get chat_clearPath => '清除路径'; + String get chat_clearPath => '明确的道路'; @override - String get chat_clearPathSubtitle => '强制下次发送时重新发现'; + String get chat_clearPathSubtitle => '在下一次发送时,重新尝试。'; @override - String get chat_pathCleared => '路径已清除。下一条消息将重新发现路线。'; + String get chat_pathCleared => '路径已清理。下一条消息将重新确定路线。'; @override - String get chat_floodModeSubtitle => '使用应用栏中的路由切换开关'; + String get chat_floodModeSubtitle => '使用应用程序栏中的路由切换功能'; @override - String get chat_floodModeEnabled => '防洪模式已启用。通过应用程序栏中的路由图标进行反转。'; + String get chat_floodModeEnabled => '防洪模式已启用。通过应用程序栏中的路由图标进行切换。'; @override String get chat_fullPath => '完整路径'; @override - String get chat_pathDetailsNotAvailable => '路径详情尚未获取。请尝试发送消息以刷新。'; + String get chat_pathDetailsNotAvailable => '路径信息尚未提供。请尝试发送消息以刷新。'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1105,20 +1106,20 @@ class AppLocalizationsZh extends AppLocalizations { other: 'hops', one: 'hop', ); - return '路径设置:$hopCount $_temp0 - $status'; + return 'Path set: $hopCount $_temp0 - $status'; } @override - String get chat_pathSavedLocally => '已本地保存。连接以同步。'; + String get chat_pathSavedLocally => '已本地保存。连接以进行同步。'; @override String get chat_pathDeviceConfirmed => '设备已确认。'; @override - String get chat_pathDeviceNotConfirmed => '设备尚未确认。'; + String get chat_pathDeviceNotConfirmed => '该设备尚未得到确认。'; @override - String get chat_type => '输入'; + String get chat_type => '类型'; @override String get chat_path => '路径'; @@ -1130,64 +1131,64 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_compressOutgoingMessages => '压缩发送的消息'; @override - String get chat_floodForced => '强制溢出'; + String get chat_floodForced => '洪水(被迫)'; @override - String get chat_directForced => '强制直接'; + String get chat_directForced => '直接(强制性的)'; @override String chat_hopsForced(int count) { - return '$count 次跳跃 (强制)'; + return '$count 根啤酒花(人工种植)'; } @override - String get chat_floodAuto => '自动防洪'; + String get chat_floodAuto => '自动洪水'; @override String get chat_direct => '直接'; @override - String get chat_poiShared => '共享位置信息'; + String get chat_poiShared => '共享位置'; @override String chat_unread(int count) { - return '未读:$count'; + return 'Unread: $count'; } @override String get chat_openLink => '打开链接?'; @override - String get chat_openLinkConfirmation => '您想在浏览器中打开此链接吗?'; + String get chat_openLinkConfirmation => '您想用浏览器打开这个链接吗?'; @override - String get chat_open => '打开'; + String get chat_open => '开放'; @override String chat_couldNotOpenLink(String url) { - return '无法打开链接:$url'; + return '[保存:$url]\n无法打开链接:$url'; } @override - String get chat_invalidLink => '链接格式无效'; + String get chat_invalidLink => '无效的链接格式'; @override - String get map_title => '节点地图'; + String get map_title => '节点图'; @override - String get map_noNodesWithLocation => '没有具有位置数据的节点'; + String get map_noNodesWithLocation => '没有包含位置信息的节点'; @override - String get map_nodesNeedGps => '节点需要共享它们的 GPS 坐标\n才能在地图上显示'; + String get map_nodesNeedGps => '节点需要共享其 GPS 坐标,以便在地图上显示'; @override String map_nodesCount(int count) { - return '节点:$count'; + return 'Nodes: $count'; } @override String map_pinsCount(int count) { - return '针:$count'; + return 'Pins: $count'; } @override @@ -1203,16 +1204,16 @@ class AppLocalizationsZh extends AppLocalizations { String get map_sensor => '传感器'; @override - String get map_pinDm => '私信 (DM)'; + String get map_pinDm => 'PIN (直接消息)'; @override - String get map_pinPrivate => '私密模式'; + String get map_pinPrivate => '私密'; @override - String get map_pinPublic => '公开(公版)'; + String get map_pinPublic => '公开'; @override - String get map_lastSeen => '最后一次登录'; + String get map_lastSeen => '最后一次被看到'; @override String get map_disconnectConfirm => '您确定要断开与此设备的连接吗?'; @@ -1227,19 +1228,19 @@ class AppLocalizationsZh extends AppLocalizations { String get map_flags => '旗帜'; @override - String get map_shareMarkerHere => '分享标记在此'; + String get map_shareMarkerHere => '在此分享标记'; @override - String get map_pinLabel => '固定标签'; + String get map_pinLabel => '标签'; @override String get map_label => '标签'; @override - String get map_pointOfInterest => '兴趣点'; + String get map_pointOfInterest => '值得参观的地方'; @override - String get map_sendToContact => '发送给联系人'; + String get map_sendToContact => '发送给联系'; @override String get map_sendToChannel => '发送到频道'; @@ -1248,18 +1249,18 @@ class AppLocalizationsZh extends AppLocalizations { String get map_noChannelsAvailable => '没有可用的频道'; @override - String get map_publicLocationShare => '公共位置共享'; + String get map_publicLocationShare => '公共场所共享'; @override String map_publicLocationShareConfirm(String channelLabel) { - return '您即将分享一个位置在 $channelLabel。此频道公开,任何拥有 PSK 的人都可以看到它。'; + return '[保存:$channelLabel]\n您即将分享一个位置,该位置位于 $channelLabel。 此频道是公开的,任何拥有 PSK 的人都可以看到它。'; } @override String get map_connectToShareMarkers => '连接设备以共享标记'; @override - String get map_filterNodes => '筛选节点'; + String get map_filterNodes => '过滤节点'; @override String get map_nodeTypes => '节点类型'; @@ -1274,10 +1275,10 @@ class AppLocalizationsZh extends AppLocalizations { String get map_otherNodes => '其他节点'; @override - String get map_keyPrefix => '键前缀'; + String get map_keyPrefix => '关键前缀'; @override - String get map_filterByKeyPrefix => '按关键词前缀筛选'; + String get map_filterByKeyPrefix => '按关键前缀筛选'; @override String get map_publicKeyPrefix => '公钥前缀'; @@ -1289,32 +1290,32 @@ class AppLocalizationsZh extends AppLocalizations { String get map_showSharedMarkers => '显示共享标记'; @override - String get map_lastSeenTime => '最后一次查看时间'; + String get map_lastSeenTime => '最后一次被看到的时间'; @override - String get map_sharedPin => '共享 PIN'; + String get map_sharedPin => '共享密码'; @override String get map_joinRoom => '加入房间'; @override - String get map_manageRepeater => '管理重复项'; + String get map_manageRepeater => '管理重复器'; @override String get mapCache_title => '离线地图缓存'; @override - String get mapCache_selectAreaFirst => '选择一个区域进行缓存'; + String get mapCache_selectAreaFirst => '选择一个用于缓存的区域'; @override - String get mapCache_noTilesToDownload => '该区域没有可下载的瓦片。'; + String get mapCache_noTilesToDownload => '此区域没有可下载的瓦片。'; @override - String get mapCache_downloadTilesTitle => '下载瓦片'; + String get mapCache_downloadTilesTitle => '下载瓷砖'; @override String mapCache_downloadTilesPrompt(int count) { - return '下载 $count 个瓦片用于离线使用?'; + return '[保存:$count]\n下载 $count 个图片用于离线使用?'; } @override @@ -1322,19 +1323,19 @@ class AppLocalizationsZh extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return '已缓存 $count 个瓦片'; + return '缓存 $count 个瓦片'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return '已缓存 $downloaded 个瓦片 ($failed 失败)'; + return 'Cached $downloaded tiles ($failed failed)'; } @override String get mapCache_clearOfflineCacheTitle => '清除离线缓存'; @override - String get mapCache_clearOfflineCachePrompt => '删除所有缓存地图瓦片?'; + String get mapCache_clearOfflineCachePrompt => '清除所有缓存的地图瓦片'; @override String get mapCache_offlineCacheCleared => '离线缓存已清除'; @@ -1349,27 +1350,27 @@ class AppLocalizationsZh extends AppLocalizations { String get mapCache_useCurrentView => '使用当前视图'; @override - String get mapCache_zoomRange => '缩放范围'; + String get mapCache_zoomRange => '变焦范围'; @override String mapCache_estimatedTiles(int count) { - return '预计瓦片数量:$count'; + return 'Estimated tiles: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return '已下载 $completed / $total'; + return 'Downloaded $completed / $total'; } @override - String get mapCache_downloadTilesButton => '下载瓦片'; + String get mapCache_downloadTilesButton => '下载瓷砖'; @override String get mapCache_clearCacheButton => '清除缓存'; @override String mapCache_failedDownloads(int count) { - return '下载失败:$count'; + return 'Failed downloads: $count'; } @override @@ -1379,7 +1380,7 @@ class AppLocalizationsZh extends AppLocalizations { String east, String west, ) { - return '北 $north, 南 $south, 东 $east, 西 $west'; + return 'N $north, S $south, E $east, W $west'; } @override @@ -1387,17 +1388,17 @@ class AppLocalizationsZh extends AppLocalizations { @override String time_minutesAgo(int minutes) { - return '$minutes分钟前'; + return '${minutes}m ago'; } @override String time_hoursAgo(int hours) { - return '$hours小时前'; + return '${hours}h ago'; } @override String time_daysAgo(int days) { - return '$days 天前'; + return '$days天前'; } @override @@ -1407,16 +1408,16 @@ class AppLocalizationsZh extends AppLocalizations { String get time_hours => '小时'; @override - String get time_day => '今天'; + String get time_day => '一天'; @override String get time_days => '天'; @override - String get time_week => '本周'; + String get time_week => '一周'; @override - String get time_weeks => '几周'; + String get time_weeks => '周'; @override String get time_month => '月份'; @@ -1440,7 +1441,7 @@ class AppLocalizationsZh extends AppLocalizations { String get login_repeaterLogin => '重复登录'; @override - String get login_roomLogin => '房间登录'; + String get login_roomLogin => '服务器登录'; @override String get login_password => '密码'; @@ -1452,13 +1453,13 @@ class AppLocalizationsZh extends AppLocalizations { String get login_savePassword => '保存密码'; @override - String get login_savePasswordSubtitle => '密码将安全地存储在这个设备上'; + String get login_savePasswordSubtitle => '密码将安全地存储在 данном设备上'; @override - String get login_repeaterDescription => '输入重复密码以访问设置和状态。'; + String get login_repeaterDescription => '输入重复器密码,即可访问设置和状态。'; @override - String get login_roomDescription => '输入房间密码以访问设置和状态。'; + String get login_roomDescription => '输入密码进入房间,即可访问设置和状态。'; @override String get login_routing => '路由'; @@ -1467,7 +1468,7 @@ class AppLocalizationsZh extends AppLocalizations { String get login_routingMode => '路由模式'; @override - String get login_autoUseSavedPath => '自动(使用已保存路径)'; + String get login_autoUseSavedPath => '自动(使用已保存的路径)'; @override String get login_forceFloodMode => '强制洪水模式'; @@ -1480,26 +1481,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String login_attempt(int current, int max) { - return '尝试 $current/$max'; + return 'Attempt $current/$max'; } @override String login_failed(String error) { - return '登录失败:$error'; + return 'Login failed: $error'; } @override - String get login_failedMessage => '登录失败。密码不正确或中继器不可达。'; + String get login_failedMessage => '登录失败。可能是密码错误,也可能是无法连接到服务器。'; @override String get common_reload => '重新加载'; @override - String get common_clear => '清除'; + String get common_clear => '清晰'; @override String path_currentPath(String path) { - return '当前路径:$path'; + return 'Current path: $path'; } @override @@ -1510,7 +1511,7 @@ class AppLocalizationsZh extends AppLocalizations { other: 'hops', one: 'hop', ); - return '使用 $count $_temp0 路径'; + return '使用 $count $_temp0 条路径'; } @override @@ -1520,29 +1521,29 @@ class AppLocalizationsZh extends AppLocalizations { String get path_currentPathLabel => '当前路径'; @override - String get path_hexPrefixInstructions => '输入2个字符的十六进制前缀,每个前缀之间用逗号分隔。'; + String get path_hexPrefixInstructions => '请输入每个跳跃步骤的 2 个字符的十六进制前缀,用逗号分隔。'; @override - String get path_hexPrefixExample => 'A1,F2,3C (每个节点使用其公钥的第一字节)'; + String get path_hexPrefixExample => '例如:A1, F2, 3C (每个节点使用其公钥的第一字节)'; @override - String get path_labelHexPrefixes => '十六进制前缀'; + String get path_labelHexPrefixes => '路径(十六进制前缀)'; @override - String get path_helperMaxHops => '最大 64 步跳。每个前缀是 2 个十六进制字符(1 字节)'; + String get path_helperMaxHops => '最大 64 个“hop”(跳跃)。每个前缀由 2 个十六进制字符(1 字节)组成。'; @override - String get path_selectFromContacts => '或从联系人中选择:'; + String get path_selectFromContacts => '或者从联系人列表中选择:'; @override - String get path_noRepeatersFound => '未找到任何重复器或房间服务器。'; + String get path_noRepeatersFound => '未找到任何重复设备或房间服务器。'; @override - String get path_customPathsRequire => '自定义路径需要中间跳转,这些跳转可以传递消息。'; + String get path_customPathsRequire => '自定义路径需要中间节点,这些节点可以转发消息。'; @override String path_invalidHexPrefixes(String prefixes) { - return '无效的十六进制前缀:$prefixes'; + return 'Invalid hex prefixes: $prefixes'; } @override @@ -1555,7 +1556,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_management => '重复器管理'; @override - String get room_management => '房间服务器管理'; + String get room_management => '服务器管理'; @override String get repeater_managementTools => '管理工具'; @@ -1567,22 +1568,22 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_statusSubtitle => '查看重复器状态、统计信息和邻居'; @override - String get repeater_telemetry => '遥测'; + String get repeater_telemetry => '远程监控'; @override - String get repeater_telemetrySubtitle => '查看传感器和系统状态的Telemetry数据'; + String get repeater_telemetrySubtitle => '查看传感器和系统状态的数据。'; @override - String get repeater_cli => 'CLI'; + String get repeater_cli => '命令行界面'; @override - String get repeater_cliSubtitle => '发送命令到重复器'; + String get repeater_cliSubtitle => '向复用器发送指令'; @override String get repeater_neighbours => '邻居'; @override - String get repeater_neighboursSubtitle => '查看零跳邻居。'; + String get repeater_neighboursSubtitle => '查看邻居节点(无需中间节点)。'; @override String get repeater_settings => '设置'; @@ -1597,7 +1598,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_routingMode => '路由模式'; @override - String get repeater_autoUseSavedPath => '自动(使用已保存路径)'; + String get repeater_autoUseSavedPath => '自动(使用已保存的路径)'; @override String get repeater_forceFloodMode => '强制洪水模式'; @@ -1606,14 +1607,14 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_pathManagement => '路径管理'; @override - String get repeater_refresh => '刷新'; + String get repeater_refresh => '更新'; @override String get repeater_statusRequestTimeout => '状态请求超时。'; @override String repeater_errorLoadingStatus(String error) { - return '错误加载状态:$error'; + return 'Error loading status: $error'; } @override @@ -1623,10 +1624,10 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_battery => '电池'; @override - String get repeater_clockAtLogin => '时间 (登录时)'; + String get repeater_clockAtLogin => '登录时的时间'; @override - String get repeater_uptime => '可用时间'; + String get repeater_uptime => '正常运行时间'; @override String get repeater_queueLength => '排队长度'; @@ -1635,28 +1636,28 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_debugFlags => '调试标志'; @override - String get repeater_radioStatistics => '无线电统计'; + String get repeater_radioStatistics => '广播统计'; @override - String get repeater_lastRssi => '上次RSSI'; + String get repeater_lastRssi => '上次的 RSSI 值'; @override - String get repeater_lastSnr => '最后 SNR'; + String get repeater_lastSnr => '最后一次信噪比'; @override - String get repeater_noiseFloor => '噪声地板'; + String get repeater_noiseFloor => '噪声水平'; @override - String get repeater_txAirtime => 'TX Airtime'; + String get repeater_txAirtime => 'TX 频道预留时间'; @override - String get repeater_rxAirtime => 'RX Airtime'; + String get repeater_rxAirtime => 'RX 空时'; @override String get repeater_packetStatistics => '数据包统计'; @override - String get repeater_sent => '已发送'; + String get repeater_sent => '发送'; @override String get repeater_received => '已收到'; @@ -1676,26 +1677,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return '总计:$total, 洪流:$flood, 直连:$direct'; + return 'Total: $total, Flood: $flood, Direct: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return '总计:$total, 洪流:$flood, 直连:$direct'; + return 'Total: $total, Flood: $flood, Direct: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return '洪水:$flood, 直通:$direct'; + return 'Flood: $flood, Direct: $direct'; } @override String repeater_duplicatesTotal(int total) { - return '总计:$total'; + return 'Total: $total'; } @override - String get repeater_settingsTitle => '重复设置'; + String get repeater_settingsTitle => '重复器设置'; @override String get repeater_basicSettings => '基本设置'; @@ -1704,7 +1705,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_repeaterName => '重复器名称'; @override - String get repeater_repeaterNameHelper => '显示此重复器的名称'; + String get repeater_repeaterNameHelper => '此复播器的显示名称'; @override String get repeater_adminPassword => '管理员密码'; @@ -1719,16 +1720,16 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_guestPasswordHelper => '只读访问密码'; @override - String get repeater_radioSettings => '射频设置'; + String get repeater_radioSettings => '收音机设置'; @override String get repeater_frequencyMhz => '频率 (MHz)'; @override - String get repeater_frequencyHelper => '300-2500 MHz'; + String get repeater_frequencyHelper => '300-2500 兆赫'; @override - String get repeater_txPower => 'TX Power'; + String get repeater_txPower => 'TX 功率'; @override String get repeater_txPowerHelper => '1-30 dBm'; @@ -1737,7 +1738,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_bandwidth => '带宽'; @override - String get repeater_spreadingFactor => '扩散因子'; + String get repeater_spreadingFactor => '传播系数'; @override String get repeater_codingRate => '编码速率'; @@ -1749,56 +1750,56 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_latitude => '纬度'; @override - String get repeater_latitudeHelper => '十进度的数字(例如:37.7749)'; + String get repeater_latitudeHelper => '十进制度(例如:37.7749)'; @override String get repeater_longitude => '经度'; @override - String get repeater_longitudeHelper => '十进度的数字(例如:-122.4194)'; + String get repeater_longitudeHelper => '十进制度(例如:-122.4194)'; @override - String get repeater_features => '功能'; + String get repeater_features => '特点'; @override String get repeater_packetForwarding => '数据包转发'; @override - String get repeater_packetForwardingSubtitle => '启用重复器以转发数据包'; + String get repeater_packetForwardingSubtitle => '启用重复器,使其能够转发数据包'; @override String get repeater_guestAccess => '访客访问'; @override - String get repeater_guestAccessSubtitle => '允许访客仅读访问'; + String get repeater_guestAccessSubtitle => '允许访客仅限读取权限'; @override String get repeater_privacyMode => '隐私模式'; @override - String get repeater_privacyModeSubtitle => '隐藏在广告中的姓名/位置'; + String get repeater_privacyModeSubtitle => '在广告中隐藏姓名/位置'; @override String get repeater_advertisementSettings => '广告设置'; @override - String get repeater_localAdvertInterval => '本地广告间隔'; + String get repeater_localAdvertInterval => '本地广告投放时间段'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes 分钟'; + return '$minutes minutes'; } @override - String get repeater_floodAdvertInterval => '洪水广告间隔'; + String get repeater_floodAdvertInterval => '洪水广告播放间隔'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours 小时'; + return '$hours hours'; } @override - String get repeater_encryptedAdvertInterval => '加密广告间隔'; + String get repeater_encryptedAdvertInterval => '加密的广告投放时间段'; @override String get repeater_dangerZone => '危险区域'; @@ -1807,10 +1808,10 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_rebootRepeater => '重启重复器'; @override - String get repeater_rebootRepeaterSubtitle => '重启重复器设备'; + String get repeater_rebootRepeaterSubtitle => '重新启动重复器设备'; @override - String get repeater_rebootRepeaterConfirm => '您确定要重启这个中继器吗?'; + String get repeater_rebootRepeaterConfirm => '您确定要重新启动这个中继器吗?'; @override String get repeater_regenerateIdentityKey => '重新生成身份密钥'; @@ -1819,7 +1820,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_regenerateIdentityKeySubtitle => '生成新的公钥/私钥对'; @override - String get repeater_regenerateIdentityKeyConfirm => '这将生成一个重复器的新身份。继续吗?'; + String get repeater_regenerateIdentityKeyConfirm => '这将为复用器生成一个新的身份。继续吗?'; @override String get repeater_eraseFileSystem => '删除文件系统'; @@ -1828,109 +1829,109 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_eraseFileSystemSubtitle => '格式化重复文件系统'; @override - String get repeater_eraseFileSystemConfirm => '警告:这将擦除重复器上的所有数据。 这无法撤销!'; + String get repeater_eraseFileSystemConfirm => '警告:此操作将清除复用器上的所有数据。 无法恢复!'; @override - String get repeater_eraseSerialOnly => '通过串行控制台才能删除。'; + String get repeater_eraseSerialOnly => '“Erase”功能仅可通过串行控制台使用。'; @override String repeater_commandSent(String command) { - return '命令已发送:$command'; + return 'Command sent: $command'; } @override String repeater_errorSendingCommand(String error) { - return '发送命令时出错:$error'; + return 'Error sending command: $error'; } @override String get repeater_confirm => '确认'; @override - String get repeater_settingsSaved => '设置已保存成功'; + String get repeater_settingsSaved => '设置已成功保存'; @override String repeater_errorSavingSettings(String error) { - return '保存设置出错:$error'; + return 'Error saving settings: $error'; } @override - String get repeater_refreshBasicSettings => '刷新基本设置'; + String get repeater_refreshBasicSettings => '重置基本设置'; @override - String get repeater_refreshRadioSettings => '刷新无线电设置'; + String get repeater_refreshRadioSettings => '重置收音机设置'; @override - String get repeater_refreshTxPower => '刷新 TX 电量'; + String get repeater_refreshTxPower => '重置 TX 电源'; @override - String get repeater_refreshLocationSettings => '刷新位置设置'; + String get repeater_refreshLocationSettings => '重置位置设置'; @override String get repeater_refreshPacketForwarding => '刷新包转发'; @override - String get repeater_refreshGuestAccess => '刷新访客访问'; + String get repeater_refreshGuestAccess => '重新获取访客访问权限'; @override - String get repeater_refreshPrivacyMode => '刷新隐私模式'; + String get repeater_refreshPrivacyMode => '重置隐私模式'; @override - String get repeater_refreshAdvertisementSettings => '刷新广告设置'; + String get repeater_refreshAdvertisementSettings => '重置广告设置'; @override String repeater_refreshed(String label) { - return '$label 已刷新'; + return '$label refreshed'; } @override String repeater_errorRefreshing(String label) { - return '刷新 $label 时出错'; + return '[保存:$label]\n刷新 $label 时出错'; } @override - String get repeater_cliTitle => '重复器命令行工具'; + String get repeater_cliTitle => '重复器命令行界面'; @override - String get repeater_debugNextCommand => '调试下一步命令'; + String get repeater_debugNextCommand => '调试下一条命令'; @override String get repeater_commandHelp => '帮助'; @override - String get repeater_clearHistory => '清除历史'; + String get repeater_clearHistory => '清晰的历史'; @override - String get repeater_noCommandsSent => '尚未发送任何命令'; + String get repeater_noCommandsSent => '尚未发送任何指令'; @override - String get repeater_typeCommandOrUseQuick => '输入命令或使用快捷命令'; + String get repeater_typeCommandOrUseQuick => '在下方输入命令,或使用快捷命令。'; @override String get repeater_enterCommandHint => '输入命令...'; @override - String get repeater_previousCommand => '上一个命令'; + String get repeater_previousCommand => '之前的命令'; @override - String get repeater_nextCommand => '下一步命令'; + String get repeater_nextCommand => '下一个指令'; @override - String get repeater_enterCommandFirst => '请输入一个命令'; + String get repeater_enterCommandFirst => '首先输入一个命令'; @override - String get repeater_cliCommandFrameTitle => 'CLI 命令窗口'; + String get repeater_cliCommandFrameTitle => 'CLI 命令框架'; @override String repeater_cliCommandError(String error) { - return '错误:$error'; + return 'Error: $error'; } @override String get repeater_cliQuickGetName => '获取姓名'; @override - String get repeater_cliQuickGetRadio => '获取收音机'; + String get repeater_cliQuickGetRadio => '收听广播'; @override String get repeater_cliQuickGetTx => '获取 TX'; @@ -1942,196 +1943,199 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_cliQuickVersion => '版本'; @override - String get repeater_cliQuickAdvertise => '发布'; + String get repeater_cliQuickAdvertise => '发布广告'; @override String get repeater_cliQuickClock => '时钟'; @override - String get repeater_cliHelpAdvert => '发送广告包'; + String get repeater_cliHelpAdvert => '发送广告资料包'; @override - String get repeater_cliHelpReboot => '重启设备。(请注意,可能会出现“超时”现象,这是正常现象)'; + String get repeater_cliHelpReboot => '重置设备。 (请注意,您可能会收到“超时”错误,这是正常的现象)'; @override String get repeater_cliHelpClock => '显示每个设备的当前时间。'; @override - String get repeater_cliHelpPassword => '设置设备的新管理员密码。'; + String get repeater_cliHelpPassword => '为设备设置新的管理员密码。'; @override String get repeater_cliHelpVersion => '显示设备版本和固件构建日期。'; @override - String get repeater_cliHelpClearStats => '重置各种统计数值为零。'; + String get repeater_cliHelpClearStats => '重置各种统计指标,将其设置为零。'; @override - String get repeater_cliHelpSetAf => '设置空闲时间因子。'; + String get repeater_cliHelpSetAf => '设置时间因素。'; @override - String get repeater_cliHelpSetTx => '设置 LoRa 传输功率 (重置生效)'; + String get repeater_cliHelpSetTx => + '设置 LoRa 传输功率,单位为 dBm (相对于参考值)。 (重启以应用更改)'; @override - String get repeater_cliHelpSetRepeat => '启用或禁用此节点的重复器角色。'; + String get repeater_cliHelpSetRepeat => '启用或禁用此节点的重复器功能。'; @override String get repeater_cliHelpSetAllowReadOnly => - '(房间服务器) 如果“开”了,则空密码登录将被允许,但不能向房间发布内容。(仅限读取)'; + '(房间服务器)如果设置为“开启”,则允许使用空密码登录,但无法向房间发送消息(只能进行读取)。'; @override - String get repeater_cliHelpSetFloodMax => '设置最大换路包数量(如果 >= 最大,则不转发包)。'; + String get repeater_cliHelpSetFloodMax => '设置最大传入数据包的跳数(如果大于或等于最大值,则不进行转发)。'; @override String get repeater_cliHelpSetIntThresh => - '设置干扰阈值(以 dB 为单位)。默认值为 14。将设置为 0 以禁用通道干扰检测。'; + '设置干扰阈值(以dB为单位)。默认值为14。将设置为0以禁用频道干扰检测。'; @override String get repeater_cliHelpSetAgcResetInterval => - '设置间隔以重置自动增益控制器。将设置为 0 以禁用。'; + '设置间隔时间,用于重置自动增益控制器。设置为 0 以禁用。'; @override - String get repeater_cliHelpSetMultiAcks => '启用或禁用“双 ACKs”功能。'; + String get repeater_cliHelpSetMultiAcks => '启用或禁用“双重确认”功能。'; @override String get repeater_cliHelpSetAdvertInterval => - '设置定时器间隔时间为分钟,以发送本地(零跳)广告包。将设置为0以禁用。'; + '设置定时器间隔,单位为分钟,用于发送本地(无中继)的广告数据包。 将设置为 0 以禁用。'; @override String get repeater_cliHelpSetFloodAdvertInterval => - '设置定时器间隔时间为小时,以发送洪水广告包。将设置为 0 以禁用。'; + '设置定时器间隔时间为小时,以便发送广告信息包。将设置为 0 以禁用。'; @override String get repeater_cliHelpSetGuestPassword => - '设置/更新客人密码。(对于重复器,客人在登录时可以发送“获取统计”请求)'; + '设置/更新访客密码。 (对于访客,登录请求可以发送“获取统计”请求)'; @override String get repeater_cliHelpSetName => '设置广告名称。'; @override - String get repeater_cliHelpSetLat => '设置广告地图纬度(十进制度)'; + String get repeater_cliHelpSetLat => '设置广告地图的纬度。(以十进制表示)'; @override - String get repeater_cliHelpSetLon => '设置广告地图经度 (十进位)'; + String get repeater_cliHelpSetLon => '设置广告地图的经度。 (十进制度)'; @override - String get repeater_cliHelpSetRadio => '设置全新的无线电参数,并保存到偏好设置。需要执行“重启”命令才能应用。'; + String get repeater_cliHelpSetRadio => '完全重新设置无线电参数,并保存到偏好设置。需要执行“重启”命令才能生效。'; @override String get repeater_cliHelpSetRxDelay => - '设置(实验性)的基础(必须大于 1 才能生效)是用于对接收到的数据包应用轻微延迟,基于信号强度/得分。将设置为 0 以禁用。'; + '设置(实验性):设置一个基础值(必须大于1才能生效),用于对接收到的数据包进行轻微延迟处理,该延迟值基于信号强度/评分。将该值设置为0以禁用。'; @override String get repeater_cliHelpSetTxDelay => - '设置一个与时间-在空气中(time-on-air)的系数,用于洪水模式的数据包,并结合随机插槽系统,以延迟其转发。(以降低碰撞的可能性)'; + '通过将一个因子与“浮动模式”数据包的时间在空中停留时间相乘,并结合随机的“时隙”系统,来延迟其转发,从而降低数据包冲突的概率。'; @override String get repeater_cliHelpSetDirectTxDelay => - '与txdelay相同,但用于为直接模式包的转发应用随机延迟。'; + '与txdelay相同,但用于对直接模式数据包的转发进行随机延迟。'; @override - String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥梁'; + String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥接。'; @override - String get repeater_cliHelpSetBridgeDelay => '设置在重新发送数据包之前延迟时间。'; + String get repeater_cliHelpSetBridgeDelay => '在重新发送数据包之前,设置延迟时间。'; @override - String get repeater_cliHelpSetBridgeSource => '选择桥梁是否会重传接收到的数据包或发送的数据包。'; + String get repeater_cliHelpSetBridgeSource => '选择桥接器是否会转发收到的数据包,还是转发发送的数据包。'; @override - String get repeater_cliHelpSetBridgeBaud => '设置rs232桥接的串口链路波特率。'; + String get repeater_cliHelpSetBridgeBaud => '为 RS232 桥接设置串行链路的波特率。'; @override - String get repeater_cliHelpSetBridgeSecret => '设置 espnow 桥的秘密。'; + String get repeater_cliHelpSetBridgeSecret => '设置 ESPNOW 桥的秘密。'; @override - String get repeater_cliHelpSetAdcMultiplier => '设置自定义因子以调整报告的电池电压(仅限部分板卡支持)。'; + String get repeater_cliHelpSetAdcMultiplier => + '设置自定义因子,用于调整报告的电池电压(仅在特定板上支持)。'; @override String get repeater_cliHelpTempRadio => - '设置临时无线电参数,持续指定的分钟数,之后恢复为原始无线电参数。(不保存到偏好设置)。'; + '设置临时收音机参数,持续指定分钟数,之后恢复到原始收音机参数。(不保存到偏好设置)。'; @override String get repeater_cliHelpSetPerm => - '修改ACL。如果“权限”为零,则删除匹配的条目(通过pubkey前缀)。如果pubkey-hex的完整长度且当前不在ACL中,则添加新条目。通过匹配pubkey前缀更新条目。权限位因固件角色而异,但低2位为:0(Guest)、1(只读)、2(读写)、3(Admin)'; + '修改 ACL。如果 \"permissions\" 的值为 0,则删除与 pubkey 相关的条目。如果 pubkey-hex 完整且当前不在 ACL 中,则添加新的条目。通过匹配 pubkey 相关的前缀来更新条目。不同固件角色的权限位有所不同,但低 2 位分别对应:0 (访客)、1 (只读)、2 (读写)、3 (管理员)。'; @override - String get repeater_cliHelpGetBridgeType => '获取桥接类型:无,RS232,ESPNow'; + String get repeater_cliHelpGetBridgeType => '支持桥接模式、RS232、ESPNOW。'; @override String get repeater_cliHelpLogStart => '开始将数据包记录到文件系统。'; @override - String get repeater_cliHelpLogStop => '停止将数据包记录到文件系统。'; + String get repeater_cliHelpLogStop => '停止将数据包记录写入文件系统。'; @override - String get repeater_cliHelpLogErase => '删除文件系统中的包日志。'; + String get repeater_cliHelpLogErase => '从文件系统中删除所有已记录的包信息。'; @override String get repeater_cliHelpNeighbors => - '显示通过零跳广告收听的其他重复节点列表。 每行是 id-prefix-hex:时间戳:snr-times-4'; + '显示了通过零跳广告收到的其他复用节点列表。 每行包含:id-前缀-十六进制:时间戳:信噪比(4次)'; @override String get repeater_cliHelpNeighborRemove => - '移除邻居列表中第一个匹配的条目(通过十六进制 pubkey 前缀)。'; + '从邻居列表中删除第一个匹配项(通过十六进制的 pubkey 前缀)。'; @override - String get repeater_cliHelpRegion => '(仅显示区域) 列出所有已定义的区域和当前的防洪权限。'; + String get repeater_cliHelpRegion => '(仅限序列)列出所有已定义的区域以及当前的防洪许可。'; @override String get repeater_cliHelpRegionLoad => - '注意:这是一个特殊的多命令调用。 随后的每个命令都是一个区域名称(用空格缩进以指示父级层次结构,至少需要一个空格)。 以发送一个空行/命令结束。'; + '请注意:这是一个特殊的、包含多个命令的调用方式。 之后的每个命令都是一个区域名称(使用空格进行缩进,以表示父级关系,至少需要一个空格)。 结束方式是通过发送一个空行/命令。'; @override String get repeater_cliHelpRegionGet => - '搜索具有给定名称前缀的区域(或“”用于全局范围)。回复为“-> region-name (parent-name) ‘F’”'; + '搜索具有指定名称前缀的区域(或使用“*”表示全局范围)。 返回结果为“-> region-name (parent-name) \'F\'”'; @override - String get repeater_cliHelpRegionPut => '添加或更新区域定义,使用指定名称。'; + String get repeater_cliHelpRegionPut => '添加或更新一个区域定义,并指定其名称。'; @override - String get repeater_cliHelpRegionRemove => '删除指定名称的区域定义。(必须没有子区域)'; + String get repeater_cliHelpRegionRemove => + '删除具有指定名称的区域定义。 (必须与指定名称完全匹配,且不能有子区域)'; @override - String get repeater_cliHelpRegionAllowf => '设置指定区域的“洪水”权限。(“”代表全局/遗留范围)'; + String get repeater_cliHelpRegionAllowf => '为指定区域设置“洪水”权限。(“*”表示全局/旧版本范围)'; @override String get repeater_cliHelpRegionDenyf => - '移除指定区域的‘F’lood权限。 (注意:目前阶段不建议在此范围内使用,尤其是全局/旧版范围!!)'; + '移除指定区域的“洪水”权限。(请注意:目前不建议在全局/旧版本中使用此功能!!)'; @override - String get repeater_cliHelpRegionHome => '回复当前“主页”区域。 (注意尚未应用,保留用于未来)'; + String get repeater_cliHelpRegionHome => '回复当前“主区域”。(此功能尚未应用,仅供未来使用)'; @override - String get repeater_cliHelpRegionHomeSet => '设置‘主页’区域。'; + String get repeater_cliHelpRegionHomeSet => '设置“主”区域。'; @override - String get repeater_cliHelpRegionSave => '保存区域列表/地图到存储。'; + String get repeater_cliHelpRegionSave => '将区域列表/地图保存到存储中。'; @override String get repeater_cliHelpGps => - '显示GPS状态。当GPS关闭时,回复仅为“关闭”,如果已开启,则回复为“开启”、“状态”、“定位”和卫星数量。'; + '显示 GPS 状态。当 GPS 处于关闭状态时,它只会显示“关闭”;当 GPS 处于开启状态时,它会显示“开启”、“状态”、“定位”、“卫星数量”等信息。'; @override - String get repeater_cliHelpGpsOnOff => '切换 GPS 开启状态。'; + String get repeater_cliHelpGpsOnOff => '切换 GPS 设备的电源状态。'; @override - String get repeater_cliHelpGpsSync => '同步节点时间与 GPS 钟。'; + String get repeater_cliHelpGpsSync => '将节点时间与 GPS 钟同步。'; @override - String get repeater_cliHelpGpsSetLoc => '设置节点位置至 GPS 坐标并保存偏好设置。'; + String get repeater_cliHelpGpsSetLoc => '将节点的坐标设置为 GPS 坐标,并保存设置。'; @override String get repeater_cliHelpGpsAdvert => - '提供节点广告配置位置:\n- none:不包含位置在广告中\n- share:分享 GPS 位置(来自 SensorManager)\n- prefs:在偏好设置中投放位置'; + '设置节点的位置广告配置:\n- none:不将位置信息包含在广告中\n- share:共享 GPS 位置(从 SensorManager 获取)\n- prefs:在偏好设置中展示的位置'; @override - String get repeater_cliHelpGpsAdvertSet => '设置广告位置配置。'; + String get repeater_cliHelpGpsAdvertSet => '设置广告的位置配置。'; @override String get repeater_commandsListTitle => '命令列表'; @override - String get repeater_commandsListNote => '注意:对于各种“设置...”命令,也存在“获取...”命令。'; + String get repeater_commandsListNote => '请注意:对于各种“set ...”命令,也存在“get ...”命令。'; @override String get repeater_general => '通用'; @@ -2146,29 +2150,29 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_logging => '记录'; @override - String get repeater_neighborsRepeaterOnly => '邻居(仅限重复器)'; + String get repeater_neighborsRepeaterOnly => '邻居(仅限重复功能)'; @override - String get repeater_regionManagementRepeaterOnly => '区域管理(仅限重复器)'; + String get repeater_regionManagementRepeaterOnly => '区域管理(仅限重复站点)'; @override - String get repeater_regionNote => '区域命令已推出,用于管理区域定义和权限。'; + String get repeater_regionNote => '区域命令已引入,用于管理区域定义和权限。'; @override - String get repeater_gpsManagement => 'GPS管理'; + String get repeater_gpsManagement => 'GPS 管理'; @override - String get repeater_gpsNote => 'GPS 命令已引入用于管理与位置相关的主题。'; + String get repeater_gpsNote => '已引入 GPS 命令,用于管理与位置相关的任务。'; @override - String get telemetry_receivedData => '接收遥测数据'; + String get telemetry_receivedData => '接收到的遥测数据'; @override String get telemetry_requestTimeout => '遥测请求超时。'; @override String telemetry_errorLoading(String error) { - return '错误加载遥测数据:$error'; + return 'Error loading telemetry: $error'; } @override @@ -2186,7 +2190,7 @@ class AppLocalizationsZh extends AppLocalizations { String get telemetry_voltageLabel => '电压'; @override - String get telemetry_mcuTemperatureLabel => 'MCU 温度'; + String get telemetry_mcuTemperatureLabel => 'MCU 的温度'; @override String get telemetry_temperatureLabel => '温度'; @@ -2215,30 +2219,30 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get neighbors_receivedData => '收到邻居数据'; + String get neighbors_receivedData => '已收到邻居信息'; @override - String get neighbors_requestTimedOut => '邻居请求超时处理。'; + String get neighbors_requestTimedOut => '邻居要求停止干扰。'; @override String neighbors_errorLoading(String error) { - return '加载邻居时出错:$error'; + return 'Error loading neighbors: $error'; } @override - String get neighbors_repeatersNeighbours => '重复器邻居'; + String get neighbors_repeatersNeighbours => '重复使用的邻居'; @override - String get neighbors_noData => '没有可用的邻居数据。'; + String get neighbors_noData => '没有可用的邻居信息。'; @override String neighbors_unknownContact(String pubkey) { - return '未知$pubkey'; + return 'Unknown $pubkey'; } @override String neighbors_heardAgo(String time) { - return '听到的时间:$time前'; + return 'Heard: $time ago'; } @override @@ -2251,10 +2255,10 @@ class AppLocalizationsZh extends AppLocalizations { String get channelPath_otherObservedPaths => '其他观察到的路径'; @override - String get channelPath_repeaterHops => '重复跳跃'; + String get channelPath_repeaterHops => '复用跳跃'; @override - String get channelPath_noHopDetails => '此包的详细信息未提供。'; + String get channelPath_noHopDetails => '对于此包,未提供详细信息。'; @override String get channelPath_messageDetails => '消息详情'; @@ -2274,15 +2278,15 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channelPath_observedLabel => '已观察'; + String get channelPath_observedLabel => '观察到的'; @override String channelPath_observedPathTitle(int index, String hops) { - return '观察路径 $index • $hops'; + return 'Observed path $index • $hops'; } @override - String get channelPath_noLocationData => '没有位置数据'; + String get channelPath_noLocationData => '没有位置信息'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2305,30 +2309,30 @@ class AppLocalizationsZh extends AppLocalizations { @override String channelPath_observedZeroOf(int total) { - return '0 of $total 跳跃'; + return '0 of $total hops'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '已观察到 $observed 步中的 $total 步'; + return '$observed of $total hops'; } @override - String get channelPath_mapTitle => '路径地图'; + String get channelPath_mapTitle => '路线图'; @override - String get channelPath_noRepeaterLocations => '此路径没有可用的重复器位置。'; + String get channelPath_noRepeaterLocations => '这条路径上没有可用的中继器位置。'; @override String channelPath_primaryPath(int index) { - return '路径 $index (主)'; + return '路径 $index (主要路径)'; } @override String get channelPath_pathLabelTitle => '路径'; @override - String get channelPath_observedPathHeader => '已观察路径'; + String get channelPath_observedPathHeader => '观察路径'; @override String channelPath_selectedPathLabel(String label, String prefixes) { @@ -2336,19 +2340,19 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channelPath_noHopDetailsAvailable => '此包的跳跃详情不可用。'; + String get channelPath_noHopDetailsAvailable => '对于此包裹,尚无详细信息。'; @override - String get channelPath_unknownRepeater => '未知重复器'; + String get channelPath_unknownRepeater => '未知的重复设备'; @override String get community_title => '社区'; @override - String get community_create => '创建社区'; + String get community_create => '建立社区'; @override - String get community_createDesc => '创建新的社区并可通过二维码分享。'; + String get community_createDesc => '创建一个新的社群,并通过二维码进行分享。'; @override String get community_join => '加入'; @@ -2358,20 +2362,20 @@ class AppLocalizationsZh extends AppLocalizations { @override String community_joinConfirmation(String name) { - return '您想加入社区 \"$name\" 吗?'; + return 'Do you want to join the community \"$name\"?'; } @override String get community_scanQr => '扫描社区二维码'; @override - String get community_scanInstructions => '将相机对准社区二维码'; + String get community_scanInstructions => '将相机对准社区的二维码。'; @override String get community_showQr => '显示二维码'; @override - String get community_publicChannel => '社区公开'; + String get community_publicChannel => '社区公共'; @override String get community_hashtagChannel => '社区标签'; @@ -2384,12 +2388,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String community_created(String name) { - return '社区“$name”已创建'; + return 'Community \"$name\" created'; } @override String community_joined(String name) { - return '加入社区 \"$name\"'; + return 'Joined community \"$name\"'; } @override @@ -2397,128 +2401,128 @@ class AppLocalizationsZh extends AppLocalizations { @override String community_qrInstructions(String name) { - return '扫描此二维码加入$name'; + return 'Scan this QR code to join \"$name\"'; } @override - String get community_hashtagPrivacyHint => '社区标签频道仅社区成员可加入'; + String get community_hashtagPrivacyHint => '仅社区成员才能加入社区话题标签的频道。'; @override String get community_invalidQrCode => '无效的社区二维码'; @override - String get community_alreadyMember => '已经是会员了'; + String get community_alreadyMember => '已经是会员'; @override String community_alreadyMemberMessage(String name) { - return '您已经是 \"$name\" 的会员。'; + return 'You are already a member of \"$name\".'; } @override - String get community_addPublicChannel => '添加社区公共频道'; + String get community_addPublicChannel => '添加公共频道'; @override String get community_addPublicChannelHint => '自动添加该社区的公共频道'; @override - String get community_noCommunities => '尚未加入任何社区'; + String get community_noCommunities => '目前还没有任何社区加入。'; @override - String get community_scanOrCreate => '扫描二维码或创建社区开始'; + String get community_scanOrCreate => '扫描二维码或创建社群,即可开始。'; @override - String get community_manageCommunities => '管理社群'; + String get community_manageCommunities => '管理社区'; @override String get community_delete => '退出社区'; @override String community_deleteConfirm(String name) { - return '退出 \"$name\"?'; + return '是否要删除\"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return '这也将删除 $count 个频道及其消息。'; + return '这将同时删除 $count 个频道及其所有消息。'; } @override String community_deleted(String name) { - return '已退出社区 \"$name\"'; + return 'Left community \"$name\"'; } @override - String get community_regenerateSecret => '重新生成密钥'; + String get community_regenerateSecret => '恢复秘密'; @override String community_regenerateSecretConfirm(String name) { - return '重新生成“$name”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。'; + return '[保存:$name]\n是否需要重新生成\"$name\"的密钥?所有成员都需要扫描新的二维码才能继续进行通信。'; } @override - String get community_regenerate => '重新生成'; + String get community_regenerate => '再生'; @override String community_secretRegenerated(String name) { - return '密码已重置为“$name”'; + return '[保护对象:$name]\n秘密已恢复至\"$name\"'; } @override - String get community_updateSecret => '更新密钥'; + String get community_updateSecret => '更新秘密'; @override String community_secretUpdated(String name) { - return '密码已更新为“$name”'; + return '“$name”的秘密已更新'; } @override String community_scanToUpdateSecret(String name) { - return '扫描新的二维码更新\"$name\"的密码'; + return 'Scan the new QR code to update the secret for \"$name\"'; } @override String get community_addHashtagChannel => '添加社区标签'; @override - String get community_addHashtagChannelDesc => '添加一个话题频道给此社区'; + String get community_addHashtagChannelDesc => '为这个社区创建一个带有话题标签的频道'; @override String get community_selectCommunity => '选择社区'; @override - String get community_regularHashtag => '常规话题标签'; + String get community_regularHashtag => '常用标签'; @override - String get community_regularHashtagDesc => '公共话题(任何人都可以加入)'; + String get community_regularHashtagDesc => '公共话题标签(任何人都可以参与)'; @override String get community_communityHashtag => '社区标签'; @override - String get community_communityHashtagDesc => '仅限社区成员使用'; + String get community_communityHashtagDesc => '仅限社区成员'; @override String community_forCommunity(String name) { - return '对于 $name'; + return 'For $name'; } @override String get listFilter_tooltip => '筛选和排序'; @override - String get listFilter_sortBy => '按类型排序'; + String get listFilter_sortBy => '按排序'; @override String get listFilter_latestMessages => '最新消息'; @override - String get listFilter_heardRecently => '最近听说'; + String get listFilter_heardRecently => '最近听到的'; @override - String get listFilter_az => 'A-Z'; + String get listFilter_az => 'A 到 Z'; @override - String get listFilter_filters => '筛选'; + String get listFilter_filters => '过滤器'; @override String get listFilter_all => '全部'; @@ -2533,46 +2537,88 @@ class AppLocalizationsZh extends AppLocalizations { String get listFilter_roomServers => '房间服务器'; @override - String get listFilter_unreadOnly => '未读消息'; + String get listFilter_unreadOnly => '仅显示未读消息'; @override - String get listFilter_newGroup => '新组'; + String get listFilter_newGroup => '新的团体'; @override - String get pathTrace_you => '你'; + String get pathTrace_you => '您'; @override String get pathTrace_failed => '路径追踪失败。'; @override - String get pathTrace_notAvailable => '路径追踪不可用'; + String get pathTrace_notAvailable => '无法获取路径信息。'; @override - String get pathTrace_refreshTooltip => '刷新路径追踪'; + String get pathTrace_refreshTooltip => '重新绘制路径。'; @override String get contacts_pathTrace => '路径追踪'; @override - String get contacts_ping => 'ping'; + String get contacts_ping => '乒'; @override - String get contacts_repeaterPathTrace => '路径追踪到中继器'; + String get contacts_repeaterPathTrace => '追踪路径至中继器'; @override - String get contacts_repeaterPing => 'Ping 中继器'; + String get contacts_repeaterPing => '中继器'; @override - String get contacts_roomPathTrace => '路径追踪至房间服务器'; + String get contacts_roomPathTrace => '追踪到房间服务器'; @override - String get contacts_roomPing => 'Ping 房间服务器'; + String get contacts_roomPing => '会议室服务器'; @override - String get contacts_chatTraceRoute => '路径追踪'; + String get contacts_chatTraceRoute => '路径跟踪路线'; @override String contacts_pathTraceTo(String name) { - return '追踪路由到 $name'; + return '追踪路径至 $name'; } + + @override + String get contacts_clipboardEmpty => '剪贴板为空。'; + + @override + String get contacts_invalidAdvertFormat => '无效的联系信息'; + + @override + String get contacts_contactImported => '已建立联系。'; + + @override + String get contacts_contactImportFailed => '未能导入联系人。'; + + @override + String get contacts_zeroHopAdvert => '零跳广告'; + + @override + String get contacts_floodAdvert => '防洪广告'; + + @override + String get contacts_copyAdvertToClipboard => '复制广告到剪贴板'; + + @override + String get contacts_addContactFromClipboard => '从剪贴板添加联系人'; + + @override + String get contacts_ShareContact => '复制联系方式到剪贴板'; + + @override + String get contacts_ShareContactZeroHop => '通过广告分享联系方式'; + + @override + String get contacts_zeroHopContactAdvertSent => '通过广告获取联系方式。'; + + @override + String get contacts_zeroHopContactAdvertFailed => '发送联系方式失败。'; + + @override + String get contacts_contactAdvertCopied => '广告内容已复制到剪贴板。'; + + @override + String get contacts_contactAdvertCopyFailed => '将广告复制到剪贴板操作失败。'; } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c118b9d..c941461 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,10 +1,11 @@ { "@@locale": "zh", "appTitle": "MeshCore Open", - "nav_contacts": "联系人", + "nav_contacts": "联系方式", "nav_channels": "频道", "nav_map": "地图", "common_cancel": "取消", + "common_ok": "好的", "common_connect": "连接", "common_unknownDevice": "未知设备", "common_save": "保存", @@ -14,18 +15,18 @@ "common_add": "添加", "common_settings": "设置", "common_disconnect": "断开", - "common_connected": "已连接", + "common_connected": "连接", "common_disconnected": "断开", - "common_create": "创建", + "common_create": "创造", "common_continue": "继续", "common_share": "分享", "common_copy": "复制", "common_retry": "重试", "common_hide": "隐藏", - "common_remove": "删除", + "common_remove": "移除", "common_enable": "启用", "common_disable": "禁用", - "common_reboot": "重启", + "common_reboot": "重新启动", "common_loading": "正在加载...", "common_notAvailable": "—", "common_voltageValue": "{volts} V", @@ -44,12 +45,12 @@ } } }, - "scanner_title": "MeshCore Open", - "scanner_scanning": "扫描设备…", - "scanner_connecting": "连接中...", - "scanner_disconnecting": "断开中...", + "scanner_title": "MeshCore 开放", + "scanner_scanning": "正在搜索设备...", + "scanner_connecting": "正在连接...", + "scanner_disconnecting": "断开连接...", "scanner_notConnected": "未连接", - "scanner_connectedTo": "已连接至 {deviceName}", + "scanner_connectedTo": "已连接到 {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -57,9 +58,9 @@ } } }, - "scanner_searchingDevices": "搜索 MeshCore 设备...", - "scanner_tapToScan": "点击扫描以查找MeshCore设备", - "scanner_connectionFailed": "连接失败:{error}", + "scanner_searchingDevices": "正在搜索 MeshCore 设备...", + "scanner_tapToScan": "点击“扫描”功能,以查找 MeshCore 设备。", + "scanner_connectionFailed": "Connection failed: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -70,7 +71,7 @@ "scanner_stop": "停止", "scanner_scan": "扫描", "device_quickSwitch": "快速切换", - "device_meshcore": "MeshCore", + "device_meshcore": "网格核心", "settings_title": "设置", "settings_deviceInfo": "设备信息", "settings_appSettings": "应用设置", @@ -80,38 +81,42 @@ "settings_nodeNameNotSet": "未设置", "settings_nodeNameHint": "请输入节点名称", "settings_nodeNameUpdated": "姓名已更新", - "settings_radioSettings": "无线设置", - "settings_radioSettingsSubtitle": "频率,功率,扩展因子", - "settings_radioSettingsUpdated": "射频设置已更新", - "settings_location": "位置", - "settings_locationSubtitle": "GPS坐标", - "settings_locationUpdated": "位置已更新", - "settings_locationBothRequired": "请输入纬度和经度。", - "settings_locationInvalid": "无效的纬度或经度。", + "settings_radioSettings": "收音机设置", + "settings_radioSettingsSubtitle": "频率、功率、扩频因子", + "settings_radioSettingsUpdated": "收音机设置已更新", + "settings_location": "地点", + "settings_locationSubtitle": "GPS 坐标", + "settings_locationUpdated": "位置和 GPS 设置已更新", + "settings_locationBothRequired": "请输入经度和纬度。", + "settings_locationInvalid": "无效的经度和纬度。", + "settings_locationGPSEnable": "开启 GPS 功能", + "settings_locationGPSEnableSubtitle": "使 GPS 能够自动更新位置。", + "settings_locationIntervalSec": "GPS 间隔时间(秒)", + "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_latitude": "纬度", "settings_longitude": "经度", "settings_privacyMode": "隐私模式", - "settings_privacyModeSubtitle": "隐藏在广告中的姓名/位置", - "settings_privacyModeToggle": "开启隐私模式以隐藏您的姓名和位置在广告中的显示。", + "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", + "settings_privacyModeToggle": "切换隐私模式,以隐藏您的姓名和位置,从而在广告中保护您的个人信息。", "settings_privacyModeEnabled": "隐私模式已启用", - "settings_privacyModeDisabled": "隐私模式已禁用", - "settings_actions": "操作", - "settings_sendAdvertisement": "发送广告", - "settings_sendAdvertisementSubtitle": "现在已广播", - "settings_advertisementSent": "广告已发送", + "settings_privacyModeDisabled": "隐私模式已关闭", + "settings_actions": "行动", + "settings_sendAdvertisement": "发布广告", + "settings_sendAdvertisementSubtitle": "现已开始进行广播节目", + "settings_advertisementSent": "已发送广告", "settings_syncTime": "同步时间", - "settings_syncTimeSubtitle": "将设备时钟设置为手机时间", + "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", "settings_timeSynchronized": "时间同步", "settings_refreshContacts": "刷新联系人", - "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", + "settings_refreshContactsSubtitle": "从设备中重新加载联系人列表", "settings_rebootDevice": "重启设备", - "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", - "settings_rebootDeviceConfirm": "您确定要重启设备吗?您将会断开连接。", + "settings_rebootDeviceSubtitle": "重新启动 MeshCore 设备", + "settings_rebootDeviceConfirm": "您确定要重启设备吗?这将导致您与设备断开连接。", "settings_debug": "调试", - "settings_bleDebugLog": "蓝牙调试日志", - "settings_bleDebugLogSubtitle": "蓝牙命令、响应和原始数据", - "settings_appDebugLog": "应用调试日志", - "settings_appDebugLogSubtitle": "应用调试消息", + "settings_bleDebugLog": "BLE 调试日志", + "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", + "settings_appDebugLog": "应用程序调试日志", + "settings_appDebugLogSubtitle": "应用程序调试消息", "settings_about": "关于", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -121,31 +126,31 @@ } } }, - "settings_aboutLegalese": "2024 MeshCore 开放源代码项目", - "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 网状网络设备。", + "settings_aboutLegalese": "2026 MeshCore 开源项目", + "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", "settings_infoName": "姓名", "settings_infoId": "ID", "settings_infoStatus": "状态", "settings_infoBattery": "电池", "settings_infoPublicKey": "公钥", "settings_infoContactsCount": "联系人数量", - "settings_infoChannelCount": "频道数量", + "settings_infoChannelCount": "通道数量", "settings_presets": "预设", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", + "settings_preset915Mhz": "915 兆赫", + "settings_preset868Mhz": "868 兆赫", + "settings_preset433Mhz": "433 兆赫", "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "无效频率 (300-2500 MHz)", + "settings_frequencyInvalid": "无效频率(300-2500 MHz)", "settings_bandwidth": "带宽", - "settings_spreadingFactor": "扩散因子", + "settings_spreadingFactor": "传播系数", "settings_codingRate": "编码速率", - "settings_txPower": "TX Power (dBm)", + "settings_txPower": "TX 功率(dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "无效的 TX 电功率 (0-22 dBm)", + "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", "settings_longRange": "远距离", - "settings_fastSpeed": "快速速度", - "settings_error": "错误:{message}", + "settings_fastSpeed": "高速", + "settings_error": "[保存:{message}]\n错误:{message}", "@settings_error": { "placeholders": { "message": { @@ -156,48 +161,50 @@ "appSettings_title": "应用设置", "appSettings_appearance": "外观", "appSettings_theme": "主题", - "appSettings_themeSystem": "系统默认", + "appSettings_themeSystem": "系统默认设置", "appSettings_themeLight": "光", - "appSettings_themeDark": "深色", + "appSettings_themeDark": "黑暗", "appSettings_language": "语言", - "appSettings_languageSystem": "系统默认", - "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", - "appSettings_languageDe": "Deutsch", - "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", - "appSettings_languageIt": "Italiano", + "appSettings_languageSystem": "系统默认设置", + "appSettings_languageEn": "英语", + "appSettings_languageFr": "法语", + "appSettings_languageEs": "西班牙语", + "appSettings_languageDe": "德语", + "appSettings_languagePl": "波兰语", + "appSettings_languageSl": "斯洛文语", + "appSettings_languagePt": "葡萄牙语", + "appSettings_languageIt": "意大利语", "appSettings_languageZh": "中文", - "appSettings_languageSv": "Svenska", - "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSv": "瑞典语", + "appSettings_languageNl": "荷兰语", + "appSettings_languageSk": "斯洛伐克语", + "appSettings_languageBg": "保加利亚", + "appSettings_languageRu": "俄语", + "appSettings_languageUk": "乌克兰", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", - "appSettings_notificationPermissionDenied": "通知权限被拒绝", + "appSettings_notificationPermissionDenied": "权限被拒绝", "appSettings_notificationsEnabled": "通知已启用", "appSettings_notificationsDisabled": "通知已关闭", "appSettings_messageNotifications": "消息通知", - "appSettings_messageNotificationsSubtitle": "显示收到新消息时的通知", + "appSettings_messageNotificationsSubtitle": "在收到新消息时显示通知", "appSettings_channelMessageNotifications": "频道消息通知", - "appSettings_channelMessageNotificationsSubtitle": "显示接收频道消息时的通知", + "appSettings_channelMessageNotificationsSubtitle": "在收到频道消息时,显示通知。", "appSettings_advertisementNotifications": "广告通知", - "appSettings_advertisementNotificationsSubtitle": "显示当新节点被发现时通知", - "appSettings_messaging": "消息", - "appSettings_clearPathOnMaxRetry": "清除最大重试路径", - "appSettings_clearPathOnMaxRetrySubtitle": "重置联系人路径,在5次发送失败尝试后", - "appSettings_pathsWillBeCleared": "路径将在5次失败重试后清除", - "appSettings_pathsWillNotBeCleared": "路径不会自动清理", - "appSettings_autoRouteRotation": "自动路径旋转", - "appSettings_autoRouteRotationSubtitle": "在最佳路径和洪水模式之间切换", + "appSettings_advertisementNotificationsSubtitle": "在发现新的节点时,显示通知。", + "appSettings_messaging": "信息传递", + "appSettings_clearPathOnMaxRetry": "关于“最大重试”的清晰说明", + "appSettings_clearPathOnMaxRetrySubtitle": "在尝试发送失败后 5 次,重置联系路径。", + "appSettings_pathsWillBeCleared": "如果尝试 5 次后仍然失败,则将重新规划路径。", + "appSettings_pathsWillNotBeCleared": "路径不会自动清除。", + "appSettings_autoRouteRotation": "自动路径轮换", + "appSettings_autoRouteRotationSubtitle": "在最佳路径和防洪模式之间切换", "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", "appSettings_battery": "电池", "appSettings_batteryChemistry": "电池化学", - "appSettings_batteryChemistryPerDevice": "设置每个设备 ({deviceName})", + "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -205,20 +212,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "连接设备以选择", - "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", + "appSettings_batteryChemistryConnectFirst": "连接到设备以进行选择", + "appSettings_batteryNmc": "18650 型号,NMC 电池(3.0-4.2V)", "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", - "appSettings_batteryLipo": "LiPo (3.0-4.2V)", - "appSettings_mapDisplay": "地图显示", - "appSettings_showRepeaters": "显示循环器", + "appSettings_batteryLipo": "锂离子电池 (3.0-4.2V)", + "appSettings_mapDisplay": "地图展示", + "appSettings_showRepeaters": "显示重复", "appSettings_showRepeatersSubtitle": "在地图上显示重复节点", "appSettings_showChatNodes": "显示聊天节点", "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", "appSettings_showOtherNodes": "显示其他节点", - "appSettings_showOtherNodesSubtitle": "显示其他节点类型在地图上", - "appSettings_timeFilter": "时间筛选", + "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", + "appSettings_timeFilter": "时间过滤器", "appSettings_timeFilterShowAll": "显示所有节点", - "appSettings_timeFilterShowLast": "显示来自过去 {hours} 小时的节点", + "appSettings_timeFilterShowLast": "Show nodes from last {hours} hours", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -227,15 +234,15 @@ } }, "appSettings_mapTimeFilter": "地图时间筛选", - "appSettings_showNodesDiscoveredWithin": "显示发现的节点在:", + "appSettings_showNodesDiscoveredWithin": "显示在以下范围内发现的节点:", "appSettings_allTime": "所有时间", - "appSettings_lastHour": "最后小时", - "appSettings_last6Hours": "最后6小时", - "appSettings_last24Hours": "最后24小时", + "appSettings_lastHour": "过去一小时", + "appSettings_last6Hours": "过去6小时", + "appSettings_last24Hours": "过去24小时", "appSettings_lastWeek": "上周", "appSettings_offlineMapCache": "离线地图缓存", "appSettings_noAreaSelected": "未选择任何区域", - "appSettings_areaSelectedZoom": "选中的区域(缩放至 {minZoom} - {maxZoom})", + "appSettings_areaSelectedZoom": "已选择区域(缩放至 {minZoom} - {maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -247,18 +254,18 @@ } }, "appSettings_debugCard": "调试", - "appSettings_appDebugLogging": "应用调试日志", - "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以供故障排除", - "appSettings_appDebugLoggingEnabled": "应用调试日志已启用", - "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", - "contacts_title": "联系人", - "contacts_noContacts": "还没有联系人", - "contacts_contactsWillAppear": "设备会广播时,联系人会显示", + "appSettings_appDebugLogging": "应用程序调试日志", + "appSettings_appDebugLoggingSubtitle": "用于故障排除的日志应用程序调试消息", + "appSettings_appDebugLoggingEnabled": "调试日志已启用", + "appSettings_appDebugLoggingDisabled": "应用程序调试日志已禁用", + "contacts_title": "联系方式", + "contacts_noContacts": "目前还没有联系人", + "contacts_contactsWillAppear": "当设备发布广告时,联系方式会显示。", "contacts_searchContacts": "搜索联系人...", - "contacts_noUnreadContacts": "未读联系人", + "contacts_noUnreadContacts": "没有未读通讯", "contacts_noContactsFound": "未找到任何联系人或群组", "contacts_deleteContact": "删除联系人", - "contacts_removeConfirm": "从联系人中删除 {contactName} 吗?", + "contacts_removeConfirm": "Remove {contactName} from contacts?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -266,11 +273,12 @@ } } }, - "contacts_manageRepeater": "管理重复项", - "contacts_roomLogin": "房间登录", - "contacts_openChat": "打开聊天", - "contacts_editGroup": "编辑组", - "contacts_deleteGroup": "删除分组", + "contacts_manageRepeater": "管理重复器", + "contacts_manageRoom": "管理房间服务器", + "contacts_roomLogin": "服务器登录", + "contacts_openChat": "开放聊天", + "contacts_editGroup": "编辑小组", + "contacts_deleteGroup": "删除群组", "contacts_deleteGroupConfirm": "删除\"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { @@ -279,10 +287,10 @@ } } }, - "contacts_newGroup": "新组", - "contacts_groupName": "组名", - "contacts_groupNameRequired": "组名不能为空", - "contacts_groupAlreadyExists": "组“{name}”已存在", + "contacts_newGroup": "新的团体", + "contacts_groupName": "团体名称", + "contacts_groupNameRequired": "需要提供组名称", + "contacts_groupAlreadyExists": "名为\"{name}\"的组已经存在", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -291,10 +299,10 @@ } }, "contacts_filterContacts": "筛选联系人...", - "contacts_noContactsMatchFilter": "未找到匹配您的筛选条件的结果", + "contacts_noContactsMatchFilter": "未找到符合您筛选条件的联系人", "contacts_noMembers": "没有会员", - "contacts_lastSeenNow": "最后一次登录时间现在", - "contacts_lastSeenMinsAgo": "最后一次出现 {minutes} 分前", + "contacts_lastSeenNow": "最后一次被看到的时间", + "contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -302,8 +310,8 @@ } } }, - "contacts_lastSeenHourAgo": "最后一次出现前1小时", - "contacts_lastSeenHoursAgo": "最后一次出现 {hours} 小时前", + "contacts_lastSeenHourAgo": "最后一次被看到的时间:1小时前", + "contacts_lastSeenHoursAgo": "Last seen {hours} hours ago", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -311,8 +319,8 @@ } } }, - "contacts_lastSeenDayAgo": "最后一次登录前一天", - "contacts_lastSeenDaysAgo": "最后一次出现 {days} 天前", + "contacts_lastSeenDayAgo": "最后一次被看到的时间是1天前", + "contacts_lastSeenDaysAgo": "Last seen {days} days ago", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -322,7 +330,7 @@ }, "channels_title": "频道", "channels_noChannelsConfigured": "未配置任何频道", - "channels_addPublicChannel": "添加公开频道", + "channels_addPublicChannel": "添加公共频道", "channels_searchChannels": "搜索频道...", "channels_noChannelsFound": "未找到任何频道", "channels_channelIndex": "频道 {index}", @@ -333,14 +341,14 @@ } } }, - "channels_hashtagChannel": "话题频道", - "channels_public": "公开", - "channels_private": "私有", - "channels_publicChannel": "公开频道", - "channels_privateChannel": "私聊频道", + "channels_hashtagChannel": "话题标签频道", + "channels_public": "公众", + "channels_private": "私人", + "channels_publicChannel": "公共频道", + "channels_privateChannel": "私密频道", "channels_editChannel": "编辑频道", "channels_deleteChannel": "删除频道", - "channels_deleteChannelConfirm": "删除\"{name}\"?此操作无法撤销。", + "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -348,7 +356,7 @@ } } }, - "channels_channelDeleted": "频道“{name}”已删除", + "channels_channelDeleted": "删除频道 \"{name}\"", "@channels_channelDeleted": { "placeholders": { "name": { @@ -360,12 +368,12 @@ "channels_channelIndexLabel": "频道索引", "channels_channelName": "频道名称", "channels_usePublicChannel": "使用公共频道", - "channels_standardPublicPsk": "标准公钥共享密钥", + "channels_standardPublicPsk": "标准公共PSK", "channels_pskHex": "PSK (十六进制)", - "channels_generateRandomPsk": "生成随机PSK", - "channels_enterChannelName": "请输入频道名称", - "channels_pskMustBe32Hex": "PSK 必须是 32 个十六进制字符", - "channels_channelAdded": "频道“{name}”已添加", + "channels_generateRandomPsk": "生成随机的PSK(正交相移键控)", + "channels_enterChannelName": "请在此处输入频道名称", + "channels_pskMustBe32Hex": "PSK 必须包含 32 个十六进制字符。", + "channels_channelAdded": "添加频道 \"{name}\"", "@channels_channelAdded": { "placeholders": { "name": { @@ -382,7 +390,7 @@ } }, "channels_smazCompression": "SMAZ 压缩", - "channels_channelUpdated": "频道“{name}”已更新", + "channels_channelUpdated": "频道 \"{name}\" 已更新", "@channels_channelUpdated": { "placeholders": { "name": { @@ -390,16 +398,28 @@ } } }, - "channels_publicChannelAdded": "公共频道已添加", - "channels_sortBy": "按类型排序", - "channels_sortManual": "手动", - "channels_sortAZ": "A-Z", + "channels_publicChannelAdded": "已添加公共频道", + "channels_sortBy": "按排序", + "channels_sortManual": "手册", + "channels_sortAZ": "A 到 Z", "channels_sortLatestMessages": "最新消息", "channels_sortUnread": "未读", - "chat_noMessages": "目前还没有消息", - "chat_sendMessageToStart": "发送消息开始", - "chat_originalMessageNotFound": "找不到原始消息", - "chat_replyingTo": "回复 {name}", + "channels_createPrivateChannel": "创建私密频道", + "channels_createPrivateChannelDesc": "使用秘密密钥进行保护。", + "channels_joinPrivateChannel": "加入私密频道", + "channels_joinPrivateChannelDesc": "手动输入密钥。", + "channels_joinPublicChannel": "加入公共频道", + "channels_joinPublicChannelDesc": "任何人都可以加入这个频道。", + "channels_joinHashtagChannel": "加入一个带有特定标签的频道", + "channels_joinHashtagChannelDesc": "任何人都可以加入带有特定标签的频道。", + "channels_scanQrCode": "扫描二维码", + "channels_scanQrCodeComingSoon": "即将发布", + "channels_enterHashtag": "输入标签", + "channels_hashtagHint": "例如:#团队", + "chat_noMessages": "目前还没有收到任何消息。", + "chat_sendMessageToStart": "发送消息以开始", + "chat_originalMessageNotFound": "无法找到原始消息", + "chat_replyingTo": "Replying to {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -407,7 +427,7 @@ } } }, - "chat_replyTo": "回复 {name}", + "chat_replyTo": "Reply to {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -415,8 +435,8 @@ } } }, - "chat_location": "位置", - "chat_sendMessageTo": "向{contactName}发送消息", + "chat_location": "地点", + "chat_sendMessageTo": "Send a message to {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -425,7 +445,7 @@ } }, "chat_typeMessage": "输入消息...", - "chat_messageTooLong": "消息太长了(最大 {maxBytes} 字节)。", + "chat_messageTooLong": "消息内容过长(最大 {maxBytes} 字节)。", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -435,8 +455,8 @@ }, "chat_messageCopied": "消息已复制", "chat_messageDeleted": "消息已删除", - "chat_retryingMessage": "重试", - "chat_retryCount": "重试 {current}/{max}", + "chat_retryingMessage": "重试消息", + "chat_retryCount": "Retry {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -447,32 +467,32 @@ } } }, - "chat_sendGif": "发送GIF", + "chat_sendGif": "发送 GIF 动画", "chat_reply": "回复", - "chat_addReaction": "添加反应", + "chat_addReaction": "添加评论", "chat_me": "我", "emojiCategorySmileys": "表情符号", "emojiCategoryGestures": "手势", - "emojiCategoryHearts": "心", - "emojiCategoryObjects": "对象", - "gifPicker_title": "选择一个 GIF", - "gifPicker_searchHint": "搜索GIF...", + "emojiCategoryHearts": "心脏", + "emojiCategoryObjects": "物体", + "gifPicker_title": "选择一个 GIF 动画", + "gifPicker_searchHint": "搜索 GIF 动画...", "gifPicker_poweredBy": "由 GIPHY 提供支持", "gifPicker_noGifsFound": "未找到 GIF 动画", - "gifPicker_failedLoad": "GIF 加载失败", - "gifPicker_failedSearch": "搜索GIF失败", - "gifPicker_noInternet": "无网络连接", - "debugLog_appTitle": "应用调试日志", - "debugLog_bleTitle": "蓝牙调试日志", + "gifPicker_failedLoad": "无法加载 GIF 动画", + "gifPicker_failedSearch": "未能搜索 GIF 动画", + "gifPicker_noInternet": "没有互联网连接", + "debugLog_appTitle": "应用程序调试日志", + "debugLog_bleTitle": "BLE 调试日志", "debugLog_copyLog": "复制日志", - "debugLog_clearLog": "清除日志", + "debugLog_clearLog": "清晰的日志", "debugLog_copied": "调试日志已复制", - "debugLog_bleCopied": "蓝牙日志复制", - "debugLog_noEntries": "尚未生成调试日志", - "debugLog_enableInSettings": "启用应用调试日志记录设置", - "debugLog_frames": "帧", + "debugLog_bleCopied": "BLE 日志已复制", + "debugLog_noEntries": "目前还没有调试日志", + "debugLog_enableInSettings": "在设置中启用应用程序调试日志功能。", + "debugLog_frames": "框架", "debugLog_rawLogRx": "原始日志-RX", - "debugLog_noBleActivity": "目前还没有蓝牙活动。", + "debugLog_noBleActivity": "目前尚未有蓝牙低功耗(BLE)活动。", "debugFrame_length": "帧长度:{count} 字节", "@debugFrame_length": { "placeholders": { @@ -489,8 +509,8 @@ } } }, - "debugFrame_textMessageHeader": "短信框", - "debugFrame_destinationPubKey": "- 目的地公钥:{pubKey}", + "debugFrame_textMessageHeader": "短信模板:", + "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -498,7 +518,7 @@ } } }, - "debugFrame_timestamp": "- 时间戳:{timestamp}", + "debugFrame_timestamp": "- Timestamp: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -514,7 +534,7 @@ } } }, - "debugFrame_textType": "- 文本类型:{type} ({label})", + "debugFrame_textType": "- Text Type: {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -525,9 +545,9 @@ } } }, - "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "简洁", - "debugFrame_text": "- 文本:\"{text}\"", + "debugFrame_textTypeCli": "命令行界面", + "debugFrame_textTypePlain": "简单", + "debugFrame_text": "- 文本:“{text}”", "@debugFrame_text": { "placeholders": { "text": { @@ -535,16 +555,16 @@ } } }, - "debugFrame_hexDump": "十六进制数据", + "debugFrame_hexDump": "十六进制数据:", "chat_pathManagement": "路径管理", "chat_routingMode": "路由模式", - "chat_autoUseSavedPath": "自动(使用已保存路径)", + "chat_autoUseSavedPath": "自动(使用已保存的路径)", "chat_forceFloodMode": "强制洪水模式", - "chat_recentAckPaths": "最近的 ACK 路径 (点击以使用):", - "chat_pathHistoryFull": "路径历史已满。删除条目以添加新条目。", - "chat_hopSingular": "跳转", - "chat_hopPlural": "跳跃", - "chat_hopsCount": "{count} {count, plural, =1{跳跃} other{跳跃}}", + "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", + "chat_pathHistoryFull": "路径历史已满。删除条目以添加新的条目。", + "chat_hopSingular": "跳跃", + "chat_hopPlural": "啤酒花", + "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -554,18 +574,18 @@ }, "chat_successes": "成功", "chat_removePath": "删除路径", - "chat_noPathHistoryYet": "还没有历史记录。\n发送消息以发现路径。", + "chat_noPathHistoryYet": "目前还没有历史记录。\n发送消息以查找路径。", "chat_pathActions": "路径操作:", "chat_setCustomPath": "设置自定义路径", "chat_setCustomPathSubtitle": "手动指定路由路径", - "chat_clearPath": "清除路径", - "chat_clearPathSubtitle": "强制下次发送时重新发现", - "chat_pathCleared": "路径已清除。下一条消息将重新发现路线。", - "chat_floodModeSubtitle": "使用应用栏中的路由切换开关", - "chat_floodModeEnabled": "防洪模式已启用。通过应用程序栏中的路由图标进行反转。", + "chat_clearPath": "明确的道路", + "chat_clearPathSubtitle": "在下一次发送时,重新尝试。", + "chat_pathCleared": "路径已清理。下一条消息将重新确定路线。", + "chat_floodModeSubtitle": "使用应用程序栏中的路由切换功能", + "chat_floodModeEnabled": "防洪模式已启用。通过应用程序栏中的路由图标进行切换。", "chat_fullPath": "完整路径", - "chat_pathDetailsNotAvailable": "路径详情尚未获取。请尝试发送消息以刷新。", - "chat_pathSetHops": "路径设置:{hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathDetailsNotAvailable": "路径信息尚未提供。请尝试发送消息以刷新。", + "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -576,16 +596,16 @@ } } }, - "chat_pathSavedLocally": "已本地保存。连接以同步。", + "chat_pathSavedLocally": "已本地保存。连接以进行同步。", "chat_pathDeviceConfirmed": "设备已确认。", - "chat_pathDeviceNotConfirmed": "设备尚未确认。", - "chat_type": "输入", + "chat_pathDeviceNotConfirmed": "该设备尚未得到确认。", + "chat_type": "类型", "chat_path": "路径", "chat_publicKey": "公钥", "chat_compressOutgoingMessages": "压缩发送的消息", - "chat_floodForced": "强制溢出", - "chat_directForced": "强制直接", - "chat_hopsForced": "{count} 次跳跃 (强制)", + "chat_floodForced": "洪水(被迫)", + "chat_directForced": "直接(强制性的)", + "chat_hopsForced": "{count} 根啤酒花(人工种植)", "@chat_hopsForced": { "placeholders": { "count": { @@ -593,10 +613,10 @@ } } }, - "chat_floodAuto": "自动防洪", + "chat_floodAuto": "自动洪水", "chat_direct": "直接", - "chat_poiShared": "共享位置信息", - "chat_unread": "未读:{count}", + "chat_poiShared": "共享位置", + "chat_unread": "Unread: {count}", "@chat_unread": { "placeholders": { "count": { @@ -605,9 +625,9 @@ } }, "chat_openLink": "打开链接?", - "chat_openLinkConfirmation": "您想在浏览器中打开此链接吗?", - "chat_open": "打开", - "chat_couldNotOpenLink": "无法打开链接:{url}", + "chat_openLinkConfirmation": "您想用浏览器打开这个链接吗?", + "chat_open": "开放", + "chat_couldNotOpenLink": "[保存:{url}]\n无法打开链接:{url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -615,11 +635,11 @@ } } }, - "chat_invalidLink": "链接格式无效", - "map_title": "节点地图", - "map_noNodesWithLocation": "没有具有位置数据的节点", - "map_nodesNeedGps": "节点需要共享它们的 GPS 坐标\n才能在地图上显示", - "map_nodesCount": "节点:{count}", + "chat_invalidLink": "无效的链接格式", + "map_title": "节点图", + "map_noNodesWithLocation": "没有包含位置信息的节点", + "map_nodesNeedGps": "节点需要共享其 GPS 坐标,以便在地图上显示", + "map_nodesCount": "Nodes: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -627,7 +647,7 @@ } } }, - "map_pinsCount": "针:{count}", + "map_pinsCount": "Pins: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -639,23 +659,23 @@ "map_repeater": "重复器", "map_room": "房间", "map_sensor": "传感器", - "map_pinDm": "私信 (DM)", - "map_pinPrivate": "私密模式", - "map_pinPublic": "公开(公版)", - "map_lastSeen": "最后一次登录", + "map_pinDm": "PIN (直接消息)", + "map_pinPrivate": "私密", + "map_pinPublic": "公开", + "map_lastSeen": "最后一次被看到", "map_disconnectConfirm": "您确定要断开与此设备的连接吗?", "map_from": "从", "map_source": "来源", "map_flags": "旗帜", - "map_shareMarkerHere": "分享标记在此", - "map_pinLabel": "固定标签", + "map_shareMarkerHere": "在此分享标记", + "map_pinLabel": "标签", "map_label": "标签", - "map_pointOfInterest": "兴趣点", - "map_sendToContact": "发送给联系人", + "map_pointOfInterest": "值得参观的地方", + "map_sendToContact": "发送给联系", "map_sendToChannel": "发送到频道", "map_noChannelsAvailable": "没有可用的频道", - "map_publicLocationShare": "公共位置共享", - "map_publicLocationShareConfirm": "您即将分享一个位置在 {channelLabel}。此频道公开,任何拥有 PSK 的人都可以看到它。", + "map_publicLocationShare": "公共场所共享", + "map_publicLocationShareConfirm": "[保存:{channelLabel}]\n您即将分享一个位置,该位置位于 {channelLabel}。 此频道是公开的,任何拥有 PSK 的人都可以看到它。", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -664,25 +684,25 @@ } }, "map_connectToShareMarkers": "连接设备以共享标记", - "map_filterNodes": "筛选节点", + "map_filterNodes": "过滤节点", "map_nodeTypes": "节点类型", "map_chatNodes": "聊天节点", "map_repeaters": "重复器", "map_otherNodes": "其他节点", - "map_keyPrefix": "键前缀", - "map_filterByKeyPrefix": "按关键词前缀筛选", + "map_keyPrefix": "关键前缀", + "map_filterByKeyPrefix": "按关键前缀筛选", "map_publicKeyPrefix": "公钥前缀", "map_markers": "标记", "map_showSharedMarkers": "显示共享标记", - "map_lastSeenTime": "最后一次查看时间", - "map_sharedPin": "共享 PIN", + "map_lastSeenTime": "最后一次被看到的时间", + "map_sharedPin": "共享密码", "map_joinRoom": "加入房间", - "map_manageRepeater": "管理重复项", + "map_manageRepeater": "管理重复器", "mapCache_title": "离线地图缓存", - "mapCache_selectAreaFirst": "选择一个区域进行缓存", - "mapCache_noTilesToDownload": "该区域没有可下载的瓦片。", - "mapCache_downloadTilesTitle": "下载瓦片", - "mapCache_downloadTilesPrompt": "下载 {count} 个瓦片用于离线使用?", + "mapCache_selectAreaFirst": "选择一个用于缓存的区域", + "mapCache_noTilesToDownload": "此区域没有可下载的瓦片。", + "mapCache_downloadTilesTitle": "下载瓷砖", + "mapCache_downloadTilesPrompt": "[保存:{count}]\n下载 {count} 个图片用于离线使用?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -691,7 +711,7 @@ } }, "mapCache_downloadAction": "下载", - "mapCache_cachedTiles": "已缓存 {count} 个瓦片", + "mapCache_cachedTiles": "缓存 {count} 个瓦片", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -699,7 +719,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片 ({failed} 失败)", + "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -711,13 +731,13 @@ } }, "mapCache_clearOfflineCacheTitle": "清除离线缓存", - "mapCache_clearOfflineCachePrompt": "删除所有缓存地图瓦片?", + "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", "mapCache_offlineCacheCleared": "离线缓存已清除", "mapCache_noAreaSelected": "未选择任何区域", "mapCache_cacheArea": "缓存区域", "mapCache_useCurrentView": "使用当前视图", - "mapCache_zoomRange": "缩放范围", - "mapCache_estimatedTiles": "预计瓦片数量:{count}", + "mapCache_zoomRange": "变焦范围", + "mapCache_estimatedTiles": "Estimated tiles: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -725,7 +745,7 @@ } } }, - "mapCache_downloadedTiles": "已下载 {completed} / {total}", + "mapCache_downloadedTiles": "Downloaded {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -736,9 +756,9 @@ } } }, - "mapCache_downloadTilesButton": "下载瓦片", + "mapCache_downloadTilesButton": "下载瓷砖", "mapCache_clearCacheButton": "清除缓存", - "mapCache_failedDownloads": "下载失败:{count}", + "mapCache_failedDownloads": "Failed downloads: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -746,7 +766,7 @@ } } }, - "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", + "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -764,7 +784,7 @@ } }, "time_justNow": "刚才", - "time_minutesAgo": "{minutes}分钟前", + "time_minutesAgo": "{minutes}m ago", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -772,7 +792,7 @@ } } }, - "time_hoursAgo": "{hours}小时前", + "time_hoursAgo": "{hours}h ago", "@time_hoursAgo": { "placeholders": { "hours": { @@ -780,7 +800,7 @@ } } }, - "time_daysAgo": "{days} 天前", + "time_daysAgo": "{days}天前", "@time_daysAgo": { "placeholders": { "days": { @@ -790,10 +810,10 @@ }, "time_hour": "小时", "time_hours": "小时", - "time_day": "今天", + "time_day": "一天", "time_days": "天", - "time_week": "本周", - "time_weeks": "几周", + "time_week": "一周", + "time_weeks": "周", "time_month": "月份", "time_months": "月份", "time_minutes": "分钟", @@ -801,20 +821,20 @@ "dialog_disconnect": "断开", "dialog_disconnectConfirm": "您确定要断开与此设备的连接吗?", "login_repeaterLogin": "重复登录", - "login_roomLogin": "房间登录", + "login_roomLogin": "服务器登录", "login_password": "密码", "login_enterPassword": "请输入密码", "login_savePassword": "保存密码", - "login_savePasswordSubtitle": "密码将安全地存储在这个设备上", - "login_repeaterDescription": "输入重复密码以访问设置和状态。", - "login_roomDescription": "输入房间密码以访问设置和状态。", + "login_savePasswordSubtitle": "密码将安全地存储在 данном设备上", + "login_repeaterDescription": "输入重复器密码,即可访问设置和状态。", + "login_roomDescription": "输入密码进入房间,即可访问设置和状态。", "login_routing": "路由", "login_routingMode": "路由模式", - "login_autoUseSavedPath": "自动(使用已保存路径)", + "login_autoUseSavedPath": "自动(使用已保存的路径)", "login_forceFloodMode": "强制洪水模式", "login_managePaths": "管理路径", "login_login": "登录", - "login_attempt": "尝试 {current}/{max}", + "login_attempt": "Attempt {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -825,7 +845,7 @@ } } }, - "login_failed": "登录失败:{error}", + "login_failed": "Login failed: {error}", "@login_failed": { "placeholders": { "error": { @@ -833,10 +853,10 @@ } } }, - "login_failedMessage": "登录失败。密码不正确或中继器不可达。", + "login_failedMessage": "登录失败。可能是密码错误,也可能是无法连接到服务器。", "common_reload": "重新加载", - "common_clear": "清除", - "path_currentPath": "当前路径:{path}", + "common_clear": "清晰", + "path_currentPath": "Current path: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -844,7 +864,7 @@ } } }, - "path_usingHopsPath": "使用 {count} {count, plural, =1{hop} other{hops}} 路径", + "path_usingHopsPath": "使用 {count} {count, plural, =1{hop} other{hops}} 条路径", "@path_usingHopsPath": { "placeholders": { "count": { @@ -854,14 +874,14 @@ }, "path_enterCustomPath": "输入自定义路径", "path_currentPathLabel": "当前路径", - "path_hexPrefixInstructions": "输入2个字符的十六进制前缀,每个前缀之间用逗号分隔。", - "path_hexPrefixExample": "A1,F2,3C (每个节点使用其公钥的第一字节)", - "path_labelHexPrefixes": "十六进制前缀", - "path_helperMaxHops": "最大 64 步跳。每个前缀是 2 个十六进制字符(1 字节)", - "path_selectFromContacts": "或从联系人中选择:", - "path_noRepeatersFound": "未找到任何重复器或房间服务器。", - "path_customPathsRequire": "自定义路径需要中间跳转,这些跳转可以传递消息。", - "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", + "path_hexPrefixInstructions": "请输入每个跳跃步骤的 2 个字符的十六进制前缀,用逗号分隔。", + "path_hexPrefixExample": "例如:A1, F2, 3C (每个节点使用其公钥的第一字节)", + "path_labelHexPrefixes": "路径(十六进制前缀)", + "path_helperMaxHops": "最大 64 个“hop”(跳跃)。每个前缀由 2 个十六进制字符(1 字节)组成。", + "path_selectFromContacts": "或者从联系人列表中选择:", + "path_noRepeatersFound": "未找到任何重复设备或房间服务器。", + "path_customPathsRequire": "自定义路径需要中间节点,这些节点可以转发消息。", + "path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -872,23 +892,26 @@ "path_tooLong": "路径太长。允许的最大跳跃次数为 64 次。", "path_setPath": "设置路径", "repeater_management": "重复器管理", + "room_management": "服务器管理", "repeater_managementTools": "管理工具", "repeater_status": "状态", "repeater_statusSubtitle": "查看重复器状态、统计信息和邻居", - "repeater_telemetry": "遥测", - "repeater_telemetrySubtitle": "查看传感器和系统状态的Telemetry数据", - "repeater_cli": "CLI", - "repeater_cliSubtitle": "发送命令到重复器", + "repeater_telemetry": "远程监控", + "repeater_telemetrySubtitle": "查看传感器和系统状态的数据。", + "repeater_cli": "命令行界面", + "repeater_cliSubtitle": "向复用器发送指令", + "repeater_neighbours": "邻居", + "repeater_neighboursSubtitle": "查看邻居节点(无需中间节点)。", "repeater_settings": "设置", "repeater_settingsSubtitle": "配置重复器参数", "repeater_statusTitle": "重复器状态", "repeater_routingMode": "路由模式", - "repeater_autoUseSavedPath": "自动(使用已保存路径)", + "repeater_autoUseSavedPath": "自动(使用已保存的路径)", "repeater_forceFloodMode": "强制洪水模式", "repeater_pathManagement": "路径管理", - "repeater_refresh": "刷新", + "repeater_refresh": "更新", "repeater_statusRequestTimeout": "状态请求超时。", - "repeater_errorLoadingStatus": "错误加载状态:{error}", + "repeater_errorLoadingStatus": "Error loading status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -898,18 +921,18 @@ }, "repeater_systemInformation": "系统信息", "repeater_battery": "电池", - "repeater_clockAtLogin": "时间 (登录时)", - "repeater_uptime": "可用时间", + "repeater_clockAtLogin": "登录时的时间", + "repeater_uptime": "正常运行时间", "repeater_queueLength": "排队长度", "repeater_debugFlags": "调试标志", - "repeater_radioStatistics": "无线电统计", - "repeater_lastRssi": "上次RSSI", - "repeater_lastSnr": "最后 SNR", - "repeater_noiseFloor": "噪声地板", - "repeater_txAirtime": "TX Airtime", - "repeater_rxAirtime": "RX Airtime", + "repeater_radioStatistics": "广播统计", + "repeater_lastRssi": "上次的 RSSI 值", + "repeater_lastSnr": "最后一次信噪比", + "repeater_noiseFloor": "噪声水平", + "repeater_txAirtime": "TX 频道预留时间", + "repeater_rxAirtime": "RX 空时", "repeater_packetStatistics": "数据包统计", - "repeater_sent": "已发送", + "repeater_sent": "发送", "repeater_received": "已收到", "repeater_duplicates": "重复", "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}秒", @@ -929,7 +952,7 @@ } } }, - "repeater_packetTxTotal": "总计:{total}, 洪流:{flood}, 直连:{direct}", + "repeater_packetTxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -943,7 +966,7 @@ } } }, - "repeater_packetRxTotal": "总计:{total}, 洪流:{flood}, 直连:{direct}", + "repeater_packetRxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -957,7 +980,7 @@ } } }, - "repeater_duplicatesFloodDirect": "洪水:{flood}, 直通:{direct}", + "repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -968,7 +991,7 @@ } } }, - "repeater_duplicatesTotal": "总计:{total}", + "repeater_duplicatesTotal": "Total: {total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -976,37 +999,37 @@ } } }, - "repeater_settingsTitle": "重复设置", + "repeater_settingsTitle": "重复器设置", "repeater_basicSettings": "基本设置", "repeater_repeaterName": "重复器名称", - "repeater_repeaterNameHelper": "显示此重复器的名称", + "repeater_repeaterNameHelper": "此复播器的显示名称", "repeater_adminPassword": "管理员密码", "repeater_adminPasswordHelper": "完整访问密码", "repeater_guestPassword": "访客密码", "repeater_guestPasswordHelper": "只读访问密码", - "repeater_radioSettings": "射频设置", + "repeater_radioSettings": "收音机设置", "repeater_frequencyMhz": "频率 (MHz)", - "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX Power", + "repeater_frequencyHelper": "300-2500 兆赫", + "repeater_txPower": "TX 功率", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "带宽", - "repeater_spreadingFactor": "扩散因子", + "repeater_spreadingFactor": "传播系数", "repeater_codingRate": "编码速率", "repeater_locationSettings": "位置设置", "repeater_latitude": "纬度", - "repeater_latitudeHelper": "十进度的数字(例如:37.7749)", + "repeater_latitudeHelper": "十进制度(例如:37.7749)", "repeater_longitude": "经度", - "repeater_longitudeHelper": "十进度的数字(例如:-122.4194)", - "repeater_features": "功能", + "repeater_longitudeHelper": "十进制度(例如:-122.4194)", + "repeater_features": "特点", "repeater_packetForwarding": "数据包转发", - "repeater_packetForwardingSubtitle": "启用重复器以转发数据包", + "repeater_packetForwardingSubtitle": "启用重复器,使其能够转发数据包", "repeater_guestAccess": "访客访问", - "repeater_guestAccessSubtitle": "允许访客仅读访问", + "repeater_guestAccessSubtitle": "允许访客仅限读取权限", "repeater_privacyMode": "隐私模式", - "repeater_privacyModeSubtitle": "隐藏在广告中的姓名/位置", + "repeater_privacyModeSubtitle": "在广告中隐藏姓名/位置", "repeater_advertisementSettings": "广告设置", - "repeater_localAdvertInterval": "本地广告间隔", - "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", + "repeater_localAdvertInterval": "本地广告投放时间段", + "repeater_localAdvertIntervalMinutes": "{minutes} minutes", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1014,8 +1037,8 @@ } } }, - "repeater_floodAdvertInterval": "洪水广告间隔", - "repeater_floodAdvertIntervalHours": "{hours} 小时", + "repeater_floodAdvertInterval": "洪水广告播放间隔", + "repeater_floodAdvertIntervalHours": "{hours} hours", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1023,19 +1046,19 @@ } } }, - "repeater_encryptedAdvertInterval": "加密广告间隔", + "repeater_encryptedAdvertInterval": "加密的广告投放时间段", "repeater_dangerZone": "危险区域", "repeater_rebootRepeater": "重启重复器", - "repeater_rebootRepeaterSubtitle": "重启重复器设备", - "repeater_rebootRepeaterConfirm": "您确定要重启这个中继器吗?", + "repeater_rebootRepeaterSubtitle": "重新启动重复器设备", + "repeater_rebootRepeaterConfirm": "您确定要重新启动这个中继器吗?", "repeater_regenerateIdentityKey": "重新生成身份密钥", "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", - "repeater_regenerateIdentityKeyConfirm": "这将生成一个重复器的新身份。继续吗?", + "repeater_regenerateIdentityKeyConfirm": "这将为复用器生成一个新的身份。继续吗?", "repeater_eraseFileSystem": "删除文件系统", "repeater_eraseFileSystemSubtitle": "格式化重复文件系统", - "repeater_eraseFileSystemConfirm": "警告:这将擦除重复器上的所有数据。 这无法撤销!", - "repeater_eraseSerialOnly": "通过串行控制台才能删除。", - "repeater_commandSent": "命令已发送:{command}", + "repeater_eraseFileSystemConfirm": "警告:此操作将清除复用器上的所有数据。 无法恢复!", + "repeater_eraseSerialOnly": "“Erase”功能仅可通过串行控制台使用。", + "repeater_commandSent": "Command sent: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1043,7 +1066,7 @@ } } }, - "repeater_errorSendingCommand": "发送命令时出错:{error}", + "repeater_errorSendingCommand": "Error sending command: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1052,8 +1075,8 @@ } }, "repeater_confirm": "确认", - "repeater_settingsSaved": "设置已保存成功", - "repeater_errorSavingSettings": "保存设置出错:{error}", + "repeater_settingsSaved": "设置已成功保存", + "repeater_errorSavingSettings": "Error saving settings: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1061,15 +1084,15 @@ } } }, - "repeater_refreshBasicSettings": "刷新基本设置", - "repeater_refreshRadioSettings": "刷新无线电设置", - "repeater_refreshTxPower": "刷新 TX 电量", - "repeater_refreshLocationSettings": "刷新位置设置", + "repeater_refreshBasicSettings": "重置基本设置", + "repeater_refreshRadioSettings": "重置收音机设置", + "repeater_refreshTxPower": "重置 TX 电源", + "repeater_refreshLocationSettings": "重置位置设置", "repeater_refreshPacketForwarding": "刷新包转发", - "repeater_refreshGuestAccess": "刷新访客访问", - "repeater_refreshPrivacyMode": "刷新隐私模式", - "repeater_refreshAdvertisementSettings": "刷新广告设置", - "repeater_refreshed": "{label} 已刷新", + "repeater_refreshGuestAccess": "重新获取访客访问权限", + "repeater_refreshPrivacyMode": "重置隐私模式", + "repeater_refreshAdvertisementSettings": "重置广告设置", + "repeater_refreshed": "{label} refreshed", "@repeater_refreshed": { "placeholders": { "label": { @@ -1077,7 +1100,7 @@ } } }, - "repeater_errorRefreshing": "刷新 {label} 时出错", + "repeater_errorRefreshing": "[保存:{label}]\n刷新 {label} 时出错", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1085,18 +1108,18 @@ } } }, - "repeater_cliTitle": "重复器命令行工具", - "repeater_debugNextCommand": "调试下一步命令", + "repeater_cliTitle": "重复器命令行界面", + "repeater_debugNextCommand": "调试下一条命令", "repeater_commandHelp": "帮助", - "repeater_clearHistory": "清除历史", - "repeater_noCommandsSent": "尚未发送任何命令", - "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", + "repeater_clearHistory": "清晰的历史", + "repeater_noCommandsSent": "尚未发送任何指令", + "repeater_typeCommandOrUseQuick": "在下方输入命令,或使用快捷命令。", "repeater_enterCommandHint": "输入命令...", - "repeater_previousCommand": "上一个命令", - "repeater_nextCommand": "下一步命令", - "repeater_enterCommandFirst": "请输入一个命令", - "repeater_cliCommandFrameTitle": "CLI 命令窗口", - "repeater_cliCommandError": "错误:{error}", + "repeater_previousCommand": "之前的命令", + "repeater_nextCommand": "下一个指令", + "repeater_enterCommandFirst": "首先输入一个命令", + "repeater_cliCommandFrameTitle": "CLI 命令框架", + "repeater_cliCommandError": "Error: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1105,80 +1128,80 @@ } }, "repeater_cliQuickGetName": "获取姓名", - "repeater_cliQuickGetRadio": "获取收音机", + "repeater_cliQuickGetRadio": "收听广播", "repeater_cliQuickGetTx": "获取 TX", "repeater_cliQuickNeighbors": "邻居", "repeater_cliQuickVersion": "版本", - "repeater_cliQuickAdvertise": "发布", + "repeater_cliQuickAdvertise": "发布广告", "repeater_cliQuickClock": "时钟", - "repeater_cliHelpAdvert": "发送广告包", - "repeater_cliHelpReboot": "重启设备。(请注意,可能会出现“超时”现象,这是正常现象)", + "repeater_cliHelpAdvert": "发送广告资料包", + "repeater_cliHelpReboot": "重置设备。 (请注意,您可能会收到“超时”错误,这是正常的现象)", "repeater_cliHelpClock": "显示每个设备的当前时间。", - "repeater_cliHelpPassword": "设置设备的新管理员密码。", + "repeater_cliHelpPassword": "为设备设置新的管理员密码。", "repeater_cliHelpVersion": "显示设备版本和固件构建日期。", - "repeater_cliHelpClearStats": "重置各种统计数值为零。", - "repeater_cliHelpSetAf": "设置空闲时间因子。", - "repeater_cliHelpSetTx": "设置 LoRa 传输功率 (重置生效)", - "repeater_cliHelpSetRepeat": "启用或禁用此节点的重复器角色。", - "repeater_cliHelpSetAllowReadOnly": "(房间服务器) 如果“开”了,则空密码登录将被允许,但不能向房间发布内容。(仅限读取)", - "repeater_cliHelpSetFloodMax": "设置最大换路包数量(如果 >= 最大,则不转发包)。", - "repeater_cliHelpSetIntThresh": "设置干扰阈值(以 dB 为单位)。默认值为 14。将设置为 0 以禁用通道干扰检测。", - "repeater_cliHelpSetAgcResetInterval": "设置间隔以重置自动增益控制器。将设置为 0 以禁用。", - "repeater_cliHelpSetMultiAcks": "启用或禁用“双 ACKs”功能。", - "repeater_cliHelpSetAdvertInterval": "设置定时器间隔时间为分钟,以发送本地(零跳)广告包。将设置为0以禁用。", - "repeater_cliHelpSetFloodAdvertInterval": "设置定时器间隔时间为小时,以发送洪水广告包。将设置为 0 以禁用。", - "repeater_cliHelpSetGuestPassword": "设置/更新客人密码。(对于重复器,客人在登录时可以发送“获取统计”请求)", + "repeater_cliHelpClearStats": "重置各种统计指标,将其设置为零。", + "repeater_cliHelpSetAf": "设置时间因素。", + "repeater_cliHelpSetTx": "设置 LoRa 传输功率,单位为 dBm (相对于参考值)。 (重启以应用更改)", + "repeater_cliHelpSetRepeat": "启用或禁用此节点的重复器功能。", + "repeater_cliHelpSetAllowReadOnly": "(房间服务器)如果设置为“开启”,则允许使用空密码登录,但无法向房间发送消息(只能进行读取)。", + "repeater_cliHelpSetFloodMax": "设置最大传入数据包的跳数(如果大于或等于最大值,则不进行转发)。", + "repeater_cliHelpSetIntThresh": "设置干扰阈值(以dB为单位)。默认值为14。将设置为0以禁用频道干扰检测。", + "repeater_cliHelpSetAgcResetInterval": "设置间隔时间,用于重置自动增益控制器。设置为 0 以禁用。", + "repeater_cliHelpSetMultiAcks": "启用或禁用“双重确认”功能。", + "repeater_cliHelpSetAdvertInterval": "设置定时器间隔,单位为分钟,用于发送本地(无中继)的广告数据包。 将设置为 0 以禁用。", + "repeater_cliHelpSetFloodAdvertInterval": "设置定时器间隔时间为小时,以便发送广告信息包。将设置为 0 以禁用。", + "repeater_cliHelpSetGuestPassword": "设置/更新访客密码。 (对于访客,登录请求可以发送“获取统计”请求)", "repeater_cliHelpSetName": "设置广告名称。", - "repeater_cliHelpSetLat": "设置广告地图纬度(十进制度)", - "repeater_cliHelpSetLon": "设置广告地图经度 (十进位)", - "repeater_cliHelpSetRadio": "设置全新的无线电参数,并保存到偏好设置。需要执行“重启”命令才能应用。", - "repeater_cliHelpSetRxDelay": "设置(实验性)的基础(必须大于 1 才能生效)是用于对接收到的数据包应用轻微延迟,基于信号强度/得分。将设置为 0 以禁用。", - "repeater_cliHelpSetTxDelay": "设置一个与时间-在空气中(time-on-air)的系数,用于洪水模式的数据包,并结合随机插槽系统,以延迟其转发。(以降低碰撞的可能性)", - "repeater_cliHelpSetDirectTxDelay": "与txdelay相同,但用于为直接模式包的转发应用随机延迟。", - "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥梁", - "repeater_cliHelpSetBridgeDelay": "设置在重新发送数据包之前延迟时间。", - "repeater_cliHelpSetBridgeSource": "选择桥梁是否会重传接收到的数据包或发送的数据包。", - "repeater_cliHelpSetBridgeBaud": "设置rs232桥接的串口链路波特率。", - "repeater_cliHelpSetBridgeSecret": "设置 espnow 桥的秘密。", - "repeater_cliHelpSetAdcMultiplier": "设置自定义因子以调整报告的电池电压(仅限部分板卡支持)。", - "repeater_cliHelpTempRadio": "设置临时无线电参数,持续指定的分钟数,之后恢复为原始无线电参数。(不保存到偏好设置)。", - "repeater_cliHelpSetPerm": "修改ACL。如果“权限”为零,则删除匹配的条目(通过pubkey前缀)。如果pubkey-hex的完整长度且当前不在ACL中,则添加新条目。通过匹配pubkey前缀更新条目。权限位因固件角色而异,但低2位为:0(Guest)、1(只读)、2(读写)、3(Admin)", - "repeater_cliHelpGetBridgeType": "获取桥接类型:无,RS232,ESPNow", + "repeater_cliHelpSetLat": "设置广告地图的纬度。(以十进制表示)", + "repeater_cliHelpSetLon": "设置广告地图的经度。 (十进制度)", + "repeater_cliHelpSetRadio": "完全重新设置无线电参数,并保存到偏好设置。需要执行“重启”命令才能生效。", + "repeater_cliHelpSetRxDelay": "设置(实验性):设置一个基础值(必须大于1才能生效),用于对接收到的数据包进行轻微延迟处理,该延迟值基于信号强度/评分。将该值设置为0以禁用。", + "repeater_cliHelpSetTxDelay": "通过将一个因子与“浮动模式”数据包的时间在空中停留时间相乘,并结合随机的“时隙”系统,来延迟其转发,从而降低数据包冲突的概率。", + "repeater_cliHelpSetDirectTxDelay": "与txdelay相同,但用于对直接模式数据包的转发进行随机延迟。", + "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接。", + "repeater_cliHelpSetBridgeDelay": "在重新发送数据包之前,设置延迟时间。", + "repeater_cliHelpSetBridgeSource": "选择桥接器是否会转发收到的数据包,还是转发发送的数据包。", + "repeater_cliHelpSetBridgeBaud": "为 RS232 桥接设置串行链路的波特率。", + "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥的秘密。", + "repeater_cliHelpSetAdcMultiplier": "设置自定义因子,用于调整报告的电池电压(仅在特定板上支持)。", + "repeater_cliHelpTempRadio": "设置临时收音机参数,持续指定分钟数,之后恢复到原始收音机参数。(不保存到偏好设置)。", + "repeater_cliHelpSetPerm": "修改 ACL。如果 \"permissions\" 的值为 0,则删除与 pubkey 相关的条目。如果 pubkey-hex 完整且当前不在 ACL 中,则添加新的条目。通过匹配 pubkey 相关的前缀来更新条目。不同固件角色的权限位有所不同,但低 2 位分别对应:0 (访客)、1 (只读)、2 (读写)、3 (管理员)。", + "repeater_cliHelpGetBridgeType": "支持桥接模式、RS232、ESPNOW。", "repeater_cliHelpLogStart": "开始将数据包记录到文件系统。", - "repeater_cliHelpLogStop": "停止将数据包记录到文件系统。", - "repeater_cliHelpLogErase": "删除文件系统中的包日志。", - "repeater_cliHelpNeighbors": "显示通过零跳广告收听的其他重复节点列表。 每行是 id-prefix-hex:时间戳:snr-times-4", - "repeater_cliHelpNeighborRemove": "移除邻居列表中第一个匹配的条目(通过十六进制 pubkey 前缀)。", - "repeater_cliHelpRegion": "(仅显示区域) 列出所有已定义的区域和当前的防洪权限。", - "repeater_cliHelpRegionLoad": "注意:这是一个特殊的多命令调用。 随后的每个命令都是一个区域名称(用空格缩进以指示父级层次结构,至少需要一个空格)。 以发送一个空行/命令结束。", - "repeater_cliHelpRegionGet": "搜索具有给定名称前缀的区域(或“”用于全局范围)。回复为“-> region-name (parent-name) ‘F’”", - "repeater_cliHelpRegionPut": "添加或更新区域定义,使用指定名称。", - "repeater_cliHelpRegionRemove": "删除指定名称的区域定义。(必须没有子区域)", - "repeater_cliHelpRegionAllowf": "设置指定区域的“洪水”权限。(“”代表全局/遗留范围)", - "repeater_cliHelpRegionDenyf": "移除指定区域的‘F’lood权限。 (注意:目前阶段不建议在此范围内使用,尤其是全局/旧版范围!!)", - "repeater_cliHelpRegionHome": "回复当前“主页”区域。 (注意尚未应用,保留用于未来)", - "repeater_cliHelpRegionHomeSet": "设置‘主页’区域。", - "repeater_cliHelpRegionSave": "保存区域列表/地图到存储。", - "repeater_cliHelpGps": "显示GPS状态。当GPS关闭时,回复仅为“关闭”,如果已开启,则回复为“开启”、“状态”、“定位”和卫星数量。", - "repeater_cliHelpGpsOnOff": "切换 GPS 开启状态。", - "repeater_cliHelpGpsSync": "同步节点时间与 GPS 钟。", - "repeater_cliHelpGpsSetLoc": "设置节点位置至 GPS 坐标并保存偏好设置。", - "repeater_cliHelpGpsAdvert": "提供节点广告配置位置:\n- none:不包含位置在广告中\n- share:分享 GPS 位置(来自 SensorManager)\n- prefs:在偏好设置中投放位置", - "repeater_cliHelpGpsAdvertSet": "设置广告位置配置。", + "repeater_cliHelpLogStop": "停止将数据包记录写入文件系统。", + "repeater_cliHelpLogErase": "从文件系统中删除所有已记录的包信息。", + "repeater_cliHelpNeighbors": "显示了通过零跳广告收到的其他复用节点列表。 每行包含:id-前缀-十六进制:时间戳:信噪比(4次)", + "repeater_cliHelpNeighborRemove": "从邻居列表中删除第一个匹配项(通过十六进制的 pubkey 前缀)。", + "repeater_cliHelpRegion": "(仅限序列)列出所有已定义的区域以及当前的防洪许可。", + "repeater_cliHelpRegionLoad": "请注意:这是一个特殊的、包含多个命令的调用方式。 之后的每个命令都是一个区域名称(使用空格进行缩进,以表示父级关系,至少需要一个空格)。 结束方式是通过发送一个空行/命令。", + "repeater_cliHelpRegionGet": "搜索具有指定名称前缀的区域(或使用“*”表示全局范围)。 返回结果为“-> region-name (parent-name) 'F'”", + "repeater_cliHelpRegionPut": "添加或更新一个区域定义,并指定其名称。", + "repeater_cliHelpRegionRemove": "删除具有指定名称的区域定义。 (必须与指定名称完全匹配,且不能有子区域)", + "repeater_cliHelpRegionAllowf": "为指定区域设置“洪水”权限。(“*”表示全局/旧版本范围)", + "repeater_cliHelpRegionDenyf": "移除指定区域的“洪水”权限。(请注意:目前不建议在全局/旧版本中使用此功能!!)", + "repeater_cliHelpRegionHome": "回复当前“主区域”。(此功能尚未应用,仅供未来使用)", + "repeater_cliHelpRegionHomeSet": "设置“主”区域。", + "repeater_cliHelpRegionSave": "将区域列表/地图保存到存储中。", + "repeater_cliHelpGps": "显示 GPS 状态。当 GPS 处于关闭状态时,它只会显示“关闭”;当 GPS 处于开启状态时,它会显示“开启”、“状态”、“定位”、“卫星数量”等信息。", + "repeater_cliHelpGpsOnOff": "切换 GPS 设备的电源状态。", + "repeater_cliHelpGpsSync": "将节点时间与 GPS 钟同步。", + "repeater_cliHelpGpsSetLoc": "将节点的坐标设置为 GPS 坐标,并保存设置。", + "repeater_cliHelpGpsAdvert": "设置节点的位置广告配置:\n- none:不将位置信息包含在广告中\n- share:共享 GPS 位置(从 SensorManager 获取)\n- prefs:在偏好设置中展示的位置", + "repeater_cliHelpGpsAdvertSet": "设置广告的位置配置。", "repeater_commandsListTitle": "命令列表", - "repeater_commandsListNote": "注意:对于各种“设置...”命令,也存在“获取...”命令。", + "repeater_commandsListNote": "请注意:对于各种“set ...”命令,也存在“get ...”命令。", "repeater_general": "通用", "repeater_settingsCategory": "设置", "repeater_bridge": "桥", "repeater_logging": "记录", - "repeater_neighborsRepeaterOnly": "邻居(仅限重复器)", - "repeater_regionManagementRepeaterOnly": "区域管理(仅限重复器)", - "repeater_regionNote": "区域命令已推出,用于管理区域定义和权限。", - "repeater_gpsManagement": "GPS管理", - "repeater_gpsNote": "GPS 命令已引入用于管理与位置相关的主题。", - "telemetry_receivedData": "接收遥测数据", + "repeater_neighborsRepeaterOnly": "邻居(仅限重复功能)", + "repeater_regionManagementRepeaterOnly": "区域管理(仅限重复站点)", + "repeater_regionNote": "区域命令已引入,用于管理区域定义和权限。", + "repeater_gpsManagement": "GPS 管理", + "repeater_gpsNote": "已引入 GPS 命令,用于管理与位置相关的任务。", + "telemetry_receivedData": "接收到的遥测数据", "telemetry_requestTimeout": "遥测请求超时。", - "telemetry_errorLoading": "错误加载遥测数据:{error}", + "telemetry_errorLoading": "Error loading telemetry: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1197,7 +1220,7 @@ }, "telemetry_batteryLabel": "电池", "telemetry_voltageLabel": "电压", - "telemetry_mcuTemperatureLabel": "MCU 温度", + "telemetry_mcuTemperatureLabel": "MCU 的温度", "telemetry_temperatureLabel": "温度", "telemetry_currentLabel": "当前", "telemetry_batteryValue": "{percent}% / {volts}V", @@ -1238,18 +1261,46 @@ } } }, + "neighbors_receivedData": "已收到邻居信息", + "neighbors_requestTimedOut": "邻居要求停止干扰。", + "neighbors_errorLoading": "Error loading neighbors: {error}", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "neighbors_repeatersNeighbours": "重复使用的邻居", + "neighbors_noData": "没有可用的邻居信息。", + "neighbors_unknownContact": "Unknown {pubkey}", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Heard: {time} ago", + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, "channelPath_title": "数据包路径", "channelPath_viewMap": "查看地图", "channelPath_otherObservedPaths": "其他观察到的路径", - "channelPath_repeaterHops": "重复跳跃", - "channelPath_noHopDetails": "此包的详细信息未提供。", + "channelPath_repeaterHops": "复用跳跃", + "channelPath_noHopDetails": "对于此包,未提供详细信息。", "channelPath_messageDetails": "消息详情", "channelPath_senderLabel": "发件人", "channelPath_timeLabel": "时间", "channelPath_repeatsLabel": "重复", "channelPath_pathLabel": "路径 {index}", - "channelPath_observedLabel": "已观察", - "channelPath_observedPathTitle": "观察路径 {index} • {hops}", + "channelPath_observedLabel": "观察到的", + "channelPath_observedPathTitle": "Observed path {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1260,7 +1311,7 @@ } } }, - "channelPath_noLocationData": "没有位置数据", + "channelPath_noLocationData": "没有位置信息", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1286,7 +1337,7 @@ "channelPath_unknownPath": "未知", "channelPath_floodPath": "洪水", "channelPath_directPath": "直接", - "channelPath_observedZeroOf": "0 of {total} 跳跃", + "channelPath_observedZeroOf": "0 of {total} hops", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1294,7 +1345,7 @@ } } }, - "channelPath_observedSomeOf": "已观察到 {observed} 步中的 {total} 步", + "channelPath_observedSomeOf": "{observed} of {total} hops", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1305,9 +1356,9 @@ } } }, - "channelPath_mapTitle": "路径地图", - "channelPath_noRepeaterLocations": "此路径没有可用的重复器位置。", - "channelPath_primaryPath": "路径 {index} (主)", + "channelPath_mapTitle": "路线图", + "channelPath_noRepeaterLocations": "这条路径上没有可用的中继器位置。", + "channelPath_primaryPath": "路径 {index} (主要路径)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1323,7 +1374,7 @@ } }, "channelPath_pathLabelTitle": "路径", - "channelPath_observedPathHeader": "已观察路径", + "channelPath_observedPathHeader": "观察路径", "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { @@ -1335,68 +1386,14 @@ } } }, - "channelPath_noHopDetailsAvailable": "此包的跳跃详情不可用。", - "channelPath_unknownRepeater": "未知重复器", - "listFilter_tooltip": "筛选和排序", - "listFilter_sortBy": "按类型排序", - "listFilter_latestMessages": "最新消息", - "listFilter_heardRecently": "最近听说", - "listFilter_az": "A-Z", - "listFilter_filters": "筛选", - "listFilter_all": "全部", - "listFilter_users": "用户", - "listFilter_repeaters": "重复器", - "listFilter_roomServers": "房间服务器", - "listFilter_unreadOnly": "未读消息", - "listFilter_newGroup": "新组", - "@neighbors_errorLoading": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "repeater_neighboursSubtitle": "查看零跳邻居。", - "repeater_neighbours": "邻居", - "neighbors_receivedData": "收到邻居数据", - "neighbors_requestTimedOut": "邻居请求超时处理。", - "neighbors_errorLoading": "加载邻居时出错:{error}", - "neighbors_repeatersNeighbours": "重复器邻居", - "neighbors_noData": "没有可用的邻居数据。", - "channels_joinPrivateChannel": "加入私密频道", - "channels_createPrivateChannelDesc": "使用密钥保护。", - "channels_joinPrivateChannelDesc": "手动输入密钥。", - "channels_createPrivateChannel": "创建私聊频道", - "channels_joinPublicChannel": "加入公共频道", - "channels_joinPublicChannelDesc": "任何人都可以加入这个频道。", - "channels_joinHashtagChannel": "加入标签频道", - "channels_joinHashtagChannelDesc": "任何人都可以加入话题频道。", - "channels_scanQrCode": "扫描二维码", - "channels_scanQrCodeComingSoon": "即将到来", - "channels_enterHashtag": "输入标签", - "channels_hashtagHint": "例如 #团队", - "@neighbors_unknownContact": { - "placeholders": { - "pubkey": { - "type": "String" - } - } - }, - "@neighbors_heardAgo": { - "placeholders": { - "time": { - "type": "String" - } - } - }, - "neighbors_heardAgo": "听到的时间:{time}前", - "neighbors_unknownContact": "未知{pubkey}", - "settings_locationGPSEnable": "启用GPS", - "settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。", - "settings_locationIntervalSec": "GPS 间隔(秒)", - "settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。", - "contacts_manageRoom": "管理房间服务器", - "room_management": "房间服务器管理", + "channelPath_noHopDetailsAvailable": "对于此包裹,尚无详细信息。", + "channelPath_unknownRepeater": "未知的重复设备", + "community_title": "社区", + "community_create": "建立社区", + "community_createDesc": "创建一个新的社群,并通过二维码进行分享。", + "community_join": "加入", + "community_joinTitle": "加入社区", + "community_joinConfirmation": "Do you want to join the community \"{name}\"?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1404,6 +1401,14 @@ } } }, + "community_scanQr": "扫描社区二维码", + "community_scanInstructions": "将相机对准社区的二维码。", + "community_showQr": "显示二维码", + "community_publicChannel": "社区公共", + "community_hashtagChannel": "社区标签", + "community_name": "社区名称", + "community_enterName": "请输入社区名称", + "community_created": "Community \"{name}\" created", "@community_created": { "placeholders": { "name": { @@ -1411,6 +1416,7 @@ } } }, + "community_joined": "Joined community \"{name}\"", "@community_joined": { "placeholders": { "name": { @@ -1418,6 +1424,8 @@ } } }, + "community_qrTitle": "分享社区", + "community_qrInstructions": "Scan this QR code to join \"{name}\"", "@community_qrInstructions": { "placeholders": { "name": { @@ -1425,6 +1433,10 @@ } } }, + "community_hashtagPrivacyHint": "仅社区成员才能加入社区话题标签的频道。", + "community_invalidQrCode": "无效的社区二维码", + "community_alreadyMember": "已经是会员", + "community_alreadyMemberMessage": "You are already a member of \"{name}\".", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1432,6 +1444,13 @@ } } }, + "community_addPublicChannel": "添加公共频道", + "community_addPublicChannelHint": "自动添加该社区的公共频道", + "community_noCommunities": "目前还没有任何社区加入。", + "community_scanOrCreate": "扫描二维码或创建社群,即可开始。", + "community_manageCommunities": "管理社区", + "community_delete": "退出社区", + "community_deleteConfirm": "是否要删除\"{name}\"?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1439,50 +1458,7 @@ } } }, - "@community_deleted": { - "placeholders": { - "name": { - "type": "String" - } - } - }, - "@community_forCommunity": { - "placeholders": { - "name": { - "type": "String" - } - } - }, - "community_create": "创建社区", - "community_title": "社区", - "community_createDesc": "创建新的社区并可通过二维码分享。", - "common_ok": "好的", - "community_join": "加入", - "community_joinTitle": "加入社区", - "community_joinConfirmation": "您想加入社区 \"{name}\" 吗?", - "community_scanQr": "扫描社区二维码", - "community_scanInstructions": "将相机对准社区二维码", - "community_showQr": "显示二维码", - "community_publicChannel": "社区公开", - "community_hashtagChannel": "社区标签", - "community_name": "社区名称", - "community_enterName": "请输入社区名称", - "community_created": "社区“{name}”已创建", - "community_joined": "加入社区 \"{name}\"", - "community_qrTitle": "分享社区", - "community_qrInstructions": "扫描此二维码加入{name}", - "community_hashtagPrivacyHint": "社区标签频道仅社区成员可加入", - "community_invalidQrCode": "无效的社区二维码", - "community_alreadyMember": "已经是会员了", - "community_alreadyMemberMessage": "您已经是 \"{name}\" 的会员。", - "community_addPublicChannel": "添加社区公共频道", - "community_addPublicChannelHint": "自动添加该社区的公共频道", - "community_noCommunities": "尚未加入任何社区", - "community_scanOrCreate": "扫描二维码或创建社区开始", - "community_manageCommunities": "管理社群", - "community_delete": "退出社区", - "community_deleteConfirm": "退出 \"{name}\"?", - "community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。", + "community_deleteChannelsWarning": "这将同时删除 {count} 个频道及其所有消息。", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1490,15 +1466,16 @@ } } }, - "community_deleted": "已退出社区 \"{name}\"", - "community_addHashtagChannel": "添加社区标签", - "community_addHashtagChannelDesc": "添加一个话题频道给此社区", - "community_selectCommunity": "选择社区", - "community_regularHashtag": "常规话题标签", - "community_regularHashtagDesc": "公共话题(任何人都可以加入)", - "community_communityHashtag": "社区标签", - "community_communityHashtagDesc": "仅限社区成员使用", - "community_forCommunity": "对于 {name}", + "community_deleted": "Left community \"{name}\"", + "@community_deleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "恢复秘密", + "community_regenerateSecretConfirm": "[保存:{name}]\n是否需要重新生成\"{name}\"的密钥?所有成员都需要扫描新的二维码才能继续进行通信。", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1506,6 +1483,8 @@ } } }, + "community_regenerate": "再生", + "community_secretRegenerated": "[保护对象:{name}]\n秘密已恢复至\"{name}\"", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1513,6 +1492,8 @@ } } }, + "community_updateSecret": "更新秘密", + "community_secretUpdated": "“{name}”的秘密已更新", "@community_secretUpdated": { "placeholders": { "name": { @@ -1520,6 +1501,7 @@ } } }, + "community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1527,13 +1509,45 @@ } } }, - "community_regenerateSecret": "重新生成密钥", - "community_secretRegenerated": "密码已重置为“{name}”", - "community_regenerate": "重新生成", - "community_regenerateSecretConfirm": "重新生成“{name}”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。", - "community_scanToUpdateSecret": "扫描新的二维码更新\"{name}\"的密码", - "community_updateSecret": "更新密钥", - "community_secretUpdated": "密码已更新为“{name}”", + "community_addHashtagChannel": "添加社区标签", + "community_addHashtagChannelDesc": "为这个社区创建一个带有话题标签的频道", + "community_selectCommunity": "选择社区", + "community_regularHashtag": "常用标签", + "community_regularHashtagDesc": "公共话题标签(任何人都可以参与)", + "community_communityHashtag": "社区标签", + "community_communityHashtagDesc": "仅限社区成员", + "community_forCommunity": "For {name}", + "@community_forCommunity": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "listFilter_tooltip": "筛选和排序", + "listFilter_sortBy": "按排序", + "listFilter_latestMessages": "最新消息", + "listFilter_heardRecently": "最近听到的", + "listFilter_az": "A 到 Z", + "listFilter_filters": "过滤器", + "listFilter_all": "全部", + "listFilter_users": "用户", + "listFilter_repeaters": "重复器", + "listFilter_roomServers": "房间服务器", + "listFilter_unreadOnly": "仅显示未读消息", + "listFilter_newGroup": "新的团体", + "pathTrace_you": "您", + "pathTrace_failed": "路径追踪失败。", + "pathTrace_notAvailable": "无法获取路径信息。", + "pathTrace_refreshTooltip": "重新绘制路径。", + "contacts_pathTrace": "路径追踪", + "contacts_ping": "乒", + "contacts_repeaterPathTrace": "追踪路径至中继器", + "contacts_repeaterPing": "中继器", + "contacts_roomPathTrace": "追踪到房间服务器", + "contacts_roomPing": "会议室服务器", + "contacts_chatTraceRoute": "路径跟踪路线", + "contacts_pathTraceTo": "追踪路径至 {name}", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1541,32 +1555,18 @@ } } }, - "pathTrace_you": "你", - "pathTrace_failed": "路径追踪失败。", - "pathTrace_notAvailable": "路径追踪不可用", - "pathTrace_refreshTooltip": "刷新路径追踪", - "contacts_pathTrace": "路径追踪", - "contacts_ping": "ping", - "contacts_repeaterPathTrace": "路径追踪到中继器", - "contacts_repeaterPing": "Ping 中继器", - "contacts_roomPathTrace": "路径追踪至房间服务器", - "contacts_roomPing": "Ping 房间服务器", - "contacts_chatTraceRoute": "路径追踪", - "contacts_pathTraceTo": "追踪路由到 {name}", - "appSettings_languageUk": "乌克兰语", - "appSettings_languageRu": "俄语", - "contacts_contactImported": "联系人已导入", - "contacts_contactImportFailed": "联系人导入失败", - "contacts_zeroHopAdvert": "零跳广告", - "contacts_floodAdvert": "洪水广告", "contacts_clipboardEmpty": "剪贴板为空。", - "contacts_invalidAdvertFormat": "无效联系人数据", - "contacts_addContactFromClipboard": "从剪贴板添加联系人", - "contacts_zeroHopContactAdvertSent": "通过广告发送的联系人", - "contacts_zeroHopContactAdvertFailed": "发送联系人失败", - "contacts_contactAdvertCopied": "广告已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "复制广告到剪贴板失败。", + "contacts_invalidAdvertFormat": "无效的联系信息", + "contacts_contactImported": "已建立联系。", + "contacts_contactImportFailed": "未能导入联系人。", + "contacts_zeroHopAdvert": "零跳广告", + "contacts_floodAdvert": "防洪广告", "contacts_copyAdvertToClipboard": "复制广告到剪贴板", - "contacts_ShareContactZeroHop": "通过广告分享联系人", - "contacts_ShareContact": "复制联系人到剪贴板" + "contacts_addContactFromClipboard": "从剪贴板添加联系人", + "contacts_ShareContact": "复制联系方式到剪贴板", + "contacts_ShareContactZeroHop": "通过广告分享联系方式", + "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", + "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", + "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 99bad87..48f94f9 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -34,6 +34,12 @@ enum RoomLoginDestination { management, } +enum ContactOperationType { + import, + export, + zeroHopShare, +} + class ContactsScreen extends StatefulWidget { final bool hideBackButton; @@ -54,9 +60,7 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; - bool _imported = false; - bool _zeroHopContact = false; - bool _copyedContact = false; + final Set _pendingOperations = {}; StreamSubscription? _frameSubscription; @@ -97,57 +101,67 @@ class _ContactsScreenState extends State if (code == respCodeExportContact) { final advertPacket = frameBuffer.readRemainingBytes(); + // Validate packet has expected minimum size (98+ bytes per protocol) + if (advertPacket.length < 98) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } + _pendingOperations.remove(ContactOperationType.export); + return; + } final hexString = pubKeyToHex(advertPacket); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } if(code == respCodeOk) { // Show a snackbar indicating success - if(_imported && mounted){ + if(!mounted) return; + + if(_pendingOperations.contains(ContactOperationType.import)){ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactImported)), ); } - if(_zeroHopContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertSent)), ); } - if(_copyedContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.export)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), ); } - - _copyedContact = false; - _zeroHopContact = false; - _imported = false; + + _pendingOperations.clear(); } if(code == respCodeErr) { // Show a snackbar indicating failure - if(_imported && mounted){ + if(!mounted) return; + + if(_pendingOperations.contains(ContactOperationType.import)){ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), ); } - if(_zeroHopContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertFailed)), ); } - if(_copyedContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.export)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactAdvertCopyFailed)), ); } - _copyedContact = false; - _imported = false; - _zeroHopContact = false; + _pendingOperations.clear(); } }); @@ -156,15 +170,14 @@ class _ContactsScreenState extends State Future _contactExport(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); - _copyedContact = true; + _pendingOperations.add(ContactOperationType.export); await connector.sendFrame(exportContactFrame); - return; } Future _contactZeroHop(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactZeroHopFrame = buildZeroHopContact(pubKey); - _zeroHopContact = true; + _pendingOperations.add(ContactOperationType.zeroHopShare); await connector.sendFrame(exportContactZeroHopFrame); } @@ -191,8 +204,8 @@ class _ContactsScreenState extends State final hexString = text.substring('meshcore://'.length); try { final importContactFrame = buildImportContactFrame(hexString); + _pendingOperations.add(ContactOperationType.import); await connector.sendFrame(importContactFrame); - _imported = true; } catch (e) { if(mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/tools/translate.py b/tools/translate.py index 84d172a..a51d3d1 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -1,42 +1,21 @@ #!/usr/bin/env python3 """ -translate_arb_with_ollama.py +translate_arb_with_translategemma.py -Translates ARB/JSON localization values using a local Ollama model, while: -- preserving keys -- skipping "@@locale" and all "@key" metadata blocks -- preserving placeholders like {deviceName}, {count, plural, ...} -- writing a new file with updated @@locale -- printing progress as it runs +Translates ARB/JSON localization files using TranslateGemma via Ollama. +Preserves placeholders like {deviceName} and ICU plural/select formats. Usage: # Translate all strings: - python translate_arb_with_ollama.py \ - --in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \ - --out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \ - --to-locale es \ - --model ministral-3:latest \ - --temperature 0 \ - --concurrency 4 + python translate.py --in lib/l10n/app_en.arb --out lib/l10n/app_es.arb --to-locale es - # Translate only missing/untranslated strings: - python translate_arb_with_ollama.py \ - --in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \ - --out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \ - --to-locale es \ - --missing-only \ - --model ministral-3:latest + # Translate only missing strings: + python translate.py --in lib/l10n/app_en.arb --out lib/l10n/app_es.arb --to-locale es --missing-only # Translate all locales (missing strings only): - python translate_arb_with_ollama.py \ - --in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \ - --l10n-dir /home/zjs81/Desktop/meshcore-open/lib/l10n \ - --missing-only \ - --model ministral-3:latest + python translate.py --in lib/l10n/app_en.arb --l10n-dir lib/l10n --missing-only """ -from __future__ import annotations - import argparse import json import os @@ -49,9 +28,8 @@ from typing import Any, Dict, List, Tuple, Optional from urllib import request -# Simple placeholder like {name}, {count}, {deviceName} +# Placeholder patterns SIMPLE_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") -# ICU plural/select variable name extraction: {count, plural, ...} or {gender, select, ...} ICU_VAR_RE = re.compile(r"\{(\w+)\s*,\s*(?:plural|select|selectordinal)\s*,", re.IGNORECASE) @@ -61,263 +39,46 @@ class OllamaConfig: model: str timeout_s: float temperature: float - num_ctx: int - num_predict: int - top_p: float -def http_post_json(url: str, payload: Dict[str, Any], timeout_s: float) -> Dict[str, Any]: - data = json.dumps(payload).encode("utf-8") - req = request.Request( - url, - data=data, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with request.urlopen(req, timeout=timeout_s) as resp: - body = resp.read().decode("utf-8") - return json.loads(body) - - -def strip_markdown(s: str) -> str: - """Remove common markdown formatting from output.""" - # Remove bold/italic markers - s = re.sub(r'\*\*(.+?)\*\*', r'\1', s) - s = re.sub(r'\*(.+?)\*', r'\1', s) - s = re.sub(r'__(.+?)__', r'\1', s) - s = re.sub(r'_(.+?)_', r'\1', s) - # Remove stray asterisks - s = re.sub(r'\*+', '', s) - return s.strip() - - -def ollama_generate(cfg: OllamaConfig, prompt: str) -> str: - url = cfg.host.rstrip("/") + "/api/generate" - payload = { - "model": cfg.model, - "prompt": prompt, - "stream": False, - "options": { - "temperature": cfg.temperature, - "num_ctx": cfg.num_ctx, - "num_predict": cfg.num_predict, - "top_p": cfg.top_p, - }, - } - resp = http_post_json(url, payload, cfg.timeout_s) - out = resp.get("response", "") - # Clean up common LLM artifacts - out = strip_markdown(out) - return out.strip() - - -def extract_placeholder_names(s: str) -> List[str]: - """Extract placeholder variable names (not the full braced expression). - - For '{name}' returns ['name'] - For '{count} {count, plural, =1{hop} other{hops}}' returns ['count'] - """ - names = set() - # Get ICU variable names first - for m in ICU_VAR_RE.finditer(s): - names.add(m.group(1)) - # Get simple placeholders, but skip if they're inside ICU blocks (text forms like {hop}) - # We do this by checking if the name is also an ICU variable - if not, it's a simple placeholder - # unless it looks like a word (ICU text forms are usually short words) - for m in SIMPLE_PLACEHOLDER_RE.finditer(s): - name = m.group(1) - # Check if this appears as a simple {name} placeholder (not inside ICU) - # by looking at what comes after it - full_match = m.group(0) - pos = m.start() - # Look for pattern like {name, plural/select - if found, skip (handled by ICU_VAR_RE) - rest = s[pos:] - if re.match(r"\{\w+\s*,\s*(?:plural|select|selectordinal)", rest, re.IGNORECASE): - continue - # Check if this is likely a text form inside ICU (preceded by =X{ or other{) - before = s[:pos] - if re.search(r"(?:=\d+|zero|one|two|few|many|other)\s*$", before, re.IGNORECASE): - continue # This is a text form like "=1{hop}", skip it - names.add(name) - return sorted(names) - - -def build_prompt(text: str, target_lang: str, placeholder_names: List[str], has_icu: bool, ask_confidence: bool = False) -> str: - preserve_list = "\n".join(f"- {{{t}}}" for t in placeholder_names) if placeholder_names else "- (none)" - - icu_note = "" - if has_icu: - icu_note = ( - "ICU FORMAT RULES:\n" - f"- This text uses ICU plural/select format: {{var, plural, =1{{singular}} other{{plural}}}}\n" - "- Keep structure keywords EXACTLY: plural, select, =0, =1, =2, zero, one, two, few, many, other\n" - f"- TRANSLATE the words inside each form to {target_lang}\n" - "- Example: =1{item} other{items} -> translate 'item'/'items' but keep =1{{ }} other{{ }} structure\n\n" - ) - - if ask_confidence: - return ( - f"Translate this UI string to {target_lang}.\n\n" - "RULES:\n" - "- Placeholders like {name}, {count} must appear EXACTLY unchanged.\n" - "- Use infinitive verb forms for buttons (Save, Delete, etc.).\n" - f"- Use natural {target_lang} word order.\n" - "- Keep brand names and technical terms unchanged.\n\n" - f"{icu_note}" - f"Placeholders: {', '.join(f'{{{t}}}' for t in placeholder_names) if placeholder_names else 'none'}\n\n" - f"English: {text}\n\n" - "Respond with EXACTLY two lines:\n" - "1. The translation (no quotes)\n" - "2. Confidence score 1-5 (5=certain, 1=unsure)\n\n" - "Example response:\n" - "Guardar archivo\n" - "5" - ) - else: - return ( - f"Translate this UI string to {target_lang}. Return ONLY the translation.\n\n" - "RULES:\n" - "- Output the translated text ONLY. No markdown, no quotes, no explanations.\n" - "- Placeholders like {name}, {count} must appear EXACTLY unchanged.\n" - "- Use infinitive verb forms for buttons (Save, Delete, etc.).\n" - f"- Use natural {target_lang} word order.\n" - "- Keep brand names and technical terms unchanged.\n" - "- Translation length should be similar to the original.\n\n" - f"{icu_note}" - f"Placeholders: {', '.join(f'{{{t}}}' for t in placeholder_names) if placeholder_names else 'none'}\n\n" - f"English: {text}\n" - f"{target_lang}:" - ) - - -def parse_confidence_response(response: str) -> Tuple[str, int]: - """Parse response with translation and confidence score. - - Returns (translation, confidence) where confidence is 1-5, or 0 if unparseable. - """ - lines = response.strip().split('\n') - if len(lines) >= 2: - translation = '\n'.join(lines[:-1]).strip() # All but last line - try: - # Try to extract number from last line - last_line = lines[-1].strip() - # Handle formats like "5", "5/5", "Confidence: 5" - match = re.search(r'\b([1-5])\b', last_line) - if match: - confidence = int(match.group(1)) - return translation, confidence - except ValueError: - pass - # Fallback: treat whole response as translation with unknown confidence - return strip_markdown(response), 0 - - -def looks_like_translation_failed(src: str, out: str) -> bool: - if not out: - return True - if src.strip() == out.strip() and len(src.strip()) > 8: - return True - # Detect hallucination: output much longer than input (3x+ for short strings, 2x for longer) - src_len = len(src.strip()) - out_len = len(out.strip()) - if src_len < 50 and out_len > src_len * 3: - return True - if src_len >= 50 and out_len > src_len * 2: - return True - return False - - -def has_icu_block(s: str) -> bool: - """Check if string contains ICU plural/select block.""" - return bool(ICU_VAR_RE.search(s)) - - -def validate_preserved_tokens(src: str, out: str) -> Tuple[bool, Optional[str]]: - """Validate that placeholder names are preserved in translation.""" - src_names = extract_placeholder_names(src) - - # Check each placeholder name appears in output - for name in src_names: - # Look for {name} or {name, plural/select...} - pattern = r"\{" + re.escape(name) + r"(?:\}|\s*,)" - if not re.search(pattern, out): - return False, f"Missing placeholder: {{{name}}}" - - # If source has ICU block, output should too - if has_icu_block(src) and not has_icu_block(out): - return False, "ICU plural/select block missing in output" - - return True, None - - -def compute_confidence(src: str, out: str) -> Tuple[float, List[str]]: - """ - Compute confidence score (0.0 to 1.0) for a translation. - Returns (score, list of issues). - """ - issues = [] - score = 1.0 - - src_len = len(src.strip()) - out_len = len(out.strip()) - - # Length ratio check - if src_len > 0: - ratio = out_len / src_len - if ratio < 0.3: # Way too short - score -= 0.4 - issues.append("too_short") - elif ratio < 0.5: - score -= 0.2 - issues.append("short") - elif ratio > 2.5: # Way too long - score -= 0.4 - issues.append("too_long") - elif ratio > 1.8: - score -= 0.2 - issues.append("long") - - # Contains question mark when source doesn't (model asking questions) - if '?' in out and '?' not in src: - score -= 0.3 - issues.append("added_question") - - # Contains common LLM artifacts - artifacts = ['```', '**', 'translation:', 'here is', 'certainly', 'i can', 'i\'ll'] - out_lower = out.lower() - for artifact in artifacts: - if artifact in out_lower: - score -= 0.3 - issues.append(f"artifact:{artifact}") - break - - # Output looks like it's in English still (common words) - english_indicators = ['the ', ' is ', ' are ', ' was ', ' were ', ' have ', ' has ', 'you ', ' your '] - english_count = sum(1 for ind in english_indicators if ind in out_lower) - if english_count >= 3 and src_len > 20: - score -= 0.3 - issues.append("likely_english") - - # Contains newlines when source doesn't - if '\n' in out and '\n' not in src: - score -= 0.2 - issues.append("added_newlines") - - # ICU/placeholder validation - ok, _ = validate_preserved_tokens(src, out) - if not ok: - score -= 0.3 - issues.append("placeholder_error") - - return max(0.0, score), issues - - -# Keys to skip translation (brand names) -SKIP_KEYS = { - "appTitle", +# Language mapping (locale_code -> (language_name, translategemma_code)) +LOCALE_MAP = { + "es": ("Spanish", "es"), + "fr": ("French", "fr"), + "de": ("German", "de"), + "it": ("Italian", "it"), + "pt": ("Portuguese", "pt"), + "pt-BR": ("Brazilian Portuguese", "pt"), + "ja": ("Japanese", "ja"), + "ko": ("Korean", "ko"), + "zh": ("Chinese", "zh-Hans"), + "zh-Hant": ("Chinese", "zh-Hant"), + "ru": ("Russian", "ru"), + "uk": ("Ukrainian", "uk"), + "ar": ("Arabic", "ar"), + "hi": ("Hindi", "hi"), + "tr": ("Turkish", "tr"), + "nl": ("Dutch", "nl"), + "sv": ("Swedish", "sv"), + "no": ("Norwegian", "no"), + "da": ("Danish", "da"), + "fi": ("Finnish", "fi"), + "pl": ("Polish", "pl"), + "cs": ("Czech", "cs"), + "sk": ("Slovak", "sk"), + "sl": ("Slovenian", "sl"), + "bg": ("Bulgarian", "bg"), + "el": ("Greek", "el"), + "he": ("Hebrew", "he"), + "th": ("Thai", "th"), + "vi": ("Vietnamese", "vi"), + "id": ("Indonesian", "id"), } -# Manual translations for problematic strings (key -> {locale: translation}) +# Keys to skip translation +SKIP_KEYS = {"appTitle"} + +# Manual translations for complex strings MANUAL_TRANSLATIONS: Dict[str, Dict[str, str]] = { "repeater_daysHoursMinsSecs": { "es": "{days} días {hours}h {minutes}m {seconds}s", @@ -340,98 +101,126 @@ MANUAL_TRANSLATIONS: Dict[str, Dict[str, str]] = { } -def is_translatable_entry(key: str, value: Any) -> bool: - if key == "@@locale": - return False - if key in SKIP_KEYS: - return False - if key.startswith("@"): - return False - if not isinstance(value, str): - return False - if value.strip() == "": - return False - return True +def http_post_json(url: str, payload: Dict[str, Any], timeout_s: float) -> Dict[str, Any]: + data = json.dumps(payload).encode("utf-8") + req = request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST") + with request.urlopen(req, timeout=timeout_s) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def ollama_generate(cfg: OllamaConfig, prompt: str) -> str: + url = cfg.host.rstrip("/") + "/api/generate" + payload = { + "model": cfg.model, + "prompt": prompt, + "stream": False, + "options": {"temperature": cfg.temperature}, + } + resp = http_post_json(url, payload, cfg.timeout_s) + return resp.get("response", "").strip() + + +def extract_placeholder_names(s: str) -> List[str]: + """Extract placeholder variable names from string.""" + names = set() + + # Get ICU variable names + for m in ICU_VAR_RE.finditer(s): + names.add(m.group(1)) + + # Get simple placeholders (excluding ICU text forms) + for m in SIMPLE_PLACEHOLDER_RE.finditer(s): + name = m.group(1) + pos = m.start() + rest = s[pos:] + + # Skip if this is part of an ICU block + if re.match(r"\{\w+\s*,\s*(?:plural|select|selectordinal)", rest, re.IGNORECASE): + continue + + # Skip if this is a text form inside ICU (preceded by =X{ or other{) + before = s[:pos] + if re.search(r"(?:=\d+|zero|one|two|few|many|other)\s*$", before, re.IGNORECASE): + continue + + names.add(name) + + return sorted(names) + + +def has_icu_block(s: str) -> bool: + """Check if string contains ICU plural/select block.""" + return bool(ICU_VAR_RE.search(s)) + + +def build_prompt(text: str, target_lang: str, target_code: str, placeholder_names: List[str], has_icu: bool) -> str: + """Build TranslateGemma-compatible prompt with placeholder preservation instructions.""" + # Build instructions for placeholder preservation + instructions = [] + if placeholder_names: + placeholders = ', '.join(f'{{{t}}}' for t in placeholder_names) + instructions.append(f"CRITICAL: Keep these placeholders EXACTLY as they appear: {placeholders}") + if has_icu: + instructions.append("CRITICAL: Preserve ICU message format structure (plural, select, =0, =1, other, etc.). Only translate the text inside the forms.") + + # Add instructions to the system prompt, not to the text itself + instruction_text = "\n".join(instructions) if instructions else "" + separator = "\n" if instruction_text else "" + + # TranslateGemma expects this exact format (note the two blank lines before text) + return f"""You are a professional English (en) to {target_lang} ({target_code}) translator. Your goal is to accurately convey the meaning and nuances of the original English text while adhering to {target_lang} grammar, vocabulary, and cultural sensitivities. +Produce only the {target_lang} translation, without any additional explanations or commentary.{separator}{instruction_text} +Please translate the following English text into {target_lang}: + + +{text}""" + + +def validate_preserved_tokens(src: str, out: str) -> Tuple[bool, Optional[str]]: + """Validate that placeholder names are preserved.""" + src_names = extract_placeholder_names(src) + + for name in src_names: + pattern = r"\{" + re.escape(name) + r"(?:\}|\s*,)" + if not re.search(pattern, out): + return False, f"Missing placeholder: {{{name}}}" + + if has_icu_block(src) and not has_icu_block(out): + return False, "ICU plural/select block missing" + + return True, None def translate_one( key: str, text: str, target_lang: str, + target_code: str, cfg: OllamaConfig, retries: int, backoff_s: float, fallback_cfg: Optional[OllamaConfig] = None, - confidence_threshold: float = 0.7, - model_confidence_threshold: int = 4, - ask_model_confidence: bool = True, ) -> Tuple[str, str, Optional[str], bool]: - """ - Translate a single string. - Returns (key, translated_text, error_or_none, used_fallback_model). - """ + """Translate a single string. Returns (key, translated_text, error_or_none, used_fallback).""" placeholder_names = extract_placeholder_names(text) text_has_icu = has_icu_block(text) - - # Ask for confidence if we have a fallback model - should_ask_confidence = ask_model_confidence and fallback_cfg and fallback_cfg.model != cfg.model - prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu, ask_confidence=should_ask_confidence) - used_fallback = False + prompt = build_prompt(text, target_lang, target_code, placeholder_names, text_has_icu) last_err: Optional[str] = None for attempt in range(retries + 1): try: - raw_out = ollama_generate(cfg, prompt) - - # Parse confidence if we asked for it - if should_ask_confidence: - out, model_confidence = parse_confidence_response(raw_out) - else: - out = raw_out - model_confidence = 5 # Assume high confidence if not asked - + out = ollama_generate(cfg, prompt) + + # Validate placeholders ok, why = validate_preserved_tokens(text, out) if not ok: last_err = f"Validation failed: {why}" - # Retry without confidence format for simpler response - prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu, ask_confidence=False) - prompt = ( - prompt - + "\n\nIMPORTANT: You MUST keep every {...} segment exactly unchanged. " - "If you cannot, return the original text unchanged." - ) + if attempt < retries: + time.sleep(backoff_s * (attempt + 1)) + continue raise ValueError(last_err) - if looks_like_translation_failed(text, out) and attempt < retries: - last_err = "Output identical/suspicious; retrying" - time.sleep(backoff_s * (attempt + 1)) - continue - - # Check if model reported low confidence - use fallback - if model_confidence > 0 and model_confidence < model_confidence_threshold and fallback_cfg: - fallback_prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu, ask_confidence=False) - fallback_out = ollama_generate(fallback_cfg, fallback_prompt) - fallback_ok, _ = validate_preserved_tokens(text, fallback_out) - if fallback_ok and not looks_like_translation_failed(text, fallback_out): - return key, fallback_out, None, True - - # Also check computed confidence and use fallback model if needed - confidence, issues = compute_confidence(text, out) - if confidence < confidence_threshold and fallback_cfg and fallback_cfg.model != cfg.model: - # Low confidence - try with bigger model - fallback_prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu) - fallback_out = ollama_generate(fallback_cfg, fallback_prompt) - fallback_ok, _ = validate_preserved_tokens(text, fallback_out) - fallback_conf, _ = compute_confidence(text, fallback_out) - - if fallback_ok and fallback_conf > confidence: - # Fallback is better - return key, fallback_out, None, True - elif fallback_ok and not ok: - # Original failed validation but fallback passed - return key, fallback_out, None, True - - return key, out, None, used_fallback + return key, out, None, False except Exception as e: last_err = str(e) @@ -439,21 +228,55 @@ def translate_one( time.sleep(backoff_s * (attempt + 1)) continue - # Last resort: try fallback model - if fallback_cfg and fallback_cfg.model != cfg.model: + # Try fallback model if available + if fallback_cfg: try: - fallback_prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu) + fallback_prompt = build_prompt(text, target_lang, target_code, placeholder_names, text_has_icu) fallback_out = ollama_generate(fallback_cfg, fallback_prompt) fallback_ok, _ = validate_preserved_tokens(text, fallback_out) - if fallback_ok and not looks_like_translation_failed(text, fallback_out): + if fallback_ok: return key, fallback_out, None, True except Exception: pass - return key, text, last_err, False # fallback to original on failure + # Fallback to original + return key, text, last_err, False + + +def is_translatable_entry(key: str, value: Any) -> bool: + """Check if an entry should be translated.""" + if key == "@@locale" or key.startswith("@") or key in SKIP_KEYS: + return False + return isinstance(value, str) and value.strip() != "" + + +def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]: + """Find keys that are missing or empty in target.""" + missing = [] + for key in source_data: + if key == "@@locale" or key.startswith("@"): + continue + if key not in target_data or (isinstance(target_data.get(key), str) and target_data[key].strip() == ""): + missing.append(key) + return missing + + +def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]: + """Find all locale .arb files excluding template. Returns [(locale_code, file_path)].""" + locales = [] + template_basename = os.path.basename(template_file) + + for filename in os.listdir(l10n_dir): + if filename.endswith('.arb') and filename != template_basename: + if filename.startswith('app_'): + locale = filename[4:-4] # app_es.arb -> es + locales.append((locale, os.path.join(l10n_dir, filename))) + + return sorted(locales) def fmt_duration(seconds: float) -> str: + """Format duration as human-readable string.""" if seconds < 60: return f"{seconds:.1f}s" m = int(seconds // 60) @@ -465,226 +288,25 @@ def fmt_duration(seconds: float) -> str: return f"{h}h {m2}m" -def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]: - """Find keys that are in source but not in target, or have empty values (excluding metadata keys).""" - missing = [] - for key in source_data: - if key == "@@locale": - continue - if key.startswith("@"): - continue - if key not in target_data: - missing.append(key) - elif isinstance(target_data.get(key), str) and target_data[key].strip() == "": - # Also include keys with empty string values - missing.append(key) - return missing - - -def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]: - """Find all locale .arb files in the directory, excluding the template. - - Returns list of (locale_code, file_path) tuples. - """ - locales = [] - template_basename = os.path.basename(template_file) - - for filename in os.listdir(l10n_dir): - if not filename.endswith('.arb'): - continue - if filename == template_basename: - continue - # Extract locale from filename like app_es.arb -> es - if filename.startswith('app_') and filename.endswith('.arb'): - locale = filename[4:-4] # Remove 'app_' prefix and '.arb' suffix - filepath = os.path.join(l10n_dir, filename) - locales.append((locale, filepath)) - - return sorted(locales) - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--in", dest="in_path", required=True, help="Input .arb/.json file path (source/template)") - ap.add_argument("--out", dest="out_path", default=None, help="Output .arb/.json file path (required unless using --l10n-dir)") - ap.add_argument("--to-locale", default=None, help="Target locale code, e.g. es, fr, de (required unless using --l10n-dir)") - ap.add_argument("--l10n-dir", default=None, help="Directory containing locale .arb files. When set, translates all locales.") - ap.add_argument("--missing-only", action="store_true", help="Only translate keys missing from target file") - ap.add_argument("--target-lang", default=None, help="Target language name for the model, e.g. Spanish (defaults from locale)") - ap.add_argument("--model", default="gemma3:4b", help="Ollama model name") - ap.add_argument("--fallback-model", default=None, help="Larger model to use for low-confidence translations") - ap.add_argument("--confidence-threshold", type=float, default=0.7, help="Computed confidence threshold to trigger fallback (0.0-1.0)") - ap.add_argument("--model-confidence-threshold", type=int, default=4, help="Model self-reported confidence threshold (1-5, use fallback if below)") - ap.add_argument("--retry-model", default="ministral-3:latest", help="Model to use for end-of-run retries") - ap.add_argument("--host", default="http://localhost:11434", help="Ollama host") - ap.add_argument("--timeout", type=float, default=120.0, help="HTTP timeout seconds") - ap.add_argument("--temperature", type=float, default=0.2, help="Model temperature") - ap.add_argument("--num-ctx", type=int, default=4096, help="Context size") - ap.add_argument("--num-predict", type=int, default=256, help="Max tokens to generate") - ap.add_argument("--top-p", type=float, default=0.9, help="Top-p") - ap.add_argument("--concurrency", type=int, default=4, help="Parallel requests") - ap.add_argument("--retries", type=int, default=2, help="Retries per string") - ap.add_argument("--backoff", type=float, default=0.6, help="Backoff seconds base") - ap.add_argument("--dry-run", action="store_true", help="Do not write file; just print summary") - ap.add_argument("--progress-every", type=int, default=1, help="Print progress every N completed strings (default: 1)") - args = ap.parse_args() - - locale_map = { - "es": "Spanish", - "fr": "French", - "de": "German", - "it": "Italian", - "pt": "Portuguese", - "pt-BR": "Brazilian Portuguese", - "ja": "Japanese", - "ko": "Korean", - "zh": "Chinese (Simplified)", - "zh-Hant": "Chinese (Traditional)", - "ru": "Russian", - "uk": "Ukrainian", - "ar": "Arabic", - "hi": "Hindi", - "tr": "Turkish", - "nl": "Dutch", - "sv": "Swedish", - "no": "Norwegian", - "da": "Danish", - "fi": "Finnish", - "pl": "Polish", - "cs": "Czech", - "sk": "Slovak", - "sl": "Slovenian", - "bg": "Bulgarian", - "el": "Greek", - "he": "Hebrew", - "th": "Thai", - "vi": "Vietnamese", - "id": "Indonesian", - } - - # Read source/template file - try: - with open(args.in_path, "r", encoding="utf-8") as f: - source_data = json.load(f) - except Exception as e: - print(f"Failed to read input: {e}", file=sys.stderr) - return 2 - - if not isinstance(source_data, dict): - print("Input JSON must be an object at top-level.", file=sys.stderr) - return 2 - - # If --l10n-dir is provided, process all locale files - if args.l10n_dir: - locales = get_all_locale_files(args.l10n_dir, args.in_path) - if not locales: - print(f"No locale files found in {args.l10n_dir}", file=sys.stderr) - return 1 - - print(f"Found {len(locales)} locale file(s) to process") - - total_translated = 0 - for locale_code, locale_path in locales: - target_lang = locale_map.get(locale_code, locale_code) - - # Read existing target file - try: - with open(locale_path, "r", encoding="utf-8") as f: - target_data = json.load(f) - except Exception as e: - print(f" [{locale_code}] Failed to read {locale_path}: {e}") - continue - - if args.missing_only: - missing_keys = find_missing_keys(source_data, target_data) - if not missing_keys: - print(f" [{locale_code}] No missing keys") - continue - print(f" [{locale_code}] {len(missing_keys)} missing key(s): {', '.join(missing_keys[:5])}{'...' if len(missing_keys) > 5 else ''}") - else: - missing_keys = None - - # Run translation for this locale - result = translate_locale( - source_data=source_data, - target_data=target_data, - target_locale=locale_code, - target_lang=target_lang, - out_path=locale_path, - args=args, - locale_map=locale_map, - missing_keys=missing_keys, - ) - total_translated += result - - print(f"\nTotal: {total_translated} string(s) translated across {len(locales)} locale(s)") - return 0 - - # Single locale mode - validate required args - if not args.out_path: - print("--out is required when not using --l10n-dir", file=sys.stderr) - return 1 - if not args.to_locale: - print("--to-locale is required when not using --l10n-dir", file=sys.stderr) - return 1 - - target_lang = args.target_lang or locale_map.get(args.to_locale, args.to_locale) - - # Read existing target file if --missing-only and file exists - target_data: Dict[str, Any] = {} - missing_keys: Optional[List[str]] = None - if args.missing_only: - if os.path.exists(args.out_path): - try: - with open(args.out_path, "r", encoding="utf-8") as f: - target_data = json.load(f) - missing_keys = find_missing_keys(source_data, target_data) - if not missing_keys: - print(f"No missing keys in {args.out_path}") - return 0 - print(f"Found {len(missing_keys)} missing key(s) to translate") - except Exception as e: - print(f"Failed to read target file: {e}", file=sys.stderr) - return 2 - else: - print(f"Target file {args.out_path} does not exist. Will translate all strings.") - - result = translate_locale( - source_data=source_data, - target_data=target_data, - target_locale=args.to_locale, - target_lang=target_lang, - out_path=args.out_path, - args=args, - locale_map=locale_map, - missing_keys=missing_keys, - ) - return 0 if result >= 0 else 1 - - def translate_locale( source_data: Dict[str, Any], target_data: Dict[str, Any], target_locale: str, target_lang: str, + target_code: str, out_path: str, args, - locale_map: Dict[str, str], missing_keys: Optional[List[str]] = None, ) -> int: """Translate a single locale. Returns number of strings translated.""" - + cfg = OllamaConfig( host=args.host, model=args.model, timeout_s=args.timeout, temperature=args.temperature, - num_ctx=args.num_ctx, - num_predict=args.num_predict, - top_p=args.top_p, ) - # Fallback model for low-confidence translations fallback_cfg = None if args.fallback_model: fallback_cfg = OllamaConfig( @@ -692,34 +314,27 @@ def translate_locale( model=args.fallback_model, timeout_s=args.timeout, temperature=args.temperature, - num_ctx=args.num_ctx, - num_predict=args.num_predict, - top_p=args.top_p, ) - # Start with target data (preserves existing translations) or source data - if target_data: - out_data: Dict[str, Any] = dict(target_data) - else: - out_data: Dict[str, Any] = dict(source_data) + # Start with target data or source data + out_data: Dict[str, Any] = dict(target_data) if target_data else dict(source_data) out_data["@@locale"] = target_locale # Build list of items to translate if missing_keys is not None: - # Only translate missing keys items: List[Tuple[str, str]] = [ - (k, source_data[k]) for k in missing_keys + (k, source_data[k]) for k in missing_keys if is_translatable_entry(k, source_data.get(k)) ] - # Also copy over any metadata keys for missing items + # Copy metadata for missing items for key in missing_keys: meta_key = f"@{key}" if meta_key in source_data: out_data[meta_key] = source_data[meta_key] else: items: List[Tuple[str, str]] = [(k, v) for k, v in source_data.items() if is_translatable_entry(k, v)] - - # Apply manual translations first + + # Apply manual translations manual_count = 0 items_to_translate: List[Tuple[str, str]] = [] for k, v in items: @@ -728,154 +343,73 @@ def translate_locale( manual_count += 1 else: items_to_translate.append((k, v)) - + if manual_count > 0: print(f"Applied {manual_count} manual translation(s)") - - total = len(items_to_translate) - if total == 0 and manual_count == 0: - print("No translatable string entries found (excluding @@locale and @metadata).") - return 0 - - if total == 0: - print("All strings handled by manual translations.") - else: - fallback_info = f" (fallback: {args.fallback_model})" if args.fallback_model else "" - print(f"Translating {total} strings -> {target_lang} using {cfg.model}{fallback_info} (concurrency={args.concurrency})") - - start = time.time() + total = len(items_to_translate) + if total == 0: + if manual_count > 0: + print("All strings handled by manual translations.") + return manual_count + + fallback_info = f" (fallback: {args.fallback_model})" if args.fallback_model else "" + print(f"Translating {total} strings -> {target_lang} using {cfg.model}{fallback_info} (concurrency={args.concurrency})") + + start = time.time() failures: List[Tuple[str, str]] = [] - translated_ok = manual_count # Count manual translations as OK + translated_ok = manual_count fallback_used = 0 completed = 0 - # Build a lookup for original text by key - items_dict: Dict[str, str] = dict(items_to_translate) + with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex: + future_to_key = { + ex.submit( + translate_one, + key=k, + text=v, + target_lang=target_lang, + target_code=target_code, + cfg=cfg, + retries=args.retries, + backoff_s=args.backoff, + fallback_cfg=fallback_cfg, + ): k + for (k, v) in items_to_translate + } - # Submit all tasks up front - if total > 0: - with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex: - future_to_key = { - ex.submit( - translate_one, - key=k, - text=v, - target_lang=target_lang, - cfg=cfg, - retries=args.retries, - backoff_s=args.backoff, - fallback_cfg=fallback_cfg, - confidence_threshold=args.confidence_threshold, - model_confidence_threshold=args.model_confidence_threshold, - ask_model_confidence=bool(args.fallback_model), - ): k - for (k, v) in items_to_translate - } + for fut in as_completed(future_to_key): + k, translated, err, used_fallback = fut.result() + out_data[k] = translated - for fut in as_completed(future_to_key): - k, translated, err, used_fallback = fut.result() - out_data[k] = translated - - completed += 1 - if err: - failures.append((k, err)) - status = "FAIL" + completed += 1 + if err: + failures.append((k, err)) + status = "FAIL" + else: + translated_ok += 1 + if used_fallback: + fallback_used += 1 + status = "OK*" else: - translated_ok += 1 - if used_fallback: - fallback_used += 1 - status = "OK*" # asterisk indicates fallback model was used - else: - status = "OK" - - if args.progress_every > 0 and (completed % args.progress_every == 0 or completed == total): - elapsed = time.time() - start - rate = completed / elapsed if elapsed > 0 else 0.0 - remaining = (total - completed) / rate if rate > 0 else 0.0 - # Keep it single-line friendly but readable. - print( - f"[{completed:>4}/{total}] {status:<4} {k} | " - f"elapsed {fmt_duration(elapsed)} | ETA {fmt_duration(remaining)}" - ) - - elapsed = time.time() - start - fallback_msg = f", used_fallback_model={fallback_used}" if fallback_used > 0 else "" - print(f"Done in {fmt_duration(elapsed)}. OK={translated_ok}{fallback_msg}, errors={len(failures)}") - - # Retry failed translations at the end with increasing temperature - retry_round = 1 - max_end_retries = 3 - retry_model = args.retry_model - while failures and retry_round <= max_end_retries: - # Increase temperature for each retry round - retry_temp = min(cfg.temperature + (0.2 * retry_round), 1.0) - print(f"\n--- Retry round {retry_round}/{max_end_retries} for {len(failures)} failed key(s) (model={retry_model}, temp={retry_temp:.1f}) ---") - retry_items = [(k, items_dict[k]) for k, _ in failures] - failures = [] - retry_completed = 0 - retry_total = len(retry_items) - retry_start = time.time() - - # Create config with higher temperature (and optionally different model) for retries - retry_cfg = OllamaConfig( - host=cfg.host, - model=retry_model, - timeout_s=cfg.timeout_s, - temperature=retry_temp, - num_ctx=cfg.num_ctx, - num_predict=cfg.num_predict, - top_p=cfg.top_p, - ) - - with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex: - future_to_key = { - ex.submit( - translate_one, - key=k, - text=v, - target_lang=target_lang, - cfg=retry_cfg, - retries=args.retries, - backoff_s=args.backoff, - ): k - for (k, v) in retry_items - } - - for fut in as_completed(future_to_key): - k, translated, err, used_fb = fut.result() - out_data[k] = translated - - retry_completed += 1 - if err: - failures.append((k, err)) - status = "FAIL" - else: - translated_ok += 1 status = "OK" - if args.progress_every > 0 and (retry_completed % args.progress_every == 0 or retry_completed == retry_total): - elapsed = time.time() - retry_start - rate = retry_completed / elapsed if elapsed > 0 else 0.0 - remaining = (retry_total - retry_completed) / rate if rate > 0 else 0.0 - print( - f"[{retry_completed:>4}/{retry_total}] {status:<4} {k} | " - f"elapsed {fmt_duration(elapsed)} | ETA {fmt_duration(remaining)}" - ) + if completed % args.progress_every == 0 or completed == total: + elapsed = time.time() - start + rate = completed / elapsed if elapsed > 0 else 0.0 + remaining = (total - completed) / rate if rate > 0 else 0.0 + print(f"[{completed:>4}/{total}] {status:<4} {k} | elapsed {fmt_duration(elapsed)} | ETA {fmt_duration(remaining)}") - retry_elapsed = time.time() - retry_start - print(f"Retry round {retry_round} done in {fmt_duration(retry_elapsed)}. Remaining failures: {len(failures)}") - retry_round += 1 - - total_elapsed = time.time() - start - print(f"\nTotal time: {fmt_duration(total_elapsed)}. OK={translated_ok}, final fallback={len(failures)}") + elapsed = time.time() - start + fallback_msg = f", fallback_used={fallback_used}" if fallback_used > 0 else "" + print(f"Done in {fmt_duration(elapsed)}. OK={translated_ok}{fallback_msg}, errors={len(failures)}") if failures: - print("Fallback keys (kept original English due to errors):") - for k, err in failures[:60]: + print(f"{len(failures)} translation(s) kept original English:") + for k, err in failures[:20]: print(f" - {k}: {err}") - if len(failures) > 60: - print(f" ... and {len(failures) - 60} more") + if len(failures) > 20: + print(f" ... and {len(failures) - 20} more") if args.dry_run: print("Dry run: not writing output file.") @@ -893,5 +427,116 @@ def translate_locale( return translated_ok +def main() -> int: + ap = argparse.ArgumentParser(description="Translate ARB files using TranslateGemma") + ap.add_argument("--in", dest="in_path", required=True, help="Input .arb file (source/template)") + ap.add_argument("--out", dest="out_path", help="Output .arb file (required unless using --l10n-dir)") + ap.add_argument("--to-locale", help="Target locale code (es, fr, de, etc.)") + ap.add_argument("--l10n-dir", help="Directory with locale files (translates all locales)") + ap.add_argument("--missing-only", action="store_true", help="Only translate missing keys") + ap.add_argument("--model", default="translategemma:latest", help="Ollama model (translategemma:latest or specific versions)") + ap.add_argument("--fallback-model", help="Fallback model for failed translations (e.g., translategemma:27b)") + ap.add_argument("--host", default="http://localhost:11434", help="Ollama host") + ap.add_argument("--timeout", type=float, default=120.0, help="HTTP timeout seconds") + ap.add_argument("--temperature", type=float, default=0.0, help="Model temperature (0.0 for deterministic)") + ap.add_argument("--concurrency", type=int, default=4, help="Parallel requests") + ap.add_argument("--retries", type=int, default=2, help="Retries per string") + ap.add_argument("--backoff", type=float, default=0.6, help="Backoff seconds base") + ap.add_argument("--dry-run", action="store_true", help="Don't write output") + ap.add_argument("--progress-every", type=int, default=1, help="Print progress every N strings") + args = ap.parse_args() + + # Read source file + try: + with open(args.in_path, "r", encoding="utf-8") as f: + source_data = json.load(f) + except Exception as e: + print(f"Failed to read input: {e}", file=sys.stderr) + return 2 + + if not isinstance(source_data, dict): + print("Input JSON must be an object at top-level.", file=sys.stderr) + return 2 + + # Process all locales if --l10n-dir is provided + if args.l10n_dir: + locales = get_all_locale_files(args.l10n_dir, args.in_path) + if not locales: + print(f"No locale files found in {args.l10n_dir}", file=sys.stderr) + return 1 + + print(f"Found {len(locales)} locale file(s) to process") + + total_translated = 0 + for locale_code, locale_path in locales: + lang_name, lang_code = LOCALE_MAP.get(locale_code, (locale_code, locale_code)) + + try: + with open(locale_path, "r", encoding="utf-8") as f: + target_data = json.load(f) + except Exception as e: + print(f" [{locale_code}] Failed to read {locale_path}: {e}") + continue + + if args.missing_only: + missing_keys = find_missing_keys(source_data, target_data) + if not missing_keys: + print(f" [{locale_code}] No missing keys") + continue + print(f" [{locale_code}] {len(missing_keys)} missing key(s)") + else: + missing_keys = None + + result = translate_locale( + source_data=source_data, + target_data=target_data, + target_locale=locale_code, + target_lang=lang_name, + target_code=lang_code, + out_path=locale_path, + args=args, + missing_keys=missing_keys, + ) + total_translated += result + + print(f"\nTotal: {total_translated} string(s) translated across {len(locales)} locale(s)") + return 0 + + # Single locale mode + if not args.out_path or not args.to_locale: + print("--out and --to-locale are required when not using --l10n-dir", file=sys.stderr) + return 1 + + lang_name, lang_code = LOCALE_MAP.get(args.to_locale, (args.to_locale, args.to_locale)) + + # Read existing target file if --missing-only + target_data: Dict[str, Any] = {} + missing_keys: Optional[List[str]] = None + if args.missing_only and os.path.exists(args.out_path): + try: + with open(args.out_path, "r", encoding="utf-8") as f: + target_data = json.load(f) + missing_keys = find_missing_keys(source_data, target_data) + if not missing_keys: + print(f"No missing keys in {args.out_path}") + return 0 + print(f"Found {len(missing_keys)} missing key(s) to translate") + except Exception as e: + print(f"Failed to read target file: {e}", file=sys.stderr) + return 2 + + result = translate_locale( + source_data=source_data, + target_data=target_data, + target_locale=args.to_locale, + target_lang=lang_name, + target_code=lang_code, + out_path=args.out_path, + args=args, + missing_keys=missing_keys, + ) + return 0 if result >= 0 else 1 + + if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/untranslated.json b/untranslated.json index b9dadf3..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,69 +1 @@ -{ - "bg": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "de": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "es": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "fr": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "it": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "nl": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "pl": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "pt": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "ru": [ - "appSettings_languageUk" - ], - - "sk": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "sl": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "sv": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "uk": [ - "appSettings_languageRu" - ], - - "zh": [ - "appSettings_languageRu", - "appSettings_languageUk" - ] -} +{} \ No newline at end of file From 818f514702011bc4ed5dd1c8a4b3619b45483953 Mon Sep 17 00:00:00 2001 From: Zach Date: Sun, 1 Feb 2026 17:08:53 -0700 Subject: [PATCH 056/421] The first issue was that the toggle switch states weren't being initialized when settings were refreshed from the device. The code would correctly update the interval values themselves, but failed to set the corresponding boolean flags that control whether the toggles appear as "on" or "off". This meant that if you refreshed settings from a device that had advertisements disabled (with an interval of zero), the toggles would incorrectly show as enabled even though the device was actually broadcasting no advertisements. We fixed this by adding two lines that explicitly set _advertEnable = _advertInterval > 0 and _floodAdvertEnable = _floodAdvertInterval > 0 after parsing the interval values from device responses. The second critical bug was in the validation logic that checks whether responses from the device contain valid data. The validator was rejecting any interval values of zero because it checked interval > 0, but zero is now a meaningful and valid value that indicates advertisements are disabled. Without this fix, any time a device reported back that advertisements were disabled, the app would silently discard that information as invalid, leaving the UI out of sync with reality. We changed the validation to use interval >= 0 instead and updated the comment to explicitly document that zero means disabled. The third fix was a minor code style issue where a single-line if statement was missing braces, causing a linter warning. This doesn't affect functionality but ensures the code meets project standards. --- lib/screens/repeater_settings_screen.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart index 018caef..bae0f50 100644 --- a/lib/screens/repeater_settings_screen.dart +++ b/lib/screens/repeater_settings_screen.dart @@ -248,12 +248,14 @@ class _RepeaterSettingsScreenState extends State { _fetchedSettings['advert.interval']!, _advertInterval, ); + _advertEnable = _advertInterval > 0; } if (_fetchedSettings.containsKey('flood.advert.interval')) { _floodAdvertInterval = _parseIntWithFallback( _fetchedSettings['flood.advert.interval']!, _floodAdvertInterval, ); + _floodAdvertEnable = _floodAdvertInterval > 0; } if (_fetchedSettings.containsKey('priv.advert.interval')) { _privAdvertInterval = _parseIntWithFallback( @@ -379,18 +381,19 @@ class _RepeaterSettingsScreenState extends State { case 'advert.interval': case 'flood.advert.interval': case 'priv.advert.interval': - // Interval: positive integer + // Interval: non-negative integer (0 means disabled) if (value.contains(',')) return false; final interval = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), '')); - return interval != null && interval > 0; + return interval != null && interval >= 0; case 'name': // Name: any non-empty string, but should NOT look like radio settings if (value.isEmpty) return false; // If it has 3+ commas and looks like numbers, probably radio data final commaCount = ','.allMatches(value).length; - if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) + if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) { return false; + } return true; default: From c742d98fbbff6769e73252772cd42fb38b7671ff Mon Sep 17 00:00:00 2001 From: Zach Date: Sun, 1 Feb 2026 18:37:14 -0700 Subject: [PATCH 057/421] issue #112 fixes and more --- TESTFLIGHT_GUIDE.md | 244 ++++++++++++++++++++++++++ lib/connector/meshcore_connector.dart | 51 +++--- lib/main.dart | 6 + lib/screens/channels_screen.dart | 2 +- lib/screens/map_screen.dart | 39 +--- lib/screens/settings_screen.dart | 13 +- macos/Podfile.lock | 7 - 7 files changed, 300 insertions(+), 62 deletions(-) create mode 100644 TESTFLIGHT_GUIDE.md diff --git a/TESTFLIGHT_GUIDE.md b/TESTFLIGHT_GUIDE.md new file mode 100644 index 0000000..b092678 --- /dev/null +++ b/TESTFLIGHT_GUIDE.md @@ -0,0 +1,244 @@ +# TestFlight and App Store Deployment Guide + +## Prerequisites + +- [x] Apple Developer Account ($99/year) - [developer.apple.com](https://developer.apple.com) +- [x] Xcode installed +- [x] Apple Transporter app installed +- [x] App icons ready (1024x1024px) +- [x] Bundle ID configured: `com.monitormx.meshcoreopen` + +## Step 1: Register Bundle Identifier + +1. Go to [Apple Developer - Identifiers](https://developer.apple.com/account/resources/identifiers/list) +2. Click the **"+"** button +3. Select **"App IDs"** → Continue +4. Select **"App"** → Continue +5. Fill in: + - **Description**: Meshcore Open + - **Bundle ID**: Explicit - `com.monitormx.meshcoreopen` + - **Capabilities**: Leave defaults (or add as needed) +6. Click **Continue** → **Register** + +## Step 2: Create App in App Store Connect + +1. Go to [App Store Connect](https://appstoreconnect.apple.com) +2. Sign in with your Apple ID +3. Click **"My Apps"** +4. Click the **"+"** button → **"New App"** +5. Fill in the form: + - **Platforms**: iOS + - **Name**: Meshcore Open + - **Primary Language**: English (U.S.) + - **Bundle ID**: Select `com.monitormx.meshcoreopen` from dropdown + - **SKU**: `meshcore-open-001` (or any unique identifier) + - **User Access**: Full Access +6. Click **"Create"** + +## Step 3: Build the IPA + +Run these commands from the project directory: + +```bash +# Add CocoaPods to PATH +export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH" + +# Clean previous builds +../flutter/bin/flutter clean + +# Build IPA for App Store +../flutter/bin/flutter build ipa +``` + +The IPA will be created at: `build/ios/ipa/meshcore_open.ipa` + +## Step 4: Upload to App Store Connect via Transporter + +1. **Open Apple Transporter** + - Launch from Applications folder + - Sign in with your Apple ID + +2. **Upload the IPA** + - Drag and drop `build/ios/ipa/meshcore_open.ipa` into Transporter + - Click **"Deliver"** + - Wait for upload to complete (usually 1-5 minutes) + +3. **Processing** + - Apple will process your build (10-30 minutes) + - You'll receive an email when processing is complete + +## Step 5: Configure App Store Connect Metadata + +### App Information +1. In App Store Connect, go to your app +2. Fill in required information: + - **Subtitle**: Short description (30 chars max) + - **Privacy Policy URL**: Required for Bluetooth apps + - **Category**: Utilities or Productivity + - **Age Rating**: Complete questionnaire + +### App Store Listing +1. Go to **App Store** tab +2. Upload **Screenshots** (required): + - iPhone 6.7" display (1290 x 2796 pixels) - At least 1 screenshot + - iPhone 6.5" display (1242 x 2688 pixels) - At least 1 screenshot + - Optional: iPad screenshots + +3. Fill in **Description**: + ``` + Meshcore Open is a Flutter client for MeshCore LoRa mesh networking devices. + + Features: + - BLE connectivity to MeshCore devices + - Real-time mesh network communication + - Map visualization with OpenStreetMap + - Community management with QR code scanning + - Message tracking and retry system + + Connect to your MeshCore LoRa device and start communicating over the mesh network. + ``` + +4. **Keywords**: `lora,mesh,networking,bluetooth,communication` +5. **Support URL**: Your GitHub or website URL +6. **Marketing URL**: (Optional) + +### Version Information +1. **What's New in This Version**: + ``` + Initial release of Meshcore Open + + - BLE device connectivity + - Mesh network messaging + - Map integration + - Community features + ``` + +2. **Build**: Select the uploaded build once processing completes + +## Step 6: TestFlight Setup + +### Internal Testing (No Review Required) +1. Go to **TestFlight** tab in App Store Connect +2. Click **Internal Testing** → **"+"** to create a group +3. Name your group (e.g., "Internal Testers") +4. Add yourself as a tester using your email +5. Select the build you uploaded +6. Testers will receive an email with TestFlight invitation + +### External Testing (Requires Beta Review) +1. Click **External Testing** → **"+"** to create a group +2. Add build and testers +3. Fill in **Test Information**: + - **What to Test**: Brief description of features + - **Feedback Email**: Your email address +4. Click **Submit for Review** +5. Beta review typically takes 24-48 hours + +## Step 7: App Store Submission + +Once you're ready for public release: + +1. Go to **App Store** tab +2. Complete all required metadata (if not done) +3. Select your build +4. Fill in **App Review Information**: + - **Contact Information**: Your name, phone, email + - **Demo Account**: If app requires login + - **Notes**: Any special instructions for reviewers +5. Answer **Export Compliance** questions: + - Does your app use encryption? **Yes** (uses TLS/HTTPS) + - Is encryption registration required? **No** (standard encryption) +6. Click **Add for Review** +7. Review summary and click **Submit to App Review** + +## Step 8: After Submission + +- **App Review**: Typically 24-48 hours +- **Common Rejection Reasons**: + - Missing privacy policy + - Incomplete app information + - Crashes or bugs + - Misleading app description + +- **If Approved**: You can release immediately or schedule a release date +- **If Rejected**: Address issues and resubmit + +## Updating the App + +When you need to release an update: + +1. **Update version** in `pubspec.yaml`: + ```yaml + version: 0.5.0+6 # Increment version (0.5.0) and build number (+6) + ``` + +2. **Build new IPA**: + ```bash + export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH" + ../flutter/bin/flutter clean + ../flutter/bin/flutter build ipa + ``` + +3. **Upload via Transporter** (same process as above) + +4. **Create new version** in App Store Connect: + - Click **"+"** next to versions + - Select version number + - Update "What's New" text + - Select new build + - Submit for review + +## macOS Build (Bonus) + +To build for macOS: + +```bash +export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH" +../flutter/bin/flutter build macos --release +cd build/macos/Build/Products/Release +zip -r meshcore_open-macos.zip meshcore_open.app +``` + +Distribution: +- Share the zip file directly +- Users unzip and drag to Applications +- First run: Right-click → Open (to bypass Gatekeeper) + +## Troubleshooting + +### Build Errors +- **CocoaPods not found**: Ensure PATH includes `/opt/homebrew/lib/ruby/gems/4.0.0/bin` +- **No signing certificate**: Configure Team in Xcode (Signing & Capabilities) +- **Bundle ID mismatch**: Check `ios/Runner.xcodeproj/project.pbxproj` + +### Upload Errors +- **No profiles found**: Create app in App Store Connect first +- **Bundle ID not registered**: Register in Apple Developer portal +- **Authentication failed**: Use Transporter app instead of CLI + +### TestFlight Issues +- **Build not appearing**: Wait 10-30 minutes for processing +- **Can't add testers**: Check you have available slots (100 internal, 10,000 external) +- **TestFlight crashes**: Check device logs in Xcode → Devices & Simulators + +## Important Files + +- **iOS IPA**: `build/ios/ipa/meshcore_open.ipa` +- **macOS App**: `build/macos/Build/Products/Release/meshcore_open.app` +- **Bundle ID Config**: `ios/Runner.xcodeproj/project.pbxproj` +- **Version Info**: `pubspec.yaml` + +## Useful Links + +- [App Store Connect](https://appstoreconnect.apple.com) +- [Apple Developer Portal](https://developer.apple.com/account) +- [TestFlight Documentation](https://developer.apple.com/testflight/) +- [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/) +- [Flutter iOS Deployment](https://docs.flutter.dev/deployment/ios) + +## Support + +For issues with: +- **App Store Process**: [Apple Developer Support](https://developer.apple.com/contact/) +- **Flutter Build Issues**: [Flutter GitHub](https://github.com/flutter/flutter/issues) +- **Meshcore Open App**: [GitHub Issues](https://github.com/wel97459/meshcore-open/issues) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 3a36a92..d7d7dc9 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -95,6 +95,7 @@ class MeshCoreConnector extends ChangeNotifier { double? _selfLongitude; bool _isLoadingContacts = false; bool _isLoadingChannels = false; + bool _hasLoadedChannels = false; bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _preserveContactsOnRefresh = false; @@ -122,7 +123,7 @@ class MeshCoreConnector extends ChangeNotifier { List _previousChannelsCache = []; static const int _maxChannelSyncRetries = 3; static const int _channelSyncTimeoutMs = 2000; // 2 second timeout per channel - static const Duration _batteryPollInterval = Duration(seconds: 30); + static const Duration _batteryPollInterval = Duration(seconds: 120); // Services MessageRetryService? _retryService; @@ -927,6 +928,7 @@ class MeshCoreConnector extends ChangeNotifier { _pendingQueueSync = false; _isSyncingChannels = false; _channelSyncInFlight = false; + _hasLoadedChannels = false; _setState(MeshCoreConnectionState.disconnected); if (!manual) { @@ -1493,13 +1495,19 @@ class MeshCoreConnector extends ChangeNotifier { await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}'); } - Future getChannels({int? maxChannels}) async { + Future getChannels({int? maxChannels, bool force = false}) async { if (!isConnected) return; if (_isSyncingChannels) { debugPrint('[ChannelSync] Already syncing channels, ignoring request'); return; } + // Skip fetching if already loaded and not forced + if (_hasLoadedChannels && !force) { + debugPrint('[ChannelSync] Channels already loaded, skipping fetch (use force=true to reload)'); + return; + } + _isLoadingChannels = true; _isSyncingChannels = true; _previousChannelsCache = List.from(_channels); @@ -1619,6 +1627,7 @@ class MeshCoreConnector extends ChangeNotifier { _totalChannelsToRequest = 0; if (completed) { + _hasLoadedChannels = true; _previousChannelsCache.clear(); } // Keep cache on failure/disconnection for future attempts @@ -1629,7 +1638,7 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildSetChannelFrame(index, name, psk)); // Refresh channels after setting - await getChannels(); + await getChannels(force: true); } Future deleteChannel(int index) async { @@ -1644,7 +1653,7 @@ class MeshCoreConnector extends ChangeNotifier { // Clear in-memory messages for this channel _channelMessages.remove(index); // Refresh channels after deleting - await getChannels(); + await getChannels(force: true); } void _handleFrame(List data) { @@ -2105,6 +2114,15 @@ class MeshCoreConnector extends ChangeNotifier { } if (message != null) { + // Ignore messages from self (device hearing its own broadcast) + // BUT allow repeated messages (pathLength indicates it went through repeater) + if (_selfPublicKey != null && + message.senderKeyHex == pubKeyToHex(_selfPublicKey!) && + (message.pathLength == null || message.pathLength == 0)) { + debugPrint('Ignoring direct message from self'); + return; + } + final contact = _contacts.cast().firstWhere( (c) => c?.publicKeyHex == message!.senderKeyHex, orElse: () => null, @@ -3066,28 +3084,19 @@ class MeshCoreConnector extends ChangeNotifier { } bool _shouldDropSelfChannelMessage(String senderName, Uint8List pathBytes) { - final selfKey = _selfPublicKey; - if (selfKey == null) return false; - if (pathBytes.length < pathHashSize) return false; final trimmed = senderName.trim(); if (trimmed.isEmpty) return false; + final selfName = _selfName?.trim(); if (selfName == null || selfName.isEmpty) return false; + + // If sender name doesn't match, keep the message if (trimmed != selfName) return false; - final prefix = selfKey.sublist(0, pathHashSize); - for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) { - var match = true; - for (int j = 0; j < pathHashSize; j++) { - if (pathBytes[i + j] != prefix[j]) { - match = false; - break; - } - } - if (match) { - return true; - } - } - return false; + + // Name matches - this is from self + // Drop only if pathBytes is empty (direct broadcast) + // Keep if pathBytes has data (repeated through another node) + return pathBytes.isEmpty; } Uint8List _selectPreferredPathBytes(Uint8List existing, Uint8List incoming) { diff --git a/lib/main.dart b/lib/main.dart index 6dac19b..cd0887f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -128,6 +128,9 @@ class MeshCoreApp extends StatelessWidget { theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + ), ), darkTheme: ThemeData( colorScheme: ColorScheme.fromSeed( @@ -135,6 +138,9 @@ class MeshCoreApp extends StatelessWidget { brightness: Brightness.dark, ), useMaterial3: true, + snackBarTheme: const SnackBarThemeData( + behavior: SnackBarBehavior.floating, + ), ), themeMode: _themeModeFromSetting(settingsService.settings.themeMode), home: const ScannerScreen(), diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index efd7340..e54f3f1 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -164,7 +164,7 @@ class _ChannelsScreenState extends State ), body: RefreshIndicator( onRefresh: () async { - await context.read().getChannels(); + await context.read().getChannels(force: true); }, child: () { if (connector.isLoadingChannels) { diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index edef811..734f2b2 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -225,13 +225,15 @@ class _MapScreenState extends State { } // Re center map after removed markers have loaded - if (!_hasInitializedMap && _removedMarkersLoaded && hasMapContent) { + if (!_hasInitializedMap && _removedMarkersLoaded) { _hasInitializedMap = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - _mapController.move(center, initialZoom); - } - }); + if (hasMapContent) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _mapController.move(center, initialZoom); + } + }); + } } final allowBack = !connector.isConnected; @@ -275,9 +277,7 @@ class _MapScreenState extends State { ), ], ), - body: !hasMapContent - ? _buildEmptyState() - : Stack( + body: Stack( children: [ FlutterMap( mapController: _mapController, @@ -376,27 +376,6 @@ class _MapScreenState extends State { ); } - Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.location_off, size: 64, color: Colors.grey[400]), - const SizedBox(height: 16), - Text( - context.l10n.map_noNodesWithLocation, - style: TextStyle(fontSize: 18, color: Colors.grey[600]), - ), - const SizedBox(height: 8), - Text( - context.l10n.map_nodesNeedGps, - textAlign: TextAlign.center, - style: TextStyle(fontSize: 14, color: Colors.grey[500]), - ), - ], - ), - ); - } List _buildMarkers(List contacts, settings) { final markers = []; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c6a85d7..04740d8 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -780,10 +780,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { return; } - if (txPower == null || txPower < 0 || txPower > 22) { + final maxTxPower = widget.connector.maxTxPower ?? 22; + if (txPower == null || txPower < 0 || txPower > maxTxPower) { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid))); + ).showSnackBar( + SnackBar( + content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), + ), + ); return; } @@ -932,7 +937,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { decoration: InputDecoration( labelText: l10n.settings_txPower, border: const OutlineInputBorder(), - helperText: l10n.settings_txPowerHelper, + helperText: widget.connector.maxTxPower != null + ? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)' + : l10n.settings_txPowerHelper, ), keyboardType: TextInputType.number, ), diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a87b4cf..65fed26 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -9,9 +9,6 @@ PODS: - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -29,7 +26,6 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -46,8 +42,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos - path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite_darwin: @@ -63,7 +57,6 @@ SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd From b34d684e67195b869987462162bc03df412748fd Mon Sep 17 00:00:00 2001 From: 446564 Date: Wed, 4 Feb 2026 08:32:35 -0800 Subject: [PATCH 058/421] format dart files formats all dart files using `dart format .` from the root project dir this makes the code style repeatable by new contributors and makes PR review easier --- lib/connector/meshcore_connector.dart | 39 +- lib/connector/meshcore_protocol.dart | 13 +- lib/helpers/cayenne_lpp.dart | 20 +- lib/helpers/link_handler.dart | 5 +- lib/helpers/reaction_helper.dart | 16 +- lib/helpers/smaz.dart | 21 +- lib/helpers/utf8_length_limiter.dart | 5 +- lib/main.dart | 30 +- lib/models/app_settings.dart | 29 +- lib/models/channel.dart | 12 +- lib/models/channel_message.dart | 39 +- lib/models/community.dart | 12 +- lib/models/contact.dart | 33 +- lib/models/contact_group.dart | 20 +- lib/models/message.dart | 9 +- lib/models/path_history.dart | 14 +- lib/models/radio_settings.dart | 60 +- lib/screens/app_debug_log_screen.dart | 38 +- lib/screens/app_settings_screen.dart | 172 ++++-- lib/screens/ble_debug_log_screen.dart | 62 +- lib/screens/channel_chat_screen.dart | 504 +++++++++-------- lib/screens/channel_message_path_screen.dart | 126 +++-- lib/screens/channels_screen.dart | 16 +- lib/screens/chat_screen.dart | 563 +++++++++++-------- lib/screens/contacts_screen.dart | 269 +++++---- lib/screens/device_screen.dart | 23 +- lib/screens/map_cache_screen.dart | 54 +- lib/screens/map_screen.dart | 158 +++--- lib/screens/repeater_cli_screen.dart | 86 ++- lib/screens/repeater_status_screen.dart | 85 ++- lib/screens/scanner_screen.dart | 41 +- lib/screens/settings_screen.dart | 7 +- lib/services/app_debug_log_service.dart | 12 +- lib/services/app_settings_service.dart | 18 +- lib/services/ble_debug_log_service.dart | 2 +- lib/services/map_tile_cache_service.dart | 55 +- lib/services/message_retry_service.dart | 197 +++++-- lib/services/notification_service.dart | 23 +- lib/services/path_history_service.dart | 46 +- lib/services/repeater_command_service.dart | 12 +- lib/services/storage_service.dart | 13 +- lib/storage/channel_message_store.dart | 20 +- lib/storage/channel_order_store.dart | 5 +- lib/storage/community_store.dart | 14 +- lib/storage/contact_store.dart | 12 +- lib/storage/message_store.dart | 29 +- lib/storage/prefs_manager.dart | 3 +- lib/storage/unread_store.dart | 10 +- lib/utils/app_logger.dart | 6 +- lib/widgets/battery_indicator.dart | 5 +- lib/widgets/debug_frame_viewer.dart | 26 +- lib/widgets/device_tile.dart | 16 +- lib/widgets/emoji_picker.dart | 222 +++++++- lib/widgets/empty_state.dart | 10 +- lib/widgets/gif_picker.dart | 60 +- lib/widgets/jump_to_bottom_button.dart | 5 +- lib/widgets/list_filter_widget.dart | 22 +- lib/widgets/path_management_dialog.dart | 94 +++- lib/widgets/path_selection_dialog.dart | 64 ++- lib/widgets/path_trace_dialog.dart | 114 ++-- lib/widgets/qr_code_display.dart | 10 +- lib/widgets/qr_scanner_widget.dart | 16 +- lib/widgets/repeater_login_dialog.dart | 326 ++++++----- lib/widgets/room_login_dialog.dart | 311 +++++----- lib/widgets/unread_badge.dart | 5 +- test/reaction_helper_test.dart | 366 +++++++++--- 66 files changed, 2882 insertions(+), 1848 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d7d7dc9..6d62f92 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1286,9 +1286,12 @@ class MeshCoreConnector extends ChangeNotifier { if (reactionInfo != null) { // Check if we've already processed this reaction _processedChannelReactions.putIfAbsent(channel.index, () => {}); - final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}'; + final reactionIdentifier = + '${reactionInfo.targetHash}_${reactionInfo.emoji}'; - if (_processedChannelReactions[channel.index]!.contains(reactionIdentifier)) { + if (_processedChannelReactions[channel.index]!.contains( + reactionIdentifier, + )) { // Already processed, don't process again return; } @@ -1504,7 +1507,9 @@ class MeshCoreConnector extends ChangeNotifier { // Skip fetching if already loaded and not forced if (_hasLoadedChannels && !force) { - debugPrint('[ChannelSync] Channels already loaded, skipping fetch (use force=true to reload)'); + debugPrint( + '[ChannelSync] Channels already loaded, skipping fetch (use force=true to reload)', + ); return; } @@ -2696,10 +2701,12 @@ class MeshCoreConnector extends ChangeNotifier { if (reactionInfo != null) { // Check if we've already processed this exact reaction _processedContactReactions.putIfAbsent(pubKeyHex, () => {}); - final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}'; + final reactionIdentifier = + '${reactionInfo.targetHash}_${reactionInfo.emoji}'; - final isDuplicate = - _processedContactReactions[pubKeyHex]!.contains(reactionIdentifier); + final isDuplicate = _processedContactReactions[pubKeyHex]!.contains( + reactionIdentifier, + ); if (!isDuplicate) { // New reaction - process it @@ -2734,20 +2741,22 @@ class MeshCoreConnector extends ChangeNotifier { for (int i = messages.length - 1; i >= 0; i--) { final msg = messages[i]; - + // For 1:1 chats: contact reacts to my outgoing messages only // For room servers: any message can be reacted to (multi-user) if (!isRoomServer && !msg.isOutgoing) continue; - + final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000; - + // For room servers, include sender name (resolve from fourByteRoomContactKey) // For 1:1 chats, sender is implicit (null) String? senderName; if (isRoomServer && !msg.isOutgoing) { // Resolve sender from the message's fourByteRoomContactKey final senderContact = _contacts.cast().firstWhere( - (c) => c != null && _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), + (c) => + c != null && + _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), orElse: () => null, ); senderName = senderContact?.name; @@ -2755,7 +2764,7 @@ class MeshCoreConnector extends ChangeNotifier { senderName = selfName; } // For 1:1, senderName stays null - + final msgHash = ReactionHelper.computeReactionHash( timestampSecs, senderName, @@ -2919,10 +2928,12 @@ class MeshCoreConnector extends ChangeNotifier { if (reactionInfo != null) { // Check if we've already processed this exact reaction _processedChannelReactions.putIfAbsent(channelIndex, () => {}); - final reactionIdentifier = '${reactionInfo.targetHash}_${reactionInfo.emoji}'; + final reactionIdentifier = + '${reactionInfo.targetHash}_${reactionInfo.emoji}'; - final isDuplicate = - _processedChannelReactions[channelIndex]!.contains(reactionIdentifier); + final isDuplicate = _processedChannelReactions[channelIndex]!.contains( + reactionIdentifier, + ); if (!isDuplicate) { // New reaction - process it diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index dfe787e..25359a8 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -113,7 +113,9 @@ class BufferWriter { final hexByte = hex.substring(i * 2, i * 2 + 2); final byte = int.tryParse(hexByte, radix: 16); if (byte == null) { - throw FormatException('Invalid hex characters at position $i: $hexByte'); + throw FormatException( + 'Invalid hex characters at position $i: $hexByte', + ); } result.add(byte); } @@ -219,8 +221,10 @@ const int maxFrameSize = 172; const int appProtocolVersion = 3; // Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE). const int maxTextPayloadBytes = 160; -const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin -const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin +const int _sendTextMsgOverheadBytes = + 1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin +const int _sendChannelTextMsgOverheadBytes = + 1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin int maxContactMessageBytes() { final byFrame = maxFrameSize - _sendTextMsgOverheadBytes; @@ -735,8 +739,7 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { //Build a trace request frame //[cmd][tag x4][auth x4][flag][payload] -Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) -{ +Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) { final writer = BufferWriter(); writer.writeByte(cmdSendTracePath); writer.writeUInt32LE(tag); diff --git a/lib/helpers/cayenne_lpp.dart b/lib/helpers/cayenne_lpp.dart index ad5aa8c..bf9b8e7 100644 --- a/lib/helpers/cayenne_lpp.dart +++ b/lib/helpers/cayenne_lpp.dart @@ -26,9 +26,11 @@ class CayenneLpp { static const int lppUnixTime = 133; // 4 bytes, unsigned static const int lppGyrometer = 134; // 2 bytes per axis, 0.01 °/s static const int lppColour = 135; // 1 byte per RGB Color - static const int lppGps = 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter + static const int lppGps = + 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter static const int lppSwitch = 142; // 1 byte, 0/1 - static const int lppPolyline = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas + static const int lppPolyline = + 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas final BufferWriter _writer = BufferWriter(); @@ -201,10 +203,10 @@ class CayenneLpp { break; } - final channelData = channels.putIfAbsent(channel, () => { - 'channel': channel, - 'values': {}, - }); + final channelData = channels.putIfAbsent( + channel, + () => {'channel': channel, 'values': {}}, + ); switch (type) { case lppGenericSensor: @@ -254,8 +256,8 @@ class CayenneLpp { } } - final List> channelsOut = channels.values.toList(); - channelsOut.sort((a, b) => a['channel'].compareTo(b['channel'])); - return channelsOut; + final List> channelsOut = channels.values.toList(); + channelsOut.sort((a, b) => a['channel'].compareTo(b['channel'])); + return channelsOut; } } diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart index fa8e5ff..7a032ef 100644 --- a/lib/helpers/link_handler.dart +++ b/lib/helpers/link_handler.dart @@ -26,10 +26,7 @@ class LinkHandler { ), child: SelectableText( url, - style: const TextStyle( - fontSize: 12, - fontFamily: 'monospace', - ), + style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), ), ), ], diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart index b75a9fd..88138d6 100644 --- a/lib/helpers/reaction_helper.dart +++ b/lib/helpers/reaction_helper.dart @@ -4,10 +4,7 @@ class ReactionInfo { final String targetHash; final String emoji; - ReactionInfo({ - required this.targetHash, - required this.emoji, - }); + ReactionInfo({required this.targetHash, required this.emoji}); } class ReactionHelper { @@ -42,7 +39,11 @@ class ReactionHelper { /// Compute a 4-char hex hash for a message reaction. /// Hash input: timestampSeconds + [senderName] + first 5 chars of text /// For 1:1 chats, senderName can be null (sender is implicit). - static String computeReactionHash(int timestampSeconds, String? senderName, String text) { + static String computeReactionHash( + int timestampSeconds, + String? senderName, + String text, + ) { final first5 = text.length >= 5 ? text.substring(0, 5) : text; final input = senderName != null ? '$timestampSeconds$senderName$first5' @@ -62,9 +63,6 @@ class ReactionHelper { final emoji = indexToEmoji(match.group(2)!); if (emoji == null) return null; - return ReactionInfo( - targetHash: match.group(1)!, - emoji: emoji, - ); + return ReactionInfo(targetHash: match.group(1)!, emoji: emoji); } } diff --git a/lib/helpers/smaz.dart b/lib/helpers/smaz.dart index 0e3e6c9..de34dde 100644 --- a/lib/helpers/smaz.dart +++ b/lib/helpers/smaz.dart @@ -262,8 +262,9 @@ class Smaz { ".com", ]; - static final List _rcbBytes = - _rcb.map((s) => Uint8List.fromList(ascii.encode(s))).toList(growable: false); + static final List _rcbBytes = _rcb + .map((s) => Uint8List.fromList(ascii.encode(s))) + .toList(growable: false); static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) { return entry.length > maxLen ? entry.length : maxLen; }); @@ -358,24 +359,32 @@ class Smaz { final code = input[index]; if (code == _verbatimSingle) { if (index + 1 >= input.length) { - throw const FormatException('Invalid SMAZ stream: truncated verbatim byte.'); + throw const FormatException( + 'Invalid SMAZ stream: truncated verbatim byte.', + ); } out.addByte(input[index + 1]); index += 2; } else if (code == _verbatimRun) { if (index + 1 >= input.length) { - throw const FormatException('Invalid SMAZ stream: truncated verbatim length.'); + throw const FormatException( + 'Invalid SMAZ stream: truncated verbatim length.', + ); } final len = input[index + 1] + 1; final end = index + 2 + len; if (end > input.length) { - throw const FormatException('Invalid SMAZ stream: truncated verbatim run.'); + throw const FormatException( + 'Invalid SMAZ stream: truncated verbatim run.', + ); } out.add(input.sublist(index + 2, end)); index = end; } else { if (code >= _rcbBytes.length) { - throw const FormatException('Invalid SMAZ stream: code out of range.'); + throw const FormatException( + 'Invalid SMAZ stream: code out of range.', + ); } out.add(_rcbBytes[code]); index += 1; diff --git a/lib/helpers/utf8_length_limiter.dart b/lib/helpers/utf8_length_limiter.dart index 843389e..c6acdd2 100644 --- a/lib/helpers/utf8_length_limiter.dart +++ b/lib/helpers/utf8_length_limiter.dart @@ -8,7 +8,10 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter { const Utf8LengthLimitingTextInputFormatter(this.maxBytes); @override - TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { if (maxBytes <= 0) return oldValue; final bytes = utf8.encode(newValue.text); if (bytes.length <= maxBytes) return newValue; diff --git a/lib/main.dart b/lib/main.dart index cd0887f..19c577f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -65,16 +65,18 @@ void main() async { await connector.loadAllChannelMessages(); await connector.loadUnreadState(); - runApp(MeshCoreApp( - connector: connector, - retryService: retryService, - pathHistoryService: pathHistoryService, - storage: storage, - appSettingsService: appSettingsService, - bleDebugLogService: bleDebugLogService, - appDebugLogService: appDebugLogService, - mapTileCacheService: mapTileCacheService, - )); + runApp( + MeshCoreApp( + connector: connector, + retryService: retryService, + pathHistoryService: pathHistoryService, + storage: storage, + appSettingsService: appSettingsService, + bleDebugLogService: bleDebugLogService, + appDebugLogService: appDebugLogService, + mapTileCacheService: mapTileCacheService, + ), + ); } class MeshCoreApp extends StatelessWidget { @@ -124,7 +126,9 @@ class MeshCoreApp extends StatelessWidget { GlobalCupertinoLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, - locale: _localeFromSetting(settingsService.settings.languageOverride), + locale: _localeFromSetting( + settingsService.settings.languageOverride, + ), theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, @@ -142,7 +146,9 @@ class MeshCoreApp extends StatelessWidget { behavior: SnackBarBehavior.floating, ), ), - themeMode: _themeModeFromSetting(settingsService.settings.themeMode), + themeMode: _themeModeFromSetting( + settingsService.settings.themeMode, + ), home: const ScannerScreen(), ); }, diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 50c31ba..3edb68f 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -76,13 +76,14 @@ class AppSettings { mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true, mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true, mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true, - mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0, + mapTimeFilterHours: + (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0, mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( - (key, value) => MapEntry(key.toString(), (value as num).toDouble()), - ), + (key, value) => MapEntry(key.toString(), (value as num).toDouble()), + ), mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10, mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15, notificationsEnabled: json['notifications_enabled'] as bool? ?? true, @@ -90,11 +91,13 @@ class AppSettings { notifyOnNewChannelMessage: json['notify_on_new_channel_message'] as bool? ?? true, notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true, - autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false, + autoRouteRotationEnabled: + json['auto_route_rotation_enabled'] as bool? ?? false, themeMode: json['theme_mode'] as String? ?? 'system', languageOverride: json['language_override'] as String?, appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false, - batteryChemistryByDeviceId: (json['battery_chemistry_by_device_id'] as Map?)?.map( + batteryChemistryByDeviceId: + (json['battery_chemistry_by_device_id'] as Map?)?.map( (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, @@ -132,8 +135,9 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, - mapCacheBounds: - mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, + mapCacheBounds: mapCacheBounds == _unset + ? this.mapCacheBounds + : mapCacheBounds as Map?, mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom, mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom, notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, @@ -141,12 +145,15 @@ class AppSettings { notifyOnNewChannelMessage: notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage, notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert, - autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, + autoRouteRotationEnabled: + autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, themeMode: themeMode ?? this.themeMode, - languageOverride: - languageOverride == _unset ? this.languageOverride : languageOverride as String?, + languageOverride: languageOverride == _unset + ? this.languageOverride + : languageOverride as String?, appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, - batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, + batteryChemistryByDeviceId: + batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, ); } } diff --git a/lib/models/channel.dart b/lib/models/channel.dart index e05a870..4e5e8c2 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -10,11 +10,7 @@ class Channel { final String name; final Uint8List psk; // 16 bytes - Channel({ - required this.index, - required this.name, - required this.psk, - }); + Channel({required this.index, required this.name, required this.psk}); String get pskHex => _bytesToHex(psk); @@ -39,11 +35,7 @@ class Channel { } static Channel empty(int index) { - return Channel( - index: index, - name: '', - psk: Uint8List(16), - ); + return Channel(index: index, name: '', psk: Uint8List(16)); } static Channel fromHex(int index, String name, String pskHex) { diff --git a/lib/models/channel_message.dart b/lib/models/channel_message.dart index 5aae28d..2418871 100644 --- a/lib/models/channel_message.dart +++ b/lib/models/channel_message.dart @@ -59,15 +59,18 @@ class ChannelMessage { this.replyToSenderName, this.replyToText, Map? reactions, - }) : messageId = messageId ?? '${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}', - reactions = reactions ?? {}, - pathBytes = pathBytes ?? Uint8List(0), - pathVariants = _mergePathVariants( - pathBytes ?? Uint8List(0), - pathVariants, - ); + }) : messageId = + messageId ?? + '${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}', + reactions = reactions ?? {}, + pathBytes = pathBytes ?? Uint8List(0), + pathVariants = _mergePathVariants( + pathBytes ?? Uint8List(0), + pathVariants, + ); - String? get senderKeyHex => senderKey != null ? pubKeyToHex(senderKey!) : null; + String? get senderKeyHex => + senderKey != null ? pubKeyToHex(senderKey!) : null; ChannelMessage copyWith({ ChannelMessageStatus? status, @@ -125,8 +128,10 @@ class ChannelMessage { final hasPathBytesFlag = (data[2] & 0x01) != 0; final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5; final hasValidTxtType = - cursor < data.length && (data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData); - if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) && canFitPath) { + cursor < data.length && + (data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData); + if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) && + canFitPath) { pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen)); cursor += pathLen; } @@ -162,7 +167,8 @@ class ChannelMessage { final potentialSender = text.substring(0, colonIndex); if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) { senderName = potentialSender; - final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ') + final offset = + (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ') ? colonIndex + 2 : colonIndex + 1; actualText = text.substring(offset); @@ -184,7 +190,11 @@ class ChannelMessage { ); } - static ChannelMessage outgoing(String text, String senderName, int channelIndex) { + static ChannelMessage outgoing( + String text, + String senderName, + int channelIndex, + ) { return ChannelMessage( senderKey: null, senderName: senderName, @@ -249,8 +259,5 @@ class ReplyInfo { final String mentionedNode; final String actualMessage; - ReplyInfo({ - required this.mentionedNode, - required this.actualMessage, - }); + ReplyInfo({required this.mentionedNode, required this.actualMessage}); } diff --git a/lib/models/community.dart b/lib/models/community.dart index 3bacf88..c829f3d 100644 --- a/lib/models/community.dart +++ b/lib/models/community.dart @@ -34,10 +34,7 @@ class Community { }) : hashtagChannels = hashtagChannels ?? []; /// Generate a new community with a random 32-byte secret - factory Community.create({ - required String id, - required String name, - }) { + factory Community.create({required String id, required String name}) { final random = Random.secure(); final secret = Uint8List(32); for (int i = 0; i < 32; i++) { @@ -84,7 +81,8 @@ class Community { name: json['name'] as String, secret: base64Decode(json['secret'] as String), createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int), - hashtagChannels: (json['hashtag_channels'] as List?) + hashtagChannels: + (json['hashtag_channels'] as List?) ?.map((e) => e as String) .toList() ?? [], @@ -234,9 +232,7 @@ class Community { @override bool operator ==(Object other) => identical(this, other) || - other is Community && - runtimeType == other.runtimeType && - id == other.id; + other is Community && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; diff --git a/lib/models/contact.dart b/lib/models/contact.dart index c9e40ab..a98580f 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -7,7 +7,8 @@ class Contact { final int type; final int pathLength; // -1 = flood, 0+ = direct hops (from device) final Uint8List path; // Path bytes from device - final int? pathOverride; // User's path override: -1 = force flood, null = auto + final int? + pathOverride; // User's path override: -1 = force flood, null = auto final Uint8List? pathOverrideBytes; // User's path override bytes final double? latitude; final double? longitude; @@ -78,8 +79,12 @@ class Contact { type: type ?? this.type, pathLength: pathLength ?? this.pathLength, path: path ?? this.path, - pathOverride: clearPathOverride ? null : (pathOverride ?? this.pathOverride), - pathOverrideBytes: clearPathOverride ? null : (pathOverrideBytes ?? this.pathOverrideBytes), + pathOverride: clearPathOverride + ? null + : (pathOverride ?? this.pathOverride), + pathOverrideBytes: clearPathOverride + ? null + : (pathOverrideBytes ?? this.pathOverrideBytes), latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, lastSeen: lastSeen ?? this.lastSeen, @@ -93,10 +98,14 @@ class Contact { final parts = []; final groupSize = pathHashSize; for (int i = 0; i < pathBytes.length; i += groupSize) { - final end = (i + groupSize) <= pathBytes.length ? (i + groupSize) : pathBytes.length; + final end = (i + groupSize) <= pathBytes.length + ? (i + groupSize) + : pathBytes.length; final chunk = pathBytes.sublist(i, end); parts.add( - chunk.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(), + chunk + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(), ); } return parts.join(','); @@ -110,32 +119,32 @@ class Contact { final pathBytes = _pathBytesForDisplay; Uint8List? traceBytes; - if(pathLength <= 0) { + if (pathLength <= 0) { traceBytes = Uint8List(1); traceBytes[0] = publicKey[0]; return traceBytes; } - if(type == advTypeRepeater || type == advTypeRoom) { + if (type == advTypeRepeater || type == advTypeRoom) { final len = (pathBytes.length + pathBytes.length + 1); traceBytes = Uint8List(len); traceBytes[pathBytes.length] = publicKey[0]; for (int i = 0; i < pathBytes.length; i++) { traceBytes[i] = pathBytes[i]; if (i < pathBytes.length) { - traceBytes[len-1-i] = pathBytes[i]; + traceBytes[len - 1 - i] = pathBytes[i]; } } } else { - if(pathBytes.length < 2) { + if (pathBytes.length < 2) { return pathBytes[0] == 0 ? null : pathBytes; } - final len = (pathBytes.length + pathBytes.length-1); + final len = (pathBytes.length + pathBytes.length - 1); traceBytes = Uint8List(len); for (int i = 0; i < pathBytes.length; i++) { traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length-1) { - traceBytes[len-1-i] = pathBytes[i]; + if (i < pathBytes.length - 1) { + traceBytes[len - 1 - i] = pathBytes[i]; } } } diff --git a/lib/models/contact_group.dart b/lib/models/contact_group.dart index 000a474..4e52585 100644 --- a/lib/models/contact_group.dart +++ b/lib/models/contact_group.dart @@ -2,15 +2,9 @@ class ContactGroup { final String name; final List memberKeys; - const ContactGroup({ - required this.name, - required this.memberKeys, - }); + const ContactGroup({required this.name, required this.memberKeys}); - ContactGroup copyWith({ - String? name, - List? memberKeys, - }) { + ContactGroup copyWith({String? name, List? memberKeys}) { return ContactGroup( name: name ?? this.name, memberKeys: memberKeys ?? List.from(this.memberKeys), @@ -18,16 +12,12 @@ class ContactGroup { } Map toJson() { - return { - 'name': name, - 'members': memberKeys, - }; + return {'name': name, 'members': memberKeys}; } factory ContactGroup.fromJson(Map json) { - final members = (json['members'] as List?) - ?.map((value) => value.toString()) - .toList() ?? + final members = + (json['members'] as List?)?.map((value) => value.toString()).toList() ?? []; return ContactGroup( name: json['name'] as String? ?? '', diff --git a/lib/models/message.dart b/lib/models/message.dart index bd397d7..4f42d96 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -43,9 +43,9 @@ class Message { Uint8List? pathBytes, Uint8List? fourByteRoomContactKey, Map? reactions, - }) : pathBytes = pathBytes ?? Uint8List(0), - fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0), - reactions = reactions ?? {}; + }) : pathBytes = pathBytes ?? Uint8List(0), + fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0), + reactions = reactions ?? {}; String get senderKeyHex => pubKeyToHex(senderKey); @@ -80,7 +80,8 @@ class Message { pathLength: pathLength ?? this.pathLength, pathBytes: pathBytes ?? this.pathBytes, reactions: reactions ?? this.reactions, - fourByteRoomContactKey: fourByteRoomContactKey ?? this.fourByteRoomContactKey, + fourByteRoomContactKey: + fourByteRoomContactKey ?? this.fourByteRoomContactKey, ); } diff --git a/lib/models/path_history.dart b/lib/models/path_history.dart index 1e2426a..5e3ea1f 100644 --- a/lib/models/path_history.dart +++ b/lib/models/path_history.dart @@ -38,7 +38,8 @@ class PathRecord { tripTimeMs: json['trip_time_ms'] as int, timestamp: DateTime.parse(json['timestamp'] as String), wasFloodDiscovery: json['was_flood'] as bool, - pathBytes: (json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [], + pathBytes: + (json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [], successCount: json['success_count'] as int? ?? 0, failureCount: json['failure_count'] as int? ?? 0, ); @@ -65,14 +66,15 @@ class ContactPathHistory { } Map toJson() { - return { - 'recent_paths': recentPaths.map((p) => p.toJson()).toList(), - }; + return {'recent_paths': recentPaths.map((p) => p.toJson()).toList()}; } factory ContactPathHistory.fromJson( - String contactPubKeyHex, Map json) { - final pathsList = (json['recent_paths'] as List?) + String contactPubKeyHex, + Map json, + ) { + final pathsList = + (json['recent_paths'] as List?) ?.map((p) => PathRecord.fromJson(p as Map)) .toList() ?? []; diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 9df96be..20b7771 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -61,44 +61,44 @@ class RadioSettings { // Preset configurations static RadioSettings get preset915MHz => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); + frequencyMHz: 915.0, + bandwidth: LoRaBandwidth.bw125, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ); static RadioSettings get preset868MHz => RadioSettings( - frequencyMHz: 868.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 14, - ); + frequencyMHz: 868.0, + bandwidth: LoRaBandwidth.bw125, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ); static RadioSettings get preset433MHz => RadioSettings( - frequencyMHz: 433.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); + frequencyMHz: 433.0, + bandwidth: LoRaBandwidth.bw125, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ); static RadioSettings get presetLongRange => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf12, - codingRate: LoRaCodingRate.cr4_8, - txPowerDbm: 20, - ); + frequencyMHz: 915.0, + bandwidth: LoRaBandwidth.bw125, + spreadingFactor: LoRaSpreadingFactor.sf12, + codingRate: LoRaCodingRate.cr4_8, + txPowerDbm: 20, + ); static RadioSettings get presetFastSpeed => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw500, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); + frequencyMHz: 915.0, + bandwidth: LoRaBandwidth.bw500, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ); int get frequencyHz => (frequencyMHz * 1000).round(); int get bandwidthHz => bandwidth.hz; diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index bdeccdb..e8a0aa4 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -26,8 +26,10 @@ class AppDebugLogScreen extends StatelessWidget { onPressed: hasEntries ? () async { final text = entries - .map((entry) => - '[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}') + .map( + (entry) => + '[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}', + ) .join('\n'); await Clipboard.setData(ClipboardData(text: text)); if (!context.mounted) return; @@ -61,11 +63,17 @@ class AppDebugLogScreen extends StatelessWidget { leading: _buildLevelIcon(entry.level), title: Text( '[${entry.tag}] ${entry.message}', - style: const TextStyle(fontSize: 12, fontFamily: 'monospace'), + style: const TextStyle( + fontSize: 12, + fontFamily: 'monospace', + ), ), subtitle: Text( entry.formattedTime, - style: TextStyle(fontSize: 10, color: Colors.grey[600]), + style: TextStyle( + fontSize: 10, + color: Colors.grey[600], + ), ), ); }, @@ -74,16 +82,26 @@ class AppDebugLogScreen extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.bug_report_outlined, size: 64, color: Colors.grey[400]), + Icon( + Icons.bug_report_outlined, + size: 64, + color: Colors.grey[400], + ), const SizedBox(height: 16), Text( context.l10n.debugLog_noEntries, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + style: TextStyle( + fontSize: 16, + color: Colors.grey[600], + ), ), const SizedBox(height: 8), Text( context.l10n.debugLog_enableInSettings, - style: TextStyle(fontSize: 12, color: Colors.grey[500]), + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), ), ], ), @@ -99,7 +117,11 @@ class AppDebugLogScreen extends StatelessWidget { case AppDebugLogLevel.info: return const Icon(Icons.info_outline, size: 18, color: Colors.blue); case AppDebugLogLevel.warning: - return const Icon(Icons.warning_amber_outlined, size: 18, color: Colors.orange); + return const Icon( + Icons.warning_amber_outlined, + size: 18, + color: Colors.orange, + ); case AppDebugLogLevel.error: return const Icon(Icons.error_outline, size: 18, color: Colors.red); } diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index ce61231..135babd 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -43,7 +43,10 @@ class AppSettingsScreen extends StatelessWidget { ); } - Widget _buildAppearanceCard(BuildContext context, AppSettingsService settingsService) { + Widget _buildAppearanceCard( + BuildContext context, + AppSettingsService settingsService, + ) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -58,7 +61,9 @@ class AppSettingsScreen extends StatelessWidget { ListTile( leading: const Icon(Icons.brightness_6_outlined), title: Text(context.l10n.appSettings_theme), - subtitle: Text(_themeModeLabel(context, settingsService.settings.themeMode)), + subtitle: Text( + _themeModeLabel(context, settingsService.settings.themeMode), + ), trailing: const Icon(Icons.chevron_right), onTap: () => _showThemeModeDialog(context, settingsService), ), @@ -66,7 +71,12 @@ class AppSettingsScreen extends StatelessWidget { ListTile( leading: const Icon(Icons.language_outlined), title: Text(context.l10n.appSettings_language), - subtitle: Text(_languageLabel(context, settingsService.settings.languageOverride)), + subtitle: Text( + _languageLabel( + context, + settingsService.settings.languageOverride, + ), + ), trailing: const Icon(Icons.chevron_right), onTap: () => _showLanguageDialog(context, settingsService), ), @@ -75,7 +85,10 @@ class AppSettingsScreen extends StatelessWidget { ); } - Widget _buildNotificationsCard(BuildContext context, AppSettingsService settingsService) { + Widget _buildNotificationsCard( + BuildContext context, + AppSettingsService settingsService, + ) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -90,17 +103,22 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: const Icon(Icons.notifications_outlined), title: Text(context.l10n.appSettings_enableNotifications), - subtitle: Text(context.l10n.appSettings_enableNotificationsSubtitle), + subtitle: Text( + context.l10n.appSettings_enableNotificationsSubtitle, + ), value: settingsService.settings.notificationsEnabled, onChanged: (value) async { if (value) { // Request permission when enabling - final granted = await NotificationService().requestPermissions(); + final granted = await NotificationService() + .requestPermissions(); if (!granted) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.appSettings_notificationPermissionDenied), + content: Text( + context.l10n.appSettings_notificationPermissionDenied, + ), duration: const Duration(seconds: 2), ), ); @@ -113,9 +131,11 @@ class AppSettingsScreen extends StatelessWidget { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(value - ? context.l10n.appSettings_notificationsEnabled - : context.l10n.appSettings_notificationsDisabled), + content: Text( + value + ? context.l10n.appSettings_notificationsEnabled + : context.l10n.appSettings_notificationsDisabled, + ), duration: const Duration(seconds: 2), ), ); @@ -126,18 +146,24 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: Icon( Icons.message_outlined, - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), title: Text( context.l10n.appSettings_messageNotifications, style: TextStyle( - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), ), subtitle: Text( context.l10n.appSettings_messageNotificationsSubtitle, style: TextStyle( - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), ), value: settingsService.settings.notifyOnNewMessage, @@ -151,18 +177,24 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: Icon( Icons.forum_outlined, - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), title: Text( context.l10n.appSettings_channelMessageNotifications, style: TextStyle( - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), ), subtitle: Text( context.l10n.appSettings_channelMessageNotificationsSubtitle, style: TextStyle( - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), ), value: settingsService.settings.notifyOnNewChannelMessage, @@ -176,18 +208,24 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: Icon( Icons.cell_tower, - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), title: Text( context.l10n.appSettings_advertisementNotifications, style: TextStyle( - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), ), subtitle: Text( context.l10n.appSettings_advertisementNotificationsSubtitle, style: TextStyle( - color: settingsService.settings.notificationsEnabled ? null : Colors.grey, + color: settingsService.settings.notificationsEnabled + ? null + : Colors.grey, ), ), value: settingsService.settings.notifyOnNewAdvert, @@ -202,7 +240,10 @@ class AppSettingsScreen extends StatelessWidget { ); } - Widget _buildMessagingCard(BuildContext context, AppSettingsService settingsService) { + Widget _buildMessagingCard( + BuildContext context, + AppSettingsService settingsService, + ) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -217,15 +258,19 @@ class AppSettingsScreen extends StatelessWidget { SwitchListTile( secondary: const Icon(Icons.refresh_outlined), title: Text(context.l10n.appSettings_clearPathOnMaxRetry), - subtitle: Text(context.l10n.appSettings_clearPathOnMaxRetrySubtitle), + subtitle: Text( + context.l10n.appSettings_clearPathOnMaxRetrySubtitle, + ), value: settingsService.settings.clearPathOnMaxRetry, onChanged: (value) { settingsService.setClearPathOnMaxRetry(value); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(value - ? context.l10n.appSettings_pathsWillBeCleared - : context.l10n.appSettings_pathsWillNotBeCleared), + content: Text( + value + ? context.l10n.appSettings_pathsWillBeCleared + : context.l10n.appSettings_pathsWillNotBeCleared, + ), duration: const Duration(seconds: 2), ), ); @@ -241,9 +286,11 @@ class AppSettingsScreen extends StatelessWidget { settingsService.setAutoRouteRotationEnabled(value); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(value - ? context.l10n.appSettings_autoRouteRotationEnabled - : context.l10n.appSettings_autoRouteRotationDisabled), + content: Text( + value + ? context.l10n.appSettings_autoRouteRotationEnabled + : context.l10n.appSettings_autoRouteRotationDisabled, + ), duration: const Duration(seconds: 2), ), ); @@ -254,7 +301,10 @@ class AppSettingsScreen extends StatelessWidget { ); } - Widget _buildMapSettingsCard(BuildContext context, AppSettingsService settingsService) { + Widget _buildMapSettingsCard( + BuildContext context, + AppSettingsService settingsService, + ) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -302,7 +352,9 @@ class AppSettingsScreen extends StatelessWidget { subtitle: Text( settingsService.settings.mapTimeFilterHours == 0 ? context.l10n.appSettings_timeFilterShowAll - : context.l10n.appSettings_timeFilterShowLast(settingsService.settings.mapTimeFilterHours.toInt()), + : context.l10n.appSettings_timeFilterShowLast( + settingsService.settings.mapTimeFilterHours.toInt(), + ), ), trailing: const Icon(Icons.chevron_right), onTap: () => _showTimeFilterDialog(context, settingsService), @@ -339,8 +391,9 @@ class AppSettingsScreen extends StatelessWidget { ) { final deviceId = connector.deviceId; final isConnected = connector.isConnected && deviceId != null; - final selection = - isConnected ? settingsService.batteryChemistryForDevice(deviceId) : 'nmc'; + final selection = isConnected + ? settingsService.batteryChemistryForDevice(deviceId) + : 'nmc'; return Card( child: Column( @@ -358,7 +411,9 @@ class AppSettingsScreen extends StatelessWidget { title: Text(context.l10n.appSettings_batteryChemistry), subtitle: Text( isConnected - ? context.l10n.appSettings_batteryChemistryPerDevice(connector.deviceDisplayName) + ? context.l10n.appSettings_batteryChemistryPerDevice( + connector.deviceDisplayName, + ) : context.l10n.appSettings_batteryChemistryConnectFirst, ), trailing: DropdownButton( @@ -366,7 +421,10 @@ class AppSettingsScreen extends StatelessWidget { onChanged: isConnected ? (value) { if (value != null) { - settingsService.setBatteryChemistryForDevice(deviceId, value); + settingsService.setBatteryChemistryForDevice( + deviceId, + value, + ); } } : null, @@ -391,7 +449,10 @@ class AppSettingsScreen extends StatelessWidget { ); } - void _showThemeModeDialog(BuildContext context, AppSettingsService settingsService) { + void _showThemeModeDialog( + BuildContext context, + AppSettingsService settingsService, + ) { showDialog( context: context, builder: (context) => AlertDialog( @@ -480,7 +541,10 @@ class AppSettingsScreen extends StatelessWidget { } } - void _showLanguageDialog(BuildContext context, AppSettingsService settingsService) { + void _showLanguageDialog( + BuildContext context, + AppSettingsService settingsService, + ) { showDialog( context: context, builder: (context) => AlertDialog( @@ -573,7 +637,10 @@ class AppSettingsScreen extends StatelessWidget { ); } - void _showTimeFilterDialog(BuildContext context, AppSettingsService settingsService) { + void _showTimeFilterDialog( + BuildContext context, + AppSettingsService settingsService, + ) { showDialog( context: context, builder: (context) => AlertDialog( @@ -593,33 +660,23 @@ class AppSettingsScreen extends StatelessWidget { const SizedBox(height: 16), ListTile( title: Text(context.l10n.appSettings_allTime), - leading: Radio( - value: 0, - ), + leading: Radio(value: 0), ), ListTile( title: Text(context.l10n.appSettings_lastHour), - leading: Radio( - value: 1, - ), + leading: Radio(value: 1), ), ListTile( title: Text(context.l10n.appSettings_last6Hours), - leading: Radio( - value: 6, - ), + leading: Radio(value: 6), ), ListTile( title: Text(context.l10n.appSettings_last24Hours), - leading: Radio( - value: 24, - ), + leading: Radio(value: 24), ), ListTile( title: Text(context.l10n.appSettings_lastWeek), - leading: Radio( - value: 168, - ), + leading: Radio(value: 168), ), ], ), @@ -634,7 +691,10 @@ class AppSettingsScreen extends StatelessWidget { ); } - Widget _buildDebugCard(BuildContext context, AppSettingsService settingsService) { + Widget _buildDebugCard( + BuildContext context, + AppSettingsService settingsService, + ) { return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -656,9 +716,11 @@ class AppSettingsScreen extends StatelessWidget { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(value - ? context.l10n.appSettings_appDebugLoggingEnabled - : context.l10n.appSettings_appDebugLoggingDisabled), + content: Text( + value + ? context.l10n.appSettings_appDebugLoggingEnabled + : context.l10n.appSettings_appDebugLoggingDisabled, + ), duration: const Duration(seconds: 2), ), ); diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 1931403..7675cae 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -24,7 +24,9 @@ class _BleDebugLogScreenState extends State { final entries = logService.entries.reversed.toList(); final rawEntries = logService.rawLogRxEntries.reversed.toList(); final showingFrames = _view == _BleLogView.frames; - final hasEntries = showingFrames ? entries.isNotEmpty : rawEntries.isNotEmpty; + final hasEntries = showingFrames + ? entries.isNotEmpty + : rawEntries.isNotEmpty; return Scaffold( appBar: AppBar( title: Text(context.l10n.debugLog_bleTitle), @@ -36,15 +38,23 @@ class _BleDebugLogScreenState extends State { ? () async { final text = showingFrames ? entries - .map((entry) => '${entry.description}\n${entry.hexPreview}\n') - .join('\n') + .map( + (entry) => + '${entry.description}\n${entry.hexPreview}\n', + ) + .join('\n') : rawEntries - .map((entry) => 'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n') - .join('\n'); + .map( + (entry) => + 'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n', + ) + .join('\n'); await Clipboard.setData(ClipboardData(text: text)); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.debugLog_bleCopied)), + SnackBar( + content: Text(context.l10n.debugLog_bleCopied), + ), ); } : null, @@ -68,8 +78,14 @@ class _BleDebugLogScreenState extends State { padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: SegmentedButton<_BleLogView>( segments: [ - ButtonSegment(value: _BleLogView.frames, label: Text(context.l10n.debugLog_frames)), - ButtonSegment(value: _BleLogView.rawLogRx, label: Text(context.l10n.debugLog_rawLogRx)), + ButtonSegment( + value: _BleLogView.frames, + label: Text(context.l10n.debugLog_frames), + ), + ButtonSegment( + value: _BleLogView.rawLogRx, + label: Text(context.l10n.debugLog_rawLogRx), + ), ], selected: {_view}, onSelectionChanged: (selection) { @@ -81,7 +97,9 @@ class _BleDebugLogScreenState extends State { Expanded( child: hasEntries ? ListView.separated( - itemCount: showingFrames ? entries.length : rawEntries.length, + itemCount: showingFrames + ? entries.length + : rawEntries.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { if (showingFrames) { @@ -94,7 +112,9 @@ class _BleDebugLogScreenState extends State { subtitle: Text('${entry.hexPreview}\n$time'), isThreeLine: true, leading: Icon( - entry.outgoing ? Icons.upload : Icons.download, + entry.outgoing + ? Icons.upload + : Icons.download, size: 18, ), ); @@ -131,9 +151,7 @@ class _BleDebugLogScreenState extends State { context: context, builder: (context) => AlertDialog( title: Text(info.title), - content: SingleChildScrollView( - child: SelectableText(info.rawHex), - ), + content: SingleChildScrollView(child: SelectableText(info.rawHex)), actions: [ TextButton( onPressed: () => Navigator.pop(context), @@ -195,11 +213,18 @@ class _BleDebugLogScreenState extends State { } final payload = raw.sublist(index); - final title = 'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer'; + final title = + 'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer'; final summary = _decodePayloadSummary(payloadType, payload); - final pathSummary = pathLen > 0 ? 'Path=${_bytesToHex(pathBytes)}' : 'Path=none'; + final pathSummary = pathLen > 0 + ? 'Path=${_bytesToHex(pathBytes)}' + : 'Path=none'; final detail = '$summary • $pathSummary • len=${raw.length}'; - return _RawPacketInfo(title: title, summary: detail, rawHex: _bytesToHex(raw)); + return _RawPacketInfo( + title: title, + summary: detail, + rawHex: _bytesToHex(raw), + ); } String _decodePayloadSummary(int payloadType, Uint8List payload) { @@ -245,7 +270,10 @@ class _BleDebugLogScreenState extends State { return 'ADVERT (short)'; } var offset = 0; - final pubKey = _bytesToHex(payload.sublist(offset, offset + 32), spaced: false); + final pubKey = _bytesToHex( + payload.sublist(offset, offset + 32), + spaced: false, + ); offset += 32; final timestamp = readUint32LE(payload, offset); offset += 4; diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 083a60b..a1139a1 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -27,10 +27,7 @@ import 'map_screen.dart'; class ChannelChatScreen extends StatefulWidget { final Channel channel; - const ChannelChatScreen({ - super.key, - required this.channel, - }); + const ChannelChatScreen({super.key, required this.channel}); @override State createState() => _ChannelChatScreenState(); @@ -135,15 +132,19 @@ class _ChannelChatScreenState extends State { children: [ Text( widget.channel.name.isEmpty - ? context.l10n.channels_channelIndex(widget.channel.index) + ? context.l10n.channels_channelIndex( + widget.channel.index, + ) : widget.channel.name, style: const TextStyle(fontSize: 16), ), Consumer( builder: (context, connector, _) { - final unreadCount = - connector.getUnreadCountForChannelIndex(widget.channel.index); - final privacy = widget.channel.isPublicChannel ? context.l10n.channels_public : context.l10n.channels_private; + final unreadCount = connector + .getUnreadCountForChannelIndex(widget.channel.index); + final privacy = widget.channel.isPublicChannel + ? context.l10n.channels_public + : context.l10n.channels_private; return Text( '$privacy • ${context.l10n.chat_unread(unreadCount)}', overflow: TextOverflow.ellipsis, @@ -202,7 +203,8 @@ class _ChannelChatScreenState extends State { // Reverse messages so newest appear at bottom with reverse: true final reversedMessages = messages.reversed.toList(); - final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); + final itemCount = + reversedMessages.length + (_isLoadingOlder ? 1 : 0); // Auto-scroll to bottom if user is already at bottom WidgetsBinding.instance.addPostFrameCallback((_) { @@ -225,7 +227,9 @@ class _ChannelChatScreenState extends State { child: SizedBox( width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2), + child: CircularProgressIndicator( + strokeWidth: 2, + ), ), ), ); @@ -241,9 +245,7 @@ class _ChannelChatScreenState extends State { ); }, ), - JumpToBottomButton( - scrollController: _scrollController, - ), + JumpToBottomButton(scrollController: _scrollController), ], ); }, @@ -262,15 +264,21 @@ class _ChannelChatScreenState extends State { final poi = _parsePoiMessage(message.text); final displayPath = message.pathBytes.isNotEmpty ? message.pathBytes - : (message.pathVariants.isNotEmpty ? message.pathVariants.first : Uint8List(0)); + : (message.pathVariants.isNotEmpty + ? message.pathVariants.first + : Uint8List(0)); return Padding( padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: Column( - crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end : CrossAxisAlignment.start, + crossAxisAlignment: isOutgoing + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ Row( - mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOutgoing) ...[ @@ -282,128 +290,160 @@ class _ChannelChatScreenState extends State { onTap: () => _showMessagePathInfo(message), onLongPress: () => _showMessageActions(message), child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, - ), - decoration: BoxDecoration( - color: isOutgoing - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - Padding( - padding: gifId != null - ? const EdgeInsets.only(left: 8, top: 4, bottom: 4) - : EdgeInsets.zero, - child: Text( - message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, ), - ), - ), - if (gifId == null) const SizedBox(height: 4), - ], - if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message), - const SizedBox(height: 8), - ], - if (poi != null) - _buildPoiMessage(context, poi, isOutgoing) - else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context).colorScheme.onPrimaryContainer.withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - ), - ) - else - Linkify( - text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => LinkHandler.handleLinkTap(context, link.url), - ), - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - 'via ${_formatPathPrefixes(displayPath)}', - style: TextStyle(fontSize: 11, color: Colors.grey[600]), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only(left: 8, right: 8, bottom: 4) - : EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.65, + ), + decoration: BoxDecoration( + color: isOutgoing + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + message.senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), ), ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon(Icons.repeat, size: 12, color: Colors.grey[600]), - const SizedBox(width: 2), - Text( - '${message.repeatCount}', - style: TextStyle(fontSize: 11, color: Colors.grey[600]), + if (gifId == null) const SizedBox(height: 4), + ], + if (message.replyToMessageId != null) ...[ + _buildReplyPreview(message), + const SizedBox(height: 8), + ], + if (poi != null) + _buildPoiMessage(context, poi, isOutgoing) + else if (gifId != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), ), - ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == ChannelMessageStatus.pending + ) + else + Linkify( + text: message.text, + style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + if (displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12, + color: Colors.grey[600], + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == + ChannelMessageStatus.pending ? Icons.schedule : Icons.error_outline, - size: 14, - color: message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], - ), + size: 14, + color: + message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], + ], + ), + ), + ], ), - ], + ), ), ), - ), - ), ], ), if (message.reactions.isNotEmpty) ...[ @@ -444,7 +484,10 @@ class _ChannelChatScreenState extends State { children: [ Icon(Icons.location_on_outlined, size: 14, color: previewTextColor), const SizedBox(width: 4), - Text(context.l10n.chat_location, style: TextStyle(fontSize: 12, color: previewTextColor)), + Text( + context.l10n.chat_location, + style: TextStyle(fontSize: 12, color: previewTextColor), + ), ], ); } else { @@ -468,10 +511,7 @@ class _ChannelChatScreenState extends State { color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(8), border: Border( - left: BorderSide( - color: colorScheme.primary, - width: 3, - ), + left: BorderSide(color: colorScheme.primary, width: 3), ), ), child: Column( @@ -509,17 +549,16 @@ class _ChannelChatScreenState extends State { color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(12), border: Border.all( - color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3), + color: Theme.of( + context, + ).colorScheme.outline.withValues(alpha: 0.3), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - emoji, - style: const TextStyle(fontSize: 16), - ), + Text(emoji, style: const TextStyle(fontSize: 16)), if (count > 1) ...[ const SizedBox(width: 4), Text( @@ -546,7 +585,9 @@ class _ChannelChatScreenState extends State { _PoiInfo? _parsePoiMessage(String text) { final trimmed = text.trim(); - final match = RegExp(r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|').firstMatch(trimmed); + final match = RegExp( + r'm:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|', + ).firstMatch(trimmed); if (match == null) return null; final lat = double.tryParse(match.group(1) ?? ''); final lon = double.tryParse(match.group(2) ?? ''); @@ -557,10 +598,13 @@ class _ChannelChatScreenState extends State { Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) { final colorScheme = Theme.of(context).colorScheme; - final textColor = - isOutgoing ? colorScheme.onPrimaryContainer : colorScheme.onSurface; + final textColor = isOutgoing + ? colorScheme.onPrimaryContainer + : colorScheme.onSurface; final metaColor = textColor.withValues(alpha: 0.7); - final channelColor = widget.channel.isPublicChannel ? Colors.orange : Colors.blue; + final channelColor = widget.channel.isPublicChannel + ? Colors.orange + : Colors.blue; return Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -588,18 +632,12 @@ class _ChannelChatScreenState extends State { children: [ Text( context.l10n.chat_poiShared, - style: TextStyle( - color: textColor, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: textColor, fontWeight: FontWeight.w600), ), if (poi.label.isNotEmpty) Text( poi.label, - style: TextStyle( - color: metaColor, - fontSize: 12, - ), + style: TextStyle(color: metaColor, fontSize: 12), ), ], ), @@ -676,10 +714,7 @@ class _ChannelChatScreenState extends State { decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondaryContainer, border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), + bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1), ), ), child: Row( @@ -708,7 +743,9 @@ class _ChannelChatScreenState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 11, - color: Theme.of(context).colorScheme.onSecondaryContainer.withValues(alpha: 0.7), + color: Theme.of( + context, + ).colorScheme.onSecondaryContainer.withValues(alpha: 0.7), ), ), ], @@ -746,73 +783,76 @@ class _ChannelChatScreenState extends State { ], ), child: Row( - children: [ - IconButton( - icon: const Icon(Icons.gif_box), - onPressed: () => _showGifPicker(context), - tooltip: context.l10n.chat_sendGif, - ), - Expanded( - child: ValueListenableBuilder( - valueListenable: _textController, - builder: (context, value, child) { - final gifId = _parseGifId(value.text); - if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, - fallbackTextColor: - Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), - maxSize: 160, + children: [ + IconButton( + icon: const Icon(Icons.gif_box), + onPressed: () => _showGifPicker(context), + tooltip: context.l10n.chat_sendGif, + ), + Expanded( + child: ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, child) { + final gifId = _parseGifId(value.text); + if (gifId != null) { + return Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + fallbackTextColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), + ), ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => _textController.clear(), + ), + ], + ); + } + + return TextField( + controller: _textController, + focusNode: _textFieldFocusNode, + inputFormatters: [ + Utf8LengthLimitingTextInputFormatter(maxBytes), + ], + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + hintText: context.l10n.chat_typeMessage, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], - ); - } - - return TextField( - controller: _textController, - focusNode: _textFieldFocusNode, - inputFormatters: [ - Utf8LengthLimitingTextInputFormatter(maxBytes), - ], - textCapitalization: TextCapitalization.sentences, - decoration: InputDecoration( - hintText: context.l10n.chat_typeMessage, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - ), - maxLines: null, - textInputAction: TextInputAction.send, - onSubmitted: (_) => _sendMessage(), - ); - }, - ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.send), - onPressed: _sendMessage, - color: Theme.of(context).colorScheme.primary, - ), - ], + maxLines: null, + textInputAction: TextInputAction.send, + onSubmitted: (_) => _sendMessage(), + ); + }, + ), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.send), + onPressed: _sendMessage, + color: Theme.of(context).colorScheme.primary, + ), + ], ), ), ], @@ -932,24 +972,28 @@ class _ChannelChatScreenState extends State { final emojiIndex = ReactionHelper.emojiToIndex(emoji); if (emojiIndex == null) return; // Unknown emoji, skip final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000; - final hash = ReactionHelper.computeReactionHash(timestampSecs, message.senderName, message.text); + final hash = ReactionHelper.computeReactionHash( + timestampSecs, + message.senderName, + message.text, + ); final reactionText = 'r:$hash:$emojiIndex'; connector.sendChannelMessage(widget.channel, reactionText); } void _copyMessageText(String text) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageCopied)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); } Future _deleteMessage(ChannelMessage message) async { await context.read().deleteChannelMessage(message); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageDeleted)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); } String _formatPathPrefixes(Uint8List pathBytes) { @@ -964,9 +1008,5 @@ class _PoiInfo { final double lon; final String label; - const _PoiInfo({ - required this.lat, - required this.lon, - required this.label, - }); + const _PoiInfo({required this.lat, required this.lon, required this.label}); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 93d510e..970c152 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -17,17 +17,17 @@ import '../models/contact.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; - const ChannelMessagePathScreen({ - super.key, - required this.message, - }); + const ChannelMessagePathScreen({super.key, required this.message}); @override Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { final l10n = context.l10n; - final primaryPath = _selectPrimaryPath(message.pathBytes, message.pathVariants); + final primaryPath = _selectPrimaryPath( + message.pathBytes, + message.pathVariants, + ); final hops = _buildPathHops(primaryPath, connector.contacts, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( @@ -88,10 +88,7 @@ class ChannelMessagePathScreen extends StatelessWidget { ); } - Widget _buildSummaryCard( - BuildContext context, { - String? observedLabel, - }) { + Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) { final l10n = context.l10n; return Card( child: Padding( @@ -105,21 +102,28 @@ class ChannelMessagePathScreen extends StatelessWidget { ), const SizedBox(height: 8), _buildDetailRow(l10n.channelPath_senderLabel, message.senderName), - _buildDetailRow(l10n.channelPath_timeLabel, _formatTime(message.timestamp, l10n)), + _buildDetailRow( + l10n.channelPath_timeLabel, + _formatTime(message.timestamp, l10n), + ), if (message.repeatCount > 0) - _buildDetailRow(l10n.channelPath_repeatsLabel, message.repeatCount.toString()), - _buildDetailRow(l10n.channelPath_pathLabelTitle, _formatPathLabel(message.pathLength, l10n)), - if (observedLabel != null) _buildDetailRow(l10n.channelPath_observedLabel, observedLabel), + _buildDetailRow( + l10n.channelPath_repeatsLabel, + message.repeatCount.toString(), + ), + _buildDetailRow( + l10n.channelPath_pathLabelTitle, + _formatPathLabel(message.pathLength, l10n), + ), + if (observedLabel != null) + _buildDetailRow(l10n.channelPath_observedLabel, observedLabel), ], ), ), ); } - Widget _buildPathVariants( - BuildContext context, - List variants, - ) { + Widget _buildPathVariants(BuildContext context, List variants) { final l10n = context.l10n; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -163,7 +167,7 @@ class ChannelMessagePathScreen extends StatelessWidget { subtitle: Text( hop.hasLocation ? '${hop.position!.latitude.toStringAsFixed(5)}, ' - '${hop.position!.longitude.toStringAsFixed(5)}' + '${hop.position!.longitude.toStringAsFixed(5)}' : l10n.channelPath_noLocationData, ), ), @@ -239,7 +243,6 @@ class ChannelMessagePathScreen extends StatelessWidget { ), ); } - } class ChannelMessagePathMapScreen extends StatefulWidget { @@ -257,7 +260,8 @@ class ChannelMessagePathMapScreen extends StatefulWidget { _ChannelMessagePathMapScreenState(); } -class _ChannelMessagePathMapScreenState extends State { +class _ChannelMessagePathMapScreenState + extends State { Uint8List? _selectedPath; @override @@ -270,8 +274,10 @@ class _ChannelMessagePathMapScreenState extends State( builder: (context, connector, _) { final tileCache = context.read(); - final primaryPath = - _selectPrimaryPath(widget.message.pathBytes, widget.message.pathVariants); - final observedPaths = - _buildObservedPaths(primaryPath, widget.message.pathVariants); + final primaryPath = _selectPrimaryPath( + widget.message.pathBytes, + widget.message.pathVariants, + ); + final observedPaths = _buildObservedPaths( + primaryPath, + widget.message.pathVariants, + ); final selectedPath = _resolveSelectedPath( _selectedPath, observedPaths, primaryPath, ); final selectedIndex = _indexForPath(selectedPath, observedPaths); - final hops = _buildPathHops(selectedPath, connector.contacts, context.l10n); + final hops = _buildPathHops( + selectedPath, + connector.contacts, + context.l10n, + ); final points = hops .where((hop) => hop.hasLocation) .map((hop) => hop.position!) @@ -306,16 +320,17 @@ class _ChannelMessagePathMapScreenState extends State[]; - final initialCenter = - points.isNotEmpty ? points.first : const LatLng(0, 0); + final initialCenter = points.isNotEmpty + ? points.first + : const LatLng(0, 0); final initialZoom = points.isNotEmpty ? 13.0 : 2.0; - final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null; + final bounds = points.length > 1 + ? LatLngBounds.fromPoints(points) + : null; final mapKey = ValueKey(_formatPathPrefixes(selectedPath)); return Scaffold( - appBar: AppBar( - title: Text(context.l10n.channelPath_mapTitle), - ), + appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)), body: SafeArea( top: false, child: Stack( @@ -343,30 +358,28 @@ class _ChannelMessagePathMapScreenState extends State 1) - _buildPathSelector( - context, - observedPaths, - selectedIndex, - (index) { - setState(() { - _selectedPath = observedPaths[index].pathBytes; - }); - }, - ), + _buildPathSelector(context, observedPaths, selectedIndex, ( + index, + ) { + setState(() { + _selectedPath = observedPaths[index].pathBytes; + }); + }), if (points.isEmpty) Center( child: Card( color: Colors.white.withValues(alpha: 0.9), child: Padding( padding: EdgeInsets.all(12), - child: Text(context.l10n.channelPath_noRepeaterLocations), + child: Text( + context.l10n.channelPath_noRepeaterLocations, + ), ), ), ), @@ -525,7 +538,7 @@ class _ChannelMessagePathMapScreenState extends State _buildPathHops( @@ -597,10 +607,12 @@ List<_PathHop> _buildPathHops( Contact? _matchContactForPrefix(List contacts, int prefix) { final matches = contacts - .where((contact) => - (contact.type == advTypeRepeater || contact.type == advTypeRoom) && - contact.publicKey.isNotEmpty && - contact.publicKey[0] == prefix) + .where( + (contact) => + (contact.type == advTypeRepeater || contact.type == advTypeRoom) && + contact.publicKey.isNotEmpty && + contact.publicKey[0] == prefix, + ) .toList(); if (matches.isEmpty) return null; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 37d56bb..6b8b92d 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -154,7 +154,9 @@ class _ChannelsScreenState extends State ), onTap: () => Navigator.push( context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), + MaterialPageRoute( + builder: (context) => const SettingsScreen(), + ), ), ), ], @@ -951,7 +953,9 @@ class _ChannelsScreenState extends State dialogContext.l10n.community_communityHashtag, ), subtitle: Text( - dialogContext.l10n.community_communityHashtagDesc, + dialogContext + .l10n + .community_communityHashtagDesc, ), dense: true, ), @@ -1047,7 +1051,7 @@ class _ChannelsScreenState extends State hashtag = hashtag.substring(1); } final String channelName; - + final Uint8List psk; if (isRegularHashtag) { channelName = '#$hashtag'; @@ -1069,8 +1073,10 @@ class _ChannelsScreenState extends State ); return; } - channelName = '${selectedCommunity!.name} #$hashtag'; - psk = selectedCommunity!.deriveCommunityHashtagPsk(hashtag); + channelName = + '${selectedCommunity!.name} #$hashtag'; + psk = selectedCommunity! + .deriveCommunityHashtagPsk(hashtag); // Track in community's hashtag list await _communityStore.addHashtagChannel( selectedCommunity!.id, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index cf34381..00cea59 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -52,7 +52,9 @@ class _ChatScreenState extends State { _scrollController.onScrollNearTop = _loadOlderMessages; SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - context.read().setActiveContact(widget.contact.publicKeyHex); + context.read().setActiveContact( + widget.contact.publicKeyHex, + ); }); } @@ -91,12 +93,15 @@ class _ChatScreenState extends State { title: Consumer2( builder: (context, pathService, connector, _) { final contact = _resolveContact(connector); - final unreadCount = connector.getUnreadCountForContactKey(widget.contact.publicKeyHex); + final unreadCount = connector.getUnreadCountForContactKey( + widget.contact.publicKeyHex, + ); final unreadLabel = context.l10n.chat_unread(unreadCount); final pathLabel = _currentPathLabel(contact); // Show path details if we have path data (from device or override) - final hasPathData = contact.path.isNotEmpty || contact.pathOverrideBytes != null; + final hasPathData = + contact.path.isNotEmpty || contact.pathOverrideBytes != null; final effectivePath = contact.pathOverrideBytes ?? contact.path; return Column( @@ -106,7 +111,9 @@ class _ChatScreenState extends State { Text(contact.name), GestureDetector( behavior: HitTestBehavior.opaque, - onTap: hasPathData ? () => _showFullPathDialog(context, effectivePath) : null, + onTap: hasPathData + ? () => _showFullPathDialog(context, effectivePath) + : null, child: Text( '$pathLabel • $unreadLabel', overflow: TextOverflow.ellipsis, @@ -144,12 +151,20 @@ class _ChatScreenState extends State { value: 'auto', child: Row( children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( context.l10n.chat_autoUseSavedPath, style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -159,12 +174,20 @@ class _ChatScreenState extends State { value: 'flood', child: Row( children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( context.l10n.chat_forceFloodMode, style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -196,9 +219,7 @@ class _ChatScreenState extends State { messages.isEmpty ? _buildEmptyState() : _buildMessageList(messages, connector), - JumpToBottomButton( - scrollController: _scrollController, - ), + JumpToBottomButton(scrollController: _scrollController), ], ), ), @@ -231,7 +252,10 @@ class _ChatScreenState extends State { ); } - Widget _buildMessageList(List messages, MeshCoreConnector connector) { + Widget _buildMessageList( + List messages, + MeshCoreConnector connector, + ) { // Reverse messages so newest appear at bottom with reverse: true final reversedMessages = messages.reversed.toList(); final itemCount = reversedMessages.length + (_isLoadingOlder ? 1 : 0); @@ -267,14 +291,21 @@ class _ChatScreenState extends State { if (widget.contact.type == advTypeRoom) { contact = _resolveContactFrom4Bytes( connector, - message.fourByteRoomContactKey.isEmpty ? Uint8List.fromList([0, 0, 0, 0]) : message.fourByteRoomContactKey, + message.fourByteRoomContactKey.isEmpty + ? Uint8List.fromList([0, 0, 0, 0]) + : message.fourByteRoomContactKey, ); - fourByteHex = message.fourByteRoomContactKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join().toUpperCase(); + fourByteHex = message.fourByteRoomContactKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join() + .toUpperCase(); } return _MessageBubble( message: message, - senderName: widget.contact.type == advTypeRoom ? "${contact.name} [$fourByteHex]" : contact.name, + senderName: widget.contact.type == advTypeRoom + ? "${contact.name} [$fourByteHex]" + : contact.name, isRoomServer: widget.contact.type == advTypeRoom, onTap: () => _openMessagePath(message, contact), onLongPress: () => _showMessageActions(message, contact), @@ -290,9 +321,7 @@ class _ChatScreenState extends State { padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: colorScheme.surface, - border: Border( - top: BorderSide(color: Theme.of(context).dividerColor), - ), + border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), ), child: SafeArea( child: Row( @@ -314,10 +343,12 @@ class _ChatScreenState extends State { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: colorScheme.surfaceContainerHighest, - fallbackTextColor: - colorScheme.onSurface.withValues(alpha: 0.6), + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + colorScheme.surfaceContainerHighest, + fallbackTextColor: colorScheme.onSurface + .withValues(alpha: 0.6), maxSize: 160, ), ), @@ -341,7 +372,10 @@ class _ChatScreenState extends State { decoration: InputDecoration( hintText: context.l10n.chat_typeMessage, border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), ), textInputAction: TextInputAction.send, onSubmitted: (_) => _sendMessage(connector), @@ -390,14 +424,10 @@ class _ChatScreenState extends State { return; } - connector.sendMessage( - widget.contact, - text, - ); + connector.sendMessage(widget.contact, text); _textController.clear(); } - void _showPathHistory(BuildContext context) { final connector = Provider.of(context, listen: false); @@ -422,13 +452,19 @@ class _ChatScreenState extends State { if (paths.isNotEmpty) ...[ Text( context.l10n.chat_recentAckPaths, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), if (paths.length >= 100) ...[ const SizedBox(height: 8), Container( width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), decoration: BoxDecoration( color: Colors.amber[100], borderRadius: BorderRadius.circular(8), @@ -447,7 +483,9 @@ class _ChatScreenState extends State { dense: true, leading: CircleAvatar( radius: 16, - backgroundColor: path.wasFloodDiscovery ? Colors.blue : Colors.green, + backgroundColor: path.wasFloodDiscovery + ? Colors.blue + : Colors.green, child: Text( '${path.hopCount}', style: const TextStyle(fontSize: 12), @@ -475,23 +513,36 @@ class _ChatScreenState extends State { }, ), path.wasFloodDiscovery - ? const Icon(Icons.waves, size: 16, color: Colors.grey) - : const Icon(Icons.route, size: 16, color: Colors.grey), + ? const Icon( + Icons.waves, + size: 16, + color: Colors.grey, + ) + : const Icon( + Icons.route, + size: 16, + color: Colors.grey, + ), ], ), - onLongPress: () => _showFullPathDialog(context, path.pathBytes), + onLongPress: () => + _showFullPathDialog(context, path.pathBytes), onTap: () async { if (path.pathBytes.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.chat_pathDetailsNotAvailable), + content: Text( + context.l10n.chat_pathDetailsNotAvailable, + ), duration: const Duration(seconds: 2), ), ); return; } - final pathBytes = Uint8List.fromList(path.pathBytes); + final pathBytes = Uint8List.fromList( + path.pathBytes, + ); final pathLength = path.pathBytes.length; // Set the path override to persist user's choice @@ -521,7 +572,10 @@ class _ChatScreenState extends State { const SizedBox(height: 8), Text( context.l10n.chat_pathActions, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), const SizedBox(height: 8), ListTile( @@ -531,8 +585,14 @@ class _ChatScreenState extends State { backgroundColor: Colors.purple, child: Icon(Icons.edit_road, size: 16), ), - title: Text(context.l10n.chat_setCustomPath, style: const TextStyle(fontSize: 14)), - subtitle: Text(context.l10n.chat_setCustomPathSubtitle, style: const TextStyle(fontSize: 11)), + title: Text( + context.l10n.chat_setCustomPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_setCustomPathSubtitle, + style: const TextStyle(fontSize: 11), + ), onTap: () { Navigator.pop(context); _showCustomPathDialog(context); @@ -545,8 +605,14 @@ class _ChatScreenState extends State { backgroundColor: Colors.orange, child: Icon(Icons.clear_all, size: 16), ), - title: Text(context.l10n.chat_clearPath, style: const TextStyle(fontSize: 14)), - subtitle: Text(context.l10n.chat_clearPathSubtitle, style: const TextStyle(fontSize: 11)), + title: Text( + context.l10n.chat_clearPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_clearPathSubtitle, + style: const TextStyle(fontSize: 11), + ), onTap: () async { await connector.clearContactPath(widget.contact); if (!context.mounted) return; @@ -566,10 +632,19 @@ class _ChatScreenState extends State { backgroundColor: Colors.blue, child: Icon(Icons.waves, size: 16), ), - title: Text(context.l10n.chat_forceFloodMode, style: const TextStyle(fontSize: 14)), - subtitle: Text(context.l10n.chat_floodModeSubtitle, style: const TextStyle(fontSize: 11)), + title: Text( + context.l10n.chat_forceFloodMode, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_floodModeSubtitle, + style: const TextStyle(fontSize: 11), + ), onTap: () async { - await connector.setPathOverride(widget.contact, pathLen: -1); + await connector.setPathOverride( + widget.contact, + pathLen: -1, + ); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -598,7 +673,8 @@ class _ChatScreenState extends State { String _formatRelativeTime(DateTime time) { final diff = DateTime.now().difference(time); if (diff.inSeconds < 60) return context.l10n.time_justNow; - if (diff.inMinutes < 60) return context.l10n.time_minutesAgo(diff.inMinutes); + if (diff.inMinutes < 60) + return context.l10n.time_minutesAgo(diff.inMinutes); if (diff.inHours < 24) return context.l10n.time_hoursAgo(diff.inHours); return context.l10n.time_daysAgo(diff.inDays); } @@ -640,7 +716,10 @@ class _ChatScreenState extends State { ); } - Contact _resolveContactFrom4Bytes(MeshCoreConnector connector, Uint8List key4Bytes) { + Contact _resolveContactFrom4Bytes( + MeshCoreConnector connector, + Uint8List key4Bytes, + ) { return connector.contacts.firstWhere( (c) => listEquals(c.publicKey.sublist(0, 4), key4Bytes.sublist(0, 4)), orElse: () => widget.contact, @@ -674,12 +753,12 @@ class _ChatScreenState extends State { final status = !connector.isConnected ? context.l10n.chat_pathSavedLocally - : (verified ? context.l10n.chat_pathDeviceConfirmed : context.l10n.chat_pathDeviceNotConfirmed); + : (verified + ? context.l10n.chat_pathDeviceConfirmed + : context.l10n.chat_pathDeviceNotConfirmed); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text( - context.l10n.chat_pathSetHops(hopCount, status), - ), + content: Text(context.l10n.chat_pathSetHops(hopCount, status)), duration: const Duration(seconds: 3), ), ); @@ -694,7 +773,9 @@ class _ChatScreenState extends State { builder: (context) => Consumer( builder: (context, connector, _) { final contact = _resolveContact(connector); - final smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex); + final smazEnabled = connector.isContactSmazEnabled( + contact.publicKeyHex, + ); return AlertDialog( title: Text(contact.name), @@ -710,7 +791,10 @@ class _ChatScreenState extends State { context.l10n.chat_location, '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}', ), - _buildInfoRow(context.l10n.chat_publicKey, '${contact.publicKeyHex.substring(0, 16)}...'), + _buildInfoRow( + context.l10n.chat_publicKey, + '${contact.publicKeyHex.substring(0, 16)}...', + ), const Divider(), SwitchListTile( contentPadding: EdgeInsets.zero, @@ -718,7 +802,10 @@ class _ChatScreenState extends State { subtitle: Text(context.l10n.chat_compressOutgoingMessages), value: smazEnabled, onChanged: (value) { - connector.setContactSmazEnabled(contact.publicKeyHex, value); + connector.setContactSmazEnabled( + contact.publicKeyHex, + value, + ); }, ), ], @@ -765,7 +852,9 @@ class _ChatScreenState extends State { final connector = Provider.of(context, listen: false); final currentContact = _resolveContact(connector); - if (currentContact.pathLength > 0 && currentContact.path.isEmpty && connector.isConnected) { + if (currentContact.pathLength > 0 && + currentContact.path.isEmpty && + connector.isConnected) { connector.getContacts(); } @@ -786,19 +875,31 @@ class _ChatScreenState extends State { onRefresh: connector.isConnected ? connector.getContacts : null, ); - appLogger.info('PathSelectionDialog returned: ${result?.length ?? 0} bytes, mounted: $mounted', tag: 'ChatScreen'); + appLogger.info( + 'PathSelectionDialog returned: ${result?.length ?? 0} bytes, mounted: $mounted', + tag: 'ChatScreen', + ); if (result == null) { - appLogger.info('PathSelectionDialog was cancelled or returned null', tag: 'ChatScreen'); + appLogger.info( + 'PathSelectionDialog was cancelled or returned null', + tag: 'ChatScreen', + ); return; } if (!mounted) { - appLogger.warn('Widget not mounted after dialog, cannot set path', tag: 'ChatScreen'); + appLogger.warn( + 'Widget not mounted after dialog, cannot set path', + tag: 'ChatScreen', + ); return; } - appLogger.info('Calling setPathOverride for ${widget.contact.name}', tag: 'ChatScreen'); + appLogger.info( + 'Calling setPathOverride for ${widget.contact.name}', + tag: 'ChatScreen', + ); await connector.setPathOverride( widget.contact, pathLen: result.length, @@ -810,7 +911,6 @@ class _ChatScreenState extends State { await _notifyPathSet(connector, widget.contact, result, result.length); } - void _openMessagePath(Message message, Contact contact) { final connector = context.read(); final fourByteHex = message.fourByteRoomContactKey @@ -877,8 +977,7 @@ class _ChatScreenState extends State { await _deleteMessage(message); }, ), - if (message.isOutgoing && - message.status == MessageStatus.failed) + if (message.isOutgoing && message.status == MessageStatus.failed) ListTile( leading: const Icon(Icons.refresh), title: Text(context.l10n.common_retry), @@ -909,29 +1008,26 @@ class _ChatScreenState extends State { void _copyMessageText(String text) { Clipboard.setData(ClipboardData(text: text)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageCopied)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageCopied))); } Future _deleteMessage(Message message) async { await context.read().deleteMessage(message); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_messageDeleted)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.chat_messageDeleted))); } void _retryMessage(Message message) { final connector = Provider.of(context, listen: false); // Retry using the contact's current path override setting - connector.sendMessage( - widget.contact, - message.text, - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.chat_retryingMessage)), - ); + connector.sendMessage(widget.contact, message.text); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage))); } void _showEmojiPicker(Message message, Contact senderContact) { @@ -951,11 +1047,17 @@ class _ChatScreenState extends State { final emojiIndex = ReactionHelper.emojiToIndex(emoji); if (emojiIndex == null) return; // Unknown emoji, skip final timestampSecs = message.timestamp.millisecondsSinceEpoch ~/ 1000; - + // For room servers, include sender name (like channels) since multiple users // For 1:1 chats, sender is implicit (null) - final senderName = widget.contact.type == advTypeRoom ? senderContact.name : null; - final hash = ReactionHelper.computeReactionHash(timestampSecs, senderName, message.text); + final senderName = widget.contact.type == advTypeRoom + ? senderContact.name + : null; + final hash = ReactionHelper.computeReactionHash( + timestampSecs, + senderName, + message.text, + ); final reactionText = 'r:$hash:$emojiIndex'; connector.sendMessage(widget.contact, reactionText); } @@ -985,7 +1087,9 @@ class _MessageBubble extends StatelessWidget { final isFailed = message.status == MessageStatus.failed; final bubbleColor = isFailed ? colorScheme.errorContainer - : (isOutgoing ? colorScheme.primary : colorScheme.surfaceContainerHighest); + : (isOutgoing + ? colorScheme.primary + : colorScheme.surfaceContainerHighest); final textColor = isFailed ? colorScheme.onErrorContainer : (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface); @@ -997,13 +1101,17 @@ class _MessageBubble extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( - crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end : CrossAxisAlignment.start, + crossAxisAlignment: isOutgoing + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ GestureDetector( onTap: onTap, onLongPress: onLongPress, child: Row( - mainAxisAlignment: isOutgoing ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isOutgoing) ...[ @@ -1012,133 +1120,154 @@ class _MessageBubble extends StatelessWidget { ], Flexible( child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, - ), - decoration: BoxDecoration( - color: bubbleColor, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - Padding( - padding: gifId != null - ? const EdgeInsets.only(left: 8, top: 4, bottom: 4) - : EdgeInsets.zero, - child: Text( - senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.primary, + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, ), - ), - ), - if (gifId == null) const SizedBox(height: 4), - ], - if (poi != null) - _buildPoiMessage(context, poi, textColor, metaColor) - else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: textColor.withValues(alpha: 0.7), - ), - ) - else - Linkify( - text: messageText, - style: TextStyle( - color: textColor, - ), - linkStyle: const TextStyle( - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => LinkHandler.handleLinkTap(context, link.url), - ), - if (isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount(message.retryCount, 4), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only(left: 8, right: 8, bottom: 4) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, - ), - ), - if (isOutgoing) ...[ - const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing ? metaColor : Colors.green[700], - ), - Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.65, + ), + decoration: BoxDecoration( + color: bubbleColor, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + senderName, style: TextStyle( - fontSize: 9, - color: isOutgoing ? metaColor : Colors.green[700], + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.primary, ), ), - ], + ), + if (gifId == null) const SizedBox(height: 4), ], - ), + if (poi != null) + _buildPoiMessage(context, poi, textColor, metaColor) + else if (gifId != null) + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues( + alpha: 0.7, + ), + ), + ) + else + Linkify( + text: messageText, + style: TextStyle(color: textColor), + linkStyle: const TextStyle( + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + if (isOutgoing && message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + 4, + ), + style: TextStyle( + fontSize: 10, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 10, + color: metaColor, + ), + ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + _buildStatusIcon(metaColor), + ], + if (message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + ), + ], + ], + ), + ), + ], ), - ], + ), ), - ), + ], + ), + ), + if (message.reactions.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), + child: _buildReactionsDisplay(context, message, colorScheme), ), ], - ), - ), - if (message.reactions.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), - child: _buildReactionsDisplay(context, message, colorScheme), - ), - ], - ], + ], ), ); } @@ -1151,8 +1280,9 @@ class _MessageBubble extends StatelessWidget { _PoiInfo? _parsePoiMessage(String text) { final trimmed = text.trim(); - final match = RegExp(r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$') - .firstMatch(trimmed); + final match = RegExp( + r'^m:([\-0-9.]+),([\-0-9.]+)\|([^|]*)\|.*$', + ).firstMatch(trimmed); if (match == null) return null; final lat = double.tryParse(match.group(1) ?? ''); final lon = double.tryParse(match.group(2) ?? ''); @@ -1193,18 +1323,12 @@ class _MessageBubble extends StatelessWidget { children: [ Text( context.l10n.chat_poiShared, - style: TextStyle( - color: textColor, - fontWeight: FontWeight.w600, - ), + style: TextStyle(color: textColor, fontWeight: FontWeight.w600), ), if (poi.label.isNotEmpty) Text( poi.label, - style: TextStyle( - color: metaColor, - fontSize: 12, - ), + style: TextStyle(color: metaColor, fontSize: 12), ), ], ), @@ -1213,7 +1337,11 @@ class _MessageBubble extends StatelessWidget { ); } - Widget _buildReactionsDisplay(BuildContext context, Message message, ColorScheme colorScheme) { + Widget _buildReactionsDisplay( + BuildContext context, + Message message, + ColorScheme colorScheme, + ) { return Wrap( spacing: 6, runSpacing: 6, @@ -1234,10 +1362,7 @@ class _MessageBubble extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - emoji, - style: const TextStyle(fontSize: 16), - ), + Text(emoji, style: const TextStyle(fontSize: 16)), if (count > 1) ...[ const SizedBox(width: 4), Text( @@ -1321,11 +1446,7 @@ class _MessageBubble extends StatelessWidget { break; } - return Icon( - icon, - size: 12, - color: color, - ); + return Icon(icon, size: 12, color: color); } String _formatTime(DateTime time) { @@ -1340,9 +1461,5 @@ class _PoiInfo { final double lon; final String label; - const _PoiInfo({ - required this.lat, - required this.lon, - required this.label, - }); + const _PoiInfo({required this.lat, required this.lon, required this.label}); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 48f94f9..f04bc50 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -29,16 +29,9 @@ import 'map_screen.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; -enum RoomLoginDestination { - chat, - management, -} +enum RoomLoginDestination { chat, management } -enum ContactOperationType { - import, - export, - zeroHopShare, -} +enum ContactOperationType { import, export, zeroHopShare } class ContactsScreen extends StatefulWidget { final bool hideBackButton; @@ -105,7 +98,9 @@ class _ContactsScreenState extends State if (advertPacket.length < 98) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + SnackBar( + content: Text(context.l10n.contacts_invalidAdvertFormat), + ), ); } _pendingOperations.remove(ContactOperationType.export); @@ -115,23 +110,25 @@ class _ContactsScreenState extends State Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } - if(code == respCodeOk) { + if (code == respCodeOk) { // Show a snackbar indicating success - if(!mounted) return; + if (!mounted) return; - if(_pendingOperations.contains(ContactOperationType.import)){ + if (_pendingOperations.contains(ContactOperationType.import)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactImported)), ); } - if(_pendingOperations.contains(ContactOperationType.zeroHopShare)) { + if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertSent)), + SnackBar( + content: Text(context.l10n.contacts_zeroHopContactAdvertSent), + ), ); } - if(_pendingOperations.contains(ContactOperationType.export)) { + if (_pendingOperations.contains(ContactOperationType.export)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), ); @@ -140,30 +137,33 @@ class _ContactsScreenState extends State _pendingOperations.clear(); } - if(code == respCodeErr) { + if (code == respCodeErr) { // Show a snackbar indicating failure - if(!mounted) return; + if (!mounted) return; - if(_pendingOperations.contains(ContactOperationType.import)){ + if (_pendingOperations.contains(ContactOperationType.import)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), ); } - if(_pendingOperations.contains(ContactOperationType.zeroHopShare)) { + if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertFailed)), + SnackBar( + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), + ), ); } - if(_pendingOperations.contains(ContactOperationType.export)) { + if (_pendingOperations.contains(ContactOperationType.export)) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactAdvertCopyFailed)), + SnackBar( + content: Text(context.l10n.contacts_contactAdvertCopyFailed), + ), ); } _pendingOperations.clear(); } - }); } @@ -185,7 +185,7 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final clipboardData = await Clipboard.getData('text/plain'); if (clipboardData == null || clipboardData.text == null) { - if(mounted) { + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), ); @@ -194,7 +194,7 @@ class _ContactsScreenState extends State } final text = clipboardData.text!.trim(); if (!text.startsWith('meshcore://')) { - if(mounted) { + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), ); @@ -207,7 +207,7 @@ class _ContactsScreenState extends State _pendingOperations.add(ContactOperationType.import); await connector.sendFrame(importContactFrame); } catch (e) { - if(mounted) { + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), ); @@ -234,59 +234,64 @@ class _ContactsScreenState extends State centerTitle: true, automaticallyImplyLeading: false, actions: [ - PopupMenuButton(itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.connect_without_contact), - const SizedBox(width: 8), - Text(context.l10n.contacts_zeroHopAdvert), - ], - ), - onTap: () => { + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.connect_without_contact), + const SizedBox(width: 8), + Text(context.l10n.contacts_zeroHopAdvert), + ], + ), + onTap: () => { connector.sendSelfAdvert(flood: false), - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.cell_tower), - const SizedBox(width: 8), - Text(context.l10n.contacts_floodAdvert), - ], + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.settings_advertisementSent), + ), + ), + }, ), - onTap: () => { + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.cell_tower), + const SizedBox(width: 8), + Text(context.l10n.contacts_floodAdvert), + ], + ), + onTap: () => { connector.sendSelfAdvert(flood: true), - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), - }, - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.copy), - const SizedBox(width: 8), - Text(context.l10n.contacts_copyAdvertToClipboard), - ], + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.settings_advertisementSent), + ), + ), + }, ), - onTap: () => _contactExport(Uint8List.fromList([])), - ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.paste), - const SizedBox(width: 8), - Text(context.l10n.contacts_addContactFromClipboard), - ], + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.copy), + const SizedBox(width: 8), + Text(context.l10n.contacts_copyAdvertToClipboard), + ], + ), + onTap: () => _contactExport(Uint8List.fromList([])), ), - onTap: () => _contactImport(), - ), - ], - icon: const Icon(Icons.connect_without_contact), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.paste), + const SizedBox(width: 8), + Text(context.l10n.contacts_addContactFromClipboard), + ], + ), + onTap: () => _contactImport(), + ), + ], + icon: const Icon(Icons.connect_without_contact), ), PopupMenuButton( itemBuilder: (context) => [ @@ -310,7 +315,9 @@ class _ContactsScreenState extends State ), onTap: () => Navigator.push( context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), + MaterialPageRoute( + builder: (context) => const SettingsScreen(), + ), ), ), ], @@ -704,7 +711,8 @@ class _ContactsScreenState extends State Navigator.push( context, MaterialPageRoute( - builder: (context) => destination == RoomLoginDestination.management + builder: (context) => + destination == RoomLoginDestination.management ? RepeaterHubScreen(repeater: room, password: password) : ChatScreen(contact: room), ), @@ -970,15 +978,22 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), + title: contact.pathLength > 0 + ? Text(context.l10n.contacts_pathTrace) + : Text(context.l10n.contacts_ping), onTap: () { - showDialog(context: context, builder: (context) { - return PathTraceDialog( - title: contact.pathLength > 0 ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing, - path: contact.traceRouteBytes ?? Uint8List(0), - ); - }); - } + showDialog( + context: context, + builder: (context) { + return PathTraceDialog( + title: contact.pathLength > 0 + ? context.l10n.contacts_repeaterPathTrace + : context.l10n.contacts_repeaterPing, + path: contact.traceRouteBytes ?? Uint8List(0), + ); + }, + ); + }, ), ListTile( leading: const Icon(Icons.cell_tower, color: Colors.orange), @@ -987,19 +1002,26 @@ class _ContactsScreenState extends State Navigator.pop(sheetContext); _showRepeaterLogin(context, contact); }, - ) - ]else if (isRoom) ...[ + ), + ] else if (isRoom) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), + title: contact.pathLength > 0 + ? Text(context.l10n.contacts_pathTrace) + : Text(context.l10n.contacts_ping), onTap: () { - showDialog(context: context, builder: (context) { - return PathTraceDialog( - title: contact.pathLength > 0 ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.traceRouteBytes ?? Uint8List(0), - ); - }); - } + showDialog( + context: context, + builder: (context) { + return PathTraceDialog( + title: contact.pathLength > 0 + ? context.l10n.contacts_roomPathTrace + : context.l10n.contacts_roomPing, + path: contact.traceRouteBytes ?? Uint8List(0), + ); + }, + ); + }, ), ListTile( leading: const Icon(Icons.room, color: Colors.blue), @@ -1010,27 +1032,39 @@ class _ContactsScreenState extends State }, ), ListTile( - leading: const Icon(Icons.room_preferences, color: Colors.orange), + leading: const Icon( + Icons.room_preferences, + color: Colors.orange, + ), title: Text(context.l10n.room_management), onTap: () { Navigator.pop(sheetContext); - _showRoomLogin(context, contact, RoomLoginDestination.management); + _showRoomLogin( + context, + contact, + RoomLoginDestination.management, + ); }, ), ] else ...[ - if(contact.pathLength > 0) - ListTile( - leading: const Icon(Icons.radar, color: Colors.green), - title: Text(context.l10n.contacts_chatTraceRoute), - onTap: () { - showDialog(context: context, builder: (context) { - return PathTraceDialog( - title: context.l10n.contacts_pathTraceTo(contact.name), - path: contact.traceRouteBytes ?? Uint8List(0), + if (contact.pathLength > 0) + ListTile( + leading: const Icon(Icons.radar, color: Colors.green), + title: Text(context.l10n.contacts_chatTraceRoute), + onTap: () { + showDialog( + context: context, + builder: (context) { + return PathTraceDialog( + title: context.l10n.contacts_pathTraceTo( + contact.name, + ), + path: contact.traceRouteBytes ?? Uint8List(0), + ); + }, ); - }); - } - ), + }, + ), ListTile( leading: const Icon(Icons.chat), title: Text(context.l10n.contacts_openChat), @@ -1051,7 +1085,7 @@ class _ContactsScreenState extends State ListTile( leading: const Icon(Icons.connect_without_contact), title: Text(context.l10n.contacts_ShareContactZeroHop), - onTap: () { + onTap: () { Navigator.pop(sheetContext); _contactZeroHop(contact.publicKey); }, @@ -1127,10 +1161,13 @@ class _ContactTile extends StatelessWidget { child: _buildContactAvatar(contact), ), title: Text(contact.name), - subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(contact.pathLabel), - Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)) - ],), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(contact.pathLabel), + Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)), + ], + ), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. trailing: MediaQuery( @@ -1154,8 +1191,8 @@ class _ContactTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), ], diff --git a/lib/screens/device_screen.dart b/lib/screens/device_screen.dart index 7a3b75b..c5967cf 100644 --- a/lib/screens/device_screen.dart +++ b/lib/screens/device_screen.dart @@ -127,9 +127,7 @@ class _DeviceScreenState extends State return Card( elevation: 0, color: colorScheme.surfaceContainerHighest, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -207,7 +205,6 @@ class _DeviceScreenState extends State ); } - Widget _buildBatteryIndicator( MeshCoreConnector connector, BuildContext context, @@ -224,11 +221,7 @@ class _DeviceScreenState extends State final icon = _batteryIcon(percent); return ActionChip( - avatar: Icon( - icon, - size: 16, - color: colorScheme.onSecondaryContainer, - ), + avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer), label: Text(displayLabel), labelStyle: theme.textTheme.labelMedium?.copyWith( color: colorScheme.onSecondaryContainer, @@ -260,25 +253,19 @@ class _DeviceScreenState extends State case 0: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const ContactsScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)), ); break; case 1: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const ChannelsScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)), ); break; case 2: Navigator.pushReplacement( context, - buildQuickSwitchRoute( - const MapScreen(hideBackButton: true), - ), + buildQuickSwitchRoute(const MapScreen(hideBackButton: true)), ); break; } diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 3a1e1a9..3f61109 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -56,10 +56,7 @@ class _MapCacheScreenState extends State { _updateEstimate(); if (bounds != null) { _mapController.fitCamera( - CameraFit.bounds( - bounds: bounds, - padding: const EdgeInsets.all(48), - ), + CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)), ); } } @@ -72,8 +69,11 @@ class _MapCacheScreenState extends State { return; } final cacheService = context.read(); - final count = - cacheService.estimateTileCount(_selectedBounds!, _minZoom, _maxZoom); + final count = cacheService.estimateTileCount( + _selectedBounds!, + _minZoom, + _maxZoom, + ); setState(() { _estimatedTiles = count; }); @@ -181,9 +181,9 @@ class _MapCacheScreenState extends State { result.failed, ) : context.l10n.mapCache_cachedTiles(result.downloaded); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(message))); } Future _clearCache() async { @@ -224,10 +224,7 @@ class _MapCacheScreenState extends State { : (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble(); return Scaffold( - appBar: AppBar( - title: Text(l10n.mapCache_title), - centerTitle: true, - ), + appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true), body: Column( children: [ Expanded( @@ -290,7 +287,10 @@ class _MapCacheScreenState extends State { children: [ Text( l10n.mapCache_cacheArea, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), const SizedBox(height: 8), Row( @@ -304,8 +304,9 @@ class _MapCacheScreenState extends State { ), const SizedBox(width: 12), TextButton( - onPressed: - _isDownloading || selectedBounds == null ? null : _clearBounds, + onPressed: _isDownloading || selectedBounds == null + ? null + : _clearBounds, child: Text(l10n.common_clear), ), ], @@ -313,11 +314,16 @@ class _MapCacheScreenState extends State { const SizedBox(height: 12), Text( l10n.mapCache_zoomRange, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), RangeSlider( - values: - RangeValues(_minZoom.toDouble(), _maxZoom.toDouble()), + values: RangeValues( + _minZoom.toDouble(), + _maxZoom.toDouble(), + ), min: 3, max: 18, divisions: 15, @@ -341,10 +347,12 @@ class _MapCacheScreenState extends State { const SizedBox(height: 8), LinearProgressIndicator(value: progressValue), const SizedBox(height: 4), - Text(l10n.mapCache_downloadedTiles( - _completedTiles, - _estimatedTiles, - )), + Text( + l10n.mapCache_downloadedTiles( + _completedTiles, + _estimatedTiles, + ), + ), ], const SizedBox(height: 12), Row( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 734f2b2..0da9960 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -269,7 +269,9 @@ class _MapScreenState extends State { ), onTap: () => Navigator.push( context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), + MaterialPageRoute( + builder: (context) => const SettingsScreen(), + ), ), ), ], @@ -278,85 +280,82 @@ class _MapScreenState extends State { ], ), body: Stack( - children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: center, - initialZoom: initialZoom, - minZoom: 2.0, - maxZoom: 18.0, - interactionOptions: InteractionOptions( - flags: ~InteractiveFlag.rotate - ), - onTap: (_, latLng) { - if (_isSelectingPoi) { - setState(() { - _isSelectingPoi = false; - }); - _shareMarker( - context: context, - connector: connector, - position: latLng, - defaultLabel: context.l10n.map_pointOfInterest, - flags: 'poi', - ); - } - }, - onLongPress: (_, latLng) { - if (_isSelectingPoi) { - setState(() { - _isSelectingPoi = false; - }); - _shareMarker( - context: context, - connector: connector, - position: latLng, - defaultLabel: context.l10n.map_pointOfInterest, - flags: 'poi', - ); - return; - } - _showShareMarkerAtPositionSheet( - context: context, - connector: connector, - position: latLng, - ); - }, - ), - children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), - MarkerLayer( - markers: [ - if (highlightPosition != null) - Marker( - point: highlightPosition, - width: 40, - height: 40, - child: Icon( - Icons.location_on_outlined, - color: Colors.red[600], - size: 34, - ), - ), - ..._buildMarkers(contactsWithLocation, settings), - ...sharedMarkers.map(_buildSharedMarker), - ], - ), - ], - ), - _buildLegend( - contactsWithLocation.length, - sharedMarkers.length, - ), - ], + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: center, + initialZoom: initialZoom, + minZoom: 2.0, + maxZoom: 18.0, + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + ), + onTap: (_, latLng) { + if (_isSelectingPoi) { + setState(() { + _isSelectingPoi = false; + }); + _shareMarker( + context: context, + connector: connector, + position: latLng, + defaultLabel: context.l10n.map_pointOfInterest, + flags: 'poi', + ); + } + }, + onLongPress: (_, latLng) { + if (_isSelectingPoi) { + setState(() { + _isSelectingPoi = false; + }); + _shareMarker( + context: context, + connector: connector, + position: latLng, + defaultLabel: context.l10n.map_pointOfInterest, + flags: 'poi', + ); + return; + } + _showShareMarkerAtPositionSheet( + context: context, + connector: connector, + position: latLng, + ); + }, ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + MarkerLayer( + markers: [ + if (highlightPosition != null) + Marker( + point: highlightPosition, + width: 40, + height: 40, + child: Icon( + Icons.location_on_outlined, + color: Colors.red[600], + size: 34, + ), + ), + ..._buildMarkers(contactsWithLocation, settings), + ...sharedMarkers.map(_buildSharedMarker), + ], + ), + ], + ), + _buildLegend(contactsWithLocation.length, sharedMarkers.length), + ], + ), bottomNavigationBar: SafeArea( top: false, child: QuickSwitchBar( @@ -376,7 +375,6 @@ class _MapScreenState extends State { ); } - List _buildMarkers(List contacts, settings) { final markers = []; diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index 2a53a16..abfb06a 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -119,14 +119,24 @@ class _RepeaterCliScreenState extends State { // Show debug info if requested if (showDebug && mounted) { - final frame = buildSendCliCommandFrame(widget.repeater.publicKey, command); - DebugFrameViewer.showFrameDebug(context, frame, context.l10n.repeater_cliCommandFrameTitle); + final frame = buildSendCliCommandFrame( + widget.repeater.publicKey, + command, + ); + DebugFrameViewer.showFrameDebug( + context, + frame, + context.l10n.repeater_cliCommandFrameTitle, + ); } // Send CLI command to repeater with retry try { if (_commandService != null) { - final connector = Provider.of(context, listen: false); + final connector = Provider.of( + context, + listen: false, + ); final repeater = _resolveRepeater(connector); final response = await _commandService!.sendCommand( repeater, @@ -230,7 +240,10 @@ class _RepeaterCliScreenState extends State { Text(l10n.repeater_cliTitle), Text( repeater.name, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), ), ], ), @@ -251,12 +264,20 @@ class _RepeaterCliScreenState extends State { value: 'auto', child: Row( children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_autoUseSavedPath, style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -266,12 +287,20 @@ class _RepeaterCliScreenState extends State { value: 'flood', child: Row( children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_forceFloodMode, style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -282,7 +311,8 @@ class _RepeaterCliScreenState extends State { IconButton( icon: const Icon(Icons.timeline), tooltip: l10n.repeater_pathManagement, - onPressed: () => PathManagementDialog.show(context, contact: repeater), + onPressed: () => + PathManagementDialog.show(context, contact: repeater), ), IconButton( icon: const Icon(Icons.bug_report), @@ -473,7 +503,10 @@ class _RepeaterCliScreenState extends State { decoration: InputDecoration( hintText: l10n.repeater_enterCommandHint, border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), prefixText: '> ', ), style: const TextStyle(fontFamily: 'monospace'), @@ -718,10 +751,7 @@ class _RepeaterCliScreenState extends State { ]; final gpsCommands = [ - _CommandHelpEntry( - command: 'gps', - description: l10n.repeater_cliHelpGps, - ), + _CommandHelpEntry(command: 'gps', description: l10n.repeater_cliHelpGps), _CommandHelpEntry( command: 'gps {on|off}', description: l10n.repeater_cliHelpGpsOnOff, @@ -758,13 +788,25 @@ class _RepeaterCliScreenState extends State { style: const TextStyle(fontSize: 13), ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_general, generalCommands), + _buildHelpSection( + context, + l10n.repeater_general, + generalCommands, + ), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_settingsCategory, settingsCommands), + _buildHelpSection( + context, + l10n.repeater_settingsCategory, + settingsCommands, + ), const SizedBox(height: 16), _buildHelpSection(context, l10n.repeater_bridge, bridgeCommands), const SizedBox(height: 16), - _buildHelpSection(context, l10n.repeater_logging, loggingCommands), + _buildHelpSection( + context, + l10n.repeater_logging, + loggingCommands, + ), const SizedBox(height: 16), _buildHelpSection( context, @@ -813,10 +855,7 @@ class _RepeaterCliScreenState extends State { ), if (note != null) ...[ const SizedBox(height: 6), - Text( - note, - style: const TextStyle(fontSize: 12), - ), + Text(note, style: const TextStyle(fontSize: 12)), ], const SizedBox(height: 8), ...commands.map((entry) => _buildHelpCommandCard(context, entry)), @@ -871,8 +910,5 @@ class _CommandHelpEntry { final String command; final String description; - const _CommandHelpEntry({ - required this.command, - required this.description, - }); + const _CommandHelpEntry({required this.command, required this.description}); } diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 759a85e..1523f77 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -28,7 +28,8 @@ class RepeaterStatusScreen extends StatefulWidget { class _RepeaterStatusScreenState extends State { static const int _statusPayloadOffset = 8; static const int _statusStatsSize = 52; - static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize; + static const int _statusResponseBytes = + _statusPayloadOffset + _statusStatsSize; bool _isLoading = false; StreamSubscription? _frameSubscription; @@ -293,7 +294,9 @@ class _RepeaterStatusScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())), + content: Text( + context.l10n.repeater_errorLoadingStatus(e.toString()), + ), backgroundColor: Colors.red, ), ); @@ -327,7 +330,10 @@ class _RepeaterStatusScreenState extends State { Text(l10n.repeater_statusTitle), Text( repeater.name, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), ), ], ), @@ -348,12 +354,20 @@ class _RepeaterStatusScreenState extends State { value: 'auto', child: Row( children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_autoUseSavedPath, style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -363,12 +377,20 @@ class _RepeaterStatusScreenState extends State { value: 'flood', child: Row( children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), const SizedBox(width: 8), Text( l10n.repeater_forceFloodMode, style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -379,7 +401,8 @@ class _RepeaterStatusScreenState extends State { IconButton( icon: const Icon(Icons.timeline), tooltip: l10n.repeater_pathManagement, - onPressed: () => PathManagementDialog.show(context, contact: repeater), + onPressed: () => + PathManagementDialog.show(context, contact: repeater), ), IconButton( icon: _isLoading @@ -423,11 +446,17 @@ class _RepeaterStatusScreenState extends State { children: [ Row( children: [ - Icon(Icons.info_outline, color: Theme.of(context).textTheme.headlineSmall?.color), + Icon( + Icons.info_outline, + color: Theme.of(context).textTheme.headlineSmall?.color, + ), const SizedBox(width: 8), Text( l10n.repeater_systemInformation, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ], ), @@ -453,18 +482,30 @@ class _RepeaterStatusScreenState extends State { children: [ Row( children: [ - Icon(Icons.radio, color: Theme.of(context).textTheme.headlineSmall?.color), + Icon( + Icons.radio, + color: Theme.of(context).textTheme.headlineSmall?.color, + ), const SizedBox(width: 8), Text( l10n.repeater_radioStatistics, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ], ), const Divider(), - _buildInfoRow(l10n.repeater_lastRssi, _formatValue(_lastRssi, suffix: ' dB')), + _buildInfoRow( + l10n.repeater_lastRssi, + _formatValue(_lastRssi, suffix: ' dB'), + ), _buildInfoRow(l10n.repeater_lastSnr, _formatSnr(_lastSnr)), - _buildInfoRow(l10n.repeater_noiseFloor, _formatValue(_noiseFloor, suffix: ' dB')), + _buildInfoRow( + l10n.repeater_noiseFloor, + _formatValue(_noiseFloor, suffix: ' dB'), + ), _buildInfoRow(l10n.repeater_txAirtime, _formatDuration(_txAirSecs)), _buildInfoRow(l10n.repeater_rxAirtime, _formatDuration(_rxAirSecs)), ], @@ -483,11 +524,17 @@ class _RepeaterStatusScreenState extends State { children: [ Row( children: [ - Icon(Icons.analytics, color: Theme.of(context).textTheme.headlineSmall?.color), + Icon( + Icons.analytics, + color: Theme.of(context).textTheme.headlineSmall?.color, + ), const SizedBox(width: 8), Text( l10n.repeater_packetStatistics, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), ], ), @@ -561,7 +608,8 @@ class _RepeaterStatusScreenState extends State { if (_statusRequestedAt == null) return '—'; final dt = _statusRequestedAt!; final date = '${dt.day}/${dt.month}/${dt.year}'; - final time = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + final time = + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; return '$date $time'; } @@ -598,7 +646,8 @@ class _RepeaterStatusScreenState extends State { final direct = _formatValue(_dupDirect); return l10n.repeater_duplicatesFloodDirect(flood, direct); } - if (_packetsRecv == null || _floodRx == null || _directRx == null) return '—'; + if (_packetsRecv == null || _floodRx == null || _directRx == null) + return '—'; final dupTotal = _packetsRecv! - _floodRx! - _directRx!; if (dupTotal < 0) return '—'; return l10n.repeater_duplicatesTotal(dupTotal); diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 0d38d98..75819a0 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -23,22 +23,21 @@ class _ScannerScreenState extends State { void initState() { super.initState(); final connector = Provider.of(context, listen: false); - + _connectionListener = () { if (connector.state == MeshCoreConnectionState.disconnected) { _changedNavigation = false; - } else if (connector.state == MeshCoreConnectionState.connected && !_changedNavigation) { + } else if (connector.state == MeshCoreConnectionState.connected && + !_changedNavigation) { _changedNavigation = true; if (mounted) { Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const ContactsScreen(), - ), + MaterialPageRoute(builder: (context) => const ContactsScreen()), ); } } }; - + connector.addListener(_connectionListener); } @@ -67,9 +66,7 @@ class _ScannerScreenState extends State { _buildStatusBar(context, connector), // Device list - Expanded( - child: _buildDeviceList(context, connector), - ), + Expanded(child: _buildDeviceList(context, connector)), ], ); }, @@ -77,8 +74,9 @@ class _ScannerScreenState extends State { ), floatingActionButton: Consumer( builder: (context, connector, child) { - final isScanning = connector.state == MeshCoreConnectionState.scanning; - + final isScanning = + connector.state == MeshCoreConnectionState.scanning; + return FloatingActionButton.extended( onPressed: () { if (isScanning) { @@ -87,7 +85,7 @@ class _ScannerScreenState extends State { connector.startScan(); } }, - icon: isScanning + icon: isScanning ? const SizedBox( width: 20, height: 20, @@ -97,7 +95,11 @@ class _ScannerScreenState extends State { ), ) : const Icon(Icons.bluetooth_searching), - label: Text(isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan), + label: Text( + isScanning + ? context.l10n.scanner_stop + : context.l10n.scanner_scan, + ), ); }, ), @@ -108,7 +110,7 @@ class _ScannerScreenState extends State { String statusText; Color statusColor; -final l10n = context.l10n; + final l10n = context.l10n; switch (connector.state) { case MeshCoreConnectionState.scanning: statusText = l10n.scanner_scanning; @@ -155,20 +157,13 @@ final l10n = context.l10n; child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.bluetooth, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( connector.state == MeshCoreConnectionState.scanning ? context.l10n.scanner_searchingDevices : context.l10n.scanner_tapToScan, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 16, color: Colors.grey[600]), ), ], ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 04740d8..415d508 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -442,7 +442,8 @@ class _SettingsScreenState extends State { bool isGPSEnabled = customVars["gps"] == "1"; // Read current interval or default to 900 (15 minutes) - final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900; + final currentInterval = + int.tryParse(customVars["gps_interval"] ?? "") ?? 900; intervalController.text = currentInterval.toString(); showDialog( @@ -782,9 +783,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { final maxTxPower = widget.connector.maxTxPower ?? 22; if (txPower == null || txPower < 0 || txPower > maxTxPower) { - ScaffoldMessenger.of( - context, - ).showSnackBar( + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'), ), diff --git a/lib/services/app_debug_log_service.dart b/lib/services/app_debug_log_service.dart index 0f4ed7d..6a35b17 100644 --- a/lib/services/app_debug_log_service.dart +++ b/lib/services/app_debug_log_service.dart @@ -1,10 +1,6 @@ import 'package:flutter/foundation.dart'; -enum AppDebugLogLevel { - info, - warning, - error, -} +enum AppDebugLogLevel { info, warning, error } class AppDebugLogEntry { final DateTime timestamp; @@ -51,7 +47,11 @@ class AppDebugLogService extends ChangeNotifier { notifyListeners(); } - void log(String message, {String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info}) { + void log( + String message, { + String tag = 'App', + AppDebugLogLevel level = AppDebugLogLevel.info, + }) { if (!_enabled) return; _entries.add( diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 5897943..c1e8fc6 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -82,10 +82,7 @@ class AppSettingsService extends ChangeNotifier { final safeMin = minZoom <= maxZoom ? minZoom : maxZoom; final safeMax = minZoom <= maxZoom ? maxZoom : minZoom; await updateSettings( - _settings.copyWith( - mapCacheMinZoom: safeMin, - mapCacheMaxZoom: safeMax, - ), + _settings.copyWith(mapCacheMinZoom: safeMin, mapCacheMaxZoom: safeMax), ); } @@ -123,9 +120,16 @@ class AppSettingsService extends ChangeNotifier { appLogger.setEnabled(value); } - Future setBatteryChemistryForDevice(String deviceId, String chemistry) async { - final updated = Map.from(_settings.batteryChemistryByDeviceId); + Future setBatteryChemistryForDevice( + String deviceId, + String chemistry, + ) async { + final updated = Map.from( + _settings.batteryChemistryByDeviceId, + ); updated[deviceId] = chemistry; - await updateSettings(_settings.copyWith(batteryChemistryByDeviceId: updated)); + await updateSettings( + _settings.copyWith(batteryChemistryByDeviceId: updated), + ); } } diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index 07ac689..0a9aeae 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -197,7 +197,7 @@ class BleDebugLogService extends ChangeNotifier { return 'RESP_CODE_CHANNEL_INFO'; case respCodeRadioSettings: return 'RESP_CODE_RADIO_SETTINGS'; - case pushCodeTraceData: + case pushCodeTraceData: return 'PUSH_CODE_TRACE_DATA'; default: return null; diff --git a/lib/services/map_tile_cache_service.dart b/lib/services/map_tile_cache_service.dart index 234481d..e0d4e57 100644 --- a/lib/services/map_tile_cache_service.dart +++ b/lib/services/map_tile_cache_service.dart @@ -42,20 +42,21 @@ class MapTileCacheService { late final TileProvider tileProvider; MapTileCacheService({BaseCacheManager? cacheManager}) - : cacheManager = cacheManager ?? - CacheManager( - Config( - cacheKey, - stalePeriod: const Duration(days: 365), - maxNrOfCacheObjects: 200000, - ), - ) { + : cacheManager = + cacheManager ?? + CacheManager( + Config( + cacheKey, + stalePeriod: const Duration(days: 365), + maxNrOfCacheObjects: 200000, + ), + ) { tileProvider = CachedNetworkTileProvider(cacheManager: this.cacheManager); } Map get defaultHeaders => { - 'User-Agent': 'flutter_map ($userAgentPackageName)', - }; + 'User-Agent': 'flutter_map ($userAgentPackageName)', + }; Future clearCache() async { await cacheManager.emptyCache(); @@ -96,17 +97,21 @@ class MapTileCacheService { final future = cacheManager .downloadFile(url, key: url, authHeaders: authHeaders) .then((_) { - completed += 1; - }).catchError((_) { - completed += 1; - failed += 1; - }).whenComplete(() { - onProgress?.call(MapTileCacheProgress( - completed: completed, - total: total, - failed: failed, - )); - }); + completed += 1; + }) + .catchError((_) { + completed += 1; + failed += 1; + }) + .whenComplete(() { + onProgress?.call( + MapTileCacheProgress( + completed: completed, + total: total, + failed: failed, + ), + ); + }); pending.add(future); if (pending.length >= safeConcurrency) { @@ -189,11 +194,9 @@ class MapTileCacheService { int _latToTileY(double lat, int zoom, int maxIndex) { final n = 1 << zoom; final rad = lat * math.pi / 180.0; - final value = ((1 - - math.log(math.tan(rad) + 1 / math.cos(rad)) / math.pi) / - 2 * - n) - .floor(); + final value = + ((1 - math.log(math.tan(rad) + 1 / math.cos(rad)) / math.pi) / 2 * n) + .floor(); return value.clamp(0, maxIndex); } diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 403cd93..9cbd68f 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -25,10 +25,7 @@ class _AckHashMapping { final String messageId; final DateTime timestamp; - _AckHashMapping({ - required this.messageId, - required this.timestamp, - }); + _AckHashMapping({required this.messageId, required this.timestamp}); } class MessageRetryService extends ChangeNotifier { @@ -39,11 +36,16 @@ class MessageRetryService extends ChangeNotifier { final Map _pendingMessages = {}; final Map _pendingContacts = {}; final Map _pendingPathSelections = {}; - final Map _ackHashToMessageId = {}; // ackHashHex → messageId + timestamp for O(1) lookup - final Map> _expectedAckHashes = {}; // Track all expected ACKs for retries (for history) - final List<_AckHistoryEntry> _ackHistory = []; // Rolling buffer of recent ACK hashes - final Map> _pendingMessageQueuePerContact = {}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed) - final Map _expectedHashToMessageId = {}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash) + final Map _ackHashToMessageId = + {}; // ackHashHex → messageId + timestamp for O(1) lookup + final Map> _expectedAckHashes = + {}; // Track all expected ACKs for retries (for history) + final List<_AckHistoryEntry> _ackHistory = + []; // Rolling buffer of recent ACK hashes + final Map> _pendingMessageQueuePerContact = + {}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed) + final Map _expectedHashToMessageId = + {}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash) Function(Contact, String, int, int)? _sendMessageCallback; Function(String, Message)? _addMessageCallback; @@ -130,7 +132,8 @@ class MessageRetryService extends ChangeNotifier { final messagePathBytes = pathBytes ?? _resolveMessagePathBytes(contact, useFlood, pathSelection); final messagePathLength = - pathLength ?? _resolveMessagePathLength(contact, useFlood, pathSelection); + pathLength ?? + _resolveMessagePathLength(contact, useFlood, pathSelection); final message = Message( senderKey: contact.publicKey, text: text, @@ -167,15 +170,25 @@ class MessageRetryService extends ChangeNotifier { if (_setContactPathCallback != null && _clearContactPathCallback != null) { if (message.pathLength != null && message.pathLength! < 0) { // Flood mode - clear the path - debugPrint('Setting flood mode for retry attempt ${message.retryCount}'); + debugPrint( + 'Setting flood mode for retry attempt ${message.retryCount}', + ); _clearContactPathCallback!(contact); } else if (message.pathLength != null && message.pathLength! >= 0) { // Specific path (including direct neighbor with pathLength=0) final pathStr = message.pathBytes.isEmpty ? 'direct' - : message.pathBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(','); - debugPrint('Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}'); - await _setContactPathCallback!(contact, message.pathBytes, message.pathLength!); + : message.pathBytes + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(','); + debugPrint( + 'Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}', + ); + await _setContactPathCallback!( + contact, + message.pathBytes, + message.pathLength!, + ); } } @@ -186,22 +199,30 @@ class MessageRetryService extends ChangeNotifier { // IMPORTANT: Use the transformed text (with SMAZ encoding if enabled) to match device's hash final selfPubKey = _getSelfPublicKeyCallback?.call(); if (selfPubKey != null) { - final outboundText = _prepareContactOutboundTextCallback?.call(contact, message.text) ?? message.text; + final outboundText = + _prepareContactOutboundTextCallback?.call(contact, message.text) ?? + message.text; final expectedHash = MessageRetryService.computeExpectedAckHash( timestampSeconds, attempt, outboundText, selfPubKey, ); - final expectedHashHex = expectedHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final expectedHashHex = expectedHash + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); _expectedHashToMessageId[expectedHashHex] = messageId; - final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; + final shortText = message.text.length > 20 + ? '${message.text.substring(0, 20)}...' + : message.text; _debugLogService?.info( 'Sent "$shortText" to ${contact.name} → expect ACK hash $expectedHashHex (attempt $attempt)', tag: 'AckHash', ); - debugPrint('Computed expected ACK hash $expectedHashHex for message $messageId'); + debugPrint( + 'Computed expected ACK hash $expectedHashHex for message $messageId', + ); } // DEPRECATED: Old queue-based matching (kept for fallback) @@ -209,17 +230,14 @@ class MessageRetryService extends ChangeNotifier { _pendingMessageQueuePerContact[contact.publicKeyHex]!.add(messageId); if (_sendMessageCallback != null) { - _sendMessageCallback!( - contact, - message.text, - attempt, - timestampSeconds, - ); + _sendMessageCallback!(contact, message.text, attempt, timestampSeconds); } } void updateMessageFromSent(Uint8List ackHash, int timeoutMs) { - final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final ackHashHex = ackHash + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); // NEW: Try hash-based matching first (fixes LoRa message drops causing mismatches) String? messageId = _expectedHashToMessageId.remove(ackHashHex); @@ -230,16 +248,21 @@ class MessageRetryService extends ChangeNotifier { final message = _pendingMessages[messageId]; if (contact != null && message != null) { - final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; + final shortText = message.text.length > 20 + ? '${message.text.substring(0, 20)}...' + : message.text; _debugLogService?.info( 'RESP_CODE_SENT received: ACK hash $ackHashHex ✓ matched "$shortText" to ${contact.name}', tag: 'AckHash', ); - debugPrint('Hash-based match: ACK hash $ackHashHex → message $messageId ✓'); + debugPrint( + 'Hash-based match: ACK hash $ackHashHex → message $messageId ✓', + ); // Remove from old queue since we matched _pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId); - if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) { + if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? + false) { _pendingMessageQueuePerContact.remove(contact.publicKeyHex); } } else { @@ -259,7 +282,9 @@ class MessageRetryService extends ChangeNotifier { 'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue', tag: 'AckHash', ); - debugPrint('Hash-based match failed for $ackHashHex, falling back to queue-based matching'); + debugPrint( + 'Hash-based match failed for $ackHashHex, falling back to queue-based matching', + ); for (var entry in _pendingMessageQueuePerContact.entries) { final contactKey = entry.key; @@ -271,7 +296,9 @@ class MessageRetryService extends ChangeNotifier { if (_pendingMessages.containsKey(candidateMessageId)) { messageId = candidateMessageId; contact = _pendingContacts[candidateMessageId]; - debugPrint('Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey'); + debugPrint( + 'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey', + ); break; } else { debugPrint('Dequeued stale message $candidateMessageId - skipping'); @@ -280,7 +307,9 @@ class MessageRetryService extends ChangeNotifier { if (_pendingMessages.containsKey(nextMessageId)) { messageId = nextMessageId; contact = _pendingContacts[nextMessageId]; - debugPrint('Queue-based match (fallback): $ackHashHex → message $messageId'); + debugPrint( + 'Queue-based match (fallback): $ackHashHex → message $messageId', + ); break; } } @@ -306,16 +335,22 @@ class MessageRetryService extends ChangeNotifier { final selection = _pendingPathSelections[messageId]; if (message == null) { - debugPrint('Message $messageId no longer pending for ACK hash: $ackHashHex'); + debugPrint( + 'Message $messageId no longer pending for ACK hash: $ackHashHex', + ); _ackHashToMessageId.remove(ackHashHex); return; } // Add this ACK hash to the list of expected ACKs for this message (for history) _expectedAckHashes[messageId] ??= []; - if (!_expectedAckHashes[messageId]!.any((hash) => listEquals(hash, ackHash))) { + if (!_expectedAckHashes[messageId]!.any( + (hash) => listEquals(hash, ackHash), + )) { _expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash)); - debugPrint('Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})'); + debugPrint( + 'Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})', + ); } // Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid @@ -330,8 +365,13 @@ class MessageRetryService extends ChangeNotifier { } else { pathLengthValue = contact.pathLength; } - actualTimeout = _calculateTimeoutCallback!(pathLengthValue, message.text.length); - debugPrint('Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue'); + actualTimeout = _calculateTimeoutCallback!( + pathLengthValue, + message.text.length, + ); + debugPrint( + 'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue', + ); } final updatedMessage = message.copyWith( @@ -364,16 +404,22 @@ class MessageRetryService extends ChangeNotifier { final selection = _pendingPathSelections[messageId]; if (message == null || contact == null) { - debugPrint('Timeout fired but message $messageId no longer pending (likely already delivered)'); + debugPrint( + 'Timeout fired but message $messageId no longer pending (likely already delivered)', + ); return; } - final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; + final shortText = message.text.length > 20 + ? '${message.text.substring(0, 20)}...' + : message.text; _debugLogService?.warn( 'Timeout: No ACK received for "$shortText" to ${contact.name} (attempt ${message.retryCount}) → retrying', tag: 'AckHash', ); - debugPrint('Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})'); + debugPrint( + 'Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})', + ); if (message.retryCount < maxRetries - 1) { final backoffMs = 1000 * (1 << message.retryCount); @@ -402,7 +448,9 @@ class MessageRetryService extends ChangeNotifier { if (_pendingMessages.containsKey(messageId)) { _attemptSend(messageId); } else { - debugPrint('Retry cancelled: message $messageId was delivered while waiting'); + debugPrint( + 'Retry cancelled: message $messageId was delivered while waiting', + ); } }); } else { @@ -420,7 +468,8 @@ class MessageRetryService extends ChangeNotifier { // Clean up the queue entry for this contact _pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId); - if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) { + if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? + false) { _pendingMessageQueuePerContact.remove(contact.publicKeyHex); } @@ -430,7 +479,13 @@ class MessageRetryService extends ChangeNotifier { _clearContactPathCallback!(contact); } - _recordPathResultFromMessage(contact.publicKeyHex, message, selection, false, null); + _recordPathResultFromMessage( + contact.publicKeyHex, + message, + selection, + false, + null, + ); if (_updateMessageCallback != null) { _updateMessageCallback!(failedMessage); @@ -443,18 +498,22 @@ class MessageRetryService extends ChangeNotifier { void _moveAckHashesToHistory(String messageId) { final ackHashes = _expectedAckHashes.remove(messageId); if (ackHashes != null && ackHashes.isNotEmpty) { - _ackHistory.add(_AckHistoryEntry( - messageId: messageId, - ackHashes: ackHashes, - timestamp: DateTime.now(), - )); + _ackHistory.add( + _AckHistoryEntry( + messageId: messageId, + ackHashes: ackHashes, + timestamp: DateTime.now(), + ), + ); // Trim history to max size (rolling buffer) while (_ackHistory.length > maxAckHistorySize) { _ackHistory.removeAt(0); } - debugPrint('Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})'); + debugPrint( + 'Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})', + ); } } @@ -462,7 +521,9 @@ class MessageRetryService extends ChangeNotifier { for (final entry in _ackHistory) { for (final expectedHash in entry.ackHashes) { if (listEquals(expectedHash, ackHash)) { - debugPrint('Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s'); + debugPrint( + 'Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s', + ); return true; } } @@ -472,7 +533,9 @@ class MessageRetryService extends ChangeNotifier { void handleAckReceived(Uint8List ackHash, int tripTimeMs) { String? matchedMessageId; - final ackHashHex = ackHash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final ackHashHex = ackHash + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); debugPrint('ACK received: $ackHashHex, trip time: ${tripTimeMs}ms'); @@ -502,7 +565,9 @@ class MessageRetryService extends ChangeNotifier { tag: 'AckHash', ); // Fallback: Check against ALL expected ACK hashes (from all retry attempts) - debugPrint('ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)'); + debugPrint( + 'ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)', + ); for (var entry in _expectedAckHashes.entries) { final messageId = entry.key; final expectedHashes = entry.value; @@ -510,7 +575,9 @@ class MessageRetryService extends ChangeNotifier { for (final expectedHash in expectedHashes) { if (listEquals(expectedHash, ackHash)) { matchedMessageId = messageId; - debugPrint('Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})'); + debugPrint( + 'Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})', + ); break; } } @@ -524,7 +591,9 @@ class MessageRetryService extends ChangeNotifier { final contact = _pendingContacts[matchedMessageId]; final selection = _pendingPathSelections[matchedMessageId]; - final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; + final shortText = message.text.length > 20 + ? '${message.text.substring(0, 20)}...' + : message.text; _debugLogService?.info( 'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} in ${tripTimeMs}ms', tag: 'AckHash', @@ -549,8 +618,11 @@ class MessageRetryService extends ChangeNotifier { // Clean up the queue entry for this contact (remove any remaining references to this message) if (contact != null) { - _pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(matchedMessageId); - if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? false) { + _pendingMessageQueuePerContact[contact.publicKeyHex]?.remove( + matchedMessageId, + ); + if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? + false) { _pendingMessageQueuePerContact.remove(contact.publicKeyHex); } } @@ -560,7 +632,13 @@ class MessageRetryService extends ChangeNotifier { } if (contact != null) { - _recordPathResultFromMessage(contact.publicKeyHex, message, selection, true, tripTimeMs); + _recordPathResultFromMessage( + contact.publicKeyHex, + message, + selection, + true, + tripTimeMs, + ); } notifyListeners(); @@ -663,7 +741,12 @@ class MessageRetryService extends ChangeNotifier { if (_recordPathResultCallback == null) return; final recordSelection = selection ?? _selectionFromMessage(message); if (recordSelection == null) return; - _recordPathResultCallback!(contactKey, recordSelection, success, tripTimeMs); + _recordPathResultCallback!( + contactKey, + recordSelection, + success, + tripTimeMs, + ); } PathSelection? _selectionFromMessage(Message message) { diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 1d25f92..ea7f031 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -6,13 +6,16 @@ class NotificationService { factory NotificationService() => _instance; NotificationService._internal(); - final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin(); + final FlutterLocalNotificationsPlugin _notifications = + FlutterLocalNotificationsPlugin(); bool _isInitialized = false; Future initialize() async { if (_isInitialized) return; - const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); + const androidSettings = AndroidInitializationSettings( + '@mipmap/ic_launcher', + ); const iosSettings = DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, @@ -47,16 +50,20 @@ class NotificationService { } // Request Android 13+ notification permission - final androidPlugin = _notifications.resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>(); + final androidPlugin = _notifications + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >(); if (androidPlugin != null) { final granted = await androidPlugin.requestNotificationsPermission(); return granted ?? false; } // iOS permissions are requested during initialization - final iosPlugin = _notifications.resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>(); + final iosPlugin = _notifications + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin + >(); if (iosPlugin != null) { final granted = await iosPlugin.requestPermissions( alert: true, @@ -204,9 +211,7 @@ class NotificationService { ); final preview = message.trim(); - final body = preview.isEmpty - ? 'Received new message' - : preview; + final body = preview.isEmpty ? 'Received new message' : preview; await _notifications.show( channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 81caef0..1314f48 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -61,7 +61,10 @@ class PathHistoryService extends ChangeNotifier { int? tripTimeMs, }) { if (selection.useFlood) { - final stats = _floodStats.putIfAbsent(contactPubKeyHex, () => _FloodStats()); + final stats = _floodStats.putIfAbsent( + contactPubKeyHex, + () => _FloodStats(), + ); if (success) { stats.successCount += 1; if (tripTimeMs != null) stats.lastTripTimeMs = tripTimeMs; @@ -88,23 +91,28 @@ class PathHistoryService extends ChangeNotifier { } PathSelection getNextAutoPathSelection(String contactPubKeyHex) { - final ranked = _getRankedPaths(contactPubKeyHex) - .take(_autoRotationTopCount) - .toList(); + final ranked = _getRankedPaths( + contactPubKeyHex, + ).take(_autoRotationTopCount).toList(); if (ranked.isEmpty) { return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true); } _trackAccess(contactPubKeyHex); - final selections = ranked - .map((path) => PathSelection( - pathBytes: path.pathBytes, - hopCount: path.hopCount, - useFlood: false, - )) - .toList() - ..add(const PathSelection(pathBytes: [], hopCount: -1, useFlood: true)); + final selections = + ranked + .map( + (path) => PathSelection( + pathBytes: path.pathBytes, + hopCount: path.hopCount, + useFlood: false, + ), + ) + .toList() + ..add( + const PathSelection(pathBytes: [], hopCount: -1, useFlood: true), + ); final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0; final selection = selections[currentIndex % selections.length]; @@ -241,7 +249,8 @@ class PathHistoryService extends ChangeNotifier { } Future _loadHistoryFromStorage( - String contactPubKeyHex) async { + String contactPubKeyHex, + ) async { return await _storage.loadPathHistory(contactPubKeyHex); } @@ -308,8 +317,10 @@ class PathHistoryService extends ChangeNotifier { ..removeWhere((p) => p.pathBytes.isEmpty); ranked.sort((a, b) { - final aRate = (a.successCount + 1) / (a.successCount + a.failureCount + 2); - final bRate = (b.successCount + 1) / (b.successCount + b.failureCount + 2); + final aRate = + (a.successCount + 1) / (a.successCount + a.failureCount + 2); + final bRate = + (b.successCount + 1) / (b.successCount + b.failureCount + 2); if (aRate != bRate) return bRate.compareTo(aRate); if (a.successCount != b.successCount) { return b.successCount.compareTo(a.successCount); @@ -329,7 +340,10 @@ class PathHistoryService extends ChangeNotifier { } void _updateFloodStats(String contactPubKeyHex) { - final stats = _floodStats.putIfAbsent(contactPubKeyHex, () => _FloodStats()); + final stats = _floodStats.putIfAbsent( + contactPubKeyHex, + () => _FloodStats(), + ); stats.lastUsed = DateTime.now(); } diff --git a/lib/services/repeater_command_service.dart b/lib/services/repeater_command_service.dart index d282bea..060f7aa 100644 --- a/lib/services/repeater_command_service.dart +++ b/lib/services/repeater_command_service.dart @@ -26,7 +26,9 @@ class RepeaterCommandService { int retries = maxRetries, }) async { final repeaterKey = repeater.publicKeyHex; - final hasPending = _pendingCommands.keys.any((id) => id.startsWith(repeaterKey)); + final hasPending = _pendingCommands.keys.any( + (id) => id.startsWith(repeaterKey), + ); if (hasPending) { throw Exception('Another command is still awaiting a response.'); } @@ -84,7 +86,9 @@ class RepeaterCommandService { attempt: attempt, timestampSeconds: timestampSeconds, ); - final responseBytes = frame.length > maxFrameSize ? frame.length : maxFrameSize; + final responseBytes = frame.length > maxFrameSize + ? frame.length + : maxFrameSize; final timeoutMs = _connector.calculateTimeout( pathLength: pathLengthValue, messageBytes: responseBytes, @@ -97,7 +101,9 @@ class RepeaterCommandService { () { final completer = _pendingCommands[commandId]; if (completer != null && !completer.isCompleted) { - completer.completeError('Command timeout after $timeoutSeconds seconds'); + completer.completeError( + 'Command timeout after $timeoutSeconds seconds', + ); _cleanup(commandId); } }, diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 39d6e6b..ce0c4f1 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -8,7 +8,9 @@ class StorageService { static const String _repeaterPasswordsKey = 'repeater_passwords'; Future savePathHistory( - String contactPubKeyHex, ContactPathHistory history) async { + String contactPubKeyHex, + ContactPathHistory history, + ) async { final prefs = PrefsManager.instance; final key = '$_pathHistoryPrefix$contactPubKeyHex'; final jsonStr = jsonEncode(history.toJson()); @@ -39,8 +41,9 @@ class StorageService { Future clearAllPathHistories() async { final prefs = PrefsManager.instance; final keys = prefs.getKeys(); - final pathHistoryKeys = - keys.where((key) => key.startsWith(_pathHistoryPrefix)); + final pathHistoryKeys = keys.where( + (key) => key.startsWith(_pathHistoryPrefix), + ); for (final key in pathHistoryKeys) { await prefs.remove(key); @@ -74,7 +77,9 @@ class StorageService { /// Save a repeater password by public key hex Future saveRepeaterPassword( - String repeaterPubKeyHex, String password) async { + String repeaterPubKeyHex, + String password, + ) async { final prefs = PrefsManager.instance; final passwords = await loadRepeaterPasswords(); passwords[repeaterPubKeyHex] = password; diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 6769c3a..1151514 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -8,7 +8,10 @@ class ChannelMessageStore { static const String _keyPrefix = 'channel_messages_'; /// Save messages for a specific channel - Future saveChannelMessages(int channelIndex, List messages) async { + Future saveChannelMessages( + int channelIndex, + List messages, + ) async { final prefs = PrefsManager.instance; final key = '$_keyPrefix$channelIndex'; @@ -96,7 +99,8 @@ class ChannelMessageStore { pathVariants: (json['pathVariants'] as List?) ?.map((entry) => Uint8List.fromList(base64Decode(entry as String))) .toList(), - repeats: (json['repeats'] as List?) + repeats: + (json['repeats'] as List?) ?.map((entry) => _repeatFromJson(entry as Map)) .toList() ?? const [], @@ -105,15 +109,19 @@ class ChannelMessageStore { replyToMessageId: json['replyToMessageId'] as String?, replyToSenderName: json['replyToSenderName'] as String?, replyToText: json['replyToText'] as String?, - reactions: (json['reactions'] as Map?)?.map( - (key, value) => MapEntry(key, value as int), - ) ?? {}, + reactions: + (json['reactions'] as Map?)?.map( + (key, value) => MapEntry(key, value as int), + ) ?? + {}, ); } Map _repeatToJson(Repeat repeat) { return { - 'repeaterKey': repeat.repeaterKey != null ? base64Encode(repeat.repeaterKey!) : null, + 'repeaterKey': repeat.repeaterKey != null + ? base64Encode(repeat.repeaterKey!) + : null, 'repeaterName': repeat.repeaterName, 'tripTimeMs': repeat.tripTimeMs, 'path': repeat.path?.map((bytes) => base64Encode(bytes)).toList() ?? [], diff --git a/lib/storage/channel_order_store.dart b/lib/storage/channel_order_store.dart index 2cf4727..b9657c4 100644 --- a/lib/storage/channel_order_store.dart +++ b/lib/storage/channel_order_store.dart @@ -16,7 +16,10 @@ class ChannelOrderStore { try { final decoded = jsonDecode(raw); if (decoded is List) { - return decoded.map((value) => value is int ? value : int.tryParse('$value')).whereType().toList(); + return decoded + .map((value) => value is int ? value : int.tryParse('$value')) + .whereType() + .toList(); } } catch (_) { // fall through to legacy parse diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index fe5c831..a81cccd 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -40,7 +40,7 @@ class CommunityStore { /// Add a new community Future addCommunity(Community community) async { final communities = await loadCommunities(); - + // Check if community with same ID already exists final existingIndex = communities.indexWhere((c) => c.id == community.id); if (existingIndex >= 0) { @@ -49,7 +49,7 @@ class CommunityStore { } else { communities.add(community); } - + await saveCommunities(communities); } @@ -92,10 +92,7 @@ class CommunityStore { } /// Add a hashtag channel to a community - Future addHashtagChannel( - String communityId, - String hashtag, - ) async { + Future addHashtagChannel(String communityId, String hashtag) async { final community = await getCommunity(communityId); if (community != null) { final updated = community.addHashtagChannel(hashtag); @@ -104,10 +101,7 @@ class CommunityStore { } /// Remove a hashtag channel from a community - Future removeHashtagChannel( - String communityId, - String hashtag, - ) async { + Future removeHashtagChannel(String communityId, String hashtag) async { final community = await getCommunity(communityId); if (community != null) { final updated = community.removeHashtagChannel(hashtag); diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 6a18b2a..08d158b 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -14,7 +14,9 @@ class ContactStore { try { final jsonList = jsonDecode(jsonStr) as List; - return jsonList.map((entry) => _fromJson(entry as Map)).toList(); + return jsonList + .map((entry) => _fromJson(entry as Map)) + .toList(); } catch (_) { return []; } @@ -57,12 +59,16 @@ class ContactStore { : Uint8List(0), pathOverride: json['pathOverride'] as int?, pathOverrideBytes: json['pathOverrideBytes'] != null - ? Uint8List.fromList(base64Decode(json['pathOverrideBytes'] as String)) + ? Uint8List.fromList( + base64Decode(json['pathOverrideBytes'] as String), + ) : null, latitude: (json['latitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(), lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), - lastMessageAt: DateTime.fromMillisecondsSinceEpoch(lastMessageMs ?? lastSeenMs), + lastMessageAt: DateTime.fromMillisecondsSinceEpoch( + lastMessageMs ?? lastSeenMs, + ), ); } } diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index c1bc640..9526ef3 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -7,7 +7,10 @@ import 'prefs_manager.dart'; class MessageStore { static const String _keyPrefix = 'messages_'; - Future saveMessages(String contactKeyHex, List messages) async { + Future saveMessages( + String contactKeyHex, + List messages, + ) async { final prefs = PrefsManager.instance; final key = '$_keyPrefix$contactKeyHex'; final jsonList = messages.map(_messageToJson).toList(); @@ -45,12 +48,16 @@ class MessageStore { 'messageId': msg.messageId, 'retryCount': msg.retryCount, 'estimatedTimeoutMs': msg.estimatedTimeoutMs, - 'expectedAckHash': msg.expectedAckHash != null ? base64Encode(msg.expectedAckHash!) : null, + 'expectedAckHash': msg.expectedAckHash != null + ? base64Encode(msg.expectedAckHash!) + : null, 'sentAt': msg.sentAt?.millisecondsSinceEpoch, 'deliveredAt': msg.deliveredAt?.millisecondsSinceEpoch, 'tripTimeMs': msg.tripTimeMs, 'pathLength': msg.pathLength, - 'pathBytes': msg.pathBytes.isNotEmpty ? base64Encode(msg.pathBytes) : null, + 'pathBytes': msg.pathBytes.isNotEmpty + ? base64Encode(msg.pathBytes) + : null, 'reactions': msg.reactions, 'fourByteRoomContactKey': base64Encode(msg.fourByteRoomContactKey), }; @@ -59,7 +66,9 @@ class MessageStore { Message _messageFromJson(Map json) { final rawText = json['text'] as String; final isCli = json['isCli'] as bool? ?? false; - final decodedText = isCli ? rawText : (Smaz.tryDecodePrefixed(rawText) ?? rawText); + final decodedText = isCli + ? rawText + : (Smaz.tryDecodePrefixed(rawText) ?? rawText); return Message( senderKey: Uint8List.fromList(base64Decode(json['senderKey'] as String)), text: decodedText, @@ -84,11 +93,15 @@ class MessageStore { pathBytes: json['pathBytes'] != null ? Uint8List.fromList(base64Decode(json['pathBytes'] as String)) : Uint8List(0), - reactions: (json['reactions'] as Map?)?.map( - (key, value) => MapEntry(key, value as int), - ) ?? {}, + reactions: + (json['reactions'] as Map?)?.map( + (key, value) => MapEntry(key, value as int), + ) ?? + {}, fourByteRoomContactKey: json['fourByteRoomContactKey'] != null - ? Uint8List.fromList(base64Decode(json['fourByteRoomContactKey'] as String)) + ? Uint8List.fromList( + base64Decode(json['fourByteRoomContactKey'] as String), + ) : null, ); } diff --git a/lib/storage/prefs_manager.dart b/lib/storage/prefs_manager.dart index a449b9c..2bf6299 100644 --- a/lib/storage/prefs_manager.dart +++ b/lib/storage/prefs_manager.dart @@ -21,7 +21,8 @@ class PrefsManager { static SharedPreferences get instance { if (_instance == null) { throw StateError( - 'PrefsManager not initialized. Call PrefsManager.initialize() in main() before use.'); + 'PrefsManager not initialized. Call PrefsManager.initialize() in main() before use.', + ); } return _instance!; } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index 520c7c4..d46a34c 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -92,8 +92,9 @@ class UnreadStore { if (_pendingChannelLastRead == null) return; final prefs = PrefsManager.instance; - final asString = - _pendingChannelLastRead!.map((key, value) => MapEntry(key.toString(), value)); + final asString = _pendingChannelLastRead!.map( + (key, value) => MapEntry(key.toString(), value), + ); final jsonStr = jsonEncode(asString); await prefs.setString(_channelLastReadKey, jsonStr); _pendingChannelLastRead = null; @@ -104,9 +105,6 @@ class UnreadStore { _contactSaveTimer?.cancel(); _channelSaveTimer?.cancel(); - await Future.wait([ - _flushContactLastRead(), - _flushChannelLastRead(), - ]); + await Future.wait([_flushContactLastRead(), _flushChannelLastRead()]); } } diff --git a/lib/utils/app_logger.dart b/lib/utils/app_logger.dart index 6ada39b..e57261e 100644 --- a/lib/utils/app_logger.dart +++ b/lib/utils/app_logger.dart @@ -44,7 +44,11 @@ class AppLogger { } /// Log a message with custom level - void log(String message, {String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info}) { + void log( + String message, { + String tag = 'App', + AppDebugLogLevel level = AppDebugLogLevel.info, + }) { if (_enabled && _service != null) { _service!.log(message, tag: tag, level: level); } diff --git a/lib/widgets/battery_indicator.dart b/lib/widgets/battery_indicator.dart index bbda1af..7837415 100644 --- a/lib/widgets/battery_indicator.dart +++ b/lib/widgets/battery_indicator.dart @@ -29,10 +29,7 @@ BatteryUi batteryUiForPercent(int? percent) { class BatteryIndicator extends StatefulWidget { final MeshCoreConnector connector; - const BatteryIndicator({ - super.key, - required this.connector, - }); + const BatteryIndicator({super.key, required this.connector}); @override State createState() => _BatteryIndicatorState(); diff --git a/lib/widgets/debug_frame_viewer.dart b/lib/widgets/debug_frame_viewer.dart index e2c6e34..c8dc371 100644 --- a/lib/widgets/debug_frame_viewer.dart +++ b/lib/widgets/debug_frame_viewer.dart @@ -5,7 +5,11 @@ import '../connector/meshcore_protocol.dart'; /// Debug widget to show the hex dump of a frame class DebugFrameViewer { - static void showFrameDebug(BuildContext context, Uint8List frame, String title) { + static void showFrameDebug( + BuildContext context, + Uint8List frame, + String title, + ) { final hexString = frame .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(' '); @@ -14,16 +18,26 @@ class DebugFrameViewer { details.writeln(context.l10n.debugFrame_length(frame.length)); details.writeln(''); details.writeln( - context.l10n.debugFrame_command(frame[0].toRadixString(16).padLeft(2, '0')), + context.l10n.debugFrame_command( + frame[0].toRadixString(16).padLeft(2, '0'), + ), ); if (frame[0] == cmdSendTxtMsg && frame.length > 37) { details.writeln(''); details.writeln(context.l10n.debugFrame_textMessageHeader); - details.writeln(context.l10n.debugFrame_destinationPubKey(pubKeyToHex(frame.sublist(1, 33)))); - details.writeln(context.l10n.debugFrame_timestamp(readUint32LE(frame, 33))); details.writeln( - context.l10n.debugFrame_flags(frame[37].toRadixString(16).padLeft(2, '0')), + context.l10n.debugFrame_destinationPubKey( + pubKeyToHex(frame.sublist(1, 33)), + ), + ); + details.writeln( + context.l10n.debugFrame_timestamp(readUint32LE(frame, 33)), + ); + details.writeln( + context.l10n.debugFrame_flags( + frame[37].toRadixString(16).padLeft(2, '0'), + ), ); final txtType = (frame[37] >> 2) & 0x03; final typeLabel = txtType == txtTypeCliData @@ -34,7 +48,7 @@ class DebugFrameViewer { final textBytes = frame.sublist(38); final nullIdx = textBytes.indexOf(0); final text = String.fromCharCodes( - nullIdx >= 0 ? textBytes.sublist(0, nullIdx) : textBytes + nullIdx >= 0 ? textBytes.sublist(0, nullIdx) : textBytes, ); details.writeln(context.l10n.debugFrame_text(text)); } diff --git a/lib/widgets/device_tile.dart b/lib/widgets/device_tile.dart index bbd4faf..9dd6d5b 100644 --- a/lib/widgets/device_tile.dart +++ b/lib/widgets/device_tile.dart @@ -7,18 +7,14 @@ class DeviceTile extends StatelessWidget { final ScanResult scanResult; final VoidCallback onTap; - const DeviceTile({ - super.key, - required this.scanResult, - required this.onTap, - }); + const DeviceTile({super.key, required this.scanResult, required this.onTap}); @override Widget build(BuildContext context) { final device = scanResult.device; final rssi = scanResult.rssi; - final name = device.platformName.isNotEmpty - ? device.platformName + final name = device.platformName.isNotEmpty + ? device.platformName : scanResult.advertisementData.advName; return ListTile( @@ -58,12 +54,8 @@ class DeviceTile extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: color), - Text( - '$rssi dBm', - style: TextStyle(fontSize: 10, color: color), - ), + Text('$rssi dBm', style: TextStyle(fontSize: 10, color: color)), ], ); } } - diff --git a/lib/widgets/emoji_picker.dart b/lib/widgets/emoji_picker.dart index 7345eff..87fd1c9 100644 --- a/lib/widgets/emoji_picker.dart +++ b/lib/widgets/emoji_picker.dart @@ -5,32 +5,196 @@ import '../l10n/l10n.dart'; class EmojiPicker extends StatelessWidget { final Function(String) onEmojiSelected; - const EmojiPicker({ - super.key, - required this.onEmojiSelected, - }); + const EmojiPicker({super.key, required this.onEmojiSelected}); static const List quickEmojis = ['👍', '❤️', '😂', '🎉', '👏', '🔥']; static const List smileys = [ - '😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '🙃', '😉', '😌', '😍', '🥰', '😘', - '😗', '😙', '😚', '😋', '😛', '😝', '😜', '🤪', '🤨', '🧐', '🤓', '😎', '🥸', '🤩', '🥳', '😏', - '😒', '😞', '😔', '😟', '😕', '🙁', '😣', '😖', '😫', '😩', '🥺', '😢', '😭', '😤', '😠', '😡', - '🤬', '🤯', '😳', '🥵', '🥶', '😱', '😨', '😰', '😥', '😓', '🤗', '🤔', '🤭', '🤫', '🤥', '😶', - ]; + '😀', + '😃', + '😄', + '😁', + '😅', + '😂', + '🤣', + '😊', + '😇', + '🙂', + '🙃', + '😉', + '😌', + '😍', + '🥰', + '😘', + '😗', + '😙', + '😚', + '😋', + '😛', + '😝', + '😜', + '🤪', + '🤨', + '🧐', + '🤓', + '😎', + '🥸', + '🤩', + '🥳', + '😏', + '😒', + '😞', + '😔', + '😟', + '😕', + '🙁', + '😣', + '😖', + '😫', + '😩', + '🥺', + '😢', + '😭', + '😤', + '😠', + '😡', + '🤬', + '🤯', + '😳', + '🥵', + '🥶', + '😱', + '😨', + '😰', + '😥', + '😓', + '🤗', + '🤔', + '🤭', + '🤫', + '🤥', + '😶', + ]; static const List gestures = [ - '👍', '👎', '👊', '✊', '🤛', '🤜', '🤞', '✌️', '🤟', '🤘', '👌', '🤌', '🤏', '👈', '👉', '👆', - '👇', '☝️', '👋', '🤚', '🖐️', '✋', '🖖', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪', - ]; + '👍', + '👎', + '👊', + '✊', + '🤛', + '🤜', + '🤞', + '✌️', + '🤟', + '🤘', + '👌', + '🤌', + '🤏', + '👈', + '👉', + '👆', + '👇', + '☝️', + '👋', + '🤚', + '🖐️', + '✋', + '🖖', + '👏', + '🙌', + '👐', + '🤲', + '🤝', + '🙏', + '✍️', + '💅', + '🤳', + '💪', + ]; static const List hearts = [ - '❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❤️‍🔥', '❤️‍🩹', '💕', '💞', '💓', '💗', - '💖', '💘', '💝', '💟', '💌', '💢', '💥', '💫', '💦', '💨', '🕳️', '💬', '👁️‍🗨️', '🗨️', '🗯️', '💭', - ]; + '❤️', + '🧡', + '💛', + '💚', + '💙', + '💜', + '🖤', + '🤍', + '🤎', + '💔', + '❤️‍🔥', + '❤️‍🩹', + '💕', + '💞', + '💓', + '💗', + '💖', + '💘', + '💝', + '💟', + '💌', + '💢', + '💥', + '💫', + '💦', + '💨', + '🕳️', + '💬', + '👁️‍🗨️', + '🗨️', + '🗯️', + '💭', + ]; static const List objects = [ - '🎉', '🎊', '🎈', '🎁', '🎀', '🪅', '🪆', '🏆', '🥇', '🥈', '🥉', '⚽', '⚾', '🥎', '🏀', '🏐', - '🏈', '🏉', '🎾', '🥏', '🎳', '🏏', '🏑', '🏒', '🥍', '🏓', '🏸', '🥊', '🥋', '🥅', '⛳', '🔥', - '⭐', '🌟', '✨', '⚡', '💡', '🔦', '🏮', '🪔', '📱', '💻', '⌚', '📷', '📺', '📻', '🎵', '🎶', '🚀', - ]; + '🎉', + '🎊', + '🎈', + '🎁', + '🎀', + '🪅', + '🪆', + '🏆', + '🥇', + '🥈', + '🥉', + '⚽', + '⚾', + '🥎', + '🏀', + '🏐', + '🏈', + '🏉', + '🎾', + '🥏', + '🎳', + '🏏', + '🏑', + '🏒', + '🥍', + '🏓', + '🏸', + '🥊', + '🥋', + '🥅', + '⛳', + '🔥', + '⭐', + '🌟', + '✨', + '⚡', + '💡', + '🔦', + '🏮', + '🪔', + '📱', + '💻', + '⌚', + '📷', + '📺', + '📻', + '🎵', + '🎶', + '🚀', + ]; Map> _emojiCategories(AppLocalizations l10n) { return { @@ -60,7 +224,10 @@ class EmojiPicker extends StatelessWidget { children: [ Text( l10n.chat_addReaction, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), IconButton( icon: const Icon(Icons.close), @@ -83,7 +250,9 @@ class EmojiPicker extends StatelessWidget { child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, + color: Theme.of( + context, + ).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(8), ), child: Text( @@ -114,11 +283,12 @@ class EmojiPicker extends StatelessWidget { .map( (emojis) => GridView.builder( padding: const EdgeInsets.all(8), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 8, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + ), itemCount: emojis.length, itemBuilder: (context, index) => InkWell( onTap: () { diff --git a/lib/widgets/empty_state.dart b/lib/widgets/empty_state.dart index 9ac28bb..172c9a4 100644 --- a/lib/widgets/empty_state.dart +++ b/lib/widgets/empty_state.dart @@ -23,10 +23,7 @@ class EmptyState extends StatelessWidget { children: [ Icon(icon, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), - Text( - title, - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), + Text(title, style: TextStyle(fontSize: 16, color: Colors.grey[600])), if (subtitle != null) ...[ const SizedBox(height: 8), Text( @@ -35,10 +32,7 @@ class EmptyState extends StatelessWidget { textAlign: TextAlign.center, ), ], - if (action != null) ...[ - const SizedBox(height: 24), - action!, - ], + if (action != null) ...[const SizedBox(height: 24), action!], ], ), ); diff --git a/lib/widgets/gif_picker.dart b/lib/widgets/gif_picker.dart index df0a6f7..9c56951 100644 --- a/lib/widgets/gif_picker.dart +++ b/lib/widgets/gif_picker.dart @@ -7,10 +7,7 @@ import '../l10n/l10n.dart'; class GifPicker extends StatefulWidget { final Function(String gifId) onGifSelected; - const GifPicker({ - super.key, - required this.onGifSelected, - }); + const GifPicker({super.key, required this.onGifSelected}); @override State createState() => _GifPickerState(); @@ -45,11 +42,13 @@ class _GifPickerState extends State { }); try { - final response = await http.get( - Uri.parse( - 'https://api.giphy.com/v1/gifs/trending?api_key=$_giphyApiKey&limit=25&rating=g', - ), - ).timeout(const Duration(seconds: 10)); + final response = await http + .get( + Uri.parse( + 'https://api.giphy.com/v1/gifs/trending?api_key=$_giphyApiKey&limit=25&rating=g', + ), + ) + .timeout(const Duration(seconds: 10)); if (response.statusCode == 200) { final data = json.decode(response.body); @@ -85,11 +84,13 @@ class _GifPickerState extends State { }); try { - final response = await http.get( - Uri.parse( - 'https://api.giphy.com/v1/gifs/search?api_key=$_giphyApiKey&q=${Uri.encodeComponent(query)}&limit=25&rating=g', - ), - ).timeout(const Duration(seconds: 10)); + final response = await http + .get( + Uri.parse( + 'https://api.giphy.com/v1/gifs/search?api_key=$_giphyApiKey&q=${Uri.encodeComponent(query)}&limit=25&rating=g', + ), + ) + .timeout(const Duration(seconds: 10)); if (response.statusCode == 200) { final data = json.decode(response.body); @@ -127,7 +128,10 @@ class _GifPickerState extends State { const SizedBox(width: 8), Text( context.l10n.gifPicker_title, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), const Spacer(), IconButton( @@ -170,18 +174,13 @@ class _GifPickerState extends State { const SizedBox(height: 16), // GIF grid - Expanded( - child: _buildContent(), - ), + Expanded(child: _buildContent()), // Powered by Giphy attribution const SizedBox(height: 8), Text( context.l10n.gifPicker_poweredBy, - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 11, color: Colors.grey[600]), ), ], ), @@ -190,9 +189,7 @@ class _GifPickerState extends State { Widget _buildContent() { if (_isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); + return const Center(child: CircularProgressIndicator()); } if (_error != null) { @@ -244,7 +241,8 @@ class _GifPickerState extends State { itemBuilder: (context, index) { final gif = _gifs[index]; final gifId = gif['id'] as String; - final previewUrl = gif['images']?['fixed_height_small']?['url'] as String?; + final previewUrl = + gif['images']?['fixed_height_small']?['url'] as String?; return GestureDetector( onTap: () { @@ -265,20 +263,16 @@ class _GifPickerState extends State { child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! + loadingProgress.expectedTotalBytes! : null, ), ); }, errorBuilder: (context, error, stackTrace) { - return const Center( - child: Icon(Icons.error_outline), - ); + return const Center(child: Icon(Icons.error_outline)); }, ) - : const Center( - child: Icon(Icons.gif_box), - ), + : const Center(child: Icon(Icons.gif_box)), ), ), ); diff --git a/lib/widgets/jump_to_bottom_button.dart b/lib/widgets/jump_to_bottom_button.dart index 08614f3..3f6d96e 100644 --- a/lib/widgets/jump_to_bottom_button.dart +++ b/lib/widgets/jump_to_bottom_button.dart @@ -4,10 +4,7 @@ import '../helpers/chat_scroll_controller.dart'; class JumpToBottomButton extends StatelessWidget { final ChatScrollController scrollController; - const JumpToBottomButton({ - super.key, - required this.scrollController, - }); + const JumpToBottomButton({super.key, required this.scrollController}); @override Widget build(BuildContext context) { diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 97ca3e7..e9c0d9e 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -1,18 +1,9 @@ import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; -enum ContactSortOption { - lastSeen, - recentMessages, - name, -} +enum ContactSortOption { lastSeen, recentMessages, name } -enum ContactTypeFilter { - all, - users, - repeaters, - rooms, -} +enum ContactTypeFilter { all, users, repeaters, rooms } class SortFilterMenuOption { final int value; @@ -30,10 +21,7 @@ class SortFilterMenuSection { final String title; final List options; - const SortFilterMenuSection({ - required this.title, - required this.options, - }); + const SortFilterMenuSection({required this.title, required this.options}); } class SortFilterMenu extends StatelessWidget { @@ -62,7 +50,9 @@ class SortFilterMenu extends StatelessWidget { color: theme.colorScheme.onSurfaceVariant, fontWeight: FontWeight.w600, ); - final visibleSections = sections.where((section) => section.options.isNotEmpty).toList(); + final visibleSections = sections + .where((section) => section.options.isNotEmpty) + .toList(); final entries = >[]; for (int i = 0; i < visibleSections.length; i++) { final section = visibleSections[i]; diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 00fc083..f47b017 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -10,15 +10,10 @@ import '../services/path_history_service.dart'; import 'path_selection_dialog.dart'; class PathManagementDialog { - static Future show( - BuildContext context, { - required Contact contact, - }) { + static Future show(BuildContext context, {required Contact contact}) { return showDialog( context: context, - builder: (context) => _PathManagementDialog( - contact: contact, - ), + builder: (context) => _PathManagementDialog(contact: contact), ); } } @@ -26,9 +21,7 @@ class PathManagementDialog { class _PathManagementDialog extends StatelessWidget { final Contact contact; - const _PathManagementDialog({ - required this.contact, - }); + const _PathManagementDialog({required this.contact}); Contact _resolveContact(MeshCoreConnector connector) { return connector.contacts.firstWhere( @@ -83,7 +76,9 @@ class _PathManagementDialog extends StatelessWidget { Contact currentContact, ) async { final l10n = context.l10n; - if (currentContact.pathLength > 0 && currentContact.path.isEmpty && connector.isConnected) { + if (currentContact.pathLength > 0 && + currentContact.path.isEmpty && + connector.isConnected) { connector.getContacts(); } @@ -140,13 +135,19 @@ class _PathManagementDialog extends StatelessWidget { if (paths.isNotEmpty) ...[ Text( l10n.chat_recentAckPaths, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), if (paths.length >= 100) ...[ const SizedBox(height: 8), Container( width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), decoration: BoxDecoration( color: Colors.amberAccent, borderRadius: BorderRadius.circular(8), @@ -165,7 +166,9 @@ class _PathManagementDialog extends StatelessWidget { dense: true, leading: CircleAvatar( radius: 16, - backgroundColor: path.wasFloodDiscovery ? Colors.blue : Colors.green, + backgroundColor: path.wasFloodDiscovery + ? Colors.blue + : Colors.green, child: Text( '${path.hopCount}', style: const TextStyle(fontSize: 12), @@ -193,16 +196,27 @@ class _PathManagementDialog extends StatelessWidget { }, ), path.wasFloodDiscovery - ? const Icon(Icons.waves, size: 16, color: Colors.grey) - : const Icon(Icons.route, size: 16, color: Colors.grey), + ? const Icon( + Icons.waves, + size: 16, + color: Colors.grey, + ) + : const Icon( + Icons.route, + size: 16, + color: Colors.grey, + ), ], ), - onLongPress: () => _showFullPathDialog(context, path.pathBytes), + onLongPress: () => + _showFullPathDialog(context, path.pathBytes), onTap: () async { if (path.pathBytes.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(l10n.chat_pathDetailsNotAvailable), + content: Text( + l10n.chat_pathDetailsNotAvailable, + ), duration: const Duration(seconds: 2), ), ); @@ -222,7 +236,9 @@ class _PathManagementDialog extends StatelessWidget { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(l10n.path_usingHopsPath(path.hopCount)), + content: Text( + l10n.path_usingHopsPath(path.hopCount), + ), duration: const Duration(seconds: 2), ), ); @@ -238,7 +254,10 @@ class _PathManagementDialog extends StatelessWidget { const SizedBox(height: 8), Text( l10n.chat_pathActions, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), const SizedBox(height: 8), ListTile( @@ -248,8 +267,14 @@ class _PathManagementDialog extends StatelessWidget { backgroundColor: Colors.purple, child: Icon(Icons.edit_road, size: 16), ), - title: Text(l10n.chat_setCustomPath, style: const TextStyle(fontSize: 14)), - subtitle: Text(l10n.chat_setCustomPathSubtitle, style: const TextStyle(fontSize: 11)), + title: Text( + l10n.chat_setCustomPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + l10n.chat_setCustomPathSubtitle, + style: const TextStyle(fontSize: 11), + ), onTap: () async { await _setCustomPath(context, connector, currentContact); }, @@ -261,8 +286,14 @@ class _PathManagementDialog extends StatelessWidget { backgroundColor: Colors.orange, child: Icon(Icons.clear_all, size: 16), ), - title: Text(l10n.chat_clearPath, style: const TextStyle(fontSize: 14)), - subtitle: Text(l10n.chat_clearPathSubtitle, style: const TextStyle(fontSize: 11)), + title: Text( + l10n.chat_clearPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + l10n.chat_clearPathSubtitle, + style: const TextStyle(fontSize: 11), + ), onTap: () async { await connector.clearContactPath(currentContact); if (!context.mounted) return; @@ -282,10 +313,19 @@ class _PathManagementDialog extends StatelessWidget { backgroundColor: Colors.blue, child: Icon(Icons.waves, size: 16), ), - title: Text(l10n.chat_forceFloodMode, style: const TextStyle(fontSize: 14)), - subtitle: Text(l10n.chat_floodModeSubtitle, style: const TextStyle(fontSize: 11)), + title: Text( + l10n.chat_forceFloodMode, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + l10n.chat_floodModeSubtitle, + style: const TextStyle(fontSize: 11), + ), onTap: () async { - await connector.setPathOverride(currentContact, pathLen: -1); + await connector.setPathOverride( + currentContact, + pathLen: -1, + ); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart index 5a5aa59..4e6cfe5 100644 --- a/lib/widgets/path_selection_dialog.dart +++ b/lib/widgets/path_selection_dialog.dart @@ -70,12 +70,15 @@ class _PathSelectionDialogState extends State { } void _updateTextFromContacts() { - final pathParts = _selectedContacts.map((contact) { - if (contact.publicKeyHex.length >= 2) { - return contact.publicKeyHex.substring(0, 2); - } - return ''; - }).where((s) => s.isNotEmpty).toList(); + final pathParts = _selectedContacts + .map((contact) { + if (contact.publicKeyHex.length >= 2) { + return contact.publicKeyHex.substring(0, 2); + } + return ''; + }) + .where((s) => s.isNotEmpty) + .toList(); _controller.text = pathParts.join(','); } @@ -107,7 +110,11 @@ class _PathSelectionDialogState extends State { } // Parse comma-separated hex prefixes - final pathIds = path.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(); + final pathIds = path + .split(',') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); final pathBytesList = []; final invalidPrefixes = []; @@ -132,7 +139,9 @@ class _PathSelectionDialogState extends State { if (invalidPrefixes.isNotEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(l10n.path_invalidHexPrefixes(invalidPrefixes.join(", "))), + content: Text( + l10n.path_invalidHexPrefixes(invalidPrefixes.join(", ")), + ), duration: const Duration(seconds: 3), backgroundColor: Colors.red, ), @@ -180,7 +189,10 @@ class _PathSelectionDialogState extends State { children: [ Text( l10n.path_currentPathLabel, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), const Spacer(), if (widget.onRefresh != null) @@ -225,7 +237,10 @@ class _PathSelectionDialogState extends State { children: [ Text( l10n.path_selectFromContacts, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), const Spacer(), if (_selectedContacts.isNotEmpty) @@ -242,7 +257,11 @@ class _PathSelectionDialogState extends State { padding: const EdgeInsets.all(16.0), child: Column( children: [ - const Icon(Icons.info_outline, size: 48, color: Colors.grey), + const Icon( + Icons.info_outline, + size: 48, + color: Colors.grey, + ), const SizedBox(height: 16), Text( l10n.path_noRepeatersFound, @@ -252,7 +271,10 @@ class _PathSelectionDialogState extends State { const SizedBox(height: 8), Text( l10n.path_customPathsRequire, - style: const TextStyle(fontSize: 12, color: Colors.grey), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), textAlign: TextAlign.center, ), ], @@ -275,20 +297,30 @@ class _PathSelectionDialogState extends State { radius: 16, backgroundColor: isSelected ? Colors.green - : (contact.type == 2 ? Colors.blue : Colors.purple), + : (contact.type == 2 + ? Colors.blue + : Colors.purple), child: Icon( - contact.type == 2 ? Icons.router : Icons.meeting_room, + contact.type == 2 + ? Icons.router + : Icons.meeting_room, size: 16, color: Colors.white, ), ), - title: Text(contact.name, style: const TextStyle(fontSize: 14)), + title: Text( + contact.name, + style: const TextStyle(fontSize: 14), + ), subtitle: Text( '${contact.typeLabel} • ${contact.publicKeyHex.substring(0, 2)}', style: const TextStyle(fontSize: 10), ), trailing: isSelected - ? const Icon(Icons.check_circle, color: Colors.green) + ? const Icon( + Icons.check_circle, + color: Colors.green, + ) : const Icon(Icons.add_circle_outline), onTap: () => _toggleContact(contact), ); diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart index 958258b..7294c86 100644 --- a/lib/widgets/path_trace_dialog.dart +++ b/lib/widgets/path_trace_dialog.dart @@ -8,13 +8,9 @@ import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; import '../widgets/snr_indicator.dart'; import '../l10n/l10n.dart'; -class PathTraceDialog extends StatefulWidget { - const PathTraceDialog({ - super.key, - required this.title, - required this.path, - }); +class PathTraceDialog extends StatefulWidget { + const PathTraceDialog({super.key, required this.title, required this.path}); final String title; final Uint8List path; @@ -31,7 +27,7 @@ class _PathTraceDialogState extends State { bool _failed2Loaded = false; bool _hasData = false; Uint8List _pathData = Uint8List(0); - Uint8List _snrData = Uint8List(0) ; + Uint8List _snrData = Uint8List(0); Map _pathContacts = {}; @override @@ -49,13 +45,13 @@ class _PathTraceDialogState extends State { } Future _doPathTrace() async { - if(mounted) { + if (mounted) { setState(() { _isLoading = true; _failed2Loaded = false; }); } - + final connector = Provider.of(context, listen: false); final frame = buildTraceReq( DateTime.now().millisecondsSinceEpoch ~/ 1000, @@ -92,18 +88,19 @@ class _PathTraceDialogState extends State { } // Check if it's a binary response - if (code == pushCodeTraceData && listEquals(frame.sublist(4, 8), tagData)) { + if (code == pushCodeTraceData && + listEquals(frame.sublist(4, 8), tagData)) { _timeoutTimer?.cancel(); if (!mounted) return; frameBuffer.skipBytes(3); //reserved + path length + flag - if(listEquals(frameBuffer.readBytes(4), tagData)){ + if (listEquals(frameBuffer.readBytes(4), tagData)) { _handleTraceResponse(frame); } } }); } - Future _handleTraceResponse(Uint8List frame)async { + Future _handleTraceResponse(Uint8List frame) async { final connector = Provider.of(context, listen: false); final buffer = BufferReader(frame); @@ -116,9 +113,7 @@ class _PathTraceDialogState extends State { Map pathContacts = {}; - connector.contacts.where((c) => c.type != advTypeChat).forEach(( - repeater, - ) { + connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) { for (var neighbourData in pathData) { if (listEquals( repeater.publicKey.sublist(0, 1), @@ -143,21 +138,26 @@ class _PathTraceDialogState extends State { if (index == 0) { return context.l10n.pathTrace_you; } else { - return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}"; + return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? + "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}"; } } else { - return _pathContacts[_pathData[index-1]]?.name ?? "0x${_pathData[index-1].toRadixString(16).toUpperCase()}"; + return _pathContacts[_pathData[index - 1]]?.name ?? + "0x${_pathData[index - 1].toRadixString(16).toUpperCase()}"; } } + String formatDirectionSubText(int index) { if (index == 0 || index == _snrData.length - 1) { if (index == 0) { - return _pathContacts[_pathData[0]]?.name ?? "0x${_pathData[0].toRadixString(16).toUpperCase()}"; + return _pathContacts[_pathData[0]]?.name ?? + "0x${_pathData[0].toRadixString(16).toUpperCase()}"; } else { return context.l10n.pathTrace_you; } } else { - return _pathContacts[_pathData[index]]?.name ?? "0x${_pathData[index].toRadixString(16).toUpperCase()}"; + return _pathContacts[_pathData[index]]?.name ?? + "0x${_pathData[index].toRadixString(16).toUpperCase()}"; } } @@ -165,47 +165,61 @@ class _PathTraceDialogState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return AlertDialog( - title: Column( children: [ - FittedBox(fit: BoxFit.scaleDown, child: Text(widget.title, style: const TextStyle(fontSize: 24))), - if(_failed2Loaded) - Text(l10n.pathTrace_failed, style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.error),), + title: Column( + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text(widget.title, style: const TextStyle(fontSize: 24)), + ), + if (_failed2Loaded) + Text( + l10n.pathTrace_failed, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.error, + ), + ), ], ), content: SafeArea( child: RefreshIndicator( onRefresh: _doPathTrace, child: !_hasData - ? Center( - child: Text(l10n.pathTrace_notAvailable), - ) - : ListView.builder( - itemCount: _snrData.length, - itemBuilder: (context, index) { - return Column( - children: [ - ListTile( - leading: index >= _snrData.length / 2 ? Icon(Icons.call_received) : Icon(Icons.call_made), - title: Text( - formatDirectionText(index), style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - formatDirectionSubText(index), - style: const TextStyle(fontSize: 14), - ), - trailing: SNRIcon(snr: _snrData[index].toSigned(8) / 4.0), - onTap: () { - // Handle item tap - }, + ? Center(child: Text(l10n.pathTrace_notAvailable)) + : ListView.builder( + itemCount: _snrData.length, + itemBuilder: (context, index) { + return Column( + children: [ + ListTile( + leading: index >= _snrData.length / 2 + ? Icon(Icons.call_received) + : Icon(Icons.call_made), + title: Text( + formatDirectionText(index), + style: const TextStyle(fontSize: 14), ), - if (index < _snrData.length - 1) const Divider(height: 0.0), - ], - ); - }, - ), + subtitle: Text( + formatDirectionSubText(index), + style: const TextStyle(fontSize: 14), + ), + trailing: SNRIcon( + snr: _snrData[index].toSigned(8) / 4.0, + ), + onTap: () { + // Handle item tap + }, + ), + if (index < _snrData.length - 1) + const Divider(height: 0.0), + ], + ); + }, + ), ), ), actions: [ - IconButton( + IconButton( icon: _isLoading ? const SizedBox( width: 20, diff --git a/lib/widgets/qr_code_display.dart b/lib/widgets/qr_code_display.dart index 4d96ebe..e8f4795 100644 --- a/lib/widgets/qr_code_display.dart +++ b/lib/widgets/qr_code_display.dart @@ -121,10 +121,7 @@ class QrCodeDisplay extends StatelessWidget { size: size, backgroundColor: bgColor, errorCorrectionLevel: errorCorrectionLevel, - eyeStyle: QrEyeStyle( - eyeShape: QrEyeShape.square, - color: fgColor, - ), + eyeStyle: QrEyeStyle(eyeShape: QrEyeShape.square, color: fgColor), dataModuleStyle: QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: fgColor, @@ -143,10 +140,7 @@ class QrCodeDisplay extends StatelessWidget { backgroundColor: bgColor, // Use higher error correction when embedding image errorCorrectionLevel: QrErrorCorrectLevel.H, - eyeStyle: QrEyeStyle( - eyeShape: QrEyeShape.square, - color: fgColor, - ), + eyeStyle: QrEyeStyle(eyeShape: QrEyeShape.square, color: fgColor), dataModuleStyle: QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: fgColor, diff --git a/lib/widgets/qr_scanner_widget.dart b/lib/widgets/qr_scanner_widget.dart index e328b6d..4dc2ee5 100644 --- a/lib/widgets/qr_scanner_widget.dart +++ b/lib/widgets/qr_scanner_widget.dart @@ -215,10 +215,7 @@ class _QrScannerWidgetState extends State ), child: Text( widget.instructions!, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - ), + style: const TextStyle(color: Colors.white, fontSize: 14), textAlign: TextAlign.center, ), ), @@ -274,7 +271,8 @@ class _QrScannerWidgetState extends State switch (error.errorCode) { case MobileScannerErrorCode.permissionDenied: - message = 'Camera permission denied.\nPlease enable camera access in settings.'; + message = + 'Camera permission denied.\nPlease enable camera access in settings.'; icon = Icons.no_photography; break; case MobileScannerErrorCode.unsupported: @@ -282,7 +280,8 @@ class _QrScannerWidgetState extends State icon = Icons.videocam_off; break; default: - message = 'Failed to start camera.\n${error.errorDetails?.message ?? ''}'; + message = + 'Failed to start camera.\n${error.errorDetails?.message ?? ''}'; icon = Icons.error_outline; } @@ -297,10 +296,7 @@ class _QrScannerWidgetState extends State Text( message, textAlign: TextAlign.center, - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), + style: TextStyle(color: Colors.grey[600], fontSize: 16), ), ], ), diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index 1f767f6..b550cc2 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -44,8 +44,9 @@ class _RepeaterLoginDialogState extends State { } Future _loadSavedPassword() async { - final savedPassword = - await _storage.getRepeaterPassword(widget.repeater.publicKeyHex); + final savedPassword = await _storage.getRepeaterPassword( + widget.repeater.publicKeyHex, + ); if (savedPassword != null) { setState(() { _passwordController.text = savedPassword; @@ -102,12 +103,10 @@ class _RepeaterLoginDialogState extends State { ); final timeoutSeconds = (timeoutMs / 1000).ceil(); final timeout = Duration(milliseconds: timeoutMs); - final selectionLabel = - selection.useFlood ? 'flood' : '${selection.hopCount} hops'; - appLogger.info( - 'Login routing: $selectionLabel', - tag: 'RepeaterLogin', - ); + final selectionLabel = selection.useFlood + ? 'flood' + : '${selection.hopCount} hops'; + appLogger.info('Login routing: $selectionLabel', tag: 'RepeaterLogin'); bool? loginResult; for (int attempt = 0; attempt < _maxAttempts; attempt++) { if (!mounted) return; @@ -119,9 +118,7 @@ class _RepeaterLoginDialogState extends State { 'Sending login attempt ${attempt + 1}/$_maxAttempts', tag: 'RepeaterLogin', ); - await _connector.sendFrame( - loginFrame, - ); + await _connector.sendFrame(loginFrame); loginResult = await _awaitLoginResponse(timeout); if (loginResult == true) { @@ -171,7 +168,9 @@ class _RepeaterLoginDialogState extends State { // Save password if requested if (_savePassword) { await _storage.saveRepeaterPassword( - widget.repeater.publicKeyHex, password); + widget.repeater.publicKeyHex, + password, + ); } else { // Remove saved password if user unchecked the box await _storage.removeRepeaterPassword(widget.repeater.publicKeyHex); @@ -269,152 +268,183 @@ class _RepeaterLoginDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.login_repeaterDescription, - style: const TextStyle(fontSize: 14), - ), - const SizedBox(height: 16), - if (_loginError != null) ...[ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.error, size: 18, color: Theme.of(context).colorScheme.error), - const SizedBox(width: 8), - Expanded( - child: Text( - _loginError!, - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: 13, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - ], - TextField( - controller: _passwordController, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: l10n.login_password, - hintText: l10n.login_enterPassword, - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility - : Icons.visibility_off, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - onChanged: (_) { - if (_loginError != null && mounted) { - setState(() { - _loginError = null; - }); - } - }, - onSubmitted: (_) => _handleLogin(), - autofocus: !(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) && - _passwordController.text.isEmpty, - ), - const SizedBox(height: 12), - CheckboxListTile( - value: _savePassword, - onChanged: (value) { - setState(() { - _savePassword = value ?? false; - }); - }, - title: Text( - l10n.login_savePassword, + Text( + l10n.login_repeaterDescription, style: const TextStyle(fontSize: 14), ), - subtitle: Text( - l10n.login_savePasswordSubtitle, - style: const TextStyle(fontSize: 12), - ), - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - ), - const Divider(), - Row( - children: [ - Text( - l10n.login_routing, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), - ), - const Spacer(), - PopupMenuButton( - icon: Icon(isFloodMode ? Icons.waves : Icons.route), - tooltip: l10n.login_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(repeater, pathLen: -1); - } else { - await connector.setPathOverride(repeater, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), - const SizedBox(width: 8), - Text( - l10n.login_autoUseSavedPath, - style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, - ), - ), - ], - ), + const SizedBox(height: 16), + if (_loginError != null) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.error, + size: 18, + color: Theme.of(context).colorScheme.error, ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), - const SizedBox(width: 8), - Text( - l10n.login_forceFloodMode, - style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, - ), - ), - ], + const SizedBox(width: 8), + Expanded( + child: Text( + _loginError!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 13, + ), ), ), ], ), + const SizedBox(height: 12), ], - ), - const SizedBox(height: 4), - Text( - repeater.pathLabel, - style: const TextStyle(fontSize: 11, color: Colors.grey), - ), - const SizedBox(height: 8), - Align( - alignment: Alignment.centerLeft, - child: TextButton.icon( - onPressed: () => PathManagementDialog.show(context, contact: repeater), - icon: const Icon(Icons.timeline, size: 18), - label: Text(l10n.login_managePaths), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: l10n.login_password, + hintText: l10n.login_enterPassword, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, + ), + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), + ), + onChanged: (_) { + if (_loginError != null && mounted) { + setState(() { + _loginError = null; + }); + } + }, + onSubmitted: (_) => _handleLogin(), + autofocus: + !(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) && + _passwordController.text.isEmpty, ), - ), - ], + const SizedBox(height: 12), + CheckboxListTile( + value: _savePassword, + onChanged: (value) { + setState(() { + _savePassword = value ?? false; + }); + }, + title: Text( + l10n.login_savePassword, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + l10n.login_savePasswordSubtitle, + style: const TextStyle(fontSize: 12), + ), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + const Divider(), + Row( + children: [ + Text( + l10n.login_routing, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + PopupMenuButton( + icon: Icon(isFloodMode ? Icons.waves : Icons.route), + tooltip: l10n.login_routingMode, + onSelected: (mode) async { + if (mode == 'flood') { + await connector.setPathOverride( + repeater, + pathLen: -1, + ); + } else { + await connector.setPathOverride( + repeater, + pathLen: null, + ); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'auto', + child: Row( + children: [ + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), + const SizedBox(width: 8), + Text( + l10n.login_autoUseSavedPath, + style: TextStyle( + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'flood', + child: Row( + children: [ + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), + const SizedBox(width: 8), + Text( + l10n.login_forceFloodMode, + style: TextStyle( + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + ], + ), + ], + ), + const SizedBox(height: 4), + Text( + repeater.pathLabel, + style: const TextStyle(fontSize: 11, color: Colors.grey), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + onPressed: () => + PathManagementDialog.show(context, contact: repeater), + icon: const Icon(Icons.timeline, size: 18), + label: Text(l10n.login_managePaths), + ), + ), + ], + ), ), - ), actions: [ TextButton( onPressed: () => Navigator.pop(context), diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index 1d2554d..cba7bec 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -15,11 +15,7 @@ class RoomLoginDialog extends StatefulWidget { final Contact room; final Function(String password) onLogin; - const RoomLoginDialog({ - super.key, - required this.room, - required this.onLogin, - }); + const RoomLoginDialog({super.key, required this.room, required this.onLogin}); @override State createState() => _RoomLoginDialogState(); @@ -43,8 +39,9 @@ class _RoomLoginDialogState extends State { } Future _loadSavedPassword() async { - final savedPassword = - await _storage.getRepeaterPassword(widget.room.publicKeyHex); + final savedPassword = await _storage.getRepeaterPassword( + widget.room.publicKeyHex, + ); if (savedPassword != null) { setState(() { _passwordController.text = savedPassword; @@ -100,12 +97,10 @@ class _RoomLoginDialogState extends State { ); final timeoutSeconds = (timeoutMs / 1000).ceil(); final timeout = Duration(milliseconds: timeoutMs); - final selectionLabel = - selection.useFlood ? 'flood' : '${selection.hopCount} hops'; - appLogger.info( - 'Login routing: $selectionLabel', - tag: 'RoomLogin', - ); + final selectionLabel = selection.useFlood + ? 'flood' + : '${selection.hopCount} hops'; + appLogger.info('Login routing: $selectionLabel', tag: 'RoomLogin'); bool? loginResult; for (int attempt = 0; attempt < _maxAttempts; attempt++) { if (!mounted) return; @@ -117,23 +112,15 @@ class _RoomLoginDialogState extends State { 'Sending login attempt ${attempt + 1}/$_maxAttempts', tag: 'RoomLogin', ); - await _connector.sendFrame( - loginFrame, - ); + await _connector.sendFrame(loginFrame); loginResult = await _awaitLoginResponse(timeout); if (loginResult == true) { - appLogger.info( - 'Login succeeded for ${room.name}', - tag: 'RoomLogin', - ); + appLogger.info('Login succeeded for ${room.name}', tag: 'RoomLogin'); break; } if (loginResult == false) { - appLogger.warn( - 'Login failed for ${room.name}', - tag: 'RoomLogin', - ); + appLogger.warn('Login failed for ${room.name}', tag: 'RoomLogin'); throw Exception('Wrong password or node is unreachable'); } appLogger.warn( @@ -143,10 +130,7 @@ class _RoomLoginDialogState extends State { } if (loginResult == null) { - appLogger.warn( - 'Login timed out for ${room.name}', - tag: 'RoomLogin', - ); + appLogger.warn('Login timed out for ${room.name}', tag: 'RoomLogin'); } if (loginResult == true) { @@ -162,8 +146,7 @@ class _RoomLoginDialogState extends State { // If we got a response, login succeeded // Save password if requested if (_savePassword) { - await _storage.saveRepeaterPassword( - widget.room.publicKeyHex, password); + await _storage.saveRepeaterPassword(widget.room.publicKeyHex, password); } else { // Remove saved password if user unchecked the box await _storage.removeRepeaterPassword(widget.room.publicKeyHex); @@ -175,10 +158,7 @@ class _RoomLoginDialogState extends State { } } catch (e) { final room = _resolveRepeater(_connector); - appLogger.warn( - 'Login error for ${room.name}: $e', - tag: 'RoomLogin', - ); + appLogger.warn('Login error for ${room.name}: $e', tag: 'RoomLogin'); if (mounted) { setState(() { _isLoggingIn = false; @@ -262,130 +242,157 @@ class _RoomLoginDialogState extends State { ), ) : SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.login_roomDescription, - style: const TextStyle(fontSize: 14), - ), - const SizedBox(height: 16), - TextField( - controller: _passwordController, - obscureText: _obscurePassword, - decoration: InputDecoration( - labelText: l10n.login_password, - hintText: l10n.login_enterPassword, - border: const OutlineInputBorder(), - prefixIcon: const Icon(Icons.lock), - suffixIcon: IconButton( - icon: Icon( - _obscurePassword - ? Icons.visibility - : Icons.visibility_off, - ), - onPressed: () { - setState(() { - _obscurePassword = !_obscurePassword; - }); - }, - ), - ), - onSubmitted: (_) => _handleLogin(), - autofocus: !(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) && - _passwordController.text.isEmpty, - ), - const SizedBox(height: 12), - CheckboxListTile( - value: _savePassword, - onChanged: (value) { - setState(() { - _savePassword = value ?? false; - }); - }, - title: Text( - l10n.login_savePassword, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.login_roomDescription, style: const TextStyle(fontSize: 14), ), - subtitle: Text( - l10n.login_savePasswordSubtitle, - style: const TextStyle(fontSize: 12), - ), - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - ), - const Divider(), - Row( - children: [ - Text( - l10n.login_routing, - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), - ), - const Spacer(), - PopupMenuButton( - icon: Icon(isFloodMode ? Icons.waves : Icons.route), - tooltip: l10n.login_routingMode, - onSelected: (mode) async { - if (mode == 'flood') { - await connector.setPathOverride(repeater, pathLen: -1); - } else { - await connector.setPathOverride(repeater, pathLen: null); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'auto', - child: Row( - children: [ - Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null), - const SizedBox(width: 8), - Text( - l10n.login_autoUseSavedPath, - style: TextStyle( - fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal, - ), - ), - ], - ), + const SizedBox(height: 16), + TextField( + controller: _passwordController, + obscureText: _obscurePassword, + decoration: InputDecoration( + labelText: l10n.login_password, + hintText: l10n.login_enterPassword, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.lock), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword + ? Icons.visibility + : Icons.visibility_off, ), - PopupMenuItem( - value: 'flood', - child: Row( - children: [ - Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null), - const SizedBox(width: 8), - Text( - l10n.login_forceFloodMode, - style: TextStyle( - fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal, - ), - ), - ], - ), - ), - ], + onPressed: () { + setState(() { + _obscurePassword = !_obscurePassword; + }); + }, + ), ), - ], - ), - const SizedBox(height: 4), - Text( - repeater.pathLabel, - style: const TextStyle(fontSize: 11, color: Colors.grey), - ), - const SizedBox(height: 8), - Align( - alignment: Alignment.centerLeft, - child: TextButton.icon( - onPressed: () => PathManagementDialog.show(context, contact: repeater), - icon: const Icon(Icons.timeline, size: 18), - label: Text(l10n.login_managePaths), + onSubmitted: (_) => _handleLogin(), + autofocus: + !(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) && + _passwordController.text.isEmpty, ), - ), - ], + const SizedBox(height: 12), + CheckboxListTile( + value: _savePassword, + onChanged: (value) { + setState(() { + _savePassword = value ?? false; + }); + }, + title: Text( + l10n.login_savePassword, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + l10n.login_savePasswordSubtitle, + style: const TextStyle(fontSize: 12), + ), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + const Divider(), + Row( + children: [ + Text( + l10n.login_routing, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + PopupMenuButton( + icon: Icon(isFloodMode ? Icons.waves : Icons.route), + tooltip: l10n.login_routingMode, + onSelected: (mode) async { + if (mode == 'flood') { + await connector.setPathOverride( + repeater, + pathLen: -1, + ); + } else { + await connector.setPathOverride( + repeater, + pathLen: null, + ); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'auto', + child: Row( + children: [ + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), + const SizedBox(width: 8), + Text( + l10n.login_autoUseSavedPath, + style: TextStyle( + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'flood', + child: Row( + children: [ + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), + const SizedBox(width: 8), + Text( + l10n.login_forceFloodMode, + style: TextStyle( + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + ], + ), + ], + ), + const SizedBox(height: 4), + Text( + repeater.pathLabel, + style: const TextStyle(fontSize: 11, color: Colors.grey), + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + onPressed: () => + PathManagementDialog.show(context, contact: repeater), + icon: const Icon(Icons.timeline, size: 18), + label: Text(l10n.login_managePaths), + ), + ), + ], + ), ), - ), actions: [ TextButton( onPressed: () => Navigator.pop(context), diff --git a/lib/widgets/unread_badge.dart b/lib/widgets/unread_badge.dart index 92b7c37..37db11a 100644 --- a/lib/widgets/unread_badge.dart +++ b/lib/widgets/unread_badge.dart @@ -3,10 +3,7 @@ import 'package:flutter/material.dart'; class UnreadBadge extends StatelessWidget { final int count; - const UnreadBadge({ - super.key, - required this.count, - }); + const UnreadBadge({super.key, required this.count}); @override Widget build(BuildContext context) { diff --git a/test/reaction_helper_test.dart b/test/reaction_helper_test.dart index d2c70b5..2f4502e 100644 --- a/test/reaction_helper_test.dart +++ b/test/reaction_helper_test.dart @@ -7,30 +7,50 @@ void main() { group('reactionEmojis', () { test('should contain all emoji categories', () { final emojis = ReactionHelper.reactionEmojis; - + // Should contain quickEmojis for (final emoji in EmojiPicker.quickEmojis) { - expect(emojis.contains(emoji), isTrue, reason: 'Missing quick emoji: $emoji'); + expect( + emojis.contains(emoji), + isTrue, + reason: 'Missing quick emoji: $emoji', + ); } - + // Should contain smileys for (final emoji in EmojiPicker.smileys) { - expect(emojis.contains(emoji), isTrue, reason: 'Missing smiley: $emoji'); + expect( + emojis.contains(emoji), + isTrue, + reason: 'Missing smiley: $emoji', + ); } - + // Should contain gestures for (final emoji in EmojiPicker.gestures) { - expect(emojis.contains(emoji), isTrue, reason: 'Missing gesture: $emoji'); + expect( + emojis.contains(emoji), + isTrue, + reason: 'Missing gesture: $emoji', + ); } - + // Should contain hearts for (final emoji in EmojiPicker.hearts) { - expect(emojis.contains(emoji), isTrue, reason: 'Missing heart: $emoji'); + expect( + emojis.contains(emoji), + isTrue, + reason: 'Missing heart: $emoji', + ); } - + // Should contain objects for (final emoji in EmojiPicker.objects) { - expect(emojis.contains(emoji), isTrue, reason: 'Missing object: $emoji'); + expect( + emojis.contains(emoji), + isTrue, + reason: 'Missing object: $emoji', + ); } }); @@ -43,7 +63,7 @@ void main() { test('should return 2-char hex for valid emoji', () { // First emoji (thumbs up) should be index 0 expect(ReactionHelper.emojiToIndex('👍'), equals('00')); - + // Second emoji (heart) should be index 1 expect(ReactionHelper.emojiToIndex('❤️'), equals('01')); }); @@ -67,7 +87,10 @@ void main() { }); test('should return null for invalid index', () { - expect(ReactionHelper.indexToEmoji('ff'), isNull); // Index 255, out of range + expect( + ReactionHelper.indexToEmoji('ff'), + isNull, + ); // Index 255, out of range expect(ReactionHelper.indexToEmoji('zz'), isNull); // Invalid hex expect(ReactionHelper.indexToEmoji(''), isNull); // Empty string // Note: indexToEmoji parses any valid hex; length validation is done by parseReaction's regex @@ -86,78 +109,158 @@ void main() { final emoji = ReactionHelper.reactionEmojis[i]; final index = ReactionHelper.emojiToIndex(emoji); expect(index, isNotNull, reason: 'emojiToIndex failed for $emoji'); - + final decoded = ReactionHelper.indexToEmoji(index!); - expect(decoded, equals(emoji), reason: 'Round-trip failed for $emoji (index $index)'); + expect( + decoded, + equals(emoji), + reason: 'Round-trip failed for $emoji (index $index)', + ); } }); }); group('computeReactionHash', () { test('should return 4-char hex hash', () { - final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello world'); + final hash = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello world', + ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); test('should be deterministic', () { - final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); - final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + final hash1 = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello', + ); + final hash2 = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello', + ); expect(hash1, equals(hash2)); }); test('should differ for different inputs', () { - final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); - final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Bob', 'Hello'); - final hash3 = ReactionHelper.computeReactionHash(1234567891, 'Alice', 'Hello'); - final hash4 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'World'); - + final hash1 = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello', + ); + final hash2 = ReactionHelper.computeReactionHash( + 1234567890, + 'Bob', + 'Hello', + ); + final hash3 = ReactionHelper.computeReactionHash( + 1234567891, + 'Alice', + 'Hello', + ); + final hash4 = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'World', + ); + expect(hash1, isNot(equals(hash2))); // Different sender expect(hash1, isNot(equals(hash3))); // Different timestamp expect(hash1, isNot(equals(hash4))); // Different text }); test('should use first 5 chars of text', () { - final hash1 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello world'); - final hash2 = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello there'); + final hash1 = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello world', + ); + final hash2 = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello there', + ); expect(hash1, equals(hash2)); // Same first 5 chars }); test('should handle short text', () { - final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hi'); + final hash = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hi', + ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); test('should handle empty text', () { - final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', ''); + final hash = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + '', + ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); }); group('computeReactionHash with null sender (1:1 chats)', () { test('should return 4-char hex hash', () { - final hash = ReactionHelper.computeReactionHash(1234567890, null, 'Hello world'); + final hash = ReactionHelper.computeReactionHash( + 1234567890, + null, + 'Hello world', + ); expect(hash, matches(RegExp(r'^[0-9a-f]{4}$'))); }); test('should be deterministic', () { - final hash1 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); - final hash2 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); + final hash1 = ReactionHelper.computeReactionHash( + 1234567890, + null, + 'Hello', + ); + final hash2 = ReactionHelper.computeReactionHash( + 1234567890, + null, + 'Hello', + ); expect(hash1, equals(hash2)); }); test('should differ for different inputs', () { - final hash1 = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); - final hash2 = ReactionHelper.computeReactionHash(1234567891, null, 'Hello'); - final hash3 = ReactionHelper.computeReactionHash(1234567890, null, 'World'); - + final hash1 = ReactionHelper.computeReactionHash( + 1234567890, + null, + 'Hello', + ); + final hash2 = ReactionHelper.computeReactionHash( + 1234567891, + null, + 'Hello', + ); + final hash3 = ReactionHelper.computeReactionHash( + 1234567890, + null, + 'World', + ); + expect(hash1, isNot(equals(hash2))); // Different timestamp expect(hash1, isNot(equals(hash3))); // Different text }); test('should differ from hash with sender name', () { // Null sender hash doesn't include sender, so should differ - final nullSenderHash = ReactionHelper.computeReactionHash(1234567890, null, 'Hello'); - final withSenderHash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + final nullSenderHash = ReactionHelper.computeReactionHash( + 1234567890, + null, + 'Hello', + ); + final withSenderHash = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello', + ); expect(nullSenderHash, isNot(equals(withSenderHash))); }); @@ -167,13 +270,21 @@ void main() { // Bob computes hash the same way Alice's app will match it const timestamp = 1234567890; const messageText = 'Hello there!'; - + // Bob (sender of reaction) computes hash with null sender - final bobHash = ReactionHelper.computeReactionHash(timestamp, null, messageText); - + final bobHash = ReactionHelper.computeReactionHash( + timestamp, + null, + messageText, + ); + // Alice (receiver of reaction) computes hash for her outgoing message - final aliceHash = ReactionHelper.computeReactionHash(timestamp, null, messageText); - + final aliceHash = ReactionHelper.computeReactionHash( + timestamp, + null, + messageText, + ); + expect(bobHash, equals(aliceHash)); }); }); @@ -188,12 +299,30 @@ void main() { test('should return null for invalid format', () { expect(ReactionHelper.parseReaction('invalid'), isNull); - expect(ReactionHelper.parseReaction('r:abc:00'), isNull); // Hash too short - expect(ReactionHelper.parseReaction('r:abcde:00'), isNull); // Hash too long - expect(ReactionHelper.parseReaction('r:a1b2:0'), isNull); // Index too short - expect(ReactionHelper.parseReaction('r:a1b2:000'), isNull); // Index too long - expect(ReactionHelper.parseReaction('R:a1b2:00'), isNull); // Uppercase R - expect(ReactionHelper.parseReaction('r:A1B2:00'), isNull); // Uppercase hash + expect( + ReactionHelper.parseReaction('r:abc:00'), + isNull, + ); // Hash too short + expect( + ReactionHelper.parseReaction('r:abcde:00'), + isNull, + ); // Hash too long + expect( + ReactionHelper.parseReaction('r:a1b2:0'), + isNull, + ); // Index too short + expect( + ReactionHelper.parseReaction('r:a1b2:000'), + isNull, + ); // Index too long + expect( + ReactionHelper.parseReaction('R:a1b2:00'), + isNull, + ); // Uppercase R + expect( + ReactionHelper.parseReaction('r:A1B2:00'), + isNull, + ); // Uppercase hash expect(ReactionHelper.parseReaction(''), isNull); }); @@ -220,31 +349,43 @@ void main() { const emoji = '🎉'; // Compute hash (sender side) - final hash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); - + final hash = ReactionHelper.computeReactionHash( + timestamp, + senderName, + messageText, + ); + // Encode emoji (sender side) final emojiIndex = ReactionHelper.emojiToIndex(emoji); expect(emojiIndex, isNotNull); - + // Build reaction text (sender side) final reactionText = 'r:$hash:$emojiIndex'; - + // Parse reaction (receiver side) final info = ReactionHelper.parseReaction(reactionText); expect(info, isNotNull); expect(info!.targetHash, equals(hash)); expect(info.emoji, equals(emoji)); - + // Verify receiver can match the hash - final receiverHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + final receiverHash = ReactionHelper.computeReactionHash( + timestamp, + senderName, + messageText, + ); expect(receiverHash, equals(info.targetHash)); }); test('reaction text should be 9 bytes', () { - final hash = ReactionHelper.computeReactionHash(1234567890, 'Alice', 'Hello'); + final hash = ReactionHelper.computeReactionHash( + 1234567890, + 'Alice', + 'Hello', + ); final index = ReactionHelper.emojiToIndex('👍')!; final reactionText = 'r:$hash:$index'; - + // r: (2) + hash (4) + : (1) + index (2) = 9 bytes expect(reactionText.length, equals(9)); }); @@ -257,7 +398,11 @@ void main() { const emoji = '👍'; // On Bob's device: message.isOutgoing = false, so senderName = contact.name = Alice - final bobSideHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); + final bobSideHash = ReactionHelper.computeReactionHash( + timestamp, + aliceName, + messageText, + ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$bobSideHash:$emojiIndex'; @@ -266,8 +411,12 @@ void main() { expect(info, isNotNull); // On Alice's device: message.isOutgoing = true, so senderName = selfName = Alice - final aliceSideHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); - + final aliceSideHash = ReactionHelper.computeReactionHash( + timestamp, + aliceName, + messageText, + ); + // Hashes should match! expect(info!.targetHash, equals(aliceSideHash)); expect(info.emoji, equals(emoji)); @@ -281,7 +430,11 @@ void main() { const emoji = '❤️'; // On Alice's device: message.isOutgoing = false, so senderName = contact.name = Bob - final aliceSideHash = ReactionHelper.computeReactionHash(timestamp, bobName, messageText); + final aliceSideHash = ReactionHelper.computeReactionHash( + timestamp, + bobName, + messageText, + ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$aliceSideHash:$emojiIndex'; @@ -290,8 +443,12 @@ void main() { expect(info, isNotNull); // On Bob's device: message.isOutgoing = true, so senderName = selfName = Bob - final bobSideHash = ReactionHelper.computeReactionHash(timestamp, bobName, messageText); - + final bobSideHash = ReactionHelper.computeReactionHash( + timestamp, + bobName, + messageText, + ); + // Hashes should match! expect(info!.targetHash, equals(bobSideHash)); expect(info.emoji, equals(emoji)); @@ -306,7 +463,11 @@ void main() { const emoji = '🎉'; // Alice computes hash including sender name (room servers are multi-user) - final aliceHash = ReactionHelper.computeReactionHash(timestamp, charlieName, messageText); + final aliceHash = ReactionHelper.computeReactionHash( + timestamp, + charlieName, + messageText, + ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$aliceHash:$emojiIndex'; @@ -319,36 +480,59 @@ void main() { expect(info, isNotNull); // Bob computes hash for Charlie's message the same way - final bobHash = ReactionHelper.computeReactionHash(timestamp, charlieName, messageText); - + final bobHash = ReactionHelper.computeReactionHash( + timestamp, + charlieName, + messageText, + ); + // Hashes should match! expect(info!.targetHash, equals(bobHash)); expect(info.emoji, equals(emoji)); }); - test('room server: hash differs from 1:1 hash for same message content', () { - // Same timestamp and text, but room server includes sender name - const timestamp = 1234567890; - const senderName = 'Dave'; - const messageText = 'Hello'; + test( + 'room server: hash differs from 1:1 hash for same message content', + () { + // Same timestamp and text, but room server includes sender name + const timestamp = 1234567890; + const senderName = 'Dave'; + const messageText = 'Hello'; - // Room server hash (with sender name) - final roomHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); - - // 1:1 hash (without sender name) - final directHash = ReactionHelper.computeReactionHash(timestamp, null, messageText); + // Room server hash (with sender name) + final roomHash = ReactionHelper.computeReactionHash( + timestamp, + senderName, + messageText, + ); - // They should be different! - expect(roomHash, isNot(equals(directHash))); - }); + // 1:1 hash (without sender name) + final directHash = ReactionHelper.computeReactionHash( + timestamp, + null, + messageText, + ); + + // They should be different! + expect(roomHash, isNot(equals(directHash))); + }, + ); test('room server: different senders produce different hashes', () { // Two users send the exact same message at the same time in a room const timestamp = 1234567890; const messageText = 'Hello'; - final aliceHash = ReactionHelper.computeReactionHash(timestamp, 'Alice', messageText); - final bobHash = ReactionHelper.computeReactionHash(timestamp, 'Bob', messageText); + final aliceHash = ReactionHelper.computeReactionHash( + timestamp, + 'Alice', + messageText, + ); + final bobHash = ReactionHelper.computeReactionHash( + timestamp, + 'Bob', + messageText, + ); // Different senders = different hashes (even with same content) expect(aliceHash, isNot(equals(bobHash))); @@ -363,7 +547,11 @@ void main() { const emoji = '👍'; // Bob computes hash for Alice's message - final bobHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); + final bobHash = ReactionHelper.computeReactionHash( + timestamp, + aliceName, + messageText, + ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$bobHash:$emojiIndex'; @@ -372,8 +560,12 @@ void main() { expect(info, isNotNull); // Alice computes hash using her selfName - final aliceHash = ReactionHelper.computeReactionHash(timestamp, aliceName, messageText); - + final aliceHash = ReactionHelper.computeReactionHash( + timestamp, + aliceName, + messageText, + ); + // Hashes should match! expect(info!.targetHash, equals(aliceHash)); }); @@ -386,7 +578,11 @@ void main() { const emoji = '🔥'; // Compute hash with sender name - final hash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + final hash = ReactionHelper.computeReactionHash( + timestamp, + senderName, + messageText, + ); final emojiIndex = ReactionHelper.emojiToIndex(emoji)!; final reactionText = 'r:$hash:$emojiIndex'; @@ -396,7 +592,11 @@ void main() { expect(info!.emoji, equals(emoji)); // Another user computes the same hash - final otherUserHash = ReactionHelper.computeReactionHash(timestamp, senderName, messageText); + final otherUserHash = ReactionHelper.computeReactionHash( + timestamp, + senderName, + messageText, + ); expect(info.targetHash, equals(otherUserHash)); }); }); From e449f5e1d5d29a5ff83ff9a92d7b956543ccedd7 Mon Sep 17 00:00:00 2001 From: 446564 Date: Wed, 4 Feb 2026 08:33:09 -0800 Subject: [PATCH 059/421] add dart format workflow checks code has been formatted with dart format on push and pull request adds a note in README for contributors --- .github/workflows/dart.yml | 21 +++++++++++++++++++++ README.md | 1 + 2 files changed, 22 insertions(+) create mode 100644 .github/workflows/dart.yml diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml new file mode 100644 index 0000000..f297787 --- /dev/null +++ b/.github/workflows/dart.yml @@ -0,0 +1,21 @@ +name: Dart Format + +on: + pull_request: + push: + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Dart + uses: dart-lang/setup-dart@v1 + + - name: Install Pub Dependencies + run: dart pub get + + - name: Verify Formatting + run: dart format --output=none --set-exit-if-changed . diff --git a/README.md b/README.md index 984e6ba..bad9b6c 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ This is an open-source project. Contributions are welcome! - Use `const` constructors where possible - Keep functions small and focused - Avoid premature abstractions +- Run dart format on all changes before submitting ## Support From 8d15f7cef6f445b89ad4a3a92c51a1d087609e8b Mon Sep 17 00:00:00 2001 From: 446564 Date: Wed, 4 Feb 2026 08:34:37 -0800 Subject: [PATCH 060/421] wrap returns from if blocks fixes two analyzer errors for return blocks on new lines from if blocks --- lib/screens/chat_screen.dart | 3 ++- lib/screens/repeater_status_screen.dart | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 00cea59..56058ef 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -673,8 +673,9 @@ class _ChatScreenState extends State { String _formatRelativeTime(DateTime time) { final diff = DateTime.now().difference(time); if (diff.inSeconds < 60) return context.l10n.time_justNow; - if (diff.inMinutes < 60) + if (diff.inMinutes < 60) { return context.l10n.time_minutesAgo(diff.inMinutes); + } if (diff.inHours < 24) return context.l10n.time_hoursAgo(diff.inHours); return context.l10n.time_daysAgo(diff.inDays); } diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 1523f77..472b013 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -646,8 +646,9 @@ class _RepeaterStatusScreenState extends State { final direct = _formatValue(_dupDirect); return l10n.repeater_duplicatesFloodDirect(flood, direct); } - if (_packetsRecv == null || _floodRx == null || _directRx == null) + if (_packetsRecv == null || _floodRx == null || _directRx == null) { return '—'; + } final dupTotal = _packetsRecv! - _floodRx! - _directRx!; if (dupTotal < 0) return '—'; return l10n.repeater_duplicatesTotal(dupTotal); From a35590a407c19c5906bd1427c68a34b06f1b8eb8 Mon Sep 17 00:00:00 2001 From: 446564 Date: Wed, 4 Feb 2026 08:36:56 -0800 Subject: [PATCH 061/421] fix dart format workflow install deps step needs to use flutter pub get not dart pub get --- .github/workflows/dart.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index f297787..7e077dc 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -5,17 +5,19 @@ on: push: jobs: - analyze: + format: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Dart - uses: dart-lang/setup-dart@v1 + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable - - name: Install Pub Dependencies - run: dart pub get + - name: Install dependencies + run: flutter pub get - name: Verify Formatting run: dart format --output=none --set-exit-if-changed . From b786c9051430a8f9b517e417a05751731382a5ee Mon Sep 17 00:00:00 2001 From: 446564 Date: Wed, 4 Feb 2026 08:56:40 -0800 Subject: [PATCH 062/421] combine flutter and dart actions reduce time to complete and stop running twice for pull requests --- .github/workflows/dart.yml | 23 --------------- .github/workflows/flutter_analyze.yml | 23 --------------- .github/workflows/flutter_dart.yml | 41 +++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 46 deletions(-) delete mode 100644 .github/workflows/dart.yml delete mode 100644 .github/workflows/flutter_analyze.yml create mode 100644 .github/workflows/flutter_dart.yml diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml deleted file mode 100644 index 7e077dc..0000000 --- a/.github/workflows/dart.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Dart Format - -on: - pull_request: - push: - -jobs: - format: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - - name: Install dependencies - run: flutter pub get - - - name: Verify Formatting - run: dart format --output=none --set-exit-if-changed . diff --git a/.github/workflows/flutter_analyze.yml b/.github/workflows/flutter_analyze.yml deleted file mode 100644 index af4a3b7..0000000 --- a/.github/workflows/flutter_analyze.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Flutter Analyze - -on: - pull_request: - push: - -jobs: - analyze: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - - name: Install dependencies - run: flutter pub get - - - name: Analyze - run: flutter analyze --fatal-infos --fatal-warnings diff --git a/.github/workflows/flutter_dart.yml b/.github/workflows/flutter_dart.yml new file mode 100644 index 0000000..f2cd5a0 --- /dev/null +++ b/.github/workflows/flutter_dart.yml @@ -0,0 +1,41 @@ +name: Flutter and Dart Analysis + +on: + pull_request: + push: + branches: + - main + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Install dependencies + run: flutter pub get + + - name: Analyze code + run: flutter analyze --fatal-infos --fatal-warnings + format: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Install dependencies + run: flutter pub get + + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . From 2525b9425b704ce324f4074e17c25907052ca301 Mon Sep 17 00:00:00 2001 From: 446564 Date: Wed, 4 Feb 2026 08:59:29 -0800 Subject: [PATCH 063/421] reduce jobs for flutter and dart no need to setup the env twice the exact same way as they don't conflict --- .github/workflows/flutter_dart.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/flutter_dart.yml b/.github/workflows/flutter_dart.yml index f2cd5a0..5b68956 100644 --- a/.github/workflows/flutter_dart.yml +++ b/.github/workflows/flutter_dart.yml @@ -7,7 +7,7 @@ on: - main jobs: - analyze: + analyze_and_format: runs-on: ubuntu-latest steps: - name: Checkout @@ -23,19 +23,6 @@ jobs: - name: Analyze code run: flutter analyze --fatal-infos --fatal-warnings - format: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable - - - name: Install dependencies - run: flutter pub get - name: Verify formatting run: dart format --output=none --set-exit-if-changed . From 6070802213eb32a655098b85c0c6422476d94b9d Mon Sep 17 00:00:00 2001 From: 446564 Date: Wed, 4 Feb 2026 09:02:03 -0800 Subject: [PATCH 064/421] stop building twice for pull requests we should only run the build steps on a pull request OR a push to main --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8826c55..05c82de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,8 @@ name: Build on: push: + branches: + - main pull_request: jobs: From c320378be1af94f5a1cc587fb395f55e80ae6ee5 Mon Sep 17 00:00:00 2001 From: zjs81 <30362347+zjs81@users.noreply.github.com> Date: Wed, 4 Feb 2026 20:34:03 -0700 Subject: [PATCH 065/421] Refactor unread message tracking and implement channel caching (#126) * Refactor unread message tracking and implement channel caching * formatted files --- lib/connector/meshcore_connector.dart | 261 ++++++++++++++++---------- lib/main.dart | 1 + lib/models/channel.dart | 8 +- lib/screens/channel_chat_screen.dart | 7 +- lib/screens/chat_screen.dart | 8 +- lib/storage/channel_store.dart | 50 +++++ lib/storage/unread_store.dart | 87 ++------- 7 files changed, 243 insertions(+), 179 deletions(-) create mode 100644 lib/storage/channel_store.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 6d62f92..2c56c37 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -24,6 +24,7 @@ import '../services/notification_service.dart'; import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; +import '../storage/channel_store.dart'; import '../storage/contact_settings_store.dart'; import '../storage/contact_store.dart'; import '../storage/message_store.dart'; @@ -139,14 +140,15 @@ class MeshCoreConnector extends ChangeNotifier { final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore(); final ContactSettingsStore _contactSettingsStore = ContactSettingsStore(); final ContactStore _contactStore = ContactStore(); + final ChannelStore _channelStore = ChannelStore(); final UnreadStore _unreadStore = UnreadStore(); + List _cachedChannels = []; final Map _channelSmazEnabled = {}; bool _lastSentWasCliCommand = false; // Track if last sent message was a CLI command final Map _contactSmazEnabled = {}; final Set _knownContactKeys = {}; - final Map _contactLastReadMs = {}; - final Map _channelLastReadMs = {}; + final Map _contactUnreadCount = {}; bool _unreadStateLoaded = false; final Map _pendingRepeaterAcks = {}; String? _activeContactKey; @@ -321,17 +323,7 @@ class MeshCoreConnector extends ChangeNotifier { int getUnreadCountForContactKey(String contactKeyHex) { if (!_unreadStateLoaded) return 0; if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return 0; - final messages = _conversations[contactKeyHex]; - if (messages == null || messages.isEmpty) return 0; - final lastReadMs = _contactLastReadMs[contactKeyHex] ?? 0; - var count = 0; - for (final message in messages) { - if (message.isOutgoing || message.isCli) continue; - if (message.timestamp.millisecondsSinceEpoch > lastReadMs) { - count++; - } - } - return count; + return _contactUnreadCount[contactKeyHex] ?? 0; } int getUnreadCountForChannel(Channel channel) { @@ -340,17 +332,7 @@ class MeshCoreConnector extends ChangeNotifier { int getUnreadCountForChannelIndex(int channelIndex) { if (!_unreadStateLoaded) return 0; - final messages = _channelMessages[channelIndex]; - if (messages == null || messages.isEmpty) return 0; - final lastReadMs = _channelLastReadMs[channelIndex] ?? 0; - var count = 0; - for (final message in messages) { - if (message.isOutgoing) continue; - if (message.timestamp.millisecondsSinceEpoch > lastReadMs) { - count++; - } - } - return count; + return _findChannelByIndex(channelIndex)?.unreadCount ?? 0; } int getTotalUnreadCount() { @@ -380,16 +362,17 @@ class MeshCoreConnector extends ChangeNotifier { } Future loadUnreadState() async { - _contactLastReadMs + _contactUnreadCount ..clear() - ..addAll(await _unreadStore.loadContactLastRead()); - _channelLastReadMs - ..clear() - ..addAll(await _unreadStore.loadChannelLastRead()); + ..addAll(await _unreadStore.loadContactUnreadCount()); _unreadStateLoaded = true; notifyListeners(); } + Future loadCachedChannels() async { + _cachedChannels = await _channelStore.loadChannels(); + } + void setActiveContact(String? contactKeyHex) { if (contactKeyHex != null && !_shouldTrackUnreadForContactKey(contactKeyHex)) { @@ -411,17 +394,36 @@ class MeshCoreConnector extends ChangeNotifier { void markContactRead(String contactKeyHex) { if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return; - final markMs = _calculateReadTimestampMs( - _conversations[contactKeyHex]?.map((m) => m.timestamp), - ); - _setContactLastReadMs(contactKeyHex, markMs); + final previousCount = _contactUnreadCount[contactKeyHex] ?? 0; + if (previousCount > 0) { + _contactUnreadCount[contactKeyHex] = 0; + _appDebugLogService?.info( + 'Contact $contactKeyHex marked as read (was $previousCount unread)', + tag: 'Unread', + ); + _unreadStore.saveContactUnreadCount( + Map.from(_contactUnreadCount), + ); + notifyListeners(); + } } void markChannelRead(int channelIndex) { - final markMs = _calculateReadTimestampMs( - _channelMessages[channelIndex]?.map((m) => m.timestamp), - ); - _setChannelLastReadMs(channelIndex, markMs); + final channel = _findChannelByIndex(channelIndex); + if (channel != null && channel.unreadCount > 0) { + final previousCount = channel.unreadCount; + channel.unreadCount = 0; + _appDebugLogService?.info( + 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} marked as read (was $previousCount unread)', + tag: 'Unread', + ); + unawaited( + _channelStore.saveChannels( + _channels.isNotEmpty ? _channels : _cachedChannels, + ), + ); + notifyListeners(); + } } Future setChannelSmazEnabled(int channelIndex, bool enabled) async { @@ -788,6 +790,9 @@ class MeshCoreConnector extends ChangeNotifier { // Keep device clock aligned on every connection. await syncTime(); + + // Fetch channels so we can track unread counts for incoming messages + unawaited(getChannels()); } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -1341,8 +1346,10 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_persistContacts()); _conversations.remove(contact.publicKeyHex); _loadedConversationKeys.remove(contact.publicKeyHex); - _contactLastReadMs.remove(contact.publicKeyHex); - _unreadStore.saveContactLastRead(Map.from(_contactLastReadMs)); + _contactUnreadCount.remove(contact.publicKeyHex); + _unreadStore.saveContactUnreadCount( + Map.from(_contactUnreadCount), + ); _messageStore.clearMessages(contact.publicKeyHex); notifyListeners(); } @@ -1617,6 +1624,10 @@ class MeshCoreConnector extends ChangeNotifier { _cleanupChannelSync(completed: true); + // Cache channels for offline use + _cachedChannels = List.from(_channels); + unawaited(_channelStore.saveChannels(_channels)); + // Apply ordering and notify UI _applyChannelOrder(); notifyListeners(); @@ -1651,8 +1662,6 @@ class MeshCoreConnector extends ChangeNotifier { // Delete by setting empty name and zero PSK await sendFrame(buildSetChannelFrame(index, '', Uint8List(16))); - _channelLastReadMs.remove(index); - _unreadStore.saveChannelLastRead(Map.from(_channelLastReadMs)); // Clear stored messages for this channel await _channelMessageStore.clearChannelMessages(index); // Clear in-memory messages for this channel @@ -1930,9 +1939,9 @@ class MeshCoreConnector extends ChangeNotifier { final contact = Contact.fromFrame(frame); if (contact != null) { if (contact.type == advTypeRepeater) { - _contactLastReadMs.remove(contact.publicKeyHex); - _unreadStore.saveContactLastRead( - Map.from(_contactLastReadMs), + _contactUnreadCount.remove(contact.publicKeyHex); + _unreadStore.saveContactUnreadCount( + Map.from(_contactUnreadCount), ); } // Check if this is a new contact @@ -2157,7 +2166,7 @@ class MeshCoreConnector extends ChangeNotifier { } } _addMessage(message.senderKeyHex, message); - _maybeMarkActiveContactRead(message); + _maybeIncrementContactUnread(message); notifyListeners(); // Show notification for new incoming message @@ -2348,7 +2357,7 @@ class MeshCoreConnector extends ChangeNotifier { pathBytes: message.pathBytes, ); final isNew = _addChannelMessage(message.channelIndex!, message); - _maybeMarkActiveChannelRead(message); + _maybeIncrementChannelUnread(message, isNew: isNew); notifyListeners(); if (isNew) { _maybeNotifyChannelMessage(message); @@ -2370,7 +2379,9 @@ class MeshCoreConnector extends ChangeNotifier { final channelHash = payload[0]; final encrypted = Uint8List.fromList(payload.sublist(1)); - for (final channel in _channels) { + // Use cached channels as fallback if live channels not yet loaded + final channelsToSearch = _channels.isNotEmpty ? _channels : _cachedChannels; + for (final channel in channelsToSearch) { if (channel.isEmpty) continue; final hash = _computeChannelHash(channel.psk); if (hash != channelHash) continue; @@ -2409,7 +2420,7 @@ class MeshCoreConnector extends ChangeNotifier { pathBytes: message.pathBytes, ); final isNew = _addChannelMessage(channel.index, message); - _maybeMarkActiveChannelRead(message); + _maybeIncrementChannelUnread(message, isNew: isNew); notifyListeners(); if (isNew) { final label = channel.name.isEmpty @@ -2539,6 +2550,15 @@ class MeshCoreConnector extends ChangeNotifier { '[ChannelSync] Received channel ${channel.index}: ${channel.isEmpty ? "empty" : channel.name}', ); + // Preserve unread count from cached channel + final cachedChannel = _cachedChannels.cast().firstWhere( + (c) => c?.index == channel.index, + orElse: () => null, + ); + if (cachedChannel != null) { + channel.unreadCount = cachedChannel.unreadCount; + } + // If we're syncing and this is the channel we're waiting for if (_isSyncingChannels && _channelSyncInFlight) { if (channel.index == _nextChannelIndexToRequest) { @@ -2578,6 +2598,8 @@ class MeshCoreConnector extends ChangeNotifier { (c) => c.index == channel.index, ); if (existingIndex >= 0) { + // Preserve unread count from existing channel + channel.unreadCount = _channels[existingIndex].unreadCount; _channels[existingIndex] = channel; } else { _channels.add(channel); @@ -2628,67 +2650,98 @@ class MeshCoreConnector extends ChangeNotifier { return contact.type != advTypeRepeater; } - int _calculateReadTimestampMs(Iterable? timestamps) { - var latestMs = 0; - if (timestamps != null) { - for (final timestamp in timestamps) { - final ms = timestamp.millisecondsSinceEpoch; - if (ms > latestMs) { - latestMs = ms; - } - } - } - return latestMs; + Channel? _findChannelByIndex(int index) { + return _channels.cast().firstWhere( + (c) => c?.index == index, + orElse: () => null, + ) ?? + _cachedChannels.cast().firstWhere( + (c) => c?.index == index, + orElse: () => null, + ); } - void _setContactLastReadMs( - String contactKeyHex, - int timestampMs, { - bool notify = true, + void _maybeIncrementChannelUnread( + ChannelMessage message, { + required bool isNew, }) { - if (!_shouldTrackUnreadForContactKey(contactKeyHex)) return; - final existing = _contactLastReadMs[contactKeyHex] ?? 0; - if (timestampMs <= existing) return; - _contactLastReadMs[contactKeyHex] = timestampMs; - _unreadStore.saveContactLastRead(Map.from(_contactLastReadMs)); - if (notify) { - notifyListeners(); + if (!isNew || message.isOutgoing) { + _appDebugLogService?.info( + 'Skip unread increment: isNew=$isNew, isOutgoing=${message.isOutgoing}', + tag: 'Unread', + ); + return; } - } - - void _setChannelLastReadMs( - int channelIndex, - int timestampMs, { - bool notify = true, - }) { - final existing = _channelLastReadMs[channelIndex] ?? 0; - if (timestampMs <= existing) return; - _channelLastReadMs[channelIndex] = timestampMs; - _unreadStore.saveChannelLastRead(Map.from(_channelLastReadMs)); - if (notify) { - notifyListeners(); - } - } - - void _maybeMarkActiveContactRead(Message message) { - if (message.isOutgoing || message.isCli) return; - if (_activeContactKey != message.senderKeyHex) return; - if (!_shouldTrackUnreadForContactKey(message.senderKeyHex)) return; - _setContactLastReadMs( - message.senderKeyHex, - message.timestamp.millisecondsSinceEpoch, - notify: false, - ); - } - - void _maybeMarkActiveChannelRead(ChannelMessage message) { - if (message.isOutgoing) return; final channelIndex = message.channelIndex; - if (channelIndex == null || _activeChannelIndex != channelIndex) return; - _setChannelLastReadMs( - channelIndex, - message.timestamp.millisecondsSinceEpoch, - notify: false, + if (channelIndex == null) { + _appDebugLogService?.info( + 'Skip unread increment: channelIndex is null', + tag: 'Unread', + ); + return; + } + // Don't increment if user is viewing this channel + if (_activeChannelIndex == channelIndex) { + _appDebugLogService?.info( + 'Skip unread increment: channel $channelIndex is active', + tag: 'Unread', + ); + return; + } + + final channel = _findChannelByIndex(channelIndex); + if (channel != null) { + channel.unreadCount++; + _appDebugLogService?.info( + 'Channel ${channel.name.isNotEmpty ? channel.name : channelIndex} unread count incremented to ${channel.unreadCount}', + tag: 'Unread', + ); + unawaited( + _channelStore.saveChannels( + _channels.isNotEmpty ? _channels : _cachedChannels, + ), + ); + } else { + _appDebugLogService?.info( + 'Channel $channelIndex not found in _channels (${_channels.length}) or _cachedChannels (${_cachedChannels.length})', + tag: 'Unread', + ); + } + } + + void _maybeIncrementContactUnread(Message message) { + if (message.isOutgoing || message.isCli) { + _appDebugLogService?.info( + 'Skip contact unread increment: isOutgoing=${message.isOutgoing}, isCli=${message.isCli}', + tag: 'Unread', + ); + return; + } + final contactKey = message.senderKeyHex; + if (!_shouldTrackUnreadForContactKey(contactKey)) { + _appDebugLogService?.info( + 'Skip contact unread increment: should not track for $contactKey', + tag: 'Unread', + ); + return; + } + // Don't increment if user is viewing this contact + if (_activeContactKey == contactKey) { + _appDebugLogService?.info( + 'Skip contact unread increment: contact $contactKey is active', + tag: 'Unread', + ); + return; + } + + final currentCount = _contactUnreadCount[contactKey] ?? 0; + _contactUnreadCount[contactKey] = currentCount + 1; + _appDebugLogService?.info( + 'Contact $contactKey unread count incremented to ${currentCount + 1}', + tag: 'Unread', + ); + _unreadStore.saveContactUnreadCount( + Map.from(_contactUnreadCount), ); } diff --git a/lib/main.dart b/lib/main.dart index 19c577f..96a853d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -60,6 +60,7 @@ void main() async { await connector.loadContactCache(); await connector.loadChannelSettings(); + await connector.loadCachedChannels(); // Load persisted channel messages await connector.loadAllChannelMessages(); diff --git a/lib/models/channel.dart b/lib/models/channel.dart index 4e5e8c2..1a2ecdc 100644 --- a/lib/models/channel.dart +++ b/lib/models/channel.dart @@ -9,8 +9,14 @@ class Channel { final int index; final String name; final Uint8List psk; // 16 bytes + int unreadCount; - Channel({required this.index, required this.name, required this.psk}); + Channel({ + required this.index, + required this.name, + required this.psk, + this.unreadCount = 0, + }); String get pskHex => _bytesToHex(psk); diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index a1139a1..c82356d 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -41,6 +41,8 @@ class _ChannelChatScreenState extends State { final Map _messageKeys = {}; bool _isLoadingOlder = false; + MeshCoreConnector? _connector; + @override void initState() { super.initState(); @@ -48,7 +50,8 @@ class _ChannelChatScreenState extends State { _scrollController.onScrollNearTop = _loadOlderMessages; SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - context.read().setActiveChannel(widget.channel.index); + _connector = context.read(); + _connector?.setActiveChannel(widget.channel.index); }); } @@ -72,7 +75,7 @@ class _ChannelChatScreenState extends State { @override void dispose() { - context.read().setActiveChannel(null); + _connector?.setActiveChannel(null); _textFieldFocusNode.removeListener(_onTextFieldFocusChange); _textFieldFocusNode.dispose(); _textController.dispose(); diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 56058ef..3477361 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -44,6 +44,7 @@ class _ChatScreenState extends State { final _scrollController = ChatScrollController(); final _textFieldFocusNode = FocusNode(); bool _isLoadingOlder = false; + MeshCoreConnector? _connector; @override void initState() { @@ -52,9 +53,8 @@ class _ChatScreenState extends State { _scrollController.onScrollNearTop = _loadOlderMessages; SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - context.read().setActiveContact( - widget.contact.publicKeyHex, - ); + _connector = context.read(); + _connector?.setActiveContact(widget.contact.publicKeyHex); }); } @@ -78,7 +78,7 @@ class _ChatScreenState extends State { @override void dispose() { - context.read().setActiveContact(null); + _connector?.setActiveContact(null); _textFieldFocusNode.removeListener(_onTextFieldFocusChange); _textFieldFocusNode.dispose(); _textController.dispose(); diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart new file mode 100644 index 0000000..eaa7a61 --- /dev/null +++ b/lib/storage/channel_store.dart @@ -0,0 +1,50 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import '../models/channel.dart'; +import 'prefs_manager.dart'; + +class ChannelStore { + static const String _key = 'channels'; + + Future> loadChannels() async { + final prefs = PrefsManager.instance; + final jsonStr = prefs.getString(_key); + if (jsonStr == null) return []; + + try { + final jsonList = jsonDecode(jsonStr) as List; + return jsonList + .map((entry) => _fromJson(entry as Map)) + .toList(); + } catch (_) { + return []; + } + } + + Future saveChannels(List channels) async { + final prefs = PrefsManager.instance; + final jsonList = channels.map(_toJson).toList(); + await prefs.setString(_key, jsonEncode(jsonList)); + } + + Map _toJson(Channel channel) { + return { + 'index': channel.index, + 'name': channel.name, + 'psk': base64Encode(channel.psk), + 'unreadCount': channel.unreadCount, + }; + } + + Channel _fromJson(Map json) { + return Channel( + index: json['index'] as int, + name: json['name'] as String? ?? '', + psk: json['psk'] != null + ? Uint8List.fromList(base64Decode(json['psk'] as String)) + : Uint8List(16), + unreadCount: json['unreadCount'] as int? ?? 0, + ); + } +} diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index d46a34c..201d25e 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -5,27 +5,23 @@ import 'prefs_manager.dart'; /// Storage for unread message tracking with debounced writes to reduce I/O. class UnreadStore { - static const String _contactLastReadKey = 'contact_last_read'; - static const String _channelLastReadKey = 'channel_last_read'; + static const String _contactUnreadCountKey = 'contact_unread_count'; // Debounce timers to batch rapid writes - Timer? _contactSaveTimer; - Timer? _channelSaveTimer; + Timer? _contactUnreadSaveTimer; static const Duration _saveDebounceDuration = Duration(milliseconds: 500); // Pending write data - Map? _pendingContactLastRead; - Map? _pendingChannelLastRead; + Map? _pendingContactUnreadCount; /// Dispose timers when no longer needed void dispose() { - _contactSaveTimer?.cancel(); - _channelSaveTimer?.cancel(); + _contactUnreadSaveTimer?.cancel(); } - Future> loadContactLastRead() async { + Future> loadContactUnreadCount() async { final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_contactLastReadKey); + final jsonStr = prefs.getString(_contactUnreadCountKey); if (jsonStr == null) return {}; try { @@ -36,75 +32,30 @@ class UnreadStore { } } - /// Save contact last read timestamps with debouncing. - /// Writes are delayed by 500ms and batched to reduce I/O operations. - void saveContactLastRead(Map lastReadMs) { - _pendingContactLastRead = lastReadMs; + void saveContactUnreadCount(Map counts) { + _pendingContactUnreadCount = counts; - // Cancel existing timer - _contactSaveTimer?.cancel(); + _contactUnreadSaveTimer?.cancel(); - // Schedule new write - _contactSaveTimer = Timer(_saveDebounceDuration, () async { - if (_pendingContactLastRead != null) { - await _flushContactLastRead(); + _contactUnreadSaveTimer = Timer(_saveDebounceDuration, () async { + if (_pendingContactUnreadCount != null) { + await _flushContactUnreadCount(); } }); } - Future> loadChannelLastRead() async { - final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_channelLastReadKey); - if (jsonStr == null) return {}; - - try { - final json = jsonDecode(jsonStr) as Map; - return json.map((key, value) => MapEntry(int.parse(key), value as int)); - } catch (_) { - return {}; - } - } - - /// Save channel last read timestamps with debouncing. - /// Writes are delayed by 500ms and batched to reduce I/O operations. - void saveChannelLastRead(Map lastReadMs) { - _pendingChannelLastRead = lastReadMs; - - _channelSaveTimer?.cancel(); - - _channelSaveTimer = Timer(_saveDebounceDuration, () async { - if (_pendingChannelLastRead != null) { - await _flushChannelLastRead(); - } - }); - } - - Future _flushContactLastRead() async { - if (_pendingContactLastRead == null) return; + Future _flushContactUnreadCount() async { + if (_pendingContactUnreadCount == null) return; final prefs = PrefsManager.instance; - final jsonStr = jsonEncode(_pendingContactLastRead); - await prefs.setString(_contactLastReadKey, jsonStr); - _pendingContactLastRead = null; - } - - Future _flushChannelLastRead() async { - if (_pendingChannelLastRead == null) return; - - final prefs = PrefsManager.instance; - final asString = _pendingChannelLastRead!.map( - (key, value) => MapEntry(key.toString(), value), - ); - final jsonStr = jsonEncode(asString); - await prefs.setString(_channelLastReadKey, jsonStr); - _pendingChannelLastRead = null; + final jsonStr = jsonEncode(_pendingContactUnreadCount); + await prefs.setString(_contactUnreadCountKey, jsonStr); + _pendingContactUnreadCount = null; } /// Immediately flush pending writes (call before app termination or disposal) Future flush() async { - _contactSaveTimer?.cancel(); - _channelSaveTimer?.cancel(); - - await Future.wait([_flushContactLastRead(), _flushChannelLastRead()]); + _contactUnreadSaveTimer?.cancel(); + await _flushContactUnreadCount(); } } From 05fb5a13fabc9716f80698c273203522bfdc25f5 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 5 Feb 2026 08:33:07 -0800 Subject: [PATCH 066/421] remove direct msg notification prefix The prefix "New message from " takes up a lot of space and was not localized anyway. --- lib/services/notification_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index ea7f031..d835d07 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -118,7 +118,7 @@ class NotificationService { await _notifications.show( contactId?.hashCode ?? 0, - 'New message from $contactName', + contactName, message, notificationDetails, payload: 'message:$contactId', From 6a3c59fa2c99218e8ae1f8bc142dc07851162430 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 5 Feb 2026 09:24:24 -0800 Subject: [PATCH 067/421] remove rotation in path map when zooming on the path map view window the rotation was too easy to trigger and provided little value to understanding the path --- lib/screens/channel_message_path_screen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 970c152..50dcb71 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -349,6 +349,9 @@ class _ChannelMessagePathMapScreenState ), minZoom: 2.0, maxZoom: 18.0, + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + ), ), children: [ TileLayer( From ddee76ced26f41028f1b2dbc299ce07d09a20909 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 5 Feb 2026 09:36:43 -0800 Subject: [PATCH 068/421] add flutter test to actions --- .github/workflows/flutter_dart.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter_dart.yml b/.github/workflows/flutter_dart.yml index 5b68956..117eb4f 100644 --- a/.github/workflows/flutter_dart.yml +++ b/.github/workflows/flutter_dart.yml @@ -1,4 +1,4 @@ -name: Flutter and Dart Analysis +name: Flutter and Dart on: pull_request: @@ -7,7 +7,7 @@ on: - main jobs: - analyze_and_format: + analyze: runs-on: ubuntu-latest steps: - name: Checkout @@ -26,3 +26,6 @@ jobs: - name: Verify formatting run: dart format --output=none --set-exit-if-changed . + + - name: Run tests + run: flutter test -r github From 8b1228bf8d8d05f558f897769f9cee830e5d4995 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 5 Feb 2026 13:38:49 -0800 Subject: [PATCH 069/421] Add GPX export functionality and related UI components --- lib/l10n/app_en.arb | 13 ++- lib/screens/settings_screen.dart | 70 +++++++++++++ lib/utils/gpx_export.dart | 165 +++++++++++++++++++++++++++++++ pubspec.yaml | 3 + 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 lib/utils/gpx_export.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee5cf7d..8f501e0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1343,5 +1343,16 @@ "contacts_zeroHopContactAdvertSent": "Sent contact by advert.", "contacts_zeroHopContactAdvertFailed": "Failed to send contact.", "contacts_contactAdvertCopied": "Advert copied to Clipboard.", - "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed." + "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", + + "settings_gpxExportRepeaters": "Export repeaters to GPX", + "settings_gpxExportRepeatersSubtitle": "Exports repeaters with a location to GPX file.", + "settings_gpxExportContacts": "Export contacts to GPX", + "settings_gpxExportContactsSubtitle": "Exports chat contacts with a location to GPX file.", + "settings_gpxExportAll": "Export all to GPX", + "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", + "settings_gpxExportSuccess": "Successfully exported GPX file.", + "settings_gpxExportNoContacts": "No contacts to export.", + "settings_gpxExportNotAvailable": "Not supported on your device/OS", + "settings_gpxExportError": "There was an error when exporting." } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 415d508..cbcbc0d 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:meshcore_open/utils/gpx_export.dart'; import 'package:meshcore_open/widgets/elements_ui.dart'; import 'package:provider/provider.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -57,6 +58,8 @@ class _SettingsScreenState extends State { const SizedBox(height: 16), _buildDebugCard(context), const SizedBox(height: 16), + _buildExportCard(connector), + const SizedBox(height: 16), _buildAboutCard(context), ], ); @@ -684,6 +687,73 @@ class _SettingsScreenState extends State { ], ); } + + _gpxExport(GpxExport exporter) async { + final l10n = context.l10n; + final result = await exporter.exportGPX(); + // Implement GPX export functionality here + switch (result) { + case GpxExportSuccess: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); + case GpxExportNoContacts: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNoContacts))); + case GpxExportNotAvailable: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNotAvailable))); + case GpxExportFailed: + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); + } + } + + _buildExportCard(MeshCoreConnector connector) { + final l10n = context.l10n; + return Card( + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.download_outlined), + title: Text(l10n.settings_gpxExportRepeaters), + subtitle: Text(l10n.settings_gpxExportRepeatersSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final exporter = GpxExport(connector); + exporter.addRepeaters(); + _gpxExport(exporter); + }, + ), + ListTile( + leading: const Icon(Icons.download_outlined), + title: Text(l10n.settings_gpxExportContacts), + subtitle: Text(l10n.settings_gpxExportContactsSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final exporter = GpxExport(connector); + exporter.addContacts(); + _gpxExport(exporter); + }, + ), + ListTile( + leading: const Icon(Icons.download_outlined), + title: Text(l10n.settings_gpxExportAll), + subtitle: Text(l10n.settings_gpxExportAllSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final exporter = GpxExport(connector); + exporter.addAll(); + _gpxExport(exporter); + }, + ), + ], + ), + ); + } } class _RadioSettingsDialog extends StatefulWidget { diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart new file mode 100644 index 0000000..ae1c128 --- /dev/null +++ b/lib/utils/gpx_export.dart @@ -0,0 +1,165 @@ +import 'package:flutter/foundation.dart'; +import 'package:gpx/gpx.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; + +import 'package:share_plus/share_plus.dart'; + +class ContactExport { + final String name; + final double lat; + final double lon; + final String desc; + final double? ele; + + ContactExport({ + required this.name, + required this.lat, + required this.lon, + required this.desc, + this.ele, + }); +} + +const int GpxExportFailed = -1; +const int GpxExportSuccess = 1; +const int GpxExportNoContacts = 2; +const int GpxExportCancelled = 3; +const int GpxExportNotAvailable = 4; + +class GpxExport { + MeshCoreConnector _connector; + List _contacts = []; + + GpxExport(this._connector); + + void _addContact(String name, double lat, double lon, String desc, [double? ele]) { + _contacts.add(ContactExport( + name: name.trim(), + lat: lat, + lon: lon, + desc: desc.trim(), + ele: ele, + )); + } + + void addRepeaters() { + final contacts = _connector.contacts; + final repeaters = contacts.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom).toList(); + for (var repeater in repeaters) { + if (repeater.latitude == null || repeater.longitude == null) { + continue; + } + _addContact( + repeater.name, + repeater.latitude ?? 0.0, + repeater.longitude ?? 0.0, + "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + ); + } + } + + void addContacts() { + final contacts = _connector.contacts; + final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); + for (var repeater in repeaters) { + if (repeater.latitude == null || repeater.longitude == null) { + continue; + } + _addContact( + repeater.name, + repeater.latitude ?? 0.0, + repeater.longitude ?? 0.0, + "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + ); + } + } + + void addAll() { + final contacts = _connector.contacts; + for (var repeater in contacts.toList()) { + if (repeater.latitude == null || repeater.longitude == null) { + continue; + } + _addContact( + repeater.name, + repeater.latitude ?? 0.0, + repeater.longitude ?? 0.0, + "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + ); + } + } + + Future exportGPX() async { + if (_contacts.isEmpty) { + debugPrint("No repeaters to export – nothing to share."); + return GpxExportNoContacts; + } + + try { + // 1. Build GPX content (your existing logic – unchanged here) + final gpx = Gpx() + ..version = '1.1' + ..creator = 'meshcore-open Repeater Exporter' + ..metadata = Metadata( + name: 'Meshcore Repeaters', + desc: 'Repeater & room locations exported from meshcore-open', + time: DateTime.now().toUtc(), + ); + + gpx.wpts = _contacts.map((c) => Wpt( + lat: c.lat, + lon: c.lon, + ele: c.ele, + name: c.name, + desc: c.desc, + )).toList(); + + final xml = GpxWriter().asString(gpx, pretty: true); + + // 2. Save to file + final dir = await getApplicationDocumentsDirectory(); + final timestamp = DateTime.now().toUtc().toIso8601String() + .replaceAll(':', '-') + .replaceAll('.', '-') + .split('T') + .join('_'); + final path = '${dir.path}/meshcore_repeaters_$timestamp.gpx'; + + final file = File(path); + await file.writeAsString(xml); + + // 3. Modern share call (2025+ style) + final result = await SharePlus.instance.share( + ShareParams( + text: 'Repeater locations exported from meshcore-open app as GPX file.', + subject: 'Meshcore Repeaters GPX Export', + files: [XFile(path)], + // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) + ), + ); + + // 4. Handle result + switch (result.status) { + case ShareResultStatus.success: + debugPrint('Share successful – user completed the action.'); + return GpxExportSuccess; + case ShareResultStatus.dismissed: + debugPrint('Share sheet was dismissed / cancelled by user.'); + return GpxExportCancelled; + case ShareResultStatus.unavailable: + debugPrint('Sharing is not available on this platform / context.'); + return GpxExportNotAvailable; + } + + // Optional cleanup (uncomment if you don't want to keep the file) + // await file.delete(); + } catch (e, stack) { + debugPrint('Export or share failed: $e\n$stack'); + // → here you could show a SnackBar / AlertDialog in real UI code + } + return GpxExportFailed; + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 8b1415f..6312ee3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,9 @@ dependencies: qr_flutter: ^4.1.0 # QR code generation url_launcher: ^6.3.0 # Launch URLs in system browser flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text + gpx: ^2.3.0 + path_provider: ^2.1.5 + share_plus: ^12.0.1 dev_dependencies: flutter_test: From 978ea4790da8dccb323d9dd5b6a2677ee15ea197 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 5 Feb 2026 13:46:05 -0800 Subject: [PATCH 070/421] Refactor GPX export constants to use lowercase naming convention and improve export function error handling --- lib/screens/settings_screen.dart | 10 +++++----- lib/utils/gpx_export.dart | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index cbcbc0d..c5a307f 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -691,21 +691,21 @@ class _SettingsScreenState extends State { _gpxExport(GpxExport exporter) async { final l10n = context.l10n; final result = await exporter.exportGPX(); - // Implement GPX export functionality here + if(!mounted) return; switch (result) { - case GpxExportSuccess: + case gpxExportSuccess: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); - case GpxExportNoContacts: + case gpxExportNoContacts: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNoContacts))); - case GpxExportNotAvailable: + case gpxExportNotAvailable: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNotAvailable))); - case GpxExportFailed: + case gpxExportFailed: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index ae1c128..12c6e27 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -23,15 +23,15 @@ class ContactExport { }); } -const int GpxExportFailed = -1; -const int GpxExportSuccess = 1; -const int GpxExportNoContacts = 2; -const int GpxExportCancelled = 3; -const int GpxExportNotAvailable = 4; +const int gpxExportFailed = -1; +const int gpxExportSuccess = 1; +const int gpxExportNoContacts = 2; +const int gpxExportCancelled = 3; +const int gpxExportNotAvailable = 4; class GpxExport { - MeshCoreConnector _connector; - List _contacts = []; + final MeshCoreConnector _connector; + final List _contacts = []; GpxExport(this._connector); @@ -95,7 +95,7 @@ class GpxExport { Future exportGPX() async { if (_contacts.isEmpty) { debugPrint("No repeaters to export – nothing to share."); - return GpxExportNoContacts; + return gpxExportNoContacts; } try { @@ -145,13 +145,13 @@ class GpxExport { switch (result.status) { case ShareResultStatus.success: debugPrint('Share successful – user completed the action.'); - return GpxExportSuccess; + return gpxExportSuccess; case ShareResultStatus.dismissed: debugPrint('Share sheet was dismissed / cancelled by user.'); - return GpxExportCancelled; + return gpxExportCancelled; case ShareResultStatus.unavailable: debugPrint('Sharing is not available on this platform / context.'); - return GpxExportNotAvailable; + return gpxExportNotAvailable; } // Optional cleanup (uncomment if you don't want to keep the file) @@ -160,6 +160,6 @@ class GpxExport { debugPrint('Export or share failed: $e\n$stack'); // → here you could show a SnackBar / AlertDialog in real UI code } - return GpxExportFailed; + return gpxExportFailed; } } \ No newline at end of file From d1009d3c20c79a500675511a38b4ed0725f8bbff Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 7 Feb 2026 11:07:57 -0800 Subject: [PATCH 071/421] ran formating --- lib/screens/settings_screen.dart | 18 ++++----- lib/utils/gpx_export.dart | 65 ++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c5a307f..6dfbc0a 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -687,24 +687,24 @@ class _SettingsScreenState extends State { ], ); } - + _gpxExport(GpxExport exporter) async { final l10n = context.l10n; final result = await exporter.exportGPX(); - if(!mounted) return; + if (!mounted) return; switch (result) { case gpxExportSuccess: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess))); case gpxExportNoContacts: - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNoContacts))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), + ); case gpxExportNotAvailable: - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportNotAvailable))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), + ); case gpxExportFailed: ScaffoldMessenger.of( context, @@ -728,7 +728,7 @@ class _SettingsScreenState extends State { _gpxExport(exporter); }, ), - ListTile( + ListTile( leading: const Icon(Icons.download_outlined), title: Text(l10n.settings_gpxExportContacts), subtitle: Text(l10n.settings_gpxExportContactsSubtitle), diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 12c6e27..995f34c 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -35,19 +35,29 @@ class GpxExport { GpxExport(this._connector); - void _addContact(String name, double lat, double lon, String desc, [double? ele]) { - _contacts.add(ContactExport( - name: name.trim(), - lat: lat, - lon: lon, - desc: desc.trim(), - ele: ele, - )); + void _addContact( + String name, + double lat, + double lon, + String desc, [ + double? ele, + ]) { + _contacts.add( + ContactExport( + name: name.trim(), + lat: lat, + lon: lon, + desc: desc.trim(), + ele: ele, + ), + ); } void addRepeaters() { final contacts = _connector.contacts; - final repeaters = contacts.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom).toList(); + final repeaters = contacts + .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) + .toList(); for (var repeater in repeaters) { if (repeater.latitude == null || repeater.longitude == null) { continue; @@ -79,7 +89,7 @@ class GpxExport { void addAll() { final contacts = _connector.contacts; - for (var repeater in contacts.toList()) { + for (var repeater in contacts.toList()) { if (repeater.latitude == null || repeater.longitude == null) { continue; } @@ -104,24 +114,30 @@ class GpxExport { ..version = '1.1' ..creator = 'meshcore-open Repeater Exporter' ..metadata = Metadata( - name: 'Meshcore Repeaters', - desc: 'Repeater & room locations exported from meshcore-open', - time: DateTime.now().toUtc(), - ); + name: 'Meshcore Repeaters', + desc: 'Repeater & room locations exported from meshcore-open', + time: DateTime.now().toUtc(), + ); - gpx.wpts = _contacts.map((c) => Wpt( - lat: c.lat, - lon: c.lon, - ele: c.ele, - name: c.name, - desc: c.desc, - )).toList(); + gpx.wpts = _contacts + .map( + (c) => Wpt( + lat: c.lat, + lon: c.lon, + ele: c.ele, + name: c.name, + desc: c.desc, + ), + ) + .toList(); final xml = GpxWriter().asString(gpx, pretty: true); // 2. Save to file final dir = await getApplicationDocumentsDirectory(); - final timestamp = DateTime.now().toUtc().toIso8601String() + final timestamp = DateTime.now() + .toUtc() + .toIso8601String() .replaceAll(':', '-') .replaceAll('.', '-') .split('T') @@ -134,7 +150,8 @@ class GpxExport { // 3. Modern share call (2025+ style) final result = await SharePlus.instance.share( ShareParams( - text: 'Repeater locations exported from meshcore-open app as GPX file.', + text: + 'Repeater locations exported from meshcore-open app as GPX file.', subject: 'Meshcore Repeaters GPX Export', files: [XFile(path)], // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) @@ -162,4 +179,4 @@ class GpxExport { } return gpxExportFailed; } -} \ No newline at end of file +} From 2a909e60813ee2427c8bd7e0892bcae1010ee157 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 7 Feb 2026 19:45:02 -0800 Subject: [PATCH 072/421] Enhance GPX export functionality with customizable parameters and improved metadata --- lib/l10n/app_en.arb | 17 ++++++++---- lib/screens/settings_screen.dart | 47 ++++++++++++++++++++++++++++---- lib/utils/gpx_export.dart | 23 ++++++++++------ 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8f501e0..18b5c6c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1345,14 +1345,19 @@ "contacts_contactAdvertCopied": "Advert copied to Clipboard.", "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", - "settings_gpxExportRepeaters": "Export repeaters to GPX", - "settings_gpxExportRepeatersSubtitle": "Exports repeaters with a location to GPX file.", - "settings_gpxExportContacts": "Export contacts to GPX", - "settings_gpxExportContactsSubtitle": "Exports chat contacts with a location to GPX file.", - "settings_gpxExportAll": "Export all to GPX", + "settings_gpxExportRepeaters": "Export repeaters / room server to GPX", + "settings_gpxExportRepeatersSubtitle": "Exports repeaters / roomserver with a location to GPX file.", + "settings_gpxExportContacts": "Export companions to GPX", + "settings_gpxExportContactsSubtitle": "Exports companions with a location to GPX file.", + "settings_gpxExportAll": "Export all contacts to GPX", "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", "settings_gpxExportSuccess": "Successfully exported GPX file.", "settings_gpxExportNoContacts": "No contacts to export.", "settings_gpxExportNotAvailable": "Not supported on your device/OS", - "settings_gpxExportError": "There was an error when exporting." + "settings_gpxExportError": "There was an error when exporting.", + "settings_gpxExportRepeatersRoom": "Repeater & room server locations", + "settings_gpxExportChat": "Companion locations", + "settings_gpxExportAllContacts": "All contacts locations", + "settings_gpxExportShareText": "Map data exported from meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open GPX map data export" } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 6dfbc0a..2212b8d 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -688,9 +688,22 @@ class _SettingsScreenState extends State { ); } - _gpxExport(GpxExport exporter) async { + _gpxExport( + GpxExport exporter, + String name, + String description, + String filename, + String shareText, + String subject, + ) async { final l10n = context.l10n; - final result = await exporter.exportGPX(); + final result = await exporter.exportGPX( + name, + description, + filename, + shareText, + subject, + ); if (!mounted) return; switch (result) { case gpxExportSuccess: @@ -701,14 +714,17 @@ class _SettingsScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_gpxExportNoContacts)), ); + break; case gpxExportNotAvailable: ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)), ); + break; case gpxExportFailed: ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError))); + break; } } @@ -725,7 +741,14 @@ class _SettingsScreenState extends State { onTap: () async { final exporter = GpxExport(connector); exporter.addRepeaters(); - _gpxExport(exporter); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportRepeatersRoom, + "meshcore_repeaters_", + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); }, ), ListTile( @@ -736,7 +759,14 @@ class _SettingsScreenState extends State { onTap: () async { final exporter = GpxExport(connector); exporter.addContacts(); - _gpxExport(exporter); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportChat, + "meshcore_contacts_", + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); }, ), ListTile( @@ -747,7 +777,14 @@ class _SettingsScreenState extends State { onTap: () async { final exporter = GpxExport(connector); exporter.addAll(); - _gpxExport(exporter); + _gpxExport( + exporter, + l10n.map_repeater, + l10n.settings_gpxExportAllContacts, + "meshcore_all_", + l10n.settings_gpxExportShareText, + l10n.settings_gpxExportShareSubject, + ); }, ), ], diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 995f34c..92494c9 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -102,7 +102,13 @@ class GpxExport { } } - Future exportGPX() async { + Future exportGPX( + String name, + String description, + String filename, + String shareText, + String subject, + ) async { if (_contacts.isEmpty) { debugPrint("No repeaters to export – nothing to share."); return gpxExportNoContacts; @@ -112,10 +118,10 @@ class GpxExport { // 1. Build GPX content (your existing logic – unchanged here) final gpx = Gpx() ..version = '1.1' - ..creator = 'meshcore-open Repeater Exporter' + ..creator = 'meshcore-open exporter' ..metadata = Metadata( - name: 'Meshcore Repeaters', - desc: 'Repeater & room locations exported from meshcore-open', + name: name, + desc: description, time: DateTime.now().toUtc(), ); @@ -142,7 +148,9 @@ class GpxExport { .replaceAll('.', '-') .split('T') .join('_'); - final path = '${dir.path}/meshcore_repeaters_$timestamp.gpx'; + + // ignore: unnecessary_string_escapes + final path = '${dir.path}/$filename$timestamp.gpx'; final file = File(path); await file.writeAsString(xml); @@ -150,9 +158,8 @@ class GpxExport { // 3. Modern share call (2025+ style) final result = await SharePlus.instance.share( ShareParams( - text: - 'Repeater locations exported from meshcore-open app as GPX file.', - subject: 'Meshcore Repeaters GPX Export', + text: shareText, + subject: subject, files: [XFile(path)], // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) ), From 98e0b05e73d0d4dc330d8fc245cd0b6f68b0e3ec Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 11:32:36 -0800 Subject: [PATCH 073/421] Implement PathTraceMapScreen and refactor path tracing functionality across screens --- lib/screens/channel_message_path_screen.dart | 79 ++- lib/screens/chat_screen.dart | 14 + lib/screens/contacts_screen.dart | 38 +- lib/screens/path_trace_map.dart | 535 +++++++++++++++++++ lib/widgets/path_trace_dialog.dart | 240 --------- 5 files changed, 641 insertions(+), 265 deletions(-) create mode 100644 lib/screens/path_trace_map.dart delete mode 100644 lib/widgets/path_trace_dialog.dart diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 970c152..1646b73 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -41,6 +42,21 @@ class ChannelMessagePathScreen extends StatelessWidget { appBar: AppBar( title: Text(l10n.channelPath_title), actions: [ + IconButton( + icon: const Icon(Icons.radar_outlined), + tooltip: l10n.channelPath_viewMap, + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: context.l10n.contacts_repeaterPathTrace, + path: Uint8List.fromList(primaryPath), + flipPathRound: true, + reversePathRound: true, + ), + ), + ), + ), IconButton( icon: const Icon(Icons.map_outlined), tooltip: l10n.channelPath_viewMap, @@ -263,6 +279,7 @@ class ChannelMessagePathMapScreen extends StatefulWidget { class _ChannelMessagePathMapScreenState extends State { Uint8List? _selectedPath; + double _pathDistance = 0.0; @override void initState() { @@ -282,6 +299,17 @@ class _ChannelMessagePathMapScreenState } } + double _getPathDistance(List points) { + double totalDistance = 0.0; + final distanceCalculator = Distance(); + + for (int i = 0; i < points.length - 1; i++) { + totalDistance += distanceCalculator(points[i], points[i + 1]); + } + + return totalDistance; + } + @override Widget build(BuildContext context) { return Consumer( @@ -306,10 +334,15 @@ class _ChannelMessagePathMapScreenState connector.contacts, context.l10n, ); - final points = hops - .where((hop) => hop.hasLocation) - .map((hop) => hop.position!) - .toList(); + + final points = []; + for (final hop in hops) { + if (hop.hasLocation) { + points.add(hop.position!); + } + } + points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + final polylines = points.length > 1 ? [ Polyline( @@ -327,7 +360,10 @@ class _ChannelMessagePathMapScreenState final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null; - final mapKey = ValueKey(_formatPathPrefixes(selectedPath)); + final mapKey = ValueKey( + '${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}', + ); + _pathDistance = _getPathDistance(points); return Scaffold( appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)), @@ -487,6 +523,37 @@ class _ChannelMessagePathMapScreenState ), ), ), + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, + ), + width: 40, + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), ]; } @@ -509,7 +576,7 @@ class _ChannelMessagePathMapScreenState Padding( padding: const EdgeInsets.all(12), child: Text( - l10n.channelPath_repeaterHops, + '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', style: const TextStyle(fontWeight: FontWeight.w600), ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 3477361..f00f242 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import 'package:latlong2/latlong.dart'; @@ -701,6 +702,19 @@ class _ChatScreenState extends State { title: Text(context.l10n.chat_fullPath), content: SelectableText(formattedPath), actions: [ + TextButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: context.l10n.contacts_repeaterPathTrace, + path: Uint8List.fromList(pathBytes), + flipPathRound: true, + ), + ), + ), + child: Text(context.l10n.contacts_pathTrace), + ), TextButton( onPressed: () => Navigator.pop(context), child: Text(context.l10n.common_close), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index f04bc50..6799d69 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:meshcore_open/widgets/path_trace_dialog.dart'; import 'package:flutter/services.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -982,16 +982,16 @@ class _ContactsScreenState extends State ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), onTap: () { - showDialog( - context: context, - builder: (context) { - return PathTraceDialog( + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( title: contact.pathLength > 0 ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing, path: contact.traceRouteBytes ?? Uint8List(0), - ); - }, + ), + ), ); }, ), @@ -1010,16 +1010,16 @@ class _ContactsScreenState extends State ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), onTap: () { - showDialog( - context: context, - builder: (context) { - return PathTraceDialog( + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( title: contact.pathLength > 0 ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, path: contact.traceRouteBytes ?? Uint8List(0), - ); - }, + ), + ), ); }, ), @@ -1052,16 +1052,16 @@ class _ContactsScreenState extends State leading: const Icon(Icons.radar, color: Colors.green), title: Text(context.l10n.contacts_chatTraceRoute), onTap: () { - showDialog( - context: context, - builder: (context) { - return PathTraceDialog( + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_pathTraceTo( contact.name, ), path: contact.traceRouteBytes ?? Uint8List(0), - ); - }, + ), + ), ); }, ), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart new file mode 100644 index 0000000..b4b280d --- /dev/null +++ b/lib/screens/path_trace_map.dart @@ -0,0 +1,535 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:meshcore_open/l10n/l10n.dart'; +import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/services/map_tile_cache_service.dart'; +import 'package:meshcore_open/widgets/snr_indicator.dart'; +import 'package:provider/provider.dart'; + +class PathTraceData { + final Uint8List pathData; + final Uint8List snrData; + final Map pathContacts; + + PathTraceData({ + required this.pathData, + required this.snrData, + required this.pathContacts, + }); +} + +class PathTraceMapScreen extends StatefulWidget { + final String title; + final Uint8List path; + final bool flipPathRound; + final bool reversePathRound; + + const PathTraceMapScreen({ + super.key, + required this.title, + required this.path, + this.flipPathRound = false, + this.reversePathRound = false, + }); + + @override + State createState() => _PathTraceMapScreenState(); +} + +class _PathTraceMapScreenState extends State { + StreamSubscription? _frameSubscription; + Timer? _timeoutTimer; + + bool _isLoading = false; + bool _failed2Loaded = false; + bool _hasData = false; + PathTraceData? _traceData; + List _points = []; + List _polylines = []; + LatLng? _initialCenter = LatLng(0, 0); + double _initialZoom = 2.0; + LatLngBounds? _bounds; + ValueKey _mapKey = const ValueKey('initial'); + double _pathDistance = 0.0; + + String _formatPathPrefixes(Uint8List pathBytes) { + return pathBytes + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(','); + } + + @override + void initState() { + super.initState(); + _setupFrameListener(); + _doPathTrace(); + } + + @override + void dispose() { + _frameSubscription?.cancel(); + _timeoutTimer?.cancel(); + super.dispose(); + } + + Uint8List addReturnpath(Uint8List pathBytes) { + Uint8List? traceBytes; + final len = (pathBytes.length + pathBytes.length - 1); + traceBytes = Uint8List(len); + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length - 1) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + return traceBytes; + } + + double getPathDistance() { + double totalDistance = 0.0; + final distanceCalculator = Distance(); + + for (int i = 0; i < _points.length - 1; i++) { + totalDistance += distanceCalculator(_points[i], _points[i + 1]); + } + + return totalDistance; + } + + Future _doPathTrace() async { + if (mounted) { + setState(() { + _isLoading = true; + _failed2Loaded = false; + }); + } + + final Uint8List path; + + Uint8List pathTmp = widget.reversePathRound + ? Uint8List.fromList(widget.path.reversed.toList()) + : widget.path; + + if (widget.flipPathRound) { + path = addReturnpath(pathTmp); + } else { + path = pathTmp; + } + + final connector = Provider.of(context, listen: false); + final frame = buildTraceReq( + DateTime.now().millisecondsSinceEpoch ~/ 1000, + 0, //flags + 0, //auth + payload: path, + ); + connector.sendFrame(frame); + } + + void _setupFrameListener() { + final connector = Provider.of(context, listen: false); + Uint8List tagData = Uint8List(4); + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + final frameBuffer = BufferReader(frame); + final code = frameBuffer.readUInt8(); + + if (code == respCodeSent) { + frameBuffer.skipBytes(1); //reserved + tagData = frameBuffer.readBytes(4); + final timeoutSeconds = frameBuffer.readUInt32LE(); + + // Start timeout timer for trace response + _timeoutTimer?.cancel(); + _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () { + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + }); + } + + // Check if it's a binary response + if (code == pushCodeTraceData && + listEquals(frame.sublist(4, 8), tagData)) { + _timeoutTimer?.cancel(); + if (!mounted) return; + frameBuffer.skipBytes(3); //reserved + path length + flag + if (listEquals(frameBuffer.readBytes(4), tagData)) { + _handleTraceResponse(frame); + } + } + }); + } + + Future _handleTraceResponse(Uint8List frame) async { + final connector = Provider.of(context, listen: false); + + final buffer = BufferReader(frame); + buffer.skipBytes(2); // Skip push code and reserved byte + int pathLength = buffer.readUInt8(); + buffer.skipBytes(5); // Skip Flag byte and tag data + buffer.skipBytes(4); // Skip auth code + Uint8List pathData = buffer.readBytes(pathLength); + Uint8List snrData = buffer.readRemainingBytes(); + + Map pathContacts = {}; + + connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) { + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + } + } + }); + + setState(() { + _isLoading = false; + _hasData = true; + _traceData = PathTraceData( + pathData: pathData, + snrData: snrData, + pathContacts: pathContacts, + ); + _points = []; + _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + for (final hop in _traceData!.pathData) { + final contact = _traceData!.pathContacts[hop]; + if (contact != null && contact.hasLocation) { + _points.add(LatLng(contact.latitude!, contact.longitude!)); + } + } + _polylines = _points.length > 1 + ? [ + Polyline( + points: _points, + strokeWidth: 4, + color: Colors.blueAccent, + ), + ] + : []; + + _initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0); + _initialZoom = _points.isNotEmpty ? 13.0 : 2.0; + _bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null; + _mapKey = ValueKey( + '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', + ); + _pathDistance = getPathDistance(); + }); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, connector, _) { + final tileCache = context.read(); + + return Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + widget.title, + style: const TextStyle(fontSize: 24), + ), + ), + ], + ), + centerTitle: false, + actions: [ + IconButton( + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + onPressed: _isLoading ? null : _doPathTrace, + tooltip: context.l10n.pathTrace_refreshTooltip, + ), + ], + ), + body: SafeArea( + top: false, + child: Stack( + children: [ + if (!_hasData) + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isLoading) const CircularProgressIndicator(), + const SizedBox(height: 16), + if (!_isLoading && _failed2Loaded) + Text(context.l10n.pathTrace_notAvailable), + ], + ), + ), + if (_hasData) + FlutterMap( + key: _mapKey, + options: MapOptions( + initialCenter: _initialCenter!, + initialZoom: _initialZoom, + initialCameraFit: _bounds == null + ? null + : CameraFit.bounds( + bounds: _bounds!, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + minZoom: 2.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: + MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + if (_polylines.isNotEmpty) + PolylineLayer(polylines: _polylines), + if (_traceData!.pathData.isNotEmpty) + MarkerLayer( + markers: _buildHopMarkers(_traceData!.pathData), + ), + ], + ), + if (_points.isEmpty && + !_hasData && + !_isLoading && + !_failed2Loaded) + Center( + child: Card( + color: Colors.white.withValues(alpha: 0.9), + child: Padding( + padding: EdgeInsets.all(12), + child: Text( + context.l10n.channelPath_noRepeaterLocations, + ), + ), + ), + ), + if (_hasData) _buildLegendCard(context, _traceData!), + ], + ), + ), + ); + }, + ); + } + + List _buildHopMarkers(List pathData) { + return [ + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, + ), + width: 40, + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + for (final hop in pathData) + if (_traceData!.pathContacts[hop]!.hasLocation) + Marker( + point: LatLng( + _traceData!.pathContacts[hop]!.latitude!, + _traceData!.pathContacts[hop]!.longitude!, + ), + width: 40, + height: 40, + child: Container( + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + _traceData!.pathContacts[hop]!.publicKey + .sublist(0, 1) + .map( + (b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(), + ) + .join(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), + ]; + } + + String formatDirectionText(PathTraceData pathTraceData, int index) { + if (index == 0 || index == pathTraceData.snrData.length - 1) { + if (index == 0) { + return context.l10n.pathTrace_you; + } else { + final contactName = pathTraceData + .pathContacts[pathTraceData.pathData[pathTraceData.pathData.length - + 1]] + ?.name; + final hex = pathTraceData.pathData[pathTraceData.pathData.length - 1] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } + } else { + final contactName = + pathTraceData.pathContacts[pathTraceData.pathData[index - 1]]?.name; + final hex = pathTraceData.pathData[index - 1] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } + } + + String formatDirectionSubText(PathTraceData pathTraceData, int index) { + if (index == 0 || index == pathTraceData.snrData.length - 1) { + if (index == 0) { + final contactName = + pathTraceData.pathContacts[pathTraceData.pathData[0]]?.name; + final hex = pathTraceData.pathData[0] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } else { + return context.l10n.pathTrace_you; + } + } else { + final contactName = + pathTraceData.pathContacts[pathTraceData.pathData[index]]?.name; + final hex = pathTraceData.pathData[index] + .toRadixString(16) + .padLeft(2, '0') + .toUpperCase(); + return contactName != null ? "$hex: $contactName" : hex; + } + } + + Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) { + final l10n = context.l10n; + final maxHeight = MediaQuery.of(context).size.height * 0.35; + final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0); + final cardHeight = max(96.0, min(maxHeight, estimatedHeight)); + + return Positioned( + left: 16, + right: 16, + bottom: 16, + child: SizedBox( + height: cardHeight, + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Text( + '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + const Divider(height: 1), + Expanded( + child: pathTraceData.pathData.isEmpty + ? Center( + child: Text(l10n.channelPath_noHopDetailsAvailable), + ) + : ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: pathTraceData.pathData.length + 1, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + return Column( + children: [ + ListTile( + leading: + index >= pathTraceData.snrData.length / 2 + ? Icon(Icons.call_received) + : Icon(Icons.call_made), + title: Text( + formatDirectionText(pathTraceData, index), + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + formatDirectionSubText(pathTraceData, index), + style: const TextStyle(fontSize: 14), + ), + trailing: SNRIcon( + snr: + pathTraceData.snrData[index].toSigned(8) / + 4.0, + ), + onTap: () { + // Handle item tap + }, + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/path_trace_dialog.dart b/lib/widgets/path_trace_dialog.dart deleted file mode 100644 index 7294c86..0000000 --- a/lib/widgets/path_trace_dialog.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import '../connector/meshcore_connector.dart'; -import '../connector/meshcore_protocol.dart'; -import '../models/contact.dart'; -import '../widgets/snr_indicator.dart'; -import '../l10n/l10n.dart'; - -class PathTraceDialog extends StatefulWidget { - const PathTraceDialog({super.key, required this.title, required this.path}); - - final String title; - final Uint8List path; - - @override - State createState() => _PathTraceDialogState(); -} - -class _PathTraceDialogState extends State { - StreamSubscription? _frameSubscription; - Timer? _timeoutTimer; - - bool _isLoading = false; - bool _failed2Loaded = false; - bool _hasData = false; - Uint8List _pathData = Uint8List(0); - Uint8List _snrData = Uint8List(0); - Map _pathContacts = {}; - - @override - void initState() { - super.initState(); - _setupFrameListener(); - _doPathTrace(); - } - - @override - void dispose() { - _frameSubscription?.cancel(); - _timeoutTimer?.cancel(); - super.dispose(); - } - - Future _doPathTrace() async { - if (mounted) { - setState(() { - _isLoading = true; - _failed2Loaded = false; - }); - } - - final connector = Provider.of(context, listen: false); - final frame = buildTraceReq( - DateTime.now().millisecondsSinceEpoch ~/ 1000, - 0, //flags - 0, //auth - payload: widget.path, - ); - connector.sendFrame(frame); - } - - void _setupFrameListener() { - final connector = Provider.of(context, listen: false); - Uint8List tagData = Uint8List(4); - // Listen for incoming text messages from the repeater - _frameSubscription = connector.receivedFrames.listen((frame) { - if (frame.isEmpty) return; - final frameBuffer = BufferReader(frame); - final code = frameBuffer.readUInt8(); - - if (code == respCodeSent) { - frameBuffer.skipBytes(1); //reserved - tagData = frameBuffer.readBytes(4); - final timeoutSeconds = frameBuffer.readUInt32LE(); - - // Start timeout timer for trace response - _timeoutTimer?.cancel(); - _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () { - if (!mounted) return; - setState(() { - _isLoading = false; - _failed2Loaded = true; - }); - }); - } - - // Check if it's a binary response - if (code == pushCodeTraceData && - listEquals(frame.sublist(4, 8), tagData)) { - _timeoutTimer?.cancel(); - if (!mounted) return; - frameBuffer.skipBytes(3); //reserved + path length + flag - if (listEquals(frameBuffer.readBytes(4), tagData)) { - _handleTraceResponse(frame); - } - } - }); - } - - Future _handleTraceResponse(Uint8List frame) async { - final connector = Provider.of(context, listen: false); - - final buffer = BufferReader(frame); - buffer.skipBytes(2); // Skip push code and reserved byte - int pathLength = buffer.readUInt8(); - buffer.skipBytes(5); // Skip Flag byte and tag data - buffer.skipBytes(4); // Skip auth code - Uint8List pathData = buffer.readBytes(pathLength); - Uint8List snrData = buffer.readRemainingBytes(); - - Map pathContacts = {}; - - connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var neighbourData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([neighbourData]), - )) { - pathContacts[neighbourData] = repeater; - } - } - }); - - setState(() { - _isLoading = false; - _hasData = true; - _pathData = pathData; - _snrData = snrData; - _pathContacts = pathContacts; - }); - } - - String formatDirectionText(int index) { - if (index == 0 || index == _snrData.length - 1) { - if (index == 0) { - return context.l10n.pathTrace_you; - } else { - return _pathContacts[_pathData[_pathData.length - 1]]?.name ?? - "0x${_pathData[_pathData.length - 1].toRadixString(16).toUpperCase()}"; - } - } else { - return _pathContacts[_pathData[index - 1]]?.name ?? - "0x${_pathData[index - 1].toRadixString(16).toUpperCase()}"; - } - } - - String formatDirectionSubText(int index) { - if (index == 0 || index == _snrData.length - 1) { - if (index == 0) { - return _pathContacts[_pathData[0]]?.name ?? - "0x${_pathData[0].toRadixString(16).toUpperCase()}"; - } else { - return context.l10n.pathTrace_you; - } - } else { - return _pathContacts[_pathData[index]]?.name ?? - "0x${_pathData[index].toRadixString(16).toUpperCase()}"; - } - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return AlertDialog( - title: Column( - children: [ - FittedBox( - fit: BoxFit.scaleDown, - child: Text(widget.title, style: const TextStyle(fontSize: 24)), - ), - if (_failed2Loaded) - Text( - l10n.pathTrace_failed, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.error, - ), - ), - ], - ), - content: SafeArea( - child: RefreshIndicator( - onRefresh: _doPathTrace, - child: !_hasData - ? Center(child: Text(l10n.pathTrace_notAvailable)) - : ListView.builder( - itemCount: _snrData.length, - itemBuilder: (context, index) { - return Column( - children: [ - ListTile( - leading: index >= _snrData.length / 2 - ? Icon(Icons.call_received) - : Icon(Icons.call_made), - title: Text( - formatDirectionText(index), - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - formatDirectionSubText(index), - style: const TextStyle(fontSize: 14), - ), - trailing: SNRIcon( - snr: _snrData[index].toSigned(8) / 4.0, - ), - onTap: () { - // Handle item tap - }, - ), - if (index < _snrData.length - 1) - const Divider(height: 0.0), - ], - ); - }, - ), - ), - ), - actions: [ - IconButton( - icon: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh), - onPressed: _isLoading ? null : _doPathTrace, - tooltip: l10n.pathTrace_refreshTooltip, - ), - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(l10n.common_close), - ), - ], - ); - } -} From 2f4b230b31ca468a81278d131be92427152940fa Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 11:57:04 -0800 Subject: [PATCH 074/421] Add localization for missing location error in path tracing --- lib/l10n/app_bg.arb | 3 +- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_it.arb | 3 +- lib/l10n/app_localizations.dart | 6 ++++ lib/l10n/app_localizations_bg.dart | 4 +++ lib/l10n/app_localizations_de.dart | 4 +++ lib/l10n/app_localizations_en.dart | 4 +++ lib/l10n/app_localizations_es.dart | 4 +++ lib/l10n/app_localizations_fr.dart | 4 +++ lib/l10n/app_localizations_it.dart | 4 +++ lib/l10n/app_localizations_nl.dart | 4 +++ lib/l10n/app_localizations_pl.dart | 4 +++ lib/l10n/app_localizations_pt.dart | 4 +++ lib/l10n/app_localizations_ru.dart | 4 +++ lib/l10n/app_localizations_sk.dart | 4 +++ lib/l10n/app_localizations_sl.dart | 4 +++ lib/l10n/app_localizations_sv.dart | 4 +++ lib/l10n/app_localizations_uk.dart | 4 +++ lib/l10n/app_localizations_zh.dart | 3 ++ lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pl.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_sk.arb | 3 +- lib/l10n/app_sl.arb | 3 +- lib/l10n/app_sv.arb | 3 +- lib/l10n/app_uk.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/screens/channel_message_path_screen.dart | 4 +-- lib/screens/path_trace_map.dart | 35 ++++++++++++++++---- 33 files changed, 125 insertions(+), 22 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 460bc9d..f0998d5 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя." + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0a72559..da3a2c5 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1568,5 +1568,6 @@ "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen." + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + "pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee5cf7d..85e2647 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1316,6 +1316,7 @@ "pathTrace_failed": "Path trace failed.", "pathTrace_notAvailable": "Path trace not available.", "pathTrace_refreshTooltip": "Refresh Path Trace.", + "pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index c6dad1f..ac2d926 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1568,5 +1568,6 @@ "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", - "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado." + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", + "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c1157ed..71f1b1f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact." + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c32e863..bf217f5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", - "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti." + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", + "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 055667f..1a7a408 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4724,6 +4724,12 @@ abstract class AppLocalizations { /// **'Refresh Path Trace.'** String get pathTrace_refreshTooltip; + /// No description provided for @pathTrace_someHopsNoLocation. + /// + /// In en, this message translates to: + /// **'One or more of the hops is missing a location!'** + String get pathTrace_someHopsNoLocation; + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 701429e..523e729 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2695,6 +2695,10 @@ class AppLocalizationsBg extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Един или повече от хмелите липсва местоположение!'; + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 514a7a1..7e51cfb 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2699,6 +2699,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.'; + @override + String get pathTrace_someHopsNoLocation => + 'Eine oder mehrere der Hopfen fehlen einen Standort!'; + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 040d809..9dc1427 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2655,6 +2655,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Refresh Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'One or more of the hops is missing a location!'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e65cbcd..c81c9cb 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2694,6 +2694,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Actualizar Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Uno o más de los lúpulos carecen de una ubicación'; + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4496fc8..3e72ef0 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2711,6 +2711,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Actualiser Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Une ou plusieurs des houblons manquent d\'une localisation !'; + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02345c4..d465f30 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2695,6 +2695,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Uno o più dei luppoli mancano di una posizione!'; + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 292181f..1aa0610 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2685,6 +2685,10 @@ class AppLocalizationsNl extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Path Trace vernieuwen.'; + @override + String get pathTrace_someHopsNoLocation => + 'Een of meer van de hops ontbreken een locatie!'; + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 0832329..07ed0a6 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2693,6 +2693,10 @@ class AppLocalizationsPl extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.'; + @override + String get pathTrace_someHopsNoLocation => + 'Jeden lub więcej z chmieli nie ma określonej lokalizacji!'; + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eadea3b..3ec98d0 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2696,6 +2696,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Atualizar Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Um ou mais dos lúpulos estão sem localização!'; + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ec0f1ba..8dc1be7 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2698,6 +2698,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Обновить Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Одному или нескольким хмелям не указано местоположение!'; + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 346047b..8dfc5ab 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2681,6 +2681,10 @@ class AppLocalizationsSk extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Jedna alebo viac chmeľov chýba lokalita!'; + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ed71122..558423f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2684,6 +2684,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Osveži Path Trace.'; + @override + String get pathTrace_someHopsNoLocation => + 'Ena ali več hmelju manjka lokacija!'; + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 97b849f..24fe764 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2669,6 +2669,10 @@ class AppLocalizationsSv extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Uppdatera Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'En eller flera av humlen saknar en plats!'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 899d540..ec0630b 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2705,6 +2705,10 @@ class AppLocalizationsUk extends AppLocalizations { @override String get pathTrace_refreshTooltip => 'Оновити Path Trace'; + @override + String get pathTrace_someHopsNoLocation => + 'Одне або більше хмелів відсутнє місце розташування!'; + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7746792..021ebf3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2554,6 +2554,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathTrace_refreshTooltip => '重新绘制路径。'; + @override + String get pathTrace_someHopsNoLocation => '其中一个或多个啤酒花缺少位置!'; + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index e94deb3..e12738b 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", - "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden" + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", + "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 44552c3..b771357 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", - "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu." + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", + "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 56a7f2b..2601096 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1568,5 +1568,6 @@ "contacts_floodAdvert": "Anúncio de Inundação", "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", - "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato." + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", + "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0bca5ef..c62e6bb 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -808,5 +808,6 @@ "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению." + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d61cca6..7e39e2f 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", - "contacts_ShareContact": "Kopírovať kontakt do schránky" + "contacts_ShareContact": "Kopírovať kontakt do schránky", + "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index cbc4e3f..c8067df 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", - "contacts_ShareContact": "Kopiraj stik v Odložišče" + "contacts_ShareContact": "Kopiraj stik v Odložišče", + "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 05f77cb..9e2f3bf 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.", "contacts_ShareContact": "Kopiera kontakt till Urklipp", "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", - "contacts_ShareContactZeroHop": "Dela kontakt via annons" + "contacts_ShareContactZeroHop": "Dela kontakt via annons", + "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3362d40..429fcdb 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1568,5 +1568,6 @@ "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням" + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c941461..75de879 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1568,5 +1568,6 @@ "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。" + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!" } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 1646b73..d8a31fc 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -525,8 +525,8 @@ class _ChannelMessagePathMapScreenState ), Marker( point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, + context.read().selfLatitude ?? 0.0, + context.read().selfLongitude ?? 0.0, ), width: 40, height: 40, diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index b4b280d..e67c825 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -50,6 +50,7 @@ class _PathTraceMapScreenState extends State { bool _isLoading = false; bool _failed2Loaded = false; bool _hasData = false; + bool _noLocationErr = false; PathTraceData? _traceData; List _points = []; List _polylines = []; @@ -108,6 +109,7 @@ class _PathTraceMapScreenState extends State { setState(() { _isLoading = true; _failed2Loaded = false; + _noLocationErr = false; }); } @@ -159,7 +161,8 @@ class _PathTraceMapScreenState extends State { } // Check if it's a binary response - if (code == pushCodeTraceData && + if (frame.length > 8 && + code == pushCodeTraceData && listEquals(frame.sublist(4, 8), tagData)) { _timeoutTimer?.cancel(); if (!mounted) return; @@ -207,8 +210,13 @@ class _PathTraceMapScreenState extends State { _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); for (final hop in _traceData!.pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact != null && contact.hasLocation) { + if (contact != null && + contact.hasLocation && + contact.latitude != null && + contact.longitude != null) { _points.add(LatLng(contact.latitude!, contact.longitude!)); + } else { + _noLocationErr = true; } } _polylines = _points.length > 1 @@ -271,7 +279,20 @@ class _PathTraceMapScreenState extends State { top: false, child: Stack( children: [ - if (!_hasData) + if (_noLocationErr) + Center( + child: Card( + color: Colors.red, + child: Padding( + padding: EdgeInsets.all(12), + child: Text( + context.l10n.pathTrace_someHopsNoLocation, + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + if (!_hasData && _noLocationErr) Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -283,7 +304,7 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData) + if (_hasData && !_noLocationErr) FlutterMap( key: _mapKey, options: MapOptions( @@ -318,7 +339,8 @@ class _PathTraceMapScreenState extends State { if (_points.isEmpty && !_hasData && !_isLoading && - !_failed2Loaded) + !_failed2Loaded && + !_noLocationErr) Center( child: Card( color: Colors.white.withValues(alpha: 0.9), @@ -330,7 +352,8 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (_hasData) _buildLegendCard(context, _traceData!), + if (_hasData && !_noLocationErr) + _buildLegendCard(context, _traceData!), ], ), ), From bcae6ac19f73da9447cbfc89311ed33ddcf148ba Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 12:14:03 -0800 Subject: [PATCH 075/421] Updated GPX export functionality for contacts and repeaters in multiple languages. --- lib/l10n/app_bg.arb | 17 +++++- lib/l10n/app_de.arb | 17 +++++- lib/l10n/app_es.arb | 17 +++++- lib/l10n/app_fr.arb | 17 +++++- lib/l10n/app_it.arb | 17 +++++- lib/l10n/app_localizations.dart | 90 ++++++++++++++++++++++++++++++ lib/l10n/app_localizations_bg.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_de.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_es.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_fr.dart | 57 +++++++++++++++++++ lib/l10n/app_localizations_it.dart | 55 ++++++++++++++++++ lib/l10n/app_localizations_nl.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_pl.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_pt.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_ru.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_sk.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_sl.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_sv.dart | 54 ++++++++++++++++++ lib/l10n/app_localizations_uk.dart | 53 ++++++++++++++++++ lib/l10n/app_localizations_zh.dart | 45 +++++++++++++++ lib/l10n/app_nl.arb | 17 +++++- lib/l10n/app_pl.arb | 17 +++++- lib/l10n/app_pt.arb | 17 +++++- lib/l10n/app_ru.arb | 17 +++++- lib/l10n/app_sk.arb | 17 +++++- lib/l10n/app_sl.arb | 17 +++++- lib/l10n/app_sv.arb | 17 +++++- lib/l10n/app_uk.arb | 17 +++++- lib/l10n/app_zh.arb | 17 +++++- 30 files changed, 1112 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 460bc9d..7a9d522 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя." + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", + "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", + "settings_gpxExportAll": "Експортирай всички контакти в GPX", + "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", + "settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX", + "settings_gpxExportContacts": "Експортирай спътници към GPX", + "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", + "settings_gpxExportNoContacts": "Няма контакти за изlexport.", + "settings_gpxExportChat": "Местоположения на спътници", + "settings_gpxExportError": "Възникна грешка при изнасяне.", + "settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята", + "settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС", + "settings_gpxExportAllContacts": "Местоположения на всички контакти", + "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0a72559..add286c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1568,5 +1568,20 @@ "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen." + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + "settings_gpxExportAll": "Alle Kontakte nach GPX exportieren", + "settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.", + "settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren", + "settings_gpxExportContacts": "Begleiter nach GPX exportieren", + "settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.", + "settings_gpxExportContactsSubtitle": "Exportiert Begleiter mit einem Ort in eine GPX-Datei.", + "settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte", + "settings_gpxExportChat": "Begleiterstandorte", + "settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.", + "settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.", + "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", + "settings_gpxExportSuccess": "Erfolgreich GPX-Datei exportiert.", + "settings_gpxExportAllContacts": "Alle Kontaktstandorte", + "settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren", + "settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index c6dad1f..f808409 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1568,5 +1568,20 @@ "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", - "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado." + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", + "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", + "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", + "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", + "settings_gpxExportNoContacts": "No hay contactos para exportar.", + "settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO", + "settings_gpxExportError": "Hubo un error al exportar.", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.", + "settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.", + "settings_gpxExportAll": "Exportar todos los contactos a GPX", + "settings_gpxExportContacts": "Exportar compañeros a GPX", + "settings_gpxExportChat": "Ubicaciones de compañero", + "settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala", + "settings_gpxExportAllContacts": "Todas las ubicaciones de contactos", + "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c1157ed..532f791 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact." + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", + "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", + "settings_gpxExportNoContacts": "Aucun contact à exporter.", + "settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation", + "settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.", + "settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle", + "settings_gpxExportContacts": "Exporter les compagnons au format GPX", + "settings_gpxExportAll": "Exporter tous les contacts au format GPX", + "settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.", + "settings_gpxExportContactsSubtitle": "Exporte les compagnons avec un emplacement vers un fichier GPX.", + "settings_gpxExportChat": "Emplacements des compagnons", + "settings_gpxExportSuccess": "Fichier GPX exporté avec succès.", + "settings_gpxExportAllContacts": "Tous les emplacements des contacts", + "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c32e863..28aea4e 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", - "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti." + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", + "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", + "settings_gpxExportContacts": "Esporta compagni in GPX", + "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", + "settings_gpxExportNoContacts": "Nessun contatto da esportare.", + "settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo", + "settings_gpxExportError": "Si è verificato un errore durante l'esportazione.", + "settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.", + "settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.", + "settings_gpxExportAll": "Esporta tutti i contatti in GPX", + "settings_gpxExportAllSubtitle": "Esporta tutti i contatti con una posizione in un file GPX.", + "settings_gpxExportChat": "Posizioni dei compagni", + "settings_gpxExportRepeatersRoom": "Posizioni del server ripetitore e della stanza", + "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", + "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 055667f..2ec2fa8 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4855,6 +4855,96 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Copying advert to Clipboard failed.'** String get contacts_contactAdvertCopyFailed; + + /// No description provided for @settings_gpxExportRepeaters. + /// + /// In en, this message translates to: + /// **'Export repeaters / room server to GPX'** + String get settings_gpxExportRepeaters; + + /// No description provided for @settings_gpxExportRepeatersSubtitle. + /// + /// In en, this message translates to: + /// **'Exports repeaters / roomserver with a location to GPX file.'** + String get settings_gpxExportRepeatersSubtitle; + + /// No description provided for @settings_gpxExportContacts. + /// + /// In en, this message translates to: + /// **'Export companions to GPX'** + String get settings_gpxExportContacts; + + /// No description provided for @settings_gpxExportContactsSubtitle. + /// + /// In en, this message translates to: + /// **'Exports companions with a location to GPX file.'** + String get settings_gpxExportContactsSubtitle; + + /// No description provided for @settings_gpxExportAll. + /// + /// In en, this message translates to: + /// **'Export all contacts to GPX'** + String get settings_gpxExportAll; + + /// No description provided for @settings_gpxExportAllSubtitle. + /// + /// In en, this message translates to: + /// **'Exports all contacts with a location to GPX file.'** + String get settings_gpxExportAllSubtitle; + + /// No description provided for @settings_gpxExportSuccess. + /// + /// In en, this message translates to: + /// **'Successfully exported GPX file.'** + String get settings_gpxExportSuccess; + + /// No description provided for @settings_gpxExportNoContacts. + /// + /// In en, this message translates to: + /// **'No contacts to export.'** + String get settings_gpxExportNoContacts; + + /// No description provided for @settings_gpxExportNotAvailable. + /// + /// In en, this message translates to: + /// **'Not supported on your device/OS'** + String get settings_gpxExportNotAvailable; + + /// No description provided for @settings_gpxExportError. + /// + /// In en, this message translates to: + /// **'There was an error when exporting.'** + String get settings_gpxExportError; + + /// No description provided for @settings_gpxExportRepeatersRoom. + /// + /// In en, this message translates to: + /// **'Repeater & room server locations'** + String get settings_gpxExportRepeatersRoom; + + /// No description provided for @settings_gpxExportChat. + /// + /// In en, this message translates to: + /// **'Companion locations'** + String get settings_gpxExportChat; + + /// No description provided for @settings_gpxExportAllContacts. + /// + /// In en, this message translates to: + /// **'All contacts locations'** + String get settings_gpxExportAllContacts; + + /// No description provided for @settings_gpxExportShareText. + /// + /// In en, this message translates to: + /// **'Map data exported from meshcore-open'** + String get settings_gpxExportShareText; + + /// No description provided for @settings_gpxExportShareSubject. + /// + /// In en, this message translates to: + /// **'meshcore-open GPX map data export'** + String get settings_gpxExportShareSubject; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 701429e..45a0267 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2766,4 +2766,58 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Копирането на обявата в клипборда не успя.'; + + @override + String get settings_gpxExportRepeaters => + 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Изпраща повторители / roomserver с местоположение в GPX файл.'; + + @override + String get settings_gpxExportContacts => 'Експортирай спътници към GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Експортира спътници с местоположение в GPX файл.'; + + @override + String get settings_gpxExportAll => 'Експортирай всички контакти в GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Експортира всички контакти с местоположение в файл GPX.'; + + @override + String get settings_gpxExportSuccess => 'Успешно изlexport на файл GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Няма контакти за изlexport.'; + + @override + String get settings_gpxExportNotAvailable => + 'Не е поддържан на вашето устройство/ОС'; + + @override + String get settings_gpxExportError => 'Възникна грешка при изнасяне.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Местоположения на повторител и сървър на стаята'; + + @override + String get settings_gpxExportChat => 'Местоположения на спътници'; + + @override + String get settings_gpxExportAllContacts => + 'Местоположения на всички контакти'; + + @override + String get settings_gpxExportShareText => + 'Картинни данни изнесени от meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open износ на данни за карта в формат GPX'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 514a7a1..8c5c50b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2773,4 +2773,58 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; + + @override + String get settings_gpxExportRepeaters => + 'Repeater und Raumserver nach GPX exportieren'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.'; + + @override + String get settings_gpxExportContacts => 'Begleiter nach GPX exportieren'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exportiert Begleiter mit einem Ort in eine GPX-Datei.'; + + @override + String get settings_gpxExportAll => 'Alle Kontakte nach GPX exportieren'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.'; + + @override + String get settings_gpxExportSuccess => 'Erfolgreich GPX-Datei exportiert.'; + + @override + String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.'; + + @override + String get settings_gpxExportNotAvailable => + 'Nicht auf Ihrem Gerät/Betriebssystem unterstützt'; + + @override + String get settings_gpxExportError => + 'Beim Export ist ein Fehler aufgetreten.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater- und Raumserver-Standorte'; + + @override + String get settings_gpxExportChat => 'Begleiterstandorte'; + + @override + String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte'; + + @override + String get settings_gpxExportShareText => + 'Kartendaten aus meshcore-open exportiert'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open GPX-Kartendaten exportieren'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 040d809..6667a36 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2723,4 +2723,57 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Copying advert to Clipboard failed.'; + + @override + String get settings_gpxExportRepeaters => + 'Export repeaters / room server to GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exports repeaters / roomserver with a location to GPX file.'; + + @override + String get settings_gpxExportContacts => 'Export companions to GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exports companions with a location to GPX file.'; + + @override + String get settings_gpxExportAll => 'Export all contacts to GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exports all contacts with a location to GPX file.'; + + @override + String get settings_gpxExportSuccess => 'Successfully exported GPX file.'; + + @override + String get settings_gpxExportNoContacts => 'No contacts to export.'; + + @override + String get settings_gpxExportNotAvailable => + 'Not supported on your device/OS'; + + @override + String get settings_gpxExportError => 'There was an error when exporting.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater & room server locations'; + + @override + String get settings_gpxExportChat => 'Companion locations'; + + @override + String get settings_gpxExportAllContacts => 'All contacts locations'; + + @override + String get settings_gpxExportShareText => + 'Map data exported from meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open GPX map data export'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e65cbcd..4661c25 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2766,4 +2766,58 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Copiar anuncio al Portapapeles ha fallado.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportar repetidores / servidor de sala a GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporta repetidores o roomserver con una ubicación a un archivo GPX.'; + + @override + String get settings_gpxExportContacts => 'Exportar compañeros a GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporta compañeros con una ubicación a archivo GPX.'; + + @override + String get settings_gpxExportAll => 'Exportar todos los contactos a GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporta todos los contactos con una ubicación a un archivo GPX.'; + + @override + String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.'; + + @override + String get settings_gpxExportNoContacts => 'No hay contactos para exportar.'; + + @override + String get settings_gpxExportNotAvailable => + 'No compatible con tu dispositivo/SO'; + + @override + String get settings_gpxExportError => 'Hubo un error al exportar.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Ubicaciones del servidor de repetidor y sala'; + + @override + String get settings_gpxExportChat => 'Ubicaciones de compañero'; + + @override + String get settings_gpxExportAllContacts => + 'Todas las ubicaciones de contactos'; + + @override + String get settings_gpxExportShareText => + 'Datos del mapa exportados desde meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open exportación de datos de mapa GPX'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4496fc8..f0d8148 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2787,4 +2787,61 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'La copie de l\'annonce vers le presse-papiers a échoué.'; + + @override + String get settings_gpxExportRepeaters => + 'Exporter les répéteurs / serveur de salle au format GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.'; + + @override + String get settings_gpxExportContacts => + 'Exporter les compagnons au format GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporte les compagnons avec un emplacement vers un fichier GPX.'; + + @override + String get settings_gpxExportAll => + 'Exporter tous les contacts au format GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporte tous les contacts avec une localisation vers un fichier GPX.'; + + @override + String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.'; + + @override + String get settings_gpxExportNoContacts => 'Aucun contact à exporter.'; + + @override + String get settings_gpxExportNotAvailable => + 'Non pris en charge sur votre appareil/Système d\'exploitation'; + + @override + String get settings_gpxExportError => + 'Une erreur s\'est produite lors de l\'exportation.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Emplacements des serveurs de répéteur et de salle'; + + @override + String get settings_gpxExportChat => 'Emplacements des compagnons'; + + @override + String get settings_gpxExportAllContacts => + 'Tous les emplacements des contacts'; + + @override + String get settings_gpxExportShareText => + 'Données de carte exportées à partir de meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open exporter les données de carte GPX'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02345c4..25c55af 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2769,4 +2769,59 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Copia dell\'annuncio nella Clipboard non riuscita.'; + + @override + String get settings_gpxExportRepeaters => + 'Esporta ripetitori / server di stanza in GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Esporta ripetitori / roomserver con una posizione in un file GPX.'; + + @override + String get settings_gpxExportContacts => 'Esporta compagni in GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Esporta i compagni con una posizione in un file GPX.'; + + @override + String get settings_gpxExportAll => 'Esporta tutti i contatti in GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Esporta tutti i contatti con una posizione in un file GPX.'; + + @override + String get settings_gpxExportSuccess => + 'Esportazione del file GPX completata con successo.'; + + @override + String get settings_gpxExportNoContacts => 'Nessun contatto da esportare.'; + + @override + String get settings_gpxExportNotAvailable => + 'Non supportato sul tuo dispositivo/Sistema Operativo'; + + @override + String get settings_gpxExportError => + 'Si è verificato un errore durante l\'esportazione.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Posizioni del server ripetitore e della stanza'; + + @override + String get settings_gpxExportChat => 'Posizioni dei compagni'; + + @override + String get settings_gpxExportAllContacts => 'Tutte le posizioni dei contatti'; + + @override + String get settings_gpxExportShareText => + 'Dati mappa esportati da meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open esportazione dati mappa GPX'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 292181f..5940333 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2758,4 +2758,57 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiëren van advertentie naar Clipboard is mislukt.'; + + @override + String get settings_gpxExportRepeaters => + 'Exporteer repeaters / roomserver naar GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporteert repeaters / roomserver met een locatie naar GPX-bestand.'; + + @override + String get settings_gpxExportContacts => 'Companions exporteren naar GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporteert metgezellen met een locatie naar een GPX-bestand.'; + + @override + String get settings_gpxExportAll => 'Alle contacten exporteren naar GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporteert alle contacten met een locatie naar een GPX-bestand.'; + + @override + String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.'; + + @override + String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.'; + + @override + String get settings_gpxExportNotAvailable => + 'Niet ondersteund op uw apparaat/besturingssysteem'; + + @override + String get settings_gpxExportError => 'Er was een fout bij het exporteren.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater- en kamer servers locaties'; + + @override + String get settings_gpxExportChat => 'Locaties van metgezellen'; + + @override + String get settings_gpxExportAllContacts => 'Alle contactlocaties'; + + @override + String get settings_gpxExportShareText => + 'Kaartgegevens geëxporteerd uit meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open GPX kaartgegevens exporteren'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 0832329..00b5c1e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2766,4 +2766,58 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; + + @override + String get settings_gpxExportRepeaters => + 'Eksportuj powtórki / serwer pokojowy do GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.'; + + @override + String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Eksportuje towarzyszy z lokalizacją do pliku GPX.'; + + @override + String get settings_gpxExportAll => 'Eksportuj wszystkie kontakty do GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.'; + + @override + String get settings_gpxExportSuccess => 'Pomyślnie wyeksportowano plik GPX.'; + + @override + String get settings_gpxExportNoContacts => + 'Brak kontaktów do wyeksportowania.'; + + @override + String get settings_gpxExportNotAvailable => + 'Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym'; + + @override + String get settings_gpxExportError => 'Wystąpił błąd podczas eksportowania.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Lokalizacje serwerów powtarzających i pomieszczeń'; + + @override + String get settings_gpxExportChat => 'Lokalizacje towarzyszy'; + + @override + String get settings_gpxExportAllContacts => 'Wszystkie lokalizacje kontaktów'; + + @override + String get settings_gpxExportShareText => + 'Dane mapy wyeksportowane z meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'Eksport danych mapy GPX meshcore-open'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eadea3b..e4877c8 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2768,4 +2768,57 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Cópia do anúncio para a Área de Transferência falhou.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportar repetidores / servidor de sala para GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporta repetidores / roomserver com localização para arquivo GPX.'; + + @override + String get settings_gpxExportContacts => 'Exportar companheiros para GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporta companheiros com uma localização para um arquivo GPX.'; + + @override + String get settings_gpxExportAll => 'Exportar todos os contatos para GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporta todos os contatos com uma localização para um arquivo GPX.'; + + @override + String get settings_gpxExportSuccess => 'Arquivo GPX exportado com sucesso.'; + + @override + String get settings_gpxExportNoContacts => 'Nenhum contato para exportar.'; + + @override + String get settings_gpxExportNotAvailable => + 'Não suportado no seu dispositivo/SO'; + + @override + String get settings_gpxExportError => 'Ocorreu um erro ao exportar.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Localizações do servidor de repetidor e sala'; + + @override + String get settings_gpxExportChat => 'Localizações de companheiros'; + + @override + String get settings_gpxExportAllContacts => 'Todos os locais de contatos'; + + @override + String get settings_gpxExportShareText => + 'Dados do mapa exportados do meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open exportação de dados de mapa GPX'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ec0f1ba..4facd82 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2773,4 +2773,57 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Копирование рекламы в буфер обмена не удалось.'; + + @override + String get settings_gpxExportRepeaters => + 'Экспортировать рипитеры / сервер комнаты в GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.'; + + @override + String get settings_gpxExportContacts => 'Экспортировать спутников в GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Экспортирует спутников с местоположением в файл GPX.'; + + @override + String get settings_gpxExportAll => 'Экспортировать все контакты в GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Экспортирует все контакты с местоположением в файл GPX.'; + + @override + String get settings_gpxExportSuccess => 'Успешно экспортирован файл GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Нет контактов для экспорта.'; + + @override + String get settings_gpxExportNotAvailable => + 'Не поддерживается на вашем устройстве/ОС'; + + @override + String get settings_gpxExportError => 'Произошла ошибка при экспорте.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Местоположения повторителей и серверов комнат'; + + @override + String get settings_gpxExportChat => 'Местоположения спутников'; + + @override + String get settings_gpxExportAllContacts => 'Все местоположения контактов'; + + @override + String get settings_gpxExportShareText => + 'Данные карты экспортированы из meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open экспорт данных карты GPX'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 346047b..c555123 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2752,4 +2752,57 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopírovanie inzerátu do schránky zlyhalo.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportovať repeater / server miestnosti do GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exportuje repeater / roomserver s lokalitou do súboru GPX.'; + + @override + String get settings_gpxExportContacts => 'Export sprievodcov do GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exportuje sprievodcov s umiestnením do súboru GPX.'; + + @override + String get settings_gpxExportAll => 'Exportovať všetky kontakty do GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exportuje všetky kontakty s lokalitou do súboru GPX.'; + + @override + String get settings_gpxExportSuccess => 'Úspešne exportovaný súbor GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.'; + + @override + String get settings_gpxExportNotAvailable => + 'Nie je podporované na vašom zariadení/operáciomnom systéme'; + + @override + String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Umiestnenia opakovačov a serverov miestností'; + + @override + String get settings_gpxExportChat => 'Lokácie sprievodcov'; + + @override + String get settings_gpxExportAllContacts => 'Všetky kontaktné lokality'; + + @override + String get settings_gpxExportShareText => + 'Mapové údaje exportované z meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open export dát GPX mapových údajov'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ed71122..7190a9c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2754,4 +2754,57 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiranje oglasa v odložišče je spodletelo.'; + + @override + String get settings_gpxExportRepeaters => + 'Izvoz ponoviteljev / strežnika sobe v GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.'; + + @override + String get settings_gpxExportContacts => 'Izvoz spremljevalcev v GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Izvozi spremljevalce z lokacijo v datoteko GPX.'; + + @override + String get settings_gpxExportAll => 'Izvozi vse kontakte v GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Izvozi vse kontakte z lokacijo v datoteko GPX.'; + + @override + String get settings_gpxExportSuccess => 'Uspešno izvoz GPX datoteke.'; + + @override + String get settings_gpxExportNoContacts => 'Ni stikov za izvoz.'; + + @override + String get settings_gpxExportNotAvailable => + 'Ni podprto na vašem napravi/operacijskem sistemu'; + + @override + String get settings_gpxExportError => 'Pri izvozu je prišlo do napake.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Lokacije ponovljivca in strežnika sobe'; + + @override + String get settings_gpxExportChat => 'Lokacije spremljevalcev'; + + @override + String get settings_gpxExportAllContacts => 'Lokacije vseh stikov'; + + @override + String get settings_gpxExportShareText => + 'Podatki kart izvoženi iz meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open izvoz podatkov GPX karte'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 97b849f..63f3ea2 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2739,4 +2739,58 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Kopiering av annons till Urklipp misslyckades.'; + + @override + String get settings_gpxExportRepeaters => + 'Exportera repeater / rumsservrar till GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Exporterar repeater / roomserver med plats till GPX-fil.'; + + @override + String get settings_gpxExportContacts => 'Exportera följeslagare till GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Exporterar följeslagare med en plats till GPX-fil.'; + + @override + String get settings_gpxExportAll => 'Exportera alla kontakter till GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Exporterar alla kontakter med en plats till GPX-fil.'; + + @override + String get settings_gpxExportSuccess => 'Har exporterat GPX-fil med framgång'; + + @override + String get settings_gpxExportNoContacts => 'Inga kontakter att exportera.'; + + @override + String get settings_gpxExportNotAvailable => + 'Stöds inte på din enhet/operativsystem'; + + @override + String get settings_gpxExportError => + 'Det uppstod ett fel när data exporterades.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Repeater- och rumsserverplatser'; + + @override + String get settings_gpxExportChat => 'Medhjälparplatser'; + + @override + String get settings_gpxExportAllContacts => 'Alla kontakters platser'; + + @override + String get settings_gpxExportShareText => + 'Kartdata exporterad från meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'meshcore-open export av GPX-kartdata'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 899d540..838961a 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2779,4 +2779,57 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => 'Копіювання оголошення в буфер обміну завершилося невдало'; + + @override + String get settings_gpxExportRepeaters => + 'Експортувати ретранслятори / сервер кімнати до GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => + 'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.'; + + @override + String get settings_gpxExportContacts => 'Експортувати супутників до GPX'; + + @override + String get settings_gpxExportContactsSubtitle => + 'Експортує супутників з місцезнаходженням у файл GPX.'; + + @override + String get settings_gpxExportAll => 'Експортувати всі контакти до GPX'; + + @override + String get settings_gpxExportAllSubtitle => + 'Експортує всі контакти з місцем розташування у файл GPX.'; + + @override + String get settings_gpxExportSuccess => 'Успішно експортовано файл GPX.'; + + @override + String get settings_gpxExportNoContacts => 'Немає контактів для експорту.'; + + @override + String get settings_gpxExportNotAvailable => + 'Не підтримується на вашому пристрої/операційній системі'; + + @override + String get settings_gpxExportError => 'Сталася помилка під час експорту.'; + + @override + String get settings_gpxExportRepeatersRoom => + 'Місцезнаходження повторювача та сервера кімнати'; + + @override + String get settings_gpxExportChat => 'Місця супутників'; + + @override + String get settings_gpxExportAllContacts => 'Усі місця контактів'; + + @override + String get settings_gpxExportShareText => + 'Дані карти експортовані з meshcore-open'; + + @override + String get settings_gpxExportShareSubject => + 'експорт даних карти meshcore-open у форматі GPX'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7746792..020f795 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2621,4 +2621,49 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => '将广告复制到剪贴板操作失败。'; + + @override + String get settings_gpxExportRepeaters => '导出重复器/房间服务器到GPX'; + + @override + String get settings_gpxExportRepeatersSubtitle => '导出带有位置的重复器/房间服务器到GPX文件。'; + + @override + String get settings_gpxExportContacts => '导出伴侣到GPX'; + + @override + String get settings_gpxExportContactsSubtitle => '导出带有位置的伙伴到GPX文件。'; + + @override + String get settings_gpxExportAll => '导出所有联系人到GPX'; + + @override + String get settings_gpxExportAllSubtitle => '导出所有带有位置的联系人到GPX文件。'; + + @override + String get settings_gpxExportSuccess => '成功导出GPX文件'; + + @override + String get settings_gpxExportNoContacts => '没有联系人可导出'; + + @override + String get settings_gpxExportNotAvailable => '您的设备/操作系统不支持'; + + @override + String get settings_gpxExportError => '导出时发生错误'; + + @override + String get settings_gpxExportRepeatersRoom => '重复器和房间服务器位置'; + + @override + String get settings_gpxExportChat => '伴侣位置'; + + @override + String get settings_gpxExportAllContacts => '所有联系人位置'; + + @override + String get settings_gpxExportShareText => '来自meshcore-open的导出地图数据'; + + @override + String get settings_gpxExportShareSubject => 'meshcore-open GPX 地图数据导出'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index e94deb3..4dbd506 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", - "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden" + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", + "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", + "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", + "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", + "settings_gpxExportNoContacts": "Geen contacten om te exporteren.", + "settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem", + "settings_gpxExportError": "Er was een fout bij het exporteren.", + "settings_gpxExportContacts": "Companions exporteren naar GPX", + "settings_gpxExportAll": "Alle contacten exporteren naar GPX", + "settings_gpxExportAllSubtitle": "Exporteert alle contacten met een locatie naar een GPX-bestand.", + "settings_gpxExportContactsSubtitle": "Exporteert metgezellen met een locatie naar een GPX-bestand.", + "settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties", + "settings_gpxExportChat": "Locaties van metgezellen", + "settings_gpxExportAllContacts": "Alle contactlocaties", + "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 44552c3..5327215 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", - "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu." + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", + "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", + "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", + "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", + "settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.", + "settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym", + "settings_gpxExportError": "Wystąpił błąd podczas eksportowania.", + "settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzających i pomieszczeń", + "settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.", + "settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX", + "settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.", + "settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów", + "settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.", + "settings_gpxExportChat": "Lokalizacje towarzyszy", + "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", + "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 56a7f2b..7da9019 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1568,5 +1568,20 @@ "contacts_floodAdvert": "Anúncio de Inundação", "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", - "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato." + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", + "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", + "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", + "settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.", + "settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO", + "settings_gpxExportError": "Ocorreu um erro ao exportar.", + "settings_gpxExportAll": "Exportar todos os contatos para GPX", + "settings_gpxExportContacts": "Exportar companheiros para GPX", + "settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.", + "settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala", + "settings_gpxExportChat": "Localizações de companheiros", + "settings_gpxExportNoContacts": "Nenhum contato para exportar.", + "settings_gpxExportAllContacts": "Todos os locais de contatos", + "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0bca5ef..e0f4402 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -808,5 +808,20 @@ "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению." + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", + "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", + "settings_gpxExportContacts": "Экспортировать спутников в GPX", + "settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС", + "settings_gpxExportError": "Произошла ошибка при экспорте.", + "settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат", + "settings_gpxExportChat": "Местоположения спутников", + "settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.", + "settings_gpxExportAll": "Экспортировать все контакты в GPX", + "settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.", + "settings_gpxExportAllContacts": "Все местоположения контактов", + "settings_gpxExportSuccess": "Успешно экспортирован файл GPX.", + "settings_gpxExportNoContacts": "Нет контактов для экспорта.", + "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d61cca6..e72888e 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", - "contacts_ShareContact": "Kopírovať kontakt do schránky" + "contacts_ShareContact": "Kopírovať kontakt do schránky", + "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", + "settings_gpxExportContacts": "Export sprievodcov do GPX", + "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", + "settings_gpxExportNoContacts": "Žiadne kontakty na export.", + "settings_gpxExportNotAvailable": "Nie je podporované na vašom zariadení/operáciomnom systéme", + "settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností", + "settings_gpxExportError": "Vyskytol sa chyba počas exportu.", + "settings_gpxExportAllSubtitle": "Exportuje všetky kontakty s lokalitou do súboru GPX.", + "settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.", + "settings_gpxExportRepeaters": "Exportovať repeater / server miestnosti do GPX", + "settings_gpxExportAll": "Exportovať všetky kontakty do GPX", + "settings_gpxExportAllContacts": "Všetky kontaktné lokality", + "settings_gpxExportChat": "Lokácie sprievodcov", + "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index cbc4e3f..97cd1f8 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", - "contacts_ShareContact": "Kopiraj stik v Odložišče" + "contacts_ShareContact": "Kopiraj stik v Odložišče", + "settings_gpxExportAll": "Izvozi vse kontakte v GPX", + "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", + "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", + "settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX", + "settings_gpxExportError": "Pri izvozu je prišlo do napake.", + "settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe", + "settings_gpxExportChat": "Lokacije spremljevalcev", + "settings_gpxExportAllContacts": "Lokacije vseh stikov", + "settings_gpxExportContactsSubtitle": "Izvozi spremljevalce z lokacijo v datoteko GPX.", + "settings_gpxExportAllSubtitle": "Izvozi vse kontakte z lokacijo v datoteko GPX.", + "settings_gpxExportSuccess": "Uspešno izvoz GPX datoteke.", + "settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open", + "settings_gpxExportNoContacts": "Ni stikov za izvoz.", + "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", + "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 05f77cb..03844f3 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.", "contacts_ShareContact": "Kopiera kontakt till Urklipp", "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", - "contacts_ShareContactZeroHop": "Dela kontakt via annons" + "contacts_ShareContactZeroHop": "Dela kontakt via annons", + "settings_gpxExportAll": "Exportera alla kontakter till GPX", + "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", + "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", + "settings_gpxExportNoContacts": "Inga kontakter att exportera.", + "settings_gpxExportNotAvailable": "Stöds inte på din enhet/operativsystem", + "settings_gpxExportRepeatersRoom": "Repeater- och rumsserverplatser", + "settings_gpxExportRepeaters": "Exportera repeater / rumsservrar till GPX", + "settings_gpxExportAllSubtitle": "Exporterar alla kontakter med en plats till GPX-fil.", + "settings_gpxExportContacts": "Exportera följeslagare till GPX", + "settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.", + "settings_gpxExportChat": "Medhjälparplatser", + "settings_gpxExportError": "Det uppstod ett fel när data exporterades.", + "settings_gpxExportAllContacts": "Alla kontakters platser", + "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", + "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3362d40..796be1e 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1568,5 +1568,20 @@ "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням" + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", + "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", + "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", + "settings_gpxExportNoContacts": "Немає контактів для експорту.", + "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", + "settings_gpxExportError": "Сталася помилка під час експорту.", + "settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", + "settings_gpxExportAll": "Експортувати всі контакти до GPX", + "settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", + "settings_gpxExportContacts": "Експортувати супутників до GPX", + "settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", + "settings_gpxExportChat": "Місця супутників", + "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", + "settings_gpxExportAllContacts": "Усі місця контактів", + "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c941461..a71541b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1568,5 +1568,20 @@ "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。" + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", + "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", + "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", + "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", + "settings_gpxExportSuccess": "成功导出GPX文件", + "settings_gpxExportError": "导出时发生错误", + "settings_gpxExportRepeatersRoom": "重复器和房间服务器位置", + "settings_gpxExportChat": "伴侣位置", + "settings_gpxExportAll": "导出所有联系人到GPX", + "settings_gpxExportContacts": "导出伴侣到GPX", + "settings_gpxExportAllSubtitle": "导出所有带有位置的联系人到GPX文件。", + "settings_gpxExportAllContacts": "所有联系人位置", + "settings_gpxExportNoContacts": "没有联系人可导出", + "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", + "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出" } From 0d8801fa7537d0540a67b13590adb51e9cdfb9c8 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 12:25:51 -0800 Subject: [PATCH 076/421] Add scrollbar to path trace details list for improved navigation --- lib/screens/path_trace_map.dart | 69 ++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index e67c825..2e557a5 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -515,38 +515,45 @@ class _PathTraceMapScreenState extends State { ? Center( child: Text(l10n.channelPath_noHopDetailsAvailable), ) - : ListView.separated( - padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: pathTraceData.pathData.length + 1, - separatorBuilder: (_, __) => const Divider(height: 1), - itemBuilder: (context, index) { - return Column( - children: [ - ListTile( - leading: - index >= pathTraceData.snrData.length / 2 - ? Icon(Icons.call_received) - : Icon(Icons.call_made), - title: Text( - formatDirectionText(pathTraceData, index), - style: const TextStyle(fontSize: 14), + : Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: pathTraceData.pathData.length + 1, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + return Column( + children: [ + ListTile( + leading: + index >= pathTraceData.snrData.length / 2 + ? Icon(Icons.call_received) + : Icon(Icons.call_made), + title: Text( + formatDirectionText(pathTraceData, index), + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + formatDirectionSubText( + pathTraceData, + index, + ), + style: const TextStyle(fontSize: 14), + ), + trailing: SNRIcon( + snr: + pathTraceData.snrData[index].toSigned( + 8, + ) / + 4.0, + ), + onTap: () { + // Handle item tap + }, ), - subtitle: Text( - formatDirectionSubText(pathTraceData, index), - style: const TextStyle(fontSize: 14), - ), - trailing: SNRIcon( - snr: - pathTraceData.snrData[index].toSigned(8) / - 4.0, - ), - onTap: () { - // Handle item tap - }, - ), - ], - ); - }, + ], + ); + }, + ), ), ), ], From 2db30ace6a1e3f7937fd562697ca5961810dddcd Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 8 Feb 2026 12:26:49 -0800 Subject: [PATCH 077/421] Integrate SharePlus plugin for enhanced sharing functionality across platforms --- lib/utils/gpx_export.dart | 60 ++++++++----------- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 50 +++++++++++++++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 5 files changed, 80 insertions(+), 36 deletions(-) diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 92494c9..595479c 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -54,50 +54,50 @@ class GpxExport { } void addRepeaters() { - final contacts = _connector.contacts; - final repeaters = contacts + final contacts = _connector.contacts .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) .toList(); - for (var repeater in repeaters) { - if (repeater.latitude == null || repeater.longitude == null) { + for (var contact in contacts) { + if (contact.latitude == null || contact.longitude == null) { continue; } _addContact( - repeater.name, - repeater.latitude ?? 0.0, - repeater.longitude ?? 0.0, - "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + contact.name, + contact.latitude!, + contact.longitude!, + "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", ); } } void addContacts() { - final contacts = _connector.contacts; - final repeaters = contacts.where((c) => c.type == advTypeChat).toList(); - for (var repeater in repeaters) { - if (repeater.latitude == null || repeater.longitude == null) { + final contacts = _connector.contacts + .where((c) => c.type == advTypeChat) + .toList(); + for (var contact in contacts) { + if (contact.latitude == null || contact.longitude == null) { continue; } _addContact( - repeater.name, - repeater.latitude ?? 0.0, - repeater.longitude ?? 0.0, - "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + contact.name, + contact.latitude!, + contact.longitude!, + "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", ); } } void addAll() { final contacts = _connector.contacts; - for (var repeater in contacts.toList()) { - if (repeater.latitude == null || repeater.longitude == null) { + for (var contact in contacts.toList()) { + if (contact.latitude == null || contact.longitude == null) { continue; } _addContact( - repeater.name, - repeater.latitude ?? 0.0, - repeater.longitude ?? 0.0, - "Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}", + contact.name, + contact.latitude ?? 0.0, + contact.longitude ?? 0.0, + "Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}", ); } } @@ -149,23 +149,17 @@ class GpxExport { .split('T') .join('_'); - // ignore: unnecessary_string_escapes final path = '${dir.path}/$filename$timestamp.gpx'; final file = File(path); await file.writeAsString(xml); - // 3. Modern share call (2025+ style) final result = await SharePlus.instance.share( - ShareParams( - text: shareText, - subject: subject, - files: [XFile(path)], - // Optional: sharePositionOrigin: ... (if you want iPad popover positioning) - ), + ShareParams(text: shareText, subject: subject, files: [XFile(path)]), ); - // 4. Handle result + await file.delete(); + switch (result.status) { case ShareResultStatus.success: debugPrint('Share successful – user completed the action.'); @@ -177,12 +171,8 @@ class GpxExport { debugPrint('Sharing is not available on this platform / context.'); return gpxExportNotAvailable; } - - // Optional cleanup (uncomment if you don't want to keep the file) - // await file.delete(); } catch (e, stack) { debugPrint('Export or share failed: $e\n$stack'); - // → here you could show a SnackBar / AlertDialog in real UI code } return gpxExportFailed; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b4a41dd..d2ea57e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import share_plus import shared_preferences_foundation import sqflite_darwin import url_launcher_macos @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 1e275d4..fc11656 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" crypto: dependency: "direct main" description: @@ -341,6 +349,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + gpx: + dependency: "direct main" + description: + name: gpx + sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e + url: "https://pub.dev" + source: hosted + version: "2.3.0" hooks: dependency: transitive description: @@ -501,6 +517,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: "direct main" description: @@ -566,7 +590,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -701,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" rxdart: dependency: transitive description: @@ -709,6 +741,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" shared_preferences: dependency: "direct main" description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index eeb548f..cd4fc19 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterBluePlusPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterBluePlusPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 68825d8..571addb 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_blue_plus_winrt + share_plus url_launcher_windows ) From ea43cf17eba8d3cfd7ba4cfeb31ef526760cc15e Mon Sep 17 00:00:00 2001 From: Ded Date: Sun, 8 Feb 2026 18:40:58 -0800 Subject: [PATCH 078/421] reduce map marker size (#131) * reduce map marker size reduces map markers from 80 to 60 px to improve visibility with higher density areas * add flutter test to actions * Add GPX export functionality and related UI components * Refactor GPX export constants to use lowercase naming convention and improve export function error handling * ran formating * Enhance GPX export functionality with customizable parameters and improved metadata * Implement PathTraceMapScreen and refactor path tracing functionality across screens * Add localization for missing location error in path tracing * Updated GPX export functionality for contacts and repeaters in multiple languages. * Add scrollbar to path trace details list for improved navigation * Integrate SharePlus plugin for enhanced sharing functionality across platforms * reduce map marker size reduces map markers from 80 to 60 px to improve visibility with higher density areas * reduce marker size to improve map clarity and add path trace navigation to path management --------- Co-authored-by: Winston Lowe --- lib/screens/channel_message_path_screen.dart | 60 ++++++++-------- lib/screens/map_screen.dart | 45 ++++++++++-- lib/screens/path_trace_map.dart | 72 +++++++++++--------- lib/widgets/path_management_dialog.dart | 14 ++++ 4 files changed, 124 insertions(+), 67 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 65487d8..8dea475 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -500,8 +500,8 @@ class _ChannelMessagePathMapScreenState if (hop.hasLocation) Marker( point: hop.position!, - width: 40, - height: 40, + width: 35, + height: 35, child: Container( decoration: BoxDecoration( color: Colors.green, @@ -526,37 +526,39 @@ class _ChannelMessagePathMapScreenState ), ), ), - Marker( - point: LatLng( - context.read().selfLatitude ?? 0.0, - context.read().selfLongitude ?? 0.0, - ), - width: 40, - height: 40, - child: Container( - decoration: BoxDecoration( - color: Colors.blue, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + if (context.read().selfLatitude != null && + context.read().selfLongitude != null) + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + width: 35, + height: 35, + child: Container( + decoration: BoxDecoration( + color: Colors.teal, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), ), ), ), - ), ]; } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 0da9960..f522407 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -349,6 +349,43 @@ class _MapScreenState extends State { ), ..._buildMarkers(contactsWithLocation, settings), ...sharedMarkers.map(_buildSharedMarker), + if (connector.selfLatitude != null && + connector.selfLongitude != null) + Marker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.teal, + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), ], ), ], @@ -394,14 +431,14 @@ class _MapScreenState extends State { final marker = Marker( point: LatLng(contact.latitude!, contact.longitude!), - width: 80, - height: 80, + width: 35, + height: 35, child: GestureDetector( onTap: () => _showNodeInfo(context, contact), child: Column( children: [ Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: _getNodeColor(contact.type), shape: BoxShape.circle, @@ -417,7 +454,7 @@ class _MapScreenState extends State { child: Icon( _getNodeIcon(contact.type), color: Colors.white, - size: 24, + size: 20, ), ), ], diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 2e557a5..39de31e 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -292,7 +292,7 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (!_hasData && _noLocationErr) + if (!_hasData && !_noLocationErr) Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -364,37 +364,6 @@ class _PathTraceMapScreenState extends State { List _buildHopMarkers(List pathData) { return [ - Marker( - point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, - ), - width: 40, - height: 40, - child: Container( - decoration: BoxDecoration( - color: Colors.blue, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ), for (final hop in pathData) if (_traceData!.pathContacts[hop]!.hasLocation) Marker( @@ -402,9 +371,10 @@ class _PathTraceMapScreenState extends State { _traceData!.pathContacts[hop]!.latitude!, _traceData!.pathContacts[hop]!.longitude!, ), - width: 40, - height: 40, + width: 35, + height: 35, child: Container( + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.green, shape: BoxShape.circle, @@ -433,6 +403,40 @@ class _PathTraceMapScreenState extends State { ), ), ), + if (context.read().selfLatitude != null && + context.read().selfLongitude != null) + Marker( + point: LatLng( + context.read().selfLatitude!, + context.read().selfLongitude!, + ), + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + context.l10n.pathTrace_you, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ), ]; } diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index f47b017..483697f 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -61,6 +62,19 @@ class _PathManagementDialog extends StatelessWidget { title: Text(l10n.chat_fullPath), content: SelectableText(formattedPath), actions: [ + TextButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: context.l10n.contacts_repeaterPathTrace, + path: Uint8List.fromList(pathBytes), + flipPathRound: true, + ), + ), + ), + child: Text(context.l10n.contacts_pathTrace), + ), TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.common_close), From daca42701c95a914676ddffe8b099a4cfa730896 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 8 Feb 2026 19:42:15 -0700 Subject: [PATCH 079/421] Notification rate limiting (#110) * Add notification rate limiting with privacy-safe debug logging - Add batching system to prevent notification storms (3s rate limit, 5s batch window) - Queue rapid notifications and show batch summaries - Debug logs show device names for adverts, sender/channel for messages (no content leaks) - Remove unused _maxBatchSize constant Context: Added after getting notification-flooded while evaluating RF flood management. The irony. * Update notification_service.dart I made a mistake and removed this * Add l10n support for notification strings Addresses PR #110 review feedback to use the translations system: - Add notification strings to app_en.arb (plurals for batch summary) - Update NotificationService to use lookupAppLocalizations() - Wire locale from MaterialApp to NotificationService - Regenerate localization files New strings added (English only, translations needed): - notification_activityTitle: "MeshCore Activity" - notification_messagesCount: "{count} message(s)" - notification_channelMessagesCount: "{count} channel message(s)" - notification_newNodesCount: "{count} new node(s)" - notification_newTypeDiscovered: "New {type} discovered" - notification_receivedNewMessage: "Received new message" * Add notification string translations for all supported languages Translated notification_activityTitle, notification_messagesCount, notification_channelMessagesCount, notification_newNodesCount, notification_newTypeDiscovered, and notification_receivedNewMessage to: bg, de, es, fr, it, nl, pl, pt, ru, sk, sl, sv, uk, zh Includes proper ICU plural forms for Slavic languages (few/many/other) and Slovenian dual form. * Apply dart format to notification_service.dart --------- Co-authored-by: Winston Lowe --- lib/l10n/app_bg.arb | 8 + lib/l10n/app_de.arb | 29 +++ lib/l10n/app_en.arb | 29 ++- lib/l10n/app_es.arb | 29 +++ lib/l10n/app_fr.arb | 8 + lib/l10n/app_it.arb | 8 + lib/l10n/app_localizations.dart | 36 ++++ lib/l10n/app_localizations_bg.dart | 44 ++++ lib/l10n/app_localizations_de.dart | 44 ++++ lib/l10n/app_localizations_en.dart | 44 ++++ lib/l10n/app_localizations_es.dart | 44 ++++ lib/l10n/app_localizations_fr.dart | 44 ++++ lib/l10n/app_localizations_it.dart | 44 ++++ lib/l10n/app_localizations_nl.dart | 44 ++++ lib/l10n/app_localizations_pl.dart | 50 +++++ lib/l10n/app_localizations_pt.dart | 44 ++++ lib/l10n/app_localizations_ru.dart | 50 +++++ lib/l10n/app_localizations_sk.dart | 47 +++++ lib/l10n/app_localizations_sl.dart | 50 +++++ lib/l10n/app_localizations_sv.dart | 44 ++++ lib/l10n/app_localizations_uk.dart | 50 +++++ lib/l10n/app_localizations_zh.dart | 26 +++ lib/l10n/app_nl.arb | 8 + lib/l10n/app_pl.arb | 8 + lib/l10n/app_pt.arb | 8 + lib/l10n/app_ru.arb | 8 + lib/l10n/app_sk.arb | 8 + lib/l10n/app_sl.arb | 8 + lib/l10n/app_sv.arb | 8 + lib/l10n/app_uk.arb | 8 + lib/l10n/app_zh.arb | 8 + lib/main.dart | 6 + lib/services/notification_service.dart | 267 ++++++++++++++++++++++++- untranslated.json | 128 +++++++++++- 34 files changed, 1282 insertions(+), 7 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 86ad421..01afb0c 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "notification_activityTitle": "Активност на MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}", + "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", + "notification_newTypeDiscovered": "Открит нов {contactType}", + "notification_receivedNewMessage": "Получено ново съобщение", + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", "settings_gpxExportAll": "Експортирай всички контакти в GPX", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!" + } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ccbdb2e..3dcd0ca 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1569,6 +1569,34 @@ "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + + "notification_activityTitle": "MeshCore Aktivität", + "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", + "@notification_messagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}", + "@notification_channelMessagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}", + "@notification_newNodesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newTypeDiscovered": "Neuer {contactType} entdeckt", + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": {"type": "String"} + } + }, + "notification_receivedNewMessage": "Neue Nachricht empfangen", + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", "settings_gpxExportAll": "Alle Kontakte nach GPX exportieren", "settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.", "settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren", @@ -1585,4 +1613,5 @@ "settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren", "settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert", "pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!" + } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d939bea..0c54be3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1346,12 +1346,39 @@ "contacts_contactAdvertCopied": "Advert copied to Clipboard.", "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", + "notification_activityTitle": "MeshCore Activity", + "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", + "@notification_messagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_channelMessagesCount": "{count} {count, plural, =1{channel message} other{channel messages}}", + "@notification_channelMessagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newNodesCount": "{count} {count, plural, =1{new node} other{new nodes}}", + "@notification_newNodesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newTypeDiscovered": "New {contactType} discovered", + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": {"type": "String"} + } + }, + "notification_receivedNewMessage": "Received new message", + "settings_gpxExportRepeaters": "Export repeaters / room server to GPX", "settings_gpxExportRepeatersSubtitle": "Exports repeaters / roomserver with a location to GPX file.", "settings_gpxExportContacts": "Export companions to GPX", "settings_gpxExportContactsSubtitle": "Exports companions with a location to GPX file.", "settings_gpxExportAll": "Export all contacts to GPX", - "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", + "settings_gpxExportAllSubtitle": "Exports all contacts with a location to GPX file.", "settings_gpxExportSuccess": "Successfully exported GPX file.", "settings_gpxExportNoContacts": "No contacts to export.", "settings_gpxExportNotAvailable": "Not supported on your device/OS", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 049d6a9..3d5ab63 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1569,6 +1569,34 @@ "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", + + "notification_activityTitle": "Actividad de MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}", + "@notification_messagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}", + "@notification_channelMessagesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}", + "@notification_newNodesCount": { + "placeholders": { + "count": {"type": "int"} + } + }, + "notification_newTypeDiscovered": "Nuevo {contactType} descubierto", + "@notification_newTypeDiscovered": { + "placeholders": { + "contactType": {"type": "String"} + } + }, + "notification_receivedNewMessage": "Nuevo mensaje recibido", + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", @@ -1585,4 +1613,5 @@ "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación" + } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 293a30e..044b806 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1569,6 +1569,13 @@ "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "notification_activityTitle": "Activité MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}", + "notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}", + "notification_newTypeDiscovered": "Nouveau {contactType} découvert", + "notification_receivedNewMessage": "Nouveau message reçu", + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", "settings_gpxExportNoContacts": "Aucun contact à exporter.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", "pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !" + } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 74c9205..dd9c373 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", + "notification_activityTitle": "Attività MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}", + "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", + "notification_newTypeDiscovered": "Nuovo {contactType} scoperto", + "notification_receivedNewMessage": "Nuovo messaggio ricevuto", + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", "settings_gpxExportContacts": "Esporta compagni in GPX", "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!" + } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b86882b..8f4d693 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4862,6 +4862,42 @@ abstract class AppLocalizations { /// **'Copying advert to Clipboard failed.'** String get contacts_contactAdvertCopyFailed; + /// No description provided for @notification_activityTitle. + /// + /// In en, this message translates to: + /// **'MeshCore Activity'** + String get notification_activityTitle; + + /// No description provided for @notification_messagesCount. + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{message} other{messages}}'** + String notification_messagesCount(int count); + + /// No description provided for @notification_channelMessagesCount. + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{channel message} other{channel messages}}'** + String notification_channelMessagesCount(int count); + + /// No description provided for @notification_newNodesCount. + /// + /// In en, this message translates to: + /// **'{count} {count, plural, =1{new node} other{new nodes}}'** + String notification_newNodesCount(int count); + + /// No description provided for @notification_newTypeDiscovered. + /// + /// In en, this message translates to: + /// **'New {contactType} discovered'** + String notification_newTypeDiscovered(String contactType); + + /// No description provided for @notification_receivedNewMessage. + /// + /// In en, this message translates to: + /// **'Received new message'** + String get notification_receivedNewMessage; + /// No description provided for @settings_gpxExportRepeaters. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 0527949..68e821e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2771,6 +2771,50 @@ class AppLocalizationsBg extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Копирането на обявата в клипборда не успя.'; + @override + String get notification_activityTitle => 'Активност на MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'съобщения', + one: 'съобщение', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'съобщения в канали', + one: 'съобщение в канал', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'нови възли', + one: 'нов възел', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Открит нов $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Получено ново съобщение'; + @override String get settings_gpxExportRepeaters => 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f66a37e..8ad7f1e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2778,6 +2778,50 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; + @override + String get notification_activityTitle => 'MeshCore Aktivität'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Nachrichten', + one: 'Nachricht', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'Kanalnachrichten', + one: 'Kanalnachricht', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'neue Knoten', + one: 'neuer Knoten', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Neuer $contactType entdeckt'; + } + + @override + String get notification_receivedNewMessage => 'Neue Nachricht empfangen'; + @override String get settings_gpxExportRepeaters => 'Repeater und Raumserver nach GPX exportieren'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b0315a8..8eb76e8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2728,6 +2728,50 @@ class AppLocalizationsEn extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Copying advert to Clipboard failed.'; + @override + String get notification_activityTitle => 'MeshCore Activity'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messages', + one: 'message', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'channel messages', + one: 'channel message', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'new nodes', + one: 'new node', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'New $contactType discovered'; + } + + @override + String get notification_receivedNewMessage => 'Received new message'; + @override String get settings_gpxExportRepeaters => 'Export repeaters / room server to GPX'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c08c67a..cc9bff7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2771,6 +2771,50 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Copiar anuncio al Portapapeles ha fallado.'; + @override + String get notification_activityTitle => 'Actividad de MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensajes', + one: 'mensaje', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensajes de canal', + one: 'mensaje de canal', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nuevos nodos', + one: 'nuevo nodo', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nuevo $contactType descubierto'; + } + + @override + String get notification_receivedNewMessage => 'Nuevo mensaje recibido'; + @override String get settings_gpxExportRepeaters => 'Exportar repetidores / servidor de sala a GPX'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 0241b3b..474d528 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2792,6 +2792,50 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'La copie de l\'annonce vers le presse-papiers a échoué.'; + @override + String get notification_activityTitle => 'Activité MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messages', + one: 'message', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messages de canal', + one: 'message de canal', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nouveaux nœuds', + one: 'nouveau nœud', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nouveau $contactType découvert'; + } + + @override + String get notification_receivedNewMessage => 'Nouveau message reçu'; + @override String get settings_gpxExportRepeaters => 'Exporter les répéteurs / serveur de salle au format GPX'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index e758ea4..68465dd 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2774,6 +2774,50 @@ class AppLocalizationsIt extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Copia dell\'annuncio nella Clipboard non riuscita.'; + @override + String get notification_activityTitle => 'Attività MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messaggi', + one: 'messaggio', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'messaggi del canale', + one: 'messaggio del canale', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nuovi nodi', + one: 'nuovo nodo', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nuovo $contactType scoperto'; + } + + @override + String get notification_receivedNewMessage => 'Nuovo messaggio ricevuto'; + @override String get settings_gpxExportRepeaters => 'Esporta ripetitori / server di stanza in GPX'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index f081a20..a093a35 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2763,6 +2763,50 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiëren van advertentie naar Clipboard is mislukt.'; + @override + String get notification_activityTitle => 'MeshCore Activiteit'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'berichten', + one: 'bericht', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'kanaalberichten', + one: 'kanaalbericht', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nieuwe knooppunten', + one: 'nieuw knooppunt', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nieuw $contactType ontdekt'; + } + + @override + String get notification_receivedNewMessage => 'Nieuw bericht ontvangen'; + @override String get settings_gpxExportRepeaters => 'Exporteer repeaters / roomserver naar GPX'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 04b959c..895e3c7 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2771,6 +2771,56 @@ class AppLocalizationsPl extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; + @override + String get notification_activityTitle => 'Aktywność MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'wiadomości', + many: 'wiadomości', + few: 'wiadomości', + one: 'wiadomość', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'wiadomości kanału', + many: 'wiadomości kanału', + few: 'wiadomości kanału', + one: 'wiadomość kanału', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nowych węzłów', + many: 'nowych węzłów', + few: 'nowe węzły', + one: 'nowy węzeł', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nowy $contactType wykryty'; + } + + @override + String get notification_receivedNewMessage => 'Otrzymano nową wiadomość'; + @override String get settings_gpxExportRepeaters => 'Eksportuj powtórki / serwer pokojowy do GPX'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index de4adf5..ce8d07c 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2773,6 +2773,50 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Cópia do anúncio para a Área de Transferência falhou.'; + @override + String get notification_activityTitle => 'Atividade MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensagens', + one: 'mensagem', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'mensagens de canal', + one: 'mensagem de canal', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'novos nós', + one: 'novo nó', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Novo $contactType descoberto'; + } + + @override + String get notification_receivedNewMessage => 'Nova mensagem recebida'; + @override String get settings_gpxExportRepeaters => 'Exportar repetidores / servidor de sala para GPX'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index e3050af..9e4cd95 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2778,6 +2778,56 @@ class AppLocalizationsRu extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Копирование рекламы в буфер обмена не удалось.'; + @override + String get notification_activityTitle => 'Активность MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'сообщений', + many: 'сообщений', + few: 'сообщения', + one: 'сообщение', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'сообщений канала', + many: 'сообщений канала', + few: 'сообщения канала', + one: 'сообщение канала', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'новых узлов', + many: 'новых узлов', + few: 'новых узла', + one: 'новый узел', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Обнаружен новый $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Получено новое сообщение'; + @override String get settings_gpxExportRepeaters => 'Экспортировать рипитеры / сервер комнаты в GPX'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 745b3fd..ed66f97 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2757,6 +2757,53 @@ class AppLocalizationsSk extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopírovanie inzerátu do schránky zlyhalo.'; + @override + String get notification_activityTitle => 'Aktivita MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'správ', + few: 'správy', + one: 'správa', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'správ kanálu', + few: 'správy kanálu', + one: 'správa kanálu', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nových uzlov', + few: 'nové uzly', + one: 'nový uzol', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Nový $contactType objavený'; + } + + @override + String get notification_receivedNewMessage => 'Prijatá nová správa'; + @override String get settings_gpxExportRepeaters => 'Exportovať repeater / server miestnosti do GPX'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 70b2839..3307547 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2759,6 +2759,56 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiranje oglasa v odložišče je spodletelo.'; + @override + String get notification_activityTitle => 'Aktivnost MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'sporočil', + few: 'sporočila', + two: 'sporočili', + one: 'sporočilo', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'sporočil kanala', + few: 'sporočila kanala', + two: 'sporočili kanala', + one: 'sporočilo kanala', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'novih vozlišč', + few: 'nova vozlišča', + two: 'novi vozlišči', + one: 'novo vozlišče', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Odkrito novo $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Prejeto novo sporočilo'; + @override String get settings_gpxExportRepeaters => 'Izvoz ponoviteljev / strežnika sobe v GPX'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index f930dcb..5239b06 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2744,6 +2744,50 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Kopiering av annons till Urklipp misslyckades.'; + @override + String get notification_activityTitle => 'MeshCore Aktivitet'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'meddelanden', + one: 'meddelande', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'kanalmeddelanden', + one: 'kanalmeddelande', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'nya noder', + one: 'ny nod', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Ny $contactType upptäckt'; + } + + @override + String get notification_receivedNewMessage => 'Nytt meddelande mottaget'; + @override String get settings_gpxExportRepeaters => 'Exportera repeater / rumsservrar till GPX'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e48563d..b6ff8ce 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2784,6 +2784,56 @@ class AppLocalizationsUk extends AppLocalizations { String get contacts_contactAdvertCopyFailed => 'Копіювання оголошення в буфер обміну завершилося невдало'; + @override + String get notification_activityTitle => 'Активність MeshCore'; + + @override + String notification_messagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'повідомлень', + many: 'повідомлень', + few: 'повідомлення', + one: 'повідомлення', + ); + return '$count $_temp0'; + } + + @override + String notification_channelMessagesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'повідомлень каналу', + many: 'повідомлень каналу', + few: 'повідомлення каналу', + one: 'повідомлення каналу', + ); + return '$count $_temp0'; + } + + @override + String notification_newNodesCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'нових вузлів', + many: 'нових вузлів', + few: 'нових вузли', + one: 'новий вузол', + ); + return '$count $_temp0'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return 'Виявлено новий $contactType'; + } + + @override + String get notification_receivedNewMessage => 'Отримано нове повідомлення'; + @override String get settings_gpxExportRepeaters => 'Експортувати ретранслятори / сервер кімнати до GPX'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 51d3463..a529a1b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2625,6 +2625,32 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => '将广告复制到剪贴板操作失败。'; + @override + String get notification_activityTitle => 'MeshCore 活动'; + + @override + String notification_messagesCount(int count) { + return '$count 条消息'; + } + + @override + String notification_channelMessagesCount(int count) { + return '$count 条频道消息'; + } + + @override + String notification_newNodesCount(int count) { + return '$count 个新节点'; + } + + @override + String notification_newTypeDiscovered(String contactType) { + return '发现新 $contactType'; + } + + @override + String get notification_receivedNewMessage => '收到新消息'; + @override String get settings_gpxExportRepeaters => '导出重复器/房间服务器到GPX'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index fece968..91163ac 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", + "notification_activityTitle": "MeshCore Activiteit", + "notification_messagesCount": "{count} {count, plural, =1{bericht} other{berichten}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{kanaalbericht} other{kanaalberichten}}", + "notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}", + "notification_newTypeDiscovered": "Nieuw {contactType} ontdekt", + "notification_receivedNewMessage": "Nieuw bericht ontvangen", + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren", "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!" + } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index a5b4925..3c2a96f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", + "notification_activityTitle": "Aktywność MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}", + "notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}", + "notification_newTypeDiscovered": "Nowy {contactType} wykryty", + "notification_receivedNewMessage": "Otrzymano nową wiadomość", + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open", "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!" + } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6c260b9..dc38c11 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1569,6 +1569,13 @@ "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", + "notification_activityTitle": "Atividade MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{mensagem} other{mensagens}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{mensagem de canal} other{mensagens de canal}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", + "notification_newTypeDiscovered": "Novo {contactType} descoberto", + "notification_receivedNewMessage": "Nova mensagem recebida", + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!" + } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 8ab90e7..ddbbe79 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -809,6 +809,13 @@ "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "notification_activityTitle": "Активность MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}", + "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", + "notification_newTypeDiscovered": "Обнаружен новый {contactType}", + "notification_receivedNewMessage": "Получено новое сообщение", + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", "settings_gpxExportContacts": "Экспортировать спутников в GPX", @@ -825,4 +832,5 @@ "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!" + } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 9988138..c09502a 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", "contacts_ShareContact": "Kopírovať kontakt do schránky", + "notification_activityTitle": "Aktivita MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}", + "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", + "notification_newTypeDiscovered": "Nový {contactType} objavený", + "notification_receivedNewMessage": "Prijatá nová správa", + "contacts_ShareContact": "Kopírovať kontakt do schránky", "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", "settings_gpxExportContacts": "Export sprievodcov do GPX", "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!" + } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 6392dff..97a396a 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1569,6 +1569,13 @@ "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", "contacts_ShareContact": "Kopiraj stik v Odložišče", + "notification_activityTitle": "Aktivnost MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", + "notification_newTypeDiscovered": "Odkrito novo {contactType}", + "notification_receivedNewMessage": "Prejeto novo sporočilo", + "contacts_ShareContact": "Kopiraj stik v Odložišče", "settings_gpxExportAll": "Izvozi vse kontakte v GPX", "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte", "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!" + } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 08ed323..6df28bd 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1569,6 +1569,13 @@ "contacts_ShareContact": "Kopiera kontakt till Urklipp", "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", "contacts_ShareContactZeroHop": "Dela kontakt via annons", + "notification_activityTitle": "MeshCore Aktivitet", + "notification_messagesCount": "{count} {count, plural, =1{meddelande} other{meddelanden}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{kanalmeddelande} other{kanalmeddelanden}}", + "notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}", + "notification_newTypeDiscovered": "Ny {contactType} upptäckt", + "notification_receivedNewMessage": "Nytt meddelande mottaget", + "contacts_ShareContactZeroHop": "Dela kontakt via annons", "settings_gpxExportAll": "Exportera alla kontakter till GPX", "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open", "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!" + } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 2cea864..9f5e64d 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "notification_activityTitle": "Активність MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}", + "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", + "notification_newTypeDiscovered": "Виявлено новий {contactType}", + "notification_receivedNewMessage": "Отримано нове повідомлення", + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", @@ -1585,4 +1592,5 @@ "settings_gpxExportAllContacts": "Усі місця контактів", "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!" + } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c6b27f5..8c65510 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1569,6 +1569,13 @@ "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "notification_activityTitle": "MeshCore 活动", + "notification_messagesCount": "{count} 条消息", + "notification_channelMessagesCount": "{count} 条频道消息", + "notification_newNodesCount": "{count} 个新节点", + "notification_newTypeDiscovered": "发现新 {contactType}", + "notification_receivedNewMessage": "收到新消息", + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", @@ -1585,4 +1592,5 @@ "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出", "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!" + } diff --git a/lib/main.dart b/lib/main.dart index 96a853d..8ee0ca4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -150,6 +150,12 @@ class MeshCoreApp extends StatelessWidget { themeMode: _themeModeFromSetting( settingsService.settings.themeMode, ), + builder: (context, child) { + // Update notification service with resolved locale + final locale = Localizations.localeOf(context); + NotificationService().setLocale(locale); + return child ?? const SizedBox.shrink(); + }, home: const ScannerScreen(), ); }, diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index d835d07..57331aa 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,6 +1,10 @@ +import 'dart:ui'; + import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; +import '../l10n/app_localizations.dart'; + class NotificationService { static final NotificationService _instance = NotificationService._internal(); factory NotificationService() => _instance; @@ -10,6 +14,34 @@ class NotificationService { FlutterLocalNotificationsPlugin(); bool _isInitialized = false; + // Locale for localized notification strings + Locale _locale = const Locale('en'); + + /// Set the locale for notification strings (call when app locale changes) + void setLocale(Locale locale) { + _locale = locale; + } + + AppLocalizations get _l10n => lookupAppLocalizations(_locale); + + // Rate limiting to prevent notification storms + // (Added after getting notification-flooded while evaluating RF flood management. The irony.) + static const _minNotificationInterval = Duration(seconds: 3); + static const _batchWindow = Duration(seconds: 5); + + DateTime? _lastNotificationTime; + final List<_PendingNotification> _pendingNotifications = []; + bool _isBatchingActive = false; + bool _suppressNotifications = false; + + /// Temporarily suppress all notifications (e.g., during sync) + void suppressNotifications(bool suppress) { + _suppressNotifications = suppress; + if (suppress) { + _pendingNotifications.clear(); + } + } + Future initialize() async { if (_isInitialized) return; @@ -76,7 +108,7 @@ class NotificationService { return true; } - Future showMessageNotification({ + Future _showMessageNotificationImpl({ required String contactName, required String message, String? contactId, @@ -125,7 +157,7 @@ class NotificationService { ); } - Future showAdvertNotification({ + Future _showAdvertNotificationImpl({ required String contactName, required String contactType, String? contactId, @@ -163,14 +195,14 @@ class NotificationService { await _notifications.show( contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - 'New $contactType discovered', + _l10n.notification_newTypeDiscovered(contactType), contactName, notificationDetails, payload: 'advert:$contactId', ); } - Future showChannelMessageNotification({ + Future _showChannelMessageNotificationImpl({ required String channelName, required String message, int? channelIndex, @@ -211,7 +243,9 @@ class NotificationService { ); final preview = message.trim(); - final body = preview.isEmpty ? 'Received new message' : preview; + final body = preview.isEmpty + ? _l10n.notification_receivedNewMessage + : preview; await _notifications.show( channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, @@ -222,6 +256,21 @@ class NotificationService { ); } + /// Returns a privacy-safe identifier for debug logging. + /// - advert: shows device name (body contains contactName) + /// - message: shows "from: sender" (avoids logging message content) + /// - channelMessage: shows "in: channel" (avoids logging message content) + String _getNotificationIdentifier(_PendingNotification n) { + switch (n.type) { + case _NotificationType.advert: + return n.body; + case _NotificationType.message: + return 'from: ${n.title}'; + case _NotificationType.channelMessage: + return 'in: ${n.title}'; + } + } + void _onNotificationTapped(NotificationResponse response) { final payload = response.payload; if (payload != null) { @@ -238,4 +287,212 @@ class NotificationService { Future cancel(int id) async { await _notifications.cancel(id); } + + // ───────────────────────────────────────────────────────────────── + // Public notification methods (rate limiting is enforced automatically) + // ───────────────────────────────────────────────────────────────── + + Future showMessageNotification({ + required String contactName, + required String message, + String? contactId, + int? badgeCount, + }) async { + if (_suppressNotifications) return; + + _queueNotification( + _PendingNotification( + type: _NotificationType.message, + title: contactName, + body: message, + id: contactId, + badgeCount: badgeCount, + ), + ); + } + + Future showAdvertNotification({ + required String contactName, + required String contactType, + String? contactId, + }) async { + if (_suppressNotifications) return; + + _queueNotification( + _PendingNotification( + type: _NotificationType.advert, + title: contactType, + body: contactName, + id: contactId, + ), + ); + } + + Future showChannelMessageNotification({ + required String channelName, + required String message, + int? channelIndex, + int? badgeCount, + }) async { + if (_suppressNotifications) return; + + _queueNotification( + _PendingNotification( + type: _NotificationType.channelMessage, + title: channelName, + body: message, + id: channelIndex?.toString(), + badgeCount: badgeCount, + ), + ); + } + + void _queueNotification(_PendingNotification notification) { + final now = DateTime.now(); + + // If we recently showed a notification, start batching + if (_lastNotificationTime != null && + now.difference(_lastNotificationTime!) < _minNotificationInterval) { + _pendingNotifications.add(notification); + debugPrint( + '[Notification] queued: ${notification.type.name} (${_getNotificationIdentifier(notification)})', + ); + + // Start batch timer if not already running + if (!_isBatchingActive) { + _isBatchingActive = true; + Future.delayed(_batchWindow, _processBatch); + } + return; + } + + // Show immediately if enough time has passed + debugPrint( + '[Notification] sent immediately: ${notification.type.name} (${_getNotificationIdentifier(notification)})', + ); + _showNotificationImmediately(notification); + _lastNotificationTime = now; + } + + Future _processBatch() async { + _isBatchingActive = false; + + if (_pendingNotifications.isEmpty) return; + + final batch = List<_PendingNotification>.from(_pendingNotifications); + _pendingNotifications.clear(); + + if (batch.length == 1) { + // Single notification, show normally + _showNotificationImmediately(batch.first); + } else { + // Multiple notifications, show summary + await _showBatchSummary(batch); + } + + _lastNotificationTime = DateTime.now(); + } + + Future _showNotificationImmediately( + _PendingNotification notification, + ) async { + switch (notification.type) { + case _NotificationType.message: + await _showMessageNotificationImpl( + contactName: notification.title, + message: notification.body, + contactId: notification.id, + badgeCount: notification.badgeCount, + ); + break; + case _NotificationType.advert: + await _showAdvertNotificationImpl( + contactName: notification.body, + contactType: notification.title, + contactId: notification.id, + ); + break; + case _NotificationType.channelMessage: + await _showChannelMessageNotificationImpl( + channelName: notification.title, + message: notification.body, + channelIndex: int.tryParse(notification.id ?? ''), + badgeCount: notification.badgeCount, + ); + break; + } + } + + Future _showBatchSummary(List<_PendingNotification> batch) async { + if (!_isInitialized) await initialize(); + + // Group by type + final messages = batch + .where((n) => n.type == _NotificationType.message) + .toList(); + final adverts = batch + .where((n) => n.type == _NotificationType.advert) + .toList(); + final channelMsgs = batch + .where((n) => n.type == _NotificationType.channelMessage) + .toList(); + + // Build summary text using localized plurals + final parts = []; + if (messages.isNotEmpty) { + parts.add(_l10n.notification_messagesCount(messages.length)); + } + if (channelMsgs.isNotEmpty) { + parts.add(_l10n.notification_channelMessagesCount(channelMsgs.length)); + } + if (adverts.isNotEmpty) { + parts.add(_l10n.notification_newNodesCount(adverts.length)); + } + + if (parts.isEmpty) return; + + // Show first few device names in batch summary for debugging (only if adverts exist) + final deviceInfo = adverts.isNotEmpty + ? ' (${adverts.take(5).map((n) => n.body).join(', ')}${adverts.length > 5 ? ', ...' : ''})' + : ''; + debugPrint('[Notification] batch summary: ${parts.join(", ")}$deviceInfo'); + + const androidDetails = AndroidNotificationDetails( + 'batch_summary', + 'Activity Summary', + channelDescription: 'Batched notification summaries', + importance: Importance.defaultImportance, + priority: Priority.defaultPriority, + icon: '@mipmap/ic_launcher', + ); + + const notificationDetails = NotificationDetails(android: androidDetails); + + await _notifications.show( + 'batch_summary'.hashCode, + _l10n.notification_activityTitle, + parts.join(', '), + notificationDetails, + payload: 'batch', + ); + } +} + +// Helper class for pending notifications +enum _NotificationType { message, advert, channelMessage } + +class _PendingNotification { + final _NotificationType type; + final String title; + final String body; + final String? id; + final int? badgeCount; + + _PendingNotification({ + required this.type, + required this.title, + required this.body, + this.id, + this.badgeCount, + }); } diff --git a/untranslated.json b/untranslated.json index 9e26dfe..e0ad904 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,127 @@ -{} \ No newline at end of file +{ + "bg": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "de": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "es": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "fr": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "it": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "nl": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "pl": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "pt": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "ru": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "sk": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "sl": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "sv": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "uk": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ], + + "zh": [ + "notification_activityTitle", + "notification_messagesCount", + "notification_channelMessagesCount", + "notification_newNodesCount", + "notification_newTypeDiscovered", + "notification_receivedNewMessage" + ] +} From 87a2807f5b94af06ae8c2c0c56284a19a6ad91a5 Mon Sep 17 00:00:00 2001 From: 446564 Date: Sun, 8 Feb 2026 18:56:24 -0800 Subject: [PATCH 080/421] chore: update version to alpha 6 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6312ee3..6474c5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 5.0.0+5 +version: 5.0.0+6 environment: sdk: ^3.9.2 From fe23e9f7a00f365232cd98e91c413dc6d3f6ead0 Mon Sep 17 00:00:00 2001 From: 446564 Date: Mon, 9 Feb 2026 05:36:25 -0800 Subject: [PATCH 081/421] add support for whipseros needed a new ble prefix --- lib/connector/meshcore_connector.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2c56c37..2bb0a17 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -658,7 +658,8 @@ class MeshCoreConnector extends ChangeNotifier { _scanResults.clear(); for (var result in results) { if (result.device.platformName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("MeshCore-")) { + result.advertisementData.advName.startsWith("MeshCore-") || + result.advertisementData.advName.startsWith("Whisper-")) { _scanResults.add(result); } } From 04021a39a1d477c1c330f0a01e538eaf6544705a Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:12:51 +0100 Subject: [PATCH 082/421] Better french translations --- lib/l10n/app_fr.arb | 72 ++++++++-------- lib/l10n/app_localizations_fr.dart | 73 ++++++++-------- untranslated.json | 128 +---------------------------- 3 files changed, 72 insertions(+), 201 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 044b806..0827576 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -210,8 +210,8 @@ "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Affichage de la carte", - "appSettings_showRepeaters": "Afficher les répétiteurs", - "appSettings_showRepeatersSubtitle": "Afficher les nœuds répétiteurs sur la carte", + "appSettings_showRepeaters": "Afficher les répéteurs", + "appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte", "appSettings_showChatNodes": "Afficher les nœuds de discussion", "appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte", "appSettings_showOtherNodes": "Afficher d'autres nœuds", @@ -266,7 +266,7 @@ } } }, - "contacts_manageRepeater": "Gérer le répétiteur", + "contacts_manageRepeater": "Gérer le répéteur", "contacts_roomLogin": "Connexion Salle", "contacts_openChat": "Ouverture du Chat", "contacts_editGroup": "Modifier le groupe", @@ -542,9 +542,9 @@ "chat_forceFloodMode": "Mode tout le réseau forcé", "chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :", "chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.", - "chat_hopSingular": "Sautez", - "chat_hopPlural": "sautez", - "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", + "chat_hopSingular": "saut", + "chat_hopPlural": "sauts", + "chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -636,7 +636,7 @@ } }, "map_chat": "Chat", - "map_repeater": "Répétiteur", + "map_repeater": "Répéteur", "map_room": "Salle", "map_sensor": "Capteur", "map_pinDm": "Clé (DM)", @@ -677,7 +677,7 @@ "map_lastSeenTime": "Dernière fois vu", "map_sharedPin": "Clé partagée", "map_joinRoom": "Rejoindre la salle", - "map_manageRepeater": "Gérer le répétiteur", + "map_manageRepeater": "Gérer le répéteur", "mapCache_title": "Cache de Carte Hors Ligne", "mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier", "mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.", @@ -800,13 +800,13 @@ "time_allTime": "Tout le temps", "dialog_disconnect": "Déconnecter", "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", - "login_repeaterLogin": "Connexion au répétiteur", + "login_repeaterLogin": "Connexion au répéteur", "login_roomLogin": "Connexion Salle", "login_password": "Mot de passe", "login_enterPassword": "Entrez votre mot de passe", "login_savePassword": "Sauvegarder le mot de passe", "login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.", - "login_repeaterDescription": "Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l'état.", + "login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.", "login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.", "login_routing": "Redirection", "login_routingMode": "Mode de routage", @@ -871,17 +871,17 @@ }, "path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.", "path_setPath": "Définir le chemin", - "repeater_management": "Gestion des répétiteurs", + "repeater_management": "Gestion des répéteurs", "repeater_managementTools": "Outils de Gestion", "repeater_status": "État", - "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur", + "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur", "repeater_telemetry": "Télémetrie", "repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Envoyer des commandes au répétiteur", + "repeater_cliSubtitle": "Envoyer des commandes au répéteur", "repeater_settings": "Paramètres", - "repeater_settingsSubtitle": "Configurer les paramètres du répétiteur", - "repeater_statusTitle": "État du répétiteur", + "repeater_settingsSubtitle": "Configurer les paramètres du répéteur", + "repeater_statusTitle": "État du répéteur", "repeater_routingMode": "Mode de routage", "repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", "repeater_forceFloodMode": "Mode tout le réseau forcé", @@ -976,10 +976,10 @@ } } }, - "repeater_settingsTitle": "Paramètres du répétiteur", + "repeater_settingsTitle": "Paramètres du répéteur", "repeater_basicSettings": "Paramètres de base", - "repeater_repeaterName": "Nom du répétiteur", - "repeater_repeaterNameHelper": "Afficher le nom de ce répétiteur", + "repeater_repeaterName": "Nom du répéteur", + "repeater_repeaterNameHelper": "Afficher le nom de ce répéteur", "repeater_adminPassword": "Mot de passe Administrateur", "repeater_adminPasswordHelper": "Mot de passe d'accès complet", "repeater_guestPassword": "Mot de passe invité", @@ -999,7 +999,7 @@ "repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)", "repeater_features": "Fonctionnalités", "repeater_packetForwarding": "Transfert de paquets", - "repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets", + "repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets", "repeater_guestAccess": "Accès Invité", "repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule", "repeater_privacyMode": "Mode de confidentialité", @@ -1026,14 +1026,14 @@ "repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées", "repeater_dangerZone": "Zone dangereuse", "repeater_rebootRepeater": "Redémarrer Répéteur", - "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur", - "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?", + "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur", + "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?", "repeater_regenerateIdentityKey": "Ré générer la clé d'identité", "repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée", - "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répétiteur. Continuer ?", + "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?", "repeater_eraseFileSystem": "Supprimer le système de fichiers", - "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répétiteur", - "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !", + "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur", + "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !", "repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.", "repeater_commandSent": "Commande envoyée : {command}", "@repeater_commandSent": { @@ -1085,7 +1085,7 @@ } } }, - "repeater_cliTitle": "Répétiteur CLI", + "repeater_cliTitle": "Répéteur CLI", "repeater_debugNextCommand": "Déboguer Prochaine Commande", "repeater_commandHelp": "Aide", "repeater_clearHistory": "Effacer l'historique", @@ -1119,13 +1119,13 @@ "repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.", "repeater_cliHelpSetAf": "Définit le facteur de temps d'air.", "repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).", - "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.", + "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.", "repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", "repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).", "repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.", "repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.", "repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».", - "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.", + "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.", "repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.", "repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")", "repeater_cliHelpSetName": "Définit le nom de l'annonce.", @@ -1147,7 +1147,7 @@ "repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.", "repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.", "repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.", - "repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", + "repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", "repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.", "repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).", "repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.", @@ -1171,8 +1171,8 @@ "repeater_settingsCategory": "Paramètres", "repeater_bridge": "Pont", "repeater_logging": "Journalisation", - "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répétiteur)", - "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répétiteur)", + "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)", + "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)", "repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.", "repeater_gpsManagement": "Gestion GPS", "repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.", @@ -1241,7 +1241,7 @@ "channelPath_title": "Chemin de paquet", "channelPath_viewMap": "Afficher la carte", "channelPath_otherObservedPaths": "Autres chemins observés", - "channelPath_repeaterHops": "Sauts du répétiteur", + "channelPath_repeaterHops": "Sauts du répéteur", "channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.", "channelPath_messageDetails": "Détails du message", "channelPath_senderLabel": "Expéditeur", @@ -1306,7 +1306,7 @@ } }, "channelPath_mapTitle": "Carte du chemin", - "channelPath_noRepeaterLocations": "Aucune position de répétiteur disponible pour ce chemin.", + "channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.", "channelPath_primaryPath": "Chemin {index} (Principal)", "@channelPath_primaryPath": { "placeholders": { @@ -1558,9 +1558,9 @@ "appSettings_languageRu": "Russe", "contacts_clipboardEmpty": "Le presse-papiers est vide.", "contacts_contactImported": "Le contact a été importé.", - "contacts_floodAdvert": "Annonce de crue", + "contacts_floodAdvert": "Annonce à tout le réseau", "contacts_contactImportFailed": "Échec de l'importation du contact.", - "contacts_zeroHopAdvert": "Annonce Zero Hop", + "contacts_zeroHopAdvert": "Annonce Zero saut", "contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers", "contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers", "contacts_ShareContact": "Copier le contact dans le presse-papiers", @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}", "notification_newTypeDiscovered": "Nouveau {contactType} découvert", "notification_receivedNewMessage": "Nouveau message reçu", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", "settings_gpxExportNoContacts": "Aucun contact à exporter.", @@ -1591,6 +1590,5 @@ "settings_gpxExportAllContacts": "Tous les emplacements des contacts", "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", - "pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !" - + "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !" } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 474d528..eb6d50f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -560,11 +560,11 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_mapDisplay => 'Affichage de la carte'; @override - String get appSettings_showRepeaters => 'Afficher les répétiteurs'; + String get appSettings_showRepeaters => 'Afficher les répéteurs'; @override String get appSettings_showRepeatersSubtitle => - 'Afficher les nœuds répétiteurs sur la carte'; + 'Afficher les nœuds répéteurs sur la carte'; @override String get appSettings_showChatNodes => 'Afficher les nœuds de discussion'; @@ -671,7 +671,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Gérer le répétiteur'; + String get contacts_manageRepeater => 'Gérer le répéteur'; @override String get contacts_manageRoom => 'Gérer le Room Server'; @@ -1094,18 +1094,18 @@ class AppLocalizationsFr extends AppLocalizations { 'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.'; @override - String get chat_hopSingular => 'Sautez'; + String get chat_hopSingular => 'saut'; @override - String get chat_hopPlural => 'sautez'; + String get chat_hopPlural => 'sauts'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'hops', - one: 'hop', + other: 'sauts', + one: 'saut', ); return '$count $_temp0'; } @@ -1259,7 +1259,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_chat => 'Chat'; @override - String get map_repeater => 'Répétiteur'; + String get map_repeater => 'Répéteur'; @override String get map_room => 'Salle'; @@ -1365,7 +1365,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_joinRoom => 'Rejoindre la salle'; @override - String get map_manageRepeater => 'Gérer le répétiteur'; + String get map_manageRepeater => 'Gérer le répéteur'; @override String get mapCache_title => 'Cache de Carte Hors Ligne'; @@ -1509,7 +1509,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; @override - String get login_repeaterLogin => 'Connexion au répétiteur'; + String get login_repeaterLogin => 'Connexion au répéteur'; @override String get login_roomLogin => 'Connexion Salle'; @@ -1529,7 +1529,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get login_repeaterDescription => - 'Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l\'état.'; + 'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.'; @override String get login_roomDescription => @@ -1634,7 +1634,7 @@ class AppLocalizationsFr extends AppLocalizations { String get path_setPath => 'Définir le chemin'; @override - String get repeater_management => 'Gestion des répétiteurs'; + String get repeater_management => 'Gestion des répéteurs'; @override String get room_management => 'Administración del Servidor de Habitación'; @@ -1647,7 +1647,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_statusSubtitle => - 'Afficher l\'état, les statistiques et les voisins du répétiteur'; + 'Afficher l\'état, les statistiques et les voisins du répéteur'; @override String get repeater_telemetry => 'Télémetrie'; @@ -1660,7 +1660,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur'; + String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; @override String get repeater_neighbours => 'Voisins'; @@ -1674,10 +1674,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_settingsSubtitle => - 'Configurer les paramètres du répétiteur'; + 'Configurer les paramètres du répéteur'; @override - String get repeater_statusTitle => 'État du répétiteur'; + String get repeater_statusTitle => 'État du répéteur'; @override String get repeater_routingMode => 'Mode de routage'; @@ -1783,16 +1783,16 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Paramètres du répétiteur'; + String get repeater_settingsTitle => 'Paramètres du répéteur'; @override String get repeater_basicSettings => 'Paramètres de base'; @override - String get repeater_repeaterName => 'Nom du répétiteur'; + String get repeater_repeaterName => 'Nom du répéteur'; @override - String get repeater_repeaterNameHelper => 'Afficher le nom de ce répétiteur'; + String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur'; @override String get repeater_adminPassword => 'Mot de passe Administrateur'; @@ -1856,7 +1856,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_packetForwardingSubtitle => - 'Activer le répétiteur pour transmettre des paquets'; + 'Activer le répéteur pour transmettre des paquets'; @override String get repeater_guestAccess => 'Accès Invité'; @@ -1905,11 +1905,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_rebootRepeaterSubtitle => - 'Réinitialiser l\'appareil répétiteur'; + 'Réinitialiser l\'appareil répéteur'; @override String get repeater_rebootRepeaterConfirm => - 'Êtes-vous sûr de vouloir redémarrer ce répétiteur ?'; + 'Êtes-vous sûr de vouloir redémarrer ce répéteur ?'; @override String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité'; @@ -1920,18 +1920,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'Cela générera une nouvelle identité pour le répétiteur. Continuer ?'; + 'Cela générera une nouvelle identité pour le répéteur. Continuer ?'; @override String get repeater_eraseFileSystem => 'Supprimer le système de fichiers'; @override String get repeater_eraseFileSystemSubtitle => - 'Formater le système de fichiers du répétiteur'; + 'Formater le système de fichiers du répéteur'; @override String get repeater_eraseFileSystemConfirm => - 'AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !'; + 'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !'; @override String get repeater_eraseSerialOnly => @@ -1999,7 +1999,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_cliTitle => 'Répétiteur CLI'; + String get repeater_cliTitle => 'Répéteur CLI'; @override String get repeater_debugNextCommand => 'Déboguer Prochaine Commande'; @@ -2091,7 +2091,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpSetRepeat => - 'Active ou désactive le rôle du répétiteur pour ce nœud.'; + 'Active ou désactive le rôle du répéteur pour ce nœud.'; @override String get repeater_cliHelpSetAllowReadOnly => @@ -2115,7 +2115,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Définit l\'intervalle du minuteur pour envoyer un paquet d\'annonce local (sans relais). Définir sur 0 pour désactiver.'; + 'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetFloodAdvertInterval => @@ -2201,7 +2201,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; + 'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; @override String get repeater_cliHelpNeighborRemove => @@ -2289,12 +2289,11 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_logging => 'Journalisation'; @override - String get repeater_neighborsRepeaterOnly => - 'Voisins (Uniquement répétiteur)'; + String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)'; @override String get repeater_regionManagementRepeaterOnly => - 'Gestion des régions (uniquement pour le répétiteur)'; + 'Gestion des régions (uniquement pour le répéteur)'; @override String get repeater_regionNote => @@ -2399,7 +2398,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_otherObservedPaths => 'Autres chemins observés'; @override - String get channelPath_repeaterHops => 'Sauts du répétiteur'; + String get channelPath_repeaterHops => 'Sauts du répéteur'; @override String get channelPath_noHopDetails => @@ -2467,7 +2466,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Aucune position de répétiteur disponible pour ce chemin.'; + 'Aucune position de répéteur disponible pour ce chemin.'; @override String channelPath_primaryPath(int index) { @@ -2713,7 +2712,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => - 'Une ou plusieurs des houblons manquent d\'une localisation !'; + 'Un ou plusieurs des sauts manquent d\'une localisation !'; @override String get contacts_pathTrace => 'Traçage de chemin'; @@ -2756,10 +2755,10 @@ class AppLocalizationsFr extends AppLocalizations { 'Échec de l\'importation du contact.'; @override - String get contacts_zeroHopAdvert => 'Annonce Zero Hop'; + String get contacts_zeroHopAdvert => 'Annonce Zero saut'; @override - String get contacts_floodAdvert => 'Annonce de crue'; + String get contacts_floodAdvert => 'Annonce à tout le réseau'; @override String get contacts_copyAdvertToClipboard => diff --git a/untranslated.json b/untranslated.json index e0ad904..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,127 +1 @@ -{ - "bg": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "de": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "es": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "fr": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "it": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "nl": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "pl": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "pt": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "ru": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "sk": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "sl": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "sv": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "uk": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "zh": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ] -} +{} \ No newline at end of file From c26174ad18740fcfa13b0521594ff673f557042a Mon Sep 17 00:00:00 2001 From: Zach Date: Tue, 10 Feb 2026 09:01:56 -0700 Subject: [PATCH 083/421] Chore bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6474c5f..e1ae023 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 5.0.0+6 +version: 6.0.0+1 environment: sdk: ^3.9.2 From 607583060a9565b41406c61661d16efadb153cbd Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 10 Feb 2026 22:55:39 +0100 Subject: [PATCH 084/421] translations to german updated. --- lib/l10n/app_de.arb | 78 +++++++++++++++--------------- lib/l10n/app_localizations_de.dart | 77 ++++++++++++++--------------- 2 files changed, 78 insertions(+), 77 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 3dcd0ca..66bc049 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -96,14 +96,14 @@ "settings_privacyModeEnabled": "Datenschutzmodus aktiviert", "settings_privacyModeDisabled": "Datenschutzmodus deaktiviert", "settings_actions": "Aktionen", - "settings_sendAdvertisement": "Sende eine Ankündigung", - "settings_sendAdvertisementSubtitle": "Sende Ankündigung", + "settings_sendAdvertisement": "Sende Ankündigung", + "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", "settings_advertisementSent": "Ankündigung gesendet", "settings_syncTime": "Zeitsynchronisierung", "settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein", "settings_timeSynchronized": "Zeit synchronisiert", "settings_refreshContacts": "Kontakte aktualisieren", - "settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden", + "settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden", "settings_rebootDevice": "Gerät neu starten", "settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten", "settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.", @@ -540,7 +540,7 @@ "chat_routingMode": "Routenmodus", "chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)", "chat_forceFloodMode": "Flut-Modus erzwingen", - "chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):", + "chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):", "chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.", "chat_hopSingular": "Sprung", "chat_hopPlural": "Sprünge", @@ -554,7 +554,7 @@ }, "chat_successes": "Erfolgreich", "chat_removePath": "Pfad entfernen", - "chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.", + "chat_noPathHistoryYet": "Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.", "chat_pathActions": "Pfadaktionen:", "chat_setCustomPath": "Lege benutzerdefinierten Pfad fest", "chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen", @@ -717,7 +717,7 @@ "mapCache_cacheArea": "Zwischenspeicherbereich", "mapCache_useCurrentView": "Aktuelle Ansicht verwenden", "mapCache_zoomRange": "Zoom Bereich", - "mapCache_estimatedTiles": "Geschätzte Fliesen: {count}", + "mapCache_estimatedTiles": "Geschätzte Kacheln: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -854,7 +854,7 @@ }, "path_enterCustomPath": "Gebe Pfad ein", "path_currentPathLabel": "Aktueller Pfad", - "path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.", + "path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.", "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)", "path_labelHexPrefixes": "Pfad (Hex-Präfixe)", "path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)", @@ -887,7 +887,7 @@ "repeater_forceFloodMode": "Flut-Modus erzwingen", "repeater_pathManagement": "Pfadverwaltung", "repeater_refresh": "Aktualisieren", - "repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.", + "repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.", "repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -957,7 +957,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Überflut: {flood}, Direkt: {direct}", + "repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -983,7 +983,7 @@ "repeater_adminPassword": "Admin-Passwort", "repeater_adminPasswordHelper": "Vollzugriffspasswort", "repeater_guestPassword": "Gast-Passwort", - "repeater_guestPasswordHelper": "Schreibgeschützter Zugriffspasswort", + "repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort", "repeater_radioSettings": "Funk Einstellungen", "repeater_frequencyMhz": "Frequenz (MHz)", "repeater_frequencyHelper": "300-2500 MHz", @@ -1086,11 +1086,11 @@ } }, "repeater_cliTitle": "Repeater CLI", - "repeater_debugNextCommand": "Fehlersuche Nächster Befehl", + "repeater_debugNextCommand": "Fehlersuche des nächsten Befehls", "repeater_commandHelp": "Hilfe", "repeater_clearHistory": "Löschen der Historie", "repeater_noCommandsSent": "Noch keine Befehle gesendet.", - "repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle", + "repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle", "repeater_enterCommandHint": "Geben Sie den Befehl ein...", "repeater_previousCommand": "Vorhergehende Aktion", "repeater_nextCommand": "Nächste Aktion", @@ -1132,7 +1132,7 @@ "repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)", "repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)", "repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.", - "repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", "repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.", "repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.", @@ -1143,14 +1143,14 @@ "repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).", "repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).", "repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)", - "repeater_cliHelpGetBridgeType": "Ruft Brückentyp none, rs232, espnow ab.", + "repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.", "repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.", "repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.", "repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.", "repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4", "repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.", "repeater_cliHelpRegion": "Listet alle definierten Regionen auf.", - "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.", + "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.", "repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".", "repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.", "repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)", @@ -1243,7 +1243,7 @@ "channelPath_otherObservedPaths": "Sonstige beobachtete Pfade", "channelPath_repeaterHops": "Repeater-Sprünge", "channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.", - "channelPath_messageDetails": "Nachrichtsdetails", + "channelPath_messageDetails": "Nachrichtendetails", "channelPath_senderLabel": "Sender", "channelPath_timeLabel": "Zeit", "channelPath_repeatsLabel": "Wiederholungen", @@ -1347,7 +1347,7 @@ "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", - "listFilter_unreadOnly": "Nur nicht gelesen", + "listFilter_unreadOnly": "Nicht gelesen", "listFilter_newGroup": "Neue Gruppe", "@neighbors_errorLoading": { "placeholders": { @@ -1358,11 +1358,11 @@ }, "repeater_neighbours": "Nachbarn", "repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", - "neighbors_receivedData": "Empfangene Nachbarendaten", - "neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.", + "neighbors_receivedData": "Empfangene Nachbarsdaten", + "neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", "neighbors_repeatersNeighbours": "Nachbarn", - "neighbors_noData": "Keine Nachbardaten verfügbar.", + "neighbors_noData": "Keine Nachbarsdaten verfügbar.", "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", "channels_createPrivateChannel": "Erstelle einen privaten Kanal", @@ -1389,8 +1389,8 @@ } } }, - "neighbors_heardAgo": "Hörte: {time} vor her.", - "neighbors_unknownContact": "Unbekannte {pubkey}", + "neighbors_heardAgo": "Gehört vor: {time}", + "neighbors_unknownContact": "Unbekannt {pubkey}", "settings_locationGPSEnable": "GPS aktivieren", "settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.", "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", @@ -1493,9 +1493,9 @@ "community_deleted": "Community \"{name}\" verlassen", "community_addHashtagChannel": "Füge einen Community-Hashtag hinzu", "community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu", - "community_selectCommunity": "Wählen Sie Community", + "community_selectCommunity": "Wählen Sie eine Community", "community_regularHashtag": "Regulärer Hashtag", - "community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)", + "community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)", "community_communityHashtagDesc": "Nur für Mitglieder der Community", "community_forCommunity": "Für {name}", "community_communityHashtag": "Community Hashtag", @@ -1559,16 +1559,16 @@ "appSettings_languageUk": "Ukrainisch", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", - "contacts_zeroHopAdvert": "Zero-Hop-Anzeige", - "contacts_floodAdvert": "Überflutungsanzeige", + "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", + "contacts_floodAdvert": "Flut-Ankündigung", "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", - "contacts_copyAdvertToClipboard": "Werbung in die Zwischenablage kopieren", + "contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren", "contacts_ShareContact": "Kontakt in die Zwischenablage kopieren", "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", "notification_activityTitle": "MeshCore Aktivität", "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", @@ -1596,22 +1596,22 @@ } }, "notification_receivedNewMessage": "Neue Nachricht empfangen", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", - "settings_gpxExportAll": "Alle Kontakte nach GPX exportieren", - "settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.", - "settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren", - "settings_gpxExportContacts": "Begleiter nach GPX exportieren", + "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", + "settings_gpxExportAll": "Alle Knoten als GPX exportieren", + "settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.", + "settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren", + "settings_gpxExportContacts": "Kontakte als GPX exportieren", "settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.", - "settings_gpxExportContactsSubtitle": "Exportiert Begleiter mit einem Ort in eine GPX-Datei.", + "settings_gpxExportContactsSubtitle": "Exportiert Kontakte mit einem Ort in eine GPX-Datei.", "settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte", - "settings_gpxExportChat": "Begleiterstandorte", + "settings_gpxExportChat": "Kontaktstandorte", "settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.", "settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.", "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", - "settings_gpxExportSuccess": "Erfolgreich GPX-Datei exportiert.", + "settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.", "settings_gpxExportAllContacts": "Alle Kontaktstandorte", - "settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren", - "settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert", - "pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!" + "settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren", + "settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert", + "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!" } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 8ad7f1e..0967227 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -244,10 +244,10 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_actions => 'Aktionen'; @override - String get settings_sendAdvertisement => 'Sende eine Ankündigung'; + String get settings_sendAdvertisement => 'Sende Ankündigung'; @override - String get settings_sendAdvertisementSubtitle => 'Sende Ankündigung'; + String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung'; @override String get settings_advertisementSent => 'Ankündigung gesendet'; @@ -267,7 +267,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_refreshContactsSubtitle => - 'Kontakte-Liste vom Gerät neu laden'; + 'Kontakt-Liste vom Gerät neu laden'; @override String get settings_rebootDevice => 'Gerät neu starten'; @@ -1086,7 +1086,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_recentAckPaths => - 'Aktuelle ACK-Pfade (tasten, um zu verwenden):'; + 'Aktuelle ACK-Pfade (antippen, um zu verwenden):'; @override String get chat_pathHistoryFull => @@ -1117,7 +1117,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.'; + 'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.'; @override String get chat_pathActions => 'Pfadaktionen:'; @@ -1418,7 +1418,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String mapCache_estimatedTiles(int count) { - return 'Geschätzte Fliesen: $count'; + return 'Geschätzte Kacheln: $count'; } @override @@ -1592,7 +1592,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.'; + 'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.'; @override String get path_hexPrefixExample => @@ -1689,7 +1689,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_statusRequestTimeout => - 'Statusanfrage zeitweise fehlgeschlagen.'; + 'Statusanfrage durch Timeout fehlgeschlagen.'; @override String repeater_errorLoadingStatus(String error) { @@ -1766,7 +1766,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Überflut: $flood, Direkt: $direct'; + return 'Flut: $flood, Direkt: $direct'; } @override @@ -1797,7 +1797,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_guestPasswordHelper => - 'Schreibgeschützter Zugriffspasswort'; + 'Schreibgeschütztes Zugriffspasswort'; @override String get repeater_radioSettings => 'Funk Einstellungen'; @@ -1992,7 +1992,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliTitle => 'Repeater CLI'; @override - String get repeater_debugNextCommand => 'Fehlersuche Nächster Befehl'; + String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls'; @override String get repeater_commandHelp => 'Hilfe'; @@ -2005,7 +2005,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_typeCommandOrUseQuick => - 'Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle'; + 'Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle'; @override String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...'; @@ -2131,7 +2131,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetRxDelay => - 'Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetTxDelay => @@ -2175,7 +2175,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpGetBridgeType => - 'Ruft Brückentyp none, rs232, espnow ab.'; + 'Ruft Brückentyp: none, rs232, espnow ab.'; @override String get repeater_cliHelpLogStart => @@ -2202,7 +2202,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.'; + 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.'; @override String get repeater_cliHelpRegionGet => @@ -2351,10 +2351,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get neighbors_receivedData => 'Empfangene Nachbarendaten'; + String get neighbors_receivedData => 'Empfangene Nachbarsdaten'; @override - String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.'; + String get neighbors_requestTimedOut => + 'Anfrage durch Timeout fehlgeschlagen.'; @override String neighbors_errorLoading(String error) { @@ -2365,16 +2366,16 @@ class AppLocalizationsDe extends AppLocalizations { String get neighbors_repeatersNeighbours => 'Nachbarn'; @override - String get neighbors_noData => 'Keine Nachbardaten verfügbar.'; + String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; @override String neighbors_unknownContact(String pubkey) { - return 'Unbekannte $pubkey'; + return 'Unbekannt $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Hörte: $time vor her.'; + return 'Gehört vor: $time'; } @override @@ -2394,7 +2395,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Die Detailangaben für dieses Paket sind nicht verfügbar.'; @override - String get channelPath_messageDetails => 'Nachrichtsdetails'; + String get channelPath_messageDetails => 'Nachrichtendetails'; @override String get channelPath_senderLabel => 'Sender'; @@ -2630,14 +2631,14 @@ class AppLocalizationsDe extends AppLocalizations { 'Füge einen Hashtag-Kanal für diese Community hinzu'; @override - String get community_selectCommunity => 'Wählen Sie Community'; + String get community_selectCommunity => 'Wählen Sie eine Community'; @override String get community_regularHashtag => 'Regulärer Hashtag'; @override String get community_regularHashtagDesc => - 'Öffentliches Hashtag (jeder kann teilnehmen)'; + 'Öffentlicher Hashtag (jeder kann teilnehmen)'; @override String get community_communityHashtag => 'Community Hashtag'; @@ -2682,7 +2683,7 @@ class AppLocalizationsDe extends AppLocalizations { String get listFilter_roomServers => 'Raumserver'; @override - String get listFilter_unreadOnly => 'Nur nicht gelesen'; + String get listFilter_unreadOnly => 'Nicht gelesen'; @override String get listFilter_newGroup => 'Neue Gruppe'; @@ -2701,7 +2702,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => - 'Eine oder mehrere der Hopfen fehlen einen Standort!'; + 'Bei einer oder mehreren Knoten fehlt der Standort!'; @override String get contacts_pathTrace => 'Pfadverfolgung'; @@ -2743,14 +2744,14 @@ class AppLocalizationsDe extends AppLocalizations { 'Kontakt konnte nicht importiert werden'; @override - String get contacts_zeroHopAdvert => 'Zero-Hop-Anzeige'; + String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung'; @override - String get contacts_floodAdvert => 'Überflutungsanzeige'; + String get contacts_floodAdvert => 'Flut-Ankündigung'; @override String get contacts_copyAdvertToClipboard => - 'Werbung in die Zwischenablage kopieren'; + 'Ankündigung in die Zwischenablage kopieren'; @override String get contacts_addContactFromClipboard => @@ -2776,7 +2777,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => - 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; + 'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.'; @override String get notification_activityTitle => 'MeshCore Aktivität'; @@ -2824,28 +2825,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_gpxExportRepeaters => - 'Repeater und Raumserver nach GPX exportieren'; + 'Repeater und Raumserver als GPX exportieren'; @override String get settings_gpxExportRepeatersSubtitle => 'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.'; @override - String get settings_gpxExportContacts => 'Begleiter nach GPX exportieren'; + String get settings_gpxExportContacts => 'Kontakte als GPX exportieren'; @override String get settings_gpxExportContactsSubtitle => - 'Exportiert Begleiter mit einem Ort in eine GPX-Datei.'; + 'Exportiert Kontakte mit einem Ort in eine GPX-Datei.'; @override - String get settings_gpxExportAll => 'Alle Kontakte nach GPX exportieren'; + String get settings_gpxExportAll => 'Alle Knoten als GPX exportieren'; @override String get settings_gpxExportAllSubtitle => - 'Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.'; + 'Exportiert alle Knoten mit einem Standort in eine GPX-Datei.'; @override - String get settings_gpxExportSuccess => 'Erfolgreich GPX-Datei exportiert.'; + String get settings_gpxExportSuccess => 'GPX-Datei erfolgreich exportiert.'; @override String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.'; @@ -2863,16 +2864,16 @@ class AppLocalizationsDe extends AppLocalizations { 'Repeater- und Raumserver-Standorte'; @override - String get settings_gpxExportChat => 'Begleiterstandorte'; + String get settings_gpxExportChat => 'Kontaktstandorte'; @override String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte'; @override String get settings_gpxExportShareText => - 'Kartendaten aus meshcore-open exportiert'; + 'GPX-Kartendaten aus meshcore-open exportiert'; @override String get settings_gpxExportShareSubject => - 'meshcore-open GPX-Kartendaten exportieren'; + 'GPX-Kartendaten aus meshcore-open exportieren'; } From 84a32c1e672ffa35b5f93e0e401d494a5f36ee54 Mon Sep 17 00:00:00 2001 From: 446564 Date: Tue, 10 Feb 2026 19:38:46 -0800 Subject: [PATCH 085/421] remove wakelock was being used to keep ble active which is not what it does in early testing the ble remains connected with display off and also when switching apps --- lib/connector/meshcore_connector.dart | 9 --------- lib/services/background_service.dart | 1 - pubspec.lock | 20 ++++++++++---------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2c56c37..37c9c74 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -5,7 +5,6 @@ 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:wakelock_plus/wakelock_plus.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; @@ -775,9 +774,6 @@ class MeshCoreConnector extends ChangeNotifier { _setState(MeshCoreConnectionState.connected); - // Enable wake lock to prevent BLE disconnection when screen turns off - await WakelockPlus.enable(); - await _requestDeviceInfo(); _startBatteryPolling(); final gotSelfInfo = await _waitForSelfInfo( @@ -886,9 +882,6 @@ class MeshCoreConnector extends ChangeNotifier { _setState(MeshCoreConnectionState.disconnecting); _stopBatteryPolling(); - // Disable wake lock when disconnecting - await WakelockPlus.disable(); - await _notifySubscription?.cancel(); _notifySubscription = null; @@ -3214,8 +3207,6 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleDisconnection() { - // Disable wake lock when connection is lost - WakelockPlus.disable(); _stopBatteryPolling(); for (final entry in _pendingRepeaterAcks.values) { diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index fce77a1..6599fbc 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -28,7 +28,6 @@ class BackgroundService { foregroundTaskOptions: const ForegroundTaskOptions( interval: 5000, autoRunOnBoot: false, - allowWakeLock: true, allowWifiLock: false, ), ); diff --git a/pubspec.lock b/pubspec.lock index fc11656..207ff51 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -489,26 +489,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: From 675083fa01b03fe77e2ce029007bf7a23f9c6588 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:10:49 +0100 Subject: [PATCH 086/421] Update .gitignore to exclude .gradle/ --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b918113..ab7ac25 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,5 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json + +.gradle/ \ No newline at end of file From a4d3d248a5707eecc3b99b9349108e7a10656fd8 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:11:00 +0100 Subject: [PATCH 087/421] Add flake.nix for development environment --- flake.nix | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 flake.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e30165e --- /dev/null +++ b/flake.nix @@ -0,0 +1,116 @@ +{ + description = "MeshCore Flutter Application"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Flutter and Dart + flutter + dart + + # Java (required for Android development) + jdk17 + + # Android development tools + android-tools + gradle + + # iOS development (macOS only) + ] ++ ( + if pkgs.stdenv.isDarwin then + with pkgs.darwin.apple_sdk.frameworks; [ + Cocoa + Security + IOKit + ] + else + [] + ) ++ [ + # Common development tools + git + curl + wget + pkg-config + + # Build tools + cmake + ninja + clang + llvm + # Linux desktop development + gtk3 + glib + libdatrie + sysprof + xorg.libX11 + xorg.libXext + xorg.libXrender + xorg.libXinerama + xorg.libXcursor + xorg.libXi + xorg.libXrandr + xorg.libXdamage + # Optional: testing + lcov + ]; + + shellHook = '' + echo "MeshCore Flutter Development Environment" + export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH" + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH" + export CMAKE_INSTALL_PREFIX="$PWD/build/bundle" + mkdir -p "$PWD/build/bundle" + + # Setup Android SDK in home directory (standard location) + export ANDROID_HOME="$HOME/Android/Sdk" + export ANDROID_SDK_ROOT="$ANDROID_HOME" + export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH" + + # Use gradle wrapper + export GRADLE_USER_HOME="$PWD/.gradle" + + echo "Android SDK: $ANDROID_HOME" + echo "Gradle Home: $GRADLE_USER_HOME" + echo "" + + # Check if Android SDK exists and offer to download if not + if [ ! -d "$ANDROID_HOME" ]; then + echo "WARNING: Android SDK not found at $ANDROID_HOME" + echo "" + echo "To download and set up the Android SDK, run this command:" + echo "" + cat << 'EOF' +mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \ +curl -o cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip && \ +unzip -q cmdline-tools.zip && \ +mkdir -p cmdline-tools/latest && \ +mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || true && \ +rm cmdline-tools.zip && \ +cd cmdline-tools/latest/bin && \ +yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' 'platforms;android-34' 'build-tools;34.0.0' && \ +echo "Android SDK setup complete!" +EOF + echo "" + echo "Then run 'flutter doctor' again to verify." + echo "" + else + echo "Android SDK found at $ANDROID_HOME" + fi + + echo "Running flutter doctor..." + flutter doctor + ''; + }; + } + ); +} From 5b699cd624b5235e26a4bd82523be7fc573c5437 Mon Sep 17 00:00:00 2001 From: Ded Date: Wed, 11 Feb 2026 08:16:07 -0800 Subject: [PATCH 088/421] keep ignores organized --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ab7ac25..2d9a3fc 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ secrets.dart **/ios/Flutter/Flutter.podspec # Android +.gradle/ **/android/.gradle/ **/android/captures/ **/android/local.properties @@ -81,5 +82,3 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json - -.gradle/ \ No newline at end of file From 4afab3f62946a3f76cad918f8dfae017575a62e2 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:25:44 +0100 Subject: [PATCH 089/421] remove unnessisary bits and nix darwin stuff --- flake.nix | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/flake.nix b/flake.nix index e30165e..68f7bca 100644 --- a/flake.nix +++ b/flake.nix @@ -24,44 +24,6 @@ # Android development tools android-tools gradle - - # iOS development (macOS only) - ] ++ ( - if pkgs.stdenv.isDarwin then - with pkgs.darwin.apple_sdk.frameworks; [ - Cocoa - Security - IOKit - ] - else - [] - ) ++ [ - # Common development tools - git - curl - wget - pkg-config - - # Build tools - cmake - ninja - clang - llvm - # Linux desktop development - gtk3 - glib - libdatrie - sysprof - xorg.libX11 - xorg.libXext - xorg.libXrender - xorg.libXinerama - xorg.libXcursor - xorg.libXi - xorg.libXrandr - xorg.libXdamage - # Optional: testing - lcov ]; shellHook = '' From dfd38b19e9b3a4639c578abe16442cac81c6900c Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:26:43 +0100 Subject: [PATCH 090/421] add flake.lock --- flake.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 flake.lock diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4d0355b --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1770562336, + "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} From aa350aa4aecbbf92132d293ac7f77c49d8b6906c Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:33:31 +0100 Subject: [PATCH 091/421] fixing copilot issues --- flake.nix | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 68f7bca..841f553 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,19 @@ # Android development tools android-tools gradle + + # For the shell hook to set up the environment for Flutter development + gtk3 + glib + sysprof + libclang + cmake + ninja + pkg-config + + # Additional tools for installing Android SDK if not present + curl + unzip ]; shellHook = '' @@ -53,10 +66,10 @@ echo "" cat << 'EOF' mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \ -curl -o cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip && \ +curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \ unzip -q cmdline-tools.zip && \ mkdir -p cmdline-tools/latest && \ -mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || true && \ +mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \ rm cmdline-tools.zip && \ cd cmdline-tools/latest/bin && \ yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' 'platforms;android-34' 'build-tools;34.0.0' && \ @@ -69,8 +82,7 @@ EOF echo "Android SDK found at $ANDROID_HOME" fi - echo "Running flutter doctor..." - flutter doctor + echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues." ''; }; } From 4e6e7b60611292429470e7b3a093e8e8d8647e5c Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:45:15 +0100 Subject: [PATCH 092/421] fix smaller copilot issues --- flake.nix | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 841f553..1671145 100644 --- a/flake.nix +++ b/flake.nix @@ -33,6 +33,7 @@ cmake ninja pkg-config + libdatrie # Additional tools for installing Android SDK if not present curl @@ -44,18 +45,13 @@ export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH" export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH" export CMAKE_INSTALL_PREFIX="$PWD/build/bundle" - mkdir -p "$PWD/build/bundle" # Setup Android SDK in home directory (standard location) export ANDROID_HOME="$HOME/Android/Sdk" export ANDROID_SDK_ROOT="$ANDROID_HOME" export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH" - # Use gradle wrapper - export GRADLE_USER_HOME="$PWD/.gradle" - echo "Android SDK: $ANDROID_HOME" - echo "Gradle Home: $GRADLE_USER_HOME" echo "" # Check if Android SDK exists and offer to download if not @@ -72,7 +68,7 @@ mkdir -p cmdline-tools/latest && \ mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \ rm cmdline-tools.zip && \ cd cmdline-tools/latest/bin && \ -yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' 'platforms;android-34' 'build-tools;34.0.0' && \ +yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \ echo "Android SDK setup complete!" EOF echo "" From 9ce00556ec7a3b8a652161514c9f656c6f801913 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 22:40:42 +0100 Subject: [PATCH 093/421] Add warning when bluetooth is off --- lib/l10n/app_en.arb | 3 ++ lib/screens/scanner_screen.dart | 55 +++++++++++++++++++++ untranslated.json | 86 ++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0c54be3..dcbc7cb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -66,6 +66,9 @@ }, "scanner_stop": "Stop", "scanner_scan": "Scan", + "scanner_bluetoothOff": "Bluetooth is off", + "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", + "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 75819a0..2049dab 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:provider/provider.dart'; @@ -18,6 +20,8 @@ class ScannerScreen extends StatefulWidget { class _ScannerScreenState extends State { bool _changedNavigation = false; late final VoidCallback _connectionListener; + BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; + late StreamSubscription _bluetoothStateSubscription; @override void initState() { @@ -39,12 +43,22 @@ class _ScannerScreenState extends State { }; connector.addListener(_connectionListener); + + _bluetoothStateSubscription = + FlutterBluePlus.adapterState.listen((state) { + if (mounted) { + setState(() { + _bluetoothState = state; + }); + } + }); } @override void dispose() { final connector = Provider.of(context, listen: false); connector.removeListener(_connectionListener); + _bluetoothStateSubscription.cancel(); super.dispose(); } @@ -62,6 +76,10 @@ class _ScannerScreenState extends State { builder: (context, connector, child) { return Column( children: [ + // Bluetooth off warning + if (_bluetoothState != BluetoothAdapterState.on) + _bluetoothOffWarning(context), + // Status bar _buildStatusBar(context, connector), @@ -205,4 +223,41 @@ class _ScannerScreenState extends State { } } } + + Widget _bluetoothOffWarning(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + color: Colors.red.withValues(alpha: 0.15), + child: Row( + children: [ + Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.scanner_bluetoothOff, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.scanner_bluetoothOffMessage, + style: TextStyle( + color: Colors.red.withValues(alpha: 0.85), + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ); + } } diff --git a/untranslated.json b/untranslated.json index 9e26dfe..2b5dc43 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,85 @@ -{} \ No newline at end of file +{ + "bg": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "de": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "es": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "fr": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "it": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "nl": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "pl": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "pt": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "ru": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "sk": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "sl": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "sv": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "uk": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "zh": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ] +} From 9332d8126f34abb128ea4059864ab33e5f23b475 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 22:58:15 +0100 Subject: [PATCH 094/421] linted and added greying out --- lib/screens/scanner_screen.dart | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 2049dab..e52afb3 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -44,12 +44,16 @@ class _ScannerScreenState extends State { connector.addListener(_connectionListener); - _bluetoothStateSubscription = - FlutterBluePlus.adapterState.listen((state) { + _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) { if (mounted) { setState(() { _bluetoothState = state; }); + // Cancel scan if Bluetooth turns off while scanning + if (state != BluetoothAdapterState.on && + connector.state == MeshCoreConnectionState.scanning) { + connector.stopScan(); + } } }); } @@ -94,15 +98,23 @@ class _ScannerScreenState extends State { builder: (context, connector, child) { final isScanning = connector.state == MeshCoreConnectionState.scanning; + final isBluetoothOn = _bluetoothState == BluetoothAdapterState.on; return FloatingActionButton.extended( - onPressed: () { - if (isScanning) { - connector.stopScan(); - } else { - connector.startScan(); - } - }, + onPressed: isBluetoothOn + ? () { + if (isScanning) { + connector.stopScan(); + } else { + connector.startScan(); + } + } + : null, + backgroundColor: isBluetoothOn ? null : Colors.grey, + foregroundColor: isBluetoothOn ? null : Colors.white, + mouseCursor: isBluetoothOn + ? SystemMouseCursors.click + : SystemMouseCursors.forbidden, icon: isScanning ? const SizedBox( width: 20, From bc77f7e28753f5a0490245902853e6d891cb34df Mon Sep 17 00:00:00 2001 From: Leah <45321184+ChaoticLeah@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:03:41 +0100 Subject: [PATCH 095/421] Remove unused translation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/l10n/app_en.arb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dcbc7cb..668f72e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -68,7 +68,6 @@ "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", - "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", From 6a666839b628af8b70db5f1bf4c68d339a388352 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:05:00 -0500 Subject: [PATCH 096/421] Fix battery chemistry dropdown layout overflow --- lib/screens/app_settings_screen.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 135babd..4e31733 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -384,6 +384,7 @@ class AppSettingsScreen extends StatelessWidget { ); } + // Fixed rendering issues Widget _buildBatteryCard( BuildContext context, AppSettingsService settingsService, @@ -399,6 +400,7 @@ class AppSettingsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 4), Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( @@ -406,6 +408,8 @@ class AppSettingsScreen extends StatelessWidget { style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), + + // Main tile (icon + text only) ListTile( leading: const Icon(Icons.battery_full), title: Text(context.l10n.appSettings_batteryChemistry), @@ -416,8 +420,19 @@ class AppSettingsScreen extends StatelessWidget { ) : context.l10n.appSettings_batteryChemistryConnectFirst, ), - trailing: DropdownButton( - value: selection, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + + // Dropdown (separate full-width row) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DropdownButtonFormField( + initialValue: selection, + isExpanded: true, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + isDense: true, + ), onChanged: isConnected ? (value) { if (value != null) { From c05f813d653d0500a936d3dd50768bc6eb3582de Mon Sep 17 00:00:00 2001 From: Leah <45321184+ChaoticLeah@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:02:56 +0100 Subject: [PATCH 097/421] Update lib/screens/scanner_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/screens/scanner_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index e52afb3..1aeba47 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -243,7 +243,7 @@ class _ScannerScreenState extends State { color: Colors.red.withValues(alpha: 0.15), child: Row( children: [ - Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), + const Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), const SizedBox(width: 12), Expanded( child: Column( From 01c83909897c8969c5b982548a3c7f401b5c3e4e Mon Sep 17 00:00:00 2001 From: Leah Date: Thu, 12 Feb 2026 20:14:56 +0100 Subject: [PATCH 098/421] make stuff unawaited + maybe fix edge case? --- lib/screens/scanner_screen.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index e52afb3..ef7f90f 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -50,9 +50,8 @@ class _ScannerScreenState extends State { _bluetoothState = state; }); // Cancel scan if Bluetooth turns off while scanning - if (state != BluetoothAdapterState.on && - connector.state == MeshCoreConnectionState.scanning) { - connector.stopScan(); + if (state != BluetoothAdapterState.on) { + unawaited(connector.stopScan()); } } }); @@ -62,7 +61,7 @@ class _ScannerScreenState extends State { void dispose() { final connector = Provider.of(context, listen: false); connector.removeListener(_connectionListener); - _bluetoothStateSubscription.cancel(); + unawaited(_bluetoothStateSubscription.cancel()); super.dispose(); } From fac062a100d224b45f6b724198638313f5d33a25 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:46:28 -0500 Subject: [PATCH 099/421] Refine device info layout and add collapsible map legend (#164) --- lib/screens/map_screen.dart | 143 ++++++++++++++++++++----------- lib/screens/settings_screen.dart | 140 ++++++++++++++++++++---------- 2 files changed, 190 insertions(+), 93 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f522407..bc213f9 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -51,6 +51,7 @@ class _MapScreenState extends State { bool _isSelectingPoi = false; bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; + bool _legendExpanded = false; @override void initState() { @@ -503,60 +504,102 @@ class _MapScreenState extends State { top: 16, right: 16, child: Card( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.l10n.map_nodesCount(nodeCount), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + setState(() { + _legendExpanded = !_legendExpanded; + }); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.map_nodesCount(nodeCount), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + Text( + context.l10n.map_pinsCount(markerCount), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ], + ), + const SizedBox(width: 8), + AnimatedRotation( + turns: _legendExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: const Icon(Icons.expand_more, size: 20), + ), + ], ), ), - Text( - context.l10n.map_pinsCount(markerCount), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, + ), + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + _buildLegendItem( + Icons.person, + context.l10n.map_chat, + Colors.blue, + ), + _buildLegendItem( + Icons.router, + context.l10n.map_repeater, + Colors.green, + ), + _buildLegendItem( + Icons.meeting_room, + context.l10n.map_room, + Colors.purple, + ), + _buildLegendItem( + Icons.sensors, + context.l10n.map_sensor, + Colors.orange, + ), + _buildLegendItem( + Icons.flag, + context.l10n.map_pinDm, + Colors.blue, + ), + _buildLegendItem( + Icons.flag, + context.l10n.map_pinPrivate, + Colors.purple, + ), + _buildLegendItem( + Icons.flag, + context.l10n.map_pinPublic, + Colors.orange, + ), + ], ), ), - const SizedBox(height: 8), - _buildLegendItem( - Icons.person, - context.l10n.map_chat, - Colors.blue, - ), - _buildLegendItem( - Icons.router, - context.l10n.map_repeater, - Colors.green, - ), - _buildLegendItem( - Icons.meeting_room, - context.l10n.map_room, - Colors.purple, - ), - _buildLegendItem( - Icons.sensors, - context.l10n.map_sensor, - Colors.orange, - ), - _buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue), - _buildLegendItem( - Icons.flag, - context.l10n.map_pinPrivate, - Colors.purple, - ), - _buildLegendItem( - Icons.flag, - context.l10n.map_pinPublic, - Colors.orange, - ), - ], - ), + crossFadeState: _legendExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 200), + ), + ], ), ), ); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 2212b8d..4943284 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -21,6 +21,7 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { bool _showBatteryVoltage = false; + bool _deviceInfoExpanded = false; String _appVersion = ''; @override @@ -74,43 +75,84 @@ class _SettingsScreenState extends State { MeshCoreConnector connector, ) { final l10n = context.l10n; + return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.settings_deviceInfo, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - _buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName), - _buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel), - _buildInfoRow( - l10n.settings_infoStatus, - connector.isConnected - ? l10n.common_connected - : l10n.common_disconnected, - ), - _buildBatteryInfoRow(context, connector), - if (connector.selfName != null) - _buildInfoRow(l10n.settings_nodeName, connector.selfName!), - if (connector.selfPublicKey != null) - _buildInfoRow( - l10n.settings_infoPublicKey, - '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + setState(() { + _deviceInfoExpanded = !_deviceInfoExpanded; + }); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Row( + children: [ + Expanded( + child: Text( + l10n.settings_deviceInfo, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + AnimatedRotation( + turns: _deviceInfoExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: const Icon(Icons.expand_more), + ), + ], ), - _buildInfoRow( - l10n.settings_infoContactsCount, - '${connector.contacts.length}', ), - _buildInfoRow( - l10n.settings_infoChannelCount, - '${connector.channels.length}', + ), + + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow( + l10n.settings_infoName, + connector.deviceDisplayName, + ), + _buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel), + _buildInfoRow( + l10n.settings_infoStatus, + connector.isConnected + ? l10n.common_connected + : l10n.common_disconnected, + ), + _buildBatteryInfoRow(context, connector), + if (connector.selfName != null) + _buildInfoRow(l10n.settings_nodeName, connector.selfName!), + if (connector.selfPublicKey != null) + _buildInfoRow( + l10n.settings_infoPublicKey, + '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...', + ), + _buildInfoRow( + l10n.settings_infoContactsCount, + '${connector.contacts.length}', + ), + _buildInfoRow( + l10n.settings_infoChannelCount, + '${connector.channels.length}', + ), + ], + ), ), - ], - ), + crossFadeState: _deviceInfoExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 200), + ), + ], ), ); } @@ -355,22 +397,33 @@ class _SettingsScreenState extends State { Color? valueColor, VoidCallback? onTap, }) { + final theme = Theme.of(context); + final row = Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (leading != null) ...[leading, const SizedBox(width: 8)], - Text(label, style: TextStyle(color: Colors.grey[600])), + Expanded( + child: Text( + label, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ), ], ), - Flexible( - child: Text( - value, - style: TextStyle(fontWeight: FontWeight.w500, color: valueColor), - overflow: TextOverflow.ellipsis, + const SizedBox(height: 4), + Text( + value, + style: theme.textTheme.bodyLarge?.copyWith( + color: valueColor, + fontWeight: FontWeight.w500, ), ), ], @@ -379,11 +432,12 @@ class _SettingsScreenState extends State { if (onTap != null) { return InkWell( + borderRadius: BorderRadius.circular(6), onTap: onTap, - borderRadius: BorderRadius.circular(4), child: row, ); } + return row; } From 73081862ad132cdc7b51616af605b94f54a3e89e Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Feb 2026 00:10:34 -0800 Subject: [PATCH 100/421] Add path tracing functionality (#165) - Implemented path tracing feature in the map screen, allowing users to add nodes to a path and visualize it on the map. - Added buttons for starting path tracing, removing the last node, and running the path trace. - Introduced a new overlay to display current path information and distance. - Updated localization files for multiple languages to include new strings related to path tracing. - Refactored map rendering logic to accommodate path tracing visuals. --- lib/l10n/app_bg.arb | 9 +- lib/l10n/app_de.arb | 26 +++-- lib/l10n/app_en.arb | 5 + lib/l10n/app_es.arb | 26 +++-- lib/l10n/app_fr.arb | 7 +- lib/l10n/app_it.arb | 9 +- lib/l10n/app_localizations.dart | 30 ++++++ lib/l10n/app_localizations_bg.dart | 16 +++ lib/l10n/app_localizations_de.dart | 16 +++ lib/l10n/app_localizations_en.dart | 15 +++ lib/l10n/app_localizations_es.dart | 15 +++ lib/l10n/app_localizations_fr.dart | 16 +++ lib/l10n/app_localizations_it.dart | 15 +++ lib/l10n/app_localizations_nl.dart | 16 +++ lib/l10n/app_localizations_pl.dart | 15 +++ lib/l10n/app_localizations_pt.dart | 15 +++ lib/l10n/app_localizations_ru.dart | 15 +++ lib/l10n/app_localizations_sk.dart | 15 +++ lib/l10n/app_localizations_sl.dart | 15 +++ lib/l10n/app_localizations_sv.dart | 15 +++ lib/l10n/app_localizations_uk.dart | 15 +++ lib/l10n/app_localizations_zh.dart | 15 +++ lib/l10n/app_nl.arb | 9 +- lib/l10n/app_pl.arb | 9 +- lib/l10n/app_pt.arb | 9 +- lib/l10n/app_ru.arb | 9 +- lib/l10n/app_sk.arb | 9 +- lib/l10n/app_sl.arb | 9 +- lib/l10n/app_sv.arb | 9 +- lib/l10n/app_uk.arb | 9 +- lib/l10n/app_zh.arb | 9 +- lib/screens/map_screen.dart | 150 +++++++++++++++++++++++++++- lib/screens/path_trace_map.dart | 153 +++++++++++++++-------------- 33 files changed, 599 insertions(+), 126 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 01afb0c..54d792e 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", "notification_newTypeDiscovered": "Открит нов {contactType}", "notification_receivedNewMessage": "Получено ново съобщение", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", "settings_gpxExportAll": "Експортирай всички контакти в GPX", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Местоположения на всички контакти", "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", - "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!" - + "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!", + "map_pathTraceCancelled": "Отменен е следването на пътя.", + "pathTrace_clearTooltip": "Изчисти пътя", + "map_removeLast": "Премахни Последно", + "map_runTrace": "Изпълни Път на Следване", + "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 66bc049..2cece8a 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1569,34 +1569,40 @@ "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", - "notification_activityTitle": "MeshCore Aktivität", "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", "@notification_messagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}", "@notification_channelMessagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}", "@notification_newNodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newTypeDiscovered": "Neuer {contactType} entdeckt", "@notification_newTypeDiscovered": { "placeholders": { - "contactType": {"type": "String"} + "contactType": { + "type": "String" + } } }, "notification_receivedNewMessage": "Neue Nachricht empfangen", - "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", "settings_gpxExportAll": "Alle Knoten als GPX exportieren", "settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.", "settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren", @@ -1612,6 +1618,10 @@ "settings_gpxExportAllContacts": "Alle Kontaktstandorte", "settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren", "settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert", - "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!" - + "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!", + "map_removeLast": "Letztes Entfernen", + "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", + "map_runTrace": "Pfadverlauf ausführen", + "pathTrace_clearTooltip": "Pfad löschen", + "map_pathTraceCancelled": "Pfadverfolgung abgebrochen." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0c54be3..9fdf51f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -619,6 +619,10 @@ "map_sharedPin": "Shared pin", "map_joinRoom": "Join Room", "map_manageRepeater": "Manage Repeater", + "map_tapToAdd": "Tap on nodes to add them to the path.", + "map_runTrace": "Run Path Trace", + "map_removeLast": "Remove Last", + "map_pathTraceCancelled": "Path trace cancelled.", "mapCache_title": "Offline Map Cache", "mapCache_selectAreaFirst": "Select an area to cache first", "mapCache_noTilesToDownload": "No tiles to download for this area", @@ -1317,6 +1321,7 @@ "pathTrace_notAvailable": "Path trace not available.", "pathTrace_refreshTooltip": "Refresh Path Trace.", "pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!", + "pathTrace_clearTooltip": "Clear path.", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3d5ab63..f1a0651 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1569,34 +1569,40 @@ "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", - "notification_activityTitle": "Actividad de MeshCore", "notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}", "@notification_messagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}", "@notification_channelMessagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}", "@notification_newNodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newTypeDiscovered": "Nuevo {contactType} descubierto", "@notification_newTypeDiscovered": { "placeholders": { - "contactType": {"type": "String"} + "contactType": { + "type": "String" + } } }, "notification_receivedNewMessage": "Nuevo mensaje recibido", - "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", @@ -1612,6 +1618,10 @@ "settings_gpxExportAllContacts": "Todas las ubicaciones de contactos", "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", - "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación" - + "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación", + "pathTrace_clearTooltip": "Borrar ruta", + "map_runTrace": "Ejecutar Rastreo de Ruta", + "map_tapToAdd": "Pulse en los nodos para agregarlos al camino.", + "map_removeLast": "Eliminar último", + "map_pathTraceCancelled": "Rastreo de ruta cancelado." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 0827576..39d7176 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1590,5 +1590,10 @@ "settings_gpxExportAllContacts": "Tous les emplacements des contacts", "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", - "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !" + "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !", + "map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.", + "pathTrace_clearTooltip": "Effacer le chemin", + "map_pathTraceCancelled": "Traçage de chemin annulé", + "map_removeLast": "Supprimer le dernier", + "map_runTrace": "Exécuter la traçage de chemin" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index dd9c373..99f11f2 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", "notification_newTypeDiscovered": "Nuovo {contactType} scoperto", "notification_receivedNewMessage": "Nuovo messaggio ricevuto", - "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", "settings_gpxExportContacts": "Esporta compagni in GPX", "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", - "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!" - + "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", + "map_removeLast": "Rimuovi ultimo", + "map_pathTraceCancelled": "Tracciamento del percorso annullato.", + "pathTrace_clearTooltip": "Pulisci percorso", + "map_runTrace": "Esegui Path Trace", + "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8f4d693..20ac664 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2518,6 +2518,30 @@ abstract class AppLocalizations { /// **'Manage Repeater'** String get map_manageRepeater; + /// No description provided for @map_tapToAdd. + /// + /// In en, this message translates to: + /// **'Tap on nodes to add them to the path.'** + String get map_tapToAdd; + + /// No description provided for @map_runTrace. + /// + /// In en, this message translates to: + /// **'Run Path Trace'** + String get map_runTrace; + + /// No description provided for @map_removeLast. + /// + /// In en, this message translates to: + /// **'Remove Last'** + String get map_removeLast; + + /// No description provided for @map_pathTraceCancelled. + /// + /// In en, this message translates to: + /// **'Path trace cancelled.'** + String get map_pathTraceCancelled; + /// No description provided for @mapCache_title. /// /// In en, this message translates to: @@ -4730,6 +4754,12 @@ abstract class AppLocalizations { /// **'One or more of the hops is missing a location!'** String get pathTrace_someHopsNoLocation; + /// No description provided for @pathTrace_clearTooltip. + /// + /// In en, this message translates to: + /// **'Clear path.'** + String get pathTrace_clearTooltip; + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 68e821e..30a7ca7 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1363,6 +1363,19 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_manageRepeater => 'Управление на Повтарящ се Елемент'; + @override + String get map_tapToAdd => + 'Натиснете върху възлите, за да ги добавите към пътя.'; + + @override + String get map_runTrace => 'Изпълни Път на Следване'; + + @override + String get map_removeLast => 'Премахни Последно'; + + @override + String get map_pathTraceCancelled => 'Отменен е следването на пътя.'; + @override String get mapCache_title => 'Кеш на офлайн карти'; @@ -2699,6 +2712,9 @@ class AppLocalizationsBg extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Един или повече от хмелите липсва местоположение!'; + @override + String get pathTrace_clearTooltip => 'Изчисти пътя'; + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 0967227..4c3245d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1362,6 +1362,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_manageRepeater => 'Repeater verwalten'; + @override + String get map_tapToAdd => + 'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.'; + + @override + String get map_runTrace => 'Pfadverlauf ausführen'; + + @override + String get map_removeLast => 'Letztes Entfernen'; + + @override + String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.'; + @override String get mapCache_title => 'Offline-Karten-Cache'; @@ -2704,6 +2717,9 @@ class AppLocalizationsDe extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Bei einer oder mehreren Knoten fehlt der Standort!'; + @override + String get pathTrace_clearTooltip => 'Pfad löschen'; + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 8eb76e8..120d242 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1342,6 +1342,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_manageRepeater => 'Manage Repeater'; + @override + String get map_tapToAdd => 'Tap on nodes to add them to the path.'; + + @override + String get map_runTrace => 'Run Path Trace'; + + @override + String get map_removeLast => 'Remove Last'; + + @override + String get map_pathTraceCancelled => 'Path trace cancelled.'; + @override String get mapCache_title => 'Offline Map Cache'; @@ -2659,6 +2671,9 @@ class AppLocalizationsEn extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'One or more of the hops is missing a location!'; + @override + String get pathTrace_clearTooltip => 'Clear path.'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cc9bff7..069e3c7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1360,6 +1360,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_manageRepeater => 'Gestionar Repetidor'; + @override + String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.'; + + @override + String get map_runTrace => 'Ejecutar Rastreo de Ruta'; + + @override + String get map_removeLast => 'Eliminar último'; + + @override + String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.'; + @override String get mapCache_title => 'Caché de Mapa Offline'; @@ -2698,6 +2710,9 @@ class AppLocalizationsEs extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Uno o más de los lúpulos carecen de una ubicación'; + @override + String get pathTrace_clearTooltip => 'Borrar ruta'; + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index eb6d50f..44b70af 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1367,6 +1367,19 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_manageRepeater => 'Gérer le répéteur'; + @override + String get map_tapToAdd => + 'Appuyez sur les nœuds pour les ajouter au chemin.'; + + @override + String get map_runTrace => 'Exécuter la traçage de chemin'; + + @override + String get map_removeLast => 'Supprimer le dernier'; + + @override + String get map_pathTraceCancelled => 'Traçage de chemin annulé'; + @override String get mapCache_title => 'Cache de Carte Hors Ligne'; @@ -2714,6 +2727,9 @@ class AppLocalizationsFr extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Un ou plusieurs des sauts manquent d\'une localisation !'; + @override + String get pathTrace_clearTooltip => 'Effacer le chemin'; + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 68465dd..0bce5b4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1359,6 +1359,18 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_manageRepeater => 'Gestisci Ripetitore'; + @override + String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.'; + + @override + String get map_runTrace => 'Esegui Path Trace'; + + @override + String get map_removeLast => 'Rimuovi ultimo'; + + @override + String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.'; + @override String get mapCache_title => 'Cache Mappa Offline'; @@ -2699,6 +2711,9 @@ class AppLocalizationsIt extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Uno o più dei luppoli mancano di una posizione!'; + @override + String get pathTrace_clearTooltip => 'Pulisci percorso'; + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a093a35..432de6c 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1355,6 +1355,19 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_manageRepeater => 'Beheer Repeater'; + @override + String get map_tapToAdd => + 'Tik op knooppunten om ze toe te voegen aan het pad'; + + @override + String get map_runTrace => 'Padeshulp traceren'; + + @override + String get map_removeLast => 'Verwijder Laatste'; + + @override + String get map_pathTraceCancelled => 'Pad traceren geannuleerd'; + @override String get mapCache_title => 'Offline Kaarten Cache'; @@ -2689,6 +2702,9 @@ class AppLocalizationsNl extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Een of meer van de hops ontbreken een locatie!'; + @override + String get pathTrace_clearTooltip => 'Weg wissen'; + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 895e3c7..fb53893 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1361,6 +1361,18 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_manageRepeater => 'Zarządzaj Powtórzami'; + @override + String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.'; + + @override + String get map_runTrace => 'Uruchom ślad ścieżki'; + + @override + String get map_removeLast => 'Usuń ostatni'; + + @override + String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.'; + @override String get mapCache_title => 'Bufor Map Offline'; @@ -2697,6 +2709,9 @@ class AppLocalizationsPl extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Jeden lub więcej z chmieli nie ma określonej lokalizacji!'; + @override + String get pathTrace_clearTooltip => 'Wyczyść ścieżkę'; + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ce8d07c..f9d8415 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1361,6 +1361,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_manageRepeater => 'Gerenciar Repetidor'; + @override + String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.'; + + @override + String get map_runTrace => 'Executar Traçado de Caminho'; + + @override + String get map_removeLast => 'Remover Último'; + + @override + String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.'; + @override String get mapCache_title => 'Cache de Mapa Offline'; @@ -2700,6 +2712,9 @@ class AppLocalizationsPt extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Um ou mais dos lúpulos estão sem localização!'; + @override + String get pathTrace_clearTooltip => 'Limpar caminho'; + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9e4cd95..f8a90ad 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1362,6 +1362,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_manageRepeater => 'Управление репитером'; + @override + String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.'; + + @override + String get map_runTrace => 'Запустить трассировку пути'; + + @override + String get map_removeLast => 'Удалить последний'; + + @override + String get map_pathTraceCancelled => 'Отмена трассировки пути'; + @override String get mapCache_title => 'Кэш офлайн-карты'; @@ -2702,6 +2714,9 @@ class AppLocalizationsRu extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Одному или нескольким хмелям не указано местоположение!'; + @override + String get pathTrace_clearTooltip => 'Очистить путь'; + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index ed66f97..7e61cc2 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1356,6 +1356,18 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_manageRepeater => 'Spravovať Opakovanie'; + @override + String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.'; + + @override + String get map_runTrace => 'Spustiť trasovaním cesty'; + + @override + String get map_removeLast => 'Odstrániť posledný'; + + @override + String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.'; + @override String get mapCache_title => 'Offline Mapa Pamäť'; @@ -2685,6 +2697,9 @@ class AppLocalizationsSk extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Jedna alebo viac chmeľov chýba lokalita!'; + @override + String get pathTrace_clearTooltip => 'Zmazať cestu'; + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 3307547..53da59a 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1352,6 +1352,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_manageRepeater => 'Upravljajte Ponovitve'; + @override + String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.'; + + @override + String get map_runTrace => 'Zaženi sledenje poti'; + + @override + String get map_removeLast => 'Odstrani Zadnji'; + + @override + String get map_pathTraceCancelled => 'Spremljanje poti je prekinjeno.'; + @override String get mapCache_title => 'Omrezni predpomnilnik zemljeških zemljejevskih slik'; @@ -2688,6 +2700,9 @@ class AppLocalizationsSl extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Ena ali več hmelju manjka lokacija!'; + @override + String get pathTrace_clearTooltip => 'Počisti pot'; + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 5239b06..d125979 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1348,6 +1348,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_manageRepeater => 'Hantera Upprepare'; + @override + String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.'; + + @override + String get map_runTrace => 'Kör spårsökning'; + + @override + String get map_removeLast => 'Ta bort sista'; + + @override + String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.'; + @override String get mapCache_title => 'Offline Kartcache'; @@ -2673,6 +2685,9 @@ class AppLocalizationsSv extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'En eller flera av humlen saknar en plats!'; + @override + String get pathTrace_clearTooltip => 'Rensa väg'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index b6ff8ce..de68840 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1361,6 +1361,18 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_manageRepeater => 'Керувати ретранслятором'; + @override + String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху'; + + @override + String get map_runTrace => 'Виконати трасування шляху'; + + @override + String get map_removeLast => 'Видалити останній'; + + @override + String get map_pathTraceCancelled => 'Відмінується трасування шляху'; + @override String get mapCache_title => 'Офлайн-кеш карти'; @@ -2709,6 +2721,9 @@ class AppLocalizationsUk extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Одне або більше хмелів відсутнє місце розташування!'; + @override + String get pathTrace_clearTooltip => 'Очистити шлях'; + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index a529a1b..040d4ef 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1301,6 +1301,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_manageRepeater => '管理重复器'; + @override + String get map_tapToAdd => '点击节点将其添加到路径中'; + + @override + String get map_runTrace => '运行路径跟踪'; + + @override + String get map_removeLast => '删除最后一个'; + + @override + String get map_pathTraceCancelled => '路径跟踪已取消'; + @override String get mapCache_title => '离线地图缓存'; @@ -2557,6 +2569,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => '其中一个或多个啤酒花缺少位置!'; + @override + String get pathTrace_clearTooltip => '清除路径'; + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 91163ac..033b424 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}", "notification_newTypeDiscovered": "Nieuw {contactType} ontdekt", "notification_receivedNewMessage": "Nieuw bericht ontvangen", - "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Alle contactlocaties", "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren", - "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!" - + "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!", + "map_removeLast": "Verwijder Laatste", + "pathTrace_clearTooltip": "Weg wissen", + "map_pathTraceCancelled": "Pad traceren geannuleerd", + "map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad", + "map_runTrace": "Padeshulp traceren" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3c2a96f..e0a08dd 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}", "notification_newTypeDiscovered": "Nowy {contactType} wykryty", "notification_receivedNewMessage": "Otrzymano nową wiadomość", - "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportChat": "Lokalizacje towarzyszy", "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open", - "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!" - + "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!", + "map_pathTraceCancelled": "Śledzenie ścieżki anulowano.", + "map_runTrace": "Uruchom ślad ścieżki", + "pathTrace_clearTooltip": "Wyczyść ścieżkę", + "map_removeLast": "Usuń ostatni", + "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index dc38c11..20eee88 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", "notification_newTypeDiscovered": "Novo {contactType} descoberto", "notification_receivedNewMessage": "Nova mensagem recebida", - "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Todos os locais de contatos", "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", - "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!" - + "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!", + "map_runTrace": "Executar Traçado de Caminho", + "map_pathTraceCancelled": "Rastreamento de caminho cancelado.", + "pathTrace_clearTooltip": "Limpar caminho", + "map_removeLast": "Remover Último", + "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index ddbbe79..165ccbb 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -815,7 +815,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", "notification_newTypeDiscovered": "Обнаружен новый {contactType}", "notification_receivedNewMessage": "Получено новое сообщение", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", "settings_gpxExportContacts": "Экспортировать спутников в GPX", @@ -831,6 +830,10 @@ "settings_gpxExportNoContacts": "Нет контактов для экспорта.", "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", - "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!" - + "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!", + "map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.", + "map_removeLast": "Удалить последний", + "map_pathTraceCancelled": "Отмена трассировки пути", + "pathTrace_clearTooltip": "Очистить путь", + "map_runTrace": "Запустить трассировку пути" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index c09502a..08439da 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", "notification_newTypeDiscovered": "Nový {contactType} objavený", "notification_receivedNewMessage": "Prijatá nová správa", - "contacts_ShareContact": "Kopírovať kontakt do schránky", "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", "settings_gpxExportContacts": "Export sprievodcov do GPX", "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportChat": "Lokácie sprievodcov", "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", - "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!" - + "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!", + "pathTrace_clearTooltip": "Zmazať cestu", + "map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.", + "map_removeLast": "Odstrániť posledný", + "map_runTrace": "Spustiť trasovaním cesty", + "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 97a396a..d321fd1 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", "notification_newTypeDiscovered": "Odkrito novo {contactType}", "notification_receivedNewMessage": "Prejeto novo sporočilo", - "contacts_ShareContact": "Kopiraj stik v Odložišče", "settings_gpxExportAll": "Izvozi vse kontakte v GPX", "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportNoContacts": "Ni stikov za izvoz.", "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte", - "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!" - + "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!", + "map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.", + "map_removeLast": "Odstrani Zadnji", + "map_runTrace": "Zaženi sledenje poti", + "pathTrace_clearTooltip": "Počisti pot", + "map_pathTraceCancelled": "Spremljanje poti je prekinjeno." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 6df28bd..4b01494 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}", "notification_newTypeDiscovered": "Ny {contactType} upptäckt", "notification_receivedNewMessage": "Nytt meddelande mottaget", - "contacts_ShareContactZeroHop": "Dela kontakt via annons", "settings_gpxExportAll": "Exportera alla kontakter till GPX", "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Alla kontakters platser", "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open", - "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!" - + "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!", + "pathTrace_clearTooltip": "Rensa väg", + "map_pathTraceCancelled": "Sökvägsspårning avbruten.", + "map_runTrace": "Kör spårsökning", + "map_tapToAdd": "Tryck på noder för att lägga till dem i banan.", + "map_removeLast": "Ta bort sista" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 9f5e64d..9ff05fb 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", "notification_newTypeDiscovered": "Виявлено новий {contactType}", "notification_receivedNewMessage": "Отримано нове повідомлення", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", "settings_gpxExportAllContacts": "Усі місця контактів", "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", - "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!" - + "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", + "map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху", + "map_runTrace": "Виконати трасування шляху", + "pathTrace_clearTooltip": "Очистити шлях", + "map_removeLast": "Видалити останній", + "map_pathTraceCancelled": "Відмінується трасування шляху" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 8c65510..0f8c079 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} 个新节点", "notification_newTypeDiscovered": "发现新 {contactType}", "notification_receivedNewMessage": "收到新消息", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", @@ -1591,6 +1590,10 @@ "settings_gpxExportNoContacts": "没有联系人可导出", "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出", - "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!" - + "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!", + "map_tapToAdd": "点击节点将其添加到路径中", + "pathTrace_clearTooltip": "清除路径", + "map_pathTraceCancelled": "路径跟踪已取消", + "map_removeLast": "删除最后一个", + "map_runTrace": "运行路径跟踪" } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index bc213f9..552f579 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,8 +1,10 @@ import 'dart:math'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -48,9 +50,13 @@ class _MapScreenState extends State { final MapMarkerService _markerService = MapMarkerService(); final Set _hiddenMarkerIds = {}; Set _removedMarkerIds = {}; + bool _isBuildingPathTrace = false; bool _isSelectingPoi = false; bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; + final List _pathTrace = []; + final List _points = []; + final List _polylines = []; bool _legendExpanded = false; @override @@ -148,6 +154,19 @@ class _MapScreenState extends State { .where((c) => c.hasLocation) .toList(); + _polylines.clear(); + _polylines.addAll( + _points.length > 1 + ? [ + Polyline( + points: _points, + strokeWidth: 4, + color: Colors.blueAccent, + ), + ] + : [], + ); + // Calculate center and zoom of all nodes, or default to (0, 0) LatLng center = const LatLng(0, 0); double initialZoom = 10.0; @@ -248,6 +267,12 @@ class _MapScreenState extends State { centerTitle: true, automaticallyImplyLeading: false, actions: [ + if (!_isBuildingPathTrace) + IconButton( + icon: const Icon(Icons.radar), + onPressed: () => _startPath(), + tooltip: context.l10n.contacts_pathTrace, + ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( @@ -335,6 +360,8 @@ class _MapScreenState extends State { MapTileCacheService.userAgentPackageName, maxZoom: 19, ), + if (_polylines.isNotEmpty && _isBuildingPathTrace) + PolylineLayer(polylines: _polylines), MarkerLayer( markers: [ if (highlightPosition != null) @@ -391,7 +418,12 @@ class _MapScreenState extends State { ), ], ), - _buildLegend(contactsWithLocation.length, sharedMarkers.length), + if (!_isBuildingPathTrace) + _buildLegend( + contactsWithLocation.length, + sharedMarkers.length, + ), + if (_isBuildingPathTrace) _buildPathTraceOverlay(), ], ), bottomNavigationBar: SafeArea( @@ -435,7 +467,11 @@ class _MapScreenState extends State { width: 35, height: 35, child: GestureDetector( - onTap: () => _showNodeInfo(context, contact), + onLongPress: () => + _isBuildingPathTrace ? _showNodeInfo(context, contact) : null, + onTap: () => _isBuildingPathTrace + ? _addToPath(context, contact) + : _showNodeInfo(context, contact), child: Column( children: [ Container( @@ -607,7 +643,7 @@ class _MapScreenState extends State { Widget _buildLegendItem(IconData icon, String label, Color color) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 1.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -1455,6 +1491,114 @@ class _MapScreenState extends State { return context.l10n.time_allTime; } } + + void _addToPath(BuildContext context, Contact contact) { + setState(() { + _pathTrace.add( + contact.publicKey[0], + ); // Add first 16 bytes of public key to path trace + _points.add(LatLng(contact.latitude!, contact.longitude!)); + }); + } + + void _startPath() { + setState(() { + _isBuildingPathTrace = true; + _pathTrace.clear(); + _points.clear(); + _polylines.clear(); + }); + } + + void _removePath() { + setState(() { + _pathTrace.removeLast(); // Remove last node from path trace + _points.removeLast(); // Remove last point from points list + _polylines.clear(); // Clear polylines + }); + } + + Widget _buildPathTraceOverlay() { + final l10n = context.l10n; + return Positioned( + top: 16, + left: 16, + right: 16, + child: Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.contacts_pathTrace, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (_pathTrace.isEmpty) const SizedBox(height: 8), + if (_pathTrace.isEmpty) + Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)), + const SizedBox(height: 6), + if (_pathTrace.isNotEmpty) + Text( + "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}", + style: TextStyle(fontSize: 12, color: Colors.grey[700]), + ), + SelectableText( + _pathTrace + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(','), + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 6), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_pathTrace.isNotEmpty) + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: l10n.contacts_pathTrace, + path: Uint8List.fromList(_pathTrace), + ), + ), + ); + setState(() { + _isBuildingPathTrace = false; + }); + }, + child: Text(l10n.map_runTrace), + ), + if (_pathTrace.isNotEmpty) + ElevatedButton( + onPressed: _removePath, + child: Text(l10n.map_removeLast), + ), + if (_pathTrace.isEmpty) + ElevatedButton( + onPressed: () { + setState(() { + _isBuildingPathTrace = false; + _pathTrace.clear(); + _points.clear(); + _polylines.clear(); + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.map_pathTraceCancelled)), + ); + }, + child: Text(l10n.common_cancel), + ), + ], + ), + ], + ), + ), + ), + ); + } } class _MarkerPayload { diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 39de31e..5b19c30 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -13,6 +13,23 @@ import 'package:meshcore_open/services/map_tile_cache_service.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; import 'package:provider/provider.dart'; +double getPathDistanceMeters(List points) { + if (points.length <= 1) return 0.0; + + double distanceMeters = 0.0; + final distanceCalculator = Distance(); + + for (int i = 0; i < points.length - 1; i++) { + distanceMeters += distanceCalculator(points[i], points[i + 1]); + } + + return distanceMeters; +} + +String formatDistance(double distanceMeters) { + return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)'; +} + class PathTraceData { final Uint8List pathData; final Uint8List snrData; @@ -50,7 +67,6 @@ class _PathTraceMapScreenState extends State { bool _isLoading = false; bool _failed2Loaded = false; bool _hasData = false; - bool _noLocationErr = false; PathTraceData? _traceData; List _points = []; List _polylines = []; @@ -58,7 +74,7 @@ class _PathTraceMapScreenState extends State { double _initialZoom = 2.0; LatLngBounds? _bounds; ValueKey _mapKey = const ValueKey('initial'); - double _pathDistance = 0.0; + double _pathDistanceMeters = 0.0; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -93,23 +109,11 @@ class _PathTraceMapScreenState extends State { return traceBytes; } - double getPathDistance() { - double totalDistance = 0.0; - final distanceCalculator = Distance(); - - for (int i = 0; i < _points.length - 1; i++) { - totalDistance += distanceCalculator(_points[i], _points[i + 1]); - } - - return totalDistance; - } - Future _doPathTrace() async { if (mounted) { setState(() { _isLoading = true; _failed2Loaded = false; - _noLocationErr = false; }); } @@ -160,6 +164,14 @@ class _PathTraceMapScreenState extends State { }); } + if (code == respCodeErr) { + _timeoutTimer?.cancel(); + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + } // Check if it's a binary response if (frame.length > 8 && code == pushCodeTraceData && @@ -215,8 +227,6 @@ class _PathTraceMapScreenState extends State { contact.latitude != null && contact.longitude != null) { _points.add(LatLng(contact.latitude!, contact.longitude!)); - } else { - _noLocationErr = true; } } _polylines = _points.length > 1 @@ -235,7 +245,7 @@ class _PathTraceMapScreenState extends State { _mapKey = ValueKey( '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', ); - _pathDistance = getPathDistance(); + _pathDistanceMeters = getPathDistanceMeters(_points); }); } @@ -279,20 +289,7 @@ class _PathTraceMapScreenState extends State { top: false, child: Stack( children: [ - if (_noLocationErr) - Center( - child: Card( - color: Colors.red, - child: Padding( - padding: EdgeInsets.all(12), - child: Text( - context.l10n.pathTrace_someHopsNoLocation, - style: TextStyle(color: Colors.white), - ), - ), - ), - ), - if (!_hasData && !_noLocationErr) + if (!_hasData) Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -304,43 +301,11 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData && !_noLocationErr) - FlutterMap( - key: _mapKey, - options: MapOptions( - initialCenter: _initialCenter!, - initialZoom: _initialZoom, - initialCameraFit: _bounds == null - ? null - : CameraFit.bounds( - bounds: _bounds!, - padding: const EdgeInsets.all(64), - maxZoom: 16, - ), - minZoom: 2.0, - maxZoom: 18.0, - ), - children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), - if (_polylines.isNotEmpty) - PolylineLayer(polylines: _polylines), - if (_traceData!.pathData.isNotEmpty) - MarkerLayer( - markers: _buildHopMarkers(_traceData!.pathData), - ), - ], - ), + if (_hasData) _buildMapPathTrace(context, tileCache), if (_points.isEmpty && !_hasData && !_isLoading && - !_failed2Loaded && - !_noLocationErr) + !_failed2Loaded) Center( child: Card( color: Colors.white.withValues(alpha: 0.9), @@ -352,8 +317,7 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (_hasData && !_noLocationErr) - _buildLegendCard(context, _traceData!), + if (_hasData) _buildLegendCard(context, _traceData!), ], ), ), @@ -365,7 +329,8 @@ class _PathTraceMapScreenState extends State { List _buildHopMarkers(List pathData) { return [ for (final hop in pathData) - if (_traceData!.pathContacts[hop]!.hasLocation) + if (_traceData!.pathContacts[hop] != null && + _traceData!.pathContacts[hop]!.hasLocation) Marker( point: LatLng( _traceData!.pathContacts[hop]!.latitude!, @@ -453,7 +418,9 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } } else { final contactName = @@ -462,7 +429,9 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } } @@ -475,7 +444,9 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } else { return context.l10n.pathTrace_you; } @@ -486,10 +457,46 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } } + Widget _buildMapPathTrace( + BuildContext context, + MapTileCacheService tileCache, + ) { + return FlutterMap( + key: _mapKey, + options: MapOptions( + interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate), + initialCenter: _initialCenter!, + initialZoom: _initialZoom, + initialCameraFit: _bounds == null + ? null + : CameraFit.bounds( + bounds: _bounds!, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + minZoom: 2.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines), + if (_traceData!.pathData.isNotEmpty) + MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)), + ], + ); + } + Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) { final l10n = context.l10n; final maxHeight = MediaQuery.of(context).size.height * 0.35; @@ -509,7 +516,7 @@ class _PathTraceMapScreenState extends State { Padding( padding: const EdgeInsets.all(12), child: Text( - '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}', style: const TextStyle(fontWeight: FontWeight.w600), ), ), From b526175be4ee978dbb136b058c07f08d49a25b5a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 01:13:06 -0700 Subject: [PATCH 101/421] bump version for android --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e1ae023..fa3bd63 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 6.0.0+1 +version: 6.0.0+7 environment: sdk: ^3.9.2 From 37db955ab2d344bf1cbfd5fb1f0e561038eca0c4 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 01:46:40 -0700 Subject: [PATCH 102/421] Fixed banner flash, added enable bluetooth button fixed theme to use app theme colors removed FAB overrides because material 3 does this for us, fixed missing translations. --- lib/l10n/app_bg.arb | 5 +- lib/l10n/app_de.arb | 5 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 5 +- lib/l10n/app_fr.arb | 5 +- lib/l10n/app_it.arb | 5 +- lib/l10n/app_localizations.dart | 18 +++++++ lib/l10n/app_localizations_bg.dart | 10 ++++ lib/l10n/app_localizations_de.dart | 10 ++++ lib/l10n/app_localizations_en.dart | 10 ++++ lib/l10n/app_localizations_es.dart | 10 ++++ lib/l10n/app_localizations_fr.dart | 10 ++++ lib/l10n/app_localizations_it.dart | 10 ++++ lib/l10n/app_localizations_nl.dart | 10 ++++ lib/l10n/app_localizations_pl.dart | 10 ++++ lib/l10n/app_localizations_pt.dart | 10 ++++ lib/l10n/app_localizations_ru.dart | 10 ++++ lib/l10n/app_localizations_sk.dart | 10 ++++ lib/l10n/app_localizations_sl.dart | 10 ++++ lib/l10n/app_localizations_sv.dart | 10 ++++ lib/l10n/app_localizations_uk.dart | 10 ++++ lib/l10n/app_localizations_zh.dart | 9 ++++ lib/l10n/app_nl.arb | 5 +- lib/l10n/app_pl.arb | 5 +- lib/l10n/app_pt.arb | 5 +- lib/l10n/app_ru.arb | 5 +- lib/l10n/app_sk.arb | 5 +- lib/l10n/app_sl.arb | 5 +- lib/l10n/app_sv.arb | 5 +- lib/l10n/app_uk.arb | 5 +- lib/l10n/app_zh.arb | 5 +- lib/screens/scanner_screen.dart | 32 +++++------ pubspec.lock | 20 +++---- untranslated.json | 86 +----------------------------- 34 files changed, 251 insertions(+), 125 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 54d792e..a1cfb3e 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "Изчисти пътя", "map_removeLast": "Премахни Последно", "map_runTrace": "Изпълни Път на Следване", - "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя." + "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", + "scanner_bluetoothOff": "Bluetooth е изключен.", + "scanner_enableBluetooth": "Активирайте Bluetooth", + "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2cece8a..2e66222 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1623,5 +1623,8 @@ "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", "map_runTrace": "Pfadverlauf ausführen", "pathTrace_clearTooltip": "Pfad löschen", - "map_pathTraceCancelled": "Pfadverfolgung abgebrochen." + "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", + "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", + "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", + "scanner_enableBluetooth": "Bluetooth aktivieren" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9f24f46..890b992 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -68,6 +68,7 @@ "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", + "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f1a0651..e6b22fa 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1623,5 +1623,8 @@ "map_runTrace": "Ejecutar Rastreo de Ruta", "map_tapToAdd": "Pulse en los nodos para agregarlos al camino.", "map_removeLast": "Eliminar último", - "map_pathTraceCancelled": "Rastreo de ruta cancelado." + "map_pathTraceCancelled": "Rastreo de ruta cancelado.", + "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", + "scanner_bluetoothOff": "Bluetooth está desactivado.", + "scanner_enableBluetooth": "Habilitar Bluetooth" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 39d7176..f11238b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "Effacer le chemin", "map_pathTraceCancelled": "Traçage de chemin annulé", "map_removeLast": "Supprimer le dernier", - "map_runTrace": "Exécuter la traçage de chemin" + "map_runTrace": "Exécuter la traçage de chemin", + "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", + "scanner_bluetoothOff": "Le Bluetooth est désactivé.", + "scanner_enableBluetooth": "Activer le Bluetooth" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 99f11f2..601f1af 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1595,5 +1595,8 @@ "map_pathTraceCancelled": "Tracciamento del percorso annullato.", "pathTrace_clearTooltip": "Pulisci percorso", "map_runTrace": "Esegui Path Trace", - "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso." + "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", + "scanner_bluetoothOff": "Il Bluetooth è disattivato.", + "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", + "scanner_enableBluetooth": "Abilita il Bluetooth" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 20ac664..bc1cfbd 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -376,6 +376,24 @@ abstract class AppLocalizations { /// **'Scan'** String get scanner_scan; + /// No description provided for @scanner_bluetoothOff. + /// + /// In en, this message translates to: + /// **'Bluetooth is off'** + String get scanner_bluetoothOff; + + /// No description provided for @scanner_bluetoothOffMessage. + /// + /// In en, this message translates to: + /// **'Please turn on Bluetooth to scan for devices'** + String get scanner_bluetoothOffMessage; + + /// No description provided for @scanner_enableBluetooth. + /// + /// In en, this message translates to: + /// **'Enable Bluetooth'** + String get scanner_enableBluetooth; + /// No description provided for @device_quickSwitch. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 30a7ca7..695bde2 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -143,6 +143,16 @@ class AppLocalizationsBg extends AppLocalizations { @override String get scanner_scan => 'Сканирай'; + @override + String get scanner_bluetoothOff => 'Bluetooth е изключен.'; + + @override + String get scanner_bluetoothOffMessage => + 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; + + @override + String get scanner_enableBluetooth => 'Активирайте Bluetooth'; + @override String get device_quickSwitch => 'Бързо превключване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 4c3245d..6e04655 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -143,6 +143,16 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_scan => 'Scannen'; + @override + String get scanner_bluetoothOff => 'Bluetooth ist deaktiviert.'; + + @override + String get scanner_bluetoothOffMessage => + 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; + + @override + String get scanner_enableBluetooth => 'Bluetooth aktivieren'; + @override String get device_quickSwitch => 'Schnelles Umschalten'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 120d242..5ed8162 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -142,6 +142,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scanner_scan => 'Scan'; + @override + String get scanner_bluetoothOff => 'Bluetooth is off'; + + @override + String get scanner_bluetoothOffMessage => + 'Please turn on Bluetooth to scan for devices'; + + @override + String get scanner_enableBluetooth => 'Enable Bluetooth'; + @override String get device_quickSwitch => 'Quick switch'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 069e3c7..ff4e8f3 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -143,6 +143,16 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_scan => 'Escanea'; + @override + String get scanner_bluetoothOff => 'Bluetooth está desactivado.'; + + @override + String get scanner_bluetoothOffMessage => + 'Por favor, active el Bluetooth para escanear dispositivos.'; + + @override + String get scanner_enableBluetooth => 'Habilitar Bluetooth'; + @override String get device_quickSwitch => 'Cambiar rápidamente'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 44b70af..f8b7775 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -143,6 +143,16 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_scan => 'Scanner'; + @override + String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.'; + + @override + String get scanner_bluetoothOffMessage => + 'Veuillez activer le Bluetooth pour rechercher des appareils.'; + + @override + String get scanner_enableBluetooth => 'Activer le Bluetooth'; + @override String get device_quickSwitch => 'Basculement rapide'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 0bce5b4..d8fd612 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -143,6 +143,16 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scanner_scan => 'Scansiona'; + @override + String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.'; + + @override + String get scanner_bluetoothOffMessage => + 'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.'; + + @override + String get scanner_enableBluetooth => 'Abilita il Bluetooth'; + @override String get device_quickSwitch => 'Passa velocemente'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 432de6c..de6c909 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -142,6 +142,16 @@ class AppLocalizationsNl extends AppLocalizations { @override String get scanner_scan => 'Scan'; + @override + String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld'; + + @override + String get scanner_bluetoothOffMessage => + 'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.'; + + @override + String get scanner_enableBluetooth => 'Activeer Bluetooth'; + @override String get device_quickSwitch => 'Snelle overschakeling'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index fb53893..c5d2bd9 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -143,6 +143,16 @@ class AppLocalizationsPl extends AppLocalizations { @override String get scanner_scan => 'Przeskanuj'; + @override + String get scanner_bluetoothOff => 'Bluetooth jest wyłączony'; + + @override + String get scanner_bluetoothOffMessage => + 'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.'; + + @override + String get scanner_enableBluetooth => 'Włącz Bluetooth'; + @override String get device_quickSwitch => 'Szybka zmiana'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index f9d8415..b5ffdd6 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -143,6 +143,16 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scanner_scan => 'Digitalizar'; + @override + String get scanner_bluetoothOff => 'Bluetooth está desativado'; + + @override + String get scanner_bluetoothOffMessage => + 'Por favor, ative o Bluetooth para escanear por dispositivos.'; + + @override + String get scanner_enableBluetooth => 'Ative o Bluetooth'; + @override String get device_quickSwitch => 'Mudar rapidamente'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index f8a90ad..c41bf20 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -142,6 +142,16 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scanner_scan => 'Сканирование'; + @override + String get scanner_bluetoothOff => 'Bluetooth выключен'; + + @override + String get scanner_bluetoothOffMessage => + 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; + + @override + String get scanner_enableBluetooth => 'Включите Bluetooth'; + @override String get device_quickSwitch => 'Быстрое переключение'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 7e61cc2..e0ee455 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -143,6 +143,16 @@ class AppLocalizationsSk extends AppLocalizations { @override String get scanner_scan => 'Skončiť'; + @override + String get scanner_bluetoothOff => 'Bluetooth je vypnutý'; + + @override + String get scanner_bluetoothOffMessage => + 'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.'; + + @override + String get scanner_enableBluetooth => 'Povolte Bluetooth'; + @override String get device_quickSwitch => 'Rýchle prepínač'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 53da59a..36445f7 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -143,6 +143,16 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_scan => 'Skeniraj'; + @override + String get scanner_bluetoothOff => 'Bluetooth je izklopljen'; + + @override + String get scanner_bluetoothOffMessage => + 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; + + @override + String get scanner_enableBluetooth => 'Omogočite Bluetooth'; + @override String get device_quickSwitch => 'Hitro preklop'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index d125979..cbfa45d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -142,6 +142,16 @@ class AppLocalizationsSv extends AppLocalizations { @override String get scanner_scan => 'Skanna'; + @override + String get scanner_bluetoothOff => 'Bluetooth är avstängt'; + + @override + String get scanner_bluetoothOffMessage => + 'Vänligen aktivera Bluetooth för att söka efter enheter.'; + + @override + String get scanner_enableBluetooth => 'Aktivera Bluetooth'; + @override String get device_quickSwitch => 'Snabb växling'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index de68840..4dfa260 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -143,6 +143,16 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scanner_scan => 'Сканувати'; + @override + String get scanner_bluetoothOff => 'Bluetooth вимкнено'; + + @override + String get scanner_bluetoothOffMessage => + 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; + + @override + String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; + @override String get device_quickSwitch => 'Швидке перемикання'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 040d4ef..4441b22 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -142,6 +142,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scanner_scan => '扫描'; + @override + String get scanner_bluetoothOff => '蓝牙已关闭'; + + @override + String get scanner_bluetoothOffMessage => '请打开蓝牙功能,以便搜索设备。'; + + @override + String get scanner_enableBluetooth => '启用蓝牙'; + @override String get device_quickSwitch => '快速切换'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 033b424..b150d62 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "Weg wissen", "map_pathTraceCancelled": "Pad traceren geannuleerd", "map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad", - "map_runTrace": "Padeshulp traceren" + "map_runTrace": "Padeshulp traceren", + "scanner_enableBluetooth": "Activeer Bluetooth", + "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", + "scanner_bluetoothOff": "Bluetooth is uitgeschakeld" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index e0a08dd..d576fca 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1595,5 +1595,8 @@ "map_runTrace": "Uruchom ślad ścieżki", "pathTrace_clearTooltip": "Wyczyść ścieżkę", "map_removeLast": "Usuń ostatni", - "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki." + "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", + "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", + "scanner_bluetoothOff": "Bluetooth jest wyłączony", + "scanner_enableBluetooth": "Włącz Bluetooth" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 20eee88..53c43fe 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1595,5 +1595,8 @@ "map_pathTraceCancelled": "Rastreamento de caminho cancelado.", "pathTrace_clearTooltip": "Limpar caminho", "map_removeLast": "Remover Último", - "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho." + "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", + "scanner_enableBluetooth": "Ative o Bluetooth", + "scanner_bluetoothOff": "Bluetooth está desativado", + "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 165ccbb..19b4990 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -835,5 +835,8 @@ "map_removeLast": "Удалить последний", "map_pathTraceCancelled": "Отмена трассировки пути", "pathTrace_clearTooltip": "Очистить путь", - "map_runTrace": "Запустить трассировку пути" + "map_runTrace": "Запустить трассировку пути", + "scanner_enableBluetooth": "Включите Bluetooth", + "scanner_bluetoothOff": "Bluetooth выключен", + "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 08439da..3f61292 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1595,5 +1595,8 @@ "map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.", "map_removeLast": "Odstrániť posledný", "map_runTrace": "Spustiť trasovaním cesty", - "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené." + "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", + "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", + "scanner_bluetoothOff": "Bluetooth je vypnutý", + "scanner_enableBluetooth": "Povolte Bluetooth" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index d321fd1..d94695e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1595,5 +1595,8 @@ "map_removeLast": "Odstrani Zadnji", "map_runTrace": "Zaženi sledenje poti", "pathTrace_clearTooltip": "Počisti pot", - "map_pathTraceCancelled": "Spremljanje poti je prekinjeno." + "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", + "scanner_enableBluetooth": "Omogočite Bluetooth", + "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", + "scanner_bluetoothOff": "Bluetooth je izklopljen" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 4b01494..59b3fca 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1595,5 +1595,8 @@ "map_pathTraceCancelled": "Sökvägsspårning avbruten.", "map_runTrace": "Kör spårsökning", "map_tapToAdd": "Tryck på noder för att lägga till dem i banan.", - "map_removeLast": "Ta bort sista" + "map_removeLast": "Ta bort sista", + "scanner_enableBluetooth": "Aktivera Bluetooth", + "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", + "scanner_bluetoothOff": "Bluetooth är avstängt" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 9ff05fb..26f3984 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1595,5 +1595,8 @@ "map_runTrace": "Виконати трасування шляху", "pathTrace_clearTooltip": "Очистити шлях", "map_removeLast": "Видалити останній", - "map_pathTraceCancelled": "Відмінується трасування шляху" + "map_pathTraceCancelled": "Відмінується трасування шляху", + "scanner_enableBluetooth": "Увімкніть Bluetooth", + "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", + "scanner_bluetoothOff": "Bluetooth вимкнено" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 0f8c079..7b4b3ab 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "清除路径", "map_pathTraceCancelled": "路径跟踪已取消", "map_removeLast": "删除最后一个", - "map_runTrace": "运行路径跟踪" + "map_runTrace": "运行路径跟踪", + "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", + "scanner_bluetoothOff": "蓝牙已关闭", + "scanner_enableBluetooth": "启用蓝牙" } diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 7428145..70f00a0 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -80,7 +80,7 @@ class _ScannerScreenState extends State { return Column( children: [ // Bluetooth off warning - if (_bluetoothState != BluetoothAdapterState.on) + if (_bluetoothState == BluetoothAdapterState.off) _bluetoothOffWarning(context), // Status bar @@ -97,23 +97,18 @@ class _ScannerScreenState extends State { builder: (context, connector, child) { final isScanning = connector.state == MeshCoreConnectionState.scanning; - final isBluetoothOn = _bluetoothState == BluetoothAdapterState.on; + final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; return FloatingActionButton.extended( - onPressed: isBluetoothOn - ? () { + onPressed: isBluetoothOff + ? null + : () { if (isScanning) { connector.stopScan(); } else { connector.startScan(); } - } - : null, - backgroundColor: isBluetoothOn ? null : Colors.grey, - foregroundColor: isBluetoothOn ? null : Colors.white, - mouseCursor: isBluetoothOn - ? SystemMouseCursors.click - : SystemMouseCursors.forbidden, + }, icon: isScanning ? const SizedBox( width: 20, @@ -236,13 +231,14 @@ class _ScannerScreenState extends State { } Widget _bluetoothOffWarning(BuildContext context) { + final errorColor = Theme.of(context).colorScheme.error; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - color: Colors.red.withValues(alpha: 0.15), + color: errorColor.withValues(alpha: 0.15), child: Row( children: [ - const Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), + Icon(Icons.bluetooth_disabled, size: 24, color: errorColor), const SizedBox(width: 12), Expanded( child: Column( @@ -250,8 +246,8 @@ class _ScannerScreenState extends State { children: [ Text( context.l10n.scanner_bluetoothOff, - style: const TextStyle( - color: Colors.red, + style: TextStyle( + color: errorColor, fontWeight: FontWeight.w600, fontSize: 14, ), @@ -260,13 +256,17 @@ class _ScannerScreenState extends State { Text( context.l10n.scanner_bluetoothOffMessage, style: TextStyle( - color: Colors.red.withValues(alpha: 0.85), + color: errorColor.withValues(alpha: 0.85), fontSize: 12, ), ), ], ), ), + TextButton( + onPressed: () => FlutterBluePlus.turnOn(), + child: Text(context.l10n.scanner_enableBluetooth), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 207ff51..fc11656 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -489,26 +489,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" timezone: dependency: transitive description: diff --git a/untranslated.json b/untranslated.json index 2b5dc43..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,85 +1 @@ -{ - "bg": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "de": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "es": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "fr": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "it": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "nl": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "pl": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "pt": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "ru": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "sk": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "sl": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "sv": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "uk": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "zh": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ] -} +{} \ No newline at end of file From 9250dfec31ca68359642d740afc36251c6e62e59 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 01:54:30 -0700 Subject: [PATCH 103/421] Gate the turn on BLE button to android --- lib/screens/scanner_screen.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 70f00a0..932e29c 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -263,10 +264,11 @@ class _ScannerScreenState extends State { ], ), ), - TextButton( - onPressed: () => FlutterBluePlus.turnOn(), - child: Text(context.l10n.scanner_enableBluetooth), - ), + if (Platform.isAndroid) + TextButton( + onPressed: () => FlutterBluePlus.turnOn(), + child: Text(context.l10n.scanner_enableBluetooth), + ), ], ), ); From 72f0aa7208cb52b91eb3c21855d4defbc87e1003 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 02:22:45 -0700 Subject: [PATCH 104/421] Update dependencies and improve code consistency across multiple files --- android/app/build.gradle.kts | 2 +- lib/screens/app_debug_log_screen.dart | 2 +- lib/screens/ble_debug_log_screen.dart | 2 +- lib/screens/channel_message_path_screen.dart | 2 +- lib/screens/path_trace_map.dart | 2 +- lib/screens/settings_screen.dart | 4 +- lib/services/background_service.dart | 16 +-- lib/services/notification_service.dart | 36 +++--- lib/widgets/qr_scanner_widget.dart | 2 +- pubspec.lock | 116 +++++++++---------- pubspec.yaml | 16 +-- windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 98 insertions(+), 103 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 740451b..e7d2b42 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -83,5 +83,5 @@ flutter { } dependencies { - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") } diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index e8a0aa4..5372ea8 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -55,7 +55,7 @@ class AppDebugLogScreen extends StatelessWidget { child: hasEntries ? ListView.separated( itemCount: entries.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final entry = entries[index]; return ListTile( diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 7675cae..7cebb76 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -100,7 +100,7 @@ class _BleDebugLogScreenState extends State { itemCount: showingFrames ? entries.length : rawEntries.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { if (showingFrames) { final entry = entries[index]; diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 8dea475..1b0544c 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -594,7 +594,7 @@ class _ChannelMessagePathMapScreenState : ListView.separated( padding: const EdgeInsets.symmetric(vertical: 4), itemCount: hops.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final hop = hops[index]; return ListTile( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5b19c30..7677d0d 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -530,7 +530,7 @@ class _PathTraceMapScreenState extends State { child: ListView.separated( padding: const EdgeInsets.symmetric(vertical: 4), itemCount: pathTraceData.pathData.length + 1, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { return Column( children: [ diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4943284..73376f0 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -742,7 +742,7 @@ class _SettingsScreenState extends State { ); } - _gpxExport( + Future _gpxExport( GpxExport exporter, String name, String description, @@ -782,7 +782,7 @@ class _SettingsScreenState extends State { } } - _buildExportCard(MeshCoreConnector connector) { + Widget _buildExportCard(MeshCoreConnector connector) { final l10n = context.l10n; return Card( child: Column( diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 6599fbc..0edd393 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,4 +1,3 @@ -import 'dart:isolate'; import 'dart:io'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; @@ -15,18 +14,13 @@ class BackgroundService { channelDescription: 'Keeps MeshCore running in the background.', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, - iconData: const NotificationIconData( - resType: ResourceType.mipmap, - resPrefix: ResourcePrefix.ic, - name: 'launcher', - ), ), iosNotificationOptions: const IOSNotificationOptions( showNotification: false, playSound: false, ), - foregroundTaskOptions: const ForegroundTaskOptions( - interval: 5000, + foregroundTaskOptions: ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(5000), autoRunOnBoot: false, allowWifiLock: false, ), @@ -63,13 +57,13 @@ void startCallback() { class _MeshCoreTaskHandler extends TaskHandler { @override - void onStart(DateTime timestamp, SendPort? sendPort) {} + Future onStart(DateTime timestamp, TaskStarter starter) async {} @override - void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {} + void onRepeatEvent(DateTime timestamp) {} @override - void onDestroy(DateTime timestamp, SendPort? sendPort) {} + Future onDestroy(DateTime timestamp, bool isTimeout) async {} @override void onNotificationButtonPressed(String id) {} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 57331aa..fc979c6 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -67,7 +67,7 @@ class NotificationService { try { await _notifications.initialize( - initSettings, + settings: initSettings, onDidReceiveNotificationResponse: _onNotificationTapped, ); _isInitialized = true; @@ -149,10 +149,10 @@ class NotificationService { ); await _notifications.show( - contactId?.hashCode ?? 0, - contactName, - message, - notificationDetails, + id: contactId?.hashCode ?? 0, + title: contactName, + body: message, + notificationDetails: notificationDetails, payload: 'message:$contactId', ); } @@ -194,10 +194,10 @@ class NotificationService { ); await _notifications.show( - contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - _l10n.notification_newTypeDiscovered(contactType), - contactName, - notificationDetails, + id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: _l10n.notification_newTypeDiscovered(contactType), + body: contactName, + notificationDetails: notificationDetails, payload: 'advert:$contactId', ); } @@ -248,10 +248,10 @@ class NotificationService { : preview; await _notifications.show( - channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - channelName, - body, - notificationDetails, + id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: channelName, + body: body, + notificationDetails: notificationDetails, payload: 'channel:$channelIndex', ); } @@ -285,7 +285,7 @@ class NotificationService { } Future cancel(int id) async { - await _notifications.cancel(id); + await _notifications.cancel(id: id); } // ───────────────────────────────────────────────────────────────── @@ -469,10 +469,10 @@ class NotificationService { const notificationDetails = NotificationDetails(android: androidDetails); await _notifications.show( - 'batch_summary'.hashCode, - _l10n.notification_activityTitle, - parts.join(', '), - notificationDetails, + id: 'batch_summary'.hashCode, + title: _l10n.notification_activityTitle, + body: parts.join(', '), + notificationDetails: notificationDetails, payload: 'batch', ); } diff --git a/lib/widgets/qr_scanner_widget.dart b/lib/widgets/qr_scanner_widget.dart index 4dc2ee5..5b6cf5e 100644 --- a/lib/widgets/qr_scanner_widget.dart +++ b/lib/widgets/qr_scanner_widget.dart @@ -156,7 +156,7 @@ class _QrScannerWidgetState extends State MobileScanner( controller: _controller, onDetect: _handleDetection, - errorBuilder: (context, error, child) { + errorBuilder: (context, error) { return _buildErrorWidget(context, error); }, ), diff --git a/pubspec.lock b/pubspec.lock index fc11656..09e9301 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,14 +153,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" dbus: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" fake_async: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" file: dependency: transitive description: @@ -202,10 +210,10 @@ packages: dependency: "direct main" description: name: flutter_blue_plus - sha256: "399b3dbc15562ef59749f04e43a99ccbb91540022380d5f269aff3c2787534e4" + sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" flutter_blue_plus_android: dependency: transitive description: @@ -218,10 +226,10 @@ packages: dependency: transitive description: name: flutter_blue_plus_darwin - sha256: d160a8128e3a016fa58dd65ab6dac05cbc73e0fa799a1f24211d041641ed63ba + sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.1" flutter_blue_plus_linux: dependency: transitive description: @@ -250,10 +258,10 @@ packages: dependency: transitive description: name: flutter_blue_plus_winrt - sha256: "34be2d8e23d5881b46accebb0e71025f7d52869d72ea98b5082c20764e06aa80" + sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 url: "https://pub.dev" source: hosted - version: "0.0.16" + version: "0.0.18" flutter_cache_manager: dependency: "direct main" description: @@ -266,18 +274,18 @@ packages: dependency: "direct main" description: name: flutter_foreground_task - sha256: "6cf10a27f5e344cd2ecad0752d3a5f4ec32846d82fda8753b3fe2480ebb832a3" + sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "9.2.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.4" flutter_linkify: dependency: "direct main" description: @@ -290,34 +298,42 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" url: "https://pub.dev" source: hosted - version: "18.0.1" + version: "20.1.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "7.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "10.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -327,10 +343,10 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da" + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "8.2.2" flutter_test: dependency: "direct dev" description: flutter @@ -361,10 +377,10 @@ packages: dependency: transitive description: name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" http: dependency: "direct main" description: @@ -397,14 +413,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: transitive description: @@ -457,10 +465,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.1.0" lists: dependency: transitive description: @@ -529,10 +537,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 url: "https://pub.dev" source: hosted - version: "6.0.11" + version: "7.1.4" native_toolchain_c: dependency: transitive description: @@ -553,10 +561,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e" + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" url: "https://pub.dev" source: hosted - version: "9.2.5" + version: "9.3.0" octo_image: dependency: transitive description: @@ -569,10 +577,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d url: "https://pub.dev" source: hosted - version: "8.3.1" + version: "9.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -665,18 +673,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" - polylabel: - dependency: transitive - description: - name: polylabel - sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "4.0.0" posix: dependency: transitive description: @@ -822,10 +822,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" sqflite: dependency: transitive description: @@ -958,10 +958,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -1030,10 +1030,10 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.4.0" wakelock_plus_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fa3bd63..f5ceaaf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,19 +41,19 @@ dependencies: provider: ^6.1.5+1 shared_preferences: ^2.2.2 uuid: ^4.3.3 - flutter_map: ^7.0.2 + flutter_map: ^8.2.2 latlong2: ^0.9.1 - flutter_local_notifications: ^18.0.1 + flutter_local_notifications: ^20.1.0 crypto: ^3.0.3 - pointycastle: ^3.7.4 + pointycastle: ^4.0.0 http: ^1.2.0 cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1 - flutter_foreground_task: ^6.1.2 + flutter_foreground_task: ^9.2.0 wakelock_plus: ^1.2.8 characters: ^1.4.0 - package_info_plus: ^8.0.0 - mobile_scanner: ^6.0.0 # QR/barcode scanning + package_info_plus: ^9.0.0 + mobile_scanner: ^7.1.4 # QR/barcode scanning qr_flutter: ^4.1.0 # QR code generation url_launcher: ^6.3.0 # Launch URLs in system browser flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text @@ -70,8 +70,8 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 - flutter_launcher_icons: ^0.13.1 + flutter_lints: ^6.0.0 + flutter_launcher_icons: ^0.14.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 571addb..4c358e7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES) From a9fbf8c7f502fef9a229e9a0ae1084e7d1a2e2f9 Mon Sep 17 00:00:00 2001 From: MGJ <62177301+MGJ520@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:30:19 +0800 Subject: [PATCH 105/421] Correct Chinese translation --- lib/l10n/app_zh.arb | 976 ++++++++++++++++++++++---------------------- 1 file changed, 488 insertions(+), 488 deletions(-) diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 7b4b3ab..3e4b1d3 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,11 +1,11 @@ { "@@locale": "zh", "appTitle": "MeshCore Open", - "nav_contacts": "联系方式", + "nav_contacts": "联系人", "nav_channels": "频道", "nav_map": "地图", "common_cancel": "取消", - "common_ok": "好的", + "common_ok": "确定", "common_connect": "连接", "common_unknownDevice": "未知设备", "common_save": "保存", @@ -15,9 +15,9 @@ "common_add": "添加", "common_settings": "设置", "common_disconnect": "断开", - "common_connected": "连接", - "common_disconnected": "断开", - "common_create": "创造", + "common_connected": "已连接", + "common_disconnected": "已断开", + "common_create": "创建", "common_continue": "继续", "common_share": "分享", "common_copy": "复制", @@ -26,7 +26,7 @@ "common_remove": "移除", "common_enable": "启用", "common_disable": "禁用", - "common_reboot": "重新启动", + "common_reboot": "重启", "common_loading": "正在加载...", "common_notAvailable": "—", "common_voltageValue": "{volts} V", @@ -45,7 +45,7 @@ } } }, - "scanner_title": "MeshCore 开放", + "scanner_title": "连接设备", "scanner_scanning": "正在搜索设备...", "scanner_connecting": "正在连接...", "scanner_disconnecting": "断开连接...", @@ -59,8 +59,8 @@ } }, "scanner_searchingDevices": "正在搜索 MeshCore 设备...", - "scanner_tapToScan": "点击“扫描”功能,以查找 MeshCore 设备。", - "scanner_connectionFailed": "Connection failed: {error}", + "scanner_tapToScan": "点击“扫描”按钮以查找 MeshCore 设备。", + "scanner_connectionFailed": "连接失败:{error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -71,8 +71,8 @@ "scanner_stop": "停止", "scanner_scan": "扫描", "device_quickSwitch": "快速切换", - "device_meshcore": "网格核心", - "settings_title": "设置", + "device_meshcore": "MeshCore", + "settings_title": " ", "settings_deviceInfo": "设备信息", "settings_appSettings": "应用设置", "settings_appSettingsSubtitle": "通知、消息和地图偏好", @@ -80,43 +80,43 @@ "settings_nodeName": "节点名称", "settings_nodeNameNotSet": "未设置", "settings_nodeNameHint": "请输入节点名称", - "settings_nodeNameUpdated": "姓名已更新", - "settings_radioSettings": "收音机设置", + "settings_nodeNameUpdated": "节点名称已更新", + "settings_radioSettings": "无线电设置", "settings_radioSettingsSubtitle": "频率、功率、扩频因子", - "settings_radioSettingsUpdated": "收音机设置已更新", - "settings_location": "地点", + "settings_radioSettingsUpdated": "无线电设置已更新", + "settings_location": "位置", "settings_locationSubtitle": "GPS 坐标", "settings_locationUpdated": "位置和 GPS 设置已更新", - "settings_locationBothRequired": "请输入经度和纬度。", - "settings_locationInvalid": "无效的经度和纬度。", - "settings_locationGPSEnable": "开启 GPS 功能", - "settings_locationGPSEnableSubtitle": "使 GPS 能够自动更新位置。", - "settings_locationIntervalSec": "GPS 间隔时间(秒)", + "settings_locationBothRequired": "请输入经度和纬度", + "settings_locationInvalid": "无效的经度和纬度", + "settings_locationGPSEnable": "启用 GPS", + "settings_locationGPSEnableSubtitle": "启用 GPS 以自动更新位置。", + "settings_locationIntervalSec": "GPS 间隔(秒)", "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_latitude": "纬度", "settings_longitude": "经度", "settings_privacyMode": "隐私模式", "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", - "settings_privacyModeToggle": "切换隐私模式,以隐藏您的姓名和位置,从而在广告中保护您的个人信息。", + "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", "settings_privacyModeEnabled": "隐私模式已启用", "settings_privacyModeDisabled": "隐私模式已关闭", - "settings_actions": "行动", - "settings_sendAdvertisement": "发布广告", - "settings_sendAdvertisementSubtitle": "现已开始进行广播节目", - "settings_advertisementSent": "已发送广告", + "settings_actions": "操作", + "settings_sendAdvertisement": "发送广播", + "settings_sendAdvertisementSubtitle": "立即发送广播", + "settings_advertisementSent": "已发送广播", "settings_syncTime": "同步时间", "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", - "settings_timeSynchronized": "时间同步", + "settings_timeSynchronized": "时间已同步", "settings_refreshContacts": "刷新联系人", - "settings_refreshContactsSubtitle": "从设备中重新加载联系人列表", + "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", "settings_rebootDevice": "重启设备", - "settings_rebootDeviceSubtitle": "重新启动 MeshCore 设备", - "settings_rebootDeviceConfirm": "您确定要重启设备吗?这将导致您与设备断开连接。", + "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", + "settings_rebootDeviceConfirm": "确定要重启设备吗?这将断开与设备的连接。", "settings_debug": "调试", "settings_bleDebugLog": "BLE 调试日志", "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", - "settings_appDebugLog": "应用程序调试日志", - "settings_appDebugLogSubtitle": "应用程序调试消息", + "settings_appDebugLog": "应用调试日志", + "settings_appDebugLogSubtitle": "应用调试消息", "settings_about": "关于", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -128,29 +128,29 @@ }, "settings_aboutLegalese": "2026 MeshCore 开源项目", "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", - "settings_infoName": "姓名", - "settings_infoId": "ID", + "settings_infoName": "名称", + "settings_infoId": "MAC ID", "settings_infoStatus": "状态", "settings_infoBattery": "电池", "settings_infoPublicKey": "公钥", "settings_infoContactsCount": "联系人数量", - "settings_infoChannelCount": "通道数量", + "settings_infoChannelCount": "频道数量", "settings_presets": "预设", - "settings_preset915Mhz": "915 兆赫", - "settings_preset868Mhz": "868 兆赫", - "settings_preset433Mhz": "433 兆赫", + "settings_preset915Mhz": "915 MHz", + "settings_preset868Mhz": "868 MHz", + "settings_preset433Mhz": "433 MHz", "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "无效频率(300-2500 MHz)", + "settings_frequencyInvalid": "无效频率范围(300-2500 MHz)", "settings_bandwidth": "带宽", - "settings_spreadingFactor": "传播系数", + "settings_spreadingFactor": "扩频因子", "settings_codingRate": "编码速率", - "settings_txPower": "TX 功率(dBm)", + "settings_txPower": "TX 功率 (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", "settings_longRange": "远距离", "settings_fastSpeed": "高速", - "settings_error": "[保存:{message}]\n错误:{message}", + "settings_error": "错误:{message}", "@settings_error": { "placeholders": { "message": { @@ -161,49 +161,49 @@ "appSettings_title": "应用设置", "appSettings_appearance": "外观", "appSettings_theme": "主题", - "appSettings_themeSystem": "系统默认设置", - "appSettings_themeLight": "光", - "appSettings_themeDark": "黑暗", + "appSettings_themeSystem": "跟随系统", + "appSettings_themeLight": "浅色", + "appSettings_themeDark": "深色", "appSettings_language": "语言", - "appSettings_languageSystem": "系统默认设置", + "appSettings_languageSystem": "跟随系统", "appSettings_languageEn": "英语", "appSettings_languageFr": "法语", "appSettings_languageEs": "西班牙语", "appSettings_languageDe": "德语", "appSettings_languagePl": "波兰语", - "appSettings_languageSl": "斯洛文语", + "appSettings_languageSl": "斯洛文尼亚语", "appSettings_languagePt": "葡萄牙语", "appSettings_languageIt": "意大利语", "appSettings_languageZh": "中文", "appSettings_languageSv": "瑞典语", "appSettings_languageNl": "荷兰语", "appSettings_languageSk": "斯洛伐克语", - "appSettings_languageBg": "保加利亚", + "appSettings_languageBg": "保加利亚语", "appSettings_languageRu": "俄语", - "appSettings_languageUk": "乌克兰", + "appSettings_languageUk": "乌克兰语", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", - "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", + "appSettings_enableNotificationsSubtitle": "接收消息和广播的通知", "appSettings_notificationPermissionDenied": "权限被拒绝", "appSettings_notificationsEnabled": "通知已启用", "appSettings_notificationsDisabled": "通知已关闭", "appSettings_messageNotifications": "消息通知", - "appSettings_messageNotificationsSubtitle": "在收到新消息时显示通知", + "appSettings_messageNotificationsSubtitle": "收到新消息时显示通知", "appSettings_channelMessageNotifications": "频道消息通知", - "appSettings_channelMessageNotificationsSubtitle": "在收到频道消息时,显示通知。", - "appSettings_advertisementNotifications": "广告通知", - "appSettings_advertisementNotificationsSubtitle": "在发现新的节点时,显示通知。", - "appSettings_messaging": "信息传递", - "appSettings_clearPathOnMaxRetry": "关于“最大重试”的清晰说明", - "appSettings_clearPathOnMaxRetrySubtitle": "在尝试发送失败后 5 次,重置联系路径。", - "appSettings_pathsWillBeCleared": "如果尝试 5 次后仍然失败,则将重新规划路径。", - "appSettings_pathsWillNotBeCleared": "路径不会自动清除。", + "appSettings_channelMessageNotificationsSubtitle": "收到频道消息时显示通知", + "appSettings_advertisementNotifications": "广播通知", + "appSettings_advertisementNotificationsSubtitle": "发现新节点时显示通知", + "appSettings_messaging": "消息", + "appSettings_clearPathOnMaxRetry": "达到最大重试次数时清除路径", + "appSettings_clearPathOnMaxRetrySubtitle": "在5次发送失败后重置联系路径。", + "appSettings_pathsWillBeCleared": "5次失败后将重新路由", + "appSettings_pathsWillNotBeCleared": "路径不会自动清除", "appSettings_autoRouteRotation": "自动路径轮换", - "appSettings_autoRouteRotationSubtitle": "在最佳路径和防洪模式之间切换", + "appSettings_autoRouteRotationSubtitle": "在最佳路径和泛洪模式之间切换", "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", "appSettings_battery": "电池", - "appSettings_batteryChemistry": "电池化学", + "appSettings_batteryChemistry": "电池类型", "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { @@ -212,20 +212,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "连接到设备以进行选择", - "appSettings_batteryNmc": "18650 型号,NMC 电池(3.0-4.2V)", + "appSettings_batteryChemistryConnectFirst": "请先连接设备", + "appSettings_batteryNmc": "18650 NMC 电池 (3.0-4.2V)", "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", - "appSettings_batteryLipo": "锂离子电池 (3.0-4.2V)", - "appSettings_mapDisplay": "地图展示", - "appSettings_showRepeaters": "显示重复", - "appSettings_showRepeatersSubtitle": "在地图上显示重复节点", + "appSettings_batteryLipo": "锂聚合物电池 (3.0-4.2V)", + "appSettings_mapDisplay": "地图显示", + "appSettings_showRepeaters": "显示转发节点", + "appSettings_showRepeatersSubtitle": "在地图上显示转发节点", "appSettings_showChatNodes": "显示聊天节点", "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", "appSettings_showOtherNodes": "显示其他节点", "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", "appSettings_timeFilter": "时间过滤器", "appSettings_timeFilterShowAll": "显示所有节点", - "appSettings_timeFilterShowLast": "Show nodes from last {hours} hours", + "appSettings_timeFilterShowLast": "显示过去 {hours} 小时内的节点", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -234,7 +234,7 @@ } }, "appSettings_mapTimeFilter": "地图时间筛选", - "appSettings_showNodesDiscoveredWithin": "显示在以下范围内发现的节点:", + "appSettings_showNodesDiscoveredWithin": "显示在此时间段内发现的节点:", "appSettings_allTime": "所有时间", "appSettings_lastHour": "过去一小时", "appSettings_last6Hours": "过去6小时", @@ -242,7 +242,7 @@ "appSettings_lastWeek": "上周", "appSettings_offlineMapCache": "离线地图缓存", "appSettings_noAreaSelected": "未选择任何区域", - "appSettings_areaSelectedZoom": "已选择区域(缩放至 {minZoom} - {maxZoom})", + "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -254,18 +254,18 @@ } }, "appSettings_debugCard": "调试", - "appSettings_appDebugLogging": "应用程序调试日志", - "appSettings_appDebugLoggingSubtitle": "用于故障排除的日志应用程序调试消息", + "appSettings_appDebugLogging": "应用调试日志", + "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以进行故障排除。", "appSettings_appDebugLoggingEnabled": "调试日志已启用", - "appSettings_appDebugLoggingDisabled": "应用程序调试日志已禁用", - "contacts_title": "联系方式", - "contacts_noContacts": "目前还没有联系人", - "contacts_contactsWillAppear": "当设备发布广告时,联系方式会显示。", + "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", + "contacts_title": " ", + "contacts_noContacts": "暂无联系人", + "contacts_contactsWillAppear": "当设备发送广播时,联系人将显示。", "contacts_searchContacts": "搜索联系人...", - "contacts_noUnreadContacts": "没有未读通讯", - "contacts_noContactsFound": "未找到任何联系人或群组", + "contacts_noUnreadContacts": "没有未读内容", + "contacts_noContactsFound": "未找到任何联系人或群聊", "contacts_deleteContact": "删除联系人", - "contacts_removeConfirm": "Remove {contactName} from contacts?", + "contacts_removeConfirm": "从联系人中移除 {contactName}?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -273,13 +273,13 @@ } } }, - "contacts_manageRepeater": "管理重复器", + "contacts_manageRepeater": "管理转发节点", "contacts_manageRoom": "管理房间服务器", "contacts_roomLogin": "服务器登录", - "contacts_openChat": "开放聊天", - "contacts_editGroup": "编辑小组", - "contacts_deleteGroup": "删除群组", - "contacts_deleteGroupConfirm": "删除\"{groupName}\"?", + "contacts_openChat": "打开聊天", + "contacts_editGroup": "编辑群聊", + "contacts_deleteGroup": "删除群聊", + "contacts_deleteGroupConfirm": "删除群聊 \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -287,10 +287,10 @@ } } }, - "contacts_newGroup": "新的团体", - "contacts_groupName": "团体名称", - "contacts_groupNameRequired": "需要提供组名称", - "contacts_groupAlreadyExists": "名为\"{name}\"的组已经存在", + "contacts_newGroup": "新建群聊", + "contacts_groupName": "群聊名称", + "contacts_groupNameRequired": "请输入群聊名称", + "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -299,10 +299,10 @@ } }, "contacts_filterContacts": "筛选联系人...", - "contacts_noContactsMatchFilter": "未找到符合您筛选条件的联系人", - "contacts_noMembers": "没有会员", - "contacts_lastSeenNow": "最后一次被看到的时间", - "contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago", + "contacts_noContactsMatchFilter": "没有符合条件的联系人", + "contacts_noMembers": "暂无成员", + "contacts_lastSeenNow": "刚刚", + "contacts_lastSeenMinsAgo": "最后在线 {minutes} 分钟前", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -310,8 +310,8 @@ } } }, - "contacts_lastSeenHourAgo": "最后一次被看到的时间:1小时前", - "contacts_lastSeenHoursAgo": "Last seen {hours} hours ago", + "contacts_lastSeenHourAgo": "最后在线 1小时前", + "contacts_lastSeenHoursAgo": "最后在线 {hours} 小时前", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -319,8 +319,8 @@ } } }, - "contacts_lastSeenDayAgo": "最后一次被看到的时间是1天前", - "contacts_lastSeenDaysAgo": "Last seen {days} days ago", + "contacts_lastSeenDayAgo": "最后在线 1天前", + "contacts_lastSeenDaysAgo": "最后在线 {days} 天前", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -328,7 +328,7 @@ } } }, - "channels_title": "频道", + "channels_title": " ", "channels_noChannelsConfigured": "未配置任何频道", "channels_addPublicChannel": "添加公共频道", "channels_searchChannels": "搜索频道...", @@ -341,14 +341,14 @@ } } }, - "channels_hashtagChannel": "话题标签频道", - "channels_public": "公众", - "channels_private": "私人", + "channels_hashtagChannel": "标签频道", + "channels_public": "公共", + "channels_private": "私有", "channels_publicChannel": "公共频道", - "channels_privateChannel": "私密频道", + "channels_privateChannel": "私有频道", "channels_editChannel": "编辑频道", "channels_deleteChannel": "删除频道", - "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", + "channels_deleteChannelConfirm": "删除频道 \"{name}\"?此操作不可撤销。", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -356,7 +356,7 @@ } } }, - "channels_channelDeleted": "删除频道 \"{name}\"", + "channels_channelDeleted": "已删除频道 \"{name}\"", "@channels_channelDeleted": { "placeholders": { "name": { @@ -368,12 +368,12 @@ "channels_channelIndexLabel": "频道索引", "channels_channelName": "频道名称", "channels_usePublicChannel": "使用公共频道", - "channels_standardPublicPsk": "标准公共PSK", + "channels_standardPublicPsk": "标准公共 PSK", "channels_pskHex": "PSK (十六进制)", - "channels_generateRandomPsk": "生成随机的PSK(正交相移键控)", - "channels_enterChannelName": "请在此处输入频道名称", - "channels_pskMustBe32Hex": "PSK 必须包含 32 个十六进制字符。", - "channels_channelAdded": "添加频道 \"{name}\"", + "channels_generateRandomPsk": "生成随机 PSK", + "channels_enterChannelName": "请输入频道名称", + "channels_pskMustBe32Hex": "PSK 必须为 32 个十六进制字符", + "channels_channelAdded": "已添加频道 \"{name}\"", "@channels_channelAdded": { "placeholders": { "name": { @@ -399,27 +399,27 @@ } }, "channels_publicChannelAdded": "已添加公共频道", - "channels_sortBy": "按排序", - "channels_sortManual": "手册", - "channels_sortAZ": "A 到 Z", + "channels_sortBy": "排序方式", + "channels_sortManual": "手动", + "channels_sortAZ": "A-Z", "channels_sortLatestMessages": "最新消息", "channels_sortUnread": "未读", - "channels_createPrivateChannel": "创建私密频道", - "channels_createPrivateChannelDesc": "使用秘密密钥进行保护。", - "channels_joinPrivateChannel": "加入私密频道", + "channels_createPrivateChannel": "创建私有频道", + "channels_createPrivateChannelDesc": "使用密钥保护。", + "channels_joinPrivateChannel": "加入私有频道", "channels_joinPrivateChannelDesc": "手动输入密钥。", "channels_joinPublicChannel": "加入公共频道", - "channels_joinPublicChannelDesc": "任何人都可以加入这个频道。", - "channels_joinHashtagChannel": "加入一个带有特定标签的频道", - "channels_joinHashtagChannelDesc": "任何人都可以加入带有特定标签的频道。", + "channels_joinPublicChannelDesc": "任何人都可以加入。", + "channels_joinHashtagChannel": "加入标签频道", + "channels_joinHashtagChannelDesc": "任何人都可以加入标签频道。", "channels_scanQrCode": "扫描二维码", - "channels_scanQrCodeComingSoon": "即将发布", + "channels_scanQrCodeComingSoon": "即将推出", "channels_enterHashtag": "输入标签", "channels_hashtagHint": "例如:#团队", - "chat_noMessages": "目前还没有收到任何消息。", - "chat_sendMessageToStart": "发送消息以开始", - "chat_originalMessageNotFound": "无法找到原始消息", - "chat_replyingTo": "Replying to {name}", + "chat_noMessages": "暂无消息", + "chat_sendMessageToStart": "发送消息开始对话", + "chat_originalMessageNotFound": "找不到原始消息", + "chat_replyingTo": "正在回复 {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -427,7 +427,7 @@ } } }, - "chat_replyTo": "Reply to {name}", + "chat_replyTo": "回复 {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -435,8 +435,8 @@ } } }, - "chat_location": "地点", - "chat_sendMessageTo": "Send a message to {contactName}", + "chat_location": "位置", + "chat_sendMessageTo": "发送消息给 {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -445,7 +445,7 @@ } }, "chat_typeMessage": "输入消息...", - "chat_messageTooLong": "消息内容过长(最大 {maxBytes} 字节)。", + "chat_messageTooLong": "消息过长(最多 {maxBytes} 字节)", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -455,8 +455,8 @@ }, "chat_messageCopied": "消息已复制", "chat_messageDeleted": "消息已删除", - "chat_retryingMessage": "重试消息", - "chat_retryCount": "Retry {current}/{max}", + "chat_retryingMessage": "正在重试消息", + "chat_retryCount": "重试 {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -467,32 +467,32 @@ } } }, - "chat_sendGif": "发送 GIF 动画", + "chat_sendGif": "发送 GIF", "chat_reply": "回复", - "chat_addReaction": "添加评论", + "chat_addReaction": "添加表情", "chat_me": "我", - "emojiCategorySmileys": "表情符号", + "emojiCategorySmileys": "表情", "emojiCategoryGestures": "手势", - "emojiCategoryHearts": "心脏", - "emojiCategoryObjects": "物体", - "gifPicker_title": "选择一个 GIF 动画", - "gifPicker_searchHint": "搜索 GIF 动画...", - "gifPicker_poweredBy": "由 GIPHY 提供支持", - "gifPicker_noGifsFound": "未找到 GIF 动画", - "gifPicker_failedLoad": "无法加载 GIF 动画", - "gifPicker_failedSearch": "未能搜索 GIF 动画", - "gifPicker_noInternet": "没有互联网连接", - "debugLog_appTitle": "应用程序调试日志", + "emojiCategoryHearts": "爱心", + "emojiCategoryObjects": "物品", + "gifPicker_title": "选择 GIF", + "gifPicker_searchHint": "搜索 GIF...", + "gifPicker_poweredBy": "由 GIPHY 提供", + "gifPicker_noGifsFound": "未找到 GIF", + "gifPicker_failedLoad": "加载 GIF 失败", + "gifPicker_failedSearch": "搜索 GIF 失败", + "gifPicker_noInternet": "无网络连接", + "debugLog_appTitle": "应用调试日志", "debugLog_bleTitle": "BLE 调试日志", "debugLog_copyLog": "复制日志", - "debugLog_clearLog": "清晰的日志", + "debugLog_clearLog": "清除日志", "debugLog_copied": "调试日志已复制", "debugLog_bleCopied": "BLE 日志已复制", - "debugLog_noEntries": "目前还没有调试日志", - "debugLog_enableInSettings": "在设置中启用应用程序调试日志功能。", - "debugLog_frames": "框架", - "debugLog_rawLogRx": "原始日志-RX", - "debugLog_noBleActivity": "目前尚未有蓝牙低功耗(BLE)活动。", + "debugLog_noEntries": "暂无调试日志", + "debugLog_enableInSettings": "请在设置中启用应用调试日志。", + "debugLog_frames": "帧", + "debugLog_rawLogRx": "原始日志 RX", + "debugLog_noBleActivity": "暂无 BLE 活动", "debugFrame_length": "帧长度:{count} 字节", "@debugFrame_length": { "placeholders": { @@ -509,7 +509,7 @@ } } }, - "debugFrame_textMessageHeader": "短信模板:", + "debugFrame_textMessageHeader": "文本消息:", "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { @@ -518,7 +518,7 @@ } } }, - "debugFrame_timestamp": "- Timestamp: {timestamp}", + "debugFrame_timestamp": "- 时间戳:{timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -534,7 +534,7 @@ } } }, - "debugFrame_textType": "- Text Type: {type} ({label})", + "debugFrame_textType": "- 文本类型:{type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -545,8 +545,8 @@ } } }, - "debugFrame_textTypeCli": "命令行界面", - "debugFrame_textTypePlain": "简单", + "debugFrame_textTypeCli": "命令行", + "debugFrame_textTypePlain": "纯文本", "debugFrame_text": "- 文本:“{text}”", "@debugFrame_text": { "placeholders": { @@ -558,13 +558,13 @@ "debugFrame_hexDump": "十六进制数据:", "chat_pathManagement": "路径管理", "chat_routingMode": "路由模式", - "chat_autoUseSavedPath": "自动(使用已保存的路径)", - "chat_forceFloodMode": "强制洪水模式", + "chat_autoUseSavedPath": "自动(使用保存的路径)", + "chat_forceFloodMode": "强制泛洪模式", "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", - "chat_pathHistoryFull": "路径历史已满。删除条目以添加新的条目。", - "chat_hopSingular": "跳跃", - "chat_hopPlural": "啤酒花", - "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", + "chat_pathHistoryFull": "路径历史已满,请删除后再添加。", + "chat_hopSingular": "跳", + "chat_hopPlural": "跳", + "chat_hopsCount": "{count} 跳", "@chat_hopsCount": { "placeholders": { "count": { @@ -573,19 +573,19 @@ } }, "chat_successes": "成功", - "chat_removePath": "删除路径", - "chat_noPathHistoryYet": "目前还没有历史记录。\n发送消息以查找路径。", + "chat_removePath": "移除路径", + "chat_noPathHistoryYet": "暂无路径历史。\n发送消息以探索路径。", "chat_pathActions": "路径操作:", "chat_setCustomPath": "设置自定义路径", "chat_setCustomPathSubtitle": "手动指定路由路径", - "chat_clearPath": "明确的道路", - "chat_clearPathSubtitle": "在下一次发送时,重新尝试。", - "chat_pathCleared": "路径已清理。下一条消息将重新确定路线。", - "chat_floodModeSubtitle": "使用应用程序栏中的路由切换功能", - "chat_floodModeEnabled": "防洪模式已启用。通过应用程序栏中的路由图标进行切换。", + "chat_clearPath": "清除路径", + "chat_clearPathSubtitle": "清除当前路径,下次发送将重新尝试。", + "chat_pathCleared": "路径已清除。下一条消息将重新路由。", + "chat_floodModeSubtitle": "在应用栏中切换路由模式。", + "chat_floodModeEnabled": "泛洪模式已启用。可通过应用栏的路由图标切换。", "chat_fullPath": "完整路径", - "chat_pathDetailsNotAvailable": "路径信息尚未提供。请尝试发送消息以刷新。", - "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathDetailsNotAvailable": "路径信息暂不可用,请尝试发送消息刷新。", + "chat_pathSetHops": "路径设置:{hopCount} 跳 - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -596,16 +596,16 @@ } } }, - "chat_pathSavedLocally": "已本地保存。连接以进行同步。", + "chat_pathSavedLocally": "已本地保存,连接设备后可同步。", "chat_pathDeviceConfirmed": "设备已确认。", - "chat_pathDeviceNotConfirmed": "该设备尚未得到确认。", + "chat_pathDeviceNotConfirmed": "设备尚未确认。", "chat_type": "类型", "chat_path": "路径", "chat_publicKey": "公钥", "chat_compressOutgoingMessages": "压缩发送的消息", - "chat_floodForced": "洪水(被迫)", - "chat_directForced": "直接(强制性的)", - "chat_hopsForced": "{count} 根啤酒花(人工种植)", + "chat_floodForced": "泛洪(强制)", + "chat_directForced": "直连(强制)", + "chat_hopsForced": "{count} 跳(强制)", "@chat_hopsForced": { "placeholders": { "count": { @@ -613,10 +613,10 @@ } } }, - "chat_floodAuto": "自动洪水", - "chat_direct": "直接", + "chat_floodAuto": "自动泛洪", + "chat_direct": "直连", "chat_poiShared": "共享位置", - "chat_unread": "Unread: {count}", + "chat_unread": "未读:{count}", "@chat_unread": { "placeholders": { "count": { @@ -625,9 +625,9 @@ } }, "chat_openLink": "打开链接?", - "chat_openLinkConfirmation": "您想用浏览器打开这个链接吗?", - "chat_open": "开放", - "chat_couldNotOpenLink": "[保存:{url}]\n无法打开链接:{url}", + "chat_openLinkConfirmation": "是否使用浏览器打开此链接?", + "chat_open": "打开", + "chat_couldNotOpenLink": "无法打开链接:{url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -636,10 +636,10 @@ } }, "chat_invalidLink": "无效的链接格式", - "map_title": "节点图", + "map_title": " ", "map_noNodesWithLocation": "没有包含位置信息的节点", - "map_nodesNeedGps": "节点需要共享其 GPS 坐标,以便在地图上显示", - "map_nodesCount": "Nodes: {count}", + "map_nodesNeedGps": "节点需要共享 GPS 坐标才能在地图上显示", + "map_nodesCount": "节点:{count}", "@map_nodesCount": { "placeholders": { "count": { @@ -647,7 +647,7 @@ } } }, - "map_pinsCount": "Pins: {count}", + "map_pinsCount": "标记:{count}", "@map_pinsCount": { "placeholders": { "count": { @@ -656,26 +656,26 @@ } }, "map_chat": "聊天", - "map_repeater": "重复器", + "map_repeater": "转发节点", "map_room": "房间", "map_sensor": "传感器", - "map_pinDm": "PIN (直接消息)", - "map_pinPrivate": "私密", - "map_pinPublic": "公开", - "map_lastSeen": "最后一次被看到", - "map_disconnectConfirm": "您确定要断开与此设备的连接吗?", - "map_from": "从", + "map_pinDm": "标记(私信)", + "map_pinPrivate": "私有", + "map_pinPublic": "公共", + "map_lastSeen": "最后在线", + "map_disconnectConfirm": "确定要断开与此设备的连接吗?", + "map_from": "来自", "map_source": "来源", - "map_flags": "旗帜", + "map_flags": "标志", "map_shareMarkerHere": "在此分享标记", "map_pinLabel": "标签", "map_label": "标签", - "map_pointOfInterest": "值得参观的地方", - "map_sendToContact": "发送给联系", + "map_pointOfInterest": "兴趣点", + "map_sendToContact": "发送给联系人", "map_sendToChannel": "发送到频道", "map_noChannelsAvailable": "没有可用的频道", - "map_publicLocationShare": "公共场所共享", - "map_publicLocationShareConfirm": "[保存:{channelLabel}]\n您即将分享一个位置,该位置位于 {channelLabel}。 此频道是公开的,任何拥有 PSK 的人都可以看到它。", + "map_publicLocationShare": "公共位置共享", + "map_publicLocationShareConfirm": "您即将在 {channelLabel} 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -687,22 +687,22 @@ "map_filterNodes": "过滤节点", "map_nodeTypes": "节点类型", "map_chatNodes": "聊天节点", - "map_repeaters": "重复器", + "map_repeaters": "转发节点", "map_otherNodes": "其他节点", - "map_keyPrefix": "关键前缀", - "map_filterByKeyPrefix": "按关键前缀筛选", - "map_publicKeyPrefix": "公钥前缀", + "map_keyPrefix": "关键字前缀", + "map_filterByKeyPrefix": "按关键字前缀筛选", + "map_publicKeyPrefix": "关键字前缀", "map_markers": "标记", "map_showSharedMarkers": "显示共享标记", - "map_lastSeenTime": "最后一次被看到的时间", - "map_sharedPin": "共享密码", + "map_lastSeenTime": "最后在线时间", + "map_sharedPin": "共享标记", "map_joinRoom": "加入房间", - "map_manageRepeater": "管理重复器", + "map_manageRepeater": "管理转发节点", "mapCache_title": "离线地图缓存", - "mapCache_selectAreaFirst": "选择一个用于缓存的区域", - "mapCache_noTilesToDownload": "此区域没有可下载的瓦片。", - "mapCache_downloadTilesTitle": "下载瓷砖", - "mapCache_downloadTilesPrompt": "[保存:{count}]\n下载 {count} 个图片用于离线使用?", + "mapCache_selectAreaFirst": "请先选择要缓存的区域", + "mapCache_noTilesToDownload": "此区域没有可下载的瓦片", + "mapCache_downloadTilesTitle": "下载瓦片", + "mapCache_downloadTilesPrompt": "这需要下载 {count} 个瓦片", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -711,7 +711,7 @@ } }, "mapCache_downloadAction": "下载", - "mapCache_cachedTiles": "缓存 {count} 个瓦片", + "mapCache_cachedTiles": "已缓存 {count} 个瓦片", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -719,7 +719,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", + "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片({failed} 个失败)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -733,11 +733,11 @@ "mapCache_clearOfflineCacheTitle": "清除离线缓存", "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", "mapCache_offlineCacheCleared": "离线缓存已清除", - "mapCache_noAreaSelected": "未选择任何区域", + "mapCache_noAreaSelected": "未选择区域", "mapCache_cacheArea": "缓存区域", "mapCache_useCurrentView": "使用当前视图", - "mapCache_zoomRange": "变焦范围", - "mapCache_estimatedTiles": "Estimated tiles: {count}", + "mapCache_zoomRange": "缩放范围", + "mapCache_estimatedTiles": "估计瓦片数:{count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -745,7 +745,7 @@ } } }, - "mapCache_downloadedTiles": "Downloaded {completed} / {total}", + "mapCache_downloadedTiles": "已下载 {completed}/{total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -756,9 +756,9 @@ } } }, - "mapCache_downloadTilesButton": "下载瓷砖", + "mapCache_downloadTilesButton": "下载瓦片", "mapCache_clearCacheButton": "清除缓存", - "mapCache_failedDownloads": "Failed downloads: {count}", + "mapCache_failedDownloads": "下载失败:{count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -766,7 +766,7 @@ } } }, - "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", + "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -784,7 +784,7 @@ } }, "time_justNow": "刚才", - "time_minutesAgo": "{minutes}m ago", + "time_minutesAgo": "{minutes}分钟前", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -792,7 +792,7 @@ } } }, - "time_hoursAgo": "{hours}h ago", + "time_hoursAgo": "{hours}小时前", "@time_hoursAgo": { "placeholders": { "hours": { @@ -810,31 +810,31 @@ }, "time_hour": "小时", "time_hours": "小时", - "time_day": "一天", + "time_day": "天", "time_days": "天", - "time_week": "一周", + "time_week": "周", "time_weeks": "周", - "time_month": "月份", - "time_months": "月份", + "time_month": "月", + "time_months": "月", "time_minutes": "分钟", "time_allTime": "所有时间", "dialog_disconnect": "断开", - "dialog_disconnectConfirm": "您确定要断开与此设备的连接吗?", - "login_repeaterLogin": "重复登录", - "login_roomLogin": "服务器登录", + "dialog_disconnectConfirm": "确定要断开与此设备的连接吗?", + "login_repeaterLogin": "转发节点登录", + "login_roomLogin": "房间服务器登录", "login_password": "密码", "login_enterPassword": "请输入密码", "login_savePassword": "保存密码", - "login_savePasswordSubtitle": "密码将安全地存储在 данном设备上", - "login_repeaterDescription": "输入重复器密码,即可访问设置和状态。", - "login_roomDescription": "输入密码进入房间,即可访问设置和状态。", + "login_savePasswordSubtitle": "密码将安全地存储在此设备上", + "login_repeaterDescription": "输入转发节点密码以访问设置和状态。", + "login_roomDescription": "输入房间服务器密码以访问设置和状态。", "login_routing": "路由", "login_routingMode": "路由模式", - "login_autoUseSavedPath": "自动(使用已保存的路径)", - "login_forceFloodMode": "强制洪水模式", + "login_autoUseSavedPath": "自动(使用保存的路径)", + "login_forceFloodMode": "强制泛洪模式", "login_managePaths": "管理路径", "login_login": "登录", - "login_attempt": "Attempt {current}/{max}", + "login_attempt": "尝试 {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -845,7 +845,7 @@ } } }, - "login_failed": "Login failed: {error}", + "login_failed": "登录失败:{error}", "@login_failed": { "placeholders": { "error": { @@ -853,10 +853,10 @@ } } }, - "login_failedMessage": "登录失败。可能是密码错误,也可能是无法连接到服务器。", + "login_failedMessage": "登录失败。可能是密码错误或无法连接到服务器。", "common_reload": "重新加载", - "common_clear": "清晰", - "path_currentPath": "Current path: {path}", + "common_clear": "清除", + "path_currentPath": "当前路径:{path}", "@path_currentPath": { "placeholders": { "path": { @@ -864,7 +864,7 @@ } } }, - "path_usingHopsPath": "使用 {count} {count, plural, =1{hop} other{hops}} 条路径", + "path_usingHopsPath": "使用 {count} 跳路径", "@path_usingHopsPath": { "placeholders": { "count": { @@ -874,14 +874,14 @@ }, "path_enterCustomPath": "输入自定义路径", "path_currentPathLabel": "当前路径", - "path_hexPrefixInstructions": "请输入每个跳跃步骤的 2 个字符的十六进制前缀,用逗号分隔。", - "path_hexPrefixExample": "例如:A1, F2, 3C (每个节点使用其公钥的第一字节)", + "path_hexPrefixInstructions": "请输入每个中继节点的2字符十六进制前缀,用逗号分隔。", + "path_hexPrefixExample": "例如:A1, F2, 3C(每个节点使用其公钥的第一字节)", "path_labelHexPrefixes": "路径(十六进制前缀)", - "path_helperMaxHops": "最大 64 个“hop”(跳跃)。每个前缀由 2 个十六进制字符(1 字节)组成。", - "path_selectFromContacts": "或者从联系人列表中选择:", - "path_noRepeatersFound": "未找到任何重复设备或房间服务器。", - "path_customPathsRequire": "自定义路径需要中间节点,这些节点可以转发消息。", - "path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}", + "path_helperMaxHops": "最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。", + "path_selectFromContacts": "或从联系人列表中选择:", + "path_noRepeatersFound": "未找到任何转发节点或房间服务器。", + "path_customPathsRequire": "自定义路径需要中间节点转发消息。", + "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -889,29 +889,29 @@ } } }, - "path_tooLong": "路径太长。允许的最大跳跃次数为 64 次。", + "path_tooLong": "路径过长,最多允许 64 跳。", "path_setPath": "设置路径", - "repeater_management": "重复器管理", - "room_management": "服务器管理", + "repeater_management": "转发节点管理", + "room_management": "房间服务器管理", "repeater_managementTools": "管理工具", "repeater_status": "状态", - "repeater_statusSubtitle": "查看重复器状态、统计信息和邻居", - "repeater_telemetry": "远程监控", - "repeater_telemetrySubtitle": "查看传感器和系统状态的数据。", - "repeater_cli": "命令行界面", - "repeater_cliSubtitle": "向复用器发送指令", + "repeater_statusSubtitle": "查看转发节点状态、统计和邻居", + "repeater_telemetry": "遥测", + "repeater_telemetrySubtitle": "查看传感器和系统状态数据", + "repeater_cli": "命令行", + "repeater_cliSubtitle": "向转发节点发送命令", "repeater_neighbours": "邻居", - "repeater_neighboursSubtitle": "查看邻居节点(无需中间节点)。", + "repeater_neighboursSubtitle": "查看邻居节点(零跳)", "repeater_settings": "设置", - "repeater_settingsSubtitle": "配置重复器参数", - "repeater_statusTitle": "重复器状态", + "repeater_settingsSubtitle": "配置转发节点参数", + "repeater_statusTitle": "转发节点状态", "repeater_routingMode": "路由模式", - "repeater_autoUseSavedPath": "自动(使用已保存的路径)", - "repeater_forceFloodMode": "强制洪水模式", + "repeater_autoUseSavedPath": "自动(使用保存的路径)", + "repeater_forceFloodMode": "强制泛洪模式", "repeater_pathManagement": "路径管理", - "repeater_refresh": "更新", - "repeater_statusRequestTimeout": "状态请求超时。", - "repeater_errorLoadingStatus": "Error loading status: {error}", + "repeater_refresh": "刷新", + "repeater_statusRequestTimeout": "状态请求超时", + "repeater_errorLoadingStatus": "加载状态时出错:{error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -921,19 +921,19 @@ }, "repeater_systemInformation": "系统信息", "repeater_battery": "电池", - "repeater_clockAtLogin": "登录时的时间", - "repeater_uptime": "正常运行时间", - "repeater_queueLength": "排队长度", + "repeater_clockAtLogin": "登录时的时钟", + "repeater_uptime": "运行时间", + "repeater_queueLength": "队列长度", "repeater_debugFlags": "调试标志", - "repeater_radioStatistics": "广播统计", - "repeater_lastRssi": "上次的 RSSI 值", - "repeater_lastSnr": "最后一次信噪比", - "repeater_noiseFloor": "噪声水平", - "repeater_txAirtime": "TX 频道预留时间", - "repeater_rxAirtime": "RX 空时", + "repeater_radioStatistics": "无线电统计", + "repeater_lastRssi": "上次 RSSI", + "repeater_lastSnr": "上次 SNR", + "repeater_noiseFloor": "底噪", + "repeater_txAirtime": "发送空中时间", + "repeater_rxAirtime": "接收空中时间", "repeater_packetStatistics": "数据包统计", "repeater_sent": "发送", - "repeater_received": "已收到", + "repeater_received": "接收", "repeater_duplicates": "重复", "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}秒", "@repeater_daysHoursMinsSecs": { @@ -952,7 +952,7 @@ } } }, - "repeater_packetTxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", + "repeater_packetTxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -966,7 +966,7 @@ } } }, - "repeater_packetRxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", + "repeater_packetRxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -980,7 +980,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}", + "repeater_duplicatesFloodDirect": "泛洪:{flood},直连:{direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -991,7 +991,7 @@ } } }, - "repeater_duplicatesTotal": "Total: {total}", + "repeater_duplicatesTotal": "总计:{total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -999,37 +999,37 @@ } } }, - "repeater_settingsTitle": "重复器设置", + "repeater_settingsTitle": "转发节点设置", "repeater_basicSettings": "基本设置", - "repeater_repeaterName": "重复器名称", - "repeater_repeaterNameHelper": "此复播器的显示名称", + "repeater_repeaterName": "转发节点名称", + "repeater_repeaterNameHelper": "此转发节点的显示名称", "repeater_adminPassword": "管理员密码", "repeater_adminPasswordHelper": "完整访问密码", "repeater_guestPassword": "访客密码", "repeater_guestPasswordHelper": "只读访问密码", - "repeater_radioSettings": "收音机设置", + "repeater_radioSettings": "无线电设置", "repeater_frequencyMhz": "频率 (MHz)", - "repeater_frequencyHelper": "300-2500 兆赫", + "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX 功率", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "带宽", - "repeater_spreadingFactor": "传播系数", + "repeater_spreadingFactor": "扩频因子", "repeater_codingRate": "编码速率", "repeater_locationSettings": "位置设置", "repeater_latitude": "纬度", - "repeater_latitudeHelper": "十进制度(例如:37.7749)", + "repeater_latitudeHelper": "十进制,例如 37.7749", "repeater_longitude": "经度", - "repeater_longitudeHelper": "十进制度(例如:-122.4194)", - "repeater_features": "特点", + "repeater_longitudeHelper": "十进制,例如 -122.4194", + "repeater_features": "功能", "repeater_packetForwarding": "数据包转发", - "repeater_packetForwardingSubtitle": "启用重复器,使其能够转发数据包", + "repeater_packetForwardingSubtitle": "启用转发节点转发数据包", "repeater_guestAccess": "访客访问", - "repeater_guestAccessSubtitle": "允许访客仅限读取权限", + "repeater_guestAccessSubtitle": "允许访客只读权限", "repeater_privacyMode": "隐私模式", - "repeater_privacyModeSubtitle": "在广告中隐藏姓名/位置", - "repeater_advertisementSettings": "广告设置", - "repeater_localAdvertInterval": "本地广告投放时间段", - "repeater_localAdvertIntervalMinutes": "{minutes} minutes", + "repeater_privacyModeSubtitle": "在广播中隐藏姓名/位置", + "repeater_advertisementSettings": "广播设置", + "repeater_localAdvertInterval": "本地广播间隔", + "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1037,8 +1037,8 @@ } } }, - "repeater_floodAdvertInterval": "洪水广告播放间隔", - "repeater_floodAdvertIntervalHours": "{hours} hours", + "repeater_floodAdvertInterval": "泛洪广播间隔", + "repeater_floodAdvertIntervalHours": "{hours} 小时", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1046,19 +1046,19 @@ } } }, - "repeater_encryptedAdvertInterval": "加密的广告投放时间段", - "repeater_dangerZone": "危险区域", - "repeater_rebootRepeater": "重启重复器", - "repeater_rebootRepeaterSubtitle": "重新启动重复器设备", - "repeater_rebootRepeaterConfirm": "您确定要重新启动这个中继器吗?", + "repeater_encryptedAdvertInterval": "加密广播间隔", + "repeater_dangerZone": "危险设置", + "repeater_rebootRepeater": "重启转发节点", + "repeater_rebootRepeaterSubtitle": "重启转发节点设备", + "repeater_rebootRepeaterConfirm": "确定要重启此转发节点吗?", "repeater_regenerateIdentityKey": "重新生成身份密钥", "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", - "repeater_regenerateIdentityKeyConfirm": "这将为复用器生成一个新的身份。继续吗?", - "repeater_eraseFileSystem": "删除文件系统", - "repeater_eraseFileSystemSubtitle": "格式化重复文件系统", - "repeater_eraseFileSystemConfirm": "警告:此操作将清除复用器上的所有数据。 无法恢复!", - "repeater_eraseSerialOnly": "“Erase”功能仅可通过串行控制台使用。", - "repeater_commandSent": "Command sent: {command}", + "repeater_regenerateIdentityKeyConfirm": "这将为转发节点生成新身份,继续吗?", + "repeater_eraseFileSystem": "擦除文件系统", + "repeater_eraseFileSystemSubtitle": "格式化转发节点文件系统", + "repeater_eraseFileSystemConfirm": "警告:此操作将清除转发节点上的所有数据,且无法恢复!", + "repeater_eraseSerialOnly": "擦除功能仅可通过串行控制台使用。", + "repeater_commandSent": "命令已发送:{command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1066,7 +1066,7 @@ } } }, - "repeater_errorSendingCommand": "Error sending command: {error}", + "repeater_errorSendingCommand": "发送命令时出错:{error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1075,8 +1075,8 @@ } }, "repeater_confirm": "确认", - "repeater_settingsSaved": "设置已成功保存", - "repeater_errorSavingSettings": "Error saving settings: {error}", + "repeater_settingsSaved": "设置保存成功", + "repeater_errorSavingSettings": "保存设置时出错:{error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1084,15 +1084,15 @@ } } }, - "repeater_refreshBasicSettings": "重置基本设置", - "repeater_refreshRadioSettings": "重置收音机设置", - "repeater_refreshTxPower": "重置 TX 电源", - "repeater_refreshLocationSettings": "重置位置设置", + "repeater_refreshBasicSettings": "刷新基本设置", + "repeater_refreshRadioSettings": "刷新无线电设置", + "repeater_refreshTxPower": "刷新 TX 功率", + "repeater_refreshLocationSettings": "刷新位置设置", "repeater_refreshPacketForwarding": "刷新包转发", - "repeater_refreshGuestAccess": "重新获取访客访问权限", - "repeater_refreshPrivacyMode": "重置隐私模式", - "repeater_refreshAdvertisementSettings": "重置广告设置", - "repeater_refreshed": "{label} refreshed", + "repeater_refreshGuestAccess": "刷新访客权限", + "repeater_refreshPrivacyMode": "刷新隐私模式", + "repeater_refreshAdvertisementSettings": "刷新广播设置", + "repeater_refreshed": "{label} 已刷新", "@repeater_refreshed": { "placeholders": { "label": { @@ -1100,7 +1100,7 @@ } } }, - "repeater_errorRefreshing": "[保存:{label}]\n刷新 {label} 时出错", + "repeater_errorRefreshing": "刷新 {label} 时出错", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1108,18 +1108,18 @@ } } }, - "repeater_cliTitle": "重复器命令行界面", + "repeater_cliTitle": "转发节点命令行", "repeater_debugNextCommand": "调试下一条命令", "repeater_commandHelp": "帮助", - "repeater_clearHistory": "清晰的历史", - "repeater_noCommandsSent": "尚未发送任何指令", - "repeater_typeCommandOrUseQuick": "在下方输入命令,或使用快捷命令。", + "repeater_clearHistory": "清除历史", + "repeater_noCommandsSent": "尚未发送命令", + "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", "repeater_enterCommandHint": "输入命令...", - "repeater_previousCommand": "之前的命令", - "repeater_nextCommand": "下一个指令", - "repeater_enterCommandFirst": "首先输入一个命令", - "repeater_cliCommandFrameTitle": "CLI 命令框架", - "repeater_cliCommandError": "Error: {error}", + "repeater_previousCommand": "上一条命令", + "repeater_nextCommand": "下一条命令", + "repeater_enterCommandFirst": "请先输入命令", + "repeater_cliCommandFrameTitle": "CLI 命令帧", + "repeater_cliCommandError": "错误:{error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1127,81 +1127,81 @@ } } }, - "repeater_cliQuickGetName": "获取姓名", - "repeater_cliQuickGetRadio": "收听广播", + "repeater_cliQuickGetName": "获取名称", + "repeater_cliQuickGetRadio": "获取无线电设置", "repeater_cliQuickGetTx": "获取 TX", "repeater_cliQuickNeighbors": "邻居", "repeater_cliQuickVersion": "版本", - "repeater_cliQuickAdvertise": "发布广告", + "repeater_cliQuickAdvertise": "发送广播", "repeater_cliQuickClock": "时钟", - "repeater_cliHelpAdvert": "发送广告资料包", - "repeater_cliHelpReboot": "重置设备。 (请注意,您可能会收到“超时”错误,这是正常的现象)", - "repeater_cliHelpClock": "显示每个设备的当前时间。", - "repeater_cliHelpPassword": "为设备设置新的管理员密码。", - "repeater_cliHelpVersion": "显示设备版本和固件构建日期。", - "repeater_cliHelpClearStats": "重置各种统计指标,将其设置为零。", - "repeater_cliHelpSetAf": "设置时间因素。", - "repeater_cliHelpSetTx": "设置 LoRa 传输功率,单位为 dBm (相对于参考值)。 (重启以应用更改)", - "repeater_cliHelpSetRepeat": "启用或禁用此节点的重复器功能。", - "repeater_cliHelpSetAllowReadOnly": "(房间服务器)如果设置为“开启”,则允许使用空密码登录,但无法向房间发送消息(只能进行读取)。", - "repeater_cliHelpSetFloodMax": "设置最大传入数据包的跳数(如果大于或等于最大值,则不进行转发)。", - "repeater_cliHelpSetIntThresh": "设置干扰阈值(以dB为单位)。默认值为14。将设置为0以禁用频道干扰检测。", - "repeater_cliHelpSetAgcResetInterval": "设置间隔时间,用于重置自动增益控制器。设置为 0 以禁用。", - "repeater_cliHelpSetMultiAcks": "启用或禁用“双重确认”功能。", - "repeater_cliHelpSetAdvertInterval": "设置定时器间隔,单位为分钟,用于发送本地(无中继)的广告数据包。 将设置为 0 以禁用。", - "repeater_cliHelpSetFloodAdvertInterval": "设置定时器间隔时间为小时,以便发送广告信息包。将设置为 0 以禁用。", - "repeater_cliHelpSetGuestPassword": "设置/更新访客密码。 (对于访客,登录请求可以发送“获取统计”请求)", - "repeater_cliHelpSetName": "设置广告名称。", - "repeater_cliHelpSetLat": "设置广告地图的纬度。(以十进制表示)", - "repeater_cliHelpSetLon": "设置广告地图的经度。 (十进制度)", - "repeater_cliHelpSetRadio": "完全重新设置无线电参数,并保存到偏好设置。需要执行“重启”命令才能生效。", - "repeater_cliHelpSetRxDelay": "设置(实验性):设置一个基础值(必须大于1才能生效),用于对接收到的数据包进行轻微延迟处理,该延迟值基于信号强度/评分。将该值设置为0以禁用。", - "repeater_cliHelpSetTxDelay": "通过将一个因子与“浮动模式”数据包的时间在空中停留时间相乘,并结合随机的“时隙”系统,来延迟其转发,从而降低数据包冲突的概率。", - "repeater_cliHelpSetDirectTxDelay": "与txdelay相同,但用于对直接模式数据包的转发进行随机延迟。", - "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接。", - "repeater_cliHelpSetBridgeDelay": "在重新发送数据包之前,设置延迟时间。", - "repeater_cliHelpSetBridgeSource": "选择桥接器是否会转发收到的数据包,还是转发发送的数据包。", - "repeater_cliHelpSetBridgeBaud": "为 RS232 桥接设置串行链路的波特率。", - "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥的秘密。", - "repeater_cliHelpSetAdcMultiplier": "设置自定义因子,用于调整报告的电池电压(仅在特定板上支持)。", - "repeater_cliHelpTempRadio": "设置临时收音机参数,持续指定分钟数,之后恢复到原始收音机参数。(不保存到偏好设置)。", - "repeater_cliHelpSetPerm": "修改 ACL。如果 \"permissions\" 的值为 0,则删除与 pubkey 相关的条目。如果 pubkey-hex 完整且当前不在 ACL 中,则添加新的条目。通过匹配 pubkey 相关的前缀来更新条目。不同固件角色的权限位有所不同,但低 2 位分别对应:0 (访客)、1 (只读)、2 (读写)、3 (管理员)。", - "repeater_cliHelpGetBridgeType": "支持桥接模式、RS232、ESPNOW。", - "repeater_cliHelpLogStart": "开始将数据包记录到文件系统。", - "repeater_cliHelpLogStop": "停止将数据包记录写入文件系统。", - "repeater_cliHelpLogErase": "从文件系统中删除所有已记录的包信息。", - "repeater_cliHelpNeighbors": "显示了通过零跳广告收到的其他复用节点列表。 每行包含:id-前缀-十六进制:时间戳:信噪比(4次)", - "repeater_cliHelpNeighborRemove": "从邻居列表中删除第一个匹配项(通过十六进制的 pubkey 前缀)。", - "repeater_cliHelpRegion": "(仅限序列)列出所有已定义的区域以及当前的防洪许可。", - "repeater_cliHelpRegionLoad": "请注意:这是一个特殊的、包含多个命令的调用方式。 之后的每个命令都是一个区域名称(使用空格进行缩进,以表示父级关系,至少需要一个空格)。 结束方式是通过发送一个空行/命令。", - "repeater_cliHelpRegionGet": "搜索具有指定名称前缀的区域(或使用“*”表示全局范围)。 返回结果为“-> region-name (parent-name) 'F'”", - "repeater_cliHelpRegionPut": "添加或更新一个区域定义,并指定其名称。", - "repeater_cliHelpRegionRemove": "删除具有指定名称的区域定义。 (必须与指定名称完全匹配,且不能有子区域)", - "repeater_cliHelpRegionAllowf": "为指定区域设置“洪水”权限。(“*”表示全局/旧版本范围)", - "repeater_cliHelpRegionDenyf": "移除指定区域的“洪水”权限。(请注意:目前不建议在全局/旧版本中使用此功能!!)", - "repeater_cliHelpRegionHome": "回复当前“主区域”。(此功能尚未应用,仅供未来使用)", - "repeater_cliHelpRegionHomeSet": "设置“主”区域。", - "repeater_cliHelpRegionSave": "将区域列表/地图保存到存储中。", - "repeater_cliHelpGps": "显示 GPS 状态。当 GPS 处于关闭状态时,它只会显示“关闭”;当 GPS 处于开启状态时,它会显示“开启”、“状态”、“定位”、“卫星数量”等信息。", - "repeater_cliHelpGpsOnOff": "切换 GPS 设备的电源状态。", - "repeater_cliHelpGpsSync": "将节点时间与 GPS 钟同步。", - "repeater_cliHelpGpsSetLoc": "将节点的坐标设置为 GPS 坐标,并保存设置。", - "repeater_cliHelpGpsAdvert": "设置节点的位置广告配置:\n- none:不将位置信息包含在广告中\n- share:共享 GPS 位置(从 SensorManager 获取)\n- prefs:在偏好设置中展示的位置", - "repeater_cliHelpGpsAdvertSet": "设置广告的位置配置。", + "repeater_cliHelpAdvert": "发送广播包", + "repeater_cliHelpReboot": "重启设备。(注意:可能会收到超时错误,属于正常现象)", + "repeater_cliHelpClock": "显示设备当前时间", + "repeater_cliHelpPassword": "设置新的管理员密码", + "repeater_cliHelpVersion": "显示设备版本和固件构建日期", + "repeater_cliHelpClearStats": "重置各种统计数据", + "repeater_cliHelpSetAf": "设置时间因子", + "repeater_cliHelpSetTx": "设置 LoRa 发射功率 (dBm)(重启生效)", + "repeater_cliHelpSetRepeat": "启用或禁用此节点的转发功能", + "repeater_cliHelpSetAllowReadOnly": "(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)", + "repeater_cliHelpSetFloodMax": "设置最大传入数据包跳数(≥该值则不转发)", + "repeater_cliHelpSetIntThresh": "设置干扰阈值 (dB),默认14,设为0禁用", + "repeater_cliHelpSetAgcResetInterval": "设置 AGC 重置间隔(秒),设为0禁用", + "repeater_cliHelpSetMultiAcks": "启用或禁用“多重确认”功能", + "repeater_cliHelpSetAdvertInterval": "设置本地广播间隔(分钟),设为0禁用", + "repeater_cliHelpSetFloodAdvertInterval": "设置泛洪广播间隔(小时),设为0禁用", + "repeater_cliHelpSetGuestPassword": "设置/更新访客密码", + "repeater_cliHelpSetName": "设置广播名称", + "repeater_cliHelpSetLat": "设置广播纬度(十进制)", + "repeater_cliHelpSetLon": "设置广播经度(十进制)", + "repeater_cliHelpSetRadio": "完全重设无线电参数并保存,需重启生效", + "repeater_cliHelpSetRxDelay": "(实验性)设置接收延迟基数,设为0禁用", + "repeater_cliHelpSetTxDelay": "通过因子和随机时隙延迟泛洪数据包转发,降低冲突", + "repeater_cliHelpSetDirectTxDelay": "同 txdelay,用于直连模式数据包", + "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接", + "repeater_cliHelpSetBridgeDelay": "设置桥接转发延迟", + "repeater_cliHelpSetBridgeSource": "选择桥接器转发接收或发送的数据包", + "repeater_cliHelpSetBridgeBaud": "设置 RS232 桥接串口波特率", + "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥接密钥", + "repeater_cliHelpSetAdcMultiplier": "设置电池电压校正系数(特定板支持)", + "repeater_cliHelpTempRadio": "临时设置无线电参数指定分钟,之后恢复(不保存)", + "repeater_cliHelpSetPerm": "修改 ACL,权限位:0访客、1只读、2读写、3管理员", + "repeater_cliHelpGetBridgeType": "支持桥接模式:RS232、ESPNOW", + "repeater_cliHelpLogStart": "开始记录数据包到文件系统", + "repeater_cliHelpLogStop": "停止记录数据包", + "repeater_cliHelpLogErase": "删除所有记录的数据包", + "repeater_cliHelpNeighbors": "显示零跳广播收到的其他转发节点列表", + "repeater_cliHelpNeighborRemove": "从邻居列表删除第一个匹配项(通过公钥前缀)", + "repeater_cliHelpRegion": "(仅串口)列出所有定义区域及当前泛洪权限", + "repeater_cliHelpRegionLoad": "特殊多命令调用,以空行结束", + "repeater_cliHelpRegionGet": "搜索指定前缀的区域", + "repeater_cliHelpRegionPut": "添加或更新区域定义", + "repeater_cliHelpRegionRemove": "删除指定区域定义", + "repeater_cliHelpRegionAllowf": "为区域设置“泛洪”权限", + "repeater_cliHelpRegionDenyf": "移除区域的“泛洪”权限", + "repeater_cliHelpRegionHome": "返回当前“主区域”(预留)", + "repeater_cliHelpRegionHomeSet": "设置“主”区域", + "repeater_cliHelpRegionSave": "保存区域列表到存储", + "repeater_cliHelpGps": "显示 GPS 状态", + "repeater_cliHelpGpsOnOff": "切换 GPS 电源", + "repeater_cliHelpGpsSync": "将节点时间与 GPS 同步", + "repeater_cliHelpGpsSetLoc": "将节点坐标设为 GPS 坐标并保存", + "repeater_cliHelpGpsAdvert": "设置位置广播配置:none/share/prefs", + "repeater_cliHelpGpsAdvertSet": "设置广播位置配置", "repeater_commandsListTitle": "命令列表", - "repeater_commandsListNote": "请注意:对于各种“set ...”命令,也存在“get ...”命令。", + "repeater_commandsListNote": "注意:多数 set 命令也有对应的 get 命令", "repeater_general": "通用", "repeater_settingsCategory": "设置", - "repeater_bridge": "桥", - "repeater_logging": "记录", - "repeater_neighborsRepeaterOnly": "邻居(仅限重复功能)", - "repeater_regionManagementRepeaterOnly": "区域管理(仅限重复站点)", - "repeater_regionNote": "区域命令已引入,用于管理区域定义和权限。", + "repeater_bridge": "桥接", + "repeater_logging": "日志", + "repeater_neighborsRepeaterOnly": "邻居(仅转发节点)", + "repeater_regionManagementRepeaterOnly": "区域管理(仅转发节点)", + "repeater_regionNote": "区域命令用于管理区域定义和权限", "repeater_gpsManagement": "GPS 管理", - "repeater_gpsNote": "已引入 GPS 命令,用于管理与位置相关的任务。", + "repeater_gpsNote": "GPS 命令用于位置相关任务", "telemetry_receivedData": "接收到的遥测数据", - "telemetry_requestTimeout": "遥测请求超时。", - "telemetry_errorLoading": "Error loading telemetry: {error}", + "telemetry_requestTimeout": "遥测请求超时", + "telemetry_errorLoading": "加载遥测数据时出错:{error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1209,7 +1209,7 @@ } } }, - "telemetry_noData": "没有可用的 telemetry 数据。", + "telemetry_noData": "暂无遥测数据", "telemetry_channelTitle": "频道 {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1220,9 +1220,9 @@ }, "telemetry_batteryLabel": "电池", "telemetry_voltageLabel": "电压", - "telemetry_mcuTemperatureLabel": "MCU 的温度", + "telemetry_mcuTemperatureLabel": "MCU 温度", "telemetry_temperatureLabel": "温度", - "telemetry_currentLabel": "当前", + "telemetry_currentLabel": "电流", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1261,9 +1261,9 @@ } } }, - "neighbors_receivedData": "已收到邻居信息", - "neighbors_requestTimedOut": "邻居要求停止干扰。", - "neighbors_errorLoading": "Error loading neighbors: {error}", + "neighbors_receivedData": "已接收邻居信息", + "neighbors_requestTimedOut": "邻居请求超时", + "neighbors_errorLoading": "加载邻居时出错:{error}", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1271,9 +1271,9 @@ } } }, - "neighbors_repeatersNeighbours": "重复使用的邻居", - "neighbors_noData": "没有可用的邻居信息。", - "neighbors_unknownContact": "Unknown {pubkey}", + "neighbors_repeatersNeighbours": "转发节点的邻居", + "neighbors_noData": "暂无邻居信息", + "neighbors_unknownContact": "未知 {pubkey}", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1281,7 +1281,7 @@ } } }, - "neighbors_heardAgo": "Heard: {time} ago", + "neighbors_heardAgo": "听到:{time}前", "@neighbors_heardAgo": { "placeholders": { "time": { @@ -1292,15 +1292,15 @@ "channelPath_title": "数据包路径", "channelPath_viewMap": "查看地图", "channelPath_otherObservedPaths": "其他观察到的路径", - "channelPath_repeaterHops": "复用跳跃", - "channelPath_noHopDetails": "对于此包,未提供详细信息。", + "channelPath_repeaterHops": "转发节点跳数", + "channelPath_noHopDetails": "此数据包未提供详细信息", "channelPath_messageDetails": "消息详情", - "channelPath_senderLabel": "发件人", + "channelPath_senderLabel": "发送者", "channelPath_timeLabel": "时间", "channelPath_repeatsLabel": "重复", "channelPath_pathLabel": "路径 {index}", "channelPath_observedLabel": "观察到的", - "channelPath_observedPathTitle": "Observed path {index} • {hops}", + "channelPath_observedPathTitle": "观察到的路径 {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1311,7 +1311,7 @@ } } }, - "channelPath_noLocationData": "没有位置信息", + "channelPath_noLocationData": "无位置信息", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1335,9 +1335,9 @@ } }, "channelPath_unknownPath": "未知", - "channelPath_floodPath": "洪水", - "channelPath_directPath": "直接", - "channelPath_observedZeroOf": "0 of {total} hops", + "channelPath_floodPath": "泛洪", + "channelPath_directPath": "直连", + "channelPath_observedZeroOf": "0 / {total} 跳", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1345,7 +1345,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} of {total} hops", + "channelPath_observedSomeOf": "{observed} / {total} 跳", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1356,9 +1356,9 @@ } } }, - "channelPath_mapTitle": "路线图", - "channelPath_noRepeaterLocations": "这条路径上没有可用的中继器位置。", - "channelPath_primaryPath": "路径 {index} (主要路径)", + "channelPath_mapTitle": "路径地图", + "channelPath_noRepeaterLocations": "此路径上没有可用的转发节点位置信息", + "channelPath_primaryPath": "路径 {index}(主要)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1374,7 +1374,7 @@ } }, "channelPath_pathLabelTitle": "路径", - "channelPath_observedPathHeader": "观察路径", + "channelPath_observedPathHeader": "观察到的路径", "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { @@ -1386,14 +1386,14 @@ } } }, - "channelPath_noHopDetailsAvailable": "对于此包裹,尚无详细信息。", - "channelPath_unknownRepeater": "未知的重复设备", + "channelPath_noHopDetailsAvailable": "此数据包暂无详细信息", + "channelPath_unknownRepeater": "未知转发节点", "community_title": "社区", - "community_create": "建立社区", - "community_createDesc": "创建一个新的社群,并通过二维码进行分享。", + "community_create": "创建社区", + "community_createDesc": "创建新社区并通过二维码分享。", "community_join": "加入", "community_joinTitle": "加入社区", - "community_joinConfirmation": "Do you want to join the community \"{name}\"?", + "community_joinConfirmation": "是否加入社区 \"{name}\"?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1402,13 +1402,13 @@ } }, "community_scanQr": "扫描社区二维码", - "community_scanInstructions": "将相机对准社区的二维码。", + "community_scanInstructions": "将摄像头对准社区的二维码", "community_showQr": "显示二维码", - "community_publicChannel": "社区公共", - "community_hashtagChannel": "社区标签", + "community_publicChannel": "社区公共频道", + "community_hashtagChannel": "社区标签频道", "community_name": "社区名称", "community_enterName": "请输入社区名称", - "community_created": "Community \"{name}\" created", + "community_created": "社区 \"{name}\" 已创建", "@community_created": { "placeholders": { "name": { @@ -1416,7 +1416,7 @@ } } }, - "community_joined": "Joined community \"{name}\"", + "community_joined": "已加入社区 \"{name}\"", "@community_joined": { "placeholders": { "name": { @@ -1425,7 +1425,7 @@ } }, "community_qrTitle": "分享社区", - "community_qrInstructions": "Scan this QR code to join \"{name}\"", + "community_qrInstructions": "扫描此二维码加入 \"{name}\"", "@community_qrInstructions": { "placeholders": { "name": { @@ -1433,10 +1433,10 @@ } } }, - "community_hashtagPrivacyHint": "仅社区成员才能加入社区话题标签的频道。", + "community_hashtagPrivacyHint": "仅社区成员可加入社区标签频道。", "community_invalidQrCode": "无效的社区二维码", - "community_alreadyMember": "已经是会员", - "community_alreadyMemberMessage": "You are already a member of \"{name}\".", + "community_alreadyMember": "已是成员", + "community_alreadyMemberMessage": "您已是 \"{name}\" 的成员。", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1445,12 +1445,12 @@ } }, "community_addPublicChannel": "添加公共频道", - "community_addPublicChannelHint": "自动添加该社区的公共频道", - "community_noCommunities": "目前还没有任何社区加入。", - "community_scanOrCreate": "扫描二维码或创建社群,即可开始。", + "community_addPublicChannelHint": "自动添加此社区的公共频道", + "community_noCommunities": "尚未加入任何社区。", + "community_scanOrCreate": "扫描二维码或创建社区以开始。", "community_manageCommunities": "管理社区", "community_delete": "退出社区", - "community_deleteConfirm": "是否要删除\"{name}\"?", + "community_deleteConfirm": "是否退出 \"{name}\"?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1466,7 +1466,7 @@ } } }, - "community_deleted": "Left community \"{name}\"", + "community_deleted": "已退出社区 \"{name}\"", "@community_deleted": { "placeholders": { "name": { @@ -1474,8 +1474,8 @@ } } }, - "community_regenerateSecret": "恢复秘密", - "community_regenerateSecretConfirm": "[保存:{name}]\n是否需要重新生成\"{name}\"的密钥?所有成员都需要扫描新的二维码才能继续进行通信。", + "community_regenerateSecret": "重新生成密钥", + "community_regenerateSecretConfirm": "是否为 \"{name}\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1483,8 +1483,8 @@ } } }, - "community_regenerate": "再生", - "community_secretRegenerated": "[保护对象:{name}]\n秘密已恢复至\"{name}\"", + "community_regenerate": "重新生成", + "community_secretRegenerated": "已为 \"{name}\" 重新生成密钥", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1492,8 +1492,8 @@ } } }, - "community_updateSecret": "更新秘密", - "community_secretUpdated": "“{name}”的秘密已更新", + "community_updateSecret": "更新密钥", + "community_secretUpdated": "“{name}”的密钥已更新", "@community_secretUpdated": { "placeholders": { "name": { @@ -1501,7 +1501,7 @@ } } }, - "community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"", + "community_scanToUpdateSecret": "扫描新二维码以更新 \"{name}\" 的密钥", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1509,14 +1509,14 @@ } } }, - "community_addHashtagChannel": "添加社区标签", - "community_addHashtagChannelDesc": "为这个社区创建一个带有话题标签的频道", + "community_addHashtagChannel": "添加标签频道", + "community_addHashtagChannelDesc": "为此社区创建标签频道", "community_selectCommunity": "选择社区", - "community_regularHashtag": "常用标签", - "community_regularHashtagDesc": "公共话题标签(任何人都可以参与)", + "community_regularHashtag": "普通标签", + "community_regularHashtagDesc": "公共标签频道(任何人都可参与)", "community_communityHashtag": "社区标签", "community_communityHashtagDesc": "仅限社区成员", - "community_forCommunity": "For {name}", + "community_forCommunity": "为 {name}", "@community_forCommunity": { "placeholders": { "name": { @@ -1524,30 +1524,30 @@ } } }, - "listFilter_tooltip": "筛选和排序", - "listFilter_sortBy": "按排序", + "listFilter_tooltip": "筛选与排序", + "listFilter_sortBy": "排序方式", "listFilter_latestMessages": "最新消息", - "listFilter_heardRecently": "最近听到的", - "listFilter_az": "A 到 Z", - "listFilter_filters": "过滤器", + "listFilter_heardRecently": "最近听到", + "listFilter_az": "A-Z", + "listFilter_filters": "筛选", "listFilter_all": "全部", "listFilter_users": "用户", - "listFilter_repeaters": "重复器", + "listFilter_repeaters": "转发节点", "listFilter_roomServers": "房间服务器", - "listFilter_unreadOnly": "仅显示未读消息", - "listFilter_newGroup": "新的团体", - "pathTrace_you": "您", + "listFilter_unreadOnly": "仅显示未读", + "listFilter_newGroup": "新建群聊", + "pathTrace_you": "我自己", "pathTrace_failed": "路径追踪失败。", "pathTrace_notAvailable": "无法获取路径信息。", - "pathTrace_refreshTooltip": "重新绘制路径。", + "pathTrace_refreshTooltip": "刷新路径追踪", "contacts_pathTrace": "路径追踪", - "contacts_ping": "乒", - "contacts_repeaterPathTrace": "追踪路径至中继器", - "contacts_repeaterPing": "中继器", - "contacts_roomPathTrace": "追踪到房间服务器", - "contacts_roomPing": "会议室服务器", - "contacts_chatTraceRoute": "路径跟踪路线", - "contacts_pathTraceTo": "追踪路径至 {name}", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Trace 转发节点", + "contacts_repeaterPing": "Ping 转发节点", + "contacts_roomPathTrace": "Trace 房间服务器", + "contacts_roomPing": "Ping 房间服务器", + "contacts_chatTraceRoute": "路由追踪", + "contacts_pathTraceTo": "追踪至 {name} 的路径", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1555,48 +1555,48 @@ } } }, - "contacts_clipboardEmpty": "剪贴板为空。", - "contacts_invalidAdvertFormat": "无效的联系信息", - "contacts_contactImported": "已建立联系。", - "contacts_contactImportFailed": "未能导入联系人。", - "contacts_zeroHopAdvert": "零跳广告", - "contacts_floodAdvert": "防洪广告", - "contacts_copyAdvertToClipboard": "复制广告到剪贴板", + "contacts_clipboardEmpty": "剪贴板为空", + "contacts_invalidAdvertFormat": "无效的联系人信息格式", + "contacts_contactImported": "联系人已导入", + "contacts_contactImportFailed": "导入联系人失败。", + "contacts_zeroHopAdvert": "发送零跳广播", + "contacts_floodAdvert": "发送泛洪广播", + "contacts_copyAdvertToClipboard": "复制广播到剪贴板", "contacts_addContactFromClipboard": "从剪贴板添加联系人", - "contacts_ShareContact": "复制联系方式到剪贴板", - "contacts_ShareContactZeroHop": "通过广告分享联系方式", - "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", - "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", - "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "contacts_ShareContact": "复制联系人信息到剪贴板", + "contacts_ShareContactZeroHop": "通过广播分享联系人", + "contacts_zeroHopContactAdvertSent": "零跳广播已发送", + "contacts_zeroHopContactAdvertFailed": "发送联系人广播失败。", + "contacts_contactAdvertCopied": "广播已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "复制广播到剪贴板失败。", "notification_activityTitle": "MeshCore 活动", "notification_messagesCount": "{count} 条消息", "notification_channelMessagesCount": "{count} 条频道消息", "notification_newNodesCount": "{count} 个新节点", "notification_newTypeDiscovered": "发现新 {contactType}", "notification_receivedNewMessage": "收到新消息", - "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", - "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", - "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", + "settings_gpxExportRepeaters": "导出转发节点/房间服务器到 GPX", + "settings_gpxExportRepeatersSubtitle": "导出带位置的转发节点/房间服务器到 GPX 文件", + "settings_gpxExportContactsSubtitle": "导出带位置的伙伴到 GPX 文件", "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", - "settings_gpxExportSuccess": "成功导出GPX文件", - "settings_gpxExportError": "导出时发生错误", - "settings_gpxExportRepeatersRoom": "重复器和房间服务器位置", - "settings_gpxExportChat": "伴侣位置", - "settings_gpxExportAll": "导出所有联系人到GPX", - "settings_gpxExportContacts": "导出伴侣到GPX", - "settings_gpxExportAllSubtitle": "导出所有带有位置的联系人到GPX文件。", + "settings_gpxExportSuccess": "GPX 文件导出成功", + "settings_gpxExportError": "导出时出错", + "settings_gpxExportRepeatersRoom": "转发节点与房间服务器位置", + "settings_gpxExportChat": "伙伴位置", + "settings_gpxExportAll": "导出所有联系人到 GPX", + "settings_gpxExportContacts": "导出伙伴到 GPX", + "settings_gpxExportAllSubtitle": "导出所有带位置的联系人到 GPX 文件", "settings_gpxExportAllContacts": "所有联系人位置", - "settings_gpxExportNoContacts": "没有联系人可导出", - "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", - "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出", - "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!", - "map_tapToAdd": "点击节点将其添加到路径中", + "settings_gpxExportNoContacts": "没有可导出的联系人", + "settings_gpxExportShareText": "来自 MeshCore Open 的地图数据导出", + "settings_gpxExportShareSubject": "MeshCore Open GPX 地图数据导出", + "pathTrace_someHopsNoLocation": "某些跳缺少位置信息!", + "map_tapToAdd": "点击节点以添加到路径", "pathTrace_clearTooltip": "清除路径", - "map_pathTraceCancelled": "路径跟踪已取消", - "map_removeLast": "删除最后一个", - "map_runTrace": "运行路径跟踪", - "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", + "map_pathTraceCancelled": "路径追踪已取消", + "map_removeLast": "移除最后一个", + "map_runTrace": "运行路径追踪", + "scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备", "scanner_bluetoothOff": "蓝牙已关闭", "scanner_enableBluetooth": "启用蓝牙" } From 947fafbbb759fb5b21b95b47fa86face97635ebd Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 17 Feb 2026 23:42:04 -0700 Subject: [PATCH 106/421] Refactor radio settings and localization updates fixes #72 - Removed preset configurations for 915 MHz, 868 MHz, and 433 MHz from the RadioSettings model. - Introduced a new list of regional preset configurations for various countries. - Updated the settings screen to use a dropdown for selecting presets instead of chips. - Added a switch for enabling client repeat functionality with appropriate warnings for frequency usage. - Updated localization files for multiple languages to reflect changes in settings related to client repeat functionality. --- lib/connector/meshcore_connector.dart | 15 +- lib/connector/meshcore_protocol.dart | 10 +- lib/l10n/app_bg.arb | 10 +- lib/l10n/app_de.arb | 10 +- lib/l10n/app_en.arb | 567 ++++++++++++++++++-------- lib/l10n/app_es.arb | 10 +- lib/l10n/app_fr.arb | 10 +- lib/l10n/app_it.arb | 10 +- lib/l10n/app_localizations.dart | 36 +- lib/l10n/app_localizations_bg.dart | 18 +- lib/l10n/app_localizations_de.dart | 18 +- lib/l10n/app_localizations_en.dart | 18 +- lib/l10n/app_localizations_es.dart | 18 +- lib/l10n/app_localizations_fr.dart | 18 +- lib/l10n/app_localizations_it.dart | 18 +- lib/l10n/app_localizations_nl.dart | 18 +- lib/l10n/app_localizations_pl.dart | 18 +- lib/l10n/app_localizations_pt.dart | 18 +- lib/l10n/app_localizations_ru.dart | 18 +- lib/l10n/app_localizations_sk.dart | 18 +- lib/l10n/app_localizations_sl.dart | 18 +- lib/l10n/app_localizations_sv.dart | 18 +- lib/l10n/app_localizations_uk.dart | 18 +- lib/l10n/app_localizations_zh.dart | 17 +- lib/l10n/app_nl.arb | 10 +- lib/l10n/app_pl.arb | 10 +- lib/l10n/app_pt.arb | 10 +- lib/l10n/app_ru.arb | 10 +- lib/l10n/app_sk.arb | 10 +- lib/l10n/app_sl.arb | 10 +- lib/l10n/app_sv.arb | 10 +- lib/l10n/app_uk.arb | 10 +- lib/l10n/app_zh.arb | 10 +- lib/models/radio_settings.dart | 63 ++- lib/screens/settings_screen.dart | 86 ++-- 35 files changed, 660 insertions(+), 526 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 5d6c7e6..5cdab78 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -90,6 +90,8 @@ class MeshCoreConnector extends ChangeNotifier { int? _currentBwHz; int? _currentSf; int? _currentCr; + bool? _clientRepeat; + int? _firmwareVerCode; int? _batteryMillivolts; double? _selfLatitude; double? _selfLongitude; @@ -200,6 +202,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + bool? get clientRepeat => _clientRepeat; + int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; @@ -916,6 +920,8 @@ class MeshCoreConnector extends ChangeNotifier { _selfName = null; _selfLatitude = null; _selfLongitude = null; + _clientRepeat = null; + _firmwareVerCode = null; _batteryMillivolts = null; _batteryRequested = false; _awaitingSelfInfo = false; @@ -1820,6 +1826,13 @@ class MeshCoreConnector extends ChangeNotifier { void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; + _firmwareVerCode = frame[1]; + + // Parse client_repeat from firmware v9+ (byte 80) + if (frame.length >= 81) { + _clientRepeat = frame[80] != 0; + } + // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; @@ -1840,8 +1853,8 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(getChannels(maxChannels: nextMaxChannels)); } } - notifyListeners(); } + notifyListeners(); } void _handleNoMoreMessages() { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 25359a8..0b78c65 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -550,18 +550,24 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) { } // Build CMD_SET_RADIO_PARAMS frame -// Format: [cmd][freq x4][bw x4][sf][cr] +// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9) +// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+) // freq: frequency in Hz (300000-2500000) // bw: bandwidth in Hz (7000-500000) // sf: spreading factor (5-12) // cr: coding rate (5-8) -Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) { +// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older) +Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr, + {bool? clientRepeat}) { final writer = BufferWriter(); writer.writeByte(cmdSetRadioParams); writer.writeUInt32LE(freqHz); writer.writeUInt32LE(bwHz); writer.writeByte(sf); writer.writeByte(cr); + if (clientRepeat != null) { + writer.writeByte(clientRepeat ? 1 : 0); + } return writer.toBytes(); } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index a1cfb3e..567c74c 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Брой контакти", "settings_infoChannelCount": "Брой канали", "settings_presets": "Предварителни настройки", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Честота (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Мощност (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)", - "settings_longRange": "Дълъг обхват", - "settings_fastSpeed": "Бърза скорост", "settings_error": "Грешка: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", "scanner_bluetoothOff": "Bluetooth е изключен.", "scanner_enableBluetooth": "Активирайте Bluetooth", - "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства." + "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", + "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", + "settings_clientRepeat": "Без електричество – повторение" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2e66222..7aba89b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Anzahl Kontakte", "settings_infoChannelCount": "Anzahl Kanäle", "settings_presets": "Voreinstellungen", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequenz (MHz)", "settings_frequencyHelper": "300,00 - 2.500,00", "settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX-Leistung (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)", - "settings_longRange": "Grosse Reichweite", - "settings_fastSpeed": "Schnelle Geschwindigkeit", "settings_error": "Fehler: {message}", "@settings_error": { "placeholders": { @@ -1626,5 +1621,8 @@ "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", - "scanner_enableBluetooth": "Bluetooth aktivieren" + "scanner_enableBluetooth": "Bluetooth aktivieren", + "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", + "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", + "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 890b992..cfd6330 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,12 +1,9 @@ { "@@locale": "en", - "appTitle": "MeshCore Open", - "nav_contacts": "Contacts", "nav_channels": "Channels", "nav_map": "Map", - "common_cancel": "Cancel", "common_ok": "OK", "common_connect": "Connect", @@ -35,16 +32,19 @@ "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { - "volts": {"type": "String"} + "volts": { + "type": "String" + } } }, "common_percentValue": "{percent}%", "@common_percentValue": { "placeholders": { - "percent": {"type": "int"} + "percent": { + "type": "int" + } } }, - "scanner_title": "MeshCore Open", "scanner_scanning": "Scanning for devices...", "scanner_connecting": "Connecting...", @@ -53,7 +53,9 @@ "scanner_connectedTo": "Connected to {deviceName}", "@scanner_connectedTo": { "placeholders": { - "deviceName": {"type": "String"} + "deviceName": { + "type": "String" + } } }, "scanner_searchingDevices": "Searching for MeshCore devices...", @@ -61,7 +63,9 @@ "scanner_connectionFailed": "Connection failed: {error}", "@scanner_connectionFailed": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "scanner_stop": "Stop", @@ -69,10 +73,8 @@ "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", "scanner_enableBluetooth": "Enable Bluetooth", - "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", - "settings_title": "Settings", "settings_deviceInfo": "Device Info", "settings_appSettings": "App Settings", @@ -122,7 +124,9 @@ "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { - "version": {"type": "String"} + "version": { + "type": "String" + } } }, "settings_aboutLegalese": "2026 MeshCore Open Source Project", @@ -135,9 +139,6 @@ "settings_infoContactsCount": "Contacts Count", "settings_infoChannelCount": "Channel Count", "settings_presets": "Presets", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequency (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "Invalid frequency (300-2500 MHz)", @@ -147,15 +148,17 @@ "settings_txPower": "TX Power (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Invalid TX power (0-22 dBm)", - "settings_longRange": "Long Range", - "settings_fastSpeed": "Fast Speed", + "settings_clientRepeat": "Off-Grid Repeat", + "settings_clientRepeatSubtitle": "Allow this device to repeat mesh packets for others", + "settings_clientRepeatFreqWarning": "Off-grid repeat requires 433, 869, or 918 MHz frequency", "settings_error": "Error: {message}", "@settings_error": { "placeholders": { - "message": {"type": "String"} + "message": { + "type": "String" + } } }, - "appSettings_title": "App Settings", "appSettings_appearance": "Appearance", "appSettings_theme": "Theme", @@ -205,7 +208,9 @@ "appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { - "deviceName": {"type": "String"} + "deviceName": { + "type": "String" + } } }, "appSettings_batteryChemistryConnectFirst": "Connect to a device to choose", @@ -224,7 +229,9 @@ "appSettings_timeFilterShowLast": "Show nodes from last {hours} hours", "@appSettings_timeFilterShowLast": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "appSettings_mapTimeFilter": "Map Time Filter", @@ -239,8 +246,12 @@ "appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { - "minZoom": {"type": "int"}, - "maxZoom": {"type": "int"} + "minZoom": { + "type": "int" + }, + "maxZoom": { + "type": "int" + } } }, "appSettings_debugCard": "Debug", @@ -248,7 +259,6 @@ "appSettings_appDebugLoggingSubtitle": "Log app debug messages for troubleshooting", "appSettings_appDebugLoggingEnabled": "App debug logging enabled", "appSettings_appDebugLoggingDisabled": "App debug logging disabled", - "contacts_title": "Contacts", "contacts_noContacts": "No contacts yet", "contacts_contactsWillAppear": "Contacts will appear when devices advertise", @@ -259,7 +269,9 @@ "contacts_removeConfirm": "Remove {contactName} from contacts?", "@contacts_removeConfirm": { "placeholders": { - "contactName": {"type": "String"} + "contactName": { + "type": "String" + } } }, "contacts_manageRepeater": "Manage Repeater", @@ -271,7 +283,9 @@ "contacts_deleteGroupConfirm": "Remove \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { - "groupName": {"type": "String"} + "groupName": { + "type": "String" + } } }, "contacts_newGroup": "New Group", @@ -280,7 +294,9 @@ "contacts_groupAlreadyExists": "Group \"{name}\" already exists", "@contacts_groupAlreadyExists": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "contacts_filterContacts": "Filter contacts...", @@ -290,24 +306,29 @@ "contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago", "@contacts_lastSeenMinsAgo": { "placeholders": { - "minutes": {"type": "int"} + "minutes": { + "type": "int" + } } }, "contacts_lastSeenHourAgo": "Last seen 1 hour ago", "contacts_lastSeenHoursAgo": "Last seen {hours} hours ago", "@contacts_lastSeenHoursAgo": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "contacts_lastSeenDayAgo": "Last seen 1 day ago", "contacts_lastSeenDaysAgo": "Last seen {days} days ago", "@contacts_lastSeenDaysAgo": { "placeholders": { - "days": {"type": "int"} + "days": { + "type": "int" + } } }, - "channels_title": "Channels", "channels_noChannelsConfigured": "No channels configured", "channels_addPublicChannel": "Add Public Channel", @@ -316,7 +337,9 @@ "channels_channelIndex": "Channel {index}", "@channels_channelIndex": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, "channels_hashtagChannel": "Hashtag channel", @@ -329,13 +352,17 @@ "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_channelDeleted": "Channel \"{name}\" deleted", "@channels_channelDeleted": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_addChannel": "Add Channel", @@ -350,20 +377,26 @@ "channels_channelAdded": "Channel \"{name}\" added", "@channels_channelAdded": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_editChannelTitle": "Edit Channel {index}", "@channels_editChannelTitle": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, "channels_smazCompression": "SMAZ compression", "channels_channelUpdated": "Channel \"{name}\" updated", "@channels_channelUpdated": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_publicChannelAdded": "Public channel added", @@ -384,34 +417,41 @@ "channels_scanQrCodeComingSoon": "Coming soon", "channels_enterHashtag": "Enter hashtag", "channels_hashtagHint": "e.g. #team", - "chat_noMessages": "No messages yet", "chat_sendMessageToStart": "Send a message to get started", "chat_originalMessageNotFound": "Original message not found", "chat_replyingTo": "Replying to {name}", "@chat_replyingTo": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "chat_replyTo": "Reply to {name}", "@chat_replyTo": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "chat_location": "Location", "chat_sendMessageTo": "Send a message to {contactName}", "@chat_sendMessageTo": { "placeholders": { - "contactName": {"type": "String"} + "contactName": { + "type": "String" + } } }, "chat_typeMessage": "Type a message...", "chat_messageTooLong": "Message too long (max {maxBytes} bytes).", "@chat_messageTooLong": { "placeholders": { - "maxBytes": {"type": "int"} + "maxBytes": { + "type": "int" + } } }, "chat_messageCopied": "Message copied", @@ -420,8 +460,12 @@ "chat_retryCount": "Retry {current}/{max}", "@chat_retryCount": { "placeholders": { - "current": {"type": "int"}, - "max": {"type": "int"} + "current": { + "type": "int" + }, + "max": { + "type": "int" + } } }, "chat_sendGif": "Send GIF", @@ -453,39 +497,53 @@ "debugFrame_length": "Frame Length: {count} bytes", "@debugFrame_length": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "debugFrame_command": "Command: 0x{value}", "@debugFrame_command": { "placeholders": { - "value": {"type": "String"} + "value": { + "type": "String" + } } }, "debugFrame_textMessageHeader": "Text Message Frame:", "debugFrame_destinationPubKey": "- Destination PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { - "pubKey": {"type": "String"} + "pubKey": { + "type": "String" + } } }, "debugFrame_timestamp": "- Timestamp: {timestamp}", "@debugFrame_timestamp": { "placeholders": { - "timestamp": {"type": "int"} + "timestamp": { + "type": "int" + } } }, "debugFrame_flags": "- Flags: 0x{value}", "@debugFrame_flags": { "placeholders": { - "value": {"type": "String"} + "value": { + "type": "String" + } } }, "debugFrame_textType": "- Text Type: {type} ({label})", "@debugFrame_textType": { "placeholders": { - "type": {"type": "int"}, - "label": {"type": "String"} + "type": { + "type": "int" + }, + "label": { + "type": "String" + } } }, "debugFrame_textTypeCli": "CLI", @@ -493,7 +551,9 @@ "debugFrame_text": "- Text: \"{text}\"", "@debugFrame_text": { "placeholders": { - "text": {"type": "String"} + "text": { + "type": "String" + } } }, "debugFrame_hexDump": "Hex Dump:", @@ -508,7 +568,9 @@ "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "chat_successes": "successes", @@ -527,8 +589,12 @@ "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { - "hopCount": {"type": "int"}, - "status": {"type": "String"} + "hopCount": { + "type": "int" + }, + "status": { + "type": "String" + } } }, "chat_pathSavedLocally": "Saved locally. Connect to sync.", @@ -543,7 +609,9 @@ "chat_hopsForced": "{count} hops (forced)", "@chat_hopsForced": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "chat_floodAuto": "Flood (auto)", @@ -552,7 +620,9 @@ "chat_unread": "Unread: {count}", "@chat_unread": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "chat_openLink": "Open Link?", @@ -561,24 +631,29 @@ "chat_couldNotOpenLink": "Could not open link: {url}", "@chat_couldNotOpenLink": { "placeholders": { - "url": {"type": "String"} + "url": { + "type": "String" + } } }, "chat_invalidLink": "Invalid link format", - "map_title": "Node Map", "map_noNodesWithLocation": "No nodes with location data", "map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map", "map_nodesCount": "Nodes: {count}", "@map_nodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "map_pinsCount": "Pins: {count}", "@map_pinsCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "map_chat": "Chat", @@ -604,7 +679,9 @@ "map_publicLocationShareConfirm": "You are about to share a location in {channelLabel}. This channel is public and anyone with the PSK can see it.", "@map_publicLocationShareConfirm": { "placeholders": { - "channelLabel": {"type": "String"} + "channelLabel": { + "type": "String" + } } }, "map_connectToShareMarkers": "Connect to a device to share markers", @@ -633,21 +710,29 @@ "mapCache_downloadTilesPrompt": "Download {count} tiles for offline use?", "@mapCache_downloadTilesPrompt": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_downloadAction": "Download", "mapCache_cachedTiles": "Cached {count} tiles", "@mapCache_cachedTiles": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", "@mapCache_cachedTilesWithFailed": { "placeholders": { - "downloaded": {"type": "int"}, - "failed": {"type": "int"} + "downloaded": { + "type": "int" + }, + "failed": { + "type": "int" + } } }, "mapCache_clearOfflineCacheTitle": "Clear offline cache", @@ -660,14 +745,20 @@ "mapCache_estimatedTiles": "Estimated tiles: {count}", "@mapCache_estimatedTiles": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_downloadedTiles": "Downloaded {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { - "completed": {"type": "int"}, - "total": {"type": "int"} + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } } }, "mapCache_downloadTilesButton": "Download Tiles", @@ -675,36 +766,51 @@ "mapCache_failedDownloads": "Failed downloads: {count}", "@mapCache_failedDownloads": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { - "north": {"type": "String"}, - "south": {"type": "String"}, - "east": {"type": "String"}, - "west": {"type": "String"} + "north": { + "type": "String" + }, + "south": { + "type": "String" + }, + "east": { + "type": "String" + }, + "west": { + "type": "String" + } } }, - "time_justNow": "Just now", "time_minutesAgo": "{minutes}m ago", "@time_minutesAgo": { "placeholders": { - "minutes": {"type": "int"} + "minutes": { + "type": "int" + } } }, "time_hoursAgo": "{hours}h ago", "@time_hoursAgo": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "time_daysAgo": "{days}d ago", "@time_daysAgo": { "placeholders": { - "days": {"type": "int"} + "days": { + "type": "int" + } } }, "time_hour": "hour", @@ -717,10 +823,8 @@ "time_months": "months", "time_minutes": "minutes", "time_allTime": "All Time", - "dialog_disconnect": "Disconnect", "dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?", - "login_repeaterLogin": "Repeater Login", "login_roomLogin": "Room Server Login", "login_password": "Password", @@ -738,32 +842,39 @@ "login_attempt": "Attempt {current}/{max}", "@login_attempt": { "placeholders": { - "current": {"type": "int"}, - "max": {"type": "int"} + "current": { + "type": "int" + }, + "max": { + "type": "int" + } } }, "login_failed": "Login failed: {error}", "@login_failed": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.", - - "common_reload": "Reload", "common_clear": "Clear", - "path_currentPath": "Current path: {path}", "@path_currentPath": { "placeholders": { - "path": {"type": "String"} + "path": { + "type": "String" + } } }, "path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path", "@path_usingHopsPath": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "path_enterCustomPath": "Enter Custom Path", @@ -778,12 +889,13 @@ "path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { - "prefixes": {"type": "String"} + "prefixes": { + "type": "String" + } } }, "path_tooLong": "Path too long. Maximum 64 hops allowed.", "path_setPath": "Set Path", - "repeater_management": "Repeater Management", "room_management": "Room Server Management", "repeater_managementTools": "Management Tools", @@ -797,7 +909,6 @@ "repeater_neighboursSubtitle": "View zero hop neighbors.", "repeater_settings": "Settings", "repeater_settingsSubtitle": "Configure repeater parameters", - "repeater_statusTitle": "Repeater Status", "repeater_routingMode": "Routing mode", "repeater_autoUseSavedPath": "Auto (use saved path)", @@ -808,7 +919,9 @@ "repeater_errorLoadingStatus": "Error loading status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_systemInformation": "System Information", @@ -830,42 +943,67 @@ "repeater_daysHoursMinsSecs": "{days} days {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { - "days": {"type": "int"}, - "hours": {"type": "int"}, - "minutes": {"type": "int"}, - "seconds": {"type": "int"} + "days": { + "type": "int" + }, + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + }, + "seconds": { + "type": "int" + } } }, "repeater_packetTxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetTxTotal": { "placeholders": { - "total": {"type": "int"}, - "flood": {"type": "String"}, - "direct": {"type": "String"} + "total": { + "type": "int" + }, + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } } }, "repeater_packetRxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetRxTotal": { "placeholders": { - "total": {"type": "int"}, - "flood": {"type": "String"}, - "direct": {"type": "String"} + "total": { + "type": "int" + }, + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } } }, "repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { - "flood": {"type": "String"}, - "direct": {"type": "String"} + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } } }, "repeater_duplicatesTotal": "Total: {total}", "@repeater_duplicatesTotal": { "placeholders": { - "total": {"type": "int"} + "total": { + "type": "int" + } } }, - "repeater_settingsTitle": "Repeater Settings", "repeater_basicSettings": "Basic Settings", "repeater_repeaterName": "Repeater Name", @@ -899,14 +1037,18 @@ "repeater_localAdvertIntervalMinutes": "{minutes} minutes", "@repeater_localAdvertIntervalMinutes": { "placeholders": { - "minutes": {"type": "int"} + "minutes": { + "type": "int" + } } }, "repeater_floodAdvertInterval": "Flood Advertisement Interval", "repeater_floodAdvertIntervalHours": "{hours} hours", "@repeater_floodAdvertIntervalHours": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "repeater_encryptedAdvertInterval": "Encrypted Advertisement Interval", @@ -924,13 +1066,17 @@ "repeater_commandSent": "Command sent: {command}", "@repeater_commandSent": { "placeholders": { - "command": {"type": "String"} + "command": { + "type": "String" + } } }, "repeater_errorSendingCommand": "Error sending command: {error}", "@repeater_errorSendingCommand": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_confirm": "Confirm", @@ -938,7 +1084,9 @@ "repeater_errorSavingSettings": "Error saving settings: {error}", "@repeater_errorSavingSettings": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_refreshBasicSettings": "Refresh Basic Settings", @@ -952,16 +1100,19 @@ "repeater_refreshed": "{label} refreshed", "@repeater_refreshed": { "placeholders": { - "label": {"type": "String"} + "label": { + "type": "String" + } } }, "repeater_errorRefreshing": "Error refreshing {label}", "@repeater_errorRefreshing": { "placeholders": { - "label": {"type": "String"} + "label": { + "type": "String" + } } }, - "repeater_cliTitle": "Repeater CLI", "repeater_debugNextCommand": "Debug Next Command", "repeater_commandHelp": "Command Help", @@ -976,7 +1127,9 @@ "repeater_cliCommandError": "Error: {error}", "@repeater_cliCommandError": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_cliQuickGetName": "Get Name", @@ -1056,14 +1209,18 @@ "telemetry_errorLoading": "Error loading telemetry: {error}", "@telemetry_errorLoading": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "telemetry_noData": "No telemetry data available.", "telemetry_channelTitle": "Channel {channel}", "@telemetry_channelTitle": { "placeholders": { - "channel": {"type": "int"} + "channel": { + "type": "int" + } } }, "telemetry_batteryLabel": "Battery", @@ -1074,36 +1231,49 @@ "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { - "percent": {"type": "int"}, - "volts": {"type": "String"} + "percent": { + "type": "int" + }, + "volts": { + "type": "String" + } } }, "telemetry_voltageValue": "{volts}V", "@telemetry_voltageValue": { "placeholders": { - "volts": {"type": "String"} + "volts": { + "type": "String" + } } }, "telemetry_currentValue": "{amps}A", "@telemetry_currentValue": { "placeholders": { - "amps": {"type": "String"} + "amps": { + "type": "String" + } } }, "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { - "celsius": {"type": "String"}, - "fahrenheit": {"type": "String"} + "celsius": { + "type": "String" + }, + "fahrenheit": { + "type": "String" + } } }, - "neighbors_receivedData": "Received Neighbours Data", "neighbors_requestTimedOut": "Neighbours request timed out.", "neighbors_errorLoading": "Error loading neighbors: {error}", "@neighbors_errorLoading": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "neighbors_repeatersNeighbours": "Repeaters Neighbours", @@ -1111,13 +1281,17 @@ "neighbors_unknownContact": "Unknown {pubkey}", "@neighbors_unknownContact": { "placeholders": { - "pubkey": {"type": "String"} + "pubkey": { + "type": "String" + } } }, "neighbors_heardAgo": "Heard: {time} ago", "@neighbors_heardAgo": { "placeholders": { - "time": {"type": "String"} + "time": { + "type": "String" + } } }, "channelPath_title": "Packet Path", @@ -1129,28 +1303,40 @@ "channelPath_senderLabel": "Sender", "channelPath_timeLabel": "Time", "channelPath_repeatsLabel": "Repeats", - "channelPath_pathLabel": "Path", + "channelPath_pathLabel": "Path {index}", "channelPath_observedLabel": "Observed", "channelPath_observedPathTitle": "Observed path {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { - "index": {"type": "int"}, - "hops": {"type": "String"} + "index": { + "type": "int" + }, + "hops": { + "type": "String" + } } }, "channelPath_noLocationData": "No location data", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { - "day": {"type": "int"}, - "month": {"type": "int"}, - "time": {"type": "String"} + "day": { + "type": "int" + }, + "month": { + "type": "int" + }, + "time": { + "type": "String" + } } }, "channelPath_timeOnly": "{time}", "@channelPath_timeOnly": { "placeholders": { - "time": {"type": "String"} + "time": { + "type": "String" + } } }, "channelPath_unknownPath": "Unknown", @@ -1159,14 +1345,20 @@ "channelPath_observedZeroOf": "0 of {total} hops", "@channelPath_observedZeroOf": { "placeholders": { - "total": {"type": "int"} + "total": { + "type": "int" + } } }, "channelPath_observedSomeOf": "{observed} of {total} hops", "@channelPath_observedSomeOf": { "placeholders": { - "observed": {"type": "int"}, - "total": {"type": "int"} + "observed": { + "type": "int" + }, + "total": { + "type": "int" + } } }, "channelPath_mapTitle": "Path Map", @@ -1174,13 +1366,16 @@ "channelPath_primaryPath": "Path {index} (Primary)", "@channelPath_primaryPath": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, - "channelPath_pathLabel": "Path {index}", "@channelPath_pathLabel": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, "channelPath_pathLabelTitle": "Path", @@ -1188,13 +1383,16 @@ "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { - "label": {"type": "String"}, - "prefixes": {"type": "String"} + "label": { + "type": "String" + }, + "prefixes": { + "type": "String" + } } }, "channelPath_noHopDetailsAvailable": "No hop details available for this packet.", "channelPath_unknownRepeater": "Unknown Repeater", - "community_title": "Community", "community_create": "Create Community", "community_createDesc": "Create a new community and share via QR code.", @@ -1203,7 +1401,9 @@ "community_joinConfirmation": "Do you want to join the community \"{name}\"?", "@community_joinConfirmation": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_scanQr": "Scan Community QR", @@ -1216,20 +1416,26 @@ "community_created": "Community \"{name}\" created", "@community_created": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_joined": "Joined community \"{name}\"", "@community_joined": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_qrTitle": "Share Community", "community_qrInstructions": "Scan this QR code to join \"{name}\"", "@community_qrInstructions": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_hashtagPrivacyHint": "Community hashtag channels are only joinable by members of the community", @@ -1238,7 +1444,9 @@ "community_alreadyMemberMessage": "You are already a member of \"{name}\".", "@community_alreadyMemberMessage": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_addPublicChannel": "Add Community Public Channel", @@ -1250,46 +1458,60 @@ "community_deleteConfirm": "Leave \"{name}\"?", "@community_deleteConfirm": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_deleteChannelsWarning": "This will also delete {count} channel(s) and their messages.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Left community \"{name}\"", "@community_deleted": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_regenerateSecret": "Regenerate Secret", "community_regenerateSecretConfirm": "Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.", "@community_regenerateSecretConfirm": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_regenerate": "Regenerate", "community_secretRegenerated": "Secret regenerated for \"{name}\"", "@community_secretRegenerated": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_updateSecret": "Update Secret", "community_secretUpdated": "Secret updated for \"{name}\"", "@community_secretUpdated": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"", "@community_scanToUpdateSecret": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_addHashtagChannel": "Add Community Hashtag", @@ -1302,10 +1524,11 @@ "community_forCommunity": "For {name}", "@community_forCommunity": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, - "listFilter_tooltip": "Filter and sort", "listFilter_sortBy": "Sort by", "listFilter_latestMessages": "Latest messages", @@ -1318,7 +1541,6 @@ "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", "listFilter_newGroup": "New group", - "pathTrace_you": "You", "pathTrace_failed": "Path trace failed.", "pathTrace_notAvailable": "Path trace not available.", @@ -1335,52 +1557,59 @@ "contacts_pathTraceTo": "Trace route to {name}", "@contacts_pathTraceTo": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, - "contacts_clipboardEmpty": "Clipboard is empty.", "contacts_invalidAdvertFormat": "Invalid contact data", "contacts_contactImported": "Contact has been imported.", "contacts_contactImportFailed": "Failed to import contact.", - "contacts_zeroHopAdvert":"Zero Hop Advert", - "contacts_floodAdvert":"Flood Advert", - "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", - "contacts_addContactFromClipboard":"Add Contact from Clipboard", + "contacts_zeroHopAdvert": "Zero Hop Advert", + "contacts_floodAdvert": "Flood Advert", + "contacts_copyAdvertToClipboard": "Copy Advert to Clipboard", + "contacts_addContactFromClipboard": "Add Contact from Clipboard", "contacts_ShareContact": "Copy contact to Clipboard", "contacts_ShareContactZeroHop": "Share contact by advert", "contacts_zeroHopContactAdvertSent": "Sent contact by advert.", "contacts_zeroHopContactAdvertFailed": "Failed to send contact.", "contacts_contactAdvertCopied": "Advert copied to Clipboard.", "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", - "notification_activityTitle": "MeshCore Activity", "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", "@notification_messagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_channelMessagesCount": "{count} {count, plural, =1{channel message} other{channel messages}}", "@notification_channelMessagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newNodesCount": "{count} {count, plural, =1{new node} other{new nodes}}", "@notification_newNodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newTypeDiscovered": "New {contactType} discovered", "@notification_newTypeDiscovered": { "placeholders": { - "contactType": {"type": "String"} + "contactType": { + "type": "String" + } } }, "notification_receivedNewMessage": "Received new message", - "settings_gpxExportRepeaters": "Export repeaters / room server to GPX", "settings_gpxExportRepeatersSubtitle": "Exports repeaters / roomserver with a location to GPX file.", "settings_gpxExportContacts": "Export companions to GPX", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e6b22fa..4250a6f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Número de contactos", "settings_infoChannelCount": "Número de canales", "settings_presets": "Preajustes", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frecuencia (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Potencia (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)", - "settings_longRange": "Largo Alcance", - "settings_fastSpeed": "Velocidad Rápida", "settings_error": "Error: {message}", "@settings_error": { "placeholders": { @@ -1626,5 +1621,8 @@ "map_pathTraceCancelled": "Rastreo de ruta cancelado.", "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", "scanner_bluetoothOff": "Bluetooth está desactivado.", - "scanner_enableBluetooth": "Habilitar Bluetooth" + "scanner_enableBluetooth": "Habilitar Bluetooth", + "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", + "settings_clientRepeat": "Repetir sin conexión", + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f11238b..56d4a41 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Nombre de contacts", "settings_infoChannelCount": "Nombre de canaux", "settings_presets": "Préréglages", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Fréquence (MHz)", "settings_frequencyHelper": "300,0 - 2 500,0", "settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Puissance (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)", - "settings_longRange": "Portée Longue", - "settings_fastSpeed": "Vitesse Rapide", "settings_error": "Erreur : {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_runTrace": "Exécuter la traçage de chemin", "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", "scanner_bluetoothOff": "Le Bluetooth est désactivé.", - "scanner_enableBluetooth": "Activer le Bluetooth" + "scanner_enableBluetooth": "Activer le Bluetooth", + "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", + "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", + "settings_clientRepeat": "Répétition hors réseau" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 601f1af..239c765 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Numero contatti", "settings_infoChannelCount": "Numero Canale", "settings_presets": "Preset", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequenza (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Potenza (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)", - "settings_longRange": "Lungo Raggio", - "settings_fastSpeed": "Velocità Rapida", "settings_error": "Errore: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", - "scanner_enableBluetooth": "Abilita il Bluetooth" + "scanner_enableBluetooth": "Abilita il Bluetooth", + "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", + "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", + "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bc1cfbd..7235e90 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -748,24 +748,6 @@ abstract class AppLocalizations { /// **'Presets'** String get settings_presets; - /// No description provided for @settings_preset915Mhz. - /// - /// In en, this message translates to: - /// **'915 MHz'** - String get settings_preset915Mhz; - - /// No description provided for @settings_preset868Mhz. - /// - /// In en, this message translates to: - /// **'868 MHz'** - String get settings_preset868Mhz; - - /// No description provided for @settings_preset433Mhz. - /// - /// In en, this message translates to: - /// **'433 MHz'** - String get settings_preset433Mhz; - /// No description provided for @settings_frequency. /// /// In en, this message translates to: @@ -820,17 +802,23 @@ abstract class AppLocalizations { /// **'Invalid TX power (0-22 dBm)'** String get settings_txPowerInvalid; - /// No description provided for @settings_longRange. + /// No description provided for @settings_clientRepeat. /// /// In en, this message translates to: - /// **'Long Range'** - String get settings_longRange; + /// **'Off-Grid Repeat'** + String get settings_clientRepeat; - /// No description provided for @settings_fastSpeed. + /// No description provided for @settings_clientRepeatSubtitle. /// /// In en, this message translates to: - /// **'Fast Speed'** - String get settings_fastSpeed; + /// **'Allow this device to repeat mesh packets for others'** + String get settings_clientRepeatSubtitle; + + /// No description provided for @settings_clientRepeatFreqWarning. + /// + /// In en, this message translates to: + /// **'Off-grid repeat requires 433, 869, or 918 MHz frequency'** + String get settings_clientRepeatFreqWarning; /// No description provided for @settings_error. /// diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 695bde2..fdf9ec7 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -350,15 +350,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_presets => 'Предварителни настройки'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Честота (MHz)'; @@ -387,10 +378,15 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)'; @override - String get settings_longRange => 'Дълъг обхват'; + String get settings_clientRepeat => 'Без електричество – повторение'; @override - String get settings_fastSpeed => 'Бърза скорост'; + String get settings_clientRepeatSubtitle => + 'Позволете на това устройство да предава пакети към мрежата за други устройства.'; + + @override + String get settings_clientRepeatFreqWarning => + 'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 6e04655..c0fc4c8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -344,15 +344,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_presets => 'Voreinstellungen'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequenz (MHz)'; @@ -381,10 +372,15 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)'; @override - String get settings_longRange => 'Grosse Reichweite'; + String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss'; @override - String get settings_fastSpeed => 'Schnelle Geschwindigkeit'; + String get settings_clientRepeatSubtitle => + 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5ed8162..4f0bed1 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -342,15 +342,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_presets => 'Presets'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequency (MHz)'; @@ -379,10 +370,15 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)'; @override - String get settings_longRange => 'Long Range'; + String get settings_clientRepeat => 'Off-Grid Repeat'; @override - String get settings_fastSpeed => 'Fast Speed'; + String get settings_clientRepeatSubtitle => + 'Allow this device to repeat mesh packets for others'; + + @override + String get settings_clientRepeatFreqWarning => + 'Off-grid repeat requires 433, 869, or 918 MHz frequency'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ff4e8f3..f56e4e4 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -347,15 +347,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_presets => 'Preajustes'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frecuencia (MHz)'; @@ -384,10 +375,15 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)'; @override - String get settings_longRange => 'Largo Alcance'; + String get settings_clientRepeat => 'Repetir sin conexión'; @override - String get settings_fastSpeed => 'Velocidad Rápida'; + String get settings_clientRepeatSubtitle => + 'Permita que este dispositivo repita los paquetes de red para otros usuarios.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f8b7775..e1325da 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -348,15 +348,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_presets => 'Préréglages'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Fréquence (MHz)'; @@ -385,10 +376,15 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)'; @override - String get settings_longRange => 'Portée Longue'; + String get settings_clientRepeat => 'Répétition hors réseau'; @override - String get settings_fastSpeed => 'Vitesse Rapide'; + String get settings_clientRepeatSubtitle => + 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8fd612..15d5354 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -346,15 +346,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_presets => 'Preset'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequenza (MHz)'; @@ -383,10 +374,15 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)'; @override - String get settings_longRange => 'Lungo Raggio'; + String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"'; @override - String get settings_fastSpeed => 'Velocità Rapida'; + String get settings_clientRepeatSubtitle => + 'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index de6c909..17b3bce 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -344,15 +344,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_presets => 'Presets'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequentie (MHz)'; @@ -381,10 +372,15 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)'; @override - String get settings_longRange => 'Lange Afstand'; + String get settings_clientRepeat => 'Herhalen: Afgekoppeld'; @override - String get settings_fastSpeed => 'Hoge Snelheid'; + String get settings_clientRepeatSubtitle => + 'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index c5d2bd9..147e160 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -347,15 +347,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_presets => 'Preset'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Częstotliwość (MHz)'; @@ -385,10 +376,15 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)'; @override - String get settings_longRange => 'Długi zasięg'; + String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci'; @override - String get settings_fastSpeed => 'Szybka prędkość'; + String get settings_clientRepeatSubtitle => + 'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b5ffdd6..b481775 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -348,15 +348,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_presets => 'Presets'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequência (MHz)'; @@ -385,10 +376,15 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)'; @override - String get settings_longRange => 'Alcance Longo'; + String get settings_clientRepeat => 'Repetição sem rede'; @override - String get settings_fastSpeed => 'Velocidade Rápida'; + String get settings_clientRepeatSubtitle => + 'Permita que este dispositivo repita pacotes de rede para outros dispositivos.'; + + @override + String get settings_clientRepeatFreqWarning => + 'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c41bf20..e5875b1 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -345,15 +345,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_presets => 'Пресеты'; - @override - String get settings_preset915Mhz => '915 МГц'; - - @override - String get settings_preset868Mhz => '868 МГц'; - - @override - String get settings_preset433Mhz => '433 МГц'; - @override String get settings_frequency => 'Частота (МГц)'; @@ -383,10 +374,15 @@ class AppLocalizationsRu extends AppLocalizations { 'Недопустимая мощность передачи (0–22 дБм)'; @override - String get settings_longRange => 'Дальний радиус'; + String get settings_clientRepeat => 'Повторение \"вне сети\"'; @override - String get settings_fastSpeed => 'Высокая скорость'; + String get settings_clientRepeatSubtitle => + 'Позвольте этому устройству повторять пакеты данных для других устройств.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index e0ee455..4e8b4cb 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -344,15 +344,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_presets => 'Prednastavenia'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frekvencia (MHz)'; @@ -381,10 +372,15 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)'; @override - String get settings_longRange => 'Dlhý dosah'; + String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete'; @override - String get settings_fastSpeed => 'Rýchla rýchlosť'; + String get settings_clientRepeatSubtitle => + 'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 36445f7..e01151e 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -343,15 +343,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_presets => 'Prednastavitve'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frekvenca (MHz)'; @@ -380,10 +371,15 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; @override - String get settings_longRange => 'DDolg doseg'; + String get settings_clientRepeat => 'Neovadno ponavljanje'; @override - String get settings_fastSpeed => 'Visoka hitrost'; + String get settings_clientRepeatSubtitle => + 'Omogočite temu naprave, da ponavlja paketne sporočila za druge.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index cbfa45d..f081711 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -341,15 +341,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_presets => 'Fördefinierade inställningar'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frekvens (MHz)'; @@ -378,10 +369,15 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)'; @override - String get settings_longRange => 'Lång räckvidd'; + String get settings_clientRepeat => 'Upprepa utan elnät'; @override - String get settings_fastSpeed => 'Snabb hastighet'; + String get settings_clientRepeatSubtitle => + 'Låt enheten repetera nätpaket för andra användare.'; + + @override + String get settings_clientRepeatFreqWarning => + 'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 4dfa260..847c3e5 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -346,15 +346,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_presets => 'Попередні налаштування'; - @override - String get settings_preset915Mhz => '915 МГц'; - - @override - String get settings_preset868Mhz => '868 МГц'; - - @override - String get settings_preset433Mhz => '433 МГц'; - @override String get settings_frequency => 'Частота (МГц)'; @@ -383,10 +374,15 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)'; @override - String get settings_longRange => 'Дальній діапазон'; + String get settings_clientRepeat => 'Автономна система'; @override - String get settings_fastSpeed => 'Висока швидкість'; + String get settings_clientRepeatSubtitle => + 'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4441b22..fdc4531 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -331,15 +331,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_presets => '预设'; - @override - String get settings_preset915Mhz => '915 兆赫'; - - @override - String get settings_preset868Mhz => '868 兆赫'; - - @override - String get settings_preset433Mhz => '433 兆赫'; - @override String get settings_frequency => '频率 (MHz)'; @@ -368,10 +359,14 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; @override - String get settings_longRange => '远距离'; + String get settings_clientRepeat => '离网重复'; @override - String get settings_fastSpeed => '高速'; + String get settings_clientRepeatSubtitle => '允许此设备重复发送网状数据包给其他设备'; + + @override + String get settings_clientRepeatFreqWarning => + '离网重复通信需要使用 433、869 或 918 兆赫兹的频率。'; @override String settings_error(String message) { diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index b150d62..7c397b4 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Aantal Contacten", "settings_infoChannelCount": "Aantal Kanalen", "settings_presets": "Presets", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequentie (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Vermogen (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)", - "settings_longRange": "Lange Afstand", - "settings_fastSpeed": "Hoge Snelheid", "settings_error": "Fout: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_runTrace": "Padeshulp traceren", "scanner_enableBluetooth": "Activeer Bluetooth", "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", - "scanner_bluetoothOff": "Bluetooth is uitgeschakeld" + "scanner_bluetoothOff": "Bluetooth is uitgeschakeld", + "settings_clientRepeat": "Herhalen: Afgekoppeld", + "settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.", + "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d576fca..5ebeebf 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Liczba kontaktów", "settings_infoChannelCount": "Liczba kanałów", "settings_presets": "Preset", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Częstotliwość (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Moc (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)", - "settings_longRange": "Długi zasięg", - "settings_fastSpeed": "Szybka prędkość", "settings_error": "Błąd: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", "scanner_bluetoothOff": "Bluetooth jest wyłączony", - "scanner_enableBluetooth": "Włącz Bluetooth" + "scanner_enableBluetooth": "Włącz Bluetooth", + "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", + "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", + "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 53c43fe..a88e038 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Número de Contatos", "settings_infoChannelCount": "Número do Canal", "settings_presets": "Presets", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequência (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Potência (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)", - "settings_longRange": "Alcance Longo", - "settings_fastSpeed": "Velocidade Rápida", "settings_error": "Erro: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", "scanner_enableBluetooth": "Ative o Bluetooth", "scanner_bluetoothOff": "Bluetooth está desativado", - "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos." + "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", + "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", + "settings_clientRepeat": "Repetição sem rede", + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 19b4990..fc17eee 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -101,9 +101,6 @@ "settings_infoContactsCount": "Количество контактов", "settings_infoChannelCount": "Количество каналов", "settings_presets": "Пресеты", - "settings_preset915Mhz": "915 МГц", - "settings_preset868Mhz": "868 МГц", - "settings_preset433Mhz": "433 МГц", "settings_frequency": "Частота (МГц)", "settings_frequencyHelper": "300.0 – 2500.0", "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", @@ -113,8 +110,6 @@ "settings_txPower": "Мощность передачи (дБм)", "settings_txPowerHelper": "0 – 22", "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", - "settings_longRange": "Дальний радиус", - "settings_fastSpeed": "Высокая скорость", "settings_error": "Ошибка: {message}", "appSettings_title": "Настройки приложения", "appSettings_appearance": "Внешний вид", @@ -838,5 +833,8 @@ "map_runTrace": "Запустить трассировку пути", "scanner_enableBluetooth": "Включите Bluetooth", "scanner_bluetoothOff": "Bluetooth выключен", - "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства." + "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", + "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", + "settings_clientRepeat": "Повторение \"вне сети\"" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 3f61292..14cd3ec 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Počet kontaktov", "settings_infoChannelCount": "Počet kanálov", "settings_presets": "Prednastavenia", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frekvencia (MHz)", "settings_frequencyHelper": "300,0 – 2500,0", "settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Výkon (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)", - "settings_longRange": "Dlhý dosah", - "settings_fastSpeed": "Rýchla rýchlosť", "settings_error": "Chyba: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", "scanner_bluetoothOff": "Bluetooth je vypnutý", - "scanner_enableBluetooth": "Povolte Bluetooth" + "scanner_enableBluetooth": "Povolte Bluetooth", + "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", + "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", + "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index d94695e..e633965 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Število stikov", "settings_infoChannelCount": "Število kanalov", "settings_presets": "Prednastavitve", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frekvenca (MHz)", "settings_frequencyHelper": "300,00 - 2500,00", "settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Moč (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)", - "settings_longRange": "DDolg doseg", - "settings_fastSpeed": "Visoka hitrost", "settings_error": "Napaka: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", "scanner_enableBluetooth": "Omogočite Bluetooth", "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", - "scanner_bluetoothOff": "Bluetooth je izklopljen" + "scanner_bluetoothOff": "Bluetooth je izklopljen", + "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", + "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", + "settings_clientRepeat": "Neovadno ponavljanje" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 59b3fca..4e50409 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Kontakterantal", "settings_infoChannelCount": "Kanalantal", "settings_presets": "Fördefinierade inställningar", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frekvens (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX-effekt (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)", - "settings_longRange": "Lång räckvidd", - "settings_fastSpeed": "Snabb hastighet", "settings_error": "Fel: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_removeLast": "Ta bort sista", "scanner_enableBluetooth": "Aktivera Bluetooth", "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", - "scanner_bluetoothOff": "Bluetooth är avstängt" + "scanner_bluetoothOff": "Bluetooth är avstängt", + "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", + "settings_clientRepeat": "Upprepa utan elnät", + "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 26f3984..afa1179 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Кількість контактів", "settings_infoChannelCount": "Кількість каналів", "settings_presets": "Попередні налаштування", - "settings_preset915Mhz": "915 МГц", - "settings_preset868Mhz": "868 МГц", - "settings_preset433Mhz": "433 МГц", "settings_frequency": "Частота (МГц)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)", @@ -143,8 +140,6 @@ "settings_txPower": "Потужність TX (дБм)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)", - "settings_longRange": "Дальній діапазон", - "settings_fastSpeed": "Висока швидкість", "settings_error": "Помилка: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_pathTraceCancelled": "Відмінується трасування шляху", "scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", - "scanner_bluetoothOff": "Bluetooth вимкнено" + "scanner_bluetoothOff": "Bluetooth вимкнено", + "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", + "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", + "settings_clientRepeat": "Автономна система" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 7b4b3ab..312ed1a 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -136,9 +136,6 @@ "settings_infoContactsCount": "联系人数量", "settings_infoChannelCount": "通道数量", "settings_presets": "预设", - "settings_preset915Mhz": "915 兆赫", - "settings_preset868Mhz": "868 兆赫", - "settings_preset433Mhz": "433 兆赫", "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "无效频率(300-2500 MHz)", @@ -148,8 +145,6 @@ "settings_txPower": "TX 功率(dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", - "settings_longRange": "远距离", - "settings_fastSpeed": "高速", "settings_error": "[保存:{message}]\n错误:{message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_runTrace": "运行路径跟踪", "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", "scanner_bluetoothOff": "蓝牙已关闭", - "scanner_enableBluetooth": "启用蓝牙" + "scanner_enableBluetooth": "启用蓝牙", + "settings_clientRepeat": "离网重复", + "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", + "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。" } diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 20b7771..5125698 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -59,46 +59,29 @@ class RadioSettings { required this.txPowerDbm, }); - // Preset configurations - static RadioSettings get preset915MHz => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); - - static RadioSettings get preset868MHz => RadioSettings( - frequencyMHz: 868.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 14, - ); - - static RadioSettings get preset433MHz => RadioSettings( - frequencyMHz: 433.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); - - static RadioSettings get presetLongRange => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf12, - codingRate: LoRaCodingRate.cr4_8, - txPowerDbm: 20, - ); - - static RadioSettings get presetFastSpeed => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw500, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); + // Regional preset configurations + static final List<(String, RadioSettings)> presets = [ + ('Australia', RadioSettings(frequencyMHz: 915.8, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Australia (Narrow)', RadioSettings(frequencyMHz: 916.575, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Australia SA, WA, QLD', RadioSettings(frequencyMHz: 923.125, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Czech Republic', RadioSettings(frequencyMHz: 869.432, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('EU 433MHz', RadioSettings(frequencyMHz: 433.650, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('EU/UK (Long Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('EU/UK (Medium Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('EU/UK (Narrow)', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('New Zealand', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('New Zealand (Narrow)', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Portugal 433', RadioSettings(frequencyMHz: 433.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf9, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Portugal 869', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('Switzerland', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('USA Arizona', RadioSettings(frequencyMHz: 908.205, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('USA/Canada', RadioSettings(frequencyMHz: 910.525, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Vietnam', RadioSettings(frequencyMHz: 920.250, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + // Off-grid repeat presets (valid client_repeat frequencies) + ('Off-Grid 433', RadioSettings(frequencyMHz: 433.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Off-Grid 869', RadioSettings(frequencyMHz: 869.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('Off-Grid 918', RadioSettings(frequencyMHz: 918.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ]; int get frequencyHz => (frequencyMHz * 1000).round(); int get bandwidthHz => bandwidth.hz; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 73376f0..f131ecb 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -862,6 +862,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7; LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5; final _txPowerController = TextEditingController(text: '20'); + bool _clientRepeat = false; @override void initState() { @@ -911,6 +912,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { if (widget.connector.currentTxPower != null) { _txPowerController.text = widget.connector.currentTxPower.toString(); } + + _clientRepeat = widget.connector.clientRepeat ?? false; } @override @@ -960,9 +963,23 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { widget.connector.currentCr, ); + final supportsRepeat = + (widget.connector.firmwareVerCode ?? 0) >= 9; + + if (supportsRepeat) { + const validRepeatFreqsKHz = {433000, 869000, 918000}; + if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)), + ); + return; + } + } + try { await widget.connector.sendFrame( - buildSetRadioParamsFrame(freqHz, bwHz, sf, cr), + buildSetRadioParamsFrame(freqHz, bwHz, sf, cr, + clientRepeat: supportsRepeat ? _clientRepeat : null), ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.refreshDeviceInfo(); @@ -1001,37 +1018,25 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.settings_presets, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - _PresetChip( - label: l10n.settings_preset915Mhz, - onTap: () => _applyPreset(RadioSettings.preset915MHz), - ), - _PresetChip( - label: l10n.settings_preset868Mhz, - onTap: () => _applyPreset(RadioSettings.preset868MHz), - ), - _PresetChip( - label: l10n.settings_preset433Mhz, - onTap: () => _applyPreset(RadioSettings.preset433MHz), - ), - _PresetChip( - label: l10n.settings_longRange, - onTap: () => _applyPreset(RadioSettings.presetLongRange), - ), - _PresetChip( - label: l10n.settings_fastSpeed, - onTap: () => _applyPreset(RadioSettings.presetFastSpeed), - ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: l10n.settings_presets, + border: const OutlineInputBorder(), + ), + items: [ + for (var i = 0; i < RadioSettings.presets.length; i++) + DropdownMenuItem( + value: i, + child: Text(RadioSettings.presets[i].$1), + ), ], + onChanged: (index) { + if (index != null) { + _applyPreset(RadioSettings.presets[index].$2); + } + }, ), - const SizedBox(height: 24), + const SizedBox(height: 16), TextField( controller: _frequencyController, decoration: InputDecoration( @@ -1103,6 +1108,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ), keyboardType: TextInputType.number, ), + if ((widget.connector.firmwareVerCode ?? 0) >= 9) ...[ + const SizedBox(height: 16), + SwitchListTile( + title: Text(l10n.settings_clientRepeat), + subtitle: Text(l10n.settings_clientRepeatSubtitle), + value: _clientRepeat, + onChanged: (value) => setState(() => _clientRepeat = value), + contentPadding: EdgeInsets.zero, + ), + ], ], ), ), @@ -1117,14 +1132,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } } -class _PresetChip extends StatelessWidget { - final String label; - final VoidCallback onTap; - - const _PresetChip({required this.label, required this.onTap}); - - @override - Widget build(BuildContext context) { - return ActionChip(label: Text(label), onPressed: onTap); - } -} From 5fae2e5f73b14153b126230ed73940fe7c44b42a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 17 Feb 2026 23:50:11 -0700 Subject: [PATCH 107/421] fix formatting --- lib/connector/meshcore_protocol.dart | 9 +- lib/models/radio_settings.dart | 209 ++++++++++++++++++++++++--- lib/screens/settings_screen.dart | 13 +- 3 files changed, 205 insertions(+), 26 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0b78c65..ee83578 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -557,8 +557,13 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) { // sf: spreading factor (5-12) // cr: coding rate (5-8) // clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older) -Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr, - {bool? clientRepeat}) { +Uint8List buildSetRadioParamsFrame( + int freqHz, + int bwHz, + int sf, + int cr, { + bool? clientRepeat, +}) { final writer = BufferWriter(); writer.writeByte(cmdSetRadioParams); writer.writeUInt32LE(freqHz); diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 5125698..37ef3cc 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -61,26 +61,197 @@ class RadioSettings { // Regional preset configurations static final List<(String, RadioSettings)> presets = [ - ('Australia', RadioSettings(frequencyMHz: 915.8, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Australia (Narrow)', RadioSettings(frequencyMHz: 916.575, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Australia SA, WA, QLD', RadioSettings(frequencyMHz: 923.125, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Czech Republic', RadioSettings(frequencyMHz: 869.432, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('EU 433MHz', RadioSettings(frequencyMHz: 433.650, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('EU/UK (Long Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('EU/UK (Medium Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('EU/UK (Narrow)', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('New Zealand', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('New Zealand (Narrow)', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Portugal 433', RadioSettings(frequencyMHz: 433.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf9, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Portugal 869', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('Switzerland', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('USA Arizona', RadioSettings(frequencyMHz: 908.205, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('USA/Canada', RadioSettings(frequencyMHz: 910.525, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Vietnam', RadioSettings(frequencyMHz: 920.250, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ( + 'Australia', + RadioSettings( + frequencyMHz: 915.8, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf10, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Australia (Narrow)', + RadioSettings( + frequencyMHz: 916.575, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Australia SA, WA, QLD', + RadioSettings( + frequencyMHz: 923.125, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Czech Republic', + RadioSettings( + frequencyMHz: 869.432, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'EU 433MHz', + RadioSettings( + frequencyMHz: 433.650, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'EU/UK (Long Range)', + RadioSettings( + frequencyMHz: 869.525, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'EU/UK (Medium Range)', + RadioSettings( + frequencyMHz: 869.525, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf10, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'EU/UK (Narrow)', + RadioSettings( + frequencyMHz: 869.618, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'New Zealand', + RadioSettings( + frequencyMHz: 917.375, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'New Zealand (Narrow)', + RadioSettings( + frequencyMHz: 917.375, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Portugal 433', + RadioSettings( + frequencyMHz: 433.375, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf9, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Portugal 869', + RadioSettings( + frequencyMHz: 869.618, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'Switzerland', + RadioSettings( + frequencyMHz: 869.618, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'USA Arizona', + RadioSettings( + frequencyMHz: 908.205, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf10, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'USA/Canada', + RadioSettings( + frequencyMHz: 910.525, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Vietnam', + RadioSettings( + frequencyMHz: 920.250, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), // Off-grid repeat presets (valid client_repeat frequencies) - ('Off-Grid 433', RadioSettings(frequencyMHz: 433.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Off-Grid 869', RadioSettings(frequencyMHz: 869.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('Off-Grid 918', RadioSettings(frequencyMHz: 918.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ( + 'Off-Grid 433', + RadioSettings( + frequencyMHz: 433.0, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Off-Grid 869', + RadioSettings( + frequencyMHz: 869.0, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'Off-Grid 918', + RadioSettings( + frequencyMHz: 918.0, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), ]; int get frequencyHz => (frequencyMHz * 1000).round(); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f131ecb..12b79de 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -963,8 +963,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { widget.connector.currentCr, ); - final supportsRepeat = - (widget.connector.firmwareVerCode ?? 0) >= 9; + final supportsRepeat = (widget.connector.firmwareVerCode ?? 0) >= 9; if (supportsRepeat) { const validRepeatFreqsKHz = {433000, 869000, 918000}; @@ -978,8 +977,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { try { await widget.connector.sendFrame( - buildSetRadioParamsFrame(freqHz, bwHz, sf, cr, - clientRepeat: supportsRepeat ? _clientRepeat : null), + buildSetRadioParamsFrame( + freqHz, + bwHz, + sf, + cr, + clientRepeat: supportsRepeat ? _clientRepeat : null, + ), ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.refreshDeviceInfo(); @@ -1131,4 +1135,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ); } } - From 4239fb11edea2d26468be19d5efd99257e19ef7f Mon Sep 17 00:00:00 2001 From: zjs81 Date: Wed, 18 Feb 2026 00:07:08 -0700 Subject: [PATCH 108/421] Fix radio settings to only send repeat byte when the current state is known --- lib/screens/settings_screen.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 12b79de..94d541b 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -963,9 +963,11 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { widget.connector.currentCr, ); - final supportsRepeat = (widget.connector.firmwareVerCode ?? 0) >= 9; + // if the client repeat isnt null then we know its supported + //otherwise we leave it out of the frame to avoid accidentally enabling + final knownRepeat = widget.connector.clientRepeat != null; - if (supportsRepeat) { + if (knownRepeat) { const validRepeatFreqsKHz = {433000, 869000, 918000}; if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { ScaffoldMessenger.of(context).showSnackBar( @@ -982,7 +984,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { bwHz, sf, cr, - clientRepeat: supportsRepeat ? _clientRepeat : null, + clientRepeat: knownRepeat ? _clientRepeat : null, ), ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); @@ -1112,7 +1114,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ), keyboardType: TextInputType.number, ), - if ((widget.connector.firmwareVerCode ?? 0) >= 9) ...[ + if (widget.connector.clientRepeat != null) ...[ const SizedBox(height: 16), SwitchListTile( title: Text(l10n.settings_clientRepeat), From 1dc90d0e89f40bf297162dbf609b7afbd9a95ce3 Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 04:06:45 -0500 Subject: [PATCH 109/421] Add device protocol version tracking and error frame handling Port from meshcore-open: parse protocol version from byte 1 of device info frame, expose supportsFloodScope getter (version >= 8), handle respCodeErr frames with debug logging. Reset on disconnect. Co-authored-by: Cursor (cherry picked from commit a29bb9cdd7a02a85af26d94dd3c787cabd124629) --- lib/connector/meshcore_connector.dart | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 5cdab78..b24e5c2 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -105,6 +105,8 @@ class MeshCoreConnector extends ChangeNotifier { static const int _defaultMaxChannels = 8; int _maxContacts = _defaultMaxContacts; int _maxChannels = _defaultMaxChannels; + int? _deviceProtocolVersion; + String? _activeFloodScopeTag; bool _isSyncingQueuedMessages = false; bool _queuedMessageSyncInFlight = false; bool _didInitialQueueSync = false; @@ -208,6 +210,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; + int? get deviceProtocolVersion => _deviceProtocolVersion; + bool get supportsFloodScope => (_deviceProtocolVersion ?? 0) >= 8; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -927,6 +931,8 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; + _deviceProtocolVersion = null; + _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _didInitialQueueSync = false; @@ -1753,11 +1759,23 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); + break; + case respCodeErr: + _handleErrorFrame(frame); + break; default: debugPrint('Unknown frame code: $code'); } } + void _handleErrorFrame(Uint8List frame) { + final errCode = frame.length > 1 ? frame[1] : -1; + _appDebugLogService?.warn( + 'Firmware responded with error code: $errCode', + tag: 'Protocol', + ); + } + void _handlePathUpdated(Uint8List frame) { // Frame format: [0]=code, [1-32]=pub_key if (frame.length >= 33 && _pathHistoryService != null) { @@ -1827,12 +1845,12 @@ class MeshCoreConnector extends ChangeNotifier { void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; _firmwareVerCode = frame[1]; + _deviceProtocolVersion = frame[1]; // Parse client_repeat from firmware v9+ (byte 80) if (frame.length >= 81) { _clientRepeat = frame[80] != 0; } - // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; @@ -3240,6 +3258,8 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; + _deviceProtocolVersion = null; + _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _isSyncingChannels = false; From 8a804a370607854eff08af35e4012b39e99ea6e7 Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 12:30:00 -0500 Subject: [PATCH 110/421] Remove unused protocol placeholder field and unify version source --- lib/connector/meshcore_connector.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b24e5c2..1f423aa 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -105,8 +105,6 @@ class MeshCoreConnector extends ChangeNotifier { static const int _defaultMaxChannels = 8; int _maxContacts = _defaultMaxContacts; int _maxChannels = _defaultMaxChannels; - int? _deviceProtocolVersion; - String? _activeFloodScopeTag; bool _isSyncingQueuedMessages = false; bool _queuedMessageSyncInFlight = false; bool _didInitialQueueSync = false; @@ -210,8 +208,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; - int? get deviceProtocolVersion => _deviceProtocolVersion; - bool get supportsFloodScope => (_deviceProtocolVersion ?? 0) >= 8; + int? get deviceProtocolVersion => _firmwareVerCode; + bool get supportsFloodScope => (_firmwareVerCode ?? 0) >= 8; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -931,8 +929,6 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; - _deviceProtocolVersion = null; - _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _didInitialQueueSync = false; @@ -1845,7 +1841,6 @@ class MeshCoreConnector extends ChangeNotifier { void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; _firmwareVerCode = frame[1]; - _deviceProtocolVersion = frame[1]; // Parse client_repeat from firmware v9+ (byte 80) if (frame.length >= 81) { @@ -3258,8 +3253,6 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; - _deviceProtocolVersion = null; - _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _isSyncingChannels = false; From dc193be8ed80997f9ba200ea1864c28e1164d24a Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 12:45:02 -0500 Subject: [PATCH 111/421] Trim protocol PR to explicit RESP_CODE_ERR handling only --- lib/connector/meshcore_connector.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 1f423aa..de59f53 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -208,8 +208,6 @@ class MeshCoreConnector extends ChangeNotifier { int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; - int? get deviceProtocolVersion => _firmwareVerCode; - bool get supportsFloodScope => (_firmwareVerCode ?? 0) >= 8; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -1756,6 +1754,7 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeCustomVars: _handleCustomVars(frame); break; + // RESP_CODE_ERR is a defined firmware response (code 1), not an unknown frame. case respCodeErr: _handleErrorFrame(frame); break; From 50ab46ed40e485228ae34be05318d7af53f9be04 Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 12:45:41 -0500 Subject: [PATCH 112/421] Remove incidental whitespace-only diff from protocol PR --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index de59f53..f74d524 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1845,6 +1845,7 @@ class MeshCoreConnector extends ChangeNotifier { if (frame.length >= 81) { _clientRepeat = frame[80] != 0; } + // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; From 19edeab9d53c3479dc93d6bf6073725e4be56b9c Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 19 Feb 2026 11:17:58 -0800 Subject: [PATCH 113/421] add rbenv support --- .ruby-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..fcdb2e1 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +4.0.0 From 4bf2519559d29d9af85fc90afe69e858f488c045 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 19 Feb 2026 11:46:57 -0800 Subject: [PATCH 114/421] clear app db of channel messages on delete we were only deleting channels and messages on device and the in app db would persist this caused weird messages to later show up in other channels as they were deleted and added due to the fact we store messages by channel index(slot #) --- lib/screens/channels_screen.dart | 22 ++++++++++++++++++++-- pubspec.lock | 20 ++++++++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 6b8b92d..12dc534 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/storage/channel_message_store.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; @@ -104,6 +105,7 @@ class _ChannelsScreenState extends State @override Widget build(BuildContext context) { final connector = context.watch(); + final channelMessageStore = ChannelMessageStore(); // Auto-navigate back to scanner if disconnected if (!checkConnectionAndNavigate(connector)) { @@ -304,6 +306,7 @@ class _ChannelsScreenState extends State return _buildChannelTile( context, connector, + channelMessageStore, channel, showDragHandle: true, dragIndex: index, @@ -323,6 +326,7 @@ class _ChannelsScreenState extends State return _buildChannelTile( context, connector, + channelMessageStore, channel, ); }, @@ -352,6 +356,7 @@ class _ChannelsScreenState extends State Widget _buildChannelTile( BuildContext context, MeshCoreConnector connector, + ChannelMessageStore channelMessageStore, Channel channel, { bool showDragHandle = false, int? dragIndex, @@ -468,7 +473,12 @@ class _ChannelsScreenState extends State ); } }, - onLongPress: () => _showChannelActions(context, connector, channel), + onLongPress: () => _showChannelActions( + context, + connector, + channelMessageStore, + channel, + ), ), ); } @@ -476,6 +486,7 @@ class _ChannelsScreenState extends State void _showChannelActions( BuildContext context, MeshCoreConnector connector, + ChannelMessageStore channelMessageStore, Channel channel, ) { showModalBottomSheet( @@ -505,7 +516,12 @@ class _ChannelsScreenState extends State Navigator.pop(context); await Future.delayed(const Duration(milliseconds: 100)); if (context.mounted) { - _confirmDeleteChannel(context, connector, channel); + _confirmDeleteChannel( + context, + connector, + channelMessageStore, + channel, + ); } }, ), @@ -1451,6 +1467,7 @@ class _ChannelsScreenState extends State void _confirmDeleteChannel( BuildContext context, MeshCoreConnector connector, + ChannelMessageStore channelMessageStore, Channel channel, ) { showDialog( @@ -1469,6 +1486,7 @@ class _ChannelsScreenState extends State onPressed: () { Navigator.pop(dialogContext); connector.deleteChannel(channel.index); + channelMessageStore.clearChannelMessages(channel.index); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( diff --git a/pubspec.lock b/pubspec.lock index 09e9301..ed84c40 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -497,26 +497,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: From ba2763a3f61109f19e80a46f4760805b01128532 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Fri, 20 Feb 2026 01:28:13 -0500 Subject: [PATCH 115/421] fix(channels): make edit/delete actions use parent context after bottom sheet closes Root cause: edit/delete dialogs were opened from the sheet context after Navigator.pop, so context.mounted was false and follow-up actions never ran. Also keeps async await/error handling for channel edit/delete so failures surface to users instead of silently dropping. --- lib/screens/channels_screen.dart | 70 +++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 6b8b92d..30c52f1 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -478,9 +478,10 @@ class _ChannelsScreenState extends State MeshCoreConnector connector, Channel channel, ) { + final parentContext = context; showModalBottomSheet( - context: context, - builder: (context) => SafeArea( + context: parentContext, + builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -488,10 +489,10 @@ class _ChannelsScreenState extends State leading: const Icon(Icons.edit_outlined), title: Text(context.l10n.channels_editChannel), onTap: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); await Future.delayed(const Duration(milliseconds: 100)); - if (context.mounted) { - _showEditChannelDialog(context, connector, channel); + if (parentContext.mounted) { + _showEditChannelDialog(parentContext, connector, channel); } }, ), @@ -502,10 +503,10 @@ class _ChannelsScreenState extends State style: const TextStyle(color: Colors.red), ), onTap: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); await Future.delayed(const Duration(milliseconds: 100)); - if (context.mounted) { - _confirmDeleteChannel(context, connector, channel); + if (parentContext.mounted) { + _confirmDeleteChannel(parentContext, connector, channel); } }, ), @@ -1415,7 +1416,7 @@ class _ChannelsScreenState extends State child: Text(dialogContext.l10n.common_cancel), ), FilledButton( - onPressed: () { + onPressed: () async { final name = nameController.text.trim(); final pskHex = pskController.text.trim(); @@ -1432,13 +1433,25 @@ class _ChannelsScreenState extends State } Navigator.pop(dialogContext); - connector.setChannel(channel.index, name, psk); - connector.setChannelSmazEnabled(channel.index, smazEnabled); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.channels_channelUpdated(name)), - ), - ); + try { + await connector.setChannel(channel.index, name, psk); + await connector.setChannelSmazEnabled( + channel.index, + smazEnabled, + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.channels_channelUpdated(name)), + ), + ); + } catch (e, st) { + debugPrint(st.toString()); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update channel: $e')), + ); + } }, child: Text(dialogContext.l10n.common_save), ), @@ -1466,16 +1479,25 @@ class _ChannelsScreenState extends State child: Text(dialogContext.l10n.common_cancel), ), TextButton( - onPressed: () { + onPressed: () async { Navigator.pop(dialogContext); - connector.deleteChannel(channel.index); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelDeleted(channel.name), + try { + await connector.deleteChannel(channel.index); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.channels_channelDeleted(channel.name), + ), ), - ), - ); + ); + } catch (e, st) { + debugPrint(st.toString()); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to delete channel: $e')), + ); + } }, child: Text( dialogContext.l10n.common_delete, From d2b693e5ce7021c27b7061c7408a4278e82064cb Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Fri, 20 Feb 2026 20:27:38 -0800 Subject: [PATCH 116/421] Add a signal readout for the nearest repeater. With improvements to app bar and other UI polish. (#200) * Refactor Cayenne LPP parsing with error handling and logging - Added error handling and logging to the Cayenne LPP parsing methods to manage malformed data gracefully. - Improved the structure of the parsing logic for better readability and maintainability. - Updated the Contact model to include error handling during frame parsing. - Refactored Channels, Contacts, Map, and Neighbours screens to utilize a new AppBarTitle widget for consistent app bar design. - Enhanced the BatteryIndicator widget to display SNR information for direct repeaters. - Introduced SNRUi class for better management of SNR icon and text representation. - Improved error handling in PathTraceMap and Neighbours screens to log errors appropriately. * Fix trace route bytes generation logic in Contact model * Ignore advertisements from self in MeshCoreConnector * Refactor PathTraceData to use List for snrData and adjust data mapping in PathTraceMapScreen * Add SNRIndicator to AppBar and refactor BatteryIndicator layout * Enhance path management dialog to display direct repeaters with color coding based on signal strength * Remove unused import from SNR indicator widget * Update lib/models/contact.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/path_trace_map.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/widgets/battery_indicator.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/helpers/cayenne_lpp.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor packet handling to skip only the RSSI byte for improved reliability * Add SNR indicator localization and update UI references for nearby repeaters * Handle loading state and error parsing in PathTraceMapScreen; update SNR indicator dialog content layout * Throw an exception for unsupported LPP types in CayenneLpp class * Refactor AppBarTitle widget to remove unused style parameter; update related screens to reflect changes Improve SNR handling by adding validation for spreading factor range in snrUiFromSNR function Update contact handling in MeshCoreConnector to fix variable naming and improve readability Stop parsing unsupported LPP types in CayenneLpp to avoid misalignment * Sort direct repeaters by last updated time and SNR; limit to top three for improved path management dialog * Prevent notifications for chat and sensor adverts without a valid path * Implement ranking system for direct repeaters based on SNR and recency; update related UI components to reflect changes * Refactor localization keys for "neighbors" terminology across multiple languages - Updated localization keys from "neighbours" to "neighbors" in the following files: - app_localizations_bg.dart - app_localizations_de.dart - app_localizations_en.dart - app_localizations_es.dart - app_localizations_fr.dart - app_localizations_it.dart - app_localizations_nl.dart - app_localizations_pl.dart - app_localizations_pt.dart - app_localizations_ru.dart - app_localizations_sk.dart - app_localizations_sl.dart - app_localizations_sv.dart - app_localizations_uk.dart - app_localizations_zh.dart - Updated corresponding ARB files to reflect the changes in keys. - Renamed the NeighboursScreen to NeighborsScreen in the chat and repeater hub screens for consistency. * Adjust ranking calculation for direct repeaters by adding offset to SNR for improved accuracy * Fix typo in variable name for second direct repeater in path management dialog * Refactor ranking calculation for direct repeaters and update path handling in channel message screens * Refactor path handling in ChannelMessagePathScreen to improve logic for outgoing messages and channel messages * Fix AppBarTitle horizontal overflow with long titles (#187) * Initial plan * Wrap title Column in Expanded to prevent horizontal overflow in AppBarTitle Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com> * Refactor AppBarTitle widget to simplify Text widget initialization * Add "Show All Paths" feature to chat path management - Implemented localization for "Show All Paths" in multiple languages (DE, EN, ES, FR, IT, NL, PL, PT, RU, SK, SL, SV, UK, ZH). - Updated path management dialog to include a toggle for showing all paths. - Refactored path history display logic to conditionally show paths based on the toggle state. - Cleaned up unused print statements and improved code readability in path tracing and chat screens. * Refactor FeatureToggleRow visibility in chat and path management dialogs based on repeaters list * Remove unused import of 'dart:ffi' in path_trace_map.dart * Refactor repeater management logic and update UI state handling in chat and path management dialogs * Refactor RX data handling and improve repeater management logic in chat and path management dialogs --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 309 ++++++++++- lib/connector/meshcore_protocol.dart | 56 +- lib/helpers/cayenne_lpp.dart | 336 +++++------ lib/l10n/app_bg.arb | 9 +- lib/l10n/app_de.arb | 9 +- lib/l10n/app_en.arb | 17 +- lib/l10n/app_es.arb | 9 +- lib/l10n/app_fr.arb | 9 +- lib/l10n/app_it.arb | 9 +- lib/l10n/app_localizations.dart | 38 +- lib/l10n/app_localizations_bg.dart | 15 +- lib/l10n/app_localizations_de.dart | 15 +- lib/l10n/app_localizations_en.dart | 21 +- lib/l10n/app_localizations_es.dart | 15 +- lib/l10n/app_localizations_fr.dart | 16 +- lib/l10n/app_localizations_it.dart | 15 +- lib/l10n/app_localizations_nl.dart | 15 +- lib/l10n/app_localizations_pl.dart | 15 +- lib/l10n/app_localizations_pt.dart | 16 +- lib/l10n/app_localizations_ru.dart | 15 +- lib/l10n/app_localizations_sk.dart | 15 +- lib/l10n/app_localizations_sl.dart | 15 +- lib/l10n/app_localizations_sv.dart | 15 +- lib/l10n/app_localizations_uk.dart | 15 +- lib/l10n/app_localizations_zh.dart | 15 +- lib/l10n/app_nl.arb | 9 +- lib/l10n/app_pl.arb | 9 +- lib/l10n/app_pt.arb | 9 +- lib/l10n/app_ru.arb | 9 +- lib/l10n/app_sk.arb | 9 +- lib/l10n/app_sl.arb | 9 +- lib/l10n/app_sv.arb | 9 +- lib/l10n/app_uk.arb | 9 +- lib/l10n/app_zh.arb | 9 +- lib/models/contact.dart | 72 +-- lib/screens/channel_chat_screen.dart | 3 +- lib/screens/channel_message_path_screen.dart | 58 +- lib/screens/channels_screen.dart | 5 +- lib/screens/chat_screen.dart | 521 ++++++++++-------- lib/screens/contacts_screen.dart | 142 ++--- lib/screens/map_screen.dart | 15 +- ...ours_screen.dart => neighbors_screen.dart} | 170 +++--- lib/screens/path_trace_map.dart | 216 +++++--- lib/screens/repeater_hub_screen.dart | 12 +- lib/widgets/app_bar.dart | 48 ++ lib/widgets/battery_indicator.dart | 30 +- lib/widgets/path_management_dialog.dart | 242 +++++--- lib/widgets/snr_indicator.dart | 205 +++++-- pubspec.lock | 16 +- 49 files changed, 1956 insertions(+), 914 deletions(-) rename lib/screens/{neighbours_screen.dart => neighbors_screen.dart} (75%) create mode 100644 lib/widgets/app_bar.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index f74d524..9b256e2 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -37,6 +37,42 @@ class MeshCoreUuids { 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; + double snr; + DateTime lastUpdated; + + DirectRepeater({ + required this.pubkeyFirstByte, + required this.snr, + DateTime? lastUpdated, + }) : lastUpdated = lastUpdated ?? DateTime.now(); + + void update(double newSNR) { + snr = newSNR; + lastUpdated = DateTime.now(); + } + + int get ranking { + if (isStale()) { + return -1; // Stale repeaters get lowest rank + } + // Higher SNR gets higher rank and recency within maxAgeMinutes breaks ties. + final ageMs = + DateTime.now().millisecondsSinceEpoch - + lastUpdated.millisecondsSinceEpoch; + final maxAgeMs = maxAgeMinutes * 60 * 1000; + final recencyScore = (maxAgeMs - ageMs).clamp(0, maxAgeMs); + return ((snr - 31.75) * 1000).round() + recencyScore; + } + + bool isStale() { + return DateTime.now().difference(lastUpdated) > + const Duration(minutes: maxAgeMinutes); + } +} + enum MeshCoreConnectionState { disconnected, scanning, @@ -95,6 +131,7 @@ class MeshCoreConnector extends ChangeNotifier { int? _batteryMillivolts; double? _selfLatitude; double? _selfLongitude; + final List _directRepeaters = List.empty(growable: true); bool _isLoadingContacts = false; bool _isLoadingChannels = false; bool _hasLoadedChannels = false; @@ -196,6 +233,7 @@ class MeshCoreConnector extends ChangeNotifier { String? get selfName => _selfName; double? get selfLatitude => _selfLatitude; double? get selfLongitude => _selfLongitude; + List get directRepeaters => _directRepeaters; int? get currentTxPower => _currentTxPower; int? get maxTxPower => _maxTxPower; int? get currentFreqHz => _currentFreqHz; @@ -1696,6 +1734,11 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; + case pushCodeNewAdvert: + debugPrint('Got New CONTACT'); + // It's the same format as respCodeContact, so we can reuse the handler + _handleContact(frame); + break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1740,6 +1783,7 @@ class MeshCoreConnector extends ChangeNotifier { case pushCodeStatusResponse: break; case pushCodeLogRxData: + _handleRxData(frame); _handleLogRxData(frame); break; case respCodeChannelInfo: @@ -2028,6 +2072,80 @@ class MeshCoreConnector extends ChangeNotifier { } } + void _handleContactAdvert(Contact contact) { + if (listEquals(contact.publicKey, _selfPublicKey)) { + return; + } + + if (contact.type == advTypeRepeater) { + _contactUnreadCount.remove(contact.publicKeyHex); + _unreadStore.saveContactUnreadCount( + Map.from(_contactUnreadCount), + ); + } + // Check if this is a new contact + final isNewContact = !_knownContactKeys.contains(contact.publicKeyHex); + final existingIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + + if (existingIndex >= 0) { + final existing = _contacts[existingIndex]; + final mergedLastMessageAt = + existing.lastMessageAt.isAfter(contact.lastMessageAt) + ? existing.lastMessageAt + : contact.lastMessageAt; + + appLogger.info( + 'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}', + tag: 'Connector', + ); + + // CRITICAL: Preserve user's path override when contact is refreshed from device + _contacts[existingIndex] = contact.copyWith( + lastMessageAt: mergedLastMessageAt, + pathOverride: existing.pathOverride, // Preserve user's path choice + pathOverrideBytes: existing.pathOverrideBytes, + ); + + appLogger.info( + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + tag: 'Connector', + ); + } else { + _contacts.add(contact); + appLogger.info( + 'Added new contact ${contact.name}: pathLen=${contact.pathLength}', + tag: 'Connector', + ); + } + _knownContactKeys.add(contact.publicKeyHex); + _loadMessagesForContact(contact.publicKeyHex); + + // Add path to history if we have a valid path + if (_pathHistoryService != null && contact.pathLength >= 0) { + _pathHistoryService!.handlePathUpdated(contact); + } + + notifyListeners(); + + // Show notification for new contact (advertisement) + if (isNewContact && _appSettingsService != null) { + final settings = _appSettingsService!.settings; + if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { + _notificationService.showAdvertNotification( + contactName: contact.name, + contactType: contact.typeLabel, + contactId: contact.publicKeyHex, + ); + } + } + + if (!_isLoadingContacts) { + unawaited(_persistContacts()); + } + } + Future _persistContacts() async { await _contactStore.saveContacts(_contacts); } @@ -3287,7 +3405,11 @@ class MeshCoreConnector extends ChangeNotifier { void _handleCustomVars(Uint8List frame) { final buf = BufferReader(frame.sublist(1)); - _currentCustomVars = _parseKeyValueString(buf.readString()); + try { + _currentCustomVars = _parseKeyValueString(buf.readString()); + } catch (e) { + appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector'); + } } void _setState(MeshCoreConnectionState newState) { @@ -3311,6 +3433,191 @@ class MeshCoreConnector extends ChangeNotifier { super.dispose(); } + + void _handleRxData(Uint8List frame) { + final packet = BufferReader(frame); + double snr = 0.0; + int routeType = 0; + int payloadType = 0; + Uint8List pathBytes = Uint8List(0); + Uint8List payload = Uint8List(0); + try { + packet.skipBytes(1); // Skip frame type byte + snr = packet.readInt8() / 4.0; + packet.skipBytes(1); // Skip RSSI byte + //final rssi = packet.readByte(); + final header = packet.readByte(); + routeType = header & 0x03; + payloadType = (header >> 2) & 0x0F; + //final payloadVer = (header >> 6) & 0x03; + final pathLen = packet.readByte(); + pathBytes = packet.readBytes(pathLen); + payload = packet.readBytes(packet.remaining); + } catch (e) { + appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); + return; + } + + switch (payloadType) { + case payloadTypeADVERT: + _handlePayloadAdvertReceived(payload, pathBytes, routeType, snr); + break; + default: + } + } + + void _handlePayloadAdvertReceived( + Uint8List frame, + Uint8List path, + int routeType, + double snr, + ) { + final advert = BufferReader(frame); + double latitude = 0.0; + double longitude = 0.0; + String name = ''; + String contactKeyHex = ''; + Uint8List publicKey = Uint8List(0); + int type = 0; + int timestamp = 0; + bool hasLocation = false; + bool hasName = false; + try { + publicKey = advert.readBytes(32); + contactKeyHex = publicKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); + + timestamp = advert.readInt32LE(); + //TODO add signature verification + advert.skipBytes(64); // Skip signature for now + final flags = advert.readByte(); + type = flags & 0x0F; + hasLocation = (flags & 0x10) != 0; + // For future use: + //final hasFeature1 = (flags & 0x20) != 0; + //final hasFeature2 = (flags & 0x40) != 0; + hasName = (flags & 0x80) != 0; + if (hasLocation && advert.remaining >= 8) { + latitude = advert.readInt32LE() / 1e6; + longitude = advert.readInt32LE() / 1e6; + } + if (hasName && advert.remaining > 0) { + name = advert.readString(); + } + } catch (e) { + appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); + return; + } + + if (listEquals(publicKey, _selfPublicKey)) { + return; + } + + // Check if this is a new contact + final isNewContact = !_knownContactKeys.contains(contactKeyHex); + + if (isNewContact) { + final newContact = Contact( + publicKey: publicKey, + name: name, + type: type, + pathLength: path.length, + path: Uint8List.fromList( + path.reversed.toList(), + ), // Store path in reverse for easier use in outgoing messages + latitude: latitude, + longitude: longitude, + lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + ); + _handleContactAdvert(newContact); + _updateDirectRepeater(newContact, snr, path); + return; + } + + final existingIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == contactKeyHex, + ); + + if (existingIndex >= 0) { + final existing = _contacts[existingIndex]; + final mergedLastMessageAt = existing.lastMessageAt.isAfter(DateTime.now()) + ? DateTime.now() + : existing.lastMessageAt; + + appLogger.info( + 'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}', + tag: 'Connector', + ); + + // CRITICAL: Preserve user's path override when contact is refreshed from device + _contacts[existingIndex] = existing.copyWith( + latitude: hasLocation ? latitude : existing.latitude, + longitude: hasLocation ? longitude : existing.longitude, + name: hasName ? name : existing.name, + path: Uint8List.fromList(path.reversed.toList()), + pathLength: path.length, + lastMessageAt: mergedLastMessageAt, + lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + pathOverride: existing.pathOverride, // Preserve user's path choice + pathOverrideBytes: existing.pathOverrideBytes, + ); + + // Add path to history if we have a valid path + if (_pathHistoryService != null && + _contacts[existingIndex].pathLength >= 0) { + _pathHistoryService!.handlePathUpdated(_contacts[existingIndex]); + } + + _updateDirectRepeater(_contacts[existingIndex], snr, path); + + appLogger.info( + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + tag: 'Connector', + ); + } + } + + void _updateDirectRepeater(Contact contact, double snr, Uint8List path) { + final pubkeyFirstByte = path.isNotEmpty + ? path.last + : contact.publicKey.first; + + _directRepeaters.removeWhere((r) => r.isStale()); + + //We can use adverts from chat and sensor nodes, but only if the advert has a path to get the last hop. + if ((contact.type == advTypeChat || contact.type == advTypeSensor) && + path.isEmpty) { + notifyListeners(); + return; + } + + final isTracked = _directRepeaters.where( + (r) => r.pubkeyFirstByte == pubkeyFirstByte, + ); + + final sortedRepeaters = List.from(_directRepeaters) + ..sort((a, b) => b.snr.compareTo(a.snr)); + final weakestRepeater = sortedRepeaters.isNotEmpty + ? sortedRepeaters.last + : null; + + if (_directRepeaters.length >= 5 && + weakestRepeater != null && + isTracked.isEmpty) { + _directRepeaters.remove(weakestRepeater); + } + + if (isTracked.isNotEmpty) { + final repeater = isTracked.first; + repeater.update(snr); + } else if (_directRepeaters.length < 5) { + _directRepeaters.add( + DirectRepeater(pubkeyFirstByte: pubkeyFirstByte, snr: snr), + ); + } + notifyListeners(); + } } const int _phRouteMask = 0x03; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index ee83578..2933e80 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -13,12 +13,22 @@ class BufferReader { int readByte() => readBytes(1)[0]; Uint8List readBytes(int count) { + if (_pointer + count > _buffer.length) { + throw RangeError( + 'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}', + ); + } final data = _buffer.sublist(_pointer, _pointer + count); _pointer += count; return data; } void skipBytes(int count) { + if (_pointer + count > _buffer.length) { + throw RangeError( + 'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}', + ); + } _pointer += count; } @@ -151,6 +161,7 @@ const int cmdGetContactByKey = 30; const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; +const int cmdSetOtherParams = 38; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; @@ -166,7 +177,7 @@ const int reqTypeGetStatus = 0x01; const int reqTypeKeepAlive = 0x02; const int reqTypeGetTelemetry = 0x03; const int reqTypeGetAccessList = 0x05; -const int reqTypeGetNeighbours = 0x06; +const int reqTypeGetNeighbors = 0x06; // Repeater response codes const int respServerLoginOk = 0; @@ -212,6 +223,30 @@ const int advTypeRepeater = 2; const int advTypeRoom = 3; const int advTypeSensor = 4; +// Payload Types +const int payloadTypeREQ = + 0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) +const int payloadTypeRESPONSE = + 0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) +const int payloadTypeTXTMSG = + 0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text) +const int payloadTypeACK = 0x03; // a simple ack +const int payloadTypeADVERT = 0x04; // a node advertising its Identity +const int payloadTypeGRPTXT = + 0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg") +const int payloadTypeGRPDATA = + 0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob) +const int payloadTypeANONREQ = + 0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...) +const int payloadTypePATH = + 0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) +const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop +const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets +const int payloadTypeCONTROL = 0x0B; // a control/discovery packet +//... +const int payloadTypeRawCustom = + 0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc + // Sizes const int pubKeySize = 32; const int maxPathSize = 64; @@ -788,3 +823,22 @@ Uint8List buildZeroHopContact(Uint8List pubKey) { writer.writeBytes(pubKey); return writer.toBytes(); } + +// Build CMD_SET_OTHER_PARAMS frame +// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks] +Uint8List buildSetOtherParamsFrame( + bool allowAutoAddContacts, + int allowTelemetryFlags, + int advertLocationPolicy, + int multiAcks, +) { + final writer = BufferWriter(); + writer.writeByte(cmdSetOtherParams); + writer.writeByte( + allowAutoAddContacts ? 0x00 : 0x01, + ); // Allow Auto Add Contacts + writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags + writer.writeByte(advertLocationPolicy); // Advertisement Location Policy + writer.writeByte(multiAcks); // Multi Acknowledgements + return writer.toBytes(); +} diff --git a/lib/helpers/cayenne_lpp.dart b/lib/helpers/cayenne_lpp.dart index bf9b8e7..07909e6 100644 --- a/lib/helpers/cayenne_lpp.dart +++ b/lib/helpers/cayenne_lpp.dart @@ -1,4 +1,6 @@ import 'dart:typed_data'; +import 'package:meshcore_open/utils/app_logger.dart'; + import '../connector/meshcore_protocol.dart'; class CayenneLpp { @@ -84,180 +86,192 @@ class CayenneLpp { static List> parse(Uint8List bytes) { final buffer = BufferReader(bytes); final telemetry = >[]; + try { + while (buffer.remaining >= 2) { + final channel = buffer.readUInt8(); + final type = buffer.readUInt8(); - while (buffer.remaining >= 2) { - final channel = buffer.readUInt8(); - final type = buffer.readUInt8(); + if (channel == 0 && type == 0) { + break; + } - if (channel == 0 && type == 0) { - break; - } - - switch (type) { - case lppGenericSensor: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt32BE(), - }); - break; - case lppLuminosity: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE(), - }); - break; - case lppPresence: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt8(), - }); - break; - case lppTemperature: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readInt16BE() / 10, - }); - break; - case lppRelativeHumidity: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt8() / 2, - }); - break; - case lppBarometricPressure: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE() / 10, - }); - break; - case lppVoltage: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readInt16BE() / 100, - }); - break; - case lppCurrent: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readInt16BE() / 1000, - }); - break; - case lppPercentage: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt8(), - }); - break; - case lppConcentration: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE(), - }); - break; - case lppPower: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE(), - }); - break; - case lppGps: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': { - 'latitude': buffer.readInt24BE() / 10000, - 'longitude': buffer.readInt24BE() / 10000, - 'altitude': buffer.readInt24BE() / 100, - }, - }); - break; - default: - return telemetry; + switch (type) { + case lppGenericSensor: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt32BE(), + }); + break; + case lppLuminosity: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE(), + }); + break; + case lppPresence: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt8(), + }); + break; + case lppTemperature: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readInt16BE() / 10, + }); + break; + case lppRelativeHumidity: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt8() / 2, + }); + break; + case lppBarometricPressure: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE() / 10, + }); + break; + case lppVoltage: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readInt16BE() / 100, + }); + break; + case lppCurrent: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readInt16BE() / 1000, + }); + break; + case lppPercentage: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt8(), + }); + break; + case lppConcentration: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE(), + }); + break; + case lppPower: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE(), + }); + break; + case lppGps: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': { + 'latitude': buffer.readInt24BE() / 10000, + 'longitude': buffer.readInt24BE() / 10000, + 'altitude': buffer.readInt24BE() / 100, + }, + }); + break; + default: + return telemetry; + } } + return telemetry; + } catch (e) { + // Handle parsing errors, possibly due to malformed data + appLogger.error('Error parsing Cayenne LPP data: $e'); + // Return any telemetry parsed so far to preserve partial data + return telemetry; } - - return telemetry; } static List> parseByChannel(Uint8List bytes) { final buffer = BufferReader(bytes); final Map> channels = {}; + try { + while (buffer.remaining >= 2) { + final channel = buffer.readUInt8(); + final type = buffer.readUInt8(); - while (buffer.remaining >= 2) { - final channel = buffer.readUInt8(); - final type = buffer.readUInt8(); + // Optional: stop on padding (00 00) + if (channel == 0 && type == 0) { + break; + } - // Optional: stop on padding (00 00) - if (channel == 0 && type == 0) { - break; + final channelData = channels.putIfAbsent( + channel, + () => {'channel': channel, 'values': {}}, + ); + + switch (type) { + case lppGenericSensor: + channelData['values']['generic'] = buffer.readUInt32BE(); + break; + case lppLuminosity: + channelData['values']['luminosity'] = buffer.readUInt16BE(); + break; + case lppPresence: + channelData['values']['presence'] = buffer.readUInt8() != 0; + break; + case lppTemperature: + channelData['values']['temperature'] = buffer.readInt16BE() / 10.0; + break; + case lppRelativeHumidity: + channelData['values']['humidity'] = buffer.readUInt8() / 2.0; + break; + case lppBarometricPressure: + channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0; + break; + case lppVoltage: + channelData['values']['voltage'] = buffer.readInt16BE() / 100.0; + break; + case lppCurrent: + channelData['values']['current'] = buffer.readInt16BE() / 1000.0; + break; + case lppPercentage: + channelData['values']['percentage'] = buffer.readUInt8(); + break; + case lppConcentration: + channelData['values']['concentration'] = buffer.readUInt16BE(); + break; + case lppPower: + channelData['values']['power'] = buffer.readUInt16BE(); + break; + case lppGps: + channelData['values']['gps'] = { + 'latitude': buffer.readInt24BE() / 10000.0, + 'longitude': buffer.readInt24BE() / 10000.0, + 'altitude': buffer.readInt24BE() / 100.0, + }; + break; + // Add more types as needed... + default: + //Stopped parsing to avoid misalignment + return channels.values.toList(); + } } - final channelData = channels.putIfAbsent( - channel, - () => {'channel': channel, 'values': {}}, - ); - - switch (type) { - case lppGenericSensor: - channelData['values']['generic'] = buffer.readUInt32BE(); - break; - case lppLuminosity: - channelData['values']['luminosity'] = buffer.readUInt16BE(); - break; - case lppPresence: - channelData['values']['presence'] = buffer.readUInt8() != 0; - break; - case lppTemperature: - channelData['values']['temperature'] = buffer.readInt16BE() / 10.0; - break; - case lppRelativeHumidity: - channelData['values']['humidity'] = buffer.readUInt8() / 2.0; - break; - case lppBarometricPressure: - channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0; - break; - case lppVoltage: - channelData['values']['voltage'] = buffer.readInt16BE() / 100.0; - break; - case lppCurrent: - channelData['values']['current'] = buffer.readInt16BE() / 1000.0; - break; - case lppPercentage: - channelData['values']['percentage'] = buffer.readUInt8(); - break; - case lppConcentration: - channelData['values']['concentration'] = buffer.readUInt16BE(); - break; - case lppPower: - channelData['values']['power'] = buffer.readUInt16BE(); - break; - case lppGps: - channelData['values']['gps'] = { - 'latitude': buffer.readInt24BE() / 10000.0, - 'longitude': buffer.readInt24BE() / 10000.0, - 'altitude': buffer.readInt24BE() / 100.0, - }; - break; - // Add more types as needed... - default: - // Unknown type: skip or handle error? - continue; - } + final List> channelsOut = channels.values.toList(); + channelsOut.sort((a, b) => a['channel'].compareTo(b['channel'])); + return channelsOut; + } catch (e) { + // Handle parsing errors, possibly due to malformed data + appLogger.error('Error parsing Cayenne LPP data: $e'); + return < + Map + >[]; // Return an empty list on error to avoid crashing the app } - - final List> channelsOut = channels.values.toList(); - channelsOut.sort((a, b) => a['channel'].compareTo(b['channel'])); - return channelsOut; } } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 567c74c..b6f4301 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.", - "repeater_neighbours": "Съседи", + "repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.", + "repeater_neighbors": "Съседи", "neighbors_receivedData": "Получени данни за съседи", "neighbors_requestTimedOut": "Съседите поискат изтичане на време.", "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", - "neighbors_repeatersNeighbours": "Повторители Съседи", + "neighbors_repeatersNeighbors": "Повторители Съседи", "neighbors_noData": "Няма налични данни за съседи.", "channels_createPrivateChannel": "Създай Частен Канал", "channels_joinPrivateChannel": "Присъедини се към Частен Канал", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOff": "Bluetooth е изключен.", "scanner_enableBluetooth": "Активирайте Bluetooth", "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "snrIndicator_lastSeen": "Последно видян", + "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", + "chat_ShowAllPaths": "Покажи всички пътища", "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", "settings_clientRepeat": "Без електричество – повторение" diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7aba89b..077c398 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Nachbarn", - "repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", + "repeater_neighbors": "Nachbarn", + "repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", "neighbors_receivedData": "Empfangene Nachbarsdaten", "neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", - "neighbors_repeatersNeighbours": "Nachbarn", + "neighbors_repeatersNeighbors": "Nachbarn", "neighbors_noData": "Keine Nachbarsdaten verfügbar.", "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", @@ -1622,6 +1622,9 @@ "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", "scanner_enableBluetooth": "Bluetooth aktivieren", + "snrIndicator_lastSeen": "Zuletzt gesehen", + "snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater", + "chat_ShowAllPaths": "Alle Pfade anzeigen", "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen." diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cfd6330..bf49d7e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -558,6 +558,7 @@ }, "debugFrame_hexDump": "Hex Dump:", "chat_pathManagement": "Path Management", + "chat_ShowAllPaths": "Show all paths", "chat_routingMode": "Routing mode", "chat_autoUseSavedPath": "Auto (use saved path)", "chat_forceFloodMode": "Force Flood Mode", @@ -905,8 +906,8 @@ "repeater_telemetrySubtitle": "View telemetry of sensors and system stats", "repeater_cli": "CLI", "repeater_cliSubtitle": "Send commands to the repeater", - "repeater_neighbours": "Neighbors", - "repeater_neighboursSubtitle": "View zero hop neighbors.", + "repeater_neighbors": "Neighbors", + "repeater_neighborsSubtitle": "View zero hop neighbors.", "repeater_settings": "Settings", "repeater_settingsSubtitle": "Configure repeater parameters", "repeater_statusTitle": "Repeater Status", @@ -1266,8 +1267,8 @@ } } }, - "neighbors_receivedData": "Received Neighbours Data", - "neighbors_requestTimedOut": "Neighbours request timed out.", + "neighbors_receivedData": "Received Neighbors Data", + "neighbors_requestTimedOut": "Neighbors request timed out.", "neighbors_errorLoading": "Error loading neighbors: {error}", "@neighbors_errorLoading": { "placeholders": { @@ -1276,8 +1277,8 @@ } } }, - "neighbors_repeatersNeighbours": "Repeaters Neighbours", - "neighbors_noData": "No neighbours data available.", + "neighbors_repeatersNeighbors": "Repeaters Neighbors", + "neighbors_noData": "No neighbors data available.", "neighbors_unknownContact": "Unknown {pubkey}", "@neighbors_unknownContact": { "placeholders": { @@ -1624,5 +1625,7 @@ "settings_gpxExportChat": "Companion locations", "settings_gpxExportAllContacts": "All contacts locations", "settings_gpxExportShareText": "Map data exported from meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open GPX map data export" + "settings_gpxExportShareSubject": "meshcore-open GPX map data export", + "snrIndicator_nearByRepeaters": "Nearby Repeaters", + "snrIndicator_lastSeen": "Last seen" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4250a6f..1896b4f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Vecinos", - "repeater_neighboursSubtitle": "Ver vecinos de salto cero.", + "repeater_neighbors": "Vecinos", + "repeater_neighborsSubtitle": "Ver vecinos de salto cero.", "neighbors_receivedData": "Recibidas Datos de Vecinos", "neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.", "neighbors_errorLoading": "Error al cargar vecinos: {error}", - "neighbors_repeatersNeighbours": "Repetidores Vecinos", + "neighbors_repeatersNeighbors": "Repetidores Vecinos", "neighbors_noData": "No hay datos de vecinos disponibles.", "channels_joinPrivateChannel": "Únete a un Canal Privado", "channels_createPrivateChannel": "Crear un Canal Privado", @@ -1622,6 +1622,9 @@ "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", "scanner_bluetoothOff": "Bluetooth está desactivado.", "scanner_enableBluetooth": "Habilitar Bluetooth", + "snrIndicator_nearByRepeaters": "Repetidores cercanos", + "snrIndicator_lastSeen": "Visto por última vez", + "chat_ShowAllPaths": "Mostrar todos los caminos", "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", "settings_clientRepeat": "Repetir sin conexión", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios." diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 56d4a41..d1befce 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Voisins", - "repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.", + "repeater_neighbors": "Voisins", + "repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.", "neighbors_receivedData": "Données des voisins reçues", "neighbors_requestTimedOut": "Les voisins demandent un délai.", "neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}", - "neighbors_repeatersNeighbours": "Répéteurs Voisins", + "neighbors_repeatersNeighbors": "Répéteurs Voisins", "neighbors_noData": "Aucune donnée concernant les voisins disponible.", "channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.", "channels_joinPrivateChannel": "Rejoindre un Canal Privé", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", "scanner_bluetoothOff": "Le Bluetooth est désactivé.", "scanner_enableBluetooth": "Activer le Bluetooth", + "snrIndicator_lastSeen": "Dernière fois vu", + "snrIndicator_nearByRepeaters": "Répéteurs à proximité", + "chat_ShowAllPaths": "Afficher tous les chemins", "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", "settings_clientRepeat": "Répétition hors réseau" diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 239c765..22371ba 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Vicini", - "repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.", + "repeater_neighbors": "Vicini", + "repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.", "neighbors_receivedData": "Ricevute dati vicini", "neighbors_requestTimedOut": "I vicini richiedono un timeout.", "neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}", - "neighbors_repeatersNeighbours": "Ripetitori Vicini", + "neighbors_repeatersNeighbors": "Ripetitori Vicini", "neighbors_noData": "Nessun dato sugli vicini disponibile.", "channels_createPrivateChannel": "Crea un Canale Privato", "channels_createPrivateChannelDesc": "Protetta con una chiave segreta.", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", "scanner_enableBluetooth": "Abilita il Bluetooth", + "snrIndicator_nearByRepeaters": "Ripetitori vicini", + "snrIndicator_lastSeen": "Ultimo accesso", + "chat_ShowAllPaths": "Mostra tutti i percorsi", "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri." diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7235e90..2bcda78 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2032,6 +2032,12 @@ abstract class AppLocalizations { /// **'Path Management'** String get chat_pathManagement; + /// No description provided for @chat_ShowAllPaths. + /// + /// In en, this message translates to: + /// **'Show all paths'** + String get chat_ShowAllPaths; + /// No description provided for @chat_routingMode. /// /// In en, this message translates to: @@ -3027,17 +3033,17 @@ abstract class AppLocalizations { /// **'Send commands to the repeater'** String get repeater_cliSubtitle; - /// No description provided for @repeater_neighbours. + /// No description provided for @repeater_neighbors. /// /// In en, this message translates to: /// **'Neighbors'** - String get repeater_neighbours; + String get repeater_neighbors; - /// No description provided for @repeater_neighboursSubtitle. + /// No description provided for @repeater_neighborsSubtitle. /// /// In en, this message translates to: /// **'View zero hop neighbors.'** - String get repeater_neighboursSubtitle; + String get repeater_neighborsSubtitle; /// No description provided for @repeater_settings. /// @@ -4181,13 +4187,13 @@ abstract class AppLocalizations { /// No description provided for @neighbors_receivedData. /// /// In en, this message translates to: - /// **'Received Neighbours Data'** + /// **'Received Neighbors Data'** String get neighbors_receivedData; /// No description provided for @neighbors_requestTimedOut. /// /// In en, this message translates to: - /// **'Neighbours request timed out.'** + /// **'Neighbors request timed out.'** String get neighbors_requestTimedOut; /// No description provided for @neighbors_errorLoading. @@ -4196,16 +4202,16 @@ abstract class AppLocalizations { /// **'Error loading neighbors: {error}'** String neighbors_errorLoading(String error); - /// No description provided for @neighbors_repeatersNeighbours. + /// No description provided for @neighbors_repeatersNeighbors. /// /// In en, this message translates to: - /// **'Repeaters Neighbours'** - String get neighbors_repeatersNeighbours; + /// **'Repeaters Neighbors'** + String get neighbors_repeatersNeighbors; /// No description provided for @neighbors_noData. /// /// In en, this message translates to: - /// **'No neighbours data available.'** + /// **'No neighbors data available.'** String get neighbors_noData; /// No description provided for @neighbors_unknownContact. @@ -5023,6 +5029,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'meshcore-open GPX map data export'** String get settings_gpxExportShareSubject; + + /// No description provided for @snrIndicator_nearByRepeaters. + /// + /// In en, this message translates to: + /// **'Nearby Repeaters'** + String get snrIndicator_nearByRepeaters; + + /// No description provided for @snrIndicator_lastSeen. + /// + /// In en, this message translates to: + /// **'Last seen'** + String get snrIndicator_lastSeen; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index fdf9ec7..137d48a 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1080,6 +1080,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_pathManagement => 'Управление на пътища'; + @override + String get chat_ShowAllPaths => 'Покажи всички пътища'; + @override String get chat_routingMode => 'Режим на маршрутизиране'; @@ -1677,10 +1680,10 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора'; @override - String get repeater_neighbours => 'Съседи'; + String get repeater_neighbors => 'Съседи'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Преглед на съседни възли с нулев скок.'; @override @@ -2380,7 +2383,7 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Повторители Съседи'; + String get neighbors_repeatersNeighbors => 'Повторители Съседи'; @override String get neighbors_noData => 'Няма налични данни за съседи.'; @@ -2890,4 +2893,10 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open износ на данни за карта в формат GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства'; + + @override + String get snrIndicator_lastSeen => 'Последно видян'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c0fc4c8..927ac48 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1080,6 +1080,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_pathManagement => 'Pfadverwaltung'; + @override + String get chat_ShowAllPaths => 'Alle Pfade anzeigen'; + @override String get chat_routingMode => 'Routenmodus'; @@ -1676,10 +1679,10 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliSubtitle => 'Sende Befehle an den Repeater'; @override - String get repeater_neighbours => 'Nachbarn'; + String get repeater_neighbors => 'Nachbarn'; @override - String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.'; + String get repeater_neighborsSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.'; @override String get repeater_settings => 'Einstellungen'; @@ -2382,7 +2385,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Nachbarn'; + String get neighbors_repeatersNeighbors => 'Nachbarn'; @override String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; @@ -2898,4 +2901,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'GPX-Kartendaten aus meshcore-open exportieren'; + + @override + String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater'; + + @override + String get snrIndicator_lastSeen => 'Zuletzt gesehen'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4f0bed1..ef7c0c3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1065,6 +1065,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_pathManagement => 'Path Management'; + @override + String get chat_ShowAllPaths => 'Show all paths'; + @override String get chat_routingMode => 'Routing mode'; @@ -1650,10 +1653,10 @@ class AppLocalizationsEn extends AppLocalizations { String get repeater_cliSubtitle => 'Send commands to the repeater'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbors => 'Neighbors'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighborsSubtitle => 'View zero hop neighbors.'; @override String get repeater_settings => 'Settings'; @@ -2329,10 +2332,10 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get neighbors_receivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Received Neighbors Data'; @override - String get neighbors_requestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Neighbors request timed out.'; @override String neighbors_errorLoading(String error) { @@ -2340,10 +2343,10 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbors => 'Repeaters Neighbors'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'No neighbors data available.'; @override String neighbors_unknownContact(String pubkey) { @@ -2845,4 +2848,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open GPX map data export'; + + @override + String get snrIndicator_nearByRepeaters => 'Nearby Repeaters'; + + @override + String get snrIndicator_lastSeen => 'Last seen'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index f56e4e4..f72196d 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1079,6 +1079,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_pathManagement => 'Gestión de Rutas'; + @override + String get chat_ShowAllPaths => 'Mostrar todos los caminos'; + @override String get chat_routingMode => 'Modo de enrutamiento'; @@ -1674,10 +1677,10 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliSubtitle => 'Enviar comandos al repetidor'; @override - String get repeater_neighbours => 'Vecinos'; + String get repeater_neighbors => 'Vecinos'; @override - String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.'; + String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.'; @override String get repeater_settings => 'Configuración'; @@ -2376,7 +2379,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Repetidores Vecinos'; + String get neighbors_repeatersNeighbors => 'Repetidores Vecinos'; @override String get neighbors_noData => 'No hay datos de vecinos disponibles.'; @@ -2889,4 +2892,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open exportación de datos de mapa GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Repetidores cercanos'; + + @override + String get snrIndicator_lastSeen => 'Visto por última vez'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index e1325da..8978568 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1082,6 +1082,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_pathManagement => 'Gestion des chemins'; + @override + String get chat_ShowAllPaths => 'Afficher tous les chemins'; + @override String get chat_routingMode => 'Mode de routage'; @@ -1682,11 +1685,10 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; @override - String get repeater_neighbours => 'Voisins'; + String get repeater_neighbors => 'Voisins'; @override - String get repeater_neighboursSubtitle => - 'Afficher les voisins de saut nuls.'; + String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.'; @override String get repeater_settings => 'Paramètres'; @@ -2391,7 +2393,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Répéteurs Voisins'; + String get neighbors_repeatersNeighbors => 'Répéteurs Voisins'; @override String get neighbors_noData => @@ -2913,4 +2915,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open exporter les données de carte GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité'; + + @override + String get snrIndicator_lastSeen => 'Dernière fois vu'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 15d5354..a2b790f 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1077,6 +1077,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_pathManagement => 'Gestione Percorsi'; + @override + String get chat_ShowAllPaths => 'Mostra tutti i percorsi'; + @override String get chat_routingMode => 'Modalità di routing'; @@ -1672,10 +1675,10 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_cliSubtitle => 'Invia comandi al ripetitore'; @override - String get repeater_neighbours => 'Vicini'; + String get repeater_neighbors => 'Vicini'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Visualizza vicini di salto pari a zero.'; @override @@ -2376,7 +2379,7 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Ripetitori Vicini'; + String get neighbors_repeatersNeighbors => 'Ripetitori Vicini'; @override String get neighbors_noData => 'Nessun dato sugli vicini disponibile.'; @@ -2893,4 +2896,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open esportazione dati mappa GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Ripetitori vicini'; + + @override + String get snrIndicator_lastSeen => 'Ultimo accesso'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 17b3bce..a958e79 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1074,6 +1074,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_pathManagement => 'Beheer van Paden'; + @override + String get chat_ShowAllPaths => 'Toon alle paden'; + @override String get chat_routingMode => 'Routeerwijze'; @@ -1668,10 +1671,10 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater'; @override - String get repeater_neighbours => 'Buren'; + String get repeater_neighbors => 'Buren'; @override - String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.'; + String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.'; @override String get repeater_settings => 'Instellingen'; @@ -2367,7 +2370,7 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Herhalingen Buren'; + String get neighbors_repeatersNeighbors => 'Herhalingen Buren'; @override String get neighbors_noData => 'Geen gegevens van buren beschikbaar.'; @@ -2881,4 +2884,10 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open GPX kaartgegevens exporteren'; + + @override + String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden'; + + @override + String get snrIndicator_lastSeen => 'Laatst gezien'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 147e160..55bc6ec 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1079,6 +1079,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_pathManagement => 'Zarządzanie ścieżkami'; + @override + String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki'; + @override String get chat_routingMode => 'Tryb routingu'; @@ -1676,10 +1679,10 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza'; @override - String get repeater_neighbours => 'Sąsiedzi'; + String get repeater_neighbors => 'Sąsiedzi'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Wyświetl sąsiedztwo zerowych hopów.'; @override @@ -2375,7 +2378,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi'; + String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi'; @override String get neighbors_noData => 'Brak danych dotyczących sąsiadów.'; @@ -2895,4 +2898,10 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'Eksport danych mapy GPX meshcore-open'; + + @override + String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu'; + + @override + String get snrIndicator_lastSeen => 'Ostatnio widziany'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b481775..596d268 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1079,6 +1079,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_pathManagement => 'Gerenciamento de Caminhos'; + @override + String get chat_ShowAllPaths => 'Mostrar todos os caminhos'; + @override String get chat_routingMode => 'Modo de roteamento'; @@ -1674,11 +1677,10 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliSubtitle => 'Enviar comandos ao repetidor'; @override - String get repeater_neighbours => 'Vizinhos'; + String get repeater_neighbors => 'Vizinhos'; @override - String get repeater_neighboursSubtitle => - 'Visualizar vizinhos de salto zero.'; + String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.'; @override String get repeater_settings => 'Configurações'; @@ -2377,7 +2379,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos'; + String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos'; @override String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.'; @@ -2890,4 +2892,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open exportação de dados de mapa GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Repetidores Próximos'; + + @override + String get snrIndicator_lastSeen => 'Visto pela última vez'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index e5875b1..4647746 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1077,6 +1077,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_pathManagement => 'Управление маршрутами'; + @override + String get chat_ShowAllPaths => 'Показать все пути'; + @override String get chat_routingMode => 'Режим маршрутизации'; @@ -1676,10 +1679,10 @@ class AppLocalizationsRu extends AppLocalizations { String get repeater_cliSubtitle => 'Отправка команд репитеру'; @override - String get repeater_neighbours => 'Соседи'; + String get repeater_neighbors => 'Соседи'; @override - String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.'; + String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.'; @override String get repeater_settings => 'Настройки'; @@ -2379,7 +2382,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Соседи репитеров'; + String get neighbors_repeatersNeighbors => 'Соседи репитеров'; @override String get neighbors_noData => 'Данные о соседях недоступны.'; @@ -2901,4 +2904,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open экспорт данных карты GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы'; + + @override + String get snrIndicator_lastSeen => 'Последний раз видели'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 4e8b4cb..8e18663 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1074,6 +1074,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_pathManagement => 'Správa ciest'; + @override + String get chat_ShowAllPaths => 'Zobraziť všetky cesty'; + @override String get chat_routingMode => 'Režim trasy'; @@ -1669,10 +1672,10 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču'; @override - String get repeater_neighbours => 'Súsezný'; + String get repeater_neighbors => 'Súsezný'; @override - String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.'; + String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.'; @override String get repeater_settings => 'Nastavenia'; @@ -2363,7 +2366,7 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná'; + String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná'; @override String get neighbors_noData => @@ -2877,4 +2880,10 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open export dát GPX mapových údajov'; + + @override + String get snrIndicator_nearByRepeaters => 'Miestne opakovače'; + + @override + String get snrIndicator_lastSeen => 'Naposledy videný'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e01151e..b95e711 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1072,6 +1072,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_pathManagement => 'Upravljanje poti'; + @override + String get chat_ShowAllPaths => 'Prikaži vse poti'; + @override String get chat_routingMode => 'Navodilo za usmerjevalni način'; @@ -1668,10 +1671,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Pošlji ukazne povelje na ponovitveno enoto.'; @override - String get repeater_neighbours => 'Sosedi'; + String get repeater_neighbors => 'Sosedi'; @override - String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.'; + String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.'; @override String get repeater_settings => 'Nastavitve'; @@ -2367,7 +2370,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi'; + String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi'; @override String get neighbors_noData => 'Niso na voljo podatki o sosedih.'; @@ -2882,4 +2885,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open izvoz podatkov GPX karte'; + + @override + String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji'; + + @override + String get snrIndicator_lastSeen => 'Zadnjič videno'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index f081711..10047ca 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1069,6 +1069,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_pathManagement => 'Stigarhantering'; + @override + String get chat_ShowAllPaths => 'Visa alla vägar'; + @override String get chat_routingMode => 'Ruttläge'; @@ -1658,10 +1661,10 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn'; @override - String get repeater_neighbours => 'Grannar'; + String get repeater_neighbors => 'Grannar'; @override - String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.'; + String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.'; @override String get repeater_settings => 'Inställningar'; @@ -2352,7 +2355,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Upprepar grannar'; + String get neighbors_repeatersNeighbors => 'Upprepar grannar'; @override String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.'; @@ -2862,4 +2865,10 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open export av GPX-kartdata'; + + @override + String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer'; + + @override + String get snrIndicator_lastSeen => 'Senast sedd'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 847c3e5..9edc64a 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1075,6 +1075,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_pathManagement => 'Керування шляхами'; + @override + String get chat_ShowAllPaths => 'Показати всі шляхи'; + @override String get chat_routingMode => 'Режим маршрутизації'; @@ -1675,10 +1678,10 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_cliSubtitle => 'Надіслати команди ретранслятору'; @override - String get repeater_neighbours => 'Сусіди'; + String get repeater_neighbors => 'Сусіди'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Показати сусідів нульового стрибка.'; @override @@ -2380,7 +2383,7 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди'; + String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди'; @override String get neighbors_noData => 'Дані про сусідів недоступні.'; @@ -2907,4 +2910,10 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'експорт даних карти meshcore-open у форматі GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори'; + + @override + String get snrIndicator_lastSeen => 'Останній раз бачили'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index fdc4531..9753da6 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1031,6 +1031,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_pathManagement => '路径管理'; + @override + String get chat_ShowAllPaths => '显示所有路径'; + @override String get chat_routingMode => '路由模式'; @@ -1596,10 +1599,10 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_cliSubtitle => '向复用器发送指令'; @override - String get repeater_neighbours => '邻居'; + String get repeater_neighbors => '邻居'; @override - String get repeater_neighboursSubtitle => '查看邻居节点(无需中间节点)。'; + String get repeater_neighborsSubtitle => '查看邻居节点(无需中间节点)。'; @override String get repeater_settings => '设置'; @@ -2246,7 +2249,7 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => '重复使用的邻居'; + String get neighbors_repeatersNeighbors => '重复使用的邻居'; @override String get neighbors_noData => '没有可用的邻居信息。'; @@ -2714,4 +2717,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open GPX 地图数据导出'; + + @override + String get snrIndicator_nearByRepeaters => '附近的重复器'; + + @override + String get snrIndicator_lastSeen => '最近访问'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7c397b4..859e48d 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Buren", - "repeater_neighboursSubtitle": "Bekijk nul hops buren.", + "repeater_neighbors": "Buren", + "repeater_neighborsSubtitle": "Bekijk nul hops buren.", "neighbors_receivedData": "Ontvangen Buurdata", "neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.", "neighbors_errorLoading": "Fout bij het laden van buren: {error}", - "neighbors_repeatersNeighbours": "Herhalingen Buren", + "neighbors_repeatersNeighbors": "Herhalingen Buren", "neighbors_noData": "Geen gegevens van buren beschikbaar.", "channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.", "channels_createPrivateChannel": "Maak een Privé Kanaal", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Activeer Bluetooth", "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", "scanner_bluetoothOff": "Bluetooth is uitgeschakeld", + "snrIndicator_lastSeen": "Laatst gezien", + "snrIndicator_nearByRepeaters": "Nabije herhalingseenheden", + "chat_ShowAllPaths": "Toon alle paden", "settings_clientRepeat": "Herhalen: Afgekoppeld", "settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.", "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist." diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 5ebeebf..d03b911 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Sąsiedzi", - "repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.", + "repeater_neighbors": "Sąsiedzi", + "repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.", "neighbors_receivedData": "Otrzymano dane sąsiedztwa", "neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.", "neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}", - "neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi", + "neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi", "neighbors_noData": "Brak danych dotyczących sąsiadów.", "channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.", "channels_createPrivateChannel": "Utwórz Prywatny Kanał", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", "scanner_bluetoothOff": "Bluetooth jest wyłączony", "scanner_enableBluetooth": "Włącz Bluetooth", + "snrIndicator_lastSeen": "Ostatnio widziany", + "snrIndicator_nearByRepeaters": "Nadajniki w pobliżu", + "chat_ShowAllPaths": "Pokaż wszystkie ścieżki", "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz." diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index a88e038..83a7719 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Vizinhos", + "repeater_neighbors": "Vizinhos", "neighbors_receivedData": "Dados dos Vizinhos Recebidos", - "repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.", + "repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.", "neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.", "neighbors_errorLoading": "Erro ao carregar vizinhos: {error}", - "neighbors_repeatersNeighbours": "Repetidores Vizinhos", + "neighbors_repeatersNeighbors": "Repetidores Vizinhos", "neighbors_noData": "Não estão disponíveis dados de vizinhos.", "channels_createPrivateChannelDesc": "Protegido com uma chave secreta.", "channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Ative o Bluetooth", "scanner_bluetoothOff": "Bluetooth está desativado", "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", + "snrIndicator_nearByRepeaters": "Repetidores Próximos", + "snrIndicator_lastSeen": "Visto pela última vez", + "chat_ShowAllPaths": "Mostrar todos os caminhos", "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", "settings_clientRepeat": "Repetição sem rede", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos." diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index fc17eee..380ba10 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -467,8 +467,8 @@ "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", "repeater_cli": "CLI", "repeater_cliSubtitle": "Отправка команд репитеру", - "repeater_neighbours": "Соседи", - "repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.", + "repeater_neighbors": "Соседи", + "repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.", "repeater_settings": "Настройки", "repeater_settingsSubtitle": "Настройка параметров репитера", "repeater_statusTitle": "Статус репитера", @@ -661,7 +661,7 @@ "neighbors_receivedData": "Полученные данные о соседях", "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", - "neighbors_repeatersNeighbours": "Соседи репитеров", + "neighbors_repeatersNeighbors": "Соседи репитеров", "neighbors_noData": "Данные о соседях недоступны.", "neighbors_unknownContact": "Неизвестный {pubkey}", "neighbors_heardA ago": "Слышали: {time} назад", @@ -834,6 +834,9 @@ "scanner_enableBluetooth": "Включите Bluetooth", "scanner_bluetoothOff": "Bluetooth выключен", "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", + "snrIndicator_lastSeen": "Последний раз видели", + "chat_ShowAllPaths": "Показать все пути", "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", "settings_clientRepeat": "Повторение \"вне сети\"" diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 14cd3ec..aca4a29 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.", + "repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.", "neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.", "neighbors_receivedData": "Obdielo dáta suseda", - "repeater_neighbours": "Súsezný", + "repeater_neighbors": "Súsezný", "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", - "neighbors_repeatersNeighbours": "Opakovadlá Súsezná", + "neighbors_repeatersNeighbors": "Opakovadlá Súsezná", "neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.", "channels_createPrivateChannel": "Vytvorte súkromný kanál", "channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", "scanner_bluetoothOff": "Bluetooth je vypnutý", "scanner_enableBluetooth": "Povolte Bluetooth", + "snrIndicator_lastSeen": "Naposledy videný", + "snrIndicator_nearByRepeaters": "Miestne opakovače", + "chat_ShowAllPaths": "Zobraziť všetky cesty", "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných." diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index e633965..59b8434 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.", - "repeater_neighbours": "Sosedi", + "repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.", + "repeater_neighbors": "Sosedi", "neighbors_receivedData": "Prejeto podatke o sosedih", "neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.", "neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}", - "neighbors_repeatersNeighbours": "Ponovitve Sosedi", + "neighbors_repeatersNeighbors": "Ponovitve Sosedi", "neighbors_noData": "Niso na voljo podatki o sosedih.", "channels_joinPrivateChannel": "Pridružite se zasebni skupini", "channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Omogočite Bluetooth", "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", "scanner_bluetoothOff": "Bluetooth je izklopljen", + "snrIndicator_lastSeen": "Zadnjič videno", + "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", + "chat_ShowAllPaths": "Prikaži vse poti", "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", "settings_clientRepeat": "Neovadno ponavljanje" diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 4e50409..fa786f7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Grannar", - "repeater_neighboursSubtitle": "Visa noll hoppgrannar.", + "repeater_neighbors": "Grannar", + "repeater_neighborsSubtitle": "Visa noll hoppgrannar.", "neighbors_receivedData": "Mottagna grannars data", "neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.", "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", - "neighbors_repeatersNeighbours": "Upprepar grannar", + "neighbors_repeatersNeighbors": "Upprepar grannar", "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", "channels_createPrivateChannel": "Skapa en privat kanal", "channels_joinPrivateChannel": "Gå med i en Privat Kanal", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Aktivera Bluetooth", "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", "scanner_bluetoothOff": "Bluetooth är avstängt", + "snrIndicator_lastSeen": "Senast sedd", + "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", + "chat_ShowAllPaths": "Visa alla vägar", "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", "settings_clientRepeat": "Upprepa utan elnät", "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz." diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index afa1179..3f7b276 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1352,12 +1352,12 @@ } } }, - "repeater_neighbours": "Сусіди", - "repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.", + "repeater_neighbors": "Сусіди", + "repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", "neighbors_receivedData": "Дані сусідів отримано", "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", - "neighbors_repeatersNeighbours": "Ретранслятори-сусіди", + "neighbors_repeatersNeighbors": "Ретранслятори-сусіди", "neighbors_noData": "Дані про сусідів недоступні.", "channels_createPrivateChannelDesc": "Захищено секретним ключем.", "channels_joinPrivateChannel": "Приєднатися до приватного каналу", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", "scanner_bluetoothOff": "Bluetooth вимкнено", + "snrIndicator_lastSeen": "Останній раз бачили", + "snrIndicator_nearByRepeaters": "Ближні ретранслятори", + "chat_ShowAllPaths": "Показати всі шляхи", "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", "settings_clientRepeat": "Автономна система" diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 312ed1a..bc43392 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -895,8 +895,8 @@ "repeater_telemetrySubtitle": "查看传感器和系统状态的数据。", "repeater_cli": "命令行界面", "repeater_cliSubtitle": "向复用器发送指令", - "repeater_neighbours": "邻居", - "repeater_neighboursSubtitle": "查看邻居节点(无需中间节点)。", + "repeater_neighbors": "邻居", + "repeater_neighborsSubtitle": "查看邻居节点(无需中间节点)。", "repeater_settings": "设置", "repeater_settingsSubtitle": "配置重复器参数", "repeater_statusTitle": "重复器状态", @@ -1266,7 +1266,7 @@ } } }, - "neighbors_repeatersNeighbours": "重复使用的邻居", + "neighbors_repeatersNeighbors": "重复使用的邻居", "neighbors_noData": "没有可用的邻居信息。", "neighbors_unknownContact": "Unknown {pubkey}", "@neighbors_unknownContact": { @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", "scanner_bluetoothOff": "蓝牙已关闭", "scanner_enableBluetooth": "启用蓝牙", + "snrIndicator_lastSeen": "最近访问", + "snrIndicator_nearByRepeaters": "附近的重复器", + "chat_ShowAllPaths": "显示所有路径", "settings_clientRepeat": "离网重复", "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。" diff --git a/lib/models/contact.dart b/lib/models/contact.dart index a98580f..143a62a 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -119,7 +119,7 @@ class Contact { final pathBytes = _pathBytesForDisplay; Uint8List? traceBytes; - if (pathLength <= 0) { + if (pathBytes.isEmpty) { traceBytes = Uint8List(1); traceBytes[0] = publicKey[0]; return traceBytes; @@ -160,43 +160,47 @@ class Contact { } static Contact? fromFrame(Uint8List data) { - if (data.length < contactFrameSize) return null; + if (data.isEmpty) return null; if (data[0] != respCodeContact) return null; + try { + final pubKey = Uint8List.fromList( + data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), + ); + final type = data[contactTypeOffset]; + final pathLen = data[contactPathLenOffset].toSigned(8); + final safePathLen = pathLen > 0 + ? (pathLen > maxPathSize ? maxPathSize : pathLen) + : 0; + final pathBytes = safePathLen > 0 + ? Uint8List.fromList( + data.sublist(contactPathOffset, contactPathOffset + safePathLen), + ) + : Uint8List(0); + final name = readCString(data, contactNameOffset, maxNameSize); + final lastmod = readUint32LE(data, contactLastmodOffset); - final pubKey = Uint8List.fromList( - data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), - ); - final type = data[contactTypeOffset]; - final pathLen = data[contactPathLenOffset].toSigned(8); - final safePathLen = pathLen > 0 - ? (pathLen > maxPathSize ? maxPathSize : pathLen) - : 0; - final pathBytes = safePathLen > 0 - ? Uint8List.fromList( - data.sublist(contactPathOffset, contactPathOffset + safePathLen), - ) - : Uint8List(0); - final name = readCString(data, contactNameOffset, maxNameSize); - final lastmod = readUint32LE(data, contactLastmodOffset); + double? lat, lon; + final latRaw = readInt32LE(data, contactLatOffset); + final lonRaw = readInt32LE(data, contactLonOffset); + if (latRaw != 0 || lonRaw != 0) { + lat = latRaw / 1e6; + lon = lonRaw / 1e6; + } - double? lat, lon; - final latRaw = readInt32LE(data, contactLatOffset); - final lonRaw = readInt32LE(data, contactLonOffset); - if (latRaw != 0 || lonRaw != 0) { - lat = latRaw / 1e6; - lon = lonRaw / 1e6; + return Contact( + publicKey: pubKey, + name: name.isEmpty ? 'Unknown' : name, + type: type, + pathLength: pathLen, + path: pathBytes, + latitude: lat, + longitude: lon, + lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000), + ); + } catch (e) { + // If parsing fails, return null + return null; } - - return Contact( - publicKey: pubKey, - name: name.isEmpty ? 'Unknown' : name, - type: type, - pathLength: pathLen, - path: pathBytes, - latitude: lat, - longitude: lon, - lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000), - ); } @override diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index c82356d..021ad7d 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -901,7 +901,8 @@ class _ChannelChatScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ChannelMessagePathScreen(message: message), + builder: (context) => + ChannelMessagePathScreen(message: message, channelMessage: true), ), ); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 1b0544c..e6fcacc 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -17,18 +17,27 @@ import '../models/contact.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; - - const ChannelMessagePathScreen({super.key, required this.message}); + final bool channelMessage; + const ChannelMessagePathScreen({ + super.key, + required this.message, + this.channelMessage = false, + }); @override Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { final l10n = context.l10n; - final primaryPath = _selectPrimaryPath( + final primaryPathTmp = _selectPrimaryPath( message.pathBytes, message.pathVariants, ); + + final primaryPath = !channelMessage && !message.isOutgoing + ? Uint8List.fromList(primaryPathTmp.reversed.toList()) + : primaryPathTmp; + final hops = _buildPathHops(primaryPath, connector.contacts, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( @@ -37,7 +46,6 @@ class ChannelMessagePathScreen extends StatelessWidget { l10n, ); final extraPaths = _otherPaths(primaryPath, message.pathVariants); - return Scaffold( appBar: AppBar( title: Text(l10n.channelPath_title), @@ -50,9 +58,9 @@ class ChannelMessagePathScreen extends StatelessWidget { MaterialPageRoute( builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, - path: Uint8List.fromList(primaryPath), + path: primaryPath, flipPathRound: true, - reversePathRound: true, + reversePathRound: !message.isOutgoing && !channelMessage, ), ), ), @@ -62,7 +70,7 @@ class ChannelMessagePathScreen extends StatelessWidget { tooltip: l10n.channelPath_viewMap, onPressed: hasHopDetails ? () { - _openPathMap(context); + _openPathMap(context, channelMessage: channelMessage); } : null, ), @@ -157,7 +165,11 @@ class ChannelMessagePathScreen extends StatelessWidget { ), subtitle: Text(_formatPathPrefixes(variants[i])), trailing: const Icon(Icons.map_outlined, size: 20), - onTap: () => _openPathMap(context, initialPath: variants[i]), + onTap: () => _openPathMap( + context, + initialPath: variants[i], + channelMessage: channelMessage, + ), ), ), ], @@ -248,13 +260,18 @@ class ChannelMessagePathScreen extends StatelessWidget { ); } - void _openPathMap(BuildContext context, {Uint8List? initialPath}) { + void _openPathMap( + BuildContext context, { + Uint8List? initialPath, + bool channelMessage = false, + }) { Navigator.push( context, MaterialPageRoute( builder: (context) => ChannelMessagePathMapScreen( message: message, initialPath: initialPath, + channelMessage: channelMessage, ), ), ); @@ -264,11 +281,13 @@ class ChannelMessagePathScreen extends StatelessWidget { class ChannelMessagePathMapScreen extends StatefulWidget { final ChannelMessage message; final Uint8List? initialPath; + final bool channelMessage; const ChannelMessagePathMapScreen({ super.key, required this.message, this.initialPath, + this.channelMessage = false, }); @override @@ -323,11 +342,18 @@ class _ChannelMessagePathMapScreenState primaryPath, widget.message.pathVariants, ); - final selectedPath = _resolveSelectedPath( + final selectedPathTmp = _resolveSelectedPath( _selectedPath, observedPaths, primaryPath, ); + + final selectedPath = + ((!widget.message.isOutgoing && !widget.channelMessage) || + (widget.message.isOutgoing && widget.channelMessage)) + ? Uint8List.fromList(selectedPathTmp.reversed.toList()) + : selectedPathTmp; + final selectedIndex = _indexForPath(selectedPath, observedPaths); final hops = _buildPathHops( selectedPath, @@ -336,12 +362,22 @@ class _ChannelMessagePathMapScreenState ); final points = []; + + if ((widget.message.isOutgoing && !widget.channelMessage) || + (widget.message.isOutgoing && widget.channelMessage)) { + points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + } + for (final hop in hops) { if (hop.hasLocation) { points.add(hop.position!); } } - points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + + if ((!widget.message.isOutgoing && !widget.channelMessage) || + (!widget.message.isOutgoing && widget.channelMessage)) { + points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + } final polylines = points.length > 1 ? [ diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 6b8b92d..26062de 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; @@ -14,7 +15,6 @@ import '../storage/community_store.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; import '../utils/route_transitions.dart'; -import '../widgets/battery_indicator.dart'; import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; import '../widgets/qr_code_display.dart'; @@ -116,8 +116,7 @@ class _ChannelsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - leading: BatteryIndicator(connector: connector), - title: Text(context.l10n.channels_title), + title: AppBarTitle(context.l10n.channels_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index f00f242..ad897a0 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -19,7 +19,9 @@ import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; import '../models/message.dart'; +import '../models/path_history.dart'; import '../services/path_history_service.dart'; +import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; import '../utils/emoji_utils.dart'; @@ -431,242 +433,317 @@ class _ChatScreenState extends State { void _showPathHistory(BuildContext context) { final connector = Provider.of(context, listen: false); - + bool showAllPaths = false; showDialog( context: context, - builder: (context) => Consumer( - builder: (context, pathService, _) { - final paths = pathService.getRecentPaths(widget.contact.publicKeyHex); - return AlertDialog( - title: Row( - children: [ - const Icon(Icons.timeline), - const SizedBox(width: 8), - Text(context.l10n.chat_pathManagement), - ], - ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => Consumer( + builder: (context, pathService, _) { + final paths = pathService.getRecentPaths( + widget.contact.publicKeyHex, + ); + + final repeatersList = List.of(connector.directRepeaters) + ..sort((a, b) => b.ranking.compareTo(a.ranking)); + + if (repeatersList.isEmpty) { + showAllPaths = true; + } + + final directRepeater = repeatersList.isEmpty + ? null + : repeatersList.first; + final secondDirectRepeater = repeatersList.length < 2 + ? null + : repeatersList.elementAt(1); + final thirdDirectRepeater = repeatersList.length < 3 + ? null + : repeatersList.elementAt(2); + + List>> + pathsWithRepeaters = paths.map((path) { + final isDirectRepeater = + directRepeater != null && + path.pathBytes.isNotEmpty && + directRepeater.pubkeyFirstByte == path.pathBytes.first; + final isSecondDirectRepeater = + secondDirectRepeater != null && + path.pathBytes.isNotEmpty && + secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + final isThirdDirectRepeater = + thirdDirectRepeater != null && + path.pathBytes.isNotEmpty && + thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + + int ranking = -1; + Color color = Colors.grey; + if (isDirectRepeater) { + color = Colors.green; + ranking = 3; + } else if (isSecondDirectRepeater) { + color = Colors.yellow; + ranking = 2; + } else if (isThirdDirectRepeater) { + color = Colors.red; + ranking = 1; + } else if (path.wasFloodDiscovery) { + color = Colors.blue; + ranking = 0; + } + + return MapEntry(ranking, MapEntry(color, path)); + }).toList(); + + pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key)); + + return AlertDialog( + title: Row( children: [ - if (paths.isNotEmpty) ...[ + const Icon(Icons.timeline), + const SizedBox(width: 8), + Text(context.l10n.chat_pathManagement), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (pathsWithRepeaters.isNotEmpty) ...[ + if (repeatersList.isNotEmpty) + FeatureToggleRow( + title: context.l10n.chat_ShowAllPaths, + subtitle: "", + value: showAllPaths, + onChanged: (val) { + setDialogState(() { + showAllPaths = val; + }); + }, + ), + Text( + context.l10n.chat_recentAckPaths, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + if (pathsWithRepeaters.length >= 100) ...[ + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.amber[100], + borderRadius: BorderRadius.circular(8), + ), + child: Text( + context.l10n.chat_pathHistoryFull, + style: const TextStyle(fontSize: 12), + ), + ), + ], + const SizedBox(height: 8), + ...pathsWithRepeaters.map((entry) { + final path = entry.value.value; + final color = entry.value.key; + if (!showAllPaths && entry.key < 1) { + return const SizedBox.shrink(); + } else { + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + dense: true, + leading: CircleAvatar( + radius: 16, + backgroundColor: color, + child: Text( + '${path.hopCount}', + style: const TextStyle(fontSize: 12), + ), + ), + title: Text( + '${path.hopCount} ${path.hopCount == 1 ? context.l10n.chat_hopSingular : context.l10n.chat_hopPlural}', + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(path.timestamp)} • ${path.successCount} ${context.l10n.chat_successes}', + style: const TextStyle(fontSize: 11), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.close, size: 16), + tooltip: context.l10n.chat_removePath, + onPressed: () async { + await pathService.removePathRecord( + widget.contact.publicKeyHex, + path.pathBytes, + ); + }, + ), + path.wasFloodDiscovery + ? const Icon( + Icons.waves, + size: 16, + color: Colors.grey, + ) + : const Icon( + Icons.route, + size: 16, + color: Colors.grey, + ), + ], + ), + onLongPress: () => + _showFullPathDialog(context, path.pathBytes), + onTap: () async { + if (path.pathBytes.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context + .l10n + .chat_pathDetailsNotAvailable, + ), + duration: const Duration(seconds: 2), + ), + ); + return; + } + + final pathBytes = Uint8List.fromList( + path.pathBytes, + ); + final pathLength = path.pathBytes.length; + + // Set the path override to persist user's choice + await connector.setPathOverride( + widget.contact, + pathLen: pathLength, + pathBytes: pathBytes, + ); + + if (!context.mounted) return; + Navigator.pop(context); + await _notifyPathSet( + connector, + widget.contact, + pathBytes, + path.hopCount, + ); + }, + ), + ); + } + }), + const Divider(), + ] else ...[ + Text(context.l10n.chat_noPathHistoryYet), + const Divider(), + ], + const SizedBox(height: 8), Text( - context.l10n.chat_recentAckPaths, + context.l10n.chat_pathActions, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 12, ), ), - if (paths.length >= 100) ...[ - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.amber[100], - borderRadius: BorderRadius.circular(8), - ), - child: Text( - context.l10n.chat_pathHistoryFull, - style: const TextStyle(fontSize: 12), - ), - ), - ], const SizedBox(height: 8), - ...paths.map((path) { - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: path.wasFloodDiscovery - ? Colors.blue - : Colors.green, - child: Text( - '${path.hopCount}', - style: const TextStyle(fontSize: 12), - ), + ListTile( + dense: true, + leading: const CircleAvatar( + radius: 16, + backgroundColor: Colors.purple, + child: Icon(Icons.edit_road, size: 16), + ), + title: Text( + context.l10n.chat_setCustomPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_setCustomPathSubtitle, + style: const TextStyle(fontSize: 11), + ), + onTap: () { + Navigator.pop(context); + _showCustomPathDialog(context); + }, + ), + ListTile( + dense: true, + leading: const CircleAvatar( + radius: 16, + backgroundColor: Colors.orange, + child: Icon(Icons.clear_all, size: 16), + ), + title: Text( + context.l10n.chat_clearPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_clearPathSubtitle, + style: const TextStyle(fontSize: 11), + ), + onTap: () async { + await connector.clearContactPath(widget.contact); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_pathCleared), + duration: const Duration(seconds: 2), ), - title: Text( - '${path.hopCount} ${path.hopCount == 1 ? context.l10n.chat_hopSingular : context.l10n.chat_hopPlural}', - style: const TextStyle(fontSize: 14), + ); + Navigator.pop(context); + }, + ), + ListTile( + dense: true, + leading: const CircleAvatar( + radius: 16, + backgroundColor: Colors.blue, + child: Icon(Icons.waves, size: 16), + ), + title: Text( + context.l10n.chat_forceFloodMode, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_floodModeSubtitle, + style: const TextStyle(fontSize: 11), + ), + onTap: () async { + await connector.setPathOverride( + widget.contact, + pathLen: -1, + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_floodModeEnabled), + duration: const Duration(seconds: 2), ), - subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(path.timestamp)} • ${path.successCount} ${context.l10n.chat_successes}', - style: const TextStyle(fontSize: 11), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close, size: 16), - tooltip: context.l10n.chat_removePath, - onPressed: () async { - await pathService.removePathRecord( - widget.contact.publicKeyHex, - path.pathBytes, - ); - }, - ), - path.wasFloodDiscovery - ? const Icon( - Icons.waves, - size: 16, - color: Colors.grey, - ) - : const Icon( - Icons.route, - size: 16, - color: Colors.grey, - ), - ], - ), - onLongPress: () => - _showFullPathDialog(context, path.pathBytes), - onTap: () async { - if (path.pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), - ), - ); - return; - } - - final pathBytes = Uint8List.fromList( - path.pathBytes, - ); - final pathLength = path.pathBytes.length; - - // Set the path override to persist user's choice - await connector.setPathOverride( - widget.contact, - pathLen: pathLength, - pathBytes: pathBytes, - ); - - if (!context.mounted) return; - Navigator.pop(context); - await _notifyPathSet( - connector, - widget.contact, - pathBytes, - path.hopCount, - ); - }, - ), - ); - }), - const Divider(), - ] else ...[ - Text(context.l10n.chat_noPathHistoryYet), - const Divider(), + ); + Navigator.pop(context); + }, + ), ], - const SizedBox(height: 8), - Text( - context.l10n.chat_pathActions, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - const SizedBox(height: 8), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.purple, - child: Icon(Icons.edit_road, size: 16), - ), - title: Text( - context.l10n.chat_setCustomPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_setCustomPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () { - Navigator.pop(context); - _showCustomPathDialog(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.orange, - child: Icon(Icons.clear_all, size: 16), - ), - title: Text( - context.l10n.chat_clearPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_clearPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.clearContactPath(widget.contact); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ), - ); - Navigator.pop(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.blue, - child: Icon(Icons.waves, size: 16), - ), - title: Text( - context.l10n.chat_forceFloodMode, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_floodModeSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.setPathOverride( - widget.contact, - pathLen: -1, - ); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ), - ); - Navigator.pop(context); - }, - ), - ], + ), ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), - ), - ], - ); - }, + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_close), + ), + ], + ); + }, + ), ), ); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 6799d69..d470107 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; +import 'package:meshcore_open/utils/app_logger.dart'; +import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -16,7 +18,6 @@ import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; import '../utils/emoji_utils.dart'; import '../utils/route_transitions.dart'; -import '../widgets/battery_indicator.dart'; import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; import '../widgets/quick_switch_bar.dart'; @@ -90,79 +91,90 @@ class _ContactsScreenState extends State _frameSubscription = connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final frameBuffer = BufferReader(frame); - final code = frameBuffer.readUInt8(); + try { + final code = frameBuffer.readUInt8(); - if (code == respCodeExportContact) { - final advertPacket = frameBuffer.readRemainingBytes(); - // Validate packet has expected minimum size (98+ bytes per protocol) - if (advertPacket.length < 98) { - if (mounted) { + if (code == respCodeExportContact) { + final advertPacket = frameBuffer.readRemainingBytes(); + // Validate packet has expected minimum size (98+ bytes per protocol) + if (advertPacket.length < 98) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_invalidAdvertFormat), + ), + ); + } + _pendingOperations.remove(ContactOperationType.export); + return; + } + final hexString = pubKeyToHex(advertPacket); + Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); + } + + if (code == respCodeOk) { + // Show a snackbar indicating success + if (!mounted) return; + + if (_pendingOperations.contains(ContactOperationType.import)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImported)), + ); + } + + if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.contacts_invalidAdvertFormat), + content: Text(context.l10n.contacts_zeroHopContactAdvertSent), ), ); } - _pendingOperations.remove(ContactOperationType.export); - return; - } - final hexString = pubKeyToHex(advertPacket); - Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); - } - if (code == respCodeOk) { - // Show a snackbar indicating success - if (!mounted) return; + if (_pendingOperations.contains(ContactOperationType.export)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_contactAdvertCopied), + ), + ); + } - if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImported)), - ); + _pendingOperations.clear(); } - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertSent), - ), - ); + if (code == respCodeErr) { + // Show a snackbar indicating failure + if (!mounted) return; + + if (_pendingOperations.contains(ContactOperationType.import)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_contactImportFailed), + ), + ); + } + + if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), + ), + ); + } + if (_pendingOperations.contains(ContactOperationType.export)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_contactAdvertCopyFailed), + ), + ); + } + + _pendingOperations.clear(); } - - if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), - ); - } - - _pendingOperations.clear(); - } - - if (code == respCodeErr) { - // Show a snackbar indicating failure - if (!mounted) return; - - if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), - ); - } - - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), - ), - ); - } - if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactAdvertCopyFailed), - ), - ); - } - - _pendingOperations.clear(); + } catch (e) { + appLogger.error( + 'Error processing received frame: $e', + tag: 'ContactsScreen', + ); } }); } @@ -229,9 +241,7 @@ class _ContactsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - leading: BatteryIndicator(connector: connector), - title: Text(context.l10n.contacts_title), - centerTitle: true, + title: AppBarTitle(context.l10n.contacts_title), automaticallyImplyLeading: false, actions: [ PopupMenuButton( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 552f579..1fad04b 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; +import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -17,7 +18,6 @@ import '../services/map_marker_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; -import '../widgets/battery_indicator.dart'; import '../widgets/quick_switch_bar.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; @@ -105,7 +105,7 @@ class _MapScreenState extends State { double _zoomFromStdDev(double latStdDev, double lonStdDev) { final maxSpread = max(latStdDev, lonStdDev); if (maxSpread <= 0) return 13.0; - // Approzimate: each zoom level halves the visible area + // Approximate: each zoom level halves the visible area // ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7 final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3; return zoom.clamp(4.0, 15.0); @@ -262,8 +262,7 @@ class _MapScreenState extends State { canPop: allowBack, child: Scaffold( appBar: AppBar( - leading: BatteryIndicator(connector: connector), - title: Text(context.l10n.map_title), + title: AppBarTitle(context.l10n.map_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ @@ -384,8 +383,8 @@ class _MapScreenState extends State { connector.selfLatitude!, connector.selfLongitude!, ), - width: 35, - height: 35, + width: 40, + height: 40, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -826,7 +825,7 @@ class _MapScreenState extends State { color: _getNodeColor(contact.type), ), const SizedBox(width: 8), - Expanded(child: Text(contact.name)), + Expanded(child: SelectableText(contact.name)), ], ), content: Column( @@ -997,7 +996,7 @@ class _MapScreenState extends State { ), ), const SizedBox(height: 2), - Text(value, style: const TextStyle(fontSize: 14)), + SelectableText(value, style: const TextStyle(fontSize: 14)), ], ), ); diff --git a/lib/screens/neighbours_screen.dart b/lib/screens/neighbors_screen.dart similarity index 75% rename from lib/screens/neighbours_screen.dart rename to lib/screens/neighbors_screen.dart index b606188..3dee339 100644 --- a/lib/screens/neighbours_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/utils/app_logger.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; @@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../widgets/snr_indicator.dart'; -class NeighboursScreen extends StatefulWidget { +class NeighborsScreen extends StatefulWidget { final Contact repeater; final String password; - const NeighboursScreen({ + const NeighborsScreen({ super.key, required this.repeater, required this.password, }); @override - State createState() => _NeighboursScreenState(); + State createState() => _NeighborsScreenState(); } -class _NeighboursScreenState extends State { - static const int _reqNeighboursKeyLen = 4; +class _NeighborsScreenState extends State { + static const int _reqNeighborsKeyLen = 4; static const int _statusPayloadOffset = 8; static const int _statusStatsSize = 52; static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize; Uint8List _tagData = Uint8List(4); - int _neighbourCount = 0; + int _neighborCount = 0; bool _isLoading = false; bool _isLoaded = false; @@ -41,7 +42,7 @@ class _NeighboursScreenState extends State { StreamSubscription? _frameSubscription; RepeaterCommandService? _commandService; PathSelection? _pendingStatusSelection; - List>? _parsedNeighbours; + List>? _parsedNeighbors; @override void initState() { @@ -49,7 +50,7 @@ class _NeighboursScreenState extends State { final connector = Provider.of(context, listen: false); _commandService = RepeaterCommandService(connector); _setupMessageListener(); - _loadNeighbours(); + _loadNeighbors(); _hasData = false; } @@ -62,13 +63,12 @@ class _NeighboursScreenState extends State { if (frame[0] == respCodeSent) { _tagData = frame.sublist(2, 6); - //_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little); } // Check if it's a binary response if (frame[0] == pushCodeBinaryResponse && listEquals(frame.sublist(2, 6), _tagData)) { - _handleNeighboursResponse(connector, frame.sublist(6)); + _handleNeighborsResponse(connector, frame.sublist(6)); } }); } @@ -91,65 +91,77 @@ class _NeighboursScreenState extends State { return '${h}h ${m2}m'; } - static List> parseNeighboursData( + static List> parseNeighborsData( BufferReader buffer, int resultsCount, ) { - final Map> neighbours = {}; - for (var i = 0; i < resultsCount; i++) { - final neighbourData = neighbours.putIfAbsent( - i, - () => { - 'contact': null, - 'publicKey': {}, - 'lastHeard': {}, - 'snr': {}, - }, - ); - neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen); - neighbourData['lastHeard'] = buffer.readUInt32LE(); - neighbourData['snr'] = buffer.readInt8() / 4.0; - } + final Map> neighbors = {}; + try { + for (var i = 0; i < resultsCount; i++) { + final neighborData = neighbors.putIfAbsent( + i, + () => { + 'contact': null, + 'publicKey': {}, + 'lastHeard': {}, + 'snr': {}, + }, + ); + neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen); + neighborData['lastHeard'] = buffer.readUInt32LE(); + neighborData['snr'] = buffer.readInt8() / 4.0; + } - return neighbours.values.toList(); + return neighbors.values.toList(); + } catch (e) { + appLogger.error( + 'Error parsing neighbors data: $e', + tag: 'NeighborsScreen', + ); + return []; + } } - void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) { + void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); - final neighbourCount = buffer.readUInt16LE(); - final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE()); - connector.contacts.where((c) => c.type == advTypeRepeater).forEach(( - repeater, - ) { - for (var neighbourData in parsedNeighbours) { - final publicKey = neighbourData['publicKey']; - if (listEquals( - repeater.publicKey.sublist(0, _reqNeighboursKeyLen), - publicKey, - )) { - neighbourData['contact'] = repeater; + try { + final neighborCount = buffer.readUInt16LE(); + final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); + connector.contacts.where((c) => c.type == advTypeRepeater).forEach(( + repeater, + ) { + for (var neighborData in parsedNeighbors) { + final publicKey = neighborData['publicKey']; + if (listEquals( + repeater.publicKey.sublist(0, _reqNeighborsKeyLen), + publicKey, + )) { + neighborData['contact'] = repeater; + } } - } - }); + }); - setState(() { - _parsedNeighbours = parsedNeighbours; - _neighbourCount = neighbourCount; - }); + setState(() { + _parsedNeighbors = parsedNeighbors; + _neighborCount = neighborCount; + }); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.neighbors_receivedData), - backgroundColor: Colors.green, - ), - ); - _statusTimeout?.cancel(); - if (!mounted) return; - setState(() { - _isLoading = false; - _isLoaded = true; - _hasData = true; - }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.neighbors_receivedData), + backgroundColor: Colors.green, + ), + ); + _statusTimeout?.cancel(); + if (!mounted) return; + setState(() { + _isLoading = false; + _isLoaded = true; + _hasData = true; + }); + } catch (e) { + appLogger.error('Error handling neighbors response: $e'); + } } Contact _resolveRepeater(MeshCoreConnector connector) { @@ -159,7 +171,7 @@ class _NeighboursScreenState extends State { ); } - Future _loadNeighbours() async { + Future _loadNeighbors() async { if (_commandService == null) return; setState(() { @@ -172,17 +184,17 @@ class _NeighboursScreenState extends State { final selection = await connector.preparePathForContactSend(repeater); _pendingStatusSelection = selection; - //[version][number of requested neighbours][offset_16bit][order by][len of public key] + //[version][number of requested neighbors][offset_16bit][order by][len of public key] final frame = buildSendBinaryReq( repeater.publicKey, payload: Uint8List.fromList([ - reqTypeGetNeighbours, + reqTypeGetNeighbors, 0x00, 0x0F, 0x00, 0x00, 0x00, - _reqNeighboursKeyLen, + _reqNeighborsKeyLen, ]), ); await connector.sendFrame(frame); @@ -258,7 +270,7 @@ class _NeighboursScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - l10n.neighbors_repeatersNeighbours, + l10n.neighbors_repeatersNeighbors, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), Text( @@ -345,7 +357,7 @@ class _NeighboursScreenState extends State { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), - onPressed: _isLoading ? null : _loadNeighbours, + onPressed: _isLoading ? null : _loadNeighbors, tooltip: l10n.repeater_refresh, ), ], @@ -353,13 +365,13 @@ class _NeighboursScreenState extends State { body: SafeArea( top: false, child: RefreshIndicator( - onRefresh: _loadNeighbours, + onRefresh: _loadNeighbors, child: ListView( padding: const EdgeInsets.all(16), children: [ if (!_isLoaded && !_hasData && - (_parsedNeighbours == null || _parsedNeighbours!.isEmpty)) + (_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) Center( child: Text( l10n.neighbors_noData, @@ -368,10 +380,9 @@ class _NeighboursScreenState extends State { ), if (_isLoaded || _hasData && - !(_parsedNeighbours == null || - _parsedNeighbours!.isEmpty)) - _buildNeighboursInfoCard( - "${l10n.repeater_neighbours} - $_neighbourCount", + !(_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) + _buildNeighborsInfoCard( + "${l10n.repeater_neighbors} - $_neighborCount", ), ], ), @@ -380,7 +391,7 @@ class _NeighboursScreenState extends State { ); } - Widget _buildNeighboursInfoCard(String title) { + Widget _buildNeighborsInfoCard(String title) { final connector = Provider.of(context, listen: false); return Card( child: Padding( @@ -405,7 +416,7 @@ class _NeighboursScreenState extends State { ], ), const Divider(), - for (final entry in _parsedNeighbours!.asMap().entries) + for (final entry in _parsedNeighbors!.asMap().entries) _buildInfoRow( entry.value['contact'] != null ? entry.value['contact'].name @@ -430,6 +441,7 @@ class _NeighboursScreenState extends State { double snr, int spreadingFactor, ) { + final snrUi = snrUiFromSNR(snr, spreadingFactor); return Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( @@ -443,9 +455,15 @@ class _NeighboursScreenState extends State { style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: Text(value), - trailing: SNRIcon( - snr: snr, - snrLevels: getSNRfromSF(spreadingFactor), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(snrUi.icon, color: snrUi.color, size: 18.0), + Text( + snrUi.text, + style: TextStyle(fontSize: 10, color: snrUi.color), + ), + ], ), ), ), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 7677d0d..8e24bee 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -10,6 +10,7 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/l10n/l10n.dart'; import 'package:meshcore_open/models/contact.dart'; import 'package:meshcore_open/services/map_tile_cache_service.dart'; +import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; import 'package:provider/provider.dart'; @@ -32,7 +33,7 @@ String formatDistance(double distanceMeters) { class PathTraceData { final Uint8List pathData; - final Uint8List snrData; + final List snrData; final Map pathContacts; PathTraceData({ @@ -45,6 +46,7 @@ class PathTraceData { class PathTraceMapScreen extends StatefulWidget { final String title; final Uint8List path; + final int? repeaterId; final bool flipPathRound; final bool reversePathRound; @@ -52,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget { super.key, required this.title, required this.path, + this.repeaterId, this.flipPathRound = false, this.reversePathRound = false, }); @@ -96,7 +99,7 @@ class _PathTraceMapScreenState extends State { super.dispose(); } - Uint8List addReturnpath(Uint8List pathBytes) { + Uint8List addReturnPath(Uint8List pathBytes) { Uint8List? traceBytes; final len = (pathBytes.length + pathBytes.length - 1); traceBytes = Uint8List(len); @@ -124,7 +127,7 @@ class _PathTraceMapScreenState extends State { : widget.path; if (widget.flipPathRound) { - path = addReturnpath(pathTmp); + path = addReturnPath(pathTmp); } else { path = pathTmp; } @@ -146,42 +149,57 @@ class _PathTraceMapScreenState extends State { _frameSubscription = connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final frameBuffer = BufferReader(frame); - final code = frameBuffer.readUInt8(); + try { + final code = frameBuffer.readUInt8(); - if (code == respCodeSent) { - frameBuffer.skipBytes(1); //reserved - tagData = frameBuffer.readBytes(4); - final timeoutSeconds = frameBuffer.readUInt32LE(); + if (code == respCodeSent) { + frameBuffer.skipBytes(1); //reserved + tagData = frameBuffer.readBytes(4); + final timeoutMilliseconds = frameBuffer.readUInt32LE(); - // Start timeout timer for trace response - _timeoutTimer?.cancel(); - _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () { + // Start timeout timer for trace response + _timeoutTimer?.cancel(); + _timeoutTimer = Timer( + Duration(milliseconds: timeoutMilliseconds), + () { + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + }, + ); + } + + if (code == respCodeErr) { + _timeoutTimer?.cancel(); if (!mounted) return; setState(() { _isLoading = false; _failed2Loaded = true; }); - }); - } + } - if (code == respCodeErr) { + // Check if it's a binary response + if (frame.length > 8 && + code == pushCodeTraceData && + listEquals(frame.sublist(4, 8), tagData)) { + _timeoutTimer?.cancel(); + if (!mounted) return; + frameBuffer.skipBytes(3); //reserved + path length + flag + if (listEquals(frameBuffer.readBytes(4), tagData)) { + _handleTraceResponse(frame); + } + } + } catch (e) { _timeoutTimer?.cancel(); if (!mounted) return; setState(() { _isLoading = false; _failed2Loaded = true; }); - } - // Check if it's a binary response - if (frame.length > 8 && - code == pushCodeTraceData && - listEquals(frame.sublist(4, 8), tagData)) { - _timeoutTimer?.cancel(); - if (!mounted) return; - frameBuffer.skipBytes(3); //reserved + path length + flag - if (listEquals(frameBuffer.readBytes(4), tagData)) { - _handleTraceResponse(frame); - } + // Handle any parsing errors gracefully + appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen'); } }); } @@ -190,63 +208,83 @@ class _PathTraceMapScreenState extends State { final connector = Provider.of(context, listen: false); final buffer = BufferReader(frame); - buffer.skipBytes(2); // Skip push code and reserved byte - int pathLength = buffer.readUInt8(); - buffer.skipBytes(5); // Skip Flag byte and tag data - buffer.skipBytes(4); // Skip auth code - Uint8List pathData = buffer.readBytes(pathLength); - Uint8List snrData = buffer.readRemainingBytes(); + try { + buffer.skipBytes(2); // Skip push code and reserved byte + int pathLength = buffer.readUInt8(); + buffer.skipBytes(5); // Skip Flag byte and tag data + buffer.skipBytes(4); // Skip auth code + Uint8List pathData = buffer.readBytes(pathLength); + List snrData = buffer + .readRemainingBytes() + .map((snr) => snr.toSigned(8).toDouble() / 4) + .toList(); - Map pathContacts = {}; + Map pathContacts = {}; - connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var repeaterData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([repeaterData]), - )) { - pathContacts[repeaterData] = repeater; + connector.contacts.where((c) => c.type != advTypeChat).forEach(( + repeater, + ) { + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + } } - } - }); + }); - setState(() { - _isLoading = false; - _hasData = true; - _traceData = PathTraceData( - pathData: pathData, - snrData: snrData, - pathContacts: pathContacts, - ); - _points = []; - _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); - for (final hop in _traceData!.pathData) { - final contact = _traceData!.pathContacts[hop]; - if (contact != null && - contact.hasLocation && - contact.latitude != null && - contact.longitude != null) { - _points.add(LatLng(contact.latitude!, contact.longitude!)); + setState(() { + _isLoading = false; + _hasData = true; + _traceData = PathTraceData( + pathData: pathData, + snrData: snrData, + pathContacts: pathContacts, + ); + _points = []; + _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + for (final hop in _traceData!.pathData) { + final contact = _traceData!.pathContacts[hop]; + if (contact != null && + contact.hasLocation && + contact.latitude != null && + contact.longitude != null) { + _points.add(LatLng(contact.latitude!, contact.longitude!)); + } } - } - _polylines = _points.length > 1 - ? [ - Polyline( - points: _points, - strokeWidth: 4, - color: Colors.blueAccent, - ), - ] - : []; + _polylines = _points.length > 1 + ? [ + Polyline( + points: _points, + strokeWidth: 4, + color: Colors.blueAccent, + ), + ] + : []; - _initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0); - _initialZoom = _points.isNotEmpty ? 13.0 : 2.0; - _bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null; - _mapKey = ValueKey( - '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', + _initialCenter = _points.isNotEmpty + ? _points.first + : const LatLng(0, 0); + _initialZoom = _points.isNotEmpty ? 13.0 : 2.0; + _bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null; + _mapKey = ValueKey( + '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', + ); + _pathDistanceMeters = getPathDistanceMeters(_points); + }); + } catch (e) { + appLogger.error( + 'Error handling trace response: $e', + tag: 'PathTraceMapScreen', ); - _pathDistanceMeters = getPathDistanceMeters(_points); - }); + if (mounted) { + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + } + } } @override @@ -532,6 +570,12 @@ class _PathTraceMapScreenState extends State { itemCount: pathTraceData.pathData.length + 1, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { + final snrUi = snrUiFromSNR( + index < pathTraceData.snrData.length + ? pathTraceData.snrData[index] + : null, + context.read().currentSf, + ); return Column( children: [ ListTile( @@ -550,12 +594,22 @@ class _PathTraceMapScreenState extends State { ), style: const TextStyle(fontSize: 14), ), - trailing: SNRIcon( - snr: - pathTraceData.snrData[index].toSigned( - 8, - ) / - 4.0, + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + snrUi.icon, + color: snrUi.color, + size: 18.0, + ), + Text( + snrUi.text, + style: TextStyle( + fontSize: 10, + color: snrUi.color, + ), + ), + ], ), onTap: () { // Handle item tap diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 903f89e..a5f503d 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -6,7 +6,7 @@ import 'repeater_status_screen.dart'; import 'repeater_cli_screen.dart'; import 'repeater_settings_screen.dart'; import 'telemetry_screen.dart'; -import 'neighbours_screen.dart'; +import 'neighbors_screen.dart'; class RepeaterHubScreen extends StatelessWidget { final Contact repeater; @@ -174,17 +174,15 @@ class RepeaterHubScreen extends StatelessWidget { _buildManagementCard( context, icon: Icons.group, - title: l10n.repeater_neighbours, - subtitle: l10n.repeater_neighboursSubtitle, + title: l10n.repeater_neighbors, + subtitle: l10n.repeater_neighborsSubtitle, color: Colors.orange, onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => NeighboursScreen( - repeater: repeater, - password: password, - ), + builder: (context) => + NeighborsScreen(repeater: repeater, password: password), ), ); }, diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart new file mode 100644 index 0000000..c88a596 --- /dev/null +++ b/lib/widgets/app_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/widgets/battery_indicator.dart'; +import 'package:provider/provider.dart'; + +import 'snr_indicator.dart'; + +class AppBarTitle extends StatelessWidget { + final String title; + final Widget? leading; + final Widget? trailing; + const AppBarTitle(this.title, {this.leading, this.trailing, super.key}); + + @override + Widget build(BuildContext context) { + final connector = context.watch(); + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + leading ?? const SizedBox.shrink(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, overflow: TextOverflow.ellipsis), + if (connector.isConnected && connector.selfName != null) + Text( + '(${connector.selfName})', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + overflow: TextOverflow.ellipsis, + ), + ], + ), + const SizedBox(width: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BatteryIndicator(connector: connector), + SNRIndicator(connector: connector), + ], + ), + trailing ?? const SizedBox.shrink(), + ], + ); + } +} diff --git a/lib/widgets/battery_indicator.dart b/lib/widgets/battery_indicator.dart index 7837415..ccea59d 100644 --- a/lib/widgets/battery_indicator.dart +++ b/lib/widgets/battery_indicator.dart @@ -68,20 +68,24 @@ class _BatteryIndicatorState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(batteryUi.icon, size: 18, color: batteryUi.color), - const SizedBox(width: 2), - Flexible( - child: Text( - displayText, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: batteryUi.color, + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(batteryUi.icon, size: 18, color: batteryUi.color), + const SizedBox(height: 2), + Flexible( + child: Text( + displayText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: batteryUi.color, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - overflow: TextOverflow.visible, - maxLines: 1, - softWrap: false, - ), + ], ), ], ), diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 483697f..c2b6d12 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -1,7 +1,9 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/models/path_history.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; +import 'package:meshcore_open/widgets/elements_ui.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -19,15 +21,22 @@ class PathManagementDialog { } } -class _PathManagementDialog extends StatelessWidget { +class _PathManagementDialog extends StatefulWidget { final Contact contact; const _PathManagementDialog({required this.contact}); + @override + State<_PathManagementDialog> createState() => _PathManagementDialogState(); +} + +class _PathManagementDialogState extends State<_PathManagementDialog> { + bool _showAllPaths = false; + Contact _resolveContact(MeshCoreConnector connector) { return connector.contacts.firstWhere( - (c) => c.publicKeyHex == contact.publicKeyHex, - orElse: () => contact, + (c) => c.publicKeyHex == widget.contact.publicKeyHex, + orElse: () => widget.contact, ); } @@ -134,6 +143,59 @@ class _PathManagementDialog extends StatelessWidget { final currentContact = _resolveContact(connector); final paths = pathService.getRecentPaths(currentContact.publicKeyHex); + final repeatersList = List.of(connector.directRepeaters) + ..sort((a, b) => b.ranking.compareTo(a.ranking)); + + if (repeatersList.isEmpty) { + _showAllPaths = true; + } + + final directRepeater = repeatersList.isEmpty + ? null + : repeatersList.first; + final secondDirectRepeater = repeatersList.length < 2 + ? null + : repeatersList.elementAt(1); + final thirdDirectRepeater = repeatersList.length < 3 + ? null + : repeatersList.elementAt(2); + + List>> pathsWithRepeaters = + paths.map((path) { + final isDirectRepeater = + directRepeater != null && + path.pathBytes.isNotEmpty && + directRepeater.pubkeyFirstByte == path.pathBytes.first; + final isSecondDirectRepeater = + secondDirectRepeater != null && + path.pathBytes.isNotEmpty && + secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + final isThirdDirectRepeater = + thirdDirectRepeater != null && + path.pathBytes.isNotEmpty && + thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + + int ranking = -1; + Color color = Colors.grey; + if (isDirectRepeater) { + color = Colors.green; + ranking = 3; + } else if (isSecondDirectRepeater) { + color = Colors.yellow; + ranking = 2; + } else if (isThirdDirectRepeater) { + color = Colors.red; + ranking = 1; + } else if (path.wasFloodDiscovery) { + color = Colors.blue; + ranking = 0; + } + + return MapEntry(ranking, MapEntry(color, path)); + }).toList(); + + pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key)); + return AlertDialog( title: Text(l10n.chat_pathManagement), content: SingleChildScrollView( @@ -147,6 +209,17 @@ class _PathManagementDialog extends StatelessWidget { ), const SizedBox(height: 12), if (paths.isNotEmpty) ...[ + if (repeatersList.isNotEmpty) + FeatureToggleRow( + title: l10n.chat_ShowAllPaths, + subtitle: "", + value: _showAllPaths, + onChanged: (val) { + setState(() { + _showAllPaths = val; + }); + }, + ), Text( l10n.chat_recentAckPaths, style: const TextStyle( @@ -154,7 +227,7 @@ class _PathManagementDialog extends StatelessWidget { fontSize: 12, ), ), - if (paths.length >= 100) ...[ + if (pathsWithRepeaters.length >= 100) ...[ const SizedBox(height: 8), Container( width: double.infinity, @@ -173,92 +246,99 @@ class _PathManagementDialog extends StatelessWidget { ), ], const SizedBox(height: 8), - ...paths.map((path) { - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: path.wasFloodDiscovery - ? Colors.blue - : Colors.green, - child: Text( - '${path.hopCount}', - style: const TextStyle(fontSize: 12), - ), - ), - title: Text( - l10n.chat_hopsCount(path.hopCount), - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)} • ${path.successCount} ${l10n.chat_successes}', - style: const TextStyle(fontSize: 11), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close, size: 16), - tooltip: l10n.chat_removePath, - onPressed: () async { - await pathService.removePathRecord( - currentContact.publicKeyHex, - path.pathBytes, - ); - }, + ...pathsWithRepeaters.map((entry) { + final path = entry.value.value; + final color = entry.value.key; + + if (!_showAllPaths && entry.key < 1) { + return const SizedBox.shrink(); + } else { + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + dense: true, + leading: CircleAvatar( + radius: 16, + backgroundColor: color, + child: Text( + '${path.hopCount}', + style: const TextStyle(fontSize: 12), ), - path.wasFloodDiscovery - ? const Icon( - Icons.waves, - size: 16, - color: Colors.grey, - ) - : const Icon( - Icons.route, - size: 16, - color: Colors.grey, + ), + title: Text( + l10n.chat_hopsCount(path.hopCount), + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)} • ${path.successCount} ${l10n.chat_successes}', + style: const TextStyle(fontSize: 11), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.close, size: 16), + tooltip: l10n.chat_removePath, + onPressed: () async { + await pathService.removePathRecord( + currentContact.publicKeyHex, + path.pathBytes, + ); + }, + ), + path.wasFloodDiscovery + ? const Icon( + Icons.waves, + size: 16, + color: Colors.grey, + ) + : const Icon( + Icons.route, + size: 16, + color: Colors.grey, + ), + ], + ), + onLongPress: () => + _showFullPathDialog(context, path.pathBytes), + onTap: () async { + if (path.pathBytes.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + l10n.chat_pathDetailsNotAvailable, ), - ], - ), - onLongPress: () => - _showFullPathDialog(context, path.pathBytes), - onTap: () async { - if (path.pathBytes.isEmpty) { + duration: const Duration(seconds: 2), + ), + ); + return; + } + + final pathBytes = Uint8List.fromList( + path.pathBytes, + ); + final pathLength = path.pathBytes.length; + + await connector.setPathOverride( + currentContact, + pathLen: pathLength, + pathBytes: pathBytes, + ); + + if (!context.mounted) return; + Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - l10n.chat_pathDetailsNotAvailable, + l10n.path_usingHopsPath(path.hopCount), ), duration: const Duration(seconds: 2), ), ); - return; - } - - final pathBytes = Uint8List.fromList(path.pathBytes); - final pathLength = path.pathBytes.length; - - await connector.setPathOverride( - currentContact, - pathLen: pathLength, - pathBytes: pathBytes, - ); - - if (!context.mounted) return; - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.path_usingHopsPath(path.hopCount), - ), - duration: const Duration(seconds: 2), - ), - ); - }, - ), - ); + }, + ), + ); + } }), const Divider(), ] else ...[ diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index da68a65..db4fb8e 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,4 +1,13 @@ import 'package:flutter/material.dart'; +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; + +class SNRUi { + final IconData icon; + final Color color; + final String text; + const SNRUi(this.icon, this.color, this.text); +} List getSNRfromSF(int spreadingFactor) { switch (spreadingFactor) { @@ -19,44 +28,178 @@ List getSNRfromSF(int spreadingFactor) { } } -class SNRIcon extends StatelessWidget { - final double snr; - final List snrLevels; +SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) { + if (snr == null || + spreadingFactor == null || + spreadingFactor < 7 || + spreadingFactor > 12) { + return const SNRUi(Icons.signal_cellular_off, Colors.grey, '—'); + } - const SNRIcon({ - super.key, - required this.snr, - this.snrLevels = const [4.0, -2.0, -4.0, -6.0], - }); + final snrLevels = getSNRfromSF(spreadingFactor); + + IconData icon; + Color color; + String text = '${snr.toStringAsFixed(1)} dB'; + + if (snr >= snrLevels[0]) { + icon = Icons.signal_cellular_alt; + color = Colors.green; + } else if (snr >= snrLevels[1]) { + icon = Icons.signal_cellular_alt; + color = Colors.lightGreen; + } else if (snr >= snrLevels[2]) { + icon = Icons.signal_cellular_alt; + color = Colors.yellow; + } else if (snr >= snrLevels[3]) { + icon = Icons.signal_cellular_alt_2_bar; + color = Colors.orange; + } else { + icon = Icons.signal_cellular_alt_1_bar; + color = Colors.red; + } + + return SNRUi(icon, color, text); +} + +class SNRIndicator extends StatefulWidget { + final MeshCoreConnector connector; + + const SNRIndicator({super.key, required this.connector}); + @override + State createState() => _SNRIndicatorState(); +} + +class _SNRIndicatorState extends State { @override Widget build(BuildContext context) { - IconData icon; - Color color; + final directRepeaters = widget.connector.directRepeaters; + final directBestRepeaters = List.of(directRepeaters) + ..sort((a, b) => (b.ranking).compareTo(a.ranking)); + final directRepeater = directBestRepeaters.isEmpty + ? null + : directBestRepeaters.first; - if (snr >= snrLevels[0]) { - icon = Icons.signal_cellular_alt; - color = Colors.green; - } else if (snr >= snrLevels[1]) { - icon = Icons.signal_cellular_alt; - color = Colors.lightGreen; - } else if (snr >= snrLevels[2]) { - icon = Icons.signal_cellular_alt; - color = Colors.yellow; - } else if (snr >= snrLevels[3]) { - icon = Icons.signal_cellular_alt_2_bar; - color = Colors.orange; - } else { - icon = Icons.signal_cellular_alt_1_bar; - color = Colors.red; + final snrUi = snrUiFromSNR( + directBestRepeaters.isNotEmpty ? directRepeater!.snr : null, + widget.connector.currentSf, + ); + + return InkWell( + onTap: () { + if (directRepeater != null) { + _showFullPathDialog(context, directBestRepeaters); + } + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(snrUi.icon, size: 18, color: snrUi.color), + Text( + snrUi.text, + style: TextStyle(fontSize: 12, color: snrUi.color), + ), + ], + ), + if (directRepeater != null) + Text( + '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + String _formatLastUpdated(DateTime lastSeen) { + final now = DateTime.now(); + final diff = now.difference(lastSeen); + if (diff.isNegative) { + return "0s"; } + if (diff.inMinutes < 1) { + return "${diff.inSeconds}s"; + } + if (diff.inMinutes < 60) { + return "${diff.inMinutes}m"; + } + if (diff.inHours < 24) { + final hours = diff.inHours; + return "${hours}h"; + } + final days = diff.inDays; + return "${days}d"; + } - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, color: color), - Text('$snr dB', style: TextStyle(fontSize: 10, color: color)), - ], + void _showFullPathDialog( + BuildContext context, + List directBestRepeaters, + ) { + final l10n = context.l10n; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.snrIndicator_nearByRepeaters), + content: SizedBox( + child: Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: directBestRepeaters.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final repeater = directBestRepeaters[index]; + final snrUi = snrUiFromSNR( + repeater.snr, + widget.connector.currentSf, + ); + + final name = widget.connector.contacts + .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) + .map((c) => c.name) + .firstOrNull; + + return Column( + children: [ + ListTile( + leading: Icon(snrUi.icon, color: snrUi.color), + title: Text( + name ?? + repeater.pubkeyFirstByte + .toRadixString(16) + .padLeft(2, '0'), + ), + subtitle: Text( + 'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}', + ), + ), + ], + ); + }, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_close), + ), + ], + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 09e9301..f695838 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -497,18 +497,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: From f4b18d97a12f1e44297fbc1a93895d19ee60fb14 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:08:23 -0500 Subject: [PATCH 117/421] Added Line Of Sight Feature for repeater placement, Added app wide Units Setting (#198) * feat: add LOS workflow, global units, l10n cleanup, and mobile UI overflow fixes Squashes prior PR commits into one changeset including: LOS map/service/tests, global metric/imperial unit system adoption, notification/BLE safety fixes, app-wide localization backfill/mojibake cleanup, and responsive UI title/overflow hardening. * l10n: revert unrelated locale churn for LOS feature * feat: keep LOS with app-wide unit settings * fix: resolve post-merge app bar/import analyzer errors * style: format screen files for CI --- lib/l10n/app_bg.arb | 117 +- lib/l10n/app_de.arb | 117 +- lib/l10n/app_en.arb | 115 ++ lib/l10n/app_es.arb | 117 +- lib/l10n/app_fr.arb | 117 +- lib/l10n/app_it.arb | 117 +- lib/l10n/app_localizations.dart | 208 ++++ lib/l10n/app_localizations_bg.dart | 129 +++ lib/l10n/app_localizations_de.dart | 130 +++ lib/l10n/app_localizations_en.dart | 128 +++ lib/l10n/app_localizations_es.dart | 131 +++ lib/l10n/app_localizations_fr.dart | 130 +++ lib/l10n/app_localizations_it.dart | 130 +++ lib/l10n/app_localizations_nl.dart | 130 +++ lib/l10n/app_localizations_pl.dart | 129 +++ lib/l10n/app_localizations_pt.dart | 129 +++ lib/l10n/app_localizations_ru.dart | 129 +++ lib/l10n/app_localizations_sk.dart | 129 +++ lib/l10n/app_localizations_sl.dart | 129 +++ lib/l10n/app_localizations_sv.dart | 127 +++ lib/l10n/app_localizations_uk.dart | 130 +++ lib/l10n/app_localizations_zh.dart | 124 ++ lib/l10n/app_nl.arb | 117 +- lib/l10n/app_pl.arb | 117 +- lib/l10n/app_pt.arb | 117 +- lib/l10n/app_ru.arb | 117 +- lib/l10n/app_sk.arb | 117 +- lib/l10n/app_sl.arb | 117 +- lib/l10n/app_sv.arb | 117 +- lib/l10n/app_uk.arb | 117 +- lib/l10n/app_zh.arb | 117 +- lib/main.dart | 23 + lib/models/app_settings.dart | 28 + lib/screens/app_debug_log_screen.dart | 3 +- lib/screens/app_settings_screen.dart | 56 +- lib/screens/ble_debug_log_screen.dart | 3 +- lib/screens/channel_message_path_screen.dart | 181 ++- lib/screens/community_qr_scanner_screen.dart | 3 +- lib/screens/contacts_screen.dart | 55 +- lib/screens/line_of_sight_map_screen.dart | 1005 +++++++++++++++++ lib/screens/map_cache_screen.dart | 6 +- lib/screens/map_screen.dart | 134 ++- lib/screens/path_trace_map.dart | 191 +++- lib/screens/scanner_screen.dart | 3 +- lib/screens/settings_screen.dart | 6 +- lib/screens/telemetry_screen.dart | 20 +- lib/services/app_settings_service.dart | 10 + lib/services/ble_debug_log_service.dart | 24 +- lib/services/line_of_sight_service.dart | 406 +++++++ lib/services/notification_service.dart | 151 ++- lib/widgets/adaptive_app_bar_title.dart | 17 + test/services/line_of_sight_service_test.dart | 72 ++ 52 files changed, 6078 insertions(+), 214 deletions(-) create mode 100644 lib/screens/line_of_sight_map_screen.dart create mode 100644 lib/services/line_of_sight_service.dart create mode 100644 lib/widgets/adaptive_app_bar_title.dart create mode 100644 test/services/line_of_sight_service_test.dart diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index b6f4301..5689f95 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Покажи всички пътища", "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", - "settings_clientRepeat": "Без електричество – повторение" + "settings_clientRepeat": "Без електричество – повторение", + "settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "единици", + "appSettings_unitsMetric": "Метрика (m / km)", + "appSettings_unitsImperial": "Имперска (ft / mi)", + "map_lineOfSight": "Линия на видимост", + "map_losScreenTitle": "Линия на видимост", + "losSelectStartEnd": "Изберете начални и крайни възли за LOS.", + "losRunFailed": "Проверката на пряката видимост е неуспешна: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Изчистете всички точки", + "losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина", + "losMenuTitle": "LOS меню", + "losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки", + "losShowDisplayNodes": "Показване на възли на дисплея", + "losCustomPoints": "Персонализирани точки", + "losCustomPointLabel": "Персонализирано {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Антена B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Стартирайте LOS", + "losNoElevationData": "Няма данни за надморска височина", + "losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: проверка...", + "losStatusNoData": "LOS: няма данни", + "losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.", + "losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.", + "losRenameCustomPoint": "Преименувайте персонализирана точка", + "losPointName": "Име на точката", + "losShowPanelTooltip": "Показване на LOS панел", + "losHidePanelTooltip": "Скриване на LOS панела", + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 077c398..22fdf6b 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1627,5 +1627,120 @@ "chat_ShowAllPaths": "Alle Pfade anzeigen", "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", - "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen." + "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.", + "settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Einheiten", + "appSettings_unitsMetric": "Metrisch (m/km)", + "appSettings_unitsImperial": "Imperial (ft/mi)", + "map_lineOfSight": "Sichtlinie", + "map_losScreenTitle": "Sichtlinie", + "losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.", + "losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Löschen Sie alle Punkte", + "losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen", + "losMenuTitle": "LOS-Menü", + "losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen", + "losShowDisplayNodes": "Anzeigeknoten anzeigen", + "losCustomPoints": "Benutzerdefinierte Punkte", + "losCustomPointLabel": "Benutzerdefiniert {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punkt A", + "losPointB": "Punkt B", + "losAntennaA": "Antenne A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenne B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Führen Sie LOS aus", + "losNoElevationData": "Keine Höhendaten", + "losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blockiert durch {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: Überprüfen...", + "losStatusNoData": "LOS: keine Daten", + "losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.", + "losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.", + "losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um", + "losPointName": "Punktname", + "losShowPanelTooltip": "LOS-Panel anzeigen", + "losHidePanelTooltip": "LOS-Panel ausblenden", + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bf49d7e..ae24539 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -131,6 +131,7 @@ }, "settings_aboutLegalese": "2026 MeshCore Open Source Project", "settings_aboutDescription": "An open-source Flutter client for MeshCore LoRa mesh networking devices.", + "settings_aboutOpenMeteoAttribution": "LOS elevation data: Open-Meteo (CC BY 4.0)", "settings_infoName": "Name", "settings_infoId": "ID", "settings_infoStatus": "Status", @@ -242,6 +243,9 @@ "appSettings_last24Hours": "Last 24 hours", "appSettings_lastWeek": "Last week", "appSettings_offlineMapCache": "Offline Map Cache", + "appSettings_unitsTitle": "Units", + "appSettings_unitsMetric": "Metric (m / km)", + "appSettings_unitsImperial": "Imperial (ft / mi)", "appSettings_noAreaSelected": "No area selected", "appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { @@ -639,6 +643,8 @@ }, "chat_invalidLink": "Invalid link format", "map_title": "Node Map", + "map_lineOfSight": "Line of Sight", + "map_losScreenTitle": "Line of Sight", "map_noNodesWithLocation": "No nodes with location data", "map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map", "map_nodesCount": "Nodes: {count}", @@ -1548,6 +1554,115 @@ "pathTrace_refreshTooltip": "Refresh Path Trace.", "pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!", "pathTrace_clearTooltip": "Clear path.", + "losSelectStartEnd": "Select start and end nodes for LOS.", + "losRunFailed": "Line-of-sight check failed: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Clear all points", + "losRunToViewElevationProfile": "Run LOS to view elevation profile", + "losMenuTitle": "LOS Menu", + "losMenuSubtitle": "Tap nodes or long-press map for custom points", + "losShowDisplayNodes": "Show display nodes", + "losCustomPoints": "Custom points", + "losCustomPointLabel": "Custom {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Point A", + "losPointB": "Point B", + "losAntennaA": "Antenna A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenna B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Run LOS", + "losNoElevationData": "No elevation data", + "losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: checking...", + "losStatusNoData": "LOS: no data", + "losStatusSummary": "LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Elevation data unavailable for one or more samples.", + "losErrorInvalidInput": "Invalid points/elevation data for LOS calculation.", + "losRenameCustomPoint": "Rename custom point", + "losPointName": "Point name", + "losShowPanelTooltip": "Show LOS panel", + "losHidePanelTooltip": "Hide LOS panel", + "losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1896b4f..3a7fe53 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1627,5 +1627,120 @@ "chat_ShowAllPaths": "Mostrar todos los caminos", "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", "settings_clientRepeat": "Repetir sin conexión", - "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios." + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.", + "settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unidades", + "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsImperial": "Imperial (pies/millas)", + "map_lineOfSight": "Línea de visión", + "map_losScreenTitle": "Línea de visión", + "losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.", + "losRunFailed": "Error en la comprobación de la línea de visión: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Borrar todos los puntos", + "losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación", + "losMenuTitle": "Menú LOS", + "losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados", + "losShowDisplayNodes": "Mostrar nodos de visualización", + "losCustomPoints": "Puntos personalizados", + "losCustomPointLabel": "Personalizado {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punto A", + "losPointB": "Punto B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Ejecutar LOS", + "losNoElevationData": "Sin datos de elevación", + "losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: comprobando...", + "losStatusNoData": "LOS: sin datos", + "losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.", + "losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.", + "losRenameCustomPoint": "Cambiar el nombre del punto personalizado", + "losPointName": "Nombre del punto", + "losShowPanelTooltip": "Mostrar panel LOS", + "losHidePanelTooltip": "Ocultar panel LOS", + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d1befce..f962ee5 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Afficher tous les chemins", "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", - "settings_clientRepeat": "Répétition hors réseau" + "settings_clientRepeat": "Répétition hors réseau", + "settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unités", + "appSettings_unitsMetric": "Métrique (m/km)", + "appSettings_unitsImperial": "Impérial (ft / mi)", + "map_lineOfSight": "Ligne de vue", + "map_losScreenTitle": "Ligne de vue", + "losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.", + "losRunFailed": "Échec de la vérification de la ligne de vue : {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Effacer tous les points", + "losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés", + "losShowDisplayNodes": "Afficher les nœuds d'affichage", + "losCustomPoints": "Points personnalisés", + "losCustomPointLabel": "Personnalisé {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Point A", + "losPointB": "Point B", + "losAntennaA": "Antenne A : {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenne B : {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Exécuter la LOS", + "losNoElevationData": "Aucune donnée d'altitude", + "losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS : vérification...", + "losStatusNoData": "LOS : aucune donnée", + "losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.", + "losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.", + "losRenameCustomPoint": "Renommer le point personnalisé", + "losPointName": "Nom du point", + "losShowPanelTooltip": "Afficher le panneau LOS", + "losHidePanelTooltip": "Masquer le panneau LOS", + "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 22371ba..6111004 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Mostra tutti i percorsi", "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", - "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri." + "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.", + "settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unità", + "appSettings_unitsMetric": "Metrico (m/km)", + "appSettings_unitsImperial": "Imperiale (ft / mi)", + "map_lineOfSight": "Linea di vista", + "map_losScreenTitle": "Linea di vista", + "losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.", + "losRunFailed": "Controllo della linea di vista fallito: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Cancella tutti i punti", + "losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico", + "losMenuTitle": "Menù LOS", + "losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati", + "losShowDisplayNodes": "Mostra i nodi di visualizzazione", + "losCustomPoints": "Punti personalizzati", + "losCustomPointLabel": "Personalizzato {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punto A", + "losPointB": "Punto B", + "losAntennaA": "Antenna A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenna B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Esegui LOS", + "losNoElevationData": "Nessun dato di elevazione", + "losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: controllo...", + "losStatusNoData": "LOS: nessun dato", + "losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.", + "losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.", + "losRenameCustomPoint": "Rinomina punto personalizzato", + "losPointName": "Nome del punto", + "losShowPanelTooltip": "Mostra il pannello LOS", + "losHidePanelTooltip": "Nascondi il pannello LOS", + "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2bcda78..20d0422 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -700,6 +700,12 @@ abstract class AppLocalizations { /// **'An open-source Flutter client for MeshCore LoRa mesh networking devices.'** String get settings_aboutDescription; + /// No description provided for @settings_aboutOpenMeteoAttribution. + /// + /// In en, this message translates to: + /// **'LOS elevation data: Open-Meteo (CC BY 4.0)'** + String get settings_aboutOpenMeteoAttribution; + /// No description provided for @settings_infoName. /// /// In en, this message translates to: @@ -1240,6 +1246,24 @@ abstract class AppLocalizations { /// **'Offline Map Cache'** String get appSettings_offlineMapCache; + /// No description provided for @appSettings_unitsTitle. + /// + /// In en, this message translates to: + /// **'Units'** + String get appSettings_unitsTitle; + + /// No description provided for @appSettings_unitsMetric. + /// + /// In en, this message translates to: + /// **'Metric (m / km)'** + String get appSettings_unitsMetric; + + /// No description provided for @appSettings_unitsImperial. + /// + /// In en, this message translates to: + /// **'Imperial (ft / mi)'** + String get appSettings_unitsImperial; + /// No description provided for @appSettings_noAreaSelected. /// /// In en, this message translates to: @@ -2290,6 +2314,18 @@ abstract class AppLocalizations { /// **'Node Map'** String get map_title; + /// No description provided for @map_lineOfSight. + /// + /// In en, this message translates to: + /// **'Line of Sight'** + String get map_lineOfSight; + + /// No description provided for @map_losScreenTitle. + /// + /// In en, this message translates to: + /// **'Line of Sight'** + String get map_losScreenTitle; + /// No description provided for @map_noNodesWithLocation. /// /// In en, this message translates to: @@ -4772,6 +4808,178 @@ abstract class AppLocalizations { /// **'Clear path.'** String get pathTrace_clearTooltip; + /// No description provided for @losSelectStartEnd. + /// + /// In en, this message translates to: + /// **'Select start and end nodes for LOS.'** + String get losSelectStartEnd; + + /// No description provided for @losRunFailed. + /// + /// In en, this message translates to: + /// **'Line-of-sight check failed: {error}'** + String losRunFailed(String error); + + /// No description provided for @losClearAllPoints. + /// + /// In en, this message translates to: + /// **'Clear all points'** + String get losClearAllPoints; + + /// No description provided for @losRunToViewElevationProfile. + /// + /// In en, this message translates to: + /// **'Run LOS to view elevation profile'** + String get losRunToViewElevationProfile; + + /// No description provided for @losMenuTitle. + /// + /// In en, this message translates to: + /// **'LOS Menu'** + String get losMenuTitle; + + /// No description provided for @losMenuSubtitle. + /// + /// In en, this message translates to: + /// **'Tap nodes or long-press map for custom points'** + String get losMenuSubtitle; + + /// No description provided for @losShowDisplayNodes. + /// + /// In en, this message translates to: + /// **'Show display nodes'** + String get losShowDisplayNodes; + + /// No description provided for @losCustomPoints. + /// + /// In en, this message translates to: + /// **'Custom points'** + String get losCustomPoints; + + /// No description provided for @losCustomPointLabel. + /// + /// In en, this message translates to: + /// **'Custom {index}'** + String losCustomPointLabel(int index); + + /// No description provided for @losPointA. + /// + /// In en, this message translates to: + /// **'Point A'** + String get losPointA; + + /// No description provided for @losPointB. + /// + /// In en, this message translates to: + /// **'Point B'** + String get losPointB; + + /// No description provided for @losAntennaA. + /// + /// In en, this message translates to: + /// **'Antenna A: {value} {unit}'** + String losAntennaA(String value, String unit); + + /// No description provided for @losAntennaB. + /// + /// In en, this message translates to: + /// **'Antenna B: {value} {unit}'** + String losAntennaB(String value, String unit); + + /// No description provided for @losRun. + /// + /// In en, this message translates to: + /// **'Run LOS'** + String get losRun; + + /// No description provided for @losNoElevationData. + /// + /// In en, this message translates to: + /// **'No elevation data'** + String get losNoElevationData; + + /// No description provided for @losProfileClear. + /// + /// In en, this message translates to: + /// **'{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}'** + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ); + + /// No description provided for @losProfileBlocked. + /// + /// In en, this message translates to: + /// **'{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}'** + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ); + + /// No description provided for @losStatusChecking. + /// + /// In en, this message translates to: + /// **'LOS: checking...'** + String get losStatusChecking; + + /// No description provided for @losStatusNoData. + /// + /// In en, this message translates to: + /// **'LOS: no data'** + String get losStatusNoData; + + /// No description provided for @losStatusSummary. + /// + /// In en, this message translates to: + /// **'LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown'** + String losStatusSummary(int clear, int total, int blocked, int unknown); + + /// No description provided for @losErrorElevationUnavailable. + /// + /// In en, this message translates to: + /// **'Elevation data unavailable for one or more samples.'** + String get losErrorElevationUnavailable; + + /// No description provided for @losErrorInvalidInput. + /// + /// In en, this message translates to: + /// **'Invalid points/elevation data for LOS calculation.'** + String get losErrorInvalidInput; + + /// No description provided for @losRenameCustomPoint. + /// + /// In en, this message translates to: + /// **'Rename custom point'** + String get losRenameCustomPoint; + + /// No description provided for @losPointName. + /// + /// In en, this message translates to: + /// **'Point name'** + String get losPointName; + + /// No description provided for @losShowPanelTooltip. + /// + /// In en, this message translates to: + /// **'Show LOS panel'** + String get losShowPanelTooltip; + + /// No description provided for @losHidePanelTooltip. + /// + /// In en, this message translates to: + /// **'Hide LOS panel'** + String get losHidePanelTooltip; + + /// No description provided for @losElevationAttribution. + /// + /// In en, this message translates to: + /// **'Elevation data: Open-Meteo (CC BY 4.0)'** + String get losElevationAttribution; + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 137d48a..9c66ff2 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -326,6 +326,10 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_aboutDescription => 'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Име'; @@ -622,6 +626,15 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Кеш на офлайн карти'; + @override + String get appSettings_unitsTitle => 'единици'; + + @override + String get appSettings_unitsMetric => 'Метрика (m / km)'; + + @override + String get appSettings_unitsImperial => 'Имперска (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Няма избрана област'; @@ -1243,6 +1256,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_title => 'Карта на възлите'; + @override + String get map_lineOfSight => 'Линия на видимост'; + + @override + String get map_losScreenTitle => 'Линия на видимост'; + @override String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.'; @@ -2724,6 +2743,116 @@ class AppLocalizationsBg extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Изчисти пътя'; + @override + String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.'; + + @override + String losRunFailed(String error) { + return 'Проверката на пряката видимост е неуспешна: $error'; + } + + @override + String get losClearAllPoints => 'Изчистете всички точки'; + + @override + String get losRunToViewElevationProfile => + 'Стартирайте LOS, за да видите профила на надморската височина'; + + @override + String get losMenuTitle => 'LOS меню'; + + @override + String get losMenuSubtitle => + 'Докоснете възли или натиснете продължително карта за персонализирани точки'; + + @override + String get losShowDisplayNodes => 'Показване на възли на дисплея'; + + @override + String get losCustomPoints => 'Персонализирани точки'; + + @override + String losCustomPointLabel(int index) { + return 'Персонализирано $index'; + } + + @override + String get losPointA => 'Точка А'; + + @override + String get losPointB => 'Точка Б'; + + @override + String losAntennaA(String value, String unit) { + return 'Антена A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Антена B: $value $unit'; + } + + @override + String get losRun => 'Стартирайте LOS'; + + @override + String get losNoElevationData => 'Няма данни за надморска височина'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, блокиран от $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: проверка...'; + + @override + String get losStatusNoData => 'LOS: няма данни'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно'; + } + + @override + String get losErrorElevationUnavailable => + 'Няма налични данни за надморска височина за една или повече проби.'; + + @override + String get losErrorInvalidInput => + 'Невалидни данни за точки/надморска височина за изчисляване на LOS.'; + + @override + String get losRenameCustomPoint => 'Преименувайте персонализирана точка'; + + @override + String get losPointName => 'Име на точката'; + + @override + String get losShowPanelTooltip => 'Показване на LOS панел'; + + @override + String get losHidePanelTooltip => 'Скриване на LOS панела'; + + @override + String get losElevationAttribution => + 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 927ac48..ef7cd9d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -320,6 +320,10 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_aboutDescription => 'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS-Höhendaten: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Name'; @@ -619,6 +623,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline-Karten-Cache'; + @override + String get appSettings_unitsTitle => 'Einheiten'; + + @override + String get appSettings_unitsMetric => 'Metrisch (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (ft/mi)'; + @override String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt'; @@ -1242,6 +1255,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_title => 'Karte'; + @override + String get map_lineOfSight => 'Sichtlinie'; + + @override + String get map_losScreenTitle => 'Sichtlinie'; + @override String get map_noNodesWithLocation => 'Keine Knoten mit Standortdaten'; @@ -2729,6 +2748,117 @@ class AppLocalizationsDe extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Pfad löschen'; + @override + String get losSelectStartEnd => + 'Wählen Sie Start- und Endknoten für LOS aus.'; + + @override + String losRunFailed(String error) { + return 'Sichtlinienprüfung fehlgeschlagen: $error'; + } + + @override + String get losClearAllPoints => 'Löschen Sie alle Punkte'; + + @override + String get losRunToViewElevationProfile => + 'Führen Sie LOS aus, um das Höhenprofil anzuzeigen'; + + @override + String get losMenuTitle => 'LOS-Menü'; + + @override + String get losMenuSubtitle => + 'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen'; + + @override + String get losShowDisplayNodes => 'Anzeigeknoten anzeigen'; + + @override + String get losCustomPoints => 'Benutzerdefinierte Punkte'; + + @override + String losCustomPointLabel(int index) { + return 'Benutzerdefiniert $index'; + } + + @override + String get losPointA => 'Punkt A'; + + @override + String get losPointB => 'Punkt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenne A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenne B: $value $unit'; + } + + @override + String get losRun => 'Führen Sie LOS aus'; + + @override + String get losNoElevationData => 'Keine Höhendaten'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, freie Sichtlinie, Mindestabstand $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blockiert durch $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: Überprüfen...'; + + @override + String get losStatusNoData => 'LOS: keine Daten'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'Sichtlinie: $clear/$total frei, $blocked blockiert, $unknown unbekannt'; + } + + @override + String get losErrorElevationUnavailable => + 'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.'; + + @override + String get losErrorInvalidInput => + 'Ungültige Punkte/Höhendaten für die LOS-Berechnung.'; + + @override + String get losRenameCustomPoint => + 'Benennen Sie den benutzerdefinierten Punkt um'; + + @override + String get losPointName => 'Punktname'; + + @override + String get losShowPanelTooltip => 'LOS-Panel anzeigen'; + + @override + String get losHidePanelTooltip => 'LOS-Panel ausblenden'; + + @override + String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ef7c0c3..7f07e26 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -318,6 +318,10 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_aboutDescription => 'An open-source Flutter client for MeshCore LoRa mesh networking devices.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS elevation data: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Name'; @@ -614,6 +618,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Map Cache'; + @override + String get appSettings_unitsTitle => 'Units'; + + @override + String get appSettings_unitsMetric => 'Metric (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (ft / mi)'; + @override String get appSettings_noAreaSelected => 'No area selected'; @@ -1222,6 +1235,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_title => 'Node Map'; + @override + String get map_lineOfSight => 'Line of Sight'; + + @override + String get map_losScreenTitle => 'Line of Sight'; + @override String get map_noNodesWithLocation => 'No nodes with location data'; @@ -2683,6 +2702,115 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Clear path.'; + @override + String get losSelectStartEnd => 'Select start and end nodes for LOS.'; + + @override + String losRunFailed(String error) { + return 'Line-of-sight check failed: $error'; + } + + @override + String get losClearAllPoints => 'Clear all points'; + + @override + String get losRunToViewElevationProfile => + 'Run LOS to view elevation profile'; + + @override + String get losMenuTitle => 'LOS Menu'; + + @override + String get losMenuSubtitle => 'Tap nodes or long-press map for custom points'; + + @override + String get losShowDisplayNodes => 'Show display nodes'; + + @override + String get losCustomPoints => 'Custom points'; + + @override + String losCustomPointLabel(int index) { + return 'Custom $index'; + } + + @override + String get losPointA => 'Point A'; + + @override + String get losPointB => 'Point B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenna A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenna B: $value $unit'; + } + + @override + String get losRun => 'Run LOS'; + + @override + String get losNoElevationData => 'No elevation data'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blocked by $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: checking...'; + + @override + String get losStatusNoData => 'LOS: no data'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown'; + } + + @override + String get losErrorElevationUnavailable => + 'Elevation data unavailable for one or more samples.'; + + @override + String get losErrorInvalidInput => + 'Invalid points/elevation data for LOS calculation.'; + + @override + String get losRenameCustomPoint => 'Rename custom point'; + + @override + String get losPointName => 'Point name'; + + @override + String get losShowPanelTooltip => 'Show LOS panel'; + + @override + String get losHidePanelTooltip => 'Hide LOS panel'; + + @override + String get losElevationAttribution => + 'Elevation data: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index f72196d..6409675 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -323,6 +323,10 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_aboutDescription => 'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Datos de elevación LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nombre'; @@ -620,6 +624,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Caché de Mapa Offline'; + @override + String get appSettings_unitsTitle => 'Unidades'; + + @override + String get appSettings_unitsMetric => 'Métrico (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (pies/millas)'; + @override String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área'; @@ -1240,6 +1253,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_title => 'Mapa de Nodos'; + @override + String get map_lineOfSight => 'Línea de visión'; + + @override + String get map_losScreenTitle => 'Línea de visión'; + @override String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación'; @@ -2722,6 +2741,118 @@ class AppLocalizationsEs extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Borrar ruta'; + @override + String get losSelectStartEnd => + 'Seleccione los nodos de inicio y fin para LOS.'; + + @override + String losRunFailed(String error) { + return 'Error en la comprobación de la línea de visión: $error'; + } + + @override + String get losClearAllPoints => 'Borrar todos los puntos'; + + @override + String get losRunToViewElevationProfile => + 'Ejecute LOS para ver el perfil de elevación'; + + @override + String get losMenuTitle => 'Menú LOS'; + + @override + String get losMenuSubtitle => + 'Toque nodos o mantenga presionado el mapa para puntos personalizados'; + + @override + String get losShowDisplayNodes => 'Mostrar nodos de visualización'; + + @override + String get losCustomPoints => 'Puntos personalizados'; + + @override + String losCustomPointLabel(int index) { + return 'Personalizado $index'; + } + + @override + String get losPointA => 'Punto A'; + + @override + String get losPointB => 'Punto B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Ejecutar LOS'; + + @override + String get losNoElevationData => 'Sin datos de elevación'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: comprobando...'; + + @override + String get losStatusNoData => 'LOS: sin datos'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido'; + } + + @override + String get losErrorElevationUnavailable => + 'Datos de elevación no disponibles para una o más muestras.'; + + @override + String get losErrorInvalidInput => + 'Datos de puntos/elevación no válidos para el cálculo de LOS.'; + + @override + String get losRenameCustomPoint => + 'Cambiar el nombre del punto personalizado'; + + @override + String get losPointName => 'Nombre del punto'; + + @override + String get losShowPanelTooltip => 'Mostrar panel LOS'; + + @override + String get losHidePanelTooltip => 'Ocultar panel LOS'; + + @override + String get losElevationAttribution => + 'Datos de elevación: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 8978568..3536cf5 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -324,6 +324,10 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_aboutDescription => 'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nom'; @@ -622,6 +626,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne'; + @override + String get appSettings_unitsTitle => 'Unités'; + + @override + String get appSettings_unitsMetric => 'Métrique (m/km)'; + + @override + String get appSettings_unitsImperial => 'Impérial (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Aucune zone sélectionnée'; @@ -1246,6 +1259,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_title => 'Carte des nœuds'; + @override + String get map_lineOfSight => 'Ligne de vue'; + + @override + String get map_losScreenTitle => 'Ligne de vue'; + @override String get map_noNodesWithLocation => 'Aucun nœud avec des données de localisation'; @@ -2738,6 +2757,117 @@ class AppLocalizationsFr extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Effacer le chemin'; + @override + String get losSelectStartEnd => + 'Sélectionnez les nœuds de début et de fin pour LOS.'; + + @override + String losRunFailed(String error) { + return 'Échec de la vérification de la ligne de vue : $error'; + } + + @override + String get losClearAllPoints => 'Effacer tous les points'; + + @override + String get losRunToViewElevationProfile => + 'Exécutez LOS pour afficher le profil d\'altitude'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés'; + + @override + String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage'; + + @override + String get losCustomPoints => 'Points personnalisés'; + + @override + String losCustomPointLabel(int index) { + return 'Personnalisé $index'; + } + + @override + String get losPointA => 'Point A'; + + @override + String get losPointB => 'Point B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenne A : $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenne B : $value $unit'; + } + + @override + String get losRun => 'Exécuter la LOS'; + + @override + String get losNoElevationData => 'Aucune donnée d\'altitude'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, LOS clair, clairance minimale $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloqué par $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS : vérification...'; + + @override + String get losStatusNoData => 'LOS : aucune donnée'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu'; + } + + @override + String get losErrorElevationUnavailable => + 'Données d\'altitude indisponibles pour un ou plusieurs échantillons.'; + + @override + String get losErrorInvalidInput => + 'Données de points/d\'altitude non valides pour le calcul de la LOS.'; + + @override + String get losRenameCustomPoint => 'Renommer le point personnalisé'; + + @override + String get losPointName => 'Nom du point'; + + @override + String get losShowPanelTooltip => 'Afficher le panneau LOS'; + + @override + String get losHidePanelTooltip => 'Masquer le panneau LOS'; + + @override + String get losElevationAttribution => + 'Données d\'altitude : Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a2b790f..521cfb7 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -322,6 +322,10 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_aboutDescription => 'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nome'; @@ -619,6 +623,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Cache Mappa Offline'; + @override + String get appSettings_unitsTitle => 'Unità'; + + @override + String get appSettings_unitsMetric => 'Metrico (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperiale (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Nessun\'area selezionata'; @@ -1239,6 +1252,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_title => 'Mappa Nodi'; + @override + String get map_lineOfSight => 'Linea di vista'; + + @override + String get map_losScreenTitle => 'Linea di vista'; + @override String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione'; @@ -2723,6 +2742,117 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Pulisci percorso'; + @override + String get losSelectStartEnd => + 'Seleziona i nodi iniziali e finali per la LOS.'; + + @override + String losRunFailed(String error) { + return 'Controllo della linea di vista fallito: $error'; + } + + @override + String get losClearAllPoints => 'Cancella tutti i punti'; + + @override + String get losRunToViewElevationProfile => + 'Eseguire LOS per visualizzare il profilo altimetrico'; + + @override + String get losMenuTitle => 'Menù LOS'; + + @override + String get losMenuSubtitle => + 'Tocca i nodi o premi a lungo la mappa per punti personalizzati'; + + @override + String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione'; + + @override + String get losCustomPoints => 'Punti personalizzati'; + + @override + String losCustomPointLabel(int index) { + return 'Personalizzato $index'; + } + + @override + String get losPointA => 'Punto A'; + + @override + String get losPointB => 'Punto B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenna A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenna B: $value $unit'; + } + + @override + String get losRun => 'Esegui LOS'; + + @override + String get losNoElevationData => 'Nessun dato di elevazione'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloccato da $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: controllo...'; + + @override + String get losStatusNoData => 'LOS: nessun dato'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto'; + } + + @override + String get losErrorElevationUnavailable => + 'Dati di elevazione non disponibili per uno o più campioni.'; + + @override + String get losErrorInvalidInput => + 'Dati punti/elevazione non validi per il calcolo della LOS.'; + + @override + String get losRenameCustomPoint => 'Rinomina punto personalizzato'; + + @override + String get losPointName => 'Nome del punto'; + + @override + String get losShowPanelTooltip => 'Mostra il pannello LOS'; + + @override + String get losHidePanelTooltip => 'Nascondi il pannello LOS'; + + @override + String get losElevationAttribution => + 'Dati di elevazione: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a958e79..a7a4c0b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -320,6 +320,10 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_aboutDescription => 'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Naam'; @@ -617,6 +621,15 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Kaarten Cache'; + @override + String get appSettings_unitsTitle => 'Eenheden'; + + @override + String get appSettings_unitsMetric => 'Metrisch (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperiaal (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Geen gebied geselecteerd'; @@ -1235,6 +1248,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_title => 'Node Map'; + @override + String get map_lineOfSight => 'Zichtlijn'; + + @override + String get map_losScreenTitle => 'Zichtlijn'; + @override String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens'; @@ -2714,6 +2733,117 @@ class AppLocalizationsNl extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Weg wissen'; + @override + String get losSelectStartEnd => + 'Selecteer begin- en eindknooppunten voor LOS.'; + + @override + String losRunFailed(String error) { + return 'Zichtlijncontrole mislukt: $error'; + } + + @override + String get losClearAllPoints => 'Wis alle punten'; + + @override + String get losRunToViewElevationProfile => + 'Voer LOS uit om het hoogteprofiel te bekijken'; + + @override + String get losMenuTitle => 'LOS-menu'; + + @override + String get losMenuSubtitle => + 'Tik op knooppunten of druk lang op de kaart voor aangepaste punten'; + + @override + String get losShowDisplayNodes => 'Toon weergaveknooppunten'; + + @override + String get losCustomPoints => 'Aangepaste punten'; + + @override + String losCustomPointLabel(int index) { + return 'Aangepast $index'; + } + + @override + String get losPointA => 'Punt A'; + + @override + String get losPointB => 'Punt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenne A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenne B: $value $unit'; + } + + @override + String get losRun => 'Voer LOS uit'; + + @override + String get losNoElevationData => 'Geen hoogtegegevens'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, vrije LOS, min. vrije ruimte $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, geblokkeerd door $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: controleren...'; + + @override + String get losStatusNoData => 'LOS: geen gegevens'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total gewist, $blocked geblokkeerd, $unknown onbekend'; + } + + @override + String get losErrorElevationUnavailable => + 'Hoogtegegevens niet beschikbaar voor een of meer monsters.'; + + @override + String get losErrorInvalidInput => + 'Ongeldige punten/hoogtegegevens voor LOS-berekening.'; + + @override + String get losRenameCustomPoint => 'Hernoem aangepast punt'; + + @override + String get losPointName => 'Puntnaam'; + + @override + String get losShowPanelTooltip => 'Toon LOS-paneel'; + + @override + String get losHidePanelTooltip => 'LOS-paneel verbergen'; + + @override + String get losElevationAttribution => + 'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 55bc6ec..8815472 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -323,6 +323,10 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_aboutDescription => 'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Imię'; @@ -621,6 +625,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Bufor Map Offline'; + @override + String get appSettings_unitsTitle => 'Jednostki'; + + @override + String get appSettings_unitsMetric => 'Metryczne (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperialne (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; @@ -1241,6 +1254,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_title => 'Mapa węzłów'; + @override + String get map_lineOfSight => 'Linia wzroku'; + + @override + String get map_losScreenTitle => 'Linia wzroku'; + @override String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi'; @@ -2721,6 +2740,116 @@ class AppLocalizationsPl extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Wyczyść ścieżkę'; + @override + String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.'; + + @override + String losRunFailed(String error) { + return 'Sprawdzenie pola widzenia nie powiodło się: $error'; + } + + @override + String get losClearAllPoints => 'Wyczyść wszystkie punkty'; + + @override + String get losRunToViewElevationProfile => + 'Uruchom LOS, aby wyświetlić profil wysokości'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty'; + + @override + String get losShowDisplayNodes => 'Pokaż węzły wyświetlające'; + + @override + String get losCustomPoints => 'Punkty niestandardowe'; + + @override + String losCustomPointLabel(int index) { + return 'Niestandardowe $index'; + } + + @override + String get losPointA => 'Punkt A'; + + @override + String get losPointB => 'Punkt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Uruchom LOS-a'; + + @override + String get losNoElevationData => 'Brak danych o wysokości'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: sprawdzam...'; + + @override + String get losStatusNoData => 'LOS: brak danych'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane'; + } + + @override + String get losErrorElevationUnavailable => + 'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.'; + + @override + String get losErrorInvalidInput => + 'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.'; + + @override + String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego'; + + @override + String get losPointName => 'Nazwa punktu'; + + @override + String get losShowPanelTooltip => 'Pokaż panel LOS'; + + @override + String get losHidePanelTooltip => 'Ukryj panel LOS'; + + @override + String get losElevationAttribution => + 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 596d268..c7fc707 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -324,6 +324,10 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_aboutDescription => 'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Dados de elevação LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nome'; @@ -620,6 +624,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Cache de Mapa Offline'; + @override + String get appSettings_unitsTitle => 'Unidades'; + + @override + String get appSettings_unitsMetric => 'Métrico (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (ft/mi)'; + @override String get appSettings_noAreaSelected => 'Nenhuma área selecionada'; @@ -1240,6 +1253,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_title => 'Mapa de Nós'; + @override + String get map_lineOfSight => 'Linha de visão'; + + @override + String get map_losScreenTitle => 'Linha de visão'; + @override String get map_noNodesWithLocation => 'Não existem nós com dados de localização.'; @@ -2723,6 +2742,116 @@ class AppLocalizationsPt extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Limpar caminho'; + @override + String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.'; + + @override + String losRunFailed(String error) { + return 'Falha na verificação da linha de visão: $error'; + } + + @override + String get losClearAllPoints => 'Limpe todos os pontos'; + + @override + String get losRunToViewElevationProfile => + 'Execute o LOS para visualizar o perfil de elevação'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados'; + + @override + String get losShowDisplayNodes => 'Mostrar nós de exibição'; + + @override + String get losCustomPoints => 'Pontos personalizados'; + + @override + String losCustomPointLabel(int index) { + return '$index personalizado'; + } + + @override + String get losPointA => 'Ponto A'; + + @override + String get losPointB => 'Ponto B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Executar LOS'; + + @override + String get losNoElevationData => 'Sem dados de elevação'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: verificando...'; + + @override + String get losStatusNoData => 'LOS: sem dados'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido'; + } + + @override + String get losErrorElevationUnavailable => + 'Dados de elevação indisponíveis para uma ou mais amostras.'; + + @override + String get losErrorInvalidInput => + 'Dados de pontos/elevação inválidos para cálculo de LOS.'; + + @override + String get losRenameCustomPoint => 'Renomear ponto personalizado'; + + @override + String get losPointName => 'Nome do ponto'; + + @override + String get losShowPanelTooltip => 'Mostrar painel LOS'; + + @override + String get losHidePanelTooltip => 'Ocultar painel LOS'; + + @override + String get losElevationAttribution => + 'Dados de elevação: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4647746..2e992bd 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -321,6 +321,10 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_aboutDescription => 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Данные о высоте LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Имя'; @@ -620,6 +624,15 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; + @override + String get appSettings_unitsTitle => 'Единицы'; + + @override + String get appSettings_unitsMetric => 'Метрическая (м/км)'; + + @override + String get appSettings_unitsImperial => 'Имперская (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Область не выбрана'; @@ -1242,6 +1255,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_title => 'Карта нод'; + @override + String get map_lineOfSight => 'Линия видимости'; + + @override + String get map_losScreenTitle => 'Линия видимости'; + @override String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; @@ -2726,6 +2745,116 @@ class AppLocalizationsRu extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Очистить путь'; + @override + String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.'; + + @override + String losRunFailed(String error) { + return 'Проверка прямой видимости не удалась: $error'; + } + + @override + String get losClearAllPoints => 'Очистить все точки'; + + @override + String get losRunToViewElevationProfile => + 'Запустите LOS, чтобы просмотреть профиль высот.'; + + @override + String get losMenuTitle => 'ЛОС Меню'; + + @override + String get losMenuSubtitle => + 'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.'; + + @override + String get losShowDisplayNodes => 'Показать узлы отображения'; + + @override + String get losCustomPoints => 'Пользовательские точки'; + + @override + String losCustomPointLabel(int index) { + return 'Пользовательский $index'; + } + + @override + String get losPointA => 'Точка А'; + + @override + String get losPointB => 'Точка Б'; + + @override + String losAntennaA(String value, String unit) { + return 'Антенна А: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Антенна Б: $value $unit'; + } + + @override + String get losRun => 'Запустить ЛОС'; + + @override + String get losNoElevationData => 'Нет данных о высоте'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, заблокирован $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'ЛОС: проверяю...'; + + @override + String get losStatusNoData => 'ЛОС: нет данных'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.'; + } + + @override + String get losErrorElevationUnavailable => + 'Данные о высоте недоступны для одного или нескольких образцов.'; + + @override + String get losErrorInvalidInput => + 'Неверные данные о точках/высоте для расчета LOS.'; + + @override + String get losRenameCustomPoint => 'Переименовать пользовательскую точку'; + + @override + String get losPointName => 'Имя точки'; + + @override + String get losShowPanelTooltip => 'Показать панель LOS'; + + @override + String get losHidePanelTooltip => 'Скрыть панель LOS'; + + @override + String get losElevationAttribution => + 'Данные о высоте: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8e18663..a51e059 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -320,6 +320,10 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_aboutDescription => 'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Meno'; @@ -614,6 +618,15 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; + @override + String get appSettings_unitsTitle => 'Jednotky'; + + @override + String get appSettings_unitsMetric => 'Metrické (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperiálne (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť'; @@ -1236,6 +1249,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_title => 'Mapa uzlov'; + @override + String get map_lineOfSight => 'Line of Sight'; + + @override + String get map_losScreenTitle => 'Line of Sight'; + @override String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe'; @@ -2709,6 +2728,116 @@ class AppLocalizationsSk extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Zmazať cestu'; + @override + String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.'; + + @override + String losRunFailed(String error) { + return 'Kontrola priamej viditeľnosti zlyhala: $error'; + } + + @override + String get losClearAllPoints => 'Vymazať všetky body'; + + @override + String get losRunToViewElevationProfile => + 'Ak chcete zobraziť výškový profil, spustite LOS'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body'; + + @override + String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia'; + + @override + String get losCustomPoints => 'Vlastné body'; + + @override + String losCustomPointLabel(int index) { + return 'Vlastné $index'; + } + + @override + String get losPointA => 'Bod A'; + + @override + String get losPointB => 'Bod B'; + + @override + String losAntennaA(String value, String unit) { + return 'Anténa A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Anténa B: $value $unit'; + } + + @override + String get losRun => 'Spustite LOS'; + + @override + String get losNoElevationData => 'Žiadne údaje o nadmorskej výške'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blokovaný $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: kontrolujem...'; + + @override + String get losStatusNoData => 'LOS: žiadne údaje'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme'; + } + + @override + String get losErrorElevationUnavailable => + 'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.'; + + @override + String get losErrorInvalidInput => + 'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.'; + + @override + String get losRenameCustomPoint => 'Premenovať vlastný bod'; + + @override + String get losPointName => 'Názov bodu'; + + @override + String get losShowPanelTooltip => 'Zobraziť panel LOS'; + + @override + String get losHidePanelTooltip => 'Skryť panel LOS'; + + @override + String get losElevationAttribution => + 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index b95e711..5ac7e8b 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -319,6 +319,10 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_aboutDescription => 'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Podatki o višini LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Ime'; @@ -615,6 +619,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; + @override + String get appSettings_unitsTitle => 'Enote'; + + @override + String get appSettings_unitsMetric => 'Metrična (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperialno (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Območje ni izbrano'; @@ -1231,6 +1244,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_title => 'Mapa omrežja'; + @override + String get map_lineOfSight => 'Linija vida'; + + @override + String get map_losScreenTitle => 'Linija vida'; + @override String get map_noNodesWithLocation => 'Nihče od notranjih elementov nima podatkov o lokaciji.'; @@ -2712,6 +2731,116 @@ class AppLocalizationsSl extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Počisti pot'; + @override + String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.'; + + @override + String losRunFailed(String error) { + return 'Preverjanje vidnega polja ni uspelo: $error'; + } + + @override + String get losClearAllPoints => 'Počisti vse točke'; + + @override + String get losRunToViewElevationProfile => + 'Zaženite LOS za ogled višinskega profila'; + + @override + String get losMenuTitle => 'LOS meni'; + + @override + String get losMenuSubtitle => + 'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri'; + + @override + String get losShowDisplayNodes => 'Pokaži prikazna vozlišča'; + + @override + String get losCustomPoints => 'Točke po meri'; + + @override + String losCustomPointLabel(int index) { + return 'Po meri $index'; + } + + @override + String get losPointA => 'Točka A'; + + @override + String get losPointB => 'Točka B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Zaženi LOS'; + + @override + String get losNoElevationData => 'Ni podatkov o višini'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blokiral $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: preverjam ...'; + + @override + String get losStatusNoData => 'LOS: ni podatkov'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total jasno, $blocked blokirano, $unknown neznano'; + } + + @override + String get losErrorElevationUnavailable => + 'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.'; + + @override + String get losErrorInvalidInput => + 'Neveljavni podatki o točkah/višini za izračun LOS.'; + + @override + String get losRenameCustomPoint => 'Preimenujte točko po meri'; + + @override + String get losPointName => 'Ime točke'; + + @override + String get losShowPanelTooltip => 'Pokaži ploščo LOS'; + + @override + String get losHidePanelTooltip => 'Skrij ploščo LOS'; + + @override + String get losElevationAttribution => + 'Podatki o višini: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 10047ca..6d355d9 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -317,6 +317,10 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_aboutDescription => 'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS-höjddata: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Namn'; @@ -610,6 +614,15 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Kartcache'; + @override + String get appSettings_unitsTitle => 'Enheter'; + + @override + String get appSettings_unitsMetric => 'Metriskt (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperialt (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Ingen area markerad'; @@ -1228,6 +1241,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_title => 'Nodkarta'; + @override + String get map_lineOfSight => 'Synlinje'; + + @override + String get map_losScreenTitle => 'Synlinje'; + @override String get map_noNodesWithLocation => 'Inga noder med platsinformation'; @@ -2697,6 +2716,114 @@ class AppLocalizationsSv extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Rensa väg'; + @override + String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.'; + + @override + String losRunFailed(String error) { + return 'Synlinjekontroll misslyckades: $error'; + } + + @override + String get losClearAllPoints => 'Rensa alla punkter'; + + @override + String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil'; + + @override + String get losMenuTitle => 'LOS-menyn'; + + @override + String get losMenuSubtitle => + 'Tryck på noder eller tryck länge på kartan för anpassade punkter'; + + @override + String get losShowDisplayNodes => 'Visa displaynoder'; + + @override + String get losCustomPoints => 'Anpassade poäng'; + + @override + String losCustomPointLabel(int index) { + return 'Anpassad $index'; + } + + @override + String get losPointA => 'Punkt A'; + + @override + String get losPointB => 'Punkt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenn A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenn B: $value $unit'; + } + + @override + String get losRun => 'Kör LOS'; + + @override + String get losNoElevationData => 'Inga höjddata'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blockerad av $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: kollar...'; + + @override + String get losStatusNoData => 'LOS: inga data'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd'; + } + + @override + String get losErrorElevationUnavailable => + 'Höjddata är inte tillgänglig för ett eller flera prover.'; + + @override + String get losErrorInvalidInput => + 'Ogiltiga poäng/höjddata för LOS-beräkning.'; + + @override + String get losRenameCustomPoint => 'Byt namn på anpassad punkt'; + + @override + String get losPointName => 'Punktnamn'; + + @override + String get losShowPanelTooltip => 'Visa LOS-panelen'; + + @override + String get losHidePanelTooltip => 'Dölj LOS-panelen'; + + @override + String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 9edc64a..0f3d550 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -322,6 +322,10 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_aboutDescription => 'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Дані про висоту LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Ім\'я'; @@ -618,6 +622,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; + @override + String get appSettings_unitsTitle => 'одиниці'; + + @override + String get appSettings_unitsMetric => 'Метричний (м / км)'; + + @override + String get appSettings_unitsImperial => 'Імперська (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Область не вибрано'; @@ -1240,6 +1253,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_title => 'Карта вузлів'; + @override + String get map_lineOfSight => 'Пряма видимість'; + + @override + String get map_losScreenTitle => 'Пряма видимість'; + @override String get map_noNodesWithLocation => 'Немає вузлів з даними про розташування'; @@ -2733,6 +2752,117 @@ class AppLocalizationsUk extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Очистити шлях'; + @override + String get losSelectStartEnd => + 'Виберіть початковий і кінцевий вузли для LOS.'; + + @override + String losRunFailed(String error) { + return 'Помилка перевірки прямої видимості: $error'; + } + + @override + String get losClearAllPoints => 'Очистити всі пункти'; + + @override + String get losRunToViewElevationProfile => + 'Запустіть LOS, щоб переглянути профіль висоти'; + + @override + String get losMenuTitle => 'Меню LOS'; + + @override + String get losMenuSubtitle => + 'Торкніться вузлів або утримуйте карту, щоб отримати власні точки'; + + @override + String get losShowDisplayNodes => 'Показати вузли відображення'; + + @override + String get losCustomPoints => 'Користувальницькі точки'; + + @override + String losCustomPointLabel(int index) { + return 'Спеціальний $index'; + } + + @override + String get losPointA => 'Точка А'; + + @override + String get losPointB => 'Точка Б'; + + @override + String losAntennaA(String value, String unit) { + return 'Антена A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Антена B: $value $unit'; + } + + @override + String get losRun => 'Запустіть LOS'; + + @override + String get losNoElevationData => 'Немає даних про висоту'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, заблоковано $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: перевірка...'; + + @override + String get losStatusNoData => 'LOS: немає даних'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо'; + } + + @override + String get losErrorElevationUnavailable => + 'Дані про висоту недоступні для одного чи кількох зразків.'; + + @override + String get losErrorInvalidInput => + 'Недійсні дані про точки/висоту для розрахунку LOS.'; + + @override + String get losRenameCustomPoint => 'Перейменуйте спеціальну точку'; + + @override + String get losPointName => 'Назва точки'; + + @override + String get losShowPanelTooltip => 'Показати панель LOS'; + + @override + String get losHidePanelTooltip => 'Приховати панель LOS'; + + @override + String get losElevationAttribution => + 'Дані про висоту: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9753da6..36a114a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -307,6 +307,10 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_aboutDescription => '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS 高程数据:Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => '姓名'; @@ -585,6 +589,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_offlineMapCache => '离线地图缓存'; + @override + String get appSettings_unitsTitle => '单位'; + + @override + String get appSettings_unitsMetric => '公制(米/公里)'; + + @override + String get appSettings_unitsImperial => '英制 (ft / mi)'; + @override String get appSettings_noAreaSelected => '未选择任何区域'; @@ -1182,6 +1195,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_title => '节点图'; + @override + String get map_lineOfSight => '视线'; + + @override + String get map_losScreenTitle => '视线'; + @override String get map_noNodesWithLocation => '没有包含位置信息的节点'; @@ -2579,6 +2598,111 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathTrace_clearTooltip => '清除路径'; + @override + String get losSelectStartEnd => '选择 LOS 的起始节点和结束节点。'; + + @override + String losRunFailed(String error) { + return '视线检查失败:$error'; + } + + @override + String get losClearAllPoints => '清除所有点'; + + @override + String get losRunToViewElevationProfile => '运行 LOS 查看高程剖面'; + + @override + String get losMenuTitle => '服务水平菜单'; + + @override + String get losMenuSubtitle => '点击节点或长按地图以获取自定义点'; + + @override + String get losShowDisplayNodes => '显示显示节点'; + + @override + String get losCustomPoints => '自定义积分'; + + @override + String losCustomPointLabel(int index) { + return '自定义 $index'; + } + + @override + String get losPointA => 'A点'; + + @override + String get losPointB => 'B点'; + + @override + String losAntennaA(String value, String unit) { + return '天线 A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return '天线 B:$value $unit'; + } + + @override + String get losRun => '运行视距'; + + @override + String get losNoElevationData => '无海拔数据'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit,清除 LOS,最小间隙 $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit,被 $obstruction $heightUnit 阻止'; + } + + @override + String get losStatusChecking => '洛斯:正在检查...'; + + @override + String get losStatusNoData => 'LOS:无数据'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS:$clear/$total 清除,$blocked 阻塞,$unknown 未知'; + } + + @override + String get losErrorElevationUnavailable => '一个或多个样本的海拔数据不可用。'; + + @override + String get losErrorInvalidInput => '用于 LOS 计算的点/高程数据无效。'; + + @override + String get losRenameCustomPoint => '重命名自定义点'; + + @override + String get losPointName => '点名称'; + + @override + String get losShowPanelTooltip => '显示 LOS 面板'; + + @override + String get losHidePanelTooltip => '隐藏 LOS 面板'; + + @override + String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 859e48d..733e4dc 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Toon alle paden", "settings_clientRepeat": "Herhalen: Afgekoppeld", "settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.", - "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist." + "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.", + "settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Eenheden", + "appSettings_unitsMetric": "Metrisch (m / km)", + "appSettings_unitsImperial": "Imperiaal (ft / mi)", + "map_lineOfSight": "Zichtlijn", + "map_losScreenTitle": "Zichtlijn", + "losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.", + "losRunFailed": "Zichtlijncontrole mislukt: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Wis alle punten", + "losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken", + "losMenuTitle": "LOS-menu", + "losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten", + "losShowDisplayNodes": "Toon weergaveknooppunten", + "losCustomPoints": "Aangepaste punten", + "losCustomPointLabel": "Aangepast {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punt A", + "losPointB": "Punt B", + "losAntennaA": "Antenne A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenne B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Voer LOS uit", + "losNoElevationData": "Geen hoogtegegevens", + "losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: controleren...", + "losStatusNoData": "LOS: geen gegevens", + "losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.", + "losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.", + "losRenameCustomPoint": "Hernoem aangepast punt", + "losPointName": "Puntnaam", + "losShowPanelTooltip": "Toon LOS-paneel", + "losHidePanelTooltip": "LOS-paneel verbergen", + "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d03b911..35efee1 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Pokaż wszystkie ścieżki", "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", - "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz." + "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.", + "settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Jednostki", + "appSettings_unitsMetric": "Metryczne (m / km)", + "appSettings_unitsImperial": "Imperialne (ft / mi)", + "map_lineOfSight": "Linia wzroku", + "map_losScreenTitle": "Linia wzroku", + "losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.", + "losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Wyczyść wszystkie punkty", + "losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty", + "losShowDisplayNodes": "Pokaż węzły wyświetlające", + "losCustomPoints": "Punkty niestandardowe", + "losCustomPointLabel": "Niestandardowe {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punkt A", + "losPointB": "Punkt B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Uruchom LOS-a", + "losNoElevationData": "Brak danych o wysokości", + "losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: sprawdzam...", + "losStatusNoData": "LOS: brak danych", + "losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.", + "losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.", + "losRenameCustomPoint": "Zmień nazwę punktu niestandardowego", + "losPointName": "Nazwa punktu", + "losShowPanelTooltip": "Pokaż panel LOS", + "losHidePanelTooltip": "Ukryj panel LOS", + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 83a7719..fd742d9 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Mostrar todos os caminhos", "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", "settings_clientRepeat": "Repetição sem rede", - "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos." + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.", + "settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unidades", + "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsImperial": "Imperial (ft/mi)", + "map_lineOfSight": "Linha de visão", + "map_losScreenTitle": "Linha de visão", + "losSelectStartEnd": "Selecione nós iniciais e finais para LOS.", + "losRunFailed": "Falha na verificação da linha de visão: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Limpe todos os pontos", + "losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados", + "losShowDisplayNodes": "Mostrar nós de exibição", + "losCustomPoints": "Pontos personalizados", + "losCustomPointLabel": "{index} personalizado", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Ponto A", + "losPointB": "Ponto B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Executar LOS", + "losNoElevationData": "Sem dados de elevação", + "losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: verificando...", + "losStatusNoData": "LOS: sem dados", + "losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.", + "losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.", + "losRenameCustomPoint": "Renomear ponto personalizado", + "losPointName": "Nome do ponto", + "losShowPanelTooltip": "Mostrar painel LOS", + "losHidePanelTooltip": "Ocultar painel LOS", + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 380ba10..04b2e04 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -839,5 +839,120 @@ "chat_ShowAllPaths": "Показать все пути", "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", - "settings_clientRepeat": "Повторение \"вне сети\"" + "settings_clientRepeat": "Повторение \"вне сети\"", + "settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Единицы", + "appSettings_unitsMetric": "Метрическая (м/км)", + "appSettings_unitsImperial": "Имперская (ft / mi)", + "map_lineOfSight": "Линия видимости", + "map_losScreenTitle": "Линия видимости", + "losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.", + "losRunFailed": "Проверка прямой видимости не удалась: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Очистить все точки", + "losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.", + "losMenuTitle": "ЛОС Меню", + "losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.", + "losShowDisplayNodes": "Показать узлы отображения", + "losCustomPoints": "Пользовательские точки", + "losCustomPointLabel": "Пользовательский {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антенна А: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Антенна Б: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Запустить ЛОС", + "losNoElevationData": "Нет данных о высоте", + "losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "ЛОС: проверяю...", + "losStatusNoData": "ЛОС: нет данных", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.", + "losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.", + "losRenameCustomPoint": "Переименовать пользовательскую точку", + "losPointName": "Имя точки", + "losShowPanelTooltip": "Показать панель LOS", + "losHidePanelTooltip": "Скрыть панель LOS", + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index aca4a29..6663094 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Zobraziť všetky cesty", "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", - "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných." + "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.", + "settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Jednotky", + "appSettings_unitsMetric": "Metrické (m / km)", + "appSettings_unitsImperial": "Imperiálne (ft / mi)", + "map_lineOfSight": "Line of Sight", + "map_losScreenTitle": "Line of Sight", + "losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.", + "losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Vymazať všetky body", + "losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body", + "losShowDisplayNodes": "Zobraziť uzly zobrazenia", + "losCustomPoints": "Vlastné body", + "losCustomPointLabel": "Vlastné {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Bod A", + "losPointB": "Bod B", + "losAntennaA": "Anténa A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Anténa B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Spustite LOS", + "losNoElevationData": "Žiadne údaje o nadmorskej výške", + "losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: kontrolujem...", + "losStatusNoData": "LOS: žiadne údaje", + "losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.", + "losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.", + "losRenameCustomPoint": "Premenovať vlastný bod", + "losPointName": "Názov bodu", + "losShowPanelTooltip": "Zobraziť panel LOS", + "losHidePanelTooltip": "Skryť panel LOS", + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 59b8434..50a9043 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Prikaži vse poti", "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", - "settings_clientRepeat": "Neovadno ponavljanje" + "settings_clientRepeat": "Neovadno ponavljanje", + "settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Enote", + "appSettings_unitsMetric": "Metrična (m/km)", + "appSettings_unitsImperial": "Imperialno (ft / mi)", + "map_lineOfSight": "Linija vida", + "map_losScreenTitle": "Linija vida", + "losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.", + "losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Počisti vse točke", + "losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila", + "losMenuTitle": "LOS meni", + "losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri", + "losShowDisplayNodes": "Pokaži prikazna vozlišča", + "losCustomPoints": "Točke po meri", + "losCustomPointLabel": "Po meri {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Točka A", + "losPointB": "Točka B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Zaženi LOS", + "losNoElevationData": "Ni podatkov o višini", + "losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: preverjam ...", + "losStatusNoData": "LOS: ni podatkov", + "losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.", + "losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.", + "losRenameCustomPoint": "Preimenujte točko po meri", + "losPointName": "Ime točke", + "losShowPanelTooltip": "Pokaži ploščo LOS", + "losHidePanelTooltip": "Skrij ploščo LOS", + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index fa786f7..260a34b 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Visa alla vägar", "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", "settings_clientRepeat": "Upprepa utan elnät", - "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz." + "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.", + "settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Enheter", + "appSettings_unitsMetric": "Metriskt (m/km)", + "appSettings_unitsImperial": "Imperialt (ft / mi)", + "map_lineOfSight": "Synlinje", + "map_losScreenTitle": "Synlinje", + "losSelectStartEnd": "Välj start- och slutnoder för LOS.", + "losRunFailed": "Synlinjekontroll misslyckades: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Rensa alla punkter", + "losRunToViewElevationProfile": "Kör LOS för att se höjdprofil", + "losMenuTitle": "LOS-menyn", + "losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter", + "losShowDisplayNodes": "Visa displaynoder", + "losCustomPoints": "Anpassade poäng", + "losCustomPointLabel": "Anpassad {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punkt A", + "losPointB": "Punkt B", + "losAntennaA": "Antenn A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenn B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Kör LOS", + "losNoElevationData": "Inga höjddata", + "losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: kollar...", + "losStatusNoData": "LOS: inga data", + "losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.", + "losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.", + "losRenameCustomPoint": "Byt namn på anpassad punkt", + "losPointName": "Punktnamn", + "losShowPanelTooltip": "Visa LOS-panelen", + "losHidePanelTooltip": "Dölj LOS-panelen", + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3f7b276..ec414b4 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Показати всі шляхи", "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", - "settings_clientRepeat": "Автономна система" + "settings_clientRepeat": "Автономна система", + "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "одиниці", + "appSettings_unitsMetric": "Метричний (м / км)", + "appSettings_unitsImperial": "Імперська (ft / mi)", + "map_lineOfSight": "Пряма видимість", + "map_losScreenTitle": "Пряма видимість", + "losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.", + "losRunFailed": "Помилка перевірки прямої видимості: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Очистити всі пункти", + "losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти", + "losMenuTitle": "Меню LOS", + "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", + "losShowDisplayNodes": "Показати вузли відображення", + "losCustomPoints": "Користувальницькі точки", + "losCustomPointLabel": "Спеціальний {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Антена B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Запустіть LOS", + "losNoElevationData": "Немає даних про висоту", + "losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: перевірка...", + "losStatusNoData": "LOS: немає даних", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", + "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", + "losRenameCustomPoint": "Перейменуйте спеціальну точку", + "losPointName": "Назва точки", + "losShowPanelTooltip": "Показати панель LOS", + "losHidePanelTooltip": "Приховати панель LOS", + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index bc43392..6b072c9 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "显示所有路径", "settings_clientRepeat": "离网重复", "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", - "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。" + "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。", + "settings_aboutOpenMeteoAttribution": "LOS 高程数据:Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "单位", + "appSettings_unitsMetric": "公制(米/公里)", + "appSettings_unitsImperial": "英制 (ft / mi)", + "map_lineOfSight": "视线", + "map_losScreenTitle": "视线", + "losSelectStartEnd": "选择 LOS 的起始节点和结束节点。", + "losRunFailed": "视线检查失败:{error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "清除所有点", + "losRunToViewElevationProfile": "运行 LOS 查看高程剖面", + "losMenuTitle": "服务水平菜单", + "losMenuSubtitle": "点击节点或长按地图以获取自定义点", + "losShowDisplayNodes": "显示显示节点", + "losCustomPoints": "自定义积分", + "losCustomPointLabel": "自定义 {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "A点", + "losPointB": "B点", + "losAntennaA": "天线 A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "天线 B:{value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "运行视距", + "losNoElevationData": "无海拔数据", + "losProfileClear": "{distance} {distanceUnit},清除 LOS,最小间隙 {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit},被 {obstruction} {heightUnit} 阻止", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "洛斯:正在检查...", + "losStatusNoData": "LOS:无数据", + "losStatusSummary": "LOS:{clear}/{total} 清除,{blocked} 阻塞,{unknown} 未知", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "一个或多个样本的海拔数据不可用。", + "losErrorInvalidInput": "用于 LOS 计算的点/高程数据无效。", + "losRenameCustomPoint": "重命名自定义点", + "losPointName": "点名称", + "losShowPanelTooltip": "显示 LOS 面板", + "losHidePanelTooltip": "隐藏 LOS 面板", + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" } diff --git a/lib/main.dart b/lib/main.dart index 8ee0ca4..3650a7e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter/foundation.dart'; import 'l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -47,6 +48,7 @@ void main() async { final notificationService = NotificationService(); await notificationService.initialize(); await backgroundService.initialize(); + _registerThirdPartyLicenses(); // Wire up connector with services connector.initialize( @@ -80,6 +82,27 @@ void main() async { ); } +void _registerThirdPartyLicenses() { + LicenseRegistry.addLicense(() async* { + yield const LicenseEntryWithLineBreaks( + ['Open-Meteo Elevation API Data'], + ''' +Data used by LOS elevation lookups is provided by Open-Meteo. + +Open-Meteo terms and attribution: +https://open-meteo.com/en/terms + +Elevation API: +https://open-meteo.com/en/docs/elevation-api + +Attribution license reference: +Creative Commons Attribution 4.0 International (CC BY 4.0) +https://creativecommons.org/licenses/by/4.0/ +''', + ); + }); +} + class MeshCoreApp extends StatelessWidget { final MeshCoreConnector connector; final MessageRetryService retryService; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 3edb68f..229a7a6 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -1,3 +1,16 @@ +enum UnitSystem { metric, imperial } + +extension UnitSystemValue on UnitSystem { + String get value { + switch (this) { + case UnitSystem.imperial: + return 'imperial'; + case UnitSystem.metric: + return 'metric'; + } + } +} + class AppSettings { static const Object _unset = Object(); @@ -21,6 +34,7 @@ class AppSettings { final String? languageOverride; // null = system default final bool appDebugLogEnabled; final Map batteryChemistryByDeviceId; + final UnitSystem unitSystem; AppSettings({ this.clearPathOnMaxRetry = false, @@ -43,6 +57,7 @@ class AppSettings { this.languageOverride, this.appDebugLogEnabled = false, Map? batteryChemistryByDeviceId, + this.unitSystem = UnitSystem.metric, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}; Map toJson() { @@ -67,10 +82,18 @@ class AppSettings { 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, + 'unit_system': unitSystem.value, }; } factory AppSettings.fromJson(Map json) { + UnitSystem parseUnitSystem(dynamic value) { + if (value is String && value.toLowerCase() == 'imperial') { + return UnitSystem.imperial; + } + return UnitSystem.metric; + } + return AppSettings( clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false, mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true, @@ -101,6 +124,9 @@ class AppSettings { (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, + unitSystem: parseUnitSystem( + json['unit_system'] ?? json['los_unit_system'], + ), ); } @@ -125,6 +151,7 @@ class AppSettings { Object? languageOverride = _unset, bool? appDebugLogEnabled, Map? batteryChemistryByDeviceId, + UnitSystem? unitSystem, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -154,6 +181,7 @@ class AppSettings { appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, + unitSystem: unitSystem ?? this.unitSystem, ); } } diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index 5372ea8..4877038 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../services/app_debug_log_service.dart'; +import '../widgets/adaptive_app_bar_title.dart'; class AppDebugLogScreen extends StatelessWidget { const AppDebugLogScreen({super.key}); @@ -17,7 +18,7 @@ class AppDebugLogScreen extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text(context.l10n.debugLog_appTitle), + title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle), centerTitle: true, actions: [ IconButton( diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 4e31733..b309b4d 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -3,8 +3,10 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../models/app_settings.dart'; import '../services/app_settings_service.dart'; import '../services/notification_service.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import 'map_cache_screen.dart'; class AppSettingsScreen extends StatelessWidget { @@ -14,7 +16,7 @@ class AppSettingsScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.l10n.appSettings_title), + title: AdaptiveAppBarTitle(context.l10n.appSettings_title), centerTitle: true, ), body: SafeArea( @@ -360,6 +362,18 @@ class AppSettingsScreen extends StatelessWidget { onTap: () => _showTimeFilterDialog(context, settingsService), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.straighten), + title: Text(context.l10n.appSettings_unitsTitle), + subtitle: Text( + settingsService.settings.unitSystem == UnitSystem.imperial + ? context.l10n.appSettings_unitsImperial + : context.l10n.appSettings_unitsMetric, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => _showUnitsDialog(context, settingsService), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.download_outlined), title: Text(context.l10n.appSettings_offlineMapCache), @@ -706,6 +720,46 @@ class AppSettingsScreen extends StatelessWidget { ); } + void _showUnitsDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.appSettings_unitsTitle), + content: RadioGroup( + groupValue: settingsService.settings.unitSystem, + onChanged: (value) { + if (value != null) { + settingsService.setUnitSystem(value); + Navigator.pop(context); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(context.l10n.appSettings_unitsMetric), + leading: const Radio(value: UnitSystem.metric), + ), + ListTile( + title: Text(context.l10n.appSettings_unitsImperial), + leading: const Radio(value: UnitSystem.imperial), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_close), + ), + ], + ), + ); + } + Widget _buildDebugCard( BuildContext context, AppSettingsService settingsService, diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 7cebb76..88f734b 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import '../l10n/l10n.dart'; import '../services/ble_debug_log_service.dart'; import '../connector/meshcore_protocol.dart'; +import '../widgets/adaptive_app_bar_title.dart'; enum _BleLogView { frames, rawLogRx } @@ -29,7 +30,7 @@ class _BleDebugLogScreenState extends State { : rawEntries.isNotEmpty; return Scaffold( appBar: AppBar( - title: Text(context.l10n.debugLog_bleTitle), + title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle), actions: [ IconButton( tooltip: context.l10n.debugLog_copyLog, diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index e6fcacc..2d1faa3 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -9,11 +9,14 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../services/map_tile_cache_service.dart'; +import '../services/app_settings_service.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/app_localizations.dart'; import '../l10n/l10n.dart'; import '../models/channel_message.dart'; +import '../models/app_settings.dart'; import '../models/contact.dart'; +import '../widgets/adaptive_app_bar_title.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; @@ -48,7 +51,7 @@ class ChannelMessagePathScreen extends StatelessWidget { final extraPaths = _otherPaths(primaryPath, message.pathVariants); return Scaffold( appBar: AppBar( - title: Text(l10n.channelPath_title), + title: AdaptiveAppBarTitle(l10n.channelPath_title), actions: [ IconButton( icon: const Icon(Icons.radar_outlined), @@ -297,8 +300,12 @@ class ChannelMessagePathMapScreen extends StatefulWidget { class _ChannelMessagePathMapScreenState extends State { + static const double _labelZoomThreshold = 8.5; + Uint8List? _selectedPath; double _pathDistance = 0.0; + bool _showNodeLabels = true; + bool _didReceivePositionUpdate = false; @override void initState() { @@ -333,6 +340,8 @@ class _ChannelMessagePathMapScreenState Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { + final settings = context.watch().settings; + final isImperial = settings.unitSystem == UnitSystem.imperial; final tileCache = context.read(); final primaryPath = _selectPrimaryPath( widget.message.pathBytes, @@ -393,6 +402,9 @@ class _ChannelMessagePathMapScreenState ? points.first : const LatLng(0, 0); final initialZoom = points.isNotEmpty ? 13.0 : 2.0; + if (!_didReceivePositionUpdate) { + _showNodeLabels = initialZoom >= _labelZoomThreshold; + } final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null; @@ -402,7 +414,9 @@ class _ChannelMessagePathMapScreenState _pathDistance = _getPathDistance(points); return Scaffold( - appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)), + appBar: AppBar( + title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle), + ), body: SafeArea( top: false, child: Stack( @@ -424,6 +438,17 @@ class _ChannelMessagePathMapScreenState interactionOptions: InteractionOptions( flags: ~InteractiveFlag.rotate, ), + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (!_didReceivePositionUpdate || + shouldShow != _showNodeLabels) { + if (!mounted) return; + setState(() { + _didReceivePositionUpdate = true; + _showNodeLabels = shouldShow; + }); + } + }, ), children: [ TileLayer( @@ -435,7 +460,12 @@ class _ChannelMessagePathMapScreenState ), if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), - MarkerLayer(markers: _buildHopMarkers(hops)), + MarkerLayer( + markers: _buildHopMarkers( + hops, + showLabels: _showNodeLabels, + ), + ), ], ), if (observedPaths.length > 1) @@ -458,7 +488,7 @@ class _ChannelMessagePathMapScreenState ), ), ), - _buildLegendCard(context, hops), + _buildLegendCard(context, hops, isImperial), ], ), ), @@ -530,45 +560,61 @@ class _ChannelMessagePathMapScreenState ); } - List _buildHopMarkers(List<_PathHop> hops) { - return [ - for (final hop in hops) - if (hop.hasLocation) - Marker( - point: hop.position!, - width: 35, - height: 35, - child: Container( - decoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: Text( - hop.index.toString(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + List _buildHopMarkers( + List<_PathHop> hops, { + required bool showLabels, + }) { + final markers = []; + for (final hop in hops) { + if (!hop.hasLocation) continue; + final point = hop.position!; + markers.add( + Marker( + point: point, + width: 35, + height: 35, + child: Container( + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), ), + ], + ), + alignment: Alignment.center, + child: Text( + hop.index.toString(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), ), ), - if (context.read().selfLatitude != null && - context.read().selfLongitude != null) - Marker( - point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, + ), + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: point, + label: hop.contact?.name ?? _formatPrefix(hop.prefix), ), + ); + } + } + + final selfLat = context.read().selfLatitude; + final selfLon = context.read().selfLongitude; + if (selfLat != null && selfLon != null) { + final selfPoint = LatLng(selfLat, selfLon); + markers.add( + Marker( + point: selfPoint, width: 35, height: 35, child: Container( @@ -595,10 +641,63 @@ class _ChannelMessagePathMapScreenState ), ), ), - ]; + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: selfPoint, + label: context.l10n.pathTrace_you, + ), + ); + } + } + + return markers; } - Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) { + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { + return Marker( + point: point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildLegendCard( + BuildContext context, + List<_PathHop> hops, + bool isImperial, + ) { final l10n = context.l10n; final maxHeight = MediaQuery.of(context).size.height * 0.35; final estimatedHeight = 72.0 + (hops.length * 56.0); @@ -617,7 +716,7 @@ class _ChannelMessagePathMapScreenState Padding( padding: const EdgeInsets.all(12), child: Text( - '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}', style: const TextStyle(fontWeight: FontWeight.w600), ), ), diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index a2914a1..9f8602d 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -6,6 +6,7 @@ import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/qr_scanner_widget.dart'; /// Screen for scanning community QR codes to join communities. @@ -29,7 +30,7 @@ class _CommunityQrScannerScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.l10n.community_scanQr), + title: AdaptiveAppBarTitle(context.l10n.community_scanQr), centerTitle: true, ), body: _isProcessing diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d470107..a6828dd 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1170,12 +1170,17 @@ class _ContactTile extends StatelessWidget { backgroundColor: _getTypeColor(contact.type), child: _buildContactAvatar(contact), ), - title: Text(contact.name), + title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(contact.pathLabel), - Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)), + Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis), + Text( + contact.shortPubKeyHex, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), ], ), // Clamp text scaling in trailing section to prevent overflow while @@ -1186,26 +1191,32 @@ class _ContactTile extends StatelessWidget { MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(height: 4), - ], - Text( - _formatLastSeen(context, lastSeen), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + child: SizedBox( + width: 120, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (unreadCount > 0) ...[ + UnreadBadge(count: unreadCount), + const SizedBox(height: 4), ], - ), - ], + Text( + _formatLastSeen(context, lastSeen), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + ], + ), + ], + ), ), ), onTap: onTap, diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart new file mode 100644 index 0000000..a2d4b4a --- /dev/null +++ b/lib/screens/line_of_sight_map_screen.dart @@ -0,0 +1,1005 @@ +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../l10n/l10n.dart'; +import '../screens/channels_screen.dart'; +import '../screens/contacts_screen.dart'; +import '../models/app_settings.dart'; +import '../services/app_settings_service.dart'; +import '../services/line_of_sight_service.dart'; +import '../services/map_tile_cache_service.dart'; +import '../utils/route_transitions.dart'; +import '../widgets/app_bar.dart'; +import '../widgets/quick_switch_bar.dart'; + +class LineOfSightEndpoint { + final String label; + final LatLng point; + final Color color; + final IconData icon; + final bool isCustom; + + const LineOfSightEndpoint({ + required this.label, + required this.point, + this.color = Colors.green, + this.icon = Icons.location_on, + this.isCustom = false, + }); +} + +class LineOfSightMapScreen extends StatefulWidget { + final String title; + final List candidates; + + const LineOfSightMapScreen({ + super.key, + required this.title, + required this.candidates, + }); + + @override + State createState() => _LineOfSightMapScreenState(); +} + +class _LineOfSightMapScreenState extends State { + static const String _errorSelectStartEnd = 'los_error_select_start_end'; + static const double _metersToFeet = 3.28084; + static const double _kmToMiles = 0.621371; + static const double _maxAntennaFeet = 400.0; + static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet; + static const double _labelZoomThreshold = 8.5; + + final LineOfSightService _lineOfSightService = LineOfSightService(); + + bool _loading = false; + String? _error; + LineOfSightPathResult? _result; + LineOfSightEndpoint? _start; + LineOfSightEndpoint? _end; + final List _customEndpoints = []; + double _startAntennaHeight = 5.0; + double _endAntennaHeight = 5.0; + bool _showHud = true; + bool _menuExpanded = true; + bool _showDisplayNodes = true; + bool _showMarkerLabels = true; + bool _didReceivePositionUpdate = false; + int _losRequestNonce = 0; + + @override + void initState() { + super.initState(); + if (widget.candidates.isNotEmpty) { + _start = widget.candidates.first; + if (widget.candidates.length > 1) { + _end = widget.candidates[1]; + } + } + _runLos(); + } + + @override + void dispose() { + _lineOfSightService.dispose(); + super.dispose(); + } + + Future _runLos() async { + final start = _start; + final end = _end; + final startAntenna = _startAntennaHeight; + final endAntenna = _endAntennaHeight; + final requestId = ++_losRequestNonce; + if (start == null || end == null) { + setState(() { + _result = null; + _error = _errorSelectStartEnd; + }); + return; + } + + setState(() { + _loading = true; + _error = null; + }); + + try { + final result = await _lineOfSightService.analyzePath( + [start.point, end.point], + startAntennaHeightMeters: startAntenna, + endAntennaHeightMeters: endAntenna, + ); + if (!mounted) return; + if (!_isRunRequestCurrent( + requestId: requestId, + start: start, + end: end, + startAntenna: startAntenna, + endAntenna: endAntenna, + )) { + return; + } + setState(() { + _result = result; + }); + } catch (e) { + if (!mounted) return; + if (!_isRunRequestCurrent( + requestId: requestId, + start: start, + end: end, + startAntenna: startAntenna, + endAntenna: endAntenna, + )) { + return; + } + setState(() { + _result = null; + _error = context.l10n.losRunFailed(e.toString()); + }); + } finally { + if (mounted && requestId == _losRequestNonce) { + setState(() { + _loading = false; + }); + } + } + } + + bool _isRunRequestCurrent({ + required int requestId, + required LineOfSightEndpoint start, + required LineOfSightEndpoint end, + required double startAntenna, + required double endAntenna, + }) { + return requestId == _losRequestNonce && + identical(_start, start) && + identical(_end, end) && + _startAntennaHeight == startAntenna && + _endAntennaHeight == endAntenna; + } + + void _selectFromMap(LineOfSightEndpoint endpoint) { + setState(() { + _result = null; + _error = null; + if (_start == null || (_start != null && _end != null)) { + _start = endpoint; + if (_end == endpoint) _end = null; + } else { + _end = endpoint; + if (_start == endpoint) _start = null; + } + }); + + if (_start != null && _end != null) { + _runLos(); + } + } + + void _addCustomPoint(LatLng point) { + final endpoint = LineOfSightEndpoint( + label: context.l10n.losCustomPointLabel(_customEndpoints.length + 1), + point: point, + color: Colors.orange, + icon: Icons.push_pin, + isCustom: true, + ); + setState(() { + _customEndpoints.add(endpoint); + }); + _selectFromMap(endpoint); + } + + List _visibleEndpoints() { + return [if (_showDisplayNodes) ...widget.candidates, ..._customEndpoints]; + } + + bool _hasEndpoint( + List endpoints, + LineOfSightEndpoint? e, + ) { + if (e == null) return false; + return endpoints.any((item) => identical(item, e)); + } + + void _sanitizeSelection() { + final visible = _visibleEndpoints(); + if (!_hasEndpoint(visible, _start)) { + _start = null; + } + if (!_hasEndpoint(visible, _end)) { + _end = null; + } + } + + void _clearAllPoints() { + setState(() { + _customEndpoints.clear(); + _start = null; + _end = null; + _result = null; + _error = _errorSelectStartEnd; + }); + } + + void _deleteCustomPoint(LineOfSightEndpoint endpoint) { + setState(() { + _customEndpoints.removeWhere((e) => identical(e, endpoint)); + if (identical(_start, endpoint)) _start = null; + if (identical(_end, endpoint)) _end = null; + _result = null; + }); + } + + Future _renameCustomPoint(LineOfSightEndpoint endpoint) async { + final controller = TextEditingController(text: endpoint.label); + final newLabel = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.losRenameCustomPoint), + content: TextField( + controller: controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: context.l10n.losPointName, + ), + autofocus: true, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () { + final value = controller.text.trim(); + Navigator.pop(dialogContext, value); + }, + child: Text(context.l10n.common_save), + ), + ], + ), + ); + + if (newLabel == null || newLabel.isEmpty) return; + final index = _customEndpoints.indexWhere((e) => identical(e, endpoint)); + if (index < 0) return; + final renamed = LineOfSightEndpoint( + label: newLabel, + point: endpoint.point, + color: endpoint.color, + icon: endpoint.icon, + isCustom: endpoint.isCustom, + ); + setState(() { + _customEndpoints[index] = renamed; + if (identical(_start, endpoint)) _start = renamed; + if (identical(_end, endpoint)) _end = renamed; + }); + } + + @override + Widget build(BuildContext context) { + final settings = context.watch().settings; + final isImperial = settings.unitSystem == UnitSystem.imperial; + final tileCache = context.read(); + final endpoints = _visibleEndpoints(); + final mapPoints = [ + if (_start != null) _start!.point, + if (_end != null) _end!.point, + ]; + final initialCenter = mapPoints.isNotEmpty + ? mapPoints.first + : const LatLng(0, 0); + final bounds = mapPoints.length > 1 + ? LatLngBounds.fromPoints(mapPoints) + : null; + final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0; + if (!_didReceivePositionUpdate) { + _showMarkerLabels = initialZoom >= _labelZoomThreshold; + } + + return Scaffold( + appBar: AppBar( + title: AppBarTitle(widget.title), + centerTitle: true, + actions: [ + IconButton( + icon: _loading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.delete_outline), + onPressed: _loading ? null : _clearAllPoints, + tooltip: context.l10n.losClearAllPoints, + ), + ], + ), + body: Stack( + children: [ + FlutterMap( + options: MapOptions( + initialCenter: initialCenter, + initialZoom: initialZoom, + initialCameraFit: bounds == null + ? null + : CameraFit.bounds( + bounds: bounds, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + ), + onLongPress: (_, point) => _addCustomPoint(point), + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (!_didReceivePositionUpdate || + shouldShow != _showMarkerLabels) { + setState(() { + _didReceivePositionUpdate = true; + _showMarkerLabels = shouldShow; + }); + } + }, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + if (_result != null && _result!.segments.isNotEmpty) + PolylineLayer(polylines: _buildSegmentPolylines(_result!)), + MarkerLayer(markers: _buildMarkers(endpoints)), + ], + ), + if (_showHud) + Positioned( + left: 12, + right: 12, + top: 12, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.52, + ), + child: _buildControlPanel(isImperial), + ), + ), + if (!_showHud && _result != null && _result!.segments.isNotEmpty) + Positioned( + left: 12, + bottom: 12, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Text( + context.l10n.losElevationAttribution, + style: const TextStyle(fontSize: 10, color: Colors.white), + ), + ), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + setState(() { + _showHud = !_showHud; + }); + }, + tooltip: _showHud + ? context.l10n.losHidePanelTooltip + : context.l10n.losShowPanelTooltip, + child: Icon(_showHud ? Icons.visibility_off : Icons.tune), + ), + bottomNavigationBar: SafeArea( + top: false, + child: QuickSwitchBar( + selectedIndex: 2, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + ), + ), + ); + } + + Widget _buildControlPanel(bool isImperial) { + _sanitizeSelection(); + final segment = _primarySegmentResult(); + final endpoints = _visibleEndpoints(); + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + final antennaAMeters = _startAntennaHeight; + final antennaBMeters = _endAntennaHeight; + final antennaADisplay = _toDisplayHeight(antennaAMeters, isImperial); + final antennaBDisplay = _toDisplayHeight(antennaBMeters, isImperial); + final antennaSliderMax = isImperial ? _maxAntennaFeet : _maxAntennaMeters; + final antennaSliderDivisions = isImperial ? 400 : 122; + return Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (segment != null) + SizedBox( + height: 160, + width: double.infinity, + child: CustomPaint( + painter: _LosProfilePainter( + samples: segment.samples, + distanceUnit: distanceUnit, + heightUnit: heightUnit, + badgeTextStyle: + Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ) ?? + const TextStyle( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ) + else + SizedBox( + height: 44, + child: Center( + child: Text( + context.l10n.losRunToViewElevationProfile, + style: const TextStyle(fontSize: 11), + ), + ), + ), + const SizedBox(height: 8), + Text( + segment != null + ? _profileStats(segment, isImperial) + : _statusText(), + style: TextStyle( + fontSize: 12, + color: segment != null + ? (segment.isClear ? Colors.green : Colors.red) + : _statusColor(), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.losElevationAttribution, + style: TextStyle(fontSize: 10, color: Colors.grey[700]), + ), + const SizedBox(height: 6), + ExpansionTile( + initiallyExpanded: _menuExpanded, + onExpansionChanged: (value) { + setState(() { + _menuExpanded = value; + }); + }, + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + title: Text( + context.l10n.losMenuTitle, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + subtitle: Text( + context.l10n.losMenuSubtitle, + style: const TextStyle(fontSize: 11), + ), + children: [ + SwitchListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text( + context.l10n.losShowDisplayNodes, + style: const TextStyle(fontSize: 12), + ), + value: _showDisplayNodes, + onChanged: (value) { + setState(() { + _showDisplayNodes = value; + _sanitizeSelection(); + _result = null; + }); + }, + ), + if (_customEndpoints.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + context.l10n.losCustomPoints, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + for (final point in _customEndpoints) + ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text( + point.label, + style: const TextStyle(fontSize: 12), + ), + subtitle: Text( + '${point.point.latitude.toStringAsFixed(5)}, ${point.point.longitude.toStringAsFixed(5)}', + style: const TextStyle(fontSize: 11), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit, size: 18), + onPressed: () => _renameCustomPoint(point), + tooltip: context.l10n.common_edit, + ), + IconButton( + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: () => _deleteCustomPoint(point), + tooltip: context.l10n.common_delete, + ), + ], + ), + ), + ], + const SizedBox(height: 8), + _buildEndpointRow( + label: context.l10n.losPointA, + value: _start, + candidates: endpoints, + onChanged: (value) { + setState(() { + _start = value; + _result = null; + }); + if (_start != null && _end != null) { + _runLos(); + } + }, + ), + const SizedBox(height: 8), + _buildEndpointRow( + label: context.l10n.losPointB, + value: _end, + candidates: endpoints, + onChanged: (value) { + setState(() { + _end = value; + _result = null; + }); + if (_start != null && _end != null) { + _runLos(); + } + }, + ), + const SizedBox(height: 10), + Text( + context.l10n.losAntennaA( + antennaADisplay.toStringAsFixed(1), + heightUnit, + ), + style: const TextStyle(fontSize: 12), + ), + Slider( + value: antennaADisplay, + min: 0, + max: antennaSliderMax, + divisions: antennaSliderDivisions, + onChanged: (value) { + setState(() { + _startAntennaHeight = _toMetersHeight( + value, + isImperial, + ); + }); + }, + ), + Text( + context.l10n.losAntennaB( + antennaBDisplay.toStringAsFixed(1), + heightUnit, + ), + style: const TextStyle(fontSize: 12), + ), + Slider( + value: antennaBDisplay, + min: 0, + max: antennaSliderMax, + divisions: antennaSliderDivisions, + onChanged: (value) { + setState(() { + _endAntennaHeight = _toMetersHeight(value, isImperial); + }); + }, + ), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: _loading ? null : _runLos, + icon: const Icon(Icons.visibility), + label: Text(context.l10n.losRun), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildEndpointRow({ + required String label, + required LineOfSightEndpoint? value, + required List candidates, + required ValueChanged onChanged, + }) { + return Row( + children: [ + SizedBox( + width: 54, + child: Text(label, style: const TextStyle(fontSize: 12)), + ), + Expanded( + child: DropdownButton( + value: value, + isExpanded: true, + items: candidates + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e.label, overflow: TextOverflow.ellipsis), + ), + ) + .toList(), + onChanged: onChanged, + ), + ), + ], + ); + } + + LineOfSightResult? _primarySegmentResult() { + if (_result == null || _result!.segments.isEmpty) return null; + return _result!.segments.first.result; + } + + String _profileStats(LineOfSightResult result, bool isImperial) { + final distance = isImperial + ? (result.totalDistanceMeters / 1000.0) * _kmToMiles + : result.totalDistanceMeters / 1000.0; + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + final minClearance = result.samples.isEmpty + ? 0.0 + : result.samples.map((s) => s.clearanceMeters).reduce(math.min); + final minClearanceDisplay = isImperial + ? minClearance * _metersToFeet + : minClearance; + final maxObstructionDisplay = isImperial + ? result.maxObstructionMeters * _metersToFeet + : result.maxObstructionMeters; + if (!result.hasData) { + return _localizedLosError(result.errorMessage); + } + if (result.isClear) { + return context.l10n.losProfileClear( + distance.toStringAsFixed(1), + distanceUnit, + minClearanceDisplay.toStringAsFixed(1), + heightUnit, + ); + } + return context.l10n.losProfileBlocked( + distance.toStringAsFixed(1), + distanceUnit, + maxObstructionDisplay.toStringAsFixed(1), + heightUnit, + ); + } + + List _buildSegmentPolylines(LineOfSightPathResult result) { + final polylines = []; + for (final segment in result.segments) { + final color = !segment.result.hasData + ? Colors.grey + : (segment.result.isClear ? Colors.green : Colors.red); + polylines.add( + Polyline( + points: [segment.start, segment.end], + strokeWidth: 4, + color: color, + ), + ); + } + return polylines; + } + + List _buildMarkers(List endpoints) { + return [ + for (final endpoint in endpoints) + Marker( + point: endpoint.point, + width: 36, + height: 36, + child: GestureDetector( + onTap: () => _selectFromMap(endpoint), + child: Container( + decoration: BoxDecoration( + color: endpoint.color, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: const [ + BoxShadow(color: Colors.black26, blurRadius: 4), + ], + ), + child: Stack( + children: [ + Center( + child: Icon(endpoint.icon, color: Colors.white, size: 16), + ), + if (endpoint == _start || endpoint == _end) + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(7), + border: Border.all(color: Colors.white, width: 1), + ), + alignment: Alignment.center, + child: Text( + endpoint == _start ? 'A' : 'B', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 9, + ), + ), + ), + ), + ], + ), + ), + ), + ), + for (final endpoint in endpoints) + if (_showMarkerLabels) + Marker( + point: endpoint.point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + endpoint.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ), + ), + ), + ), + ), + ]; + } + + String _statusText() { + if (_loading) return context.l10n.losStatusChecking; + if (_error == _errorSelectStartEnd) { + return context.l10n.losSelectStartEnd; + } + if (_error != null) return _error!; + if (_result == null) return context.l10n.losStatusNoData; + final total = _result!.segments.length; + return context.l10n.losStatusSummary( + _result!.clearSegments, + total, + _result!.blockedSegments, + _result!.unknownSegments, + ); + } + + Color _statusColor() { + if (_error != null) return Colors.red; + if (_loading) return Colors.orange; + if (_result == null) return Colors.grey; + if (_result!.blockedSegments > 0) return Colors.red; + if (_result!.clearSegments > 0) return Colors.green; + return Colors.grey; + } + + double _toDisplayHeight(double meters, bool isImperial) { + return isImperial ? meters * _metersToFeet : meters; + } + + double _toMetersHeight(double displayHeight, bool isImperial) { + return isImperial ? displayHeight / _metersToFeet : displayHeight; + } + + String _localizedLosError(String? message) { + if (message == LineOfSightService.errorElevationUnavailable) { + return context.l10n.losErrorElevationUnavailable; + } + if (message == LineOfSightService.errorInvalidInput) { + return context.l10n.losErrorInvalidInput; + } + return context.l10n.losNoElevationData; + } + + void _handleQuickSwitch(int index, BuildContext context) { + if (index == 2) { + Navigator.pop(context); + return; + } + switch (index) { + case 0: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)), + ); + break; + case 1: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)), + ); + break; + } + } +} + +class _LosProfilePainter extends CustomPainter { + final List samples; + final String distanceUnit; + final String heightUnit; + final TextStyle badgeTextStyle; + + const _LosProfilePainter({ + required this.samples, + required this.distanceUnit, + required this.heightUnit, + required this.badgeTextStyle, + }); + + @override + void paint(Canvas canvas, Size size) { + final bg = Paint()..color = const Color(0xFF243A63); + canvas.drawRect(Offset.zero & size, bg); + _drawUnitBadge(canvas, size); + + if (samples.length < 2) return; + + final minY = samples + .map((s) => math.min(s.terrainMeters, s.lineHeightMeters)) + .reduce(math.min); + final maxY = samples + .map((s) => math.max(s.terrainMeters, s.lineHeightMeters)) + .reduce(math.max); + final ySpan = math.max(1.0, maxY - minY); + final maxDist = math.max(1.0, samples.last.distanceMeters); + + Offset mapPoint(double x, double y) { + final px = (x / maxDist) * size.width; + final py = size.height - ((y - minY) / ySpan) * size.height; + return Offset(px, py); + } + + final terrainPath = ui.Path(); + terrainPath.moveTo(0, size.height); + for (final s in samples) { + final p = mapPoint(s.distanceMeters, s.terrainMeters); + terrainPath.lineTo(p.dx, p.dy); + } + terrainPath.lineTo(size.width, size.height); + terrainPath.close(); + + canvas.drawPath(terrainPath, Paint()..color = const Color(0xCC7C6F5D)); + + final terrainLine = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint(samples[i].distanceMeters, samples[i].terrainMeters); + if (i == 0) { + terrainLine.moveTo(p.dx, p.dy); + } else { + terrainLine.lineTo(p.dx, p.dy); + } + } + canvas.drawPath( + terrainLine, + Paint() + ..color = const Color(0xFF9FE870) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + + final losLine = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].lineHeightMeters, + ); + if (i == 0) { + losLine.moveTo(p.dx, p.dy); + } else { + losLine.lineTo(p.dx, p.dy); + } + } + canvas.drawPath( + losLine, + Paint() + ..color = const Color(0xFFE0E7FF) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + } + + @override + bool shouldRepaint(covariant _LosProfilePainter oldDelegate) { + return oldDelegate.samples != samples || + oldDelegate.distanceUnit != distanceUnit || + oldDelegate.heightUnit != heightUnit || + oldDelegate.badgeTextStyle != badgeTextStyle; + } + + void _drawUnitBadge(Canvas canvas, Size size) { + final span = TextSpan( + text: '$heightUnit / $distanceUnit', + style: badgeTextStyle, + ); + final painter = TextPainter(text: span, textDirection: TextDirection.ltr) + ..layout(); + painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); + } +} diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 3f61109..1391660 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -7,6 +7,7 @@ import '../l10n/app_localizations.dart'; import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; +import '../widgets/adaptive_app_bar_title.dart'; class MapCacheScreen extends StatefulWidget { const MapCacheScreen({super.key}); @@ -224,7 +225,10 @@ class _MapCacheScreenState extends State { : (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble(); return Scaffold( - appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true), + appBar: AppBar( + title: AdaptiveAppBarTitle(l10n.mapCache_title), + centerTitle: true, + ), body: Column( children: [ Expanded( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 1fad04b..8c13a71 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -11,6 +11,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../connector/meshcore_protocol.dart'; +import '../models/app_settings.dart'; import '../models/channel.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; @@ -26,6 +27,7 @@ import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; +import 'line_of_sight_map_screen.dart'; class MapScreen extends StatefulWidget { final LatLng? highlightPosition; @@ -46,6 +48,8 @@ class MapScreen extends StatefulWidget { } class _MapScreenState extends State { + static const double _labelZoomThreshold = 8.5; + final MapController _mapController = MapController(); final MapMarkerService _markerService = MapMarkerService(); final Set _hiddenMarkerIds = {}; @@ -58,6 +62,7 @@ class _MapScreenState extends State { final List _points = []; final List _polylines = []; bool _legendExpanded = false; + bool _showNodeLabels = true; @override void initState() { @@ -247,6 +252,7 @@ class _MapScreenState extends State { // Re center map after removed markers have loaded if (!_hasInitializedMap && _removedMarkersLoaded) { _hasInitializedMap = true; + _showNodeLabels = initialZoom >= _labelZoomThreshold; if (hasMapContent) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -272,6 +278,47 @@ class _MapScreenState extends State { onPressed: () => _startPath(), tooltip: context.l10n.contacts_pathTrace, ), + if (!_isBuildingPathTrace) + IconButton( + icon: const Icon(Icons.visibility), + onPressed: () { + final candidates = []; + if (connector.selfLatitude != null && + connector.selfLongitude != null) { + candidates.add( + LineOfSightEndpoint( + label: context.l10n.pathTrace_you, + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + color: Colors.teal, + icon: Icons.person_pin_circle, + ), + ); + } + for (final c in contactsWithLocation) { + candidates.add( + LineOfSightEndpoint( + label: c.name, + point: LatLng(c.latitude!, c.longitude!), + color: _getNodeColor(c.type), + icon: _getNodeIcon(c.type), + ), + ); + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LineOfSightMapScreen( + title: context.l10n.map_losScreenTitle, + candidates: candidates, + ), + ), + ); + }, + tooltip: context.l10n.map_lineOfSight, + ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( @@ -350,6 +397,14 @@ class _MapScreenState extends State { position: latLng, ); }, + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (shouldShow != _showNodeLabels && mounted) { + setState(() { + _showNodeLabels = shouldShow; + }); + } + }, ), children: [ TileLayer( @@ -374,7 +429,11 @@ class _MapScreenState extends State { size: 34, ), ), - ..._buildMarkers(contactsWithLocation, settings), + ..._buildMarkers( + contactsWithLocation, + settings, + showLabels: _showNodeLabels, + ), ...sharedMarkers.map(_buildSharedMarker), if (connector.selfLatitude != null && connector.selfLongitude != null) @@ -413,6 +472,16 @@ class _MapScreenState extends State { ), ), ), + if (_showNodeLabels && + connector.selfLatitude != null && + connector.selfLongitude != null) + _buildNodeLabelMarker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + label: connector.deviceDisplayName, + ), ], ), ], @@ -444,7 +513,11 @@ class _MapScreenState extends State { ); } - List _buildMarkers(List contacts, settings) { + List _buildMarkers( + List contacts, + settings, { + required bool showLabels, + }) { final markers = []; for (final contact in contacts) { @@ -499,11 +572,57 @@ class _MapScreenState extends State { ); markers.add(marker); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: LatLng(contact.latitude!, contact.longitude!), + label: contact.name, + ), + ); + } } return markers; } + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { + return Marker( + point: point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ), + ); + } + Color _getNodeColor(int type) { switch (type) { case advTypeChat: @@ -1519,6 +1638,9 @@ class _MapScreenState extends State { Widget _buildPathTraceOverlay() { final l10n = context.l10n; + final isImperial = + context.read().settings.unitSystem == + UnitSystem.imperial; return Positioned( top: 16, left: 16, @@ -1539,7 +1661,7 @@ class _MapScreenState extends State { const SizedBox(height: 6), if (_pathTrace.isNotEmpty) Text( - "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}", + "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", style: TextStyle(fontSize: 12, color: Colors.grey[700]), ), SelectableText( @@ -1549,8 +1671,10 @@ class _MapScreenState extends State { style: TextStyle(fontSize: 18), ), const SizedBox(height: 6), - Row( - mainAxisSize: MainAxisSize.min, + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, children: [ if (_pathTrace.isNotEmpty) ElevatedButton( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 8e24bee..c1f7f44 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -8,7 +8,9 @@ import 'package:latlong2/latlong.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/l10n/l10n.dart'; +import 'package:meshcore_open/models/app_settings.dart'; import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/services/app_settings_service.dart'; import 'package:meshcore_open/services/map_tile_cache_service.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; @@ -27,8 +29,11 @@ double getPathDistanceMeters(List points) { return distanceMeters; } -String formatDistance(double distanceMeters) { - return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)'; +String formatDistance(double distanceMeters, {required bool isImperial}) { + if (isImperial) { + return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)'; + } + return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)'; } class PathTraceData { @@ -64,6 +69,8 @@ class PathTraceMapScreen extends StatefulWidget { } class _PathTraceMapScreenState extends State { + static const double _labelZoomThreshold = 8.5; + StreamSubscription? _frameSubscription; Timer? _timeoutTimer; @@ -78,6 +85,7 @@ class _PathTraceMapScreenState extends State { LatLngBounds? _bounds; ValueKey _mapKey = const ValueKey('initial'); double _pathDistanceMeters = 0.0; + bool _showNodeLabels = true; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -291,6 +299,8 @@ class _PathTraceMapScreenState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { + final settings = context.watch().settings; + final isImperial = settings.unitSystem == UnitSystem.imperial; final tileCache = context.read(); return Scaffold( @@ -355,7 +365,8 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (_hasData) _buildLegendCard(context, _traceData!), + if (_hasData) + _buildLegendCard(context, _traceData!, isImperial), ], ), ), @@ -364,55 +375,61 @@ class _PathTraceMapScreenState extends State { ); } - List _buildHopMarkers(List pathData) { - return [ - for (final hop in pathData) - if (_traceData!.pathContacts[hop] != null && - _traceData!.pathContacts[hop]!.hasLocation) - Marker( - point: LatLng( - _traceData!.pathContacts[hop]!.latitude!, - _traceData!.pathContacts[hop]!.longitude!, - ), - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: Text( - _traceData!.pathContacts[hop]!.publicKey - .sublist(0, 1) - .map( - (b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(), - ) - .join(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ), - if (context.read().selfLatitude != null && - context.read().selfLongitude != null) + List _buildHopMarkers( + List pathData, { + required bool showLabels, + }) { + final markers = []; + for (final hop in pathData) { + final contact = _traceData!.pathContacts[hop]; + if (contact == null || !contact.hasLocation) continue; + final point = LatLng(contact.latitude!, contact.longitude!); + markers.add( Marker( - point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, + point: point, + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + contact.publicKey + .sublist(0, 1) + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), ), + ), + ); + if (showLabels) { + markers.add(_buildNodeLabelMarker(point: point, label: contact.name)); + } + } + + final selfLat = context.read().selfLatitude; + final selfLon = context.read().selfLongitude; + if (selfLat != null && selfLon != null) { + final selfPoint = LatLng(selfLat, selfLon); + markers.add( + Marker( + point: selfPoint, width: 35, height: 35, child: Container( @@ -440,7 +457,56 @@ class _PathTraceMapScreenState extends State { ), ), ), - ]; + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: selfPoint, + label: context.l10n.pathTrace_you, + ), + ); + } + } + + return markers; + } + + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { + return Marker( + point: point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ), + ); } String formatDirectionText(PathTraceData pathTraceData, int index) { @@ -520,6 +586,14 @@ class _PathTraceMapScreenState extends State { ), minZoom: 2.0, maxZoom: 18.0, + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (shouldShow != _showNodeLabels && mounted) { + setState(() { + _showNodeLabels = shouldShow; + }); + } + }, ), children: [ TileLayer( @@ -530,12 +604,21 @@ class _PathTraceMapScreenState extends State { ), if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines), if (_traceData!.pathData.isNotEmpty) - MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)), + MarkerLayer( + markers: _buildHopMarkers( + _traceData!.pathData, + showLabels: _showNodeLabels, + ), + ), ], ); } - Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) { + Widget _buildLegendCard( + BuildContext context, + PathTraceData pathTraceData, + bool isImperial, + ) { final l10n = context.l10n; final maxHeight = MediaQuery.of(context).size.height * 0.35; final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0); @@ -554,7 +637,7 @@ class _PathTraceMapScreenState extends State { Padding( padding: const EdgeInsets.all(12), child: Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}', + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}', style: const TextStyle(fontWeight: FontWeight.w600), ), ), diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 932e29c..af9d75e 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; import 'contacts_screen.dart'; @@ -70,7 +71,7 @@ class _ScannerScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.l10n.scanner_title), + title: AdaptiveAppBarTitle(context.l10n.scanner_title), centerTitle: true, automaticallyImplyLeading: false, ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 94d541b..a198f99 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -41,7 +42,10 @@ class _SettingsScreenState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return Scaffold( - appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true), + appBar: AppBar( + title: AdaptiveAppBarTitle(l10n.settings_title), + centerTitle: true, + ), body: SafeArea( top: false, child: Consumer( diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 8770938..88c204d 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -5,8 +5,10 @@ import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../models/path_selection.dart'; +import '../models/app_settings.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../helpers/cayenne_lpp.dart'; @@ -181,6 +183,8 @@ class _TelemetryScreenState extends State { Widget build(BuildContext context) { final l10n = context.l10n; final connector = context.watch(); + final settings = context.watch().settings; + final isImperialUnits = settings.unitSystem == UnitSystem.imperial; final repeater = _resolveRepeater(connector); final isFloodMode = repeater.pathOverride == -1; @@ -307,6 +311,7 @@ class _TelemetryScreenState extends State { entry['values'], l10n.telemetry_channelTitle(entry['channel']), entry['channel'], + isImperialUnits, ), ], ), @@ -319,6 +324,7 @@ class _TelemetryScreenState extends State { Map channelData, String title, int channel, + bool isImperialUnits, ) { final l10n = context.l10n; return Card( @@ -358,12 +364,12 @@ class _TelemetryScreenState extends State { else if (entry.key == 'temperature' && channel == 1) _buildInfoRow( l10n.telemetry_mcuTemperatureLabel, - _temperatureText(entry.value), + _temperatureText(entry.value, isImperialUnits), ) else if (entry.key == 'temperature') _buildInfoRow( l10n.telemetry_temperatureLabel, - _temperatureText(entry.value), + _temperatureText(entry.value, isImperialUnits), ) else if (entry.key == 'current' && channel == 1) _buildInfoRow( @@ -421,13 +427,13 @@ class _TelemetryScreenState extends State { return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); } - String _temperatureText(double? tempC) { + String _temperatureText(double? tempC, bool isImperialUnits) { final l10n = context.l10n; if (tempC == null) return l10n.common_notAvailable; final tempF = (tempC * 9 / 5) + 32; - return l10n.telemetry_temperatureValue( - tempC.toStringAsFixed(1), - tempF.toStringAsFixed(1), - ); + if (isImperialUnits) { + return '${tempF.toStringAsFixed(1)}°F'; + } + return '${tempC.toStringAsFixed(1)}°C'; } } diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index c1e8fc6..a85ab92 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -132,4 +132,14 @@ class AppSettingsService extends ChangeNotifier { _settings.copyWith(batteryChemistryByDeviceId: updated), ); } + + Future setUnitSystem(UnitSystem value) async { + await updateSettings(_settings.copyWith(unitSystem: value)); + } + + Future setLosUnitSystem(String value) async { + await setUnitSystem( + value == 'imperial' ? UnitSystem.imperial : UnitSystem.metric, + ); + } } diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index 0a9aeae..bc46b59 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; import '../connector/meshcore_protocol.dart'; class BleDebugLogEntry { @@ -44,6 +45,7 @@ class BleDebugLogService extends ChangeNotifier { static const int maxEntries = 500; final List _entries = []; final List _rawLogRxEntries = []; + bool _notifyScheduled = false; List get entries => List.unmodifiable(_entries); List get rawLogRxEntries => @@ -78,13 +80,31 @@ class BleDebugLogService extends ChangeNotifier { } } - notifyListeners(); + _notifyListenersSafely(); } void clear() { _entries.clear(); _rawLogRxEntries.clear(); - notifyListeners(); + _notifyListenersSafely(); + } + + void _notifyListenersSafely() { + final phase = SchedulerBinding.instance.schedulerPhase; + final canNotifyNow = + phase == SchedulerPhase.idle || + phase == SchedulerPhase.postFrameCallbacks; + if (canNotifyNow) { + notifyListeners(); + return; + } + + if (_notifyScheduled) return; + _notifyScheduled = true; + SchedulerBinding.instance.addPostFrameCallback((_) { + _notifyScheduled = false; + notifyListeners(); + }); } String _describeFrame( diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart new file mode 100644 index 0000000..e9f9f7b --- /dev/null +++ b/lib/services/line_of_sight_service.dart @@ -0,0 +1,406 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:math'; + +import 'package:http/http.dart' as http; +import 'package:latlong2/latlong.dart'; + +typedef ElevationDataSource = + Future> Function(List points); + +class LineOfSightSample { + final double distanceMeters; + final double terrainMeters; + final double lineHeightMeters; + final double clearanceMeters; + + const LineOfSightSample({ + required this.distanceMeters, + required this.terrainMeters, + required this.lineHeightMeters, + required this.clearanceMeters, + }); +} + +class LineOfSightResult { + final bool hasData; + final bool isClear; + final double totalDistanceMeters; + final double maxObstructionMeters; + final double? firstObstructionDistanceMeters; + final List samples; + final String? errorMessage; + + const LineOfSightResult({ + required this.hasData, + required this.isClear, + required this.totalDistanceMeters, + required this.maxObstructionMeters, + required this.firstObstructionDistanceMeters, + required this.samples, + this.errorMessage, + }); + + const LineOfSightResult.error({ + required this.totalDistanceMeters, + required this.errorMessage, + }) : hasData = false, + isClear = false, + maxObstructionMeters = 0, + firstObstructionDistanceMeters = null, + samples = const []; +} + +class LineOfSightPathSegment { + final int index; + final LatLng start; + final LatLng end; + final LineOfSightResult result; + + const LineOfSightPathSegment({ + required this.index, + required this.start, + required this.end, + required this.result, + }); +} + +class LineOfSightPathResult { + final List segments; + final int clearSegments; + final int blockedSegments; + final int unknownSegments; + + const LineOfSightPathResult({ + required this.segments, + required this.clearSegments, + required this.blockedSegments, + required this.unknownSegments, + }); +} + +class LineOfSightService { + static const String errorElevationUnavailable = + 'los_error_elevation_unavailable'; + static const String errorInvalidInput = 'los_error_invalid_input'; + + static const double _earthRadiusMeters = 6371000.0; + static const Distance _distance = Distance(); + static const Duration _cacheTtl = Duration(hours: 24); + static const int _maxFetchAttempts = 4; // initial try + 3 retries + static const Duration _initialBackoff = Duration(milliseconds: 300); + + final http.Client _httpClient; + final bool _ownsHttpClient; + final ElevationDataSource? _elevationDataSource; + final Map _elevationCache = {}; + + LineOfSightService({ + http.Client? httpClient, + ElevationDataSource? elevationDataSource, + }) : _httpClient = httpClient ?? http.Client(), + _ownsHttpClient = httpClient == null, + _elevationDataSource = elevationDataSource; + + Future analyzePath( + List points, { + double startAntennaHeightMeters = 1.5, + double endAntennaHeightMeters = 1.5, + double kFactor = 4.0 / 3.0, + double obstructionToleranceMeters = 0.0, + }) async { + if (points.length < 2) { + return const LineOfSightPathResult( + segments: [], + clearSegments: 0, + blockedSegments: 0, + unknownSegments: 0, + ); + } + + final segments = []; + var clearSegments = 0; + var blockedSegments = 0; + var unknownSegments = 0; + + for (int i = 0; i < points.length - 1; i++) { + final result = await analyzeLink( + points[i], + points[i + 1], + startAntennaHeightMeters: startAntennaHeightMeters, + endAntennaHeightMeters: endAntennaHeightMeters, + kFactor: kFactor, + obstructionToleranceMeters: obstructionToleranceMeters, + ); + segments.add( + LineOfSightPathSegment( + index: i, + start: points[i], + end: points[i + 1], + result: result, + ), + ); + + if (!result.hasData) { + unknownSegments++; + } else if (result.isClear) { + clearSegments++; + } else { + blockedSegments++; + } + } + + return LineOfSightPathResult( + segments: segments, + clearSegments: clearSegments, + blockedSegments: blockedSegments, + unknownSegments: unknownSegments, + ); + } + + Future analyzeLink( + LatLng start, + LatLng end, { + double startAntennaHeightMeters = 1.5, + double endAntennaHeightMeters = 1.5, + double kFactor = 4.0 / 3.0, + double obstructionToleranceMeters = 0.0, + }) async { + final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end); + if (totalDistanceMeters <= 1) { + return LineOfSightResult( + hasData: true, + isClear: true, + totalDistanceMeters: totalDistanceMeters, + maxObstructionMeters: 0, + firstObstructionDistanceMeters: null, + samples: const [], + ); + } + + final samplePoints = _buildSamplePoints(start, end, totalDistanceMeters); + final elevations = await _getElevations(samplePoints); + + if (elevations.any((e) => e == null)) { + return LineOfSightResult.error( + totalDistanceMeters: totalDistanceMeters, + errorMessage: errorElevationUnavailable, + ); + } + + return computeFromElevations( + points: samplePoints, + elevations: elevations.cast(), + startAntennaHeightMeters: startAntennaHeightMeters, + endAntennaHeightMeters: endAntennaHeightMeters, + kFactor: kFactor, + obstructionToleranceMeters: obstructionToleranceMeters, + ); + } + + static LineOfSightResult computeFromElevations({ + required List points, + required List elevations, + double startAntennaHeightMeters = 1.5, + double endAntennaHeightMeters = 1.5, + double kFactor = 4.0 / 3.0, + double obstructionToleranceMeters = 0.0, + }) { + if (points.length < 2 || elevations.length != points.length) { + return const LineOfSightResult.error( + totalDistanceMeters: 0, + errorMessage: errorInvalidInput, + ); + } + + final totalDistanceMeters = _distance.as( + LengthUnit.Meter, + points.first, + points.last, + ); + final effectiveEarthRadius = _earthRadiusMeters * kFactor; + final startLineHeight = elevations.first + startAntennaHeightMeters; + final endLineHeight = elevations.last + endAntennaHeightMeters; + + var maxObstructionMeters = 0.0; + double? firstObstructionDistanceMeters; + final samples = []; + var isClear = true; + + for (int i = 0; i < points.length; i++) { + final fraction = points.length == 1 ? 0.0 : i / (points.length - 1); + final distanceFromStart = totalDistanceMeters * fraction; + final lineHeight = + startLineHeight + (endLineHeight - startLineHeight) * fraction; + + final earthBulge = + (distanceFromStart * (totalDistanceMeters - distanceFromStart)) / + (2 * effectiveEarthRadius); + final terrainHeight = elevations[i] + earthBulge; + final clearance = lineHeight - terrainHeight; + + if (clearance < -obstructionToleranceMeters) { + isClear = false; + final obstruction = -clearance; + if (obstruction > maxObstructionMeters) { + maxObstructionMeters = obstruction; + } + firstObstructionDistanceMeters ??= distanceFromStart; + } + + samples.add( + LineOfSightSample( + distanceMeters: distanceFromStart, + terrainMeters: terrainHeight, + lineHeightMeters: lineHeight, + clearanceMeters: clearance, + ), + ); + } + + return LineOfSightResult( + hasData: true, + isClear: isClear, + totalDistanceMeters: totalDistanceMeters, + maxObstructionMeters: maxObstructionMeters, + firstObstructionDistanceMeters: firstObstructionDistanceMeters, + samples: samples, + ); + } + + List _buildSamplePoints( + LatLng start, + LatLng end, + double distanceMeters, + ) { + final sampleCount = distanceMeters < 2000 + ? 21 + : distanceMeters < 10000 + ? 41 + : 81; + + final points = []; + for (int i = 0; i < sampleCount; i++) { + final t = i / (sampleCount - 1); + points.add( + LatLng( + start.latitude + (end.latitude - start.latitude) * t, + start.longitude + (end.longitude - start.longitude) * t, + ), + ); + } + return points; + } + + Future> _getElevations(List points) async { + final dataSource = _elevationDataSource; + if (dataSource != null) { + return dataSource(points); + } + + final uncached = {}; + final values = List.filled(points.length, null); + for (int i = 0; i < points.length; i++) { + final key = _cacheKey(points[i]); + final cached = _readCachedValue(key); + if (cached != null) { + values[i] = cached; + } else { + uncached[i] = points[i]; + } + } + + if (uncached.isEmpty) return values; + + final latCsv = uncached.values + .map((p) => p.latitude.toStringAsFixed(6)) + .join(','); + final lonCsv = uncached.values + .map((p) => p.longitude.toStringAsFixed(6)) + .join(','); + + final uri = Uri.parse( + 'https://api.open-meteo.com/v1/elevation?latitude=$latCsv&longitude=$lonCsv', + ); + + final response = await _getWithBackoff(uri); + if (response.statusCode != 200) { + return values; + } + + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + return values; + } + final elevations = decoded['elevation']; + if (elevations is! List) { + return values; + } + + final indices = uncached.keys.toList(); + for (int i = 0; i < min(indices.length, elevations.length); i++) { + final value = elevations[i]; + if (value is! num) continue; + final index = indices[i]; + final elevation = value.toDouble(); + values[index] = elevation; + _elevationCache[_cacheKey(points[index])] = _CachedElevation( + value: elevation, + expiresAt: DateTime.now().add(_cacheTtl), + ); + } + return values; + } + + Future _getWithBackoff(Uri uri) async { + var attempt = 0; + Duration backoff = _initialBackoff; + + while (true) { + attempt++; + try { + final response = await _httpClient.get(uri); + if (!_shouldRetryStatus(response.statusCode) || + attempt >= _maxFetchAttempts) { + return response; + } + } catch (_) { + if (attempt >= _maxFetchAttempts) rethrow; + } + + await Future.delayed(backoff); + backoff *= 2; + } + } + + bool _shouldRetryStatus(int statusCode) { + return statusCode == 429 || statusCode >= 500; + } + + double? _readCachedValue(String key) { + final cached = _elevationCache[key]; + if (cached == null) return null; + if (DateTime.now().isAfter(cached.expiresAt)) { + _elevationCache.remove(key); + return null; + } + return cached.value; + } + + String _cacheKey(LatLng point) { + return '${point.latitude.toStringAsFixed(5)},${point.longitude.toStringAsFixed(5)}'; + } + + void dispose() { + if (_ownsHttpClient) { + _httpClient.close(); + } + } +} + +class _CachedElevation { + final double value; + final DateTime expiresAt; + + const _CachedElevation({required this.value, required this.expiresAt}); +} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index fc979c6..0b59bbc 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -58,11 +58,17 @@ class NotificationService { requestBadgePermission: true, requestSoundPermission: true, ); + const windowsSettings = WindowsInitializationSettings( + appName: 'MeshCore Open', + appUserModelId: 'org.meshcore.open.app', + guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86', + ); const initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, macOS: macSettings, + windows: windowsSettings, ); try { @@ -76,6 +82,13 @@ class NotificationService { } } + Future _ensureInitialized() async { + if (!_isInitialized) { + await initialize(); + } + return _isInitialized; + } + Future requestPermissions() async { if (!_isInitialized) { await initialize(); @@ -114,9 +127,7 @@ class NotificationService { String? contactId, int? badgeCount, }) async { - if (!_isInitialized) { - await initialize(); - } + if (!await _ensureInitialized()) return; final androidDetails = AndroidNotificationDetails( 'messages', @@ -148,13 +159,17 @@ class NotificationService { macOS: macDetails, ); - await _notifications.show( - id: contactId?.hashCode ?? 0, - title: contactName, - body: message, - notificationDetails: notificationDetails, - payload: 'message:$contactId', - ); + try { + await _notifications.show( + id: contactId?.hashCode ?? 0, + title: contactName, + body: message, + notificationDetails: notificationDetails, + payload: 'message:$contactId', + ); + } catch (e) { + debugPrint('Failed to show message notification: $e'); + } } Future _showAdvertNotificationImpl({ @@ -162,9 +177,7 @@ class NotificationService { required String contactType, String? contactId, }) async { - if (!_isInitialized) { - await initialize(); - } + if (!await _ensureInitialized()) return; const androidDetails = AndroidNotificationDetails( 'adverts', @@ -193,13 +206,17 @@ class NotificationService { macOS: macDetails, ); - await _notifications.show( - id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - title: _l10n.notification_newTypeDiscovered(contactType), - body: contactName, - notificationDetails: notificationDetails, - payload: 'advert:$contactId', - ); + try { + await _notifications.show( + id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: _l10n.notification_newTypeDiscovered(contactType), + body: contactName, + notificationDetails: notificationDetails, + payload: 'advert:$contactId', + ); + } catch (e) { + debugPrint('Failed to show advert notification: $e'); + } } Future _showChannelMessageNotificationImpl({ @@ -208,9 +225,7 @@ class NotificationService { int? channelIndex, int? badgeCount, }) async { - if (!_isInitialized) { - await initialize(); - } + if (!await _ensureInitialized()) return; final androidDetails = AndroidNotificationDetails( 'channel_messages', @@ -247,13 +262,17 @@ class NotificationService { ? _l10n.notification_receivedNewMessage : preview; - await _notifications.show( - id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - title: channelName, - body: body, - notificationDetails: notificationDetails, - payload: 'channel:$channelIndex', - ); + try { + await _notifications.show( + id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: channelName, + body: body, + notificationDetails: notificationDetails, + payload: 'channel:$channelIndex', + ); + } catch (e) { + debugPrint('Failed to show channel notification: $e'); + } } /// Returns a privacy-safe identifier for debug logging. @@ -396,35 +415,39 @@ class NotificationService { Future _showNotificationImmediately( _PendingNotification notification, ) async { - switch (notification.type) { - case _NotificationType.message: - await _showMessageNotificationImpl( - contactName: notification.title, - message: notification.body, - contactId: notification.id, - badgeCount: notification.badgeCount, - ); - break; - case _NotificationType.advert: - await _showAdvertNotificationImpl( - contactName: notification.body, - contactType: notification.title, - contactId: notification.id, - ); - break; - case _NotificationType.channelMessage: - await _showChannelMessageNotificationImpl( - channelName: notification.title, - message: notification.body, - channelIndex: int.tryParse(notification.id ?? ''), - badgeCount: notification.badgeCount, - ); - break; + try { + switch (notification.type) { + case _NotificationType.message: + await _showMessageNotificationImpl( + contactName: notification.title, + message: notification.body, + contactId: notification.id, + badgeCount: notification.badgeCount, + ); + break; + case _NotificationType.advert: + await _showAdvertNotificationImpl( + contactName: notification.body, + contactType: notification.title, + contactId: notification.id, + ); + break; + case _NotificationType.channelMessage: + await _showChannelMessageNotificationImpl( + channelName: notification.title, + message: notification.body, + channelIndex: int.tryParse(notification.id ?? ''), + badgeCount: notification.badgeCount, + ); + break; + } + } catch (e) { + debugPrint('Failed to show immediate notification: $e'); } } Future _showBatchSummary(List<_PendingNotification> batch) async { - if (!_isInitialized) await initialize(); + if (!await _ensureInitialized()) return; // Group by type final messages = batch @@ -468,13 +491,17 @@ class NotificationService { const notificationDetails = NotificationDetails(android: androidDetails); - await _notifications.show( - id: 'batch_summary'.hashCode, - title: _l10n.notification_activityTitle, - body: parts.join(', '), - notificationDetails: notificationDetails, - payload: 'batch', - ); + try { + await _notifications.show( + id: 'batch_summary'.hashCode, + title: _l10n.notification_activityTitle, + body: parts.join(', '), + notificationDetails: notificationDetails, + payload: 'batch', + ); + } catch (e) { + debugPrint('Failed to show batch summary notification: $e'); + } } } diff --git a/lib/widgets/adaptive_app_bar_title.dart b/lib/widgets/adaptive_app_bar_title.dart new file mode 100644 index 0000000..12363dd --- /dev/null +++ b/lib/widgets/adaptive_app_bar_title.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class AdaptiveAppBarTitle extends StatelessWidget { + final String text; + + const AdaptiveAppBarTitle(this.text, {super.key}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) => SizedBox( + width: constraints.maxWidth, + child: FittedBox(fit: BoxFit.scaleDown, child: Text(text, maxLines: 1)), + ), + ); + } +} diff --git a/test/services/line_of_sight_service_test.dart b/test/services/line_of_sight_service_test.dart new file mode 100644 index 0000000..987ee6c --- /dev/null +++ b/test/services/line_of_sight_service_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/services/line_of_sight_service.dart'; + +void main() { + List makePoints(int count) { + return List.generate(count, (i) => LatLng(0, i * 0.00001)); + } + + test('computeFromElevations reports clear LOS on flat terrain', () { + final points = makePoints(21); + final elevations = List.filled(points.length, 100); + + final result = LineOfSightService.computeFromElevations( + points: points, + elevations: elevations, + startAntennaHeightMeters: 2, + endAntennaHeightMeters: 2, + ); + + expect(result.hasData, isTrue); + expect(result.isClear, isTrue); + expect(result.maxObstructionMeters, equals(0)); + expect(result.firstObstructionDistanceMeters, isNull); + }); + + test( + 'computeFromElevations reports blocked LOS with central obstruction', + () { + final points = makePoints(21); + final elevations = List.filled(points.length, 100); + elevations[10] = 300; + + final result = LineOfSightService.computeFromElevations( + points: points, + elevations: elevations, + startAntennaHeightMeters: 1.5, + endAntennaHeightMeters: 1.5, + ); + + expect(result.hasData, isTrue); + expect(result.isClear, isFalse); + expect(result.maxObstructionMeters, greaterThan(0)); + expect(result.firstObstructionDistanceMeters, isNotNull); + }, + ); + + test('analyzePath summarizes clear and blocked segments', () async { + final service = LineOfSightService( + elevationDataSource: (points) async { + final elevations = List.filled(points.length, 100); + if (points.first.longitude > 0.00005) { + elevations[elevations.length ~/ 2] = 300; + } + return elevations; + }, + ); + + final path = [ + const LatLng(0, 0), + const LatLng(0, 0.0001), + const LatLng(0, 0.0002), + ]; + + final result = await service.analyzePath(path); + + expect(result.segments.length, 2); + expect(result.clearSegments, 1); + expect(result.blockedSegments, 1); + expect(result.unknownSegments, 0); + }); +} From 7acfe47fd78e0b49d718887258d7d767ed13a34b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Fri, 20 Feb 2026 22:09:11 -0800 Subject: [PATCH 118/421] Refactor map legend and filtering logic for contacts with location, to show count of active markers. (#203) --- lib/screens/map_screen.dart | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 8c13a71..ce7a7d1 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -488,7 +488,8 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) _buildLegend( - contactsWithLocation.length, + contactsWithLocation, + settings, sharedMarkers.length, ), if (_isBuildingPathTrace) _buildPathTraceOverlay(), @@ -524,13 +525,17 @@ class _MapScreenState extends State { if (!contact.hasLocation) continue; // Apply node type filters - if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) { + if (contact.type == advTypeRepeater && + (!settings.mapShowRepeaters && !_isBuildingPathTrace)) { + continue; + } + if (contact.type == advTypeChat && + !(settings.mapShowChatNodes && !_isBuildingPathTrace)) { continue; } - if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue; if (contact.type != advTypeChat && contact.type != advTypeRepeater && - !settings.mapShowOtherNodes) { + (!settings.mapShowOtherNodes && !_isBuildingPathTrace)) { continue; } @@ -653,7 +658,26 @@ class _MapScreenState extends State { } } - Widget _buildLegend(int nodeCount, int markerCount) { + Widget _buildLegend( + List contactsWithLocation, + settings, + int markerCount, + ) { + int nodeCount = 0; + for (final contact in contactsWithLocation) { + // Apply node type filters + if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) { + continue; + } + if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue; + if (contact.type != advTypeChat && + contact.type != advTypeRepeater && + !settings.mapShowOtherNodes) { + continue; + } + nodeCount++; + } + return Positioned( top: 16, right: 16, From 304c3896692f5ad80a4e4a8d904a4fc0f1d7d33c Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Fri, 20 Feb 2026 23:41:20 -0800 Subject: [PATCH 119/421] Refactor label display in Line Of Sight and Map screens for improved alignment and styling (#204) --- lib/screens/line_of_sight_map_screen.dart | 44 +++++++++---------- lib/screens/map_screen.dart | 52 ++++++++++------------- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index a2d4b4a..b073685 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -802,29 +802,27 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - endpoint.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + endpoint.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index ce7a7d1..77ec98c 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -462,13 +462,10 @@ class _MapScreenState extends State { ], ), alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 12, - ), + child: const Icon( + Icons.person_pin_circle, + color: Colors.white, + size: 20, ), ), ), @@ -480,7 +477,7 @@ class _MapScreenState extends State { connector.selfLatitude!, connector.selfLongitude!, ), - label: connector.deviceDisplayName, + label: context.l10n.pathTrace_you, ), ], ), @@ -598,27 +595,24 @@ class _MapScreenState extends State { alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), From 061b7156946e51ae3e98acfdb9b85cfdb2ba5bdf Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:54:39 -0500 Subject: [PATCH 120/421] Fix repeater battery % inconsistency and add configurable repeater battery chemistry (#199) * fix(repeater): unify battery percentage math and add repeater chemistry setting - Add shared battery percent utility used by connector, repeater status, and telemetry - Add repeater-specific battery chemistry persistence and service accessors - Add repeater chemistry selector in Repeater Hub - Ensure telemetry and status compute percentages consistently from same chemistry - Add focused battery utility tests Refs #116 Refs #174 * fix: Flutter Analyzer Errors fixed Recent Merge Compatible * Unify repeater battery source across status and telemetry --- lib/connector/meshcore_connector.dart | 60 +++++++++++++++--------- lib/models/app_settings.dart | 18 +++++-- lib/screens/repeater_hub_screen.dart | 62 +++++++++++++++++++++++++ lib/screens/repeater_status_screen.dart | 44 ++++++++++++++---- lib/screens/telemetry_screen.dart | 46 ++++++++++++++---- lib/services/app_settings_service.dart | 25 +++++++--- lib/utils/battery_utils.dart | 26 +++++++++++ test/utils/battery_utils_test.dart | 23 +++++++++ 8 files changed, 253 insertions(+), 51 deletions(-) create mode 100644 lib/utils/battery_utils.dart create mode 100644 test/utils/battery_utils_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9b256e2..10ee15b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -29,6 +29,7 @@ import '../storage/contact_store.dart'; import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; +import '../utils/battery_utils.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -81,6 +82,18 @@ enum MeshCoreConnectionState { disconnecting, } +class RepeaterBatterySnapshot { + final int millivolts; + final DateTime updatedAt; + final String source; + + const RepeaterBatterySnapshot({ + required this.millivolts, + required this.updatedAt, + required this.source, + }); +} + class MeshCoreConnector extends ChangeNotifier { // Message windowing to limit memory usage static const int _messageWindowSize = 200; @@ -187,6 +200,7 @@ class MeshCoreConnector extends ChangeNotifier { final Map _contactSmazEnabled = {}; final Set _knownContactKeys = {}; final Map _contactUnreadCount = {}; + final Map _repeaterBatterySnapshots = {}; bool _unreadStateLoaded = false; final Map _pendingRepeaterAcks = {}; String? _activeContactKey; @@ -254,10 +268,32 @@ class MeshCoreConnector extends ChangeNotifier { : 0; int? get batteryPercent => _batteryMillivolts == null ? null - : _estimateBatteryPercent( + : estimateBatteryPercentFromMillivolts( _batteryMillivolts!, _batteryChemistryForDevice(), ); + RepeaterBatterySnapshot? getRepeaterBatterySnapshot(String contactKeyHex) => + _repeaterBatterySnapshots[contactKeyHex]; + int? getRepeaterBatteryMillivolts(String contactKeyHex) => + _repeaterBatterySnapshots[contactKeyHex]?.millivolts; + + void updateRepeaterBatterySnapshot( + String contactKeyHex, + int millivolts, { + String source = 'unknown', + }) { + if (contactKeyHex.isEmpty || millivolts <= 0) return; + final previous = _repeaterBatterySnapshots[contactKeyHex]; + final snapshot = RepeaterBatterySnapshot( + millivolts: millivolts, + updatedAt: DateTime.now(), + source: source, + ); + _repeaterBatterySnapshots[contactKeyHex] = snapshot; + if (previous?.millivolts != millivolts) { + notifyListeners(); + } + } String _batteryChemistryForDevice() { final deviceId = _device?.remoteId.toString(); @@ -265,27 +301,6 @@ class MeshCoreConnector extends ChangeNotifier { return _appSettingsService!.batteryChemistryForDevice(deviceId); } - int _estimateBatteryPercent(int millivolts, String chemistry) { - final range = _batteryVoltageRange(chemistry); - final minMv = range.$1; - final maxMv = range.$2; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); - } - - (int, int) _batteryVoltageRange(String chemistry) { - switch (chemistry) { - case 'lifepo4': - return (2600, 3650); - case 'lipo': - return (3000, 4200); - case 'nmc': - default: - return (3000, 4200); - } - } - List getMessages(Contact contact) { return _conversations[contact.publicKeyHex] ?? []; } @@ -961,6 +976,7 @@ class MeshCoreConnector extends ChangeNotifier { _clientRepeat = null; _firmwareVerCode = null; _batteryMillivolts = null; + _repeaterBatterySnapshots.clear(); _batteryRequested = false; _awaitingSelfInfo = false; _maxContacts = _defaultMaxContacts; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 229a7a6..71cafa0 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -34,6 +34,7 @@ class AppSettings { final String? languageOverride; // null = system default final bool appDebugLogEnabled; final Map batteryChemistryByDeviceId; + final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; AppSettings({ @@ -57,8 +58,10 @@ class AppSettings { this.languageOverride, this.appDebugLogEnabled = false, Map? batteryChemistryByDeviceId, + Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, - }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}; + }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, + batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}; Map toJson() { return { @@ -82,6 +85,7 @@ class AppSettings { 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, + 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, }; } @@ -124,9 +128,12 @@ class AppSettings { (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, - unitSystem: parseUnitSystem( - json['unit_system'] ?? json['los_unit_system'], - ), + batteryChemistryByRepeaterId: + (json['battery_chemistry_by_repeater_id'] as Map?)?.map( + (key, value) => MapEntry(key.toString(), value.toString()), + ) ?? + {}, + unitSystem: parseUnitSystem(json['unit_system']), ); } @@ -151,6 +158,7 @@ class AppSettings { Object? languageOverride = _unset, bool? appDebugLogEnabled, Map? batteryChemistryByDeviceId, + Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, }) { return AppSettings( @@ -181,6 +189,8 @@ class AppSettings { appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, + batteryChemistryByRepeaterId: + batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, unitSystem: unitSystem ?? this.unitSystem, ); } diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index a5f503d..fd2da8e 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../services/app_settings_service.dart'; import 'repeater_status_screen.dart'; import 'repeater_cli_screen.dart'; import 'repeater_settings_screen.dart'; @@ -21,6 +23,10 @@ class RepeaterHubScreen extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final settingsService = context.watch(); + final chemistry = settingsService.batteryChemistryForRepeater( + repeater.publicKeyHex, + ); return Scaffold( appBar: AppBar( title: Column( @@ -107,6 +113,62 @@ class RepeaterHubScreen extends StatelessWidget { ), ), const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.battery_full), + const SizedBox(width: 10), + Expanded( + child: Text( + l10n.appSettings_batteryChemistry, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: chemistry, + isExpanded: true, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + isDense: true, + ), + onChanged: (value) { + if (value == null) return; + settingsService.setBatteryChemistryForRepeater( + repeater.publicKeyHex, + value, + ); + }, + items: [ + DropdownMenuItem( + value: 'nmc', + child: Text(l10n.appSettings_batteryNmc), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 24), Text( l10n.repeater_managementTools, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 472b013..95254f4 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -8,7 +8,9 @@ import '../models/contact.dart'; import '../models/path_selection.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; +import '../utils/battery_utils.dart'; import '../widgets/path_management_dialog.dart'; class RepeaterStatusScreen extends StatefulWidget { @@ -179,6 +181,12 @@ class _RepeaterStatusScreenState extends State { _dupDirect = directDups; _dupFlood = floodDups; }); + final connector = Provider.of(context, listen: false); + connector.updateRepeaterBatterySnapshot( + widget.repeater.publicKeyHex, + batteryMv, + source: 'status_binary', + ); _recordStatusResult(true); } @@ -201,6 +209,18 @@ class _RepeaterStatusScreenState extends State { _uptimeSecs = _asInt(data['uptime_secs']); _queueLen = _asInt(data['queue_len']); _debugFlags = _asInt(data['errors']); + final batteryMv = _batteryMv; + if (batteryMv != null) { + final connector = Provider.of( + context, + listen: false, + ); + connector.updateRepeaterBatterySnapshot( + widget.repeater.publicKeyHex, + batteryMv, + source: 'status_text', + ); + } } else if (data.containsKey('noise_floor')) { _noiseFloor = _asInt(data['noise_floor']); _lastRssi = _asInt(data['last_rssi']); @@ -590,18 +610,24 @@ class _RepeaterStatusScreenState extends State { } String _batteryText() { - if (_batteryMv == null) return '—'; - final percent = _batteryPercentFromMv(_batteryMv!); - final volts = (_batteryMv! / 1000.0).toStringAsFixed(2); + final connector = context.watch(); + final batteryMv = + connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? + _batteryMv; + if (batteryMv == null) return '—'; + final percent = estimateBatteryPercentFromMillivolts( + batteryMv, + _batteryChemistry(), + ); + final volts = (batteryMv / 1000.0).toStringAsFixed(2); return '$percent% / ${volts}V'; } - int _batteryPercentFromMv(int millivolts) { - const minMv = 3000; - const maxMv = 4200; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); + String _batteryChemistry() { + final settingsService = context.read(); + return settingsService.batteryChemistryForRepeater( + widget.repeater.publicKeyHex, + ); } String _clockText() { diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 88c204d..3f95ccd 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -12,6 +12,7 @@ import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../helpers/cayenne_lpp.dart'; +import '../utils/battery_utils.dart'; class TelemetryScreen extends StatefulWidget { final Contact repeater; @@ -74,9 +75,19 @@ class _TelemetryScreenState extends State { } void _handleStatusResponse(Uint8List frame) { + final parsedTelemetry = CayenneLpp.parseByChannel(frame); + final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry); + if (batteryMv != null) { + final connector = Provider.of(context, listen: false); + connector.updateRepeaterBatterySnapshot( + widget.repeater.publicKeyHex, + batteryMv, + source: 'telemetry', + ); + } if (!mounted) return; setState(() { - _parsedTelemetry = CayenneLpp.parseByChannel(frame); + _parsedTelemetry = parsedTelemetry; }); ScaffoldMessenger.of(context).showSnackBar( @@ -411,20 +422,35 @@ class _TelemetryScreenState extends State { ); } - String _batteryText(double? batteryMv) { + int? _extractTelemetryBatteryMillivolts(List> entries) { + for (final entry in entries) { + if (entry['channel'] != 1) continue; + final values = entry['values']; + if (values is! Map) continue; + final voltage = values['voltage']; + if (voltage is num) return (voltage.toDouble() * 1000).round(); + } + return null; + } + + String _batteryText(double? telemetryVolts) { final l10n = context.l10n; + final connector = context.watch(); + final batteryMv = + connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? + (telemetryVolts == null ? null : (telemetryVolts * 1000).round()); if (batteryMv == null) return l10n.common_notAvailable; - final percent = _batteryPercentFromMv(batteryMv); - final volts = batteryMv.toStringAsFixed(2); + final chemistry = _batteryChemistry(); + final percent = estimateBatteryPercentFromMillivolts(batteryMv, chemistry); + final volts = (batteryMv / 1000).toStringAsFixed(2); return l10n.telemetry_batteryValue(percent, volts); } - int _batteryPercentFromMv(double millivolts) { - const minMv = 2.800; - const maxMv = 4.200; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); + String _batteryChemistry() { + final settingsService = context.read(); + return settingsService.batteryChemistryForRepeater( + widget.repeater.publicKeyHex, + ); } String _temperatureText(double? tempC, bool isImperialUnits) { diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index a85ab92..e131eb8 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -17,6 +17,12 @@ class AppSettingsService extends ChangeNotifier { return stored ?? 'nmc'; } + String batteryChemistryForRepeater(String repeaterPubKeyHex) { + final stored = _settings.batteryChemistryByRepeaterId[repeaterPubKeyHex]; + if (stored == 'liion') return 'nmc'; + return stored ?? 'nmc'; + } + Future loadSettings() async { final prefs = PrefsManager.instance; final jsonStr = prefs.getString(_settingsKey); @@ -133,13 +139,20 @@ class AppSettingsService extends ChangeNotifier { ); } + Future setBatteryChemistryForRepeater( + String repeaterPubKeyHex, + String chemistry, + ) async { + final updated = Map.from( + _settings.batteryChemistryByRepeaterId, + ); + updated[repeaterPubKeyHex] = chemistry; + await updateSettings( + _settings.copyWith(batteryChemistryByRepeaterId: updated), + ); + } + Future setUnitSystem(UnitSystem value) async { await updateSettings(_settings.copyWith(unitSystem: value)); } - - Future setLosUnitSystem(String value) async { - await setUnitSystem( - value == 'imperial' ? UnitSystem.imperial : UnitSystem.metric, - ); - } } diff --git a/lib/utils/battery_utils.dart b/lib/utils/battery_utils.dart new file mode 100644 index 0000000..2bf4a5d --- /dev/null +++ b/lib/utils/battery_utils.dart @@ -0,0 +1,26 @@ +typedef BatteryVoltageRange = ({int minMv, int maxMv}); + +BatteryVoltageRange batteryVoltageRange(String chemistry) { + switch (chemistry) { + case 'lifepo4': + return (minMv: 2600, maxMv: 3650); + case 'lipo': + return (minMv: 3000, maxMv: 4200); + case 'nmc': + default: + return (minMv: 3000, maxMv: 4200); + } +} + +int estimateBatteryPercentFromMillivolts(int millivolts, String chemistry) { + final range = batteryVoltageRange(chemistry); + if (millivolts <= range.minMv) return 0; + if (millivolts >= range.maxMv) return 100; + return (((millivolts - range.minMv) * 100) / (range.maxMv - range.minMv)) + .round(); +} + +int estimateBatteryPercentFromVolts(double volts, String chemistry) { + final millivolts = (volts * 1000).round(); + return estimateBatteryPercentFromMillivolts(millivolts, chemistry); +} diff --git a/test/utils/battery_utils_test.dart b/test/utils/battery_utils_test.dart new file mode 100644 index 0000000..65dec1e --- /dev/null +++ b/test/utils/battery_utils_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/utils/battery_utils.dart'; + +void main() { + group('battery utils', () { + test('nmc range maps 3.0V to 0% and 4.2V to 100%', () { + expect(estimateBatteryPercentFromVolts(3.0, 'nmc'), 0); + expect(estimateBatteryPercentFromVolts(4.2, 'nmc'), 100); + }); + + test('lifepo4 range maps 2.6V to 0% and 3.65V to 100%', () { + expect(estimateBatteryPercentFromVolts(2.6, 'lifepo4'), 0); + expect(estimateBatteryPercentFromVolts(3.65, 'lifepo4'), 100); + }); + + test('unknown chemistry falls back to nmc mapping', () { + expect( + estimateBatteryPercentFromMillivolts(3600, 'unknown'), + estimateBatteryPercentFromMillivolts(3600, 'nmc'), + ); + }); + }); +} From b05b62eeee8431ab4c8bea0582fd9eadebec6426 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 21 Feb 2026 14:55:42 -0800 Subject: [PATCH 121/421] Changed all map lables to look the same across all map ui (#206) * Refactor label display in Line Of Sight and Map screens for improved alignment and styling * Refactor label positioning and styling in ChannelMessagePathMap and PathTraceMap screens for improved alignment --- lib/screens/channel_message_path_screen.dart | 39 +++++++++----------- lib/screens/path_trace_map.dart | 39 +++++++++----------- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 2d1faa3..44dfe79 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -663,27 +663,24 @@ class _ChannelMessagePathMapScreenState alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index c1f7f44..5f86cc1 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -479,27 +479,24 @@ class _PathTraceMapScreenState extends State { alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), From 51d70ce0869e1953eb7d67ed7bf684741fb22d44 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:20:56 -0500 Subject: [PATCH 122/421] fix(appbar): prevent title overflow on narrow widths (#205) Use width-aware layout in AppBarTitle to avoid RenderFlex overflows under tight title constraints and larger text scaling. Hide subtitle and signal indicators progressively when space is limited while preserving normal behavior on wider layouts. --- lib/widgets/app_bar.dart | 69 +++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index c88a596..e1cda77 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -14,35 +14,54 @@ class AppBarTitle extends StatelessWidget { @override Widget build(BuildContext context) { final connector = context.watch(); + final selfName = connector.selfName; - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - leading ?? const SizedBox.shrink(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + return LayoutBuilder( + builder: (context, constraints) { + final availableWidth = constraints.hasBoundedWidth + ? constraints.maxWidth + : MediaQuery.sizeOf(context).width; + final compact = availableWidth < 240; + final showSubtitle = + !compact && connector.isConnected && selfName != null; + final showBattery = availableWidth >= 60; + final showSnr = availableWidth >= 110; + final showIndicators = showBattery || showSnr; + + return Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - Text(title, overflow: TextOverflow.ellipsis), - if (connector.isConnected && connector.selfName != null) - Text( - '(${connector.selfName})', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - overflow: TextOverflow.ellipsis, + leading ?? const SizedBox.shrink(), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, maxLines: 1, overflow: TextOverflow.ellipsis), + if (showSubtitle) + Text( + '($selfName)', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), + ), + if (showIndicators) const SizedBox(width: 6), + if (showIndicators) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (showBattery) BatteryIndicator(connector: connector), + if (showSnr) SNRIndicator(connector: connector), + ], + ), + trailing ?? const SizedBox.shrink(), ], - ), - const SizedBox(width: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BatteryIndicator(connector: connector), - SNRIndicator(connector: connector), - ], - ), - trailing ?? const SizedBox.shrink(), - ], + ); + }, ); } } From 2feff809ff76c18ab08913dfe0ce08c862df7f62 Mon Sep 17 00:00:00 2001 From: Aaron Easterling <111671335+Specter242@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:31:51 -0500 Subject: [PATCH 123/421] Mark pending channel messages sent on RESP_CODE_SENT (#186) * Mark pending channel message sent on RESP_CODE_SENT * Disambiguate RESP_CODE_SENT handling for direct vs channel * Handle channel sent feedback when firmware returns RESP_CODE_OK * Correlate channel OK ACKs and queue reaction channel sends --- lib/connector/meshcore_connector.dart | 166 ++++++++++++++++++++++-- lib/screens/contacts_screen.dart | 9 +- lib/services/message_retry_service.dart | 15 ++- 3 files changed, 175 insertions(+), 15 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 10ee15b..2fbddc7 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -114,6 +114,10 @@ class MeshCoreConnector extends ChangeNotifier { final List _channels = []; final Map> _conversations = {}; final Map> _channelMessages = {}; + final List _pendingChannelSentQueue = []; + final List<_PendingCommandAck> _pendingGenericAckQueue = []; + static const String _reactionSendQueuePrefix = '__reaction_send__'; + int _reactionSendQueueSequence = 0; final Set _loadedConversationKeys = {}; final Map> _processedChannelReactions = {}; // channelIndex -> Set of "targetHash_emoji" @@ -988,6 +992,9 @@ class MeshCoreConnector extends ChangeNotifier { _isSyncingChannels = false; _channelSyncInFlight = false; _hasLoadedChannels = false; + _pendingChannelSentQueue.clear(); + _pendingGenericAckQueue.clear(); + _reactionSendQueueSequence = 0; _setState(MeshCoreConnectionState.disconnected); if (!manual) { @@ -995,7 +1002,11 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future sendFrame(Uint8List data) async { + Future sendFrame( + Uint8List data, { + String? channelSendQueueId, + bool expectsGenericAck = false, + }) async { if (!isConnected || _rxCharacteristic == null) { throw Exception("Not connected to a MeshCore device"); } @@ -1014,6 +1025,11 @@ class MeshCoreConnector extends ChangeNotifier { data.toList(), withoutResponse: canWriteWithoutResponse, ); + _trackPendingGenericAck( + data, + channelSendQueueId: channelSendQueueId, + expectsGenericAck: expectsGenericAck, + ); } Future requestBatteryStatus({bool force = false}) async { @@ -1369,7 +1385,13 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); // Send the reaction to the device (don't add as a visible message) - await sendFrame(buildSendChannelTextMsgFrame(channel.index, text)); + final reactionQueueId = _nextReactionSendQueueId(); + _pendingChannelSentQueue.add(reactionQueueId); + await sendFrame( + buildSendChannelTextMsgFrame(channel.index, text), + channelSendQueueId: reactionQueueId, + expectsGenericAck: true, + ); return; } @@ -1379,6 +1401,7 @@ class MeshCoreConnector extends ChangeNotifier { channel.index, ); _addChannelMessage(channel.index, message); + _pendingChannelSentQueue.add(message.messageId); notifyListeners(); final trimmed = text.trim(); @@ -1388,7 +1411,11 @@ class MeshCoreConnector extends ChangeNotifier { (isChannelSmazEnabled(channel.index) && !isStructuredPayload) ? Smaz.encodeIfSmaller(text) : text; - await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText)); + await sendFrame( + buildSendChannelTextMsgFrame(channel.index, outboundText), + channelSendQueueId: message.messageId, + expectsGenericAck: true, + ); } Future removeContact(Contact contact) async { @@ -1735,6 +1762,9 @@ class MeshCoreConnector extends ChangeNotifier { debugPrint('RX frame: code=$code len=${frame.length}'); switch (code) { + case respCodeOk: + _handleOk(); + break; case respCodeDeviceInfo: _handleDeviceInfo(frame); break; @@ -1829,6 +1859,17 @@ class MeshCoreConnector extends ChangeNotifier { 'Firmware responded with error code: $errCode', tag: 'Protocol', ); + + if (_pendingGenericAckQueue.isEmpty) { + return; + } + + final failedAck = _pendingGenericAckQueue.removeAt(0); + if (failedAck.commandCode != cmdSendChannelTxtMsg || + failedAck.channelSendQueueId == null) { + return; + } + _pendingChannelSentQueue.remove(failedAck.channelSendQueueId); } void _handlePathUpdated(Uint8List frame) { @@ -2611,8 +2652,22 @@ class MeshCoreConnector extends ChangeNotifier { return; } - if (_retryService != null) { - _retryService!.updateMessageFromSent(ackHash, timeoutMs); + final retryService = _retryService; + if (retryService != null && + retryService.updateMessageFromSent( + ackHash, + timeoutMs, + allowQueueFallback: false, + )) { + return; + } + + if (_markNextPendingChannelMessageSent()) { + return; + } + + if (retryService != null) { + retryService.updateMessageFromSent(ackHash, timeoutMs); } } else { // Fallback to old behavior @@ -2629,6 +2684,64 @@ class MeshCoreConnector extends ChangeNotifier { } } + bool _markNextPendingChannelMessageSent() { + while (_pendingChannelSentQueue.isNotEmpty) { + final queuedMessageId = _pendingChannelSentQueue.removeAt(0); + if (_isReactionSendQueueId(queuedMessageId)) { + return true; + } + if (_markPendingChannelMessageSentById(queuedMessageId)) { + return true; + } + } + return false; + } + + bool _markPendingChannelMessageSentById(String messageId) { + for (final entry in _channelMessages.entries) { + final channelMessages = entry.value; + for (int i = channelMessages.length - 1; i >= 0; i--) { + final message = channelMessages[i]; + if (message.messageId != messageId) { + continue; + } + if (!message.isOutgoing || + message.status != ChannelMessageStatus.pending) { + return false; + } + channelMessages[i] = message.copyWith( + status: ChannelMessageStatus.sent, + ); + _pendingChannelSentQueue.remove(messageId); + unawaited( + _channelMessageStore.saveChannelMessages(entry.key, channelMessages), + ); + notifyListeners(); + return true; + } + } + return false; + } + + void _handleOk() { + if (_pendingGenericAckQueue.isEmpty) { + return; + } + + final pendingAck = _pendingGenericAckQueue.removeAt(0); + if (pendingAck.commandCode != cmdSendChannelTxtMsg || + pendingAck.channelSendQueueId == null) { + return; + } + + final queueId = pendingAck.channelSendQueueId!; + _pendingChannelSentQueue.remove(queueId); + if (_isReactionSendQueueId(queueId)) { + return; + } + _markPendingChannelMessageSentById(queueId); + } + void _handleSendConfirmed(Uint8List frame) { // Frame format from C++: // [0] = PUSH_CODE_SEND_CONFIRMED @@ -3207,18 +3320,22 @@ class MeshCoreConnector extends ChangeNotifier { mergedPathBytes.length, ); final newRepeatCount = existing.repeatCount + 1; + final promotedFromPending = + newRepeatCount == 1 && + existing.status == ChannelMessageStatus.pending; messages[existingIndex] = existing.copyWith( repeatCount: newRepeatCount, pathLength: mergedPathLength, pathBytes: mergedPathBytes, pathVariants: mergedPathVariants, // Mark as sent when first repeat is heard - status: - newRepeatCount == 1 && - existing.status == ChannelMessageStatus.pending + status: promotedFromPending ? ChannelMessageStatus.sent : existing.status, ); + if (promotedFromPending) { + _pendingChannelSentQueue.remove(existing.messageId); + } } else { messages.add(processedMessage); } @@ -3391,11 +3508,37 @@ class MeshCoreConnector extends ChangeNotifier { _queuedMessageSyncInFlight = false; _isSyncingChannels = false; _channelSyncInFlight = false; + _pendingChannelSentQueue.clear(); + _pendingGenericAckQueue.clear(); + _reactionSendQueueSequence = 0; _setState(MeshCoreConnectionState.disconnected); _scheduleReconnect(); } + void _trackPendingGenericAck( + Uint8List data, { + String? channelSendQueueId, + required bool expectsGenericAck, + }) { + if (!expectsGenericAck || data.isEmpty) return; + _pendingGenericAckQueue.add( + _PendingCommandAck( + commandCode: data[0], + channelSendQueueId: channelSendQueueId, + ), + ); + } + + String _nextReactionSendQueueId() { + _reactionSendQueueSequence++; + return '$_reactionSendQueuePrefix$_reactionSendQueueSequence'; + } + + bool _isReactionSendQueueId(String queueId) { + return queueId.startsWith(_reactionSendQueuePrefix); + } + Map _parseKeyValueString(String input) { final result = {}; @@ -3691,3 +3834,10 @@ class _RepeaterAckContext { required this.messageBytes, }); } + +class _PendingCommandAck { + final int commandCode; + final String? channelSendQueueId; + + _PendingCommandAck({required this.commandCode, this.channelSendQueueId}); +} diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index a6828dd..c3f783c 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -183,14 +183,17 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); _pendingOperations.add(ContactOperationType.export); - await connector.sendFrame(exportContactFrame); + await connector.sendFrame(exportContactFrame, expectsGenericAck: true); } Future _contactZeroHop(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactZeroHopFrame = buildZeroHopContact(pubKey); _pendingOperations.add(ContactOperationType.zeroHopShare); - await connector.sendFrame(exportContactZeroHopFrame); + await connector.sendFrame( + exportContactZeroHopFrame, + expectsGenericAck: true, + ); } Future _contactImport() async { @@ -217,7 +220,7 @@ class _ContactsScreenState extends State try { final importContactFrame = buildImportContactFrame(hexString); _pendingOperations.add(ContactOperationType.import); - await connector.sendFrame(importContactFrame); + await connector.sendFrame(importContactFrame, expectsGenericAck: true); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 9cbd68f..694a616 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -234,7 +234,11 @@ class MessageRetryService extends ChangeNotifier { } } - void updateMessageFromSent(Uint8List ackHash, int timeoutMs) { + bool updateMessageFromSent( + Uint8List ackHash, + int timeoutMs, { + bool allowQueueFallback = true, + }) { final ackHashHex = ackHash .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(); @@ -277,7 +281,7 @@ class MessageRetryService extends ChangeNotifier { } // FALLBACK: Old queue-based matching (for messages sent before hash computation was added) - if (messageId == null) { + if (messageId == null && allowQueueFallback) { _debugLogService?.warn( 'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue', tag: 'AckHash', @@ -320,7 +324,7 @@ class MessageRetryService extends ChangeNotifier { if (messageId == null || contact == null) { debugPrint('No pending message found for ACK hash: $ackHashHex'); - return; + return false; } // Store the mapping for future lookups (e.g., when ACK arrives) @@ -339,7 +343,7 @@ class MessageRetryService extends ChangeNotifier { 'Message $messageId no longer pending for ACK hash: $ackHashHex', ); _ackHashToMessageId.remove(ackHashHex); - return; + return false; } // Add this ACK hash to the list of expected ACKs for this message (for history) @@ -389,8 +393,11 @@ class MessageRetryService extends ChangeNotifier { _startTimeoutTimer(messageId, actualTimeout); debugPrint('Updated message $messageId with ACK hash: $ackHashHex'); + return true; } + bool get hasPendingMessages => _pendingMessages.isNotEmpty; + void _startTimeoutTimer(String messageId, int timeoutMs) { _timeoutTimers[messageId]?.cancel(); _timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () { From 8fe412920453307572275c638c4020a152c874fc Mon Sep 17 00:00:00 2001 From: Specter242 Date: Sat, 21 Feb 2026 21:01:57 -0500 Subject: [PATCH 124/421] Align Android app module to Java 17 and bump wakelock_plus --- android/app/build.gradle.kts | 6 +++--- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e7d2b42..e0a8029 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -19,13 +19,13 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { diff --git a/pubspec.yaml b/pubspec.yaml index f5ceaaf..3624b93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1 flutter_foreground_task: ^9.2.0 - wakelock_plus: ^1.2.8 + wakelock_plus: ^1.4.0 characters: ^1.4.0 package_info_plus: ^9.0.0 mobile_scanner: ^7.1.4 # QR/barcode scanning From 7cb4c5a33445a8fe2759edde44c504561f2325fc Mon Sep 17 00:00:00 2001 From: Leah <45321184+ChaoticLeah@users.noreply.github.com> Date: Sun, 22 Feb 2026 08:44:20 +0100 Subject: [PATCH 125/421] Swipe to reply (#160) * Add swipe to reply * format * Cleaned up code * format * remove my gitignore change - ignore this * fix * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor onHorizontalDragStart for readability fixed formating. * Fix swipe end handling in channel chat screen * Refactor swipe gesture handling in chat screen * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor swipe handling for reply functionality * Adjust swipe thresholds and logic in chat screen * Conditionally render reply bubble or padding --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Winston Lowe --- lib/screens/channel_chat_screen.dart | 532 ++++++++++++++++++--------- 1 file changed, 367 insertions(+), 165 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 021ad7d..bf05110 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -271,193 +272,243 @@ class _ChannelChatScreenState extends State { ? message.pathVariants.first : Uint8List(0)); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: Column( - crossAxisAlignment: isOutgoing - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: isOutgoing - ? MainAxisAlignment.end - : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - _buildAvatar(message.senderName), - const SizedBox(width: 8), - ], - Flexible( - child: GestureDetector( - onTap: () => _showMessagePathInfo(message), - onLongPress: () => _showMessageActions(message), - child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, - ), - decoration: BoxDecoration( - color: isOutgoing - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - top: 4, - bottom: 4, - ) - : EdgeInsets.zero, - child: Text( - message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), + const maxSwipeOffset = 64.0; + const replySwipeThreshold = 64.0; + final messageBody = Column( + crossAxisAlignment: isOutgoing + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + _buildAvatar(message.senderName), + const SizedBox(width: 8), + ], + Flexible( + child: GestureDetector( + onTap: () => _showMessagePathInfo(message), + onLongPress: () => _showMessageActions(message), + child: Container( + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.65, + ), + decoration: BoxDecoration( + color: isOutgoing + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + message.senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, ), ), - if (gifId == null) const SizedBox(height: 4), - ], - if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message), - const SizedBox(height: 8), - ], - if (poi != null) - _buildPoiMessage(context, poi, isOutgoing) - else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), - ) - else - Linkify( - text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + ), + if (gifId == null) const SizedBox(height: 4), + ], + if (message.replyToMessageId != null) ...[ + _buildReplyPreview(message), + const SizedBox(height: 8), + ], + if (poi != null) + _buildPoiMessage(context, poi, isOutgoing) + else if (gifId != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), ), - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - 'via ${_formatPathPrefixes(displayPath)}', + ) + else + Linkify( + text: message.text, + style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + if (displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatTime(message.timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12, + color: Colors.grey[600], + ), + const SizedBox(width: 2), Text( - _formatTime(message.timestamp), + '${message.repeatCount}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), - Text( - '${message.repeatCount}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], ], - ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == + ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: + message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], + ], ), - ], - ), + ), + ], ), ), ), - ], - ), - if (message.reactions.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), - child: _buildReactionsDisplay(message), ), ], + ), + if (message.reactions.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), + child: _buildReactionsDisplay(message), + ), ], - ), + ], + ); + + if (!isOutgoing) { + return _SwipeReplyBubble( + maxSwipeOffset: maxSwipeOffset, + replySwipeThreshold: replySwipeThreshold, + onReplyTriggered: () => _setReplyingTo(message), + hintBuilder: ({required isStart}) => + _buildReplySwipeHint(isStart: isStart), + child: messageBody, + ); + } else { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: messageBody, + ); + } + } + + Widget _buildReplySwipeHint({required bool isStart}) { + final colorScheme = Theme.of(context).colorScheme; + final content = Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.reply, color: colorScheme.primary), + const SizedBox(width: 6), + Text( + context.l10n.chat_reply, + style: TextStyle( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + + return Container( + alignment: isStart ? Alignment.centerLeft : Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 16), + color: colorScheme.primary.withValues(alpha: 0.08), + child: isStart + ? content + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.chat_reply, + style: TextStyle( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 6), + Icon(Icons.reply, color: colorScheme.primary), + ], + ), ); } @@ -1007,6 +1058,157 @@ class _ChannelChatScreenState extends State { } } +class _SwipeReplyBubble extends StatefulWidget { + final double maxSwipeOffset; + final double replySwipeThreshold; + final VoidCallback onReplyTriggered; + final Widget Function({required bool isStart}) hintBuilder; + final Widget child; + + const _SwipeReplyBubble({ + required this.maxSwipeOffset, + required this.replySwipeThreshold, + required this.onReplyTriggered, + required this.hintBuilder, + required this.child, + }); + + @override + State<_SwipeReplyBubble> createState() => _SwipeReplyBubbleState(); +} + +class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { + Offset? _swipeStartPosition; + double _swipeOffset = 0; + double _maxSwipeDistance = 0; + int? _swipePointerId; + bool _swipeLockedToHorizontal = false; + + void _handleSwipeStart(Offset position) { + _swipeStartPosition = position; + _maxSwipeDistance = 0; + if (_swipeOffset != 0) { + setState(() => _swipeOffset = 0); + } + } + + void _handleSwipePointerDown(PointerDownEvent event) { + _swipePointerId = event.pointer; + _swipeLockedToHorizontal = false; + _handleSwipeStart(event.position); + } + + void _handleSwipePointerMove(PointerMoveEvent event) { + if (_swipePointerId != event.pointer || _swipeStartPosition == null) { + return; + } + + final dx = event.position.dx - _swipeStartPosition!.dx; + + const axisLockThreshold = 12.0; + if (!_swipeLockedToHorizontal) { + if (-dx < axisLockThreshold) { + return; + } + _swipeLockedToHorizontal = true; + } + + _handleSwipeUpdate(event.position); + } + + void _handleSwipeUpdate(Offset position) { + if (_swipeStartPosition == null) return; + + final dx = position.dx - _swipeStartPosition!.dx; + if (dx >= 0) return; + + if (-dx < 6) return; + + if (-dx > _maxSwipeDistance) { + _maxSwipeDistance = -dx; + } + + final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble(); + final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset); + if (adjusted != _swipeOffset) { + setState(() => _swipeOffset = adjusted); + } + } + + void _handleSwipePointerUp(Offset position) { + if (_swipeLockedToHorizontal && _swipeStartPosition != null) { + final dx = position.dx - _swipeStartPosition!.dx; + final peak = math.max( + _maxSwipeDistance, + (-dx).clamp(0.0, double.infinity), + ); + if (peak >= widget.replySwipeThreshold) { + widget.onReplyTriggered(); + HapticFeedback.selectionClick(); + } + } + _resetSwipe(); + } + + void _resetSwipe() { + if (_swipeOffset != 0) { + setState(() => _swipeOffset = 0); + } + _swipeStartPosition = null; + _maxSwipeDistance = 0; + _swipePointerId = null; + _swipeLockedToHorizontal = false; + } + + double _applySwipeResistance(double rawOffset, double maxOffset) { + final abs = rawOffset.abs(); + if (abs <= 0) return 0; + final norm = (abs / maxOffset).clamp(0.0, 1.0); + const deadZone = 0.18; + if (norm <= deadZone) { + return rawOffset.sign * maxOffset * (norm * 0.08); + } + final t = ((norm - deadZone) / (1 - deadZone)).clamp(0.0, 1.0); + final curved = t < 0.5 + ? 16 * math.pow(t, 5) + : 1 - math.pow(-2 * t + 2, 5) / 2; + const deadZoneEnd = 0.0144; + return rawOffset.sign * + maxOffset * + (deadZoneEnd + curved * (1 - deadZoneEnd)); + } + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: _handleSwipePointerDown, + onPointerMove: _handleSwipePointerMove, + onPointerUp: (event) => _handleSwipePointerUp(event.position), + onPointerCancel: (_) => _resetSwipe(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: Opacity( + opacity: _swipeOffset.abs() / widget.maxSwipeOffset, + child: widget.hintBuilder(isStart: false), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 150), + transform: Matrix4.translationValues(_swipeOffset, 0, 0), + curve: Curves.easeOut, + child: widget.child, + ), + ], + ), + ), + ); + } +} + class _PoiInfo { final double lat; final double lon; From b3ad54f2964c498bd6c6ea942b57804a5f109274 Mon Sep 17 00:00:00 2001 From: Krasimir Kazakov Date: Sun, 22 Feb 2026 09:51:48 +0200 Subject: [PATCH 126/421] Added mute channel functionality (#209) --- lib/connector/meshcore_connector.dart | 2 ++ lib/l10n/app_bg.arb | 2 ++ lib/l10n/app_de.arb | 2 ++ lib/l10n/app_en.arb | 2 ++ lib/l10n/app_es.arb | 2 ++ lib/l10n/app_fr.arb | 2 ++ lib/l10n/app_it.arb | 2 ++ lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_bg.dart | 6 ++++++ lib/l10n/app_localizations_de.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ lib/l10n/app_localizations_es.dart | 6 ++++++ lib/l10n/app_localizations_fr.dart | 6 ++++++ lib/l10n/app_localizations_it.dart | 6 ++++++ lib/l10n/app_localizations_nl.dart | 6 ++++++ lib/l10n/app_localizations_pl.dart | 6 ++++++ lib/l10n/app_localizations_pt.dart | 6 ++++++ lib/l10n/app_localizations_ru.dart | 6 ++++++ lib/l10n/app_localizations_sk.dart | 6 ++++++ lib/l10n/app_localizations_sl.dart | 6 ++++++ lib/l10n/app_localizations_sv.dart | 6 ++++++ lib/l10n/app_localizations_uk.dart | 6 ++++++ lib/l10n/app_localizations_zh.dart | 6 ++++++ lib/l10n/app_nl.arb | 2 ++ lib/l10n/app_pl.arb | 2 ++ lib/l10n/app_pt.arb | 2 ++ lib/l10n/app_ru.arb | 2 ++ lib/l10n/app_sk.arb | 2 ++ lib/l10n/app_sl.arb | 2 ++ lib/l10n/app_sv.arb | 2 ++ lib/l10n/app_uk.arb | 2 ++ lib/l10n/app_zh.arb | 2 ++ lib/models/app_settings.dart | 13 ++++++++++++- lib/screens/channels_screen.dart | 24 ++++++++++++++++++++++++ lib/services/app_settings_service.dart | 15 +++++++++++++++ 35 files changed, 185 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2fbddc7..afd1626 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2529,6 +2529,8 @@ class MeshCoreConnector extends ChangeNotifier { } final label = channelName ?? _channelDisplayName(channelIndex); + if (_appSettingsService!.isChannelMuted(label)) return; + _notificationService.showChannelMessageNotification( channelName: label, message: message.text, diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 5689f95..8609023 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Публичен канал", "channels_privateChannel": "Частен канал", "channels_editChannel": "Редактирай канал", + "channels_muteChannel": "Заглуши канала", + "channels_unmuteChannel": "Включи известията на канала", "channels_deleteChannel": "Изтрий канала", "channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 22fdf6b..e5c82f7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Öffentlicher Kanal", "channels_privateChannel": "Privater Kanal", "channels_editChannel": "Kanal bearbeiten", + "channels_muteChannel": "Kanal stummschalten", + "channels_unmuteChannel": "Kanal Stummschaltung aufheben", "channels_deleteChannel": "Lösche den Kanal", "channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ae24539..67ca72e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -352,6 +352,8 @@ "channels_publicChannel": "Public channel", "channels_privateChannel": "Private channel", "channels_editChannel": "Edit channel", + "channels_muteChannel": "Mute channel", + "channels_unmuteChannel": "Unmute channel", "channels_deleteChannel": "Delete channel", "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3a7fe53..483b4d3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", + "channels_muteChannel": "Silenciar canal", + "channels_unmuteChannel": "Activar canal", "channels_deleteChannel": "Eliminar canal", "channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f962ee5..e162cdb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canal public", "channels_privateChannel": "Canal privé", "channels_editChannel": "Modifier le canal", + "channels_muteChannel": "Désactiver les notifications du canal", + "channels_unmuteChannel": "Réactiver les notifications du canal", "channels_deleteChannel": "Supprimer le canal", "channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 6111004..2f8d186 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canale pubblico", "channels_privateChannel": "Canale privato", "channels_editChannel": "Modifica canale", + "channels_muteChannel": "Silenzia canale", + "channels_unmuteChannel": "Attiva notifiche canale", "channels_deleteChannel": "Elimina canale", "channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 20d0422..e9686ce 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1546,6 +1546,18 @@ abstract class AppLocalizations { /// **'Edit channel'** String get channels_editChannel; + /// No description provided for @channels_muteChannel. + /// + /// In en, this message translates to: + /// **'Mute channel'** + String get channels_muteChannel; + + /// No description provided for @channels_unmuteChannel. + /// + /// In en, this message translates to: + /// **'Unmute channel'** + String get channels_unmuteChannel; + /// No description provided for @channels_deleteChannel. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 9c66ff2..cf4bf7b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -798,6 +798,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get channels_editChannel => 'Редактирай канал'; + @override + String get channels_muteChannel => 'Заглуши канала'; + + @override + String get channels_unmuteChannel => 'Включи известията на канала'; + @override String get channels_deleteChannel => 'Изтрий канала'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ef7cd9d..c6a07a4 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -795,6 +795,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_editChannel => 'Kanal bearbeiten'; + @override + String get channels_muteChannel => 'Kanal stummschalten'; + + @override + String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben'; + @override String get channels_deleteChannel => 'Lösche den Kanal'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 7f07e26..254b5f4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -787,6 +787,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get channels_editChannel => 'Edit channel'; + @override + String get channels_muteChannel => 'Mute channel'; + + @override + String get channels_unmuteChannel => 'Unmute channel'; + @override String get channels_deleteChannel => 'Delete channel'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6409675..dcde365 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -796,6 +796,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channels_editChannel => 'Editar canal'; + @override + String get channels_muteChannel => 'Silenciar canal'; + + @override + String get channels_unmuteChannel => 'Activar canal'; + @override String get channels_deleteChannel => 'Eliminar canal'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3536cf5..d572b8f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -798,6 +798,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channels_editChannel => 'Modifier le canal'; + @override + String get channels_muteChannel => 'Désactiver les notifications du canal'; + + @override + String get channels_unmuteChannel => 'Réactiver les notifications du canal'; + @override String get channels_deleteChannel => 'Supprimer le canal'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 521cfb7..d8e27f8 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -794,6 +794,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channels_editChannel => 'Modifica canale'; + @override + String get channels_muteChannel => 'Silenzia canale'; + + @override + String get channels_unmuteChannel => 'Attiva notifiche canale'; + @override String get channels_deleteChannel => 'Elimina canale'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a7a4c0b..0a50e8b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -792,6 +792,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get channels_editChannel => 'Kanaal bewerken'; + @override + String get channels_muteChannel => 'Kanaal dempen'; + + @override + String get channels_unmuteChannel => 'Kanaal dempen opheffen'; + @override String get channels_deleteChannel => 'Kanaal verwijderen'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 8815472..31dd8b5 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -797,6 +797,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get channels_editChannel => 'Edytuj kanał'; + @override + String get channels_muteChannel => 'Wycisz kanał'; + + @override + String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału'; + @override String get channels_deleteChannel => 'Usuń kanał'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index c7fc707..5092826 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -797,6 +797,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channels_editChannel => 'Editar canal'; + @override + String get channels_muteChannel => 'Silenciar canal'; + + @override + String get channels_unmuteChannel => 'Ativar canal'; + @override String get channels_deleteChannel => 'Excluir canal'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 2e992bd..570b7c8 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -795,6 +795,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get channels_editChannel => 'Изменить канал'; + @override + String get channels_muteChannel => 'Отключить уведомления канала'; + + @override + String get channels_unmuteChannel => 'Включить уведомления канала'; + @override String get channels_deleteChannel => 'Удалить канал'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index a51e059..8bbb6de 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -792,6 +792,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channels_editChannel => 'Upraviť kanál'; + @override + String get channels_muteChannel => 'Stlmiť kanál'; + + @override + String get channels_unmuteChannel => 'Zrušiť stlmenie kanála'; + @override String get channels_deleteChannel => 'Odstrániť kanál'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 5ac7e8b..61e3058 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -790,6 +790,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channels_editChannel => 'Uredi kanal'; + @override + String get channels_muteChannel => 'Utišaj kanal'; + + @override + String get channels_unmuteChannel => 'Vklopi obvestila kanala'; + @override String get channels_deleteChannel => 'Pošlji kanal'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6d355d9..79b30b8 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -786,6 +786,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get channels_editChannel => 'Redigera kanal'; + @override + String get channels_muteChannel => 'Tysta kanal'; + + @override + String get channels_unmuteChannel => 'Slå på ljud för kanal'; + @override String get channels_deleteChannel => 'Ta bort kanal'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 0f3d550..f367002 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -793,6 +793,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get channels_editChannel => 'Редагувати канал'; + @override + String get channels_muteChannel => 'Вимкнути сповіщення каналу'; + + @override + String get channels_unmuteChannel => 'Увімкнути сповіщення каналу'; + @override String get channels_deleteChannel => 'Видалити канал'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 36a114a..7641800 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -755,6 +755,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get channels_editChannel => '编辑频道'; + @override + String get channels_muteChannel => '静音频道'; + + @override + String get channels_unmuteChannel => '取消静音频道'; + @override String get channels_deleteChannel => '删除频道'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 733e4dc..57b2fdd 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Open kanaal", "channels_privateChannel": "Private kanaal", "channels_editChannel": "Kanaal bewerken", + "channels_muteChannel": "Kanaal dempen", + "channels_unmuteChannel": "Kanaal dempen opheffen", "channels_deleteChannel": "Kanaal verwijderen", "channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 35efee1..3787fa7 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Kanał publiczny", "channels_privateChannel": "Prywatny kanał", "channels_editChannel": "Edytuj kanał", + "channels_muteChannel": "Wycisz kanał", + "channels_unmuteChannel": "Wyłącz wyciszenie kanału", "channels_deleteChannel": "Usuń kanał", "channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index fd742d9..7be6694 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", + "channels_muteChannel": "Silenciar canal", + "channels_unmuteChannel": "Ativar canal", "channels_deleteChannel": "Excluir canal", "channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 04b2e04..26cfce3 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -226,6 +226,8 @@ "channels_publicChannel": "Публичный канал", "channels_privateChannel": "Приватный канал", "channels_editChannel": "Изменить канал", + "channels_muteChannel": "Отключить уведомления канала", + "channels_unmuteChannel": "Включить уведомления канала", "channels_deleteChannel": "Удалить канал", "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", "channels_channelDeleted": "Канал \"{name}\" удалён", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 6663094..8b2cb0a 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Veľké verejne kanály", "channels_privateChannel": "Osobné kanál", "channels_editChannel": "Upraviť kanál", + "channels_muteChannel": "Stlmiť kanál", + "channels_unmuteChannel": "Zrušiť stlmenie kanála", "channels_deleteChannel": "Odstrániť kanál", "channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 50a9043..4d3415d 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Javni kanal", "channels_privateChannel": "Zasebni kanal", "channels_editChannel": "Uredi kanal", + "channels_muteChannel": "Utišaj kanal", + "channels_unmuteChannel": "Vklopi obvestila kanala", "channels_deleteChannel": "Pošlji kanal", "channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 260a34b..8c5e399 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Allmänt kanal", "channels_privateChannel": "Privat kanal", "channels_editChannel": "Redigera kanal", + "channels_muteChannel": "Tysta kanal", + "channels_unmuteChannel": "Slå på ljud för kanal", "channels_deleteChannel": "Ta bort kanal", "channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index ec414b4..910f8b0 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -335,6 +335,8 @@ "channels_publicChannel": "Публічний канал", "channels_privateChannel": "Приватний канал", "channels_editChannel": "Редагувати канал", + "channels_muteChannel": "Вимкнути сповіщення каналу", + "channels_unmuteChannel": "Увімкнути сповіщення каналу", "channels_deleteChannel": "Видалити канал", "channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6b072c9..d9efce7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -342,6 +342,8 @@ "channels_publicChannel": "公共频道", "channels_privateChannel": "私密频道", "channels_editChannel": "编辑频道", + "channels_muteChannel": "静音频道", + "channels_unmuteChannel": "取消静音频道", "channels_deleteChannel": "删除频道", "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 71cafa0..d9504b3 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -36,6 +36,7 @@ class AppSettings { final Map batteryChemistryByDeviceId; final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; + final Set mutedChannels; AppSettings({ this.clearPathOnMaxRetry = false, @@ -60,8 +61,10 @@ class AppSettings { Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, + Set? mutedChannels, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, - batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}; + batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, + mutedChannels = mutedChannels ?? {}; Map toJson() { return { @@ -87,6 +90,7 @@ class AppSettings { 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, + 'muted_channels': mutedChannels.toList(), }; } @@ -134,6 +138,11 @@ class AppSettings { ) ?? {}, unitSystem: parseUnitSystem(json['unit_system']), + mutedChannels: + ((json['muted_channels'] as List?) + ?.map((e) => e.toString()) + .toSet()) ?? + {}, ); } @@ -160,6 +169,7 @@ class AppSettings { Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, + Set? mutedChannels, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -192,6 +202,7 @@ class AppSettings { batteryChemistryByRepeaterId: batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, unitSystem: unitSystem ?? this.unitSystem, + mutedChannels: mutedChannels ?? this.mutedChannels, ); } } diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 26062de..994d3e7 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -9,6 +9,7 @@ import 'package:uuid/uuid.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../services/app_settings_service.dart'; import '../models/channel.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; @@ -477,6 +478,9 @@ class _ChannelsScreenState extends State MeshCoreConnector connector, Channel channel, ) { + final settingsService = context.read(); + final isMuted = settingsService.isChannelMuted(channel.name); + showModalBottomSheet( context: context, builder: (context) => SafeArea( @@ -494,6 +498,26 @@ class _ChannelsScreenState extends State } }, ), + ListTile( + leading: Icon( + isMuted + ? Icons.notifications_outlined + : Icons.notifications_off_outlined, + ), + title: Text( + isMuted + ? context.l10n.channels_unmuteChannel + : context.l10n.channels_muteChannel, + ), + onTap: () async { + Navigator.pop(context); + if (isMuted) { + await settingsService.unmuteChannel(channel.name); + } else { + await settingsService.muteChannel(channel.name); + } + }, + ), ListTile( leading: const Icon(Icons.delete_outline, color: Colors.red), title: Text( diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index e131eb8..e80f903 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -155,4 +155,19 @@ class AppSettingsService extends ChangeNotifier { Future setUnitSystem(UnitSystem value) async { await updateSettings(_settings.copyWith(unitSystem: value)); } + + bool isChannelMuted(String channelName) { + return _settings.mutedChannels.contains(channelName); + } + + Future muteChannel(String channelName) async { + final updated = Set.from(_settings.mutedChannels)..add(channelName); + await updateSettings(_settings.copyWith(mutedChannels: updated)); + } + + Future unmuteChannel(String channelName) async { + final updated = Set.from(_settings.mutedChannels) + ..remove(channelName); + await updateSettings(_settings.copyWith(mutedChannels: updated)); + } } From 230626938448e6e950312161226d10e65fb1131a Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:20:55 +0100 Subject: [PATCH 127/421] Better french translations --- lib/l10n/app_fr.arb | 6 +++--- lib/l10n/app_localizations_fr.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e162cdb..2d4846c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -262,7 +262,7 @@ } }, "contacts_manageRepeater": "Gérer le répéteur", - "contacts_roomLogin": "Connexion Salle", + "contacts_roomLogin": "Connexion Room Server", "contacts_openChat": "Ouverture du Chat", "contacts_editGroup": "Modifier le groupe", "contacts_deleteGroup": "Supprimer le groupe", @@ -798,7 +798,7 @@ "dialog_disconnect": "Déconnecter", "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", "login_repeaterLogin": "Connexion au répéteur", - "login_roomLogin": "Connexion Salle", + "login_roomLogin": "Connexion Room Server", "login_password": "Mot de passe", "login_enterPassword": "Entrez votre mot de passe", "login_savePassword": "Sauvegarder le mot de passe", @@ -1393,7 +1393,7 @@ "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", "contacts_manageRoom": "Gérer le Room Server", - "room_management": "Administración del Servidor de Habitación", + "room_management": "Administrattion Room Server", "@community_joinConfirmation": { "placeholders": { "name": { diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d572b8f..c4e1e27 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -696,7 +696,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_manageRoom => 'Gérer le Room Server'; @override - String get contacts_roomLogin => 'Connexion Salle'; + String get contacts_roomLogin => 'Connexion Room Server'; @override String get contacts_openChat => 'Ouverture du Chat'; @@ -1559,7 +1559,7 @@ class AppLocalizationsFr extends AppLocalizations { String get login_repeaterLogin => 'Connexion au répéteur'; @override - String get login_roomLogin => 'Connexion Salle'; + String get login_roomLogin => 'Connexion Room Server'; @override String get login_password => 'Mot de passe'; @@ -1684,7 +1684,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_management => 'Gestion des répéteurs'; @override - String get room_management => 'Administración del Servidor de Habitación'; + String get room_management => 'Administrattion Room Server'; @override String get repeater_managementTools => 'Outils de Gestion'; From 7288f11c88299234c394ca490d6fbc1f40ab96b0 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 06:49:14 -0800 Subject: [PATCH 128/421] add chrome in planning --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bad9b6c..da92d47 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - ✅ **Android**: Full support (API 21+) - ✅ **iOS**: Full support (iOS 12+) - 🚧 **Desktop**: Limited support (macOS/Linux/Windows) +- 🚧 **Web**: Limited support (Chrome) ### Dependencies From c7b33f1d1b1db1e1babf13192edc72671ae12b61 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 06:51:40 -0800 Subject: [PATCH 129/421] readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da92d47..10fb0a5 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - ✅ **Android**: Full support (API 21+) - ✅ **iOS**: Full support (iOS 12+) - 🚧 **Desktop**: Limited support (macOS/Linux/Windows) -- 🚧 **Web**: Limited support (Chrome) +- 🚧 **Web**: Under construction (Chrome) ### Dependencies From 6d63e49938d0d783d3a9dee2e28d0a8bd20b16e4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 06:54:27 -0800 Subject: [PATCH 130/421] add platforminfo helper --- lib/connector/meshcore_connector.dart | 4 ++-- lib/screens/scanner_screen.dart | 5 ++-- lib/services/background_service.dart | 9 ++++---- lib/utils/gpx_export.dart | 5 ++++ lib/utils/platform_info.dart | 32 ++++++++++++++++++++++++++ lib/widgets/repeater_login_dialog.dart | 4 ++-- lib/widgets/room_login_dialog.dart | 4 ++-- 7 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 lib/utils/platform_info.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index afd1626..773e182 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -30,6 +30,7 @@ import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; import '../utils/battery_utils.dart'; +import '../utils/platform_info.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -693,8 +694,7 @@ class MeshCoreConnector extends ChangeNotifier { await _scanSubscription?.cancel(); // On iOS/macOS, wait for Bluetooth to be powered on before scanning - if (defaultTargetPlatform == TargetPlatform.iOS || - defaultTargetPlatform == TargetPlatform.macOS) { + if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { // Wait for adapter state to be powered on final adapterState = await FlutterBluePlus.adapterState.first; if (adapterState != BluetoothAdapterState.on) { diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index af9d75e..b5dedc1 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -1,7 +1,6 @@ import 'dart:async'; -import 'dart:io' show Platform; - import 'package:flutter/material.dart'; +import '../utils/platform_info.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:provider/provider.dart'; @@ -265,7 +264,7 @@ class _ScannerScreenState extends State { ], ), ), - if (Platform.isAndroid) + if (PlatformInfo.isAndroid) TextButton( onPressed: () => FlutterBluePlus.turnOn(), child: Text(context.l10n.scanner_enableBluetooth), diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 0edd393..6202b3b 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,12 +1,11 @@ -import 'dart:io'; - +import '../utils/platform_info.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; class BackgroundService { bool _initialized = false; Future initialize() async { - if (!Platform.isAndroid || _initialized) return; + if (!PlatformInfo.isAndroid || _initialized) return; FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'meshcore_background', @@ -29,7 +28,7 @@ class BackgroundService { } Future start() async { - if (!Platform.isAndroid) return; + if (!PlatformInfo.isAndroid) return; if (!_initialized) { await initialize(); } @@ -43,7 +42,7 @@ class BackgroundService { } Future stop() async { - if (!Platform.isAndroid) return; + if (!PlatformInfo.isAndroid) return; final running = await FlutterForegroundTask.isRunningService; if (!running) return; await FlutterForegroundTask.stopService(); diff --git a/lib/utils/gpx_export.dart b/lib/utils/gpx_export.dart index 595479c..b0165bd 100644 --- a/lib/utils/gpx_export.dart +++ b/lib/utils/gpx_export.dart @@ -4,6 +4,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:path_provider/path_provider.dart'; import 'dart:io'; +import '../utils/platform_info.dart'; import 'package:share_plus/share_plus.dart'; @@ -109,6 +110,10 @@ class GpxExport { String shareText, String subject, ) async { + if (PlatformInfo.isWeb) { + debugPrint("GPX export is not supported on Web."); + return gpxExportNotAvailable; + } if (_contacts.isEmpty) { debugPrint("No repeaters to export – nothing to share."); return gpxExportNoContacts; diff --git a/lib/utils/platform_info.dart b/lib/utils/platform_info.dart new file mode 100644 index 0000000..399bf13 --- /dev/null +++ b/lib/utils/platform_info.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; +import 'dart:io' show Platform; + +/// Utility class to safely check the current platform across web and native. +/// +/// Using `Platform` from `dart:io` directly on Web causes a crash. +/// This class handles the `kIsWeb` check first to avoid those crashes. +class PlatformInfo { + /// Whether the app is running in a web browser. + static bool get isWeb => kIsWeb; + + /// Whether the app is running on Android. + static bool get isAndroid => !kIsWeb && Platform.isAndroid; + + /// Whether the app is running on iOS. + static bool get isIOS => !kIsWeb && Platform.isIOS; + + /// Whether the app is running on macOS. + static bool get isMacOS => !kIsWeb && Platform.isMacOS; + + /// Whether the app is running on Windows. + static bool get isWindows => !kIsWeb && Platform.isWindows; + + /// Whether the app is running on Linux. + static bool get isLinux => !kIsWeb && Platform.isLinux; + + /// Whether the app is running on a mobile platform (Android or iOS). + static bool get isMobile => isAndroid || isIOS; + + /// Whether the app is running on a desktop platform (macOS, Windows, or Linux). + static bool get isDesktop => isMacOS || isWindows || isLinux; +} diff --git a/lib/widgets/repeater_login_dialog.dart b/lib/widgets/repeater_login_dialog.dart index b550cc2..ec0af66 100644 --- a/lib/widgets/repeater_login_dialog.dart +++ b/lib/widgets/repeater_login_dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import '../utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; @@ -326,8 +327,7 @@ class _RepeaterLoginDialogState extends State { }, onSubmitted: (_) => _handleLogin(), autofocus: - !(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) && + !PlatformInfo.isMobile && _passwordController.text.isEmpty, ), const SizedBox(height: 12), diff --git a/lib/widgets/room_login_dialog.dart b/lib/widgets/room_login_dialog.dart index cba7bec..7324f44 100644 --- a/lib/widgets/room_login_dialog.dart +++ b/lib/widgets/room_login_dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import '../utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; @@ -274,8 +275,7 @@ class _RoomLoginDialogState extends State { ), onSubmitted: (_) => _handleLogin(), autofocus: - !(defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) && + !PlatformInfo.isMobile && _passwordController.text.isEmpty, ), const SizedBox(height: 12), From b5e47ce44f7dda117361a1683cb755c90ec26db3 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 07:09:35 -0800 Subject: [PATCH 131/421] filter BLE at OS level --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 773e182..f8eabdb 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -727,6 +727,7 @@ class MeshCoreConnector extends ChangeNotifier { }); await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, ); From cf8f01128b5a364d3e61ca131cecd94619390891 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 07:09:35 -0800 Subject: [PATCH 132/421] filter BLE at OS level --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index afd1626..63da3ba 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -727,6 +727,7 @@ class MeshCoreConnector extends ChangeNotifier { }); await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, ); From 5676cbd84e9de4835efd53f0f4d6a7688ca299ed Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 07:40:40 -0800 Subject: [PATCH 133/421] chrome required screen --- lib/l10n/app_bg.arb | 2 + lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 2 + lib/l10n/app_fr.arb | 2 + lib/l10n/app_it.arb | 2 + lib/l10n/app_localizations.dart | 12 ++++ lib/l10n/app_localizations_bg.dart | 7 ++ lib/l10n/app_localizations_de.dart | 7 ++ lib/l10n/app_localizations_en.dart | 7 ++ lib/l10n/app_localizations_es.dart | 7 ++ lib/l10n/app_localizations_fr.dart | 7 ++ lib/l10n/app_localizations_it.dart | 7 ++ lib/l10n/app_localizations_nl.dart | 7 ++ lib/l10n/app_localizations_pl.dart | 7 ++ lib/l10n/app_localizations_pt.dart | 7 ++ lib/l10n/app_localizations_ru.dart | 7 ++ lib/l10n/app_localizations_sk.dart | 7 ++ lib/l10n/app_localizations_sl.dart | 7 ++ lib/l10n/app_localizations_sv.dart | 7 ++ lib/l10n/app_localizations_uk.dart | 7 ++ lib/l10n/app_localizations_zh.dart | 7 ++ lib/l10n/app_nl.arb | 2 + lib/l10n/app_pl.arb | 2 + lib/l10n/app_pt.arb | 2 + lib/l10n/app_ru.arb | 2 + lib/l10n/app_sk.arb | 2 + lib/l10n/app_sl.arb | 2 + lib/l10n/app_sv.arb | 2 + lib/l10n/app_uk.arb | 2 + lib/l10n/app_zh.arb | 2 + lib/main.dart | 7 +- lib/screens/chrome_required_screen.dart | 91 +++++++++++++++++++++++++ lib/utils/browser_detection.dart | 2 + lib/utils/browser_detection_stub.dart | 3 + lib/utils/browser_detection_web.dart | 15 ++++ lib/utils/platform_info.dart | 4 ++ 37 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 lib/screens/chrome_required_screen.dart create mode 100644 lib/utils/browser_detection.dart create mode 100644 lib/utils/browser_detection_stub.dart create mode 100644 lib/utils/browser_detection_web.dart diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023..848b6de 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1596,6 +1596,8 @@ "scanner_bluetoothOff": "Bluetooth е изключен.", "scanner_enableBluetooth": "Активирайте Bluetooth", "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "scanner_chromeRequired": "Изисква се браузър Chrome", + "scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.", "snrIndicator_lastSeen": "Последно видян", "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", "chat_ShowAllPaths": "Покажи всички пътища", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f7..e645bdc 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1622,6 +1622,8 @@ "pathTrace_clearTooltip": "Pfad löschen", "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", + "scanner_chromeRequired": "Chrome Browser erforderlich", + "scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", "scanner_enableBluetooth": "Bluetooth aktivieren", "snrIndicator_lastSeen": "Zuletzt gesehen", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72e..7f1ecbd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -72,6 +72,8 @@ "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", + "scanner_chromeRequired": "Chrome Browser Required", + "scanner_chromeRequiredMessage": "This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.", "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d3..5e333e5 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1622,6 +1622,8 @@ "map_removeLast": "Eliminar último", "map_pathTraceCancelled": "Rastreo de ruta cancelado.", "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", + "scanner_chromeRequired": "Navegador Chrome requerido", + "scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.", "scanner_bluetoothOff": "Bluetooth está desactivado.", "scanner_enableBluetooth": "Habilitar Bluetooth", "snrIndicator_nearByRepeaters": "Repetidores cercanos", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e162cdb..a0ae3b8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1594,6 +1594,8 @@ "map_removeLast": "Supprimer le dernier", "map_runTrace": "Exécuter la traçage de chemin", "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", + "scanner_chromeRequired": "Navigateur Chrome requis", + "scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.", "scanner_bluetoothOff": "Le Bluetooth est désactivé.", "scanner_enableBluetooth": "Activer le Bluetooth", "snrIndicator_lastSeen": "Dernière fois vu", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186..93359ac 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1595,6 +1595,8 @@ "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", + "scanner_chromeRequired": "Browser Chrome richiesto", + "scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.", "scanner_enableBluetooth": "Abilita il Bluetooth", "snrIndicator_nearByRepeaters": "Ripetitori vicini", "snrIndicator_lastSeen": "Ultimo accesso", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686ce..76210fd 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -388,6 +388,18 @@ abstract class AppLocalizations { /// **'Please turn on Bluetooth to scan for devices'** String get scanner_bluetoothOffMessage; + /// No description provided for @scanner_chromeRequired. + /// + /// In en, this message translates to: + /// **'Chrome Browser Required'** + String get scanner_chromeRequired; + + /// No description provided for @scanner_chromeRequiredMessage. + /// + /// In en, this message translates to: + /// **'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'** + String get scanner_chromeRequiredMessage; + /// No description provided for @scanner_enableBluetooth. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b..971d189 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -150,6 +150,13 @@ class AppLocalizationsBg extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; + @override + String get scanner_chromeRequired => 'Изисква се браузър Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.'; + @override String get scanner_enableBluetooth => 'Активирайте Bluetooth'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4..f5cb3d4 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -150,6 +150,13 @@ class AppLocalizationsDe extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; + @override + String get scanner_chromeRequired => 'Chrome Browser erforderlich'; + + @override + String get scanner_chromeRequiredMessage => + 'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.'; + @override String get scanner_enableBluetooth => 'Bluetooth aktivieren'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4..a858737 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -149,6 +149,13 @@ class AppLocalizationsEn extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Please turn on Bluetooth to scan for devices'; + @override + String get scanner_chromeRequired => 'Chrome Browser Required'; + + @override + String get scanner_chromeRequiredMessage => + 'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.'; + @override String get scanner_enableBluetooth => 'Enable Bluetooth'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde365..1d0bb2d 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -150,6 +150,13 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Por favor, active el Bluetooth para escanear dispositivos.'; + @override + String get scanner_chromeRequired => 'Navegador Chrome requerido'; + + @override + String get scanner_chromeRequiredMessage => + 'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.'; + @override String get scanner_enableBluetooth => 'Habilitar Bluetooth'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d572b8f..8ad6c74 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -150,6 +150,13 @@ class AppLocalizationsFr extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Veuillez activer le Bluetooth pour rechercher des appareils.'; + @override + String get scanner_chromeRequired => 'Navigateur Chrome requis'; + + @override + String get scanner_chromeRequiredMessage => + 'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.'; + @override String get scanner_enableBluetooth => 'Activer le Bluetooth'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f8..29bb627 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -150,6 +150,13 @@ class AppLocalizationsIt extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.'; + @override + String get scanner_chromeRequired => 'Browser Chrome richiesto'; + + @override + String get scanner_chromeRequiredMessage => + 'Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.'; + @override String get scanner_enableBluetooth => 'Abilita il Bluetooth'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b..77cc083 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -149,6 +149,13 @@ class AppLocalizationsNl extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.'; + @override + String get scanner_chromeRequired => 'Chrome-browser vereist'; + + @override + String get scanner_chromeRequiredMessage => + 'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.'; + @override String get scanner_enableBluetooth => 'Activeer Bluetooth'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b5..d9c0539 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -150,6 +150,13 @@ class AppLocalizationsPl extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.'; + @override + String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.'; + @override String get scanner_enableBluetooth => 'Włącz Bluetooth'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826..640988f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -150,6 +150,13 @@ class AppLocalizationsPt extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Por favor, ative o Bluetooth para escanear por dispositivos.'; + @override + String get scanner_chromeRequired => 'Navegador Chrome necessário'; + + @override + String get scanner_chromeRequiredMessage => + 'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.'; + @override String get scanner_enableBluetooth => 'Ative o Bluetooth'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c8..4de8db2 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -149,6 +149,13 @@ class AppLocalizationsRu extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; + @override + String get scanner_chromeRequired => 'Требуется браузер Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.'; + @override String get scanner_enableBluetooth => 'Включите Bluetooth'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6de..fe87700 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -150,6 +150,13 @@ class AppLocalizationsSk extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.'; + @override + String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.'; + @override String get scanner_enableBluetooth => 'Povolte Bluetooth'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e3058..5d9901a 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -150,6 +150,13 @@ class AppLocalizationsSl extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; + @override + String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.'; + @override String get scanner_enableBluetooth => 'Omogočite Bluetooth'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b8..c5a0ee9 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -149,6 +149,13 @@ class AppLocalizationsSv extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Vänligen aktivera Bluetooth för att söka efter enheter.'; + @override + String get scanner_chromeRequired => 'Chrome-webbläsare krävs'; + + @override + String get scanner_chromeRequiredMessage => + 'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.'; + @override String get scanner_enableBluetooth => 'Aktivera Bluetooth'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f367002..35b5143 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -150,6 +150,13 @@ class AppLocalizationsUk extends AppLocalizations { String get scanner_bluetoothOffMessage => 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; + @override + String get scanner_chromeRequired => 'Потрібен браузер Chrome'; + + @override + String get scanner_chromeRequiredMessage => + 'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.'; + @override String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800..a97e0fb 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -148,6 +148,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scanner_bluetoothOffMessage => '请打开蓝牙功能,以便搜索设备。'; + @override + String get scanner_chromeRequired => '需要 Chrome 浏览器'; + + @override + String get scanner_chromeRequiredMessage => + '此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。'; + @override String get scanner_enableBluetooth => '启用蓝牙'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdd..5a5b0ab 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1595,6 +1595,8 @@ "map_runTrace": "Padeshulp traceren", "scanner_enableBluetooth": "Activeer Bluetooth", "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", + "scanner_chromeRequired": "Chrome-browser vereist", + "scanner_chromeRequiredMessage": "Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.", "scanner_bluetoothOff": "Bluetooth is uitgeschakeld", "snrIndicator_lastSeen": "Laatst gezien", "snrIndicator_nearByRepeaters": "Nabije herhalingseenheden", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa7..72af443 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1594,6 +1594,8 @@ "map_removeLast": "Usuń ostatni", "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", + "scanner_chromeRequired": "Wymagana przeglądarka Chrome", + "scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.", "scanner_bluetoothOff": "Bluetooth jest wyłączony", "scanner_enableBluetooth": "Włącz Bluetooth", "snrIndicator_lastSeen": "Ostatnio widziany", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be6694..35c4635 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1596,6 +1596,8 @@ "scanner_enableBluetooth": "Ative o Bluetooth", "scanner_bluetoothOff": "Bluetooth está desativado", "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", + "scanner_chromeRequired": "Navegador Chrome necessário", + "scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.", "snrIndicator_nearByRepeaters": "Repetidores Próximos", "snrIndicator_lastSeen": "Visto pela última vez", "chat_ShowAllPaths": "Mostrar todos os caminhos", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3..d4d1939 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -836,6 +836,8 @@ "scanner_enableBluetooth": "Включите Bluetooth", "scanner_bluetoothOff": "Bluetooth выключен", "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "scanner_chromeRequired": "Требуется браузер Chrome", + "scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.", "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", "snrIndicator_lastSeen": "Последний раз видели", "chat_ShowAllPaths": "Показать все пути", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0a..8e8d1e8 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1594,6 +1594,8 @@ "map_runTrace": "Spustiť trasovaním cesty", "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", + "scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome", + "scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.", "scanner_bluetoothOff": "Bluetooth je vypnutý", "scanner_enableBluetooth": "Povolte Bluetooth", "snrIndicator_lastSeen": "Naposledy videný", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d..2e749b9 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1595,6 +1595,8 @@ "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", "scanner_enableBluetooth": "Omogočite Bluetooth", "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", + "scanner_chromeRequired": "Zahtevan brskalnik Chrome", + "scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.", "scanner_bluetoothOff": "Bluetooth je izklopljen", "snrIndicator_lastSeen": "Zadnjič videno", "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399..b311a45 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1595,6 +1595,8 @@ "map_removeLast": "Ta bort sista", "scanner_enableBluetooth": "Aktivera Bluetooth", "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", + "scanner_chromeRequired": "Chrome-webbläsare krävs", + "scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.", "scanner_bluetoothOff": "Bluetooth är avstängt", "snrIndicator_lastSeen": "Senast sedd", "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0..f5d3a42 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1595,6 +1595,8 @@ "map_pathTraceCancelled": "Відмінується трасування шляху", "scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", + "scanner_chromeRequired": "Потрібен браузер Chrome", + "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", "scanner_bluetoothOff": "Bluetooth вимкнено", "snrIndicator_lastSeen": "Останній раз бачили", "snrIndicator_nearByRepeaters": "Ближні ретранслятори", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7..1fac003 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1594,6 +1594,8 @@ "map_removeLast": "删除最后一个", "map_runTrace": "运行路径跟踪", "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", + "scanner_chromeRequired": "需要 Chrome 浏览器", + "scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。", "scanner_bluetoothOff": "蓝牙已关闭", "scanner_enableBluetooth": "启用蓝牙", "snrIndicator_lastSeen": "最近访问", diff --git a/lib/main.dart b/lib/main.dart index 3650a7e..af065ef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,9 @@ import 'package:flutter/foundation.dart'; import 'l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'screens/chrome_required_screen.dart'; +import 'utils/platform_info.dart'; + import 'connector/meshcore_connector.dart'; import 'screens/scanner_screen.dart'; import 'services/storage_service.dart'; @@ -179,7 +182,9 @@ class MeshCoreApp extends StatelessWidget { NotificationService().setLocale(locale); return child ?? const SizedBox.shrink(); }, - home: const ScannerScreen(), + home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) + ? const ChromeRequiredScreen() + : const ScannerScreen(), ); }, ), diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart new file mode 100644 index 0000000..a229c0a --- /dev/null +++ b/lib/screens/chrome_required_screen.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import '../l10n/l10n.dart'; + +class ChromeRequiredScreen extends StatelessWidget { + const ChromeRequiredScreen({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + return Scaffold( + body: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 32), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [const Color(0xFF1A1A1A), const Color(0xFF0D0D0D)] + : [const Color(0xFFF5F7FA), const Color(0xFFE4E7EB)], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.browser_not_supported_rounded, + size: 80, + color: Colors.orange, + ), + ), + const SizedBox(height: 32), + Text( + l10n.scanner_chromeRequired, + textAlign: TextAlign.center, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, + ), + ), + const SizedBox(height: 16), + Text( + l10n.scanner_chromeRequiredMessage, + textAlign: TextAlign.center, + style: theme.textTheme.bodyLarge?.copyWith( + color: isDark ? Colors.white70 : Colors.black54, + height: 1.5, + ), + ), + const SizedBox(height: 48), + // We can't really "fix" it for them other than telling them to use Chrome + // but we can provide a nice visual. + Container( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Colors.blue.withValues(alpha: 0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outline, size: 20, color: Colors.blue), + const SizedBox(width: 12), + Text( + "Web Bluetooth requires a Chromium browser", + style: theme.textTheme.bodyMedium?.copyWith( + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/utils/browser_detection.dart b/lib/utils/browser_detection.dart new file mode 100644 index 0000000..aafb838 --- /dev/null +++ b/lib/utils/browser_detection.dart @@ -0,0 +1,2 @@ +export 'browser_detection_stub.dart' + if (dart.library.html) 'browser_detection_web.dart'; diff --git a/lib/utils/browser_detection_stub.dart b/lib/utils/browser_detection_stub.dart new file mode 100644 index 0000000..f872d9a --- /dev/null +++ b/lib/utils/browser_detection_stub.dart @@ -0,0 +1,3 @@ +class BrowserDetection { + static bool get isChrome => false; +} diff --git a/lib/utils/browser_detection_web.dart b/lib/utils/browser_detection_web.dart new file mode 100644 index 0000000..e5b4b69 --- /dev/null +++ b/lib/utils/browser_detection_web.dart @@ -0,0 +1,15 @@ +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class BrowserDetection { + static bool get isChrome { + final userAgent = html.window.navigator.userAgent.toLowerCase(); + final vendor = html.window.navigator.vendor.toLowerCase(); + + // Chrome UA typically contains 'chrome' and vendor is 'Google Inc.' + // This also excludes Firefox, Safari, and sometimes Edge/Brave depending on strictness. + // For Web Bluetooth, Chrome, Edge (latest), and Brave usually work. + // But we'll follow the user's request for "isChrome". + return userAgent.contains('chrome') && vendor.contains('google'); + } +} diff --git a/lib/utils/platform_info.dart b/lib/utils/platform_info.dart index 399bf13..dc8e27e 100644 --- a/lib/utils/platform_info.dart +++ b/lib/utils/platform_info.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'dart:io' show Platform; +import 'browser_detection.dart'; /// Utility class to safely check the current platform across web and native. /// @@ -9,6 +10,9 @@ class PlatformInfo { /// Whether the app is running in a web browser. static bool get isWeb => kIsWeb; + /// Whether the app is running in the Chrome browser (only relevant if [isWeb] is true). + static bool get isChrome => isWeb && BrowserDetection.isChrome; + /// Whether the app is running on Android. static bool get isAndroid => !kIsWeb && Platform.isAndroid; From 5522f9a236648b270dc45cc83cb23a81317ae888 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:05:19 -0800 Subject: [PATCH 134/421] BLE select cancel --- lib/connector/meshcore_connector.dart | 145 +++++++++++++++++--------- lib/screens/scanner_screen.dart | 29 ++++-- 2 files changed, 111 insertions(+), 63 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index f8eabdb..ef24be7 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -686,64 +686,96 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.scanning) return; - _scanResults.clear(); - _setState(MeshCoreConnectionState.scanning); - - // Ensure any previous scan is fully stopped - await FlutterBluePlus.stopScan(); - await _scanSubscription?.cancel(); - - // On iOS/macOS, wait for Bluetooth to be powered on before scanning - if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { - // Wait for adapter state to be powered on - final adapterState = await FlutterBluePlus.adapterState.first; - if (adapterState != BluetoothAdapterState.on) { - // Wait for the adapter to turn on, with timeout - await FlutterBluePlus.adapterState - .firstWhere((state) => state == BluetoothAdapterState.on) - .timeout( - const Duration(seconds: 5), - onTimeout: () { - _setState(MeshCoreConnectionState.disconnected); - throw Exception('Bluetooth adapter not available'); - }, - ); - } - - // Add a small delay to allow BLE stack to fully initialize - await Future.delayed(const Duration(milliseconds: 300)); - } - - _scanSubscription = FlutterBluePlus.scanResults.listen((results) { + try { _scanResults.clear(); - for (var result in results) { - if (result.device.platformName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("Whisper-")) { - _scanResults.add(result); + _setState(MeshCoreConnectionState.scanning); + + // Ensure any previous scan is fully stopped + try { + await FlutterBluePlus.stopScan(); + } catch (_) {} + + try { + await _scanSubscription?.cancel(); + } catch (_) {} + _scanSubscription = null; + + // On iOS/macOS, wait for Bluetooth to be powered on before scanning + if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { + // Wait for adapter state to be powered on + final adapterState = await FlutterBluePlus.adapterState.first; + if (adapterState != BluetoothAdapterState.on) { + // Wait for the adapter to turn on, with timeout + await FlutterBluePlus.adapterState + .firstWhere((state) => state == BluetoothAdapterState.on) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + _setState(MeshCoreConnectionState.disconnected); + throw Exception('Bluetooth adapter not available'); + }, + ); } + + // Add a small delay to allow BLE stack to fully initialize + await Future.delayed(const Duration(milliseconds: 300)); } - notifyListeners(); - }); - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); + _scanSubscription = FlutterBluePlus.scanResults.listen( + (results) { + _scanResults.clear(); + for (var result in results) { + if (result.device.platformName.startsWith("MeshCore-") || + result.advertisementData.advName.startsWith("MeshCore-") || + result.advertisementData.advName.startsWith("Whisper-")) { + _scanResults.add(result); + } + } + notifyListeners(); + }, + onError: (Object e) { + debugPrint("scanResults stream error: $e"); + stopScan(); + }, + ); - await Future.delayed(timeout); - await stopScan(); + await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); + + await Future.delayed(timeout); + } catch (e) { + debugPrint("Scan error: $e"); + // On web, suppress common cancellation and chooser errors + if (kIsWeb) return; + + if (!PlatformInfo.isWeb) { + rethrow; + } + } finally { + await stopScan(); + } } Future stopScan() async { - await FlutterBluePlus.stopScan(); - await _scanSubscription?.cancel(); - _scanSubscription = null; - if (_state == MeshCoreConnectionState.scanning) { _setState(MeshCoreConnectionState.disconnected); } + + try { + await FlutterBluePlus.stopScan(); + } catch (e) { + debugPrint("stopScan error: $e"); + } + + try { + if (_scanSubscription != null) { + await _scanSubscription!.cancel(); + } + } catch (_) {} + _scanSubscription = null; } Future connect(BluetoothDevice device, {String? displayName}) async { @@ -770,11 +802,17 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); try { - _connectionSubscription = device.connectionState.listen((state) { - if (state == BluetoothConnectionState.disconnected && isConnected) { - _handleDisconnection(); - } - }); + _connectionSubscription = device.connectionState.listen( + (state) { + if (state == BluetoothConnectionState.disconnected && isConnected) { + _handleDisconnection(); + } + }, + onError: (Object e) { + debugPrint("connectionState stream error: $e"); + if (isConnected) _handleDisconnection(); + }, + ); await device.connect( timeout: const Duration(seconds: 15), @@ -833,6 +871,9 @@ class MeshCoreConnector extends ChangeNotifier { } _notifySubscription = _txCharacteristic!.onValueReceived.listen( _handleFrame, + onError: (Object e) { + debugPrint("onValueReceived stream error: $e"); + }, ); _setState(MeshCoreConnectionState.connected); diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index b5dedc1..bfd1230 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -45,17 +45,22 @@ class _ScannerScreenState extends State { connector.addListener(_connectionListener); - _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) { - if (mounted) { - setState(() { - _bluetoothState = state; - }); - // Cancel scan if Bluetooth turns off while scanning - if (state != BluetoothAdapterState.on) { - unawaited(connector.stopScan()); + _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen( + (state) { + if (mounted) { + setState(() { + _bluetoothState = state; + }); + // Cancel scan if Bluetooth turns off while scanning + if (state != BluetoothAdapterState.on) { + unawaited(connector.stopScan()); + } } - } - }); + }, + onError: (Object e) { + debugPrint("Scanner adapterState stream error: $e"); + }, + ); } @override @@ -107,7 +112,9 @@ class _ScannerScreenState extends State { if (isScanning) { connector.stopScan(); } else { - connector.startScan(); + unawaited(connector.startScan().catchError((e) { + debugPrint("Scanner screen startScan error: $e"); + })); } }, icon: isScanning From 6ac987e7cf0b80e86cbc963d5a4127ff2f39c508 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:10:16 -0800 Subject: [PATCH 135/421] select BLE device --- lib/connector/meshcore_connector.dart | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef24be7..e4a3324 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -739,13 +739,23 @@ class MeshCoreConnector extends ChangeNotifier { }, ); - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); + if (PlatformInfo.isWeb) { + await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], + ); + // On web, the chooser returns once a device is picked, but the scanResults + // stream might take a moment to emit the last result. Wait briefly so the + // device appears in the UI before stopScan() clears the list. + await Future.delayed(const Duration(milliseconds: 500)); + } else { + await FlutterBluePlus.startScan( + withServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); - await Future.delayed(timeout); + await Future.delayed(timeout); + } } catch (e) { debugPrint("Scan error: $e"); // On web, suppress common cancellation and chooser errors @@ -776,6 +786,12 @@ class MeshCoreConnector extends ChangeNotifier { } } catch (_) {} _scanSubscription = null; + + // On web, don't clear results immediately so the picked device remains visible + if (!PlatformInfo.isWeb) { + _scanResults.clear(); + notifyListeners(); + } } Future connect(BluetoothDevice device, {String? displayName}) async { From 452e5337f0ac84ad79f18ace74f22123f820bae7 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:31:29 -0800 Subject: [PATCH 136/421] chrome connect --- lib/connector/meshcore_connector.dart | 61 ++++++++++++++++++--------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e4a3324..8eacdab 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -837,11 +837,17 @@ class MeshCoreConnector extends ChangeNotifier { ); // Request larger MTU for sending larger frames - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); + if (!PlatformInfo.isWeb) { + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); + } + } else { + // On Chrome Web Bluetooth, give the GATT connection a moment to settle + // before discovering services, which is a common quirk to avoid timeouts. + await Future.delayed(const Duration(milliseconds: 500)); } List services = await device.discoverServices(); @@ -871,20 +877,7 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - // Retry setNotifyValue with increasing delays - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); - } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; - } - } + // Setup listener BEFORE enabling notifications so we don't miss anything _notifySubscription = _txCharacteristic!.onValueReceived.listen( _handleFrame, onError: (Object e) { @@ -892,6 +885,36 @@ class MeshCoreConnector extends ChangeNotifier { }, ); + debugPrint('Starting setNotifyValue(true)'); + if (PlatformInfo.isWeb) { + // On Web, setNotifyValue often hangs indefinitely on the Promise resolution. + // We trigger it but don't await its completion to avoid blocking the connection flow. + debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + // ignore: unawaited_futures + _txCharacteristic!.setNotifyValue(true).catchError((e) { + debugPrint('Web setNotifyValue error (ignoring): $e'); + }); + // Give the browser a moment to process the underlying startNotifications call + await Future.delayed(const Duration(milliseconds: 500)); + } else { + // Native platforms handle setNotifyValue blockingly with CCCD descriptors + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); + } + debugPrint('Calling setNotifyValue(true), attempt ${attempt + 1}'); + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; + } + } + } + debugPrint('setNotifyValue(true) configuration completed'); + _setState(MeshCoreConnectionState.connected); await _requestDeviceInfo(); From ab05cf8b3e8d07c5397e24b26f3a3cb4a8c6fca5 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:33:45 -0800 Subject: [PATCH 137/421] chrome BLE sync --- lib/connector/meshcore_connector.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 8eacdab..c558171 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -891,8 +891,9 @@ class MeshCoreConnector extends ChangeNotifier { // We trigger it but don't await its completion to avoid blocking the connection flow. debugPrint('Web: Calling setNotifyValue(true) without awaiting'); // ignore: unawaited_futures - _txCharacteristic!.setNotifyValue(true).catchError((e) { + _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { debugPrint('Web setNotifyValue error (ignoring): $e'); + return false; // catchError must return a bool to match Future }); // Give the browser a moment to process the underlying startNotifications call await Future.delayed(const Duration(milliseconds: 500)); From 71129bdf4d773c030eb2ac2a9d405e71da2eebe4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:37:07 -0800 Subject: [PATCH 138/421] chrome BLE load fix --- lib/connector/meshcore_connector.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c558171..9f94e72 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -893,7 +893,6 @@ class MeshCoreConnector extends ChangeNotifier { // ignore: unawaited_futures _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { debugPrint('Web setNotifyValue error (ignoring): $e'); - return false; // catchError must return a bool to match Future }); // Give the browser a moment to process the underlying startNotifications call await Future.delayed(const Duration(milliseconds: 500)); From 8e07440114074ea72ec8348b438a121eb8a092c3 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 08:38:22 -0800 Subject: [PATCH 139/421] BLE fix --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9f94e72..c558171 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -893,6 +893,7 @@ class MeshCoreConnector extends ChangeNotifier { // ignore: unawaited_futures _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { debugPrint('Web setNotifyValue error (ignoring): $e'); + return false; // catchError must return a bool to match Future }); // Give the browser a moment to process the underlying startNotifications call await Future.delayed(const Duration(milliseconds: 500)); From 1b4d31a36e32b3eb2990dabd1bbcf4e399557263 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:11:49 -0800 Subject: [PATCH 140/421] gitignore update --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2d9a3fc..0fc464a 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json + +# Cloudflare Wrangler +.wrangler \ No newline at end of file From a5555bd6062ee4361d2aa45b9822623ae29e8b18 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:16:07 -0800 Subject: [PATCH 141/421] fix: return cursor to message window after send --- lib/screens/channel_chat_screen.dart | 1 + lib/screens/chat_screen.dart | 1 + lib/screens/repeater_cli_screen.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110..9f40684 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -935,6 +935,7 @@ class _ChannelChatScreenState extends State { connector.sendChannelMessage(widget.channel, messageText); _textController.clear(); _cancelReply(); + _textFieldFocusNode.requestFocus(); } String _formatTime(DateTime time) { diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0..ea657ff 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -429,6 +429,7 @@ class _ChatScreenState extends State { connector.sendMessage(widget.contact, text); _textController.clear(); + _textFieldFocusNode.requestFocus(); } void _showPathHistory(BuildContext context) { diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index abfb06a..1c7ff43 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State { _commandController.clear(); _historyIndex = -1; + _commandFocusNode.requestFocus(); // Auto-scroll to bottom Future.delayed(const Duration(milliseconds: 100), () { From 9865a03c536127ecc2373293e0f49895a082f33c Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:20:20 -0800 Subject: [PATCH 142/421] fix: to send giphy --- lib/screens/channel_chat_screen.dart | 59 +++++++++++++++++----------- lib/screens/chat_screen.dart | 55 ++++++++++++++++---------- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9f40684..8c979c0 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -849,30 +849,45 @@ class _ChannelChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + fallbackTextColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ea657ff..c604ea6 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -340,28 +340,43 @@ class _ChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - colorScheme.surfaceContainerHighest, - fallbackTextColor: colorScheme.onSurface - .withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(connector); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + colorScheme.surfaceContainerHighest, + fallbackTextColor: colorScheme.onSurface + .withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } From 377f1df445aa64d890db6b609176d6032dd9e37a Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 10:47:51 -0800 Subject: [PATCH 143/421] fix: browser detection --- lib/utils/browser_detection_web.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/utils/browser_detection_web.dart b/lib/utils/browser_detection_web.dart index e5b4b69..0e4fccb 100644 --- a/lib/utils/browser_detection_web.dart +++ b/lib/utils/browser_detection_web.dart @@ -4,12 +4,7 @@ import 'dart:html' as html; class BrowserDetection { static bool get isChrome { final userAgent = html.window.navigator.userAgent.toLowerCase(); - final vendor = html.window.navigator.vendor.toLowerCase(); - - // Chrome UA typically contains 'chrome' and vendor is 'Google Inc.' - // This also excludes Firefox, Safari, and sometimes Edge/Brave depending on strictness. - // For Web Bluetooth, Chrome, Edge (latest), and Brave usually work. - // But we'll follow the user's request for "isChrome". - return userAgent.contains('chrome') && vendor.contains('google'); + final isChrome = userAgent.contains('chrome'); + return isChrome; } } From 40ac95e8e665d20ed736c621fbccc01d130ce31f Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 10:48:22 -0800 Subject: [PATCH 144/421] wrangler deploy --- .github/workflows/deploy.yml | 38 ++++++++++++++++++++++++++++++++++ package.json | 7 +++++++ pubspec.lock | 40 ++++++++++++++++++++++++++++++++++++ pubspec.yaml | 14 +++++++++++++ wrangler.toml | 7 +++++++ 5 files changed, 106 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 package.json create mode 100644 wrangler.toml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..892a95e --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,38 @@ +name: Deploy to Cloudflare Workers + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + # Match local development version which provides Dart 3.11.0 + flutter-version: '3.41.2' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Get dependencies + run: flutter pub get + + - name: Build Web + run: bun run build + + - name: Deploy to Cloudflare + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy diff --git a/package.json b/package.json new file mode 100644 index 0000000..1684721 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "meshcore-open", + "scripts": { + "build": "dart run build_pipe:build", + "deploy": "bun x wrangler deploy" + } +} diff --git a/pubspec.lock b/pubspec.lock index f695838..18eaff5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _discoveryapis_commons: + dependency: transitive + description: + name: _discoveryapis_commons + sha256: "113c4100b90a5b70a983541782431b82168b3cae166ab130649c36eb3559d498" + url: "https://pub.dev" + source: hosted + version: "1.0.7" archive: dependency: transitive description: @@ -41,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + build_pipe: + dependency: "direct main" + description: + name: build_pipe + sha256: "3cdfd6e2327805074a9a5c815e49b1cb781764c57d84097c257e0dce4534f419" + url: "https://pub.dev" + source: hosted + version: "0.3.1" cached_network_image: dependency: "direct main" description: @@ -365,6 +381,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + googleapis: + dependency: transitive + description: + name: googleapis + sha256: "692fb9e90c321b61a7a2123de0353ec8a20691cd979db2553d8d732f710f6535" + url: "https://pub.dev" + source: hosted + version: "15.0.0" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db + url: "https://pub.dev" + source: hosted + version: "2.0.0" gpx: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f5ceaaf..a17edde 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + build_pipe: ^0.3.1 dev_dependencies: flutter_test: @@ -124,3 +125,16 @@ flutter_launcher_icons: # # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package + +build_pipe: + workflows: + default: + clean_flutter: true # Optional: Cleans old build artifacts before running + generate_log: true # Optional: Outputs a build log file for debugging + platforms: + web: + build: + build_command: flutter build web --release --pwa-strategy=none + # Strongly recommended: disables the default service worker which often causes more cache headaches + add_version_query_param: true + # This is the key flag! It appends ?v= to bootstrap/JS files \ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..f3c57d9 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,7 @@ +#:schema node_modules/wrangler/config-schema.json +name = "meshcore" +compatibility_date = "2025-10-08" + +[assets] +directory = "build/web" + From 096e0a4184b5e5e8fb5907ab38cbb13f4e4f4630 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:16:07 -0800 Subject: [PATCH 145/421] fix: return cursor to message window after send --- lib/screens/channel_chat_screen.dart | 1 + lib/screens/chat_screen.dart | 1 + lib/screens/repeater_cli_screen.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110..9f40684 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -935,6 +935,7 @@ class _ChannelChatScreenState extends State { connector.sendChannelMessage(widget.channel, messageText); _textController.clear(); _cancelReply(); + _textFieldFocusNode.requestFocus(); } String _formatTime(DateTime time) { diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0..ea657ff 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -429,6 +429,7 @@ class _ChatScreenState extends State { connector.sendMessage(widget.contact, text); _textController.clear(); + _textFieldFocusNode.requestFocus(); } void _showPathHistory(BuildContext context) { diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index abfb06a..1c7ff43 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State { _commandController.clear(); _historyIndex = -1; + _commandFocusNode.requestFocus(); // Auto-scroll to bottom Future.delayed(const Duration(milliseconds: 100), () { From 3ca53e967c5d39ef496ec5bbda39ba6ec4ad2784 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:20:20 -0800 Subject: [PATCH 146/421] fix: to send giphy --- lib/screens/channel_chat_screen.dart | 59 +++++++++++++++++----------- lib/screens/chat_screen.dart | 55 ++++++++++++++++---------- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110..9fecdc7 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -849,30 +849,45 @@ class _ChannelChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + fallbackTextColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0..c5cd4c6 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -340,28 +340,43 @@ class _ChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - colorScheme.surfaceContainerHighest, - fallbackTextColor: colorScheme.onSurface - .withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(connector); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + colorScheme.surfaceContainerHighest, + fallbackTextColor: colorScheme.onSurface + .withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } From a1ee0789a6b5a3b68caefd84d73a6625948d002c Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:04:54 -0800 Subject: [PATCH 147/421] deploy on tag only --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 892a95e..c3926c9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,8 +2,8 @@ name: Deploy to Cloudflare Workers on: push: - branches: - - main + tags: + - '*' workflow_dispatch: jobs: From c284e571b089c4519d98078b038f00c25e762c1b Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:27:06 -0800 Subject: [PATCH 148/421] hide message tracing --- lib/l10n/app_bg.arb | 4 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 4 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 12 ++ lib/l10n/app_localizations_bg.dart | 8 + lib/l10n/app_localizations_de.dart | 8 + lib/l10n/app_localizations_en.dart | 7 + lib/l10n/app_localizations_es.dart | 8 + lib/l10n/app_localizations_fr.dart | 8 + lib/l10n/app_localizations_it.dart | 8 + lib/l10n/app_localizations_nl.dart | 7 + lib/l10n/app_localizations_pl.dart | 7 + lib/l10n/app_localizations_pt.dart | 8 + lib/l10n/app_localizations_ru.dart | 8 + lib/l10n/app_localizations_sk.dart | 7 + lib/l10n/app_localizations_sl.dart | 7 + lib/l10n/app_localizations_sv.dart | 7 + lib/l10n/app_localizations_uk.dart | 8 + lib/l10n/app_localizations_zh.dart | 6 + lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pl.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_sk.arb | 4 +- lib/l10n/app_sl.arb | 4 +- lib/l10n/app_sv.arb | 4 +- lib/l10n/app_uk.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/models/app_settings.dart | 6 + lib/screens/app_settings_screen.dart | 12 ++ lib/screens/channel_chat_screen.dart | 231 ++++++++++++++++--------- lib/screens/chat_screen.dart | 225 +++++++++++++++--------- lib/services/app_settings_service.dart | 4 + macos/Podfile.lock | 17 +- 37 files changed, 491 insertions(+), 186 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 848b6de..bd7fb3f 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1554,6 +1554,8 @@ "contacts_clipboardEmpty": "Клипборда е празна.", "contacts_invalidAdvertFormat": "Невалидни данни за контакт", "appSettings_languageRu": "Руски", + "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", + "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", "contacts_contactImported": "Контактът е импортиран.", "contacts_zeroHopAdvert": "Реклама без скок", "contacts_contactImportFailed": "Контактът не е успешно импортиран.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e645bdc..e5b0c46 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1554,6 +1554,8 @@ "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", "appSettings_languageUk": "Ukrainisch", + "appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren", + "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", @@ -1747,4 +1749,4 @@ "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7f1ecbd..12145f0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -185,6 +185,8 @@ "appSettings_languageBg": "Български", "appSettings_languageRu": "Русский", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Enable Message Tracing", + "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_notifications": "Notifications", "appSettings_enableNotifications": "Enable Notifications", "appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5e333e5..ab4febb 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1553,6 +1553,8 @@ "appSettings_languageUk": "Ucraniano", "contacts_clipboardEmpty": "El portapapeles está vacío.", "appSettings_languageRu": "Ruso", + "appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes", "contacts_invalidAdvertFormat": "Datos de contacto no válidos", "contacts_floodAdvert": "Anuncio de inundación", "contacts_contactImported": "El contacto ha sido importado.", @@ -1747,4 +1749,4 @@ "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a0ae3b8..61b4e63 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1553,6 +1553,8 @@ "contacts_invalidAdvertFormat": "Données de contact non valides", "appSettings_languageUk": "Ukrainien", "appSettings_languageRu": "Russe", + "appSettings_enableMessageTracing": "Activer le traçage des messages", + "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", "contacts_clipboardEmpty": "Le presse-papiers est vide.", "contacts_contactImported": "Le contact a été importé.", "contacts_floodAdvert": "Annonce à tout le réseau", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 93359ac..894874b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1553,6 +1553,8 @@ "appSettings_languageRu": "Russo", "contacts_invalidAdvertFormat": "Dati di contatto non validi", "appSettings_languageUk": "Ucraino", + "appSettings_enableMessageTracing": "Abilita tracciamento messaggi", + "appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi", "contacts_zeroHopAdvert": "Annuncio Zero Hop", "contacts_floodAdvert": "Annuncio alluvionale", "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 76210fd..b1f9762 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -982,6 +982,18 @@ abstract class AppLocalizations { /// **'Українська'** String get appSettings_languageUk; + /// No description provided for @appSettings_enableMessageTracing. + /// + /// In en, this message translates to: + /// **'Enable Message Tracing'** + String get appSettings_enableMessageTracing; + + /// No description provided for @appSettings_enableMessageTracingSubtitle. + /// + /// In en, this message translates to: + /// **'Show detailed routing and timing metadata for messages'** + String get appSettings_enableMessageTracingSubtitle; + /// No description provided for @appSettings_notifications. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 971d189..6e4f5fa 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -473,6 +473,14 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_languageUk => 'Украински'; + @override + String get appSettings_enableMessageTracing => + 'Разрешаване на проследяване на съобщения'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f5cb3d4..06da00e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -467,6 +467,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainisch'; + @override + String get appSettings_enableMessageTracing => + 'Nachrichtenverfolgung aktivieren'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; + @override String get appSettings_notifications => 'Benachrichtigungen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a858737..00e60ec 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -465,6 +465,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => 'Enable Message Tracing'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Show detailed routing and timing metadata for messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 1d0bb2d..d669219 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -470,6 +470,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Habilitar seguimiento de mensajes'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes'; + @override String get appSettings_notifications => 'Notificaciones'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 8ad6c74..bf747c9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -471,6 +471,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainien'; + @override + String get appSettings_enableMessageTracing => + 'Activer le traçage des messages'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 29bb627..caab1d3 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -469,6 +469,14 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraino'; + @override + String get appSettings_enableMessageTracing => + 'Abilita tracciamento messaggi'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostra metadati dettagliati su instradamento e tempi per i messaggi'; + @override String get appSettings_notifications => 'Notifiche'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 77cc083..fb3ce5e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -467,6 +467,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_languageUk => 'Oekraïens'; + @override + String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Gedetailleerde routerings- en timing-metadata voor berichten weergeven'; + @override String get appSettings_notifications => 'Notificaties'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index d9c0539..7c79eb9 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -471,6 +471,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukraińska'; + @override + String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości'; + @override String get appSettings_notifications => 'Powiadomienia'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 640988f..e352353 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -471,6 +471,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Ativar rastreamento de mensagens'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadados detalhados de roteamento e tempo para as mensagens'; + @override String get appSettings_notifications => 'Notificações'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4de8db2..85c3341 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -469,6 +469,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Включить трассировку сообщений'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index fe87700..14750cf 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -467,6 +467,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinská'; + @override + String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Zobraziť podrobné metadáta o smerovaní a časovaní správ'; + @override String get appSettings_notifications => 'Upozornenia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 5d9901a..eb7b814 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -466,6 +466,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinsko'; + @override + String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; + @override String get appSettings_notifications => 'Obvestila'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index c5a0ee9..f1cf84c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -464,6 +464,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainska'; + @override + String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; + @override String get appSettings_notifications => 'Meddelanden'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 35b5143..f236390 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -469,6 +469,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Увімкнути відстеження повідомлень'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; + @override String get appSettings_notifications => 'Сповіщення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index a97e0fb..21323e0 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -453,6 +453,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_languageUk => '乌克兰'; + @override + String get appSettings_enableMessageTracing => '启用消息追踪'; + + @override + String get appSettings_enableMessageTracingSubtitle => '显示消息的详细路由和时间元数据'; + @override String get appSettings_notifications => '通知'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 5a5b0ab..372be07 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1557,6 +1557,8 @@ "contacts_floodAdvert": "Overstromingsadvertentie", "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", "appSettings_languageRu": "Russisch", + "appSettings_enableMessageTracing": "Berichttracking inschakelen", + "appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven", "contacts_clipboardEmpty": "Knipbord is leeg.", "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", "contacts_contactImported": "Contact is geïmporteerd.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 72af443..e36232e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1552,6 +1552,8 @@ "contacts_chatTraceRoute": "Śledź trasę promienia", "appSettings_languageRu": "Rosyjski", "appSettings_languageUk": "Ukraińska", + "appSettings_enableMessageTracing": "Włącz śledzenie wiadomości", + "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości", "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", "contacts_zeroHopAdvert": "Reklama Zero Hop", "contacts_floodAdvert": "Reklama powodziowa", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 35c4635..482451d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", "appSettings_languageRu": "Russo", + "appSettings_enableMessageTracing": "Ativar rastreamento de mensagens", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens", "contacts_ShareContact": "Copiar contato para Área de Transferência", "contacts_contactImportFailed": "Contato falhou ao ser importado.", "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index d4d1939..85796ba 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -796,6 +796,8 @@ "contacts_invalidAdvertFormat": "Недействительные контактные данные", "contacts_zeroHopAdvert": "Реклама Zero Hop", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Включить трассировку сообщений", + "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", "contacts_floodAdvert": "Рекламный поток", "contacts_clipboardEmpty": "Буфер обмена пуст.", "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", @@ -959,4 +961,4 @@ "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8e8d1e8..f4d4671 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", "appSettings_languageRu": "Ruština", + "appSettings_enableMessageTracing": "Povoliť sledovanie správ", + "appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ", "contacts_addContactFromClipboard": "Pridať kontakt z schránky", "contacts_contactImported": "Kontakt bol importovaný.", "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 2e749b9..e1d2ce4 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1552,6 +1552,8 @@ "contacts_pathTraceTo": "Trace route to {name}", "appSettings_languageRu": "Ruščina", "appSettings_languageUk": "Ukrajinsko", + "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", + "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", "contacts_contactImported": "Kontakt je bil uvožen.", "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", "contacts_zeroHopAdvert": "Reklama brez posrednikov", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b311a45..ed4f9e5 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", "appSettings_languageUk": "Ukrainska", + "appSettings_enableMessageTracing": "Aktivera meddelandespårning", + "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", "contacts_contactImported": "Kontakt har importerats.", "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index f5d3a42..0575302 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1559,6 +1559,8 @@ "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", "contacts_clipboardEmpty": "Буфер обміну порожній", "appSettings_languageRu": "Російська", + "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", + "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", "contacts_ShareContact": "Копіювати контакт у буфер обміну", "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1fac003..37af48e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -176,6 +176,8 @@ "appSettings_languageBg": "保加利亚", "appSettings_languageRu": "俄语", "appSettings_languageUk": "乌克兰", + "appSettings_enableMessageTracing": "启用消息追踪", + "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", @@ -1719,4 +1721,4 @@ "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index d9504b3..62ba9ca 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -22,6 +22,7 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; @@ -47,6 +48,7 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, @@ -76,6 +78,7 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, @@ -112,6 +115,7 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), ), @@ -155,6 +159,7 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, @@ -180,6 +185,7 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index b309b4d..a2c920e 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -82,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget { trailing: const Icon(Icons.chevron_right), onTap: () => _showLanguageDialog(context, settingsService), ), + const Divider(height: 1), + SwitchListTile( + secondary: const Icon(Icons.location_searching), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, + ), + value: settingsService.settings.enableMessageTracing, + onChanged: (value) { + settingsService.setEnableMessageTracing(value); + }, + ), ], ), ); diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 8c979c0..9ac5a23 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -17,6 +17,7 @@ import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; +import '../services/app_settings_service.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; @@ -263,6 +264,8 @@ class _ChannelChatScreenState extends State { } Widget _buildMessageBubble(ChannelMessage message) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final gifId = _parseGifId(message.text); final poi = _parsePoiMessage(message.text); @@ -336,108 +339,166 @@ class _ChannelChatScreenState extends State { if (poi != null) _buildPoiMessage(context, poi, isOutgoing) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), + ), + ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - 'via ${_formatPathPrefixes(displayPath)}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( + Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), + Flexible( + child: Linkify( + text: message.text, + style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + ), + if (!enableTracing && isOutgoing) ...[ + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, + ), + ), + ], + ], + ), + if (enableTracing) ...[ + if (displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - '${message.repeatCount}', + _formatTime(message.timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12, + color: Colors.grey[600], + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == + ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: + message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], + ), ), - ), + ], ], ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index c604ea6..a40a269 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -20,6 +20,7 @@ import '../models/channel_message.dart'; import '../models/contact.dart'; import '../models/message.dart'; import '../models/path_history.dart'; +import '../services/app_settings_service.dart'; import '../services/path_history_service.dart'; import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; @@ -1188,6 +1189,8 @@ class _MessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; final gifId = _parseGifId(message.text); @@ -1267,100 +1270,158 @@ class _MessageBubble extends StatelessWidget { if (poi != null) _buildPoiMessage(context, poi, textColor, metaColor) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: textColor.withValues( - alpha: 0.7, + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues( + alpha: 0.7, + ), + ), ), - ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: messageText, - style: TextStyle(color: textColor), - linkStyle: const TextStyle( - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount( - message.retryCount, - 4, - ), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, + Flexible( + child: Linkify( + text: messageText, + style: TextStyle(color: textColor), + linkStyle: const TextStyle( + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), ), ), - if (isOutgoing) ...[ + if (!enableTracing && isOutgoing) ...[ const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == - MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', - style: TextStyle( - fontSize: 9, - color: isOutgoing - ? metaColor - : Colors.green[700], + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], ], ), - ), + if (enableTracing) ...[ + if (isOutgoing && message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + 4, + ), + style: TextStyle( + fontSize: 10, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 10, + color: metaColor, + ), + ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + _buildStatusIcon(metaColor), + ], + if (message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + ), + ], + ], + ), + ), + ], ], ), ), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index e80f903..eacf26f 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setEnableMessageTracing(bool value) async { + await updateSettings(_settings.copyWith(enableMessageTracing: value)); + } + Future setMapCacheBounds(Map? value) async { await updateSettings(_settings.copyWith(mapCacheBounds: value)); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 65fed26..8224cfb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,10 +5,13 @@ PODS: - flutter_local_notifications (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (6.0.2): + - mobile_scanner (7.0.0): + - Flutter - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS + - share_plus (0.0.1): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -24,8 +27,9 @@ DEPENDENCIES: - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -39,9 +43,11 @@ EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite_darwin: @@ -53,10 +59,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 - flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7 + flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd From bf4f52a4e30d2c585361c2865bdecc4c82e5b4d0 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:27:06 -0800 Subject: [PATCH 149/421] hide message tracing --- lib/l10n/app_bg.arb | 4 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 4 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 12 ++ lib/l10n/app_localizations_bg.dart | 8 + lib/l10n/app_localizations_de.dart | 8 + lib/l10n/app_localizations_en.dart | 7 + lib/l10n/app_localizations_es.dart | 8 + lib/l10n/app_localizations_fr.dart | 8 + lib/l10n/app_localizations_it.dart | 8 + lib/l10n/app_localizations_nl.dart | 7 + lib/l10n/app_localizations_pl.dart | 7 + lib/l10n/app_localizations_pt.dart | 8 + lib/l10n/app_localizations_ru.dart | 8 + lib/l10n/app_localizations_sk.dart | 7 + lib/l10n/app_localizations_sl.dart | 7 + lib/l10n/app_localizations_sv.dart | 7 + lib/l10n/app_localizations_uk.dart | 8 + lib/l10n/app_localizations_zh.dart | 6 + lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pl.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_sk.arb | 4 +- lib/l10n/app_sl.arb | 4 +- lib/l10n/app_sv.arb | 4 +- lib/l10n/app_uk.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/models/app_settings.dart | 6 + lib/screens/app_settings_screen.dart | 12 ++ lib/screens/channel_chat_screen.dart | 231 ++++++++++++++++--------- lib/screens/chat_screen.dart | 225 +++++++++++++++--------- lib/services/app_settings_service.dart | 4 + macos/Podfile.lock | 17 +- 37 files changed, 491 insertions(+), 186 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023..e9f46c6 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1554,6 +1554,8 @@ "contacts_clipboardEmpty": "Клипборда е празна.", "contacts_invalidAdvertFormat": "Невалидни данни за контакт", "appSettings_languageRu": "Руски", + "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", + "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", "contacts_contactImported": "Контактът е импортиран.", "contacts_zeroHopAdvert": "Реклама без скок", "contacts_contactImportFailed": "Контактът не е успешно импортиран.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f7..bdea574 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1554,6 +1554,8 @@ "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", "appSettings_languageUk": "Ukrainisch", + "appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren", + "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", @@ -1745,4 +1747,4 @@ "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72e..0e96e46 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -183,6 +183,8 @@ "appSettings_languageBg": "Български", "appSettings_languageRu": "Русский", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Enable Message Tracing", + "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_notifications": "Notifications", "appSettings_enableNotifications": "Enable Notifications", "appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d3..99db15d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1553,6 +1553,8 @@ "appSettings_languageUk": "Ucraniano", "contacts_clipboardEmpty": "El portapapeles está vacío.", "appSettings_languageRu": "Ruso", + "appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes", "contacts_invalidAdvertFormat": "Datos de contacto no válidos", "contacts_floodAdvert": "Anuncio de inundación", "contacts_contactImported": "El contacto ha sido importado.", @@ -1745,4 +1747,4 @@ "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e162cdb..8f5a40b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1553,6 +1553,8 @@ "contacts_invalidAdvertFormat": "Données de contact non valides", "appSettings_languageUk": "Ukrainien", "appSettings_languageRu": "Russe", + "appSettings_enableMessageTracing": "Activer le traçage des messages", + "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", "contacts_clipboardEmpty": "Le presse-papiers est vide.", "contacts_contactImported": "Le contact a été importé.", "contacts_floodAdvert": "Annonce à tout le réseau", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186..fe4bffc 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1553,6 +1553,8 @@ "appSettings_languageRu": "Russo", "contacts_invalidAdvertFormat": "Dati di contatto non validi", "appSettings_languageUk": "Ucraino", + "appSettings_enableMessageTracing": "Abilita tracciamento messaggi", + "appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi", "contacts_zeroHopAdvert": "Annuncio Zero Hop", "contacts_floodAdvert": "Annuncio alluvionale", "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686ce..6097f86 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -970,6 +970,18 @@ abstract class AppLocalizations { /// **'Українська'** String get appSettings_languageUk; + /// No description provided for @appSettings_enableMessageTracing. + /// + /// In en, this message translates to: + /// **'Enable Message Tracing'** + String get appSettings_enableMessageTracing; + + /// No description provided for @appSettings_enableMessageTracingSubtitle. + /// + /// In en, this message translates to: + /// **'Show detailed routing and timing metadata for messages'** + String get appSettings_enableMessageTracingSubtitle; + /// No description provided for @appSettings_notifications. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b..94f9f7a 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -466,6 +466,14 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_languageUk => 'Украински'; + @override + String get appSettings_enableMessageTracing => + 'Разрешаване на проследяване на съобщения'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4..ba0f5da 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -460,6 +460,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainisch'; + @override + String get appSettings_enableMessageTracing => + 'Nachrichtenverfolgung aktivieren'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; + @override String get appSettings_notifications => 'Benachrichtigungen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4..ce5b2f0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -458,6 +458,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => 'Enable Message Tracing'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Show detailed routing and timing metadata for messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde365..3c7838d 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -463,6 +463,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Habilitar seguimiento de mensajes'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes'; + @override String get appSettings_notifications => 'Notificaciones'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d572b8f..a8851a2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -464,6 +464,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainien'; + @override + String get appSettings_enableMessageTracing => + 'Activer le traçage des messages'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f8..b3c41e7 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -462,6 +462,14 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraino'; + @override + String get appSettings_enableMessageTracing => + 'Abilita tracciamento messaggi'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostra metadati dettagliati su instradamento e tempi per i messaggi'; + @override String get appSettings_notifications => 'Notifiche'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b..0ff00ad 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -460,6 +460,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_languageUk => 'Oekraïens'; + @override + String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Gedetailleerde routerings- en timing-metadata voor berichten weergeven'; + @override String get appSettings_notifications => 'Notificaties'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b5..d6c1e15 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -464,6 +464,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukraińska'; + @override + String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości'; + @override String get appSettings_notifications => 'Powiadomienia'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826..a300ba9 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -464,6 +464,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Ativar rastreamento de mensagens'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadados detalhados de roteamento e tempo para as mensagens'; + @override String get appSettings_notifications => 'Notificações'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c8..c4c1633 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -462,6 +462,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Включить трассировку сообщений'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6de..0df70a6 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -460,6 +460,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinská'; + @override + String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Zobraziť podrobné metadáta o smerovaní a časovaní správ'; + @override String get appSettings_notifications => 'Upozornenia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e3058..4be105e 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -459,6 +459,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinsko'; + @override + String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; + @override String get appSettings_notifications => 'Obvestila'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b8..52fa531 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -457,6 +457,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainska'; + @override + String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; + @override String get appSettings_notifications => 'Meddelanden'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f367002..4847009 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -462,6 +462,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Увімкнути відстеження повідомлень'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; + @override String get appSettings_notifications => 'Сповіщення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800..454f127 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -446,6 +446,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_languageUk => '乌克兰'; + @override + String get appSettings_enableMessageTracing => '启用消息追踪'; + + @override + String get appSettings_enableMessageTracingSubtitle => '显示消息的详细路由和时间元数据'; + @override String get appSettings_notifications => '通知'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdd..2f39fdf 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1557,6 +1557,8 @@ "contacts_floodAdvert": "Overstromingsadvertentie", "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", "appSettings_languageRu": "Russisch", + "appSettings_enableMessageTracing": "Berichttracking inschakelen", + "appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven", "contacts_clipboardEmpty": "Knipbord is leeg.", "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", "contacts_contactImported": "Contact is geïmporteerd.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa7..0432f8f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1552,6 +1552,8 @@ "contacts_chatTraceRoute": "Śledź trasę promienia", "appSettings_languageRu": "Rosyjski", "appSettings_languageUk": "Ukraińska", + "appSettings_enableMessageTracing": "Włącz śledzenie wiadomości", + "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości", "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", "contacts_zeroHopAdvert": "Reklama Zero Hop", "contacts_floodAdvert": "Reklama powodziowa", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be6694..01c5a83 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", "appSettings_languageRu": "Russo", + "appSettings_enableMessageTracing": "Ativar rastreamento de mensagens", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens", "contacts_ShareContact": "Copiar contato para Área de Transferência", "contacts_contactImportFailed": "Contato falhou ao ser importado.", "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3..b8a20d9 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -796,6 +796,8 @@ "contacts_invalidAdvertFormat": "Недействительные контактные данные", "contacts_zeroHopAdvert": "Реклама Zero Hop", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Включить трассировку сообщений", + "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", "contacts_floodAdvert": "Рекламный поток", "contacts_clipboardEmpty": "Буфер обмена пуст.", "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", @@ -957,4 +959,4 @@ "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0a..3245282 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", "appSettings_languageRu": "Ruština", + "appSettings_enableMessageTracing": "Povoliť sledovanie správ", + "appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ", "contacts_addContactFromClipboard": "Pridať kontakt z schránky", "contacts_contactImported": "Kontakt bol importovaný.", "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d..c560c31 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1552,6 +1552,8 @@ "contacts_pathTraceTo": "Trace route to {name}", "appSettings_languageRu": "Ruščina", "appSettings_languageUk": "Ukrajinsko", + "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", + "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", "contacts_contactImported": "Kontakt je bil uvožen.", "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", "contacts_zeroHopAdvert": "Reklama brez posrednikov", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399..b93c5ca 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", "appSettings_languageUk": "Ukrainska", + "appSettings_enableMessageTracing": "Aktivera meddelandespårning", + "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", "contacts_contactImported": "Kontakt har importerats.", "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0..235e4ed 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1559,6 +1559,8 @@ "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", "contacts_clipboardEmpty": "Буфер обміну порожній", "appSettings_languageRu": "Російська", + "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", + "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", "contacts_ShareContact": "Копіювати контакт у буфер обміну", "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7..72f48ad 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -176,6 +176,8 @@ "appSettings_languageBg": "保加利亚", "appSettings_languageRu": "俄语", "appSettings_languageUk": "乌克兰", + "appSettings_enableMessageTracing": "启用消息追踪", + "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index d9504b3..62ba9ca 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -22,6 +22,7 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; @@ -47,6 +48,7 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, @@ -76,6 +78,7 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, @@ -112,6 +115,7 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), ), @@ -155,6 +159,7 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, @@ -180,6 +185,7 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index b309b4d..a2c920e 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -82,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget { trailing: const Icon(Icons.chevron_right), onTap: () => _showLanguageDialog(context, settingsService), ), + const Divider(height: 1), + SwitchListTile( + secondary: const Icon(Icons.location_searching), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, + ), + value: settingsService.settings.enableMessageTracing, + onChanged: (value) { + settingsService.setEnableMessageTracing(value); + }, + ), ], ), ); diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110..8bb004e 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -17,6 +17,7 @@ import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; +import '../services/app_settings_service.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; @@ -263,6 +264,8 @@ class _ChannelChatScreenState extends State { } Widget _buildMessageBubble(ChannelMessage message) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final gifId = _parseGifId(message.text); final poi = _parsePoiMessage(message.text); @@ -336,108 +339,166 @@ class _ChannelChatScreenState extends State { if (poi != null) _buildPoiMessage(context, poi, isOutgoing) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), + ), + ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - 'via ${_formatPathPrefixes(displayPath)}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( + Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), + Flexible( + child: Linkify( + text: message.text, + style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + ), + if (!enableTracing && isOutgoing) ...[ + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, + ), + ), + ], + ], + ), + if (enableTracing) ...[ + if (displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - '${message.repeatCount}', + _formatTime(message.timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12, + color: Colors.grey[600], + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == + ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: + message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], + ), ), - ), + ], ], ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0..f97d4bb 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -20,6 +20,7 @@ import '../models/channel_message.dart'; import '../models/contact.dart'; import '../models/message.dart'; import '../models/path_history.dart'; +import '../services/app_settings_service.dart'; import '../services/path_history_service.dart'; import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; @@ -1172,6 +1173,8 @@ class _MessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; final gifId = _parseGifId(message.text); @@ -1251,100 +1254,158 @@ class _MessageBubble extends StatelessWidget { if (poi != null) _buildPoiMessage(context, poi, textColor, metaColor) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: textColor.withValues( - alpha: 0.7, + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues( + alpha: 0.7, + ), + ), ), - ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: messageText, - style: TextStyle(color: textColor), - linkStyle: const TextStyle( - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount( - message.retryCount, - 4, - ), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, + Flexible( + child: Linkify( + text: messageText, + style: TextStyle(color: textColor), + linkStyle: const TextStyle( + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), ), ), - if (isOutgoing) ...[ + if (!enableTracing && isOutgoing) ...[ const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == - MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', - style: TextStyle( - fontSize: 9, - color: isOutgoing - ? metaColor - : Colors.green[700], + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], ], ), - ), + if (enableTracing) ...[ + if (isOutgoing && message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + 4, + ), + style: TextStyle( + fontSize: 10, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 10, + color: metaColor, + ), + ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + _buildStatusIcon(metaColor), + ], + if (message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + ), + ], + ], + ), + ), + ], ], ), ), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index e80f903..eacf26f 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setEnableMessageTracing(bool value) async { + await updateSettings(_settings.copyWith(enableMessageTracing: value)); + } + Future setMapCacheBounds(Map? value) async { await updateSettings(_settings.copyWith(mapCacheBounds: value)); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 65fed26..8224cfb 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,10 +5,13 @@ PODS: - flutter_local_notifications (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (6.0.2): + - mobile_scanner (7.0.0): + - Flutter - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS + - share_plus (0.0.1): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -24,8 +27,9 @@ DEPENDENCIES: - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -39,9 +43,11 @@ EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite_darwin: @@ -53,10 +59,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 - flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7 + flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd From 35498c1b9046a958206c174e582760fa31bd67cf Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:31:56 -0800 Subject: [PATCH 150/421] formatting fix --- lib/screens/channel_chat_screen.dart | 52 ++++++++++++++++++---------- lib/screens/chat_screen.dart | 50 +++++++++++++++++--------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 8bb004e..9b9de35 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -367,17 +367,24 @@ class _ChannelChatScreenState extends State { shape: BoxShape.circle, ), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -402,8 +409,10 @@ class _ChannelChatScreenState extends State { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -411,17 +420,24 @@ class _ChannelChatScreenState extends State { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index f97d4bb..32a7882 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1274,21 +1274,30 @@ class _MessageBubble extends StatelessWidget { child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), + color: Colors.black.withValues( + alpha: 0.3, + ), shape: BoxShape.circle, ), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + MessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + MessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -1312,8 +1321,10 @@ class _MessageBubble extends StatelessWidget { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -1321,17 +1332,22 @@ class _MessageBubble extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + ? Colors.red + : Colors.grey, ), ), ], From d269e181c38d01c017d6bbeb9098bf4e3b6d7164 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:34:18 -0800 Subject: [PATCH 151/421] formatting fix --- lib/screens/channel_chat_screen.dart | 10 ++++++---- lib/screens/chat_screen.dart | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9fecdc7..5173852 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -854,7 +854,8 @@ class _ChannelChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(); return KeyEventResult.handled; } @@ -871,9 +872,10 @@ class _ChannelChatScreenState extends State { backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), + fallbackTextColor: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), maxSize: 160, ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index c5cd4c6..fb31c00 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -345,7 +345,8 @@ class _ChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(connector); return KeyEventResult.handled; } From 4975b5366edea931b8a65cc6ecbd8b4fd714cfdd Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:34:37 -0800 Subject: [PATCH 152/421] formatting fixes --- lib/connector/meshcore_connector.dart | 6 +-- lib/screens/channel_chat_screen.dart | 62 ++++++++++++++++--------- lib/screens/chat_screen.dart | 53 ++++++++++++++------- lib/screens/chrome_required_screen.dart | 4 +- lib/screens/scanner_screen.dart | 8 ++-- 5 files changed, 84 insertions(+), 49 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c558171..e4eaaf6 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -694,7 +694,7 @@ class MeshCoreConnector extends ChangeNotifier { try { await FlutterBluePlus.stopScan(); } catch (_) {} - + try { await _scanSubscription?.cancel(); } catch (_) {} @@ -743,7 +743,7 @@ class MeshCoreConnector extends ChangeNotifier { await FlutterBluePlus.startScan( withServices: [Guid(MeshCoreUuids.service)], ); - // On web, the chooser returns once a device is picked, but the scanResults + // On web, the chooser returns once a device is picked, but the scanResults // stream might take a moment to emit the last result. Wait briefly so the // device appears in the UI before stopScan() clears the list. await Future.delayed(const Duration(milliseconds: 500)); @@ -760,7 +760,7 @@ class MeshCoreConnector extends ChangeNotifier { debugPrint("Scan error: $e"); // On web, suppress common cancellation and chooser errors if (kIsWeb) return; - + if (!PlatformInfo.isWeb) { rethrow; } diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9ac5a23..b59a691 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -367,17 +367,24 @@ class _ChannelChatScreenState extends State { shape: BoxShape.circle, ), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -402,8 +409,10 @@ class _ChannelChatScreenState extends State { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -411,17 +420,24 @@ class _ChannelChatScreenState extends State { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], @@ -915,7 +931,8 @@ class _ChannelChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(); return KeyEventResult.handled; } @@ -932,9 +949,10 @@ class _ChannelChatScreenState extends State { backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), + fallbackTextColor: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), maxSize: 160, ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index a40a269..6ee846c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -346,7 +346,8 @@ class _ChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(connector); return KeyEventResult.handled; } @@ -1290,21 +1291,30 @@ class _MessageBubble extends StatelessWidget { child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), + color: Colors.black.withValues( + alpha: 0.3, + ), shape: BoxShape.circle, ), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + MessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + MessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -1328,8 +1338,10 @@ class _MessageBubble extends StatelessWidget { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -1337,17 +1349,22 @@ class _MessageBubble extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + ? Colors.red + : Colors.grey, ), ), ], diff --git a/lib/screens/chrome_required_screen.dart b/lib/screens/chrome_required_screen.dart index a229c0a..1827aeb 100644 --- a/lib/screens/chrome_required_screen.dart +++ b/lib/screens/chrome_required_screen.dart @@ -64,9 +64,7 @@ class ChromeRequiredScreen extends StatelessWidget { decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(30), - border: Border.all( - color: Colors.blue.withValues(alpha: 0.3), - ), + border: Border.all(color: Colors.blue.withValues(alpha: 0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index bfd1230..4017408 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -112,9 +112,11 @@ class _ScannerScreenState extends State { if (isScanning) { connector.stopScan(); } else { - unawaited(connector.startScan().catchError((e) { - debugPrint("Scanner screen startScan error: $e"); - })); + unawaited( + connector.startScan().catchError((e) { + debugPrint("Scanner screen startScan error: $e"); + }), + ); } }, icon: isScanning From 332bb5ef3afcb64eef89ceccf143ac5ac2570391 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:06:08 -0500 Subject: [PATCH 153/421] Updated PR and Added snackbar Translations --- .local-agent/AGENTS.local.md | 48 +++++++++++++++++++++++++ .local-agent/memory.local.md | 6 ++++ lib/l10n/app_en.arb | 8 +++++ lib/l10n/app_localizations.dart | 6 ++++ lib/l10n/app_localizations_bg.dart | 5 +++ lib/l10n/app_localizations_de.dart | 5 +++ lib/l10n/app_localizations_en.dart | 5 +++ lib/l10n/app_localizations_es.dart | 5 +++ lib/l10n/app_localizations_fr.dart | 5 +++ lib/l10n/app_localizations_it.dart | 5 +++ lib/l10n/app_localizations_nl.dart | 5 +++ lib/l10n/app_localizations_pl.dart | 5 +++ lib/l10n/app_localizations_pt.dart | 5 +++ lib/l10n/app_localizations_ru.dart | 5 +++ lib/l10n/app_localizations_sk.dart | 5 +++ lib/l10n/app_localizations_sl.dart | 5 +++ lib/l10n/app_localizations_sv.dart | 5 +++ lib/l10n/app_localizations_uk.dart | 5 +++ lib/l10n/app_localizations_zh.dart | 5 +++ lib/screens/channels_screen.dart | 16 +++------ untranslated.json | 58 +++++++++++++++++++++++++++++- 21 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 .local-agent/AGENTS.local.md create mode 100644 .local-agent/memory.local.md diff --git a/.local-agent/AGENTS.local.md b/.local-agent/AGENTS.local.md new file mode 100644 index 0000000..2c61ef8 --- /dev/null +++ b/.local-agent/AGENTS.local.md @@ -0,0 +1,48 @@ +# Local Agent Operating Rules (Untracked) + +This file is NOT version-controlled. +It overrides default agent behavior for this workstation only. + +--- + +## Core Behavior + +- Always search the codebase before editing. +- Produce a short plan before modifying BLE or protocol logic. +- Never modify BLE frame structure or command codes without explicit approval. +- After editing connector code, re-check command/response mappings. +- Never perform destructive operations (delete files, mass refactor) without confirmation. + +--- + +## Protocol Discipline + +- maxFrameSize must remain 172 unless explicitly instructed. +- Identity hash size is 1 byte (PATH_HASH_SIZE). +- Companion radio formats must not change silently. +- Command codes and response codes must remain backward-compatible. + +--- + +## Coding Discipline + +- Keep modifications minimal. +- Prefer refactoring over rewriting. +- Follow existing Flutter patterns (StatelessWidget + Consumer). +- Avoid premature abstraction. +- Explain what changed and why. + +--- + +## Learning Mode + +When discovering: +- a working build command +- a protocol quirk +- a confirmed packet layout rule + +Append a concise bullet to: + +.local-agent/memory.local.md + +Keep memory under 15 bullets max. diff --git a/.local-agent/memory.local.md b/.local-agent/memory.local.md new file mode 100644 index 0000000..a714fbc --- /dev/null +++ b/.local-agent/memory.local.md @@ -0,0 +1,6 @@ +\# Local Learned Patterns (Machine-Specific) + +(empty) + + + diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72e..6af00bc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -363,6 +363,14 @@ } } }, + "channels_channelDeleteFailed": "Failed to delete channel \"{name}\"", + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_channelDeleted": "Channel \"{name}\" deleted", "@channels_channelDeleted": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686ce..8030843 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1570,6 +1570,12 @@ abstract class AppLocalizations { /// **'Delete \"{name}\"? This cannot be undone.'** String channels_deleteChannelConfirm(String name); + /// No description provided for @channels_channelDeleteFailed. + /// + /// In en, this message translates to: + /// **'Failed to delete channel \"{name}\"'** + String channels_channelDeleteFailed(String name); + /// No description provided for @channels_channelDeleted. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b..25b0631 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -812,6 +812,11 @@ class AppLocalizationsBg extends AppLocalizations { return 'Изтрий \"$name\"? Това не може да бъде отменено.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Каналът \"$name\" е изтрит'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4..9ba8409 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -809,6 +809,11 @@ class AppLocalizationsDe extends AppLocalizations { return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanal \"$name\" gelöscht'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4..146edb8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -801,6 +801,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'Delete \"$name\"? This cannot be undone.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Channel \"$name\" deleted'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde365..6c9a517 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -810,6 +810,11 @@ class AppLocalizationsEs extends AppLocalizations { return 'Eliminar \"$name\"? Esto no se puede deshacer.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Canal \"$name\" eliminado'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c4e1e27..3b99772 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -812,6 +812,11 @@ class AppLocalizationsFr extends AppLocalizations { return 'Supprimer $name? Cela ne peut pas être annulé.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Le canal \"$name\" a été supprimé'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f8..7447fad 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -808,6 +808,11 @@ class AppLocalizationsIt extends AppLocalizations { return 'Eliminare \"$name\"? Non può essere annullato.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Canale \"$name\" eliminato'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b..6e4a2af 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -806,6 +806,11 @@ class AppLocalizationsNl extends AppLocalizations { return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanaal \"$name\" verwijderd'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b5..dee97c0 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -811,6 +811,11 @@ class AppLocalizationsPl extends AppLocalizations { return 'Usuń \"$name\"? Nie można tego cofnąć.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanał \"$name\" usunięto'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826..22c5a2e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -811,6 +811,11 @@ class AppLocalizationsPt extends AppLocalizations { return 'Excluir \"$name\"? Não pode ser desfeito.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Canal \"$name\" excluído'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c8..5afbd87 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -809,6 +809,11 @@ class AppLocalizationsRu extends AppLocalizations { return 'Удалить \"$name\"? Это действие нельзя отменить.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Канал \"$name\" удалён'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6de..44f5cbf 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -806,6 +806,11 @@ class AppLocalizationsSk extends AppLocalizations { return 'Odstrániť \"$name\"? To sa nedá zrušiť.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanál \"$name\" bol odstránený'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e3058..455d5aa 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -804,6 +804,11 @@ class AppLocalizationsSl extends AppLocalizations { return 'Izbrišem \"$name\"? To se ne da povrniti.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanal \"$name\" izbrisan.'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b8..6f94870 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -800,6 +800,11 @@ class AppLocalizationsSv extends AppLocalizations { return 'Radera \"$name\"? Detta kan inte ångras.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanalen \"$name\" raderad'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f367002..1d6cf23 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -807,6 +807,11 @@ class AppLocalizationsUk extends AppLocalizations { return 'Видалити $name? Це не можна скасувати.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Канал «$name» видалено'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800..b297297 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -769,6 +769,11 @@ class AppLocalizationsZh extends AppLocalizations { return 'Delete \"$name\"? This cannot be undone.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return '删除频道 \"$name\"'; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 892ac63..d2a2e3d 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1524,18 +1524,14 @@ class _ChannelsScreenState extends State try { await connector.deleteChannel(channel.index); - channelMessageStore.clearChannelMessages( - channel.index, - ); + channelMessageStore.clearChannelMessages(channel.index); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.l10n.channels_channelDeleted( - channel.name, - ), + context.l10n.channels_channelDeleted(channel.name), ), ), ); @@ -1545,17 +1541,13 @@ class _ChannelsScreenState extends State ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.l10n.channels_channelDeleteFailed( - channel.name, - ), + context.l10n.channels_channelDeleteFailed(channel.name), ), ), ); // Preserve existing logging (if it was there) - debugPrint( - 'Failed to delete channel: $e\n$st', - ); + debugPrint('Failed to delete channel: $e\n$st'); } }, child: Text( diff --git a/untranslated.json b/untranslated.json index 9e26dfe..d68e753 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,57 @@ -{} \ No newline at end of file +{ + "bg": [ + "channels_channelDeleteFailed" + ], + + "de": [ + "channels_channelDeleteFailed" + ], + + "es": [ + "channels_channelDeleteFailed" + ], + + "fr": [ + "channels_channelDeleteFailed" + ], + + "it": [ + "channels_channelDeleteFailed" + ], + + "nl": [ + "channels_channelDeleteFailed" + ], + + "pl": [ + "channels_channelDeleteFailed" + ], + + "pt": [ + "channels_channelDeleteFailed" + ], + + "ru": [ + "channels_channelDeleteFailed" + ], + + "sk": [ + "channels_channelDeleteFailed" + ], + + "sl": [ + "channels_channelDeleteFailed" + ], + + "sv": [ + "channels_channelDeleteFailed" + ], + + "uk": [ + "channels_channelDeleteFailed" + ], + + "zh": [ + "channels_channelDeleteFailed" + ] +} From ab76a52d71cf1d476b43cab72b5a5b05522f2a86 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:07:19 -0500 Subject: [PATCH 154/421] Delete .local-agent/AGENTS.local.md --- .local-agent/AGENTS.local.md | 48 ------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 .local-agent/AGENTS.local.md diff --git a/.local-agent/AGENTS.local.md b/.local-agent/AGENTS.local.md deleted file mode 100644 index 2c61ef8..0000000 --- a/.local-agent/AGENTS.local.md +++ /dev/null @@ -1,48 +0,0 @@ -# Local Agent Operating Rules (Untracked) - -This file is NOT version-controlled. -It overrides default agent behavior for this workstation only. - ---- - -## Core Behavior - -- Always search the codebase before editing. -- Produce a short plan before modifying BLE or protocol logic. -- Never modify BLE frame structure or command codes without explicit approval. -- After editing connector code, re-check command/response mappings. -- Never perform destructive operations (delete files, mass refactor) without confirmation. - ---- - -## Protocol Discipline - -- maxFrameSize must remain 172 unless explicitly instructed. -- Identity hash size is 1 byte (PATH_HASH_SIZE). -- Companion radio formats must not change silently. -- Command codes and response codes must remain backward-compatible. - ---- - -## Coding Discipline - -- Keep modifications minimal. -- Prefer refactoring over rewriting. -- Follow existing Flutter patterns (StatelessWidget + Consumer). -- Avoid premature abstraction. -- Explain what changed and why. - ---- - -## Learning Mode - -When discovering: -- a working build command -- a protocol quirk -- a confirmed packet layout rule - -Append a concise bullet to: - -.local-agent/memory.local.md - -Keep memory under 15 bullets max. From f4dd76a4591b4062930cdcd3bee654a4f1d017bc Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:07:32 -0500 Subject: [PATCH 155/421] Delete .local-agent/memory.local.md --- .local-agent/memory.local.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .local-agent/memory.local.md diff --git a/.local-agent/memory.local.md b/.local-agent/memory.local.md deleted file mode 100644 index a714fbc..0000000 --- a/.local-agent/memory.local.md +++ /dev/null @@ -1,6 +0,0 @@ -\# Local Learned Patterns (Machine-Specific) - -(empty) - - - From 47044ae14e9f8ae2f35edc087d84f866d9ae0858 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:37:10 -0500 Subject: [PATCH 156/421] fix(l10n): add channels_channelDeleteFailed with proper placeholder typing and translations --- lib/l10n/app_bg.arb | 4 ++- lib/l10n/app_de.arb | 4 ++- lib/l10n/app_es.arb | 4 ++- lib/l10n/app_fr.arb | 4 ++- lib/l10n/app_it.arb | 4 ++- lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/l10n/app_nl.arb | 4 ++- lib/l10n/app_pl.arb | 4 ++- lib/l10n/app_pt.arb | 4 ++- lib/l10n/app_ru.arb | 4 ++- lib/l10n/app_sk.arb | 4 ++- lib/l10n/app_sl.arb | 4 ++- lib/l10n/app_sv.arb | 4 ++- lib/l10n/app_uk.arb | 4 ++- lib/l10n/app_zh.arb | 4 ++- pubspec.lock | 20 +++++------ untranslated.json | 58 +----------------------------- 30 files changed, 67 insertions(+), 95 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023..6725c24 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "bg", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f7..dbd0606 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "de", "appTitle": "MeshCore Open", "nav_contacts": "Kontakte", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d3..52e7c25 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "es", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2d4846c..0e96ba8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "fr", "appTitle": "MeshCore Open", "nav_contacts": "Contacts", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186..421fcb0 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "it", "appTitle": "MeshCore Open", "nav_contacts": "Contatti", diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 25b0631..3876b3c 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -814,7 +814,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Неуспешно изтриване на канала \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9ba8409..96cfee1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -811,7 +811,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kanal $name konnte nicht gelöscht werden'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6c9a517..d0e5028 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -812,7 +812,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'No se pudo eliminar el canal \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3b99772..6ad0335 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -814,7 +814,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Échec de la suppression de la chaîne \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 7447fad..b53acb5 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -810,7 +810,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Impossibile eliminare il canale \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6e4a2af..fa684e2 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -808,7 +808,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kan kanaal $name niet verwijderen'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index dee97c0..b62d4b6 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -813,7 +813,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Nie udało się usunąć kanału \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 22c5a2e..34713e6 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -813,7 +813,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Falha ao excluir o canal \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5afbd87..f0de018 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -811,7 +811,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Не удалось удалить канал $name.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 44f5cbf..b826056 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -808,7 +808,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kanál \"$name\" sa nepodarilo odstrániť'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 455d5aa..5f2d1a4 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -806,7 +806,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kanala $name ni bilo mogoče izbrisati'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6f94870..6cf18b3 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -802,7 +802,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Det gick inte att ta bort kanalen \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1d6cf23..7869afa 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -809,7 +809,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Не вдалося видалити канал \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b297297..ac61263 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -771,7 +771,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return '无法删除频道 \"$name\"'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdd..f158646 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "nl", "appTitle": "MeshCore Open", "nav_contacts": "Contacten", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa7..2eb67dd 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pl", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be6694..4b947ad 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pt", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3..f9ab2d3 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "ru", "appTitle": "MeshCore Open", "nav_contacts": "Контакты", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0a..e9653ec 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sk", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d..384f669 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sl", "appTitle": "MeshCore Open", "nav_contacts": "Stiki", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399..39422fc 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sv", "appTitle": "MeshCore Open", "nav_contacts": "Kontakter", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0..bb0dd92 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "uk", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7..6bbfe48 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "zh", "appTitle": "MeshCore Open", "nav_contacts": "联系方式", diff --git a/pubspec.lock b/pubspec.lock index ed84c40..09e9301 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -497,26 +497,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" timezone: dependency: transitive description: diff --git a/untranslated.json b/untranslated.json index d68e753..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,57 +1 @@ -{ - "bg": [ - "channels_channelDeleteFailed" - ], - - "de": [ - "channels_channelDeleteFailed" - ], - - "es": [ - "channels_channelDeleteFailed" - ], - - "fr": [ - "channels_channelDeleteFailed" - ], - - "it": [ - "channels_channelDeleteFailed" - ], - - "nl": [ - "channels_channelDeleteFailed" - ], - - "pl": [ - "channels_channelDeleteFailed" - ], - - "pt": [ - "channels_channelDeleteFailed" - ], - - "ru": [ - "channels_channelDeleteFailed" - ], - - "sk": [ - "channels_channelDeleteFailed" - ], - - "sl": [ - "channels_channelDeleteFailed" - ], - - "sv": [ - "channels_channelDeleteFailed" - ], - - "uk": [ - "channels_channelDeleteFailed" - ], - - "zh": [ - "channels_channelDeleteFailed" - ] -} +{} \ No newline at end of file From f3db63ceeaf590c8042a75999356b6a5be51c1af Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:37:58 -0500 Subject: [PATCH 157/421] Delete pubspec.lock --- pubspec.lock | 1095 -------------------------------------------------- 1 file changed, 1095 deletions(-) delete mode 100644 pubspec.lock diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 09e9301..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,1095 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - bluez: - dependency: transitive - description: - name: bluez - sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" - url: "https://pub.dev" - source: hosted - version: "0.8.3" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - characters: - dependency: "direct main" - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" - source: hosted - version: "0.3.5+2" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - dart_earcut: - dependency: transitive - description: - name: dart_earcut - sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dart_polylabel2: - dependency: transitive - description: - name: dart_polylabel2 - sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - dbus: - dependency: transitive - description: - name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.dev" - source: hosted - version: "0.7.12" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_blue_plus: - dependency: "direct main" - description: - name: flutter_blue_plus - sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - flutter_blue_plus_android: - dependency: transitive - description: - name: flutter_blue_plus_android - sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_darwin: - dependency: transitive - description: - name: flutter_blue_plus_darwin - sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" - url: "https://pub.dev" - source: hosted - version: "8.1.1" - flutter_blue_plus_linux: - dependency: transitive - description: - name: flutter_blue_plus_linux - sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242 - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_platform_interface: - dependency: transitive - description: - name: flutter_blue_plus_platform_interface - sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_web: - dependency: transitive - description: - name: flutter_blue_plus_web - sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_winrt: - dependency: transitive - description: - name: flutter_blue_plus_winrt - sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 - url: "https://pub.dev" - source: hosted - version: "0.0.18" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - flutter_foreground_task: - dependency: "direct main" - description: - name: flutter_foreground_task - sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" - url: "https://pub.dev" - source: hosted - version: "9.2.0" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" - source: hosted - version: "0.14.4" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" - url: "https://pub.dev" - source: hosted - version: "20.1.0" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 - url: "https://pub.dev" - source: hosted - version: "7.0.0" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - flutter_local_notifications_windows: - dependency: transitive - description: - name: flutter_local_notifications_windows - sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a - url: "https://pub.dev" - source: hosted - version: "2.0.1" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_map: - dependency: "direct main" - description: - name: flutter_map - sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" - url: "https://pub.dev" - source: hosted - version: "8.2.2" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - gpx: - dependency: "direct main" - description: - name: gpx - sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e - url: "https://pub.dev" - source: hosted - version: "2.3.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" - url: "https://pub.dev" - source: hosted - version: "4.7.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" - url: "https://pub.dev" - source: hosted - version: "4.10.0" - latlong2: - dependency: "direct main" - description: - name: latlong2 - sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" - url: "https://pub.dev" - source: hosted - version: "0.9.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - linkify: - dependency: transitive - description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - lists: - dependency: transitive - description: - name: lists - sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - logger: - dependency: transitive - description: - name: logger - sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 - url: "https://pub.dev" - source: hosted - version: "2.6.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mgrs_dart: - dependency: transitive - description: - name: mgrs_dart - sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 - url: "https://pub.dev" - source: hosted - version: "7.1.4" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.dev" - source: hosted - version: "9.0.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" - url: "https://pub.dev" - source: hosted - version: "7.0.1" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pointycastle: - dependency: "direct main" - description: - name: pointycastle - sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - posix: - dependency: transitive - description: - name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.dev" - source: hosted - version: "6.0.3" - proj4dart: - dependency: transitive - description: - name: proj4dart - sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e - url: "https://pub.dev" - source: hosted - version: "2.1.0" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - qr: - dependency: transitive - description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - qr_flutter: - dependency: "direct main" - description: - name: qr_flutter - sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.dev" - source: hosted - version: "12.0.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f - url: "https://pub.dev" - source: hosted - version: "2.4.20" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 - url: "https://pub.dev" - source: hosted - version: "2.4.2+2" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - timezone: - dependency: transitive - description: - name: timezone - sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - unicode: - dependency: transitive - description: - name: unicode - sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.dev" - source: hosted - version: "6.3.28" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.dev" - source: hosted - version: "2.4.2" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - wakelock_plus: - dependency: "direct main" - description: - name: wakelock_plus - sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - wkt_parser: - dependency: transitive - description: - name: wkt_parser - sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" From 298951f8bc39951f7fe6ed612ca7bb356111429b Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:43:37 -0500 Subject: [PATCH 158/421] bring up to current main --- pubspec.lock | 1095 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1095 insertions(+) create mode 100644 pubspec.lock diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..492eb4d --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1095 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: "direct main" + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" + url: "https://pub.dev" + source: hosted + version: "8.1.1" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242 + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_platform_interface: + dependency: transitive + description: + name: flutter_blue_plus_platform_interface + sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_winrt: + dependency: transitive + description: + name: flutter_blue_plus_winrt + sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 + url: "https://pub.dev" + source: hosted + version: "0.0.18" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" + url: "https://pub.dev" + source: hosted + version: "9.2.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" + url: "https://pub.dev" + source: hosted + version: "20.1.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + gpx: + dependency: "direct main" + description: + name: gpx + sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e + url: "https://pub.dev" + source: hosted + version: "2.3.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + url: "https://pub.dev" + source: hosted + version: "4.7.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.dev" + source: hosted + version: "2.6.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 + url: "https://pub.dev" + source: hosted + version: "7.1.4" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: "direct main" + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + url: "https://pub.dev" + source: hosted + version: "2.4.20" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + timezone: + dependency: transitive + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" \ No newline at end of file From 0f2d18d6fa797f7d7ed9bb05af4fd3a0f67cc810 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:39:52 -0500 Subject: [PATCH 159/421] feat: add custom los icon --- lib/icons/los_icon.dart | 26 ++++++++++ lib/screens/line_of_sight_map_screen.dart | 3 +- lib/screens/map_screen.dart | 3 +- pubspec.lock | 60 +++++++++++++++++++---- pubspec.yaml | 1 + 5 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 lib/icons/los_icon.dart diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart new file mode 100644 index 0000000..309cc7d --- /dev/null +++ b/lib/icons/los_icon.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class LosIcon extends StatelessWidget { + final double size; + final Color? color; + + const LosIcon({super.key, this.size = 24, this.color}); + + @override + Widget build(BuildContext context) { + final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; + return SvgPicture.string( + _losSvg, + width: size, + height: size, + colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), + ); + } +} + +const String _losSvg = ''' + + + +'''; diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index b073685..dfda1c1 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -16,6 +16,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; class LineOfSightEndpoint { final String label; @@ -642,7 +643,7 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _loading ? null : _runLos, - icon: const Icon(Icons.visibility), + icon: const LosIcon(), label: Text(context.l10n.losRun), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 77ec98c..b688a30 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -20,6 +20,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; @@ -280,7 +281,7 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) IconButton( - icon: const Icon(Icons.visibility), + icon: const LosIcon(), onPressed: () { final candidates = []; if (connector.selfLatitude != null && diff --git a/pubspec.lock b/pubspec.lock index ed84c40..d9d480f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -497,26 +505,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -910,10 +926,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" timezone: dependency: transitive description: @@ -1010,6 +1026,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3624b93..54e3648 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + flutter_svg: ^2.0.10 dev_dependencies: flutter_test: From 08edd2696ee33a8376d2fb244d3bc6cf0ca6e560 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:47:49 -0500 Subject: [PATCH 160/421] Revert "feat: add custom los icon" This reverts commit 0f2d18d6fa797f7d7ed9bb05af4fd3a0f67cc810. --- lib/icons/los_icon.dart | 26 ---------- lib/screens/line_of_sight_map_screen.dart | 3 +- lib/screens/map_screen.dart | 3 +- pubspec.lock | 60 ++++------------------- pubspec.yaml | 1 - 5 files changed, 12 insertions(+), 81 deletions(-) delete mode 100644 lib/icons/los_icon.dart diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart deleted file mode 100644 index 309cc7d..0000000 --- a/lib/icons/los_icon.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class LosIcon extends StatelessWidget { - final double size; - final Color? color; - - const LosIcon({super.key, this.size = 24, this.color}); - - @override - Widget build(BuildContext context) { - final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; - return SvgPicture.string( - _losSvg, - width: size, - height: size, - colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), - ); - } -} - -const String _losSvg = ''' - - - -'''; diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index dfda1c1..b073685 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -16,7 +16,6 @@ import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; -import '../icons/los_icon.dart'; class LineOfSightEndpoint { final String label; @@ -643,7 +642,7 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _loading ? null : _runLos, - icon: const LosIcon(), + icon: const Icon(Icons.visibility), label: Text(context.l10n.losRun), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index b688a30..77ec98c 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -20,7 +20,6 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; -import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; @@ -281,7 +280,7 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) IconButton( - icon: const LosIcon(), + icon: const Icon(Icons.visibility), onPressed: () { final candidates = []; if (connector.selfLatitude != null && diff --git a/pubspec.lock b/pubspec.lock index d9d480f..ed84c40 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -347,14 +347,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -505,26 +497,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mgrs_dart: dependency: transitive description: @@ -605,14 +597,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" path_provider: dependency: "direct main" description: @@ -926,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: @@ -1026,30 +1010,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 54e3648..3624b93 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,6 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 - flutter_svg: ^2.0.10 dev_dependencies: flutter_test: From aaf79c90c91a987527d811a6dfe22142c3d07724 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:01:13 -0500 Subject: [PATCH 161/421] feat: show los elevation icon --- lib/icons/los_icon.dart | 80 +++++++++++++++++++++++ lib/screens/line_of_sight_map_screen.dart | 3 +- lib/screens/map_screen.dart | 3 +- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 lib/icons/los_icon.dart diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart new file mode 100644 index 0000000..86bef06 --- /dev/null +++ b/lib/icons/los_icon.dart @@ -0,0 +1,80 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class LosIcon extends StatelessWidget { + final double size; + final Color? color; + + const LosIcon({ + super.key, + this.size = 24, + this.color, + }); + + @override + Widget build(BuildContext context) { + final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; + final canvasSize = size; + + return SizedBox( + width: canvasSize, + height: canvasSize, + child: CustomPaint( + painter: _LosIconPainter(iconColor), + ), + ); + } +} + +class _LosIconPainter extends CustomPainter { + final Color color; + + _LosIconPainter(this.color); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final path = Path() + ..moveTo(82, -120) + ..relativeLineTo(258, -360) + ..relativeLineTo(202, 0) + ..relativeLineTo(298, -348) + ..relativeLineTo(0, 708) + ..lineTo(82, -120) + ..close() + ..moveTo(152, -353) + ..relativeLineTo(-64, -46) + ..relativeLineTo(172, -241) + ..relativeLineTo(202, 0) + ..relativeLineTo(188, -219) + ..relativeLineTo(60, 52) + ..relativeLineTo(-212, 247) + ..lineTo(300, -560) + ..lineTo(152, -353) + ..close() + ..moveTo(238, -200) + ..relativeLineTo(522, 0) + ..relativeLineTo(0, -412) + ..lineTo(578, -400) + ..lineTo(380, -400) + ..lineTo(238, -200) + ..close(); + + final scale = math.min(size.width, size.height) / 960; + + canvas.save(); + canvas.translate(0, 960); + canvas.scale(scale, scale); + canvas.drawPath(path, paint); + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _LosIconPainter oldDelegate) { + return oldDelegate.color != color; + } +} diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index b073685..dfda1c1 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -16,6 +16,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; class LineOfSightEndpoint { final String label; @@ -642,7 +643,7 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _loading ? null : _runLos, - icon: const Icon(Icons.visibility), + icon: const LosIcon(), label: Text(context.l10n.losRun), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 77ec98c..b688a30 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -20,6 +20,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; @@ -280,7 +281,7 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) IconButton( - icon: const Icon(Icons.visibility), + icon: const LosIcon(), onPressed: () { final candidates = []; if (connector.selfLatitude != null && From 9bcb8b9ca67d6478dff1ca3fcf3b6481bca5d1dd Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:36:49 -0500 Subject: [PATCH 162/421] feat: render los elevation via svg --- assets/icons/los_elevation.svg | 5 +++ lib/icons/los_icon.dart | 68 +++------------------------------- pubspec.lock | 68 +++++++++++++++++++++++++++------- pubspec.yaml | 2 + 4 files changed, 67 insertions(+), 76 deletions(-) create mode 100644 assets/icons/los_elevation.svg diff --git a/assets/icons/los_elevation.svg b/assets/icons/los_elevation.svg new file mode 100644 index 0000000..78c7a1b --- /dev/null +++ b/assets/icons/los_elevation.svg @@ -0,0 +1,5 @@ + + + diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart index 86bef06..fef6a45 100644 --- a/lib/icons/los_icon.dart +++ b/lib/icons/los_icon.dart @@ -1,6 +1,5 @@ -import 'dart:math' as math; - import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class LosIcon extends StatelessWidget { final double size; @@ -15,66 +14,11 @@ class LosIcon extends StatelessWidget { @override Widget build(BuildContext context) { final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; - final canvasSize = size; - - return SizedBox( - width: canvasSize, - height: canvasSize, - child: CustomPaint( - painter: _LosIconPainter(iconColor), - ), + return SvgPicture.asset( + 'assets/icons/los_elevation.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), ); } } - -class _LosIconPainter extends CustomPainter { - final Color color; - - _LosIconPainter(this.color); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - - final path = Path() - ..moveTo(82, -120) - ..relativeLineTo(258, -360) - ..relativeLineTo(202, 0) - ..relativeLineTo(298, -348) - ..relativeLineTo(0, 708) - ..lineTo(82, -120) - ..close() - ..moveTo(152, -353) - ..relativeLineTo(-64, -46) - ..relativeLineTo(172, -241) - ..relativeLineTo(202, 0) - ..relativeLineTo(188, -219) - ..relativeLineTo(60, 52) - ..relativeLineTo(-212, 247) - ..lineTo(300, -560) - ..lineTo(152, -353) - ..close() - ..moveTo(238, -200) - ..relativeLineTo(522, 0) - ..relativeLineTo(0, -412) - ..lineTo(578, -400) - ..lineTo(380, -400) - ..lineTo(238, -200) - ..close(); - - final scale = math.min(size.width, size.height) / 960; - - canvas.save(); - canvas.translate(0, 960); - canvas.scale(scale, scale); - canvas.drawPath(path, paint); - canvas.restore(); - } - - @override - bool shouldRepaint(covariant _LosIconPainter oldDelegate) { - return oldDelegate.color != color; - } -} diff --git a/pubspec.lock b/pubspec.lock index ed84c40..266bfb7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -401,10 +409,10 @@ packages: dependency: transitive description: name: image - sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.7.2" + version: "4.8.0" intl: dependency: "direct main" description: @@ -417,10 +425,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.11.0" latlong2: dependency: "direct main" description: @@ -537,10 +545,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 + sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce url: "https://pub.dev" source: hosted - version: "7.1.4" + version: "7.2.0" native_toolchain_c: dependency: transitive description: @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -649,10 +665,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -681,10 +697,10 @@ packages: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" proj4dart: dependency: transitive description: @@ -1006,10 +1022,34 @@ packages: dependency: "direct main" description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3624b93..3dc40d5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + flutter_svg: ^2.0.10 dev_dependencies: flutter_test: @@ -87,6 +88,7 @@ flutter: assets: - assets/images/ + - assets/icons/los_elevation.svg flutter_launcher_icons: android: true From bd27c90216c70eadec03e375d3de9aeb60537626 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:42:36 -0500 Subject: [PATCH 163/421] feat: render los elevation via material symbol --- assets/icons/los_elevation.svg | 5 ---- lib/icons/los_icon.dart | 19 +++++++++----- pubspec.lock | 48 ++++++---------------------------- pubspec.yaml | 3 +-- 4 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 assets/icons/los_elevation.svg diff --git a/assets/icons/los_elevation.svg b/assets/icons/los_elevation.svg deleted file mode 100644 index 78c7a1b..0000000 --- a/assets/icons/los_elevation.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart index fef6a45..43c6cdc 100644 --- a/lib/icons/los_icon.dart +++ b/lib/icons/los_icon.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; class LosIcon extends StatelessWidget { final double size; @@ -13,12 +13,17 @@ class LosIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; - return SvgPicture.asset( - 'assets/icons/los_elevation.svg', - width: size, - height: size, - colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), + final theme = Theme.of(context); + final iconTheme = IconTheme.of(context); + final iconColor = color ?? + iconTheme.color ?? + theme.iconTheme.color ?? + theme.colorScheme.onSurface; + + return Icon( + Symbols.elevation, + size: size, + color: iconColor, ); } } diff --git a/pubspec.lock b/pubspec.lock index 266bfb7..9605e96 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,14 +347,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -517,6 +509,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 + url: "https://pub.dev" + source: hosted + version: "4.2906.0" meta: dependency: transitive description: @@ -605,14 +605,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1026,30 +1018,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.3" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3dc40d5..7d6b5c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 - flutter_svg: ^2.0.10 + material_symbols_icons: ^4.2906.0 dev_dependencies: flutter_test: @@ -88,7 +88,6 @@ flutter: assets: - assets/images/ - - assets/icons/los_elevation.svg flutter_launcher_icons: android: true From 1f816f7e087d5c85be2c763d22bf19a4a11951ab Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:06:25 -0500 Subject: [PATCH 164/421] ran dart format . on libs/icons/los_icon.dart --- lib/icons/los_icon.dart | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart index 43c6cdc..58d75b0 100644 --- a/lib/icons/los_icon.dart +++ b/lib/icons/los_icon.dart @@ -5,25 +5,18 @@ class LosIcon extends StatelessWidget { final double size; final Color? color; - const LosIcon({ - super.key, - this.size = 24, - this.color, - }); + const LosIcon({super.key, this.size = 24, this.color}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final iconTheme = IconTheme.of(context); - final iconColor = color ?? + final iconColor = + color ?? iconTheme.color ?? theme.iconTheme.color ?? theme.colorScheme.onSurface; - return Icon( - Symbols.elevation, - size: size, - color: iconColor, - ); + return Icon(Symbols.elevation, size: size, color: iconColor); } } From 2bdd9d35cc8a1eee151bb2d353df845087c72841 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:53:41 -0500 Subject: [PATCH 165/421] feat: show radio horizon on los profile --- lib/screens/line_of_sight_map_screen.dart | 154 +++++++++++++++++++++- lib/services/line_of_sight_service.dart | 4 + 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index b073685..101f013 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -943,7 +943,10 @@ class _LosProfilePainter extends CustomPainter { terrainPath.lineTo(size.width, size.height); terrainPath.close(); - canvas.drawPath(terrainPath, Paint()..color = const Color(0xCC7C6F5D)); + const terrainFillColor = Color(0xCC7C6F5D); + const terrainLineColor = Color(0xFF9FE870); + const losLineColor = Color(0xFFE0E7FF); + canvas.drawPath(terrainPath, Paint()..color = terrainFillColor); final terrainLine = ui.Path(); for (int i = 0; i < samples.length; i++) { @@ -957,7 +960,7 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( terrainLine, Paint() - ..color = const Color(0xFF9FE870) + ..color = terrainLineColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); @@ -977,10 +980,64 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( losLine, Paint() - ..color = const Color(0xFFE0E7FF) + ..color = losLineColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); + + final horizonLine = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].radioHorizonMeters, + ); + if (i == 0) { + horizonLine.moveTo(p.dx, p.dy); + } else { + horizonLine.lineTo(p.dx, p.dy); + } + } + const horizonLineColor = Color(0xFF4BC0FF); + final horizonPaint = Paint() + ..color = horizonLineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + canvas.drawPath(horizonLine, horizonPaint); + + final capPath = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].radioHorizonMeters, + ); + if (i == 0) { + capPath.moveTo(p.dx, p.dy); + } else { + capPath.lineTo(p.dx, p.dy); + } + } + for (int i = samples.length - 1; i >= 0; i--) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].lineHeightMeters, + ); + capPath.lineTo(p.dx, p.dy); + } + capPath.close(); + const horizonFillColor = Color(0x404BC0FF); + canvas.drawPath( + capPath, + Paint() + ..color = horizonFillColor + ..style = PaintingStyle.fill, + ); + + _drawLegend( + canvas, + horizonLineColor, + losLineColor, + terrainLineColor, + ); } @override @@ -1000,4 +1057,95 @@ class _LosProfilePainter extends CustomPainter { ..layout(); painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); } + + void _drawLegend( + Canvas canvas, + Color horizonColor, + Color losColor, + Color terrainColor, + ) { + const legendX = 8.0; + const legendY = 8.0; + const swatchSize = 10.0; + const swatchTextGap = 6.0; + const entrySpacing = 4.0; + const legendPadding = 6.0; + + final entries = [ + _LegendEntry('Terrain', terrainColor), + _LegendEntry('LOS beam', losColor), + _LegendEntry('Radio horizon', horizonColor), + ]; + + final textStyle = badgeTextStyle.copyWith( + fontSize: 10, + fontWeight: FontWeight.w500, + ); + + final painters = entries.map((entry) { + final painter = TextPainter( + text: TextSpan(text: entry.label, style: textStyle), + textDirection: TextDirection.ltr, + )..layout(); + return painter; + }).toList(); + + final maxTextWidth = painters.map((p) => p.width).fold( + 0, + math.max, + ); + + final legendWidth = + legendPadding * 2 + swatchSize + swatchTextGap + maxTextWidth; + + final legendHeight = legendPadding * 2 + + entries.length * swatchSize + + (entries.length - 1) * entrySpacing; + + final legendRect = RRect.fromLTRBR( + legendX, + legendY, + legendX + legendWidth, + legendY + legendHeight, + const Radius.circular(10), + ); + + canvas.drawRRect( + legendRect, + Paint()..color = const Color.fromARGB(90, 0, 0, 0), + ); + + var yOffset = legendY + legendPadding; + for (int i = 0; i < entries.length; i++) { + final entry = entries[i]; + final painter = painters[i]; + final swatchRect = Rect.fromLTWH( + legendX + legendPadding, + yOffset, + swatchSize, + swatchSize, + ); + canvas.drawRect( + swatchRect, + Paint()..color = entry.color, + ); + + painter.paint( + canvas, + Offset( + swatchRect.right + swatchTextGap, + yOffset + (swatchSize - painter.height) / 2, + ), + ); + + yOffset += swatchSize + entrySpacing; + } + } +} + +class _LegendEntry { + final String label; + final Color color; + + const _LegendEntry(this.label, this.color); } diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index e9f9f7b..b73ab51 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -12,12 +12,14 @@ class LineOfSightSample { final double distanceMeters; final double terrainMeters; final double lineHeightMeters; + final double radioHorizonMeters; final double clearanceMeters; const LineOfSightSample({ required this.distanceMeters, required this.terrainMeters, required this.lineHeightMeters, + required this.radioHorizonMeters, required this.clearanceMeters, }); } @@ -238,6 +240,7 @@ class LineOfSightService { (2 * effectiveEarthRadius); final terrainHeight = elevations[i] + earthBulge; final clearance = lineHeight - terrainHeight; + final radioHorizonHeight = lineHeight - earthBulge; if (clearance < -obstructionToleranceMeters) { isClear = false; @@ -253,6 +256,7 @@ class LineOfSightService { distanceMeters: distanceFromStart, terrainMeters: terrainHeight, lineHeightMeters: lineHeight, + radioHorizonMeters: radioHorizonHeight, clearanceMeters: clearance, ), ); From fc55fb98ce8211166bf3e44a4285b884f43e22e8 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:42:58 -0500 Subject: [PATCH 166/421] Document LOS frequency and k-factor math Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact --- lib/screens/line_of_sight_map_screen.dart | 152 ++++++++++++++---- lib/services/line_of_sight_service.dart | 47 +++++- test/services/line_of_sight_service_test.dart | 2 + 3 files changed, 166 insertions(+), 35 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 101f013..5eb532b 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -14,6 +14,7 @@ import '../services/app_settings_service.dart'; import '../services/line_of_sight_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; +import '../connector/meshcore_connector.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; @@ -110,10 +111,13 @@ class _LineOfSightMapScreenState extends State { }); try { + final connector = context.read(); + final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz); final result = await _lineOfSightService.analyzePath( [start.point, end.point], startAntennaHeightMeters: startAntenna, endAntennaHeightMeters: endAntenna, + frequencyMHz: frequencyMHz, ); if (!mounted) return; if (!_isRunRequestCurrent( @@ -424,6 +428,12 @@ class _LineOfSightMapScreenState extends State { Widget _buildControlPanel(bool isImperial) { _sanitizeSelection(); final segment = _primarySegmentResult(); + final connector = context.watch(); + final reportedFrequencyMHz = _normalizeFrequencyMHz( + connector.currentFreqHz, + ); + final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz; + final kFactorUsed = segment?.usedKFactor; final endpoints = _visibleEndpoints(); final distanceUnit = isImperial ? 'mi' : 'km'; final heightUnit = isImperial ? 'ft' : 'm'; @@ -488,6 +498,52 @@ class _LineOfSightMapScreenState extends State { ), ), const SizedBox(height: 4), + if (displayFrequencyMHz != null) + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 4), + child: Row( + children: [ + Text( + 'Frequency', + style: TextStyle( + fontSize: 11, + color: Colors.grey[700], + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Text( + '${displayFrequencyMHz.toStringAsFixed(3)} MHz', + style: TextStyle(fontSize: 11, color: Colors.grey[700]), + ), + if (kFactorUsed != null) ...[ + const SizedBox(width: 8), + Text( + 'k=${kFactorUsed.toStringAsFixed(3)}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[700], + ), + ), + const SizedBox(width: 4), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.info_outline, size: 16), + color: Colors.grey[600], + tooltip: 'View calculation details', + onPressed: () { + _showFrequencyInfoDialog( + context, + displayFrequencyMHz, + kFactorUsed, + ); + }, + ), + ], + ], + ), + ), Text( context.l10n.losElevationAttribution, style: TextStyle(fontSize: 10, color: Colors.grey[700]), @@ -896,6 +952,56 @@ class _LineOfSightMapScreenState extends State { break; } } + + void _showFrequencyInfoDialog( + BuildContext context, + double frequencyMHz, + double kFactor, + ) { + final baselineFreq = LineOfSightService.baselineFrequencyMHz; + final baselineK = LineOfSightService.baselineKFactor; + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Radio horizon calculation'), + content: Text.rich( + TextSpan( + children: [ + TextSpan( + text: + 'Starting from k=$baselineK at ${baselineFreq.toStringAsFixed(3)} MHz, ', + ), + const TextSpan(text: 'the calculation multiplies the offset by '), + TextSpan( + text: + '0.15 × (frequency − ${baselineFreq.toStringAsFixed(3)}) / ${baselineFreq.toStringAsFixed(3)} ', + ), + TextSpan( + text: + 'to get k ≈ ${kFactor.toStringAsFixed(3)} for the current ${frequencyMHz.toStringAsFixed(3)} MHz band, ', + ), + const TextSpan( + text: 'which defines the curved radio horizon cap.', + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_ok), + ), + ], + ), + ); + } + + double? _normalizeFrequencyMHz(int? frequencyHz) { + if (frequencyHz == null || frequencyHz <= 0) return null; + if (frequencyHz >= 1000000) return frequencyHz / 1e6; + if (frequencyHz >= 1000) return frequencyHz / 1e3; + return frequencyHz.toDouble(); + } } class _LosProfilePainter extends CustomPainter { @@ -985,30 +1091,32 @@ class _LosProfilePainter extends CustomPainter { ..strokeWidth = 2, ); - final horizonLine = ui.Path(); + const refractedLineColor = Color(0xFFFFD57F); + final refractedLine = ui.Path(); for (int i = 0; i < samples.length; i++) { final p = mapPoint( samples[i].distanceMeters, - samples[i].radioHorizonMeters, + samples[i].refractedHeightMeters, ); if (i == 0) { - horizonLine.moveTo(p.dx, p.dy); + refractedLine.moveTo(p.dx, p.dy); } else { - horizonLine.lineTo(p.dx, p.dy); + refractedLine.lineTo(p.dx, p.dy); } } - const horizonLineColor = Color(0xFF4BC0FF); - final horizonPaint = Paint() - ..color = horizonLineColor - ..style = PaintingStyle.stroke - ..strokeWidth = 1.5; - canvas.drawPath(horizonLine, horizonPaint); + canvas.drawPath( + refractedLine, + Paint() + ..color = refractedLineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); final capPath = ui.Path(); for (int i = 0; i < samples.length; i++) { final p = mapPoint( samples[i].distanceMeters, - samples[i].radioHorizonMeters, + samples[i].refractedHeightMeters, ); if (i == 0) { capPath.moveTo(p.dx, p.dy); @@ -1024,7 +1132,7 @@ class _LosProfilePainter extends CustomPainter { capPath.lineTo(p.dx, p.dy); } capPath.close(); - const horizonFillColor = Color(0x404BC0FF); + const horizonFillColor = Color(0x40FFD57F); canvas.drawPath( capPath, Paint() @@ -1032,12 +1140,7 @@ class _LosProfilePainter extends CustomPainter { ..style = PaintingStyle.fill, ); - _drawLegend( - canvas, - horizonLineColor, - losLineColor, - terrainLineColor, - ); + _drawLegend(canvas, refractedLineColor, losLineColor, terrainLineColor); } @override @@ -1090,15 +1193,13 @@ class _LosProfilePainter extends CustomPainter { return painter; }).toList(); - final maxTextWidth = painters.map((p) => p.width).fold( - 0, - math.max, - ); + final maxTextWidth = painters.map((p) => p.width).fold(0, math.max); final legendWidth = legendPadding * 2 + swatchSize + swatchTextGap + maxTextWidth; - final legendHeight = legendPadding * 2 + + final legendHeight = + legendPadding * 2 + entries.length * swatchSize + (entries.length - 1) * entrySpacing; @@ -1125,10 +1226,7 @@ class _LosProfilePainter extends CustomPainter { swatchSize, swatchSize, ); - canvas.drawRect( - swatchRect, - Paint()..color = entry.color, - ); + canvas.drawRect(swatchRect, Paint()..color = entry.color); painter.paint( canvas, diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index b73ab51..14d8fc6 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -12,14 +12,14 @@ class LineOfSightSample { final double distanceMeters; final double terrainMeters; final double lineHeightMeters; - final double radioHorizonMeters; + final double refractedHeightMeters; final double clearanceMeters; const LineOfSightSample({ required this.distanceMeters, required this.terrainMeters, required this.lineHeightMeters, - required this.radioHorizonMeters, + required this.refractedHeightMeters, required this.clearanceMeters, }); } @@ -32,6 +32,8 @@ class LineOfSightResult { final double? firstObstructionDistanceMeters; final List samples; final String? errorMessage; + final double usedKFactor; + final double? frequencyMHz; const LineOfSightResult({ required this.hasData, @@ -40,6 +42,8 @@ class LineOfSightResult { required this.maxObstructionMeters, required this.firstObstructionDistanceMeters, required this.samples, + required this.usedKFactor, + this.frequencyMHz, this.errorMessage, }); @@ -50,7 +54,9 @@ class LineOfSightResult { isClear = false, maxObstructionMeters = 0, firstObstructionDistanceMeters = null, - samples = const []; + samples = const [], + usedKFactor = 4.0 / 3.0, + frequencyMHz = null; } class LineOfSightPathSegment { @@ -91,6 +97,11 @@ class LineOfSightService { static const Duration _cacheTtl = Duration(hours: 24); static const int _maxFetchAttempts = 4; // initial try + 3 retries static const Duration _initialBackoff = Duration(milliseconds: 300); + static const double _baselineFrequencyMHz = 915.0; + static const double _baselineKFactor = 4.0 / 3.0; + + static double get baselineFrequencyMHz => _baselineFrequencyMHz; + static double get baselineKFactor => _baselineKFactor; final http.Client _httpClient; final bool _ownsHttpClient; @@ -108,7 +119,7 @@ class LineOfSightService { List points, { double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) async { if (points.length < 2) { @@ -125,6 +136,7 @@ class LineOfSightService { var blockedSegments = 0; var unknownSegments = 0; + final kFactor = _kFactorForFrequency(frequencyMHz); for (int i = 0; i < points.length - 1; i++) { final result = await analyzeLink( points[i], @@ -132,6 +144,7 @@ class LineOfSightService { startAntennaHeightMeters: startAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters, kFactor: kFactor, + frequencyMHz: frequencyMHz, obstructionToleranceMeters: obstructionToleranceMeters, ); segments.add( @@ -165,7 +178,8 @@ class LineOfSightService { LatLng end, { double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + required double kFactor, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) async { final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end); @@ -177,6 +191,8 @@ class LineOfSightService { maxObstructionMeters: 0, firstObstructionDistanceMeters: null, samples: const [], + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } @@ -205,7 +221,8 @@ class LineOfSightService { required List elevations, double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + required double kFactor, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) { if (points.length < 2 || elevations.length != points.length) { @@ -240,7 +257,10 @@ class LineOfSightService { (2 * effectiveEarthRadius); final terrainHeight = elevations[i] + earthBulge; final clearance = lineHeight - terrainHeight; - final radioHorizonHeight = lineHeight - earthBulge; + final unrefBulge = + (distanceFromStart * (totalDistanceMeters - distanceFromStart)) / + (2 * _earthRadiusMeters); + final refractedHeight = lineHeight + (unrefBulge - earthBulge); if (clearance < -obstructionToleranceMeters) { isClear = false; @@ -256,7 +276,7 @@ class LineOfSightService { distanceMeters: distanceFromStart, terrainMeters: terrainHeight, lineHeightMeters: lineHeight, - radioHorizonMeters: radioHorizonHeight, + refractedHeightMeters: refractedHeight, clearanceMeters: clearance, ), ); @@ -269,9 +289,20 @@ class LineOfSightService { maxObstructionMeters: maxObstructionMeters, firstObstructionDistanceMeters: firstObstructionDistanceMeters, samples: samples, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } + static double _kFactorForFrequency(double? frequencyMHz) { + if (frequencyMHz == null) return _baselineKFactor; + final delta = + (frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz; + final adjustment = delta * 0.15; + final scaled = _baselineKFactor * (1 + adjustment); + return scaled.clamp(1.1, 1.6).toDouble(); + } + List _buildSamplePoints( LatLng start, LatLng end, diff --git a/test/services/line_of_sight_service_test.dart b/test/services/line_of_sight_service_test.dart index 987ee6c..267a70b 100644 --- a/test/services/line_of_sight_service_test.dart +++ b/test/services/line_of_sight_service_test.dart @@ -16,6 +16,7 @@ void main() { elevations: elevations, startAntennaHeightMeters: 2, endAntennaHeightMeters: 2, + kFactor: 4.0 / 3.0, ); expect(result.hasData, isTrue); @@ -36,6 +37,7 @@ void main() { elevations: elevations, startAntennaHeightMeters: 1.5, endAntennaHeightMeters: 1.5, + kFactor: 4.0 / 3.0, ); expect(result.hasData, isTrue); From 677b25908ade7edc8b1e84487b74393be7e3388c Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:42:58 -0500 Subject: [PATCH 167/421] Document LOS frequency and k-factor math Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact --- lib/l10n/app_bg.arb | 18 +++++++- lib/l10n/app_de.arb | 18 +++++++- lib/l10n/app_en.arb | 16 +++++++ lib/l10n/app_es.arb | 18 +++++++- lib/l10n/app_fr.arb | 18 +++++++- lib/l10n/app_it.arb | 18 +++++++- lib/l10n/app_localizations.dart | 47 ++++++++++++++++++++ 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 | 28 ++++++++++++ lib/l10n/app_localizations_fr.dart | 28 ++++++++++++ lib/l10n/app_localizations_it.dart | 28 ++++++++++++ lib/l10n/app_localizations_nl.dart | 28 ++++++++++++ lib/l10n/app_localizations_pl.dart | 28 ++++++++++++ lib/l10n/app_localizations_pt.dart | 28 ++++++++++++ lib/l10n/app_localizations_ru.dart | 28 ++++++++++++ lib/l10n/app_localizations_sk.dart | 28 ++++++++++++ lib/l10n/app_localizations_sl.dart | 28 ++++++++++++ lib/l10n/app_localizations_sv.dart | 28 ++++++++++++ lib/l10n/app_localizations_uk.dart | 28 ++++++++++++ lib/l10n/app_localizations_zh.dart | 28 ++++++++++++ lib/l10n/app_nl.arb | 18 +++++++- lib/l10n/app_pl.arb | 18 +++++++- lib/l10n/app_pt.arb | 18 +++++++- lib/l10n/app_ru.arb | 18 +++++++- lib/l10n/app_sk.arb | 18 +++++++- lib/l10n/app_sl.arb | 18 +++++++- lib/l10n/app_sv.arb | 18 +++++++- lib/l10n/app_uk.arb | 18 +++++++- lib/l10n/app_zh.arb | 18 +++++++- lib/screens/line_of_sight_map_screen.dart | 52 +++++++++++------------ 32 files changed, 747 insertions(+), 41 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023..dc0ca4e 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1716,5 +1716,21 @@ "losPointName": "Име на точката", "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", - "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиохоризонт", + "losLegendLosBeam": "LOS лъч", + "losLegendTerrain": "Терен", + "losFrequencyLabel": "Честота", + "losFrequencyInfoTooltip": "Преглед на подробностите за изчислението", + "losFrequencyDialogTitle": "Изчисление на радиохоризонта", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението умножава 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, за да достигне k approx {kFactor} за текущата лента {frequencyMHz} MHz, която определя извитата граница на радиохоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f7..25c899c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1744,5 +1744,21 @@ "losPointName": "Punktname", "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", - "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Funkhorizont", + "losLegendLosBeam": "LOS-Strahl", + "losLegendTerrain": "Gelände", + "losFrequencyLabel": "Frequenz", + "losFrequencyInfoTooltip": "Berechnungsdetails anzeigen", + "losFrequencyDialogTitle": "Funkhorizont-Berechnung", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz multipliziert die Berechnung 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, um k approx {kFactor} für das aktuelle {frequencyMHz}-MHz-Band zu erreichen, das die gekrümmte Funkhorizont-Grenze definiert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72e..3f89e48 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1665,6 +1665,22 @@ "losShowPanelTooltip": "Show LOS panel", "losHidePanelTooltip": "Hide LOS panel", "losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radio horizon", + "losLegendLosBeam": "LOS beam", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Frequency", + "losFrequencyInfoTooltip": "View calculation details", + "losFrequencyDialogTitle": "Radio horizon calculation", + "losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + }, "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d3..0616454 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1744,5 +1744,21 @@ "losPointName": "Nombre del punto", "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", - "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte radioeléctrico", + "losLegendLosBeam": "Haz LOS", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frecuencia", + "losFrequencyInfoTooltip": "Ver detalles del cálculo", + "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", + "losFrequencyDialogDescription": "Partiendo de k={baselineK} a {baselineFreq} MHz, el cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para llegar a k approx {kFactor} para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte radioeléctrico.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2d4846c..cfdd3ba 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nom du point", "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", - "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizon radio", + "losLegendLosBeam": "Faisceau LOS", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Fréquence", + "losFrequencyInfoTooltip": "Voir les détails du calcul", + "losFrequencyDialogTitle": "Calcul de l’horizon radio", + "losFrequencyDialogDescription": "En partant de k={baselineK} à {baselineFreq} MHz, le calcul multiplie 0.15 × (frequency − {baselineFreq}) / {baselineFreq} pour atteindre k approx {kFactor} pour la bande actuelle de {frequencyMHz} MHz, qui définit la limite courbe de l’horizon radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186..427eb0a 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nome del punto", "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", - "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Orizzonte radio", + "losLegendLosBeam": "Raggio LOS", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequenza", + "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", + "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo moltiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} per raggiungere k approx {kFactor} per la banda corrente di {frequencyMHz} MHz, che definisce il limite curvo dell’orizzonte radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686ce..015dcca 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4992,6 +4992,53 @@ abstract class AppLocalizations { /// **'Elevation data: Open-Meteo (CC BY 4.0)'** String get losElevationAttribution; + /// No description provided for @losLegendRadioHorizon. + /// + /// In en, this message translates to: + /// **'Radio horizon'** + String get losLegendRadioHorizon; + + /// No description provided for @losLegendLosBeam. + /// + /// In en, this message translates to: + /// **'LOS beam'** + String get losLegendLosBeam; + + /// No description provided for @losLegendTerrain. + /// + /// In en, this message translates to: + /// **'Terrain'** + String get losLegendTerrain; + + /// No description provided for @losFrequencyLabel. + /// + /// In en, this message translates to: + /// **'Frequency'** + String get losFrequencyLabel; + + /// No description provided for @losFrequencyInfoTooltip. + /// + /// In en, this message translates to: + /// **'View calculation details'** + String get losFrequencyInfoTooltip; + + /// No description provided for @losFrequencyDialogTitle. + /// + /// In en, this message translates to: + /// **'Radio horizon calculation'** + String get losFrequencyDialogTitle; + + /// Explain how the calculation uses the baseline frequency and derived k-factor. + /// + /// In en, this message translates to: + /// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'** + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ); + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b..68ed8e5 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2859,6 +2859,35 @@ class AppLocalizationsBg extends AppLocalizations { String get losElevationAttribution => 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радиохоризонт'; + + @override + String get losLegendLosBeam => 'LOS лъч'; + + @override + String get losLegendTerrain => 'Терен'; + + @override + String get losFrequencyLabel => 'Честота'; + + @override + String get losFrequencyInfoTooltip => + 'Преглед на подробностите за изчислението'; + + @override + String get losFrequencyDialogTitle => 'Изчисление на радиохоризонта'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението умножава 0.15 × (frequency − $baselineFreq) / $baselineFreq, за да достигне k approx $kFactor за текущата лента $frequencyMHz MHz, която определя извитата граница на радиохоризонта.'; + } + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4..1c97a4c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2865,6 +2865,34 @@ class AppLocalizationsDe extends AppLocalizations { @override String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Funkhorizont'; + + @override + String get losLegendLosBeam => 'LOS-Strahl'; + + @override + String get losLegendTerrain => 'Gelände'; + + @override + String get losFrequencyLabel => 'Frequenz'; + + @override + String get losFrequencyInfoTooltip => 'Berechnungsdetails anzeigen'; + + @override + String get losFrequencyDialogTitle => 'Funkhorizont-Berechnung'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz multipliziert die Berechnung 0.15 × (frequency − $baselineFreq) / $baselineFreq, um k approx $kFactor für das aktuelle $frequencyMHz-MHz-Band zu erreichen, das die gekrümmte Funkhorizont-Grenze definiert.'; + } + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4..c5ef344 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2817,6 +2817,34 @@ class AppLocalizationsEn extends AppLocalizations { String get losElevationAttribution => 'Elevation data: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radio horizon'; + + @override + String get losLegendLosBeam => 'LOS beam'; + + @override + String get losLegendTerrain => 'Terrain'; + + @override + String get losFrequencyLabel => 'Frequency'; + + @override + String get losFrequencyInfoTooltip => 'View calculation details'; + + @override + String get losFrequencyDialogTitle => 'Radio horizon calculation'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + } + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde365..33db872 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2859,6 +2859,34 @@ class AppLocalizationsEs extends AppLocalizations { String get losElevationAttribution => 'Datos de elevación: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; + + @override + String get losLegendLosBeam => 'Haz LOS'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frecuencia'; + + @override + String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; + + @override + String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Partiendo de k=$baselineK a $baselineFreq MHz, el cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para llegar a k approx $kFactor para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte radioeléctrico.'; + } + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c4e1e27..fc059a9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2874,6 +2874,34 @@ class AppLocalizationsFr extends AppLocalizations { String get losElevationAttribution => 'Données d\'altitude : Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizon radio'; + + @override + String get losLegendLosBeam => 'Faisceau LOS'; + + @override + String get losLegendTerrain => 'Terrain'; + + @override + String get losFrequencyLabel => 'Fréquence'; + + @override + String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; + + @override + String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'En partant de k=$baselineK à $baselineFreq MHz, le calcul multiplie 0.15 × (frequency − $baselineFreq) / $baselineFreq pour atteindre k approx $kFactor pour la bande actuelle de $frequencyMHz MHz, qui définit la limite courbe de l’horizon radio.'; + } + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f8..123d194 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2859,6 +2859,34 @@ class AppLocalizationsIt extends AppLocalizations { String get losElevationAttribution => 'Dati di elevazione: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Orizzonte radio'; + + @override + String get losLegendLosBeam => 'Raggio LOS'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frequenza'; + + @override + String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; + + @override + String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo moltiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq per raggiungere k approx $kFactor per la banda corrente di $frequencyMHz MHz, che definisce il limite curvo dell’orizzonte radio.'; + } + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b..3c6d5c4 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2850,6 +2850,34 @@ class AppLocalizationsNl extends AppLocalizations { String get losElevationAttribution => 'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radiohorizon'; + + @override + String get losLegendLosBeam => 'LOS-straal'; + + @override + String get losLegendTerrain => 'Terrein'; + + @override + String get losFrequencyLabel => 'Frequentie'; + + @override + String get losFrequencyInfoTooltip => 'Berekeningsdetails bekijken'; + + @override + String get losFrequencyDialogTitle => 'Radiohorizon-berekening'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Uitgaande van k=$baselineK bij $baselineFreq MHz vermenigvuldigt de berekening 0.15 × (frequency − $baselineFreq) / $baselineFreq om k approx $kFactor te bereiken voor de huidige $frequencyMHz-MHz-band, die de gebogen radiohorizon-limiet definieert.'; + } + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b5..02a6a11 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2856,6 +2856,34 @@ class AppLocalizationsPl extends AppLocalizations { String get losElevationAttribution => 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horyzont radiowy'; + + @override + String get losLegendLosBeam => 'Wiązka LOS'; + + @override + String get losLegendTerrain => 'Teren'; + + @override + String get losFrequencyLabel => 'Częstotliwość'; + + @override + String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczeń'; + + @override + String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Wychodząc od k=$baselineK przy $baselineFreq MHz, obliczenie mnoży 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby osiągnąć k approx $kFactor dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywioną granicę horyzontu radiowego.'; + } + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826..7cb0986 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2858,6 +2858,34 @@ class AppLocalizationsPt extends AppLocalizations { String get losElevationAttribution => 'Dados de elevação: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizonte de rádio'; + + @override + String get losLegendLosBeam => 'Feixe LOS'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frequência'; + + @override + String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; + + @override + String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Partindo de k=$baselineK a $baselineFreq MHz, o cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para chegar a k approx $kFactor para a banda atual de $frequencyMHz MHz, que define o limite curvo do horizonte de rádio.'; + } + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c8..6e35b43 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2861,6 +2861,34 @@ class AppLocalizationsRu extends AppLocalizations { String get losElevationAttribution => 'Данные о высоте: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радиогоризонт'; + + @override + String get losLegendLosBeam => 'Луч LOS'; + + @override + String get losLegendTerrain => 'Рельеф'; + + @override + String get losFrequencyLabel => 'Частота'; + + @override + String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта'; + + @override + String get losFrequencyDialogTitle => 'Расчёт радиогоризонта'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Исходя из k=$baselineK при $baselineFreq MHz, расчёт умножает 0.15 × (frequency − $baselineFreq) / $baselineFreq, чтобы получить k approx $kFactor для текущего диапазона $frequencyMHz MHz, который определяет изогнутую границу радиогоризонта.'; + } + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6de..041e7fd 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2844,6 +2844,34 @@ class AppLocalizationsSk extends AppLocalizations { String get losElevationAttribution => 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Rádiový horizont'; + + @override + String get losLegendLosBeam => 'LOS lúč'; + + @override + String get losLegendTerrain => 'Terén'; + + @override + String get losFrequencyLabel => 'Frekvencia'; + + @override + String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu'; + + @override + String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Vychádzajúc z k=$baselineK pri $baselineFreq MHz výpočet násobí 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby dosiahol k approx $kFactor pre aktuálne pásmo $frequencyMHz MHz, ktoré definuje zakrivenú hranicu rádiového horizontu.'; + } + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e3058..0a46533 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2847,6 +2847,34 @@ class AppLocalizationsSl extends AppLocalizations { String get losElevationAttribution => 'Podatki o višini: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radijski horizont'; + + @override + String get losLegendLosBeam => 'LOS žarek'; + + @override + String get losLegendTerrain => 'Teren'; + + @override + String get losFrequencyLabel => 'Frekvenca'; + + @override + String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; + + @override + String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Izhajajoč iz k=$baselineK pri $baselineFreq MHz izračun množi 0.15 × (frequency − $baselineFreq) / $baselineFreq, da doseže k approx $kFactor za trenutno $frequencyMHz-MHz območje, ki določa ukrivljeno mejo radijskega horizonta.'; + } + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b8..0fb8e70 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2830,6 +2830,34 @@ class AppLocalizationsSv extends AppLocalizations { @override String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radiohorisont'; + + @override + String get losLegendLosBeam => 'LOS-stråle'; + + @override + String get losLegendTerrain => 'Terräng'; + + @override + String get losFrequencyLabel => 'Frekvens'; + + @override + String get losFrequencyInfoTooltip => 'Visa beräkningsdetaljer'; + + @override + String get losFrequencyDialogTitle => 'Beräkning av radiohorisont'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Med start från k=$baselineK vid $baselineFreq MHz multiplicerar beräkningen 0.15 × (frequency − $baselineFreq) / $baselineFreq för att nå k approx $kFactor för det aktuella $frequencyMHz-MHz-bandet, vilket definierar den krökta radiohorisontgränsen.'; + } + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f367002..e81f8e1 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2869,6 +2869,34 @@ class AppLocalizationsUk extends AppLocalizations { String get losElevationAttribution => 'Дані про висоту: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радіогоризонт'; + + @override + String get losLegendLosBeam => 'Промінь LOS'; + + @override + String get losLegendTerrain => 'Рельєф'; + + @override + String get losFrequencyLabel => 'Частота'; + + @override + String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку'; + + @override + String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Виходячи з k=$baselineK при $baselineFreq MHz, розрахунок множить 0.15 × (frequency − $baselineFreq) / $baselineFreq, щоб отримати k approx $kFactor для поточного діапазону $frequencyMHz MHz, який визначає вигнуту межу радіогоризонту.'; + } + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800..a7f4a8a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2709,6 +2709,34 @@ class AppLocalizationsZh extends AppLocalizations { @override String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => '无线电地平线'; + + @override + String get losLegendLosBeam => 'LOS 波束'; + + @override + String get losLegendTerrain => '地形'; + + @override + String get losFrequencyLabel => '频率'; + + @override + String get losFrequencyInfoTooltip => '查看计算详情'; + + @override + String get losFrequencyDialogTitle => '无线电地平线计算'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return '从 k=$baselineK($baselineFreq MHz)开始,计算将 0.15 × (frequency − $baselineFreq) / $baselineFreq,以得到当前 $frequencyMHz MHz 频段的 k approx $kFactor,从而定义弯曲的无线电地平线边界。'; + } + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdd..3855e14 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1716,5 +1716,21 @@ "losPointName": "Puntnaam", "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", - "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorizon", + "losLegendLosBeam": "LOS-straal", + "losLegendTerrain": "Terrein", + "losFrequencyLabel": "Frequentie", + "losFrequencyInfoTooltip": "Berekeningsdetails bekijken", + "losFrequencyDialogTitle": "Radiohorizon-berekening", + "losFrequencyDialogDescription": "Uitgaande van k={baselineK} bij {baselineFreq} MHz vermenigvuldigt de berekening 0.15 × (frequency − {baselineFreq}) / {baselineFreq} om k approx {kFactor} te bereiken voor de huidige {frequencyMHz}-MHz-band, die de gebogen radiohorizon-limiet definieert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa7..dde149c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nazwa punktu", "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", - "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horyzont radiowy", + "losLegendLosBeam": "Wiązka LOS", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Częstotliwość", + "losFrequencyInfoTooltip": "Zobacz szczegóły obliczeń", + "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", + "losFrequencyDialogDescription": "Wychodząc od k={baselineK} przy {baselineFreq} MHz, obliczenie mnoży 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby osiągnąć k approx {kFactor} dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywioną granicę horyzontu radiowego.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be6694..7edff74 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nome do ponto", "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", - "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte de rádio", + "losLegendLosBeam": "Feixe LOS", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequência", + "losFrequencyInfoTooltip": "Ver detalhes do cálculo", + "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", + "losFrequencyDialogDescription": "Partindo de k={baselineK} a {baselineFreq} MHz, o cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para chegar a k approx {kFactor} para a banda atual de {frequencyMHz} MHz, que define o limite curvo do horizonte de rádio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3..139074b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -956,5 +956,21 @@ "losPointName": "Имя точки", "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", - "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиогоризонт", + "losLegendLosBeam": "Луч LOS", + "losLegendTerrain": "Рельеф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Просмотреть детали расчёта", + "losFrequencyDialogTitle": "Расчёт радиогоризонта", + "losFrequencyDialogDescription": "Исходя из k={baselineK} при {baselineFreq} MHz, расчёт умножает 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, чтобы получить k approx {kFactor} для текущего диапазона {frequencyMHz} MHz, который определяет изогнутую границу радиогоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0a..89a1b0d 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1716,5 +1716,21 @@ "losPointName": "Názov bodu", "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", - "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádiový horizont", + "losLegendLosBeam": "LOS lúč", + "losLegendTerrain": "Terén", + "losFrequencyLabel": "Frekvencia", + "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", + "losFrequencyDialogTitle": "Výpočet rádiového horizontu", + "losFrequencyDialogDescription": "Vychádzajúc z k={baselineK} pri {baselineFreq} MHz výpočet násobí 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby dosiahol k approx {kFactor} pre aktuálne pásmo {frequencyMHz} MHz, ktoré definuje zakrivenú hranicu rádiového horizontu.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d..2fe86e0 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1716,5 +1716,21 @@ "losPointName": "Ime točke", "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", - "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radijski horizont", + "losLegendLosBeam": "LOS žarek", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Frekvenca", + "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", + "losFrequencyDialogTitle": "Izračun radijskega horizonta", + "losFrequencyDialogDescription": "Izhajajoč iz k={baselineK} pri {baselineFreq} MHz izračun množi 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, da doseže k approx {kFactor} za trenutno {frequencyMHz}-MHz območje, ki določa ukrivljeno mejo radijskega horizonta.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399..2625fb2 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1716,5 +1716,21 @@ "losPointName": "Punktnamn", "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", - "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorisont", + "losLegendLosBeam": "LOS-stråle", + "losLegendTerrain": "Terräng", + "losFrequencyLabel": "Frekvens", + "losFrequencyInfoTooltip": "Visa beräkningsdetaljer", + "losFrequencyDialogTitle": "Beräkning av radiohorisont", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz multiplicerar beräkningen 0.15 × (frequency − {baselineFreq}) / {baselineFreq} för att nå k approx {kFactor} för det aktuella {frequencyMHz}-MHz-bandet, vilket definierar den krökta radiohorisontgränsen.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0..8c28f19 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1716,5 +1716,21 @@ "losPointName": "Назва точки", "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", - "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радіогоризонт", + "losLegendLosBeam": "Промінь LOS", + "losLegendTerrain": "Рельєф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", + "losFrequencyDialogTitle": "Розрахунок радіогоризонту", + "losFrequencyDialogDescription": "Виходячи з k={baselineK} при {baselineFreq} MHz, розрахунок множить 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, щоб отримати k approx {kFactor} для поточного діапазону {frequencyMHz} MHz, який визначає вигнуту межу радіогоризонту.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7..bd8067f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1716,5 +1716,21 @@ "losPointName": "点名称", "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", - "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "无线电地平线", + "losLegendLosBeam": "LOS 波束", + "losLegendTerrain": "地形", + "losFrequencyLabel": "频率", + "losFrequencyInfoTooltip": "查看计算详情", + "losFrequencyDialogTitle": "无线电地平线计算", + "losFrequencyDialogDescription": "从 k={baselineK}({baselineFreq} MHz)开始,计算将 0.15 × (frequency − {baselineFreq}) / {baselineFreq},以得到当前 {frequencyMHz} MHz 频段的 k approx {kFactor},从而定义弯曲的无线电地平线边界。", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 5eb532b..72c2232 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -471,6 +471,9 @@ class _LineOfSightMapScreenState extends State { fontSize: 10, fontWeight: FontWeight.w600, ), + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, ), ), ) @@ -504,7 +507,7 @@ class _LineOfSightMapScreenState extends State { child: Row( children: [ Text( - 'Frequency', + context.l10n.losFrequencyLabel, style: TextStyle( fontSize: 11, color: Colors.grey[700], @@ -531,7 +534,7 @@ class _LineOfSightMapScreenState extends State { constraints: const BoxConstraints(), icon: const Icon(Icons.info_outline, size: 16), color: Colors.grey[600], - tooltip: 'View calculation details', + tooltip: context.l10n.losFrequencyInfoTooltip, onPressed: () { _showFrequencyInfoDialog( context, @@ -963,27 +966,13 @@ class _LineOfSightMapScreenState extends State { showDialog( context: context, builder: (dialogContext) => AlertDialog( - title: const Text('Radio horizon calculation'), - content: Text.rich( - TextSpan( - children: [ - TextSpan( - text: - 'Starting from k=$baselineK at ${baselineFreq.toStringAsFixed(3)} MHz, ', - ), - const TextSpan(text: 'the calculation multiplies the offset by '), - TextSpan( - text: - '0.15 × (frequency − ${baselineFreq.toStringAsFixed(3)}) / ${baselineFreq.toStringAsFixed(3)} ', - ), - TextSpan( - text: - 'to get k ≈ ${kFactor.toStringAsFixed(3)} for the current ${frequencyMHz.toStringAsFixed(3)} MHz band, ', - ), - const TextSpan( - text: 'which defines the curved radio horizon cap.', - ), - ], + title: Text(context.l10n.losFrequencyDialogTitle), + content: Text( + context.l10n.losFrequencyDialogDescription( + baselineK, + baselineFreq, + frequencyMHz, + kFactor, ), ), actions: [ @@ -1009,12 +998,18 @@ class _LosProfilePainter extends CustomPainter { final String distanceUnit; final String heightUnit; final TextStyle badgeTextStyle; + final String terrainLabel; + final String losBeamLabel; + final String radioHorizonLabel; const _LosProfilePainter({ required this.samples, required this.distanceUnit, required this.heightUnit, required this.badgeTextStyle, + required this.terrainLabel, + required this.losBeamLabel, + required this.radioHorizonLabel, }); @override @@ -1148,7 +1143,10 @@ class _LosProfilePainter extends CustomPainter { return oldDelegate.samples != samples || oldDelegate.distanceUnit != distanceUnit || oldDelegate.heightUnit != heightUnit || - oldDelegate.badgeTextStyle != badgeTextStyle; + oldDelegate.badgeTextStyle != badgeTextStyle || + oldDelegate.terrainLabel != terrainLabel || + oldDelegate.losBeamLabel != losBeamLabel || + oldDelegate.radioHorizonLabel != radioHorizonLabel; } void _drawUnitBadge(Canvas canvas, Size size) { @@ -1175,9 +1173,9 @@ class _LosProfilePainter extends CustomPainter { const legendPadding = 6.0; final entries = [ - _LegendEntry('Terrain', terrainColor), - _LegendEntry('LOS beam', losColor), - _LegendEntry('Radio horizon', horizonColor), + _LegendEntry(terrainLabel, terrainColor), + _LegendEntry(losBeamLabel, losColor), + _LegendEntry(radioHorizonLabel, horizonColor), ]; final textStyle = badgeTextStyle.copyWith( From 7465e81996271830ba460b77cd4cfdf4ce6b47bb Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 03:31:01 -0800 Subject: [PATCH 168/421] add done_all icon --- assets/icons/done_all.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/icons/done_all.svg diff --git a/assets/icons/done_all.svg b/assets/icons/done_all.svg new file mode 100644 index 0000000..bfeeec0 --- /dev/null +++ b/assets/icons/done_all.svg @@ -0,0 +1 @@ + \ No newline at end of file From 53d073d8f2a10ffb376defeddd1ca087d6caa87f Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 03:43:49 -0800 Subject: [PATCH 169/421] deprecation fix --- lib/utils/browser_detection.dart | 2 +- lib/utils/browser_detection_web.dart | 5 ++--- pubspec.lock | 2 +- pubspec.yaml | 1 + 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/utils/browser_detection.dart b/lib/utils/browser_detection.dart index aafb838..b8dca93 100644 --- a/lib/utils/browser_detection.dart +++ b/lib/utils/browser_detection.dart @@ -1,2 +1,2 @@ export 'browser_detection_stub.dart' - if (dart.library.html) 'browser_detection_web.dart'; + if (dart.library.js_interop) 'browser_detection_web.dart'; diff --git a/lib/utils/browser_detection_web.dart b/lib/utils/browser_detection_web.dart index 0e4fccb..bbb9c76 100644 --- a/lib/utils/browser_detection_web.dart +++ b/lib/utils/browser_detection_web.dart @@ -1,9 +1,8 @@ -// ignore: avoid_web_libraries_in_flutter -import 'dart:html' as html; +import 'package:web/web.dart' as web; class BrowserDetection { static bool get isChrome { - final userAgent = html.window.navigator.userAgent.toLowerCase(); + final userAgent = web.window.navigator.userAgent.toLowerCase(); final isChrome = userAgent.contains('chrome'); return isChrome; } diff --git a/pubspec.lock b/pubspec.lock index 18eaff5..756f192 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1083,7 +1083,7 @@ packages: source: hosted version: "1.3.0" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" diff --git a/pubspec.yaml b/pubspec.yaml index a17edde..a7564a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: path_provider: ^2.1.5 share_plus: ^12.0.1 build_pipe: ^0.3.1 + web: ^1.1.1 dev_dependencies: flutter_test: From 549fc6263290b44d1f5ae6792960a9ae47d25ca9 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:09:27 -0800 Subject: [PATCH 170/421] chat fixes --- assets/icons/done_all.svg | 1 + lib/screens/channel_chat_screen.dart | 104 +++++++++++++++------------ lib/screens/chat_screen.dart | 94 ++++++++++++------------ lib/widgets/message_status_icon.dart | 36 ++++++++++ pubspec.lock | 40 +++++++++++ pubspec.yaml | 2 + 6 files changed, 187 insertions(+), 90 deletions(-) create mode 100644 assets/icons/done_all.svg create mode 100644 lib/widgets/message_status_icon.dart diff --git a/assets/icons/done_all.svg b/assets/icons/done_all.svg new file mode 100644 index 0000000..bfeeec0 --- /dev/null +++ b/assets/icons/done_all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index b59a691..c02425b 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -23,6 +24,7 @@ import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; +import '../widgets/message_status_icon.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -337,7 +339,23 @@ class _ChannelChatScreenState extends State { const SizedBox(height: 8), ], if (poi != null) - _buildPoiMessage(context, poi, isOutgoing) + _buildPoiMessage( + context, + poi, + isOutgoing, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: message.status == + ChannelMessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -358,33 +376,31 @@ class _ChannelChatScreenState extends State { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), - shape: BoxShape.circle, + color: isOutgoing + ? Theme.of( + context, + ).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(8), + ), ), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ), @@ -419,25 +435,14 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ], @@ -727,7 +732,12 @@ class _ChannelChatScreenState extends State { return _PoiInfo(lat: lat, lon: lon, label: label); } - Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) { + Widget _buildPoiMessage( + BuildContext context, + _PoiInfo poi, + bool isOutgoing, { + Widget? trailing, + }) { final colorScheme = Theme.of(context).colorScheme; final textColor = isOutgoing ? colorScheme.onPrimaryContainer @@ -773,6 +783,10 @@ class _ChannelChatScreenState extends State { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 6ee846c..a6d2399 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; @@ -13,6 +14,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; +import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; @@ -1269,7 +1271,24 @@ class _MessageBubble extends StatelessWidget { if (gifId == null) const SizedBox(height: 4), ], if (poi != null) - _buildPoiMessage(context, poi, textColor, metaColor) + _buildPoiMessage( + context, + poi, + textColor, + metaColor, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: message.status == + MessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -1286,35 +1305,25 @@ class _MessageBubble extends StatelessWidget { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues( - alpha: 0.3, + color: bubbleColor, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(12), ), - shape: BoxShape.circle, ), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == - MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == - MessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == + MessageStatus.failed, ), ), ), @@ -1348,23 +1357,13 @@ class _MessageBubble extends StatelessWidget { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == MessageStatus.failed, ), ), ], @@ -1481,8 +1480,9 @@ class _MessageBubble extends StatelessWidget { BuildContext context, _PoiInfo poi, Color textColor, - Color metaColor, - ) { + Color metaColor, { + Widget? trailing, + }) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -1519,6 +1519,10 @@ class _MessageBubble extends StatelessWidget { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart new file mode 100644 index 0000000..0689f0b --- /dev/null +++ b/lib/widgets/message_status_icon.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class MessageStatusIcon extends StatelessWidget { + final bool isAcked; + final bool isFailed; + final double size; + + const MessageStatusIcon({ + super.key, + required this.isAcked, + this.isFailed = false, + this.size = 14, + }); + + @override + Widget build(BuildContext context) { + if (isFailed) { + return Icon(Icons.cancel, size: size, color: Colors.red); + } + + final Color color; + if (isAcked) { + color = Colors.green; + } else { + color = Colors.grey; + } + + return SvgPicture.asset( + 'assets/icons/done_all.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 756f192..e2254cb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -363,6 +363,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -637,6 +645,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1050,6 +1066,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a7564a9..3330d39 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: share_plus: ^12.0.1 build_pipe: ^0.3.1 web: ^1.1.1 + flutter_svg: ^2.0.10+1 dev_dependencies: flutter_test: @@ -89,6 +90,7 @@ flutter: assets: - assets/images/ + - assets/icons/ flutter_launcher_icons: android: true From 173fdf7168e8cdd1ccf06cfe08bb4d8abbb71bd0 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:09:27 -0800 Subject: [PATCH 171/421] chat fixes --- lib/screens/channel_chat_screen.dart | 104 +++++++++++++++------------ lib/screens/chat_screen.dart | 94 ++++++++++++------------ lib/widgets/message_status_icon.dart | 36 ++++++++++ pubspec.lock | 40 +++++++++++ pubspec.yaml | 3 + 5 files changed, 187 insertions(+), 90 deletions(-) create mode 100644 lib/widgets/message_status_icon.dart diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9b9de35..a6354c3 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -23,6 +24,7 @@ import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; +import '../widgets/message_status_icon.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -337,7 +339,23 @@ class _ChannelChatScreenState extends State { const SizedBox(height: 8), ], if (poi != null) - _buildPoiMessage(context, poi, isOutgoing) + _buildPoiMessage( + context, + poi, + isOutgoing, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: message.status == + ChannelMessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -358,33 +376,31 @@ class _ChannelChatScreenState extends State { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), - shape: BoxShape.circle, + color: isOutgoing + ? Theme.of( + context, + ).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(8), + ), ), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ), @@ -419,25 +435,14 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ], @@ -727,7 +732,12 @@ class _ChannelChatScreenState extends State { return _PoiInfo(lat: lat, lon: lon, label: label); } - Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) { + Widget _buildPoiMessage( + BuildContext context, + _PoiInfo poi, + bool isOutgoing, { + Widget? trailing, + }) { final colorScheme = Theme.of(context).colorScheme; final textColor = isOutgoing ? colorScheme.onPrimaryContainer @@ -773,6 +783,10 @@ class _ChannelChatScreenState extends State { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 32a7882..bfbd88f 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; @@ -13,6 +14,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; +import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; @@ -1252,7 +1254,24 @@ class _MessageBubble extends StatelessWidget { if (gifId == null) const SizedBox(height: 4), ], if (poi != null) - _buildPoiMessage(context, poi, textColor, metaColor) + _buildPoiMessage( + context, + poi, + textColor, + metaColor, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: message.status == + MessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -1269,35 +1288,25 @@ class _MessageBubble extends StatelessWidget { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues( - alpha: 0.3, + color: bubbleColor, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(12), ), - shape: BoxShape.circle, ), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == - MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == - MessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == + MessageStatus.failed, ), ), ), @@ -1331,23 +1340,13 @@ class _MessageBubble extends StatelessWidget { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == MessageStatus.failed, ), ), ], @@ -1464,8 +1463,9 @@ class _MessageBubble extends StatelessWidget { BuildContext context, _PoiInfo poi, Color textColor, - Color metaColor, - ) { + Color metaColor, { + Widget? trailing, + }) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -1502,6 +1502,10 @@ class _MessageBubble extends StatelessWidget { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart new file mode 100644 index 0000000..0689f0b --- /dev/null +++ b/lib/widgets/message_status_icon.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class MessageStatusIcon extends StatelessWidget { + final bool isAcked; + final bool isFailed; + final double size; + + const MessageStatusIcon({ + super.key, + required this.isAcked, + this.isFailed = false, + this.size = 14, + }); + + @override + Widget build(BuildContext context) { + if (isFailed) { + return Icon(Icons.cancel, size: size, color: Colors.red); + } + + final Color color; + if (isAcked) { + color = Colors.green; + } else { + color = Colors.grey; + } + + return SvgPicture.asset( + 'assets/icons/done_all.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index f695838..9830433 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1010,6 +1026,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f5ceaaf..dcca7f8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,8 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + web: ^1.1.1 + flutter_svg: ^2.0.10+1 dev_dependencies: flutter_test: @@ -87,6 +89,7 @@ flutter: assets: - assets/images/ + - assets/icons/ flutter_launcher_icons: android: true From 3730b2a6c2e0f8890ca58c13c6cbbe3988fc316d Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:13:38 -0800 Subject: [PATCH 172/421] formatting --- lib/screens/channel_chat_screen.dart | 11 +++++------ lib/screens/chat_screen.dart | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index a6354c3..937fa87 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -347,10 +347,12 @@ class _ChannelChatScreenState extends State { ? Padding( padding: const EdgeInsets.only(bottom: 2), child: MessageStatusIcon( - isAcked: message.status == + isAcked: + message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty, - isFailed: message.status == + isFailed: + message.status == ChannelMessageStatus.failed, ), ) @@ -783,10 +785,7 @@ class _ChannelChatScreenState extends State { ], ), ), - if (trailing != null) ...[ - const SizedBox(width: 4), - trailing, - ], + if (trailing != null) ...[const SizedBox(width: 4), trailing], ], ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index bfbd88f..180f813 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1263,10 +1263,12 @@ class _MessageBubble extends StatelessWidget { ? Padding( padding: const EdgeInsets.only(bottom: 2), child: MessageStatusIcon( - isAcked: message.status == + isAcked: + message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty, - isFailed: message.status == + isFailed: + message.status == MessageStatus.failed, ), ) @@ -1502,10 +1504,7 @@ class _MessageBubble extends StatelessWidget { ], ), ), - if (trailing != null) ...[ - const SizedBox(width: 4), - trailing, - ], + if (trailing != null) ...[const SizedBox(width: 4), trailing], ], ); } From bf5fadd15eea2f101c52fe4a55946dca6748a6a4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:13:52 -0800 Subject: [PATCH 173/421] revert lockfile --- pubspec.lock | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 9830433..f695838 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,14 +347,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -605,14 +597,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1026,30 +1010,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" vector_math: dependency: transitive description: From c8f93f990266f545a4e33c21494ca099f9039117 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:30:13 -0800 Subject: [PATCH 174/421] code cleanup --- lib/screens/channel_chat_screen.dart | 1 - lib/screens/chat_screen.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 10066e6..70c9c20 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 49204f8..70cb036 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; From 88f8066ed3fe4e904f4fefd2ad5f529e8673dae3 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:53:01 -0800 Subject: [PATCH 175/421] code formatting --- lib/screens/channel_chat_screen.dart | 1 - lib/screens/chat_screen.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 937fa87..9df91c3 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 180f813..3556d6d 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; From 16b2c249830aa5692b7decc82980a4990cb3dcd4 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:18:42 -0500 Subject: [PATCH 176/421] Propagate LOS frequency data and clamp bounds --- lib/screens/line_of_sight_map_screen.dart | 14 ++++++-- lib/services/line_of_sight_service.dart | 1 + pubspec.lock | 42 ++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 72c2232..c3fe476 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -1021,10 +1021,20 @@ class _LosProfilePainter extends CustomPainter { if (samples.length < 2) return; final minY = samples - .map((s) => math.min(s.terrainMeters, s.lineHeightMeters)) + .map( + (s) => math.min( + math.min(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) .reduce(math.min); final maxY = samples - .map((s) => math.max(s.terrainMeters, s.lineHeightMeters)) + .map( + (s) => math.max( + math.max(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) .reduce(math.max); final ySpan = math.max(1.0, maxY - minY); final maxDist = math.max(1.0, samples.last.distanceMeters); diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index 14d8fc6..61e9e27 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -212,6 +212,7 @@ class LineOfSightService { startAntennaHeightMeters: startAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters, kFactor: kFactor, + frequencyMHz: frequencyMHz, obstructionToleranceMeters: obstructionToleranceMeters, ); } diff --git a/pubspec.lock b/pubspec.lock index ed84c40..22cad80 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1010,6 +1026,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: @@ -1043,7 +1083,7 @@ packages: source: hosted version: "1.3.0" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" From c0516a475d24bbc6391470b74b3b0994713023b9 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:36:10 -0500 Subject: [PATCH 177/421] fix: extend los profile edges --- lib/screens/line_of_sight_map_screen.dart | 219 +++++++++++++--------- 1 file changed, 129 insertions(+), 90 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index c3fe476..785dfe5 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -487,6 +487,14 @@ class _LineOfSightMapScreenState extends State { ), ), ), + if (segment != null) ...[ + const SizedBox(height: 8), + _LosLegend( + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, + ), + ], const SizedBox(height: 8), Text( segment != null @@ -1038,36 +1046,85 @@ class _LosProfilePainter extends CustomPainter { .reduce(math.max); final ySpan = math.max(1.0, maxY - minY); final maxDist = math.max(1.0, samples.last.distanceMeters); + const horizontalPadding = 12.0; + const verticalPadding = 12.0; + final chartWidth = math.max(1.0, size.width - horizontalPadding * 2); + final chartHeight = math.max(1.0, size.height - verticalPadding * 2); Offset mapPoint(double x, double y) { - final px = (x / maxDist) * size.width; - final py = size.height - ((y - minY) / ySpan) * size.height; + final px = horizontalPadding + (x / maxDist) * chartWidth; + final py = + size.height - verticalPadding - ((y - minY) / ySpan) * chartHeight; return Offset(px, py); } - final terrainPath = ui.Path(); - terrainPath.moveTo(0, size.height); - for (final s in samples) { - final p = mapPoint(s.distanceMeters, s.terrainMeters); + final firstTerrainPoint = mapPoint( + samples.first.distanceMeters, + samples.first.terrainMeters, + ); + final lastTerrainPoint = mapPoint( + samples.last.distanceMeters, + samples.last.terrainMeters, + ); + + double distanceForCanvasX(double x) => + ((x - horizontalPadding) / chartWidth) * maxDist; + + double elevationToPixel(double elevation) => + size.height - + verticalPadding - + ((elevation - minY) / ySpan) * chartHeight; + + double extrapolateTerrain(double distance, bool isLeft) { + final samplesForSlope = isLeft + ? samples.sublist(0, math.min(2, samples.length)) + : samples.sublist(samples.length - math.min(2, samples.length)); + if (samplesForSlope.length < 2) { + return samplesForSlope.first.terrainMeters; + } + final a = samplesForSlope.first; + final b = samplesForSlope.last; + final dx = b.distanceMeters - a.distanceMeters; + if (dx.abs() < 1e-6) return a.terrainMeters; + final slope = (b.terrainMeters - a.terrainMeters) / dx; + return a.terrainMeters + slope * (distance - a.distanceMeters); + } + + final leftDistance = distanceForCanvasX(0.0); + final rightDistance = distanceForCanvasX(size.width); + final leftEdgeTerrain = extrapolateTerrain(leftDistance, true); + final rightEdgeTerrain = extrapolateTerrain(rightDistance, false); + final leftEdgePoint = Offset(0.0, elevationToPixel(leftEdgeTerrain)); + final rightEdgePoint = Offset( + size.width, + elevationToPixel(rightEdgeTerrain), + ); + + final terrainPath = ui.Path() + ..moveTo(0, size.height) + ..lineTo(leftEdgePoint.dx, leftEdgePoint.dy) + ..lineTo(firstTerrainPoint.dx, firstTerrainPoint.dy); + for (final sample in samples) { + final p = mapPoint(sample.distanceMeters, sample.terrainMeters); terrainPath.lineTo(p.dx, p.dy); } - terrainPath.lineTo(size.width, size.height); - terrainPath.close(); + terrainPath + ..lineTo(lastTerrainPoint.dx, lastTerrainPoint.dy) + ..lineTo(rightEdgePoint.dx, rightEdgePoint.dy) + ..lineTo(size.width, size.height) + ..close(); const terrainFillColor = Color(0xCC7C6F5D); const terrainLineColor = Color(0xFF9FE870); const losLineColor = Color(0xFFE0E7FF); canvas.drawPath(terrainPath, Paint()..color = terrainFillColor); - final terrainLine = ui.Path(); - for (int i = 0; i < samples.length; i++) { - final p = mapPoint(samples[i].distanceMeters, samples[i].terrainMeters); - if (i == 0) { - terrainLine.moveTo(p.dx, p.dy); - } else { - terrainLine.lineTo(p.dx, p.dy); - } + final terrainLine = ui.Path()..moveTo(leftEdgePoint.dx, leftEdgePoint.dy); + for (final sample in samples) { + final p = mapPoint(sample.distanceMeters, sample.terrainMeters); + terrainLine.lineTo(p.dx, p.dy); } + terrainLine.lineTo(rightEdgePoint.dx, rightEdgePoint.dy); canvas.drawPath( terrainLine, Paint() @@ -1144,8 +1201,6 @@ class _LosProfilePainter extends CustomPainter { ..color = horizonFillColor ..style = PaintingStyle.fill, ); - - _drawLegend(canvas, refractedLineColor, losLineColor, terrainLineColor); } @override @@ -1168,84 +1223,68 @@ class _LosProfilePainter extends CustomPainter { ..layout(); painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); } +} - void _drawLegend( - Canvas canvas, - Color horizonColor, - Color losColor, - Color terrainColor, - ) { - const legendX = 8.0; - const legendY = 8.0; - const swatchSize = 10.0; - const swatchTextGap = 6.0; - const entrySpacing = 4.0; - const legendPadding = 6.0; +class _LosLegend extends StatelessWidget { + static const _terrainColor = Color(0xFF9FE870); + static const _losColor = Color(0xFFE0E7FF); + static const _radioColor = Color(0xFFFFD57F); + + final String terrainLabel; + final String losBeamLabel; + final String radioHorizonLabel; + + const _LosLegend({ + required this.terrainLabel, + required this.losBeamLabel, + required this.radioHorizonLabel, + }); + + @override + Widget build(BuildContext context) { + final textStyle = + Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white70, + fontSize: 11, + fontWeight: FontWeight.w500, + ) ?? + const TextStyle( + color: Colors.white70, + fontSize: 11, + fontWeight: FontWeight.w500, + ); final entries = [ - _LegendEntry(terrainLabel, terrainColor), - _LegendEntry(losBeamLabel, losColor), - _LegendEntry(radioHorizonLabel, horizonColor), + _LegendEntry(terrainLabel, _terrainColor), + _LegendEntry(losBeamLabel, _losColor), + _LegendEntry(radioHorizonLabel, _radioColor), ]; - final textStyle = badgeTextStyle.copyWith( - fontSize: 10, - fontWeight: FontWeight.w500, + const swatchSize = 10.0; + + return Wrap( + spacing: 16, + runSpacing: 6, + children: entries + .map( + (entry) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: swatchSize, + height: swatchSize, + decoration: BoxDecoration( + color: entry.color, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 6), + Text(entry.label, style: textStyle), + ], + ), + ) + .toList(), ); - - final painters = entries.map((entry) { - final painter = TextPainter( - text: TextSpan(text: entry.label, style: textStyle), - textDirection: TextDirection.ltr, - )..layout(); - return painter; - }).toList(); - - final maxTextWidth = painters.map((p) => p.width).fold(0, math.max); - - final legendWidth = - legendPadding * 2 + swatchSize + swatchTextGap + maxTextWidth; - - final legendHeight = - legendPadding * 2 + - entries.length * swatchSize + - (entries.length - 1) * entrySpacing; - - final legendRect = RRect.fromLTRBR( - legendX, - legendY, - legendX + legendWidth, - legendY + legendHeight, - const Radius.circular(10), - ); - - canvas.drawRRect( - legendRect, - Paint()..color = const Color.fromARGB(90, 0, 0, 0), - ); - - var yOffset = legendY + legendPadding; - for (int i = 0; i < entries.length; i++) { - final entry = entries[i]; - final painter = painters[i]; - final swatchRect = Rect.fromLTWH( - legendX + legendPadding, - yOffset, - swatchSize, - swatchSize, - ); - canvas.drawRect(swatchRect, Paint()..color = entry.color); - - painter.paint( - canvas, - Offset( - swatchRect.right + swatchTextGap, - yOffset + (swatchSize - painter.height) / 2, - ), - ); - - yOffset += swatchSize + entrySpacing; - } } } From ec14870aeda7607b2d097224922c23b85e195474 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:42:30 -0500 Subject: [PATCH 178/421] Update after upstream merged other commits --- lib/l10n/app_bg.arb | 20 +++- lib/l10n/app_de.arb | 20 +++- lib/l10n/app_es.arb | 20 +++- lib/l10n/app_fr.arb | 20 +++- lib/l10n/app_it.arb | 20 +++- lib/l10n/app_localizations_bg.dart | 14 +-- lib/l10n/app_localizations_de.dart | 14 +-- lib/l10n/app_localizations_es.dart | 14 +-- lib/l10n/app_localizations_fr.dart | 14 +-- lib/l10n/app_localizations_it.dart | 14 +-- lib/l10n/app_localizations_nl.dart | 14 +-- lib/l10n/app_localizations_pl.dart | 14 +-- lib/l10n/app_localizations_pt.dart | 14 +-- lib/l10n/app_localizations_ru.dart | 14 +-- lib/l10n/app_localizations_sk.dart | 14 +-- lib/l10n/app_localizations_sl.dart | 14 +-- lib/l10n/app_localizations_sv.dart | 14 +-- lib/l10n/app_localizations_uk.dart | 14 +-- lib/l10n/app_localizations_zh.dart | 14 +-- 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 +++- untranslated.json | 142 +---------------------------- 29 files changed, 351 insertions(+), 267 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index e9f46c6..c245101 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1718,5 +1718,21 @@ "losPointName": "Име на точката", "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", - "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиохоризонт", + "losLegendLosBeam": "Линия на видимост", + "losLegendTerrain": "Терен", + "losFrequencyLabel": "Честота", + "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", + "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението умножава 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, за да достигне k приблизително {kFactor} за текущата лента {frequencyMHz} MHz, което определя извитата граница на радиохоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index bdea574..1f67898 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1746,5 +1746,21 @@ "losPointName": "Punktname", "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", - "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Funkhorizont", + "losLegendLosBeam": "Sichtlinie", + "losLegendTerrain": "Gelände", + "losFrequencyLabel": "Frequenz", + "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", + "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz multipliziert die Berechnung 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, um k etwa {kFactor} für das aktuelle Band {frequencyMHz} MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 99db15d..9721f6b 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1746,5 +1746,21 @@ "losPointName": "Nombre del punto", "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", - "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte radioeléctrico", + "losLegendLosBeam": "Línea de visión", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frecuencia", + "losFrequencyInfoTooltip": "Ver detalles del cálculo", + "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", + "losFrequencyDialogDescription": "Partiendo de k={baselineK} a {baselineFreq} MHz, el cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para alcanzar k aprox {kFactor} para la banda actual {frequencyMHz} MHz, lo que define el límite curvo del horizonte radioeléctrico.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bc82195..c6d5de3 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nom du point", "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", - "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizon radio", + "losLegendLosBeam": "Ligne de visée", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Fréquence", + "losFrequencyInfoTooltip": "Voir les détails du calcul", + "losFrequencyDialogTitle": "Calcul de l’horizon radio", + "losFrequencyDialogDescription": "En partant de k={baselineK} à {baselineFreq} MHz, le calcul multiplie 0.15 × (frequency − {baselineFreq}) / {baselineFreq} pour atteindre k env {kFactor} pour la bande actuelle {frequencyMHz} MHz, ce qui définit la limite courbe de l’horizon radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index fe4bffc..a51cb6b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nome del punto", "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", - "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Orizzonte radio", + "losLegendLosBeam": "Linea di vista", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequenza", + "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", + "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo moltiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} per raggiungere k circa {kFactor} per la banda corrente {frequencyMHz} MHz, che definisce il limite curvo dell’orizzonte radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 7da0714..52c4246 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2868,22 +2868,22 @@ class AppLocalizationsBg extends AppLocalizations { 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Радиохоризонт'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Линия на видимост'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Терен'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Честота'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта'; @override String losFrequencyDialogDescription( @@ -2892,7 +2892,7 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението умножава 0.15 × (frequency − $baselineFreq) / $baselineFreq, за да достигне k приблизително $kFactor за текущата лента $frequencyMHz MHz, което определя извитата граница на радиохоризонта.'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b81565c..e5eb247 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2874,22 +2874,22 @@ class AppLocalizationsDe extends AppLocalizations { String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Funkhorizont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Sichtlinie'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Gelände'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequenz'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts'; @override String losFrequencyDialogDescription( @@ -2898,7 +2898,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz multipliziert die Berechnung 0.15 × (frequency − $baselineFreq) / $baselineFreq, um k etwa $kFactor für das aktuelle Band $frequencyMHz MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 971d9d3..f411b18 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2868,22 +2868,22 @@ class AppLocalizationsEs extends AppLocalizations { 'Datos de elevación: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Línea de visión'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frecuencia'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico'; @override String losFrequencyDialogDescription( @@ -2892,7 +2892,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Partiendo de k=$baselineK a $baselineFreq MHz, el cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para alcanzar k aprox $kFactor para la banda actual $frequencyMHz MHz, lo que define el límite curvo del horizonte radioeléctrico.'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1627987..2269ea1 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2880,25 +2880,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losElevationAttribution => - 'Données d\'altitude : Open-Meteo (CC BY 4.0)'; + 'Données d’altitude : Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horizon radio'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Ligne de visée'; @override String get losLegendTerrain => 'Terrain'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Fréquence'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; @override String losFrequencyDialogDescription( @@ -2907,7 +2907,7 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'En partant de k=$baselineK à $baselineFreq MHz, le calcul multiplie 0.15 × (frequency − $baselineFreq) / $baselineFreq pour atteindre k env $kFactor pour la bande actuelle $frequencyMHz MHz, ce qui définit la limite courbe de l’horizon radio.'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index dccc31a..91d7c35 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2868,22 +2868,22 @@ class AppLocalizationsIt extends AppLocalizations { 'Dati di elevazione: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Orizzonte radio'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linea di vista'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequenza'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; @override String losFrequencyDialogDescription( @@ -2892,7 +2892,7 @@ class AppLocalizationsIt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo moltiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq per raggiungere k circa $kFactor per la banda corrente $frequencyMHz MHz, che definisce il limite curvo dell’orizzonte radio.'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index b02ffa7..8e03b53 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2858,22 +2858,22 @@ class AppLocalizationsNl extends AppLocalizations { 'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Radiohorizon'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Zichtlijn'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terrein'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequentie'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Bekijk details van de berekening'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Berekening van de radiohorizon'; @override String losFrequencyDialogDescription( @@ -2882,7 +2882,7 @@ class AppLocalizationsNl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Uitgaande van k=$baselineK bij $baselineFreq MHz vermenigvuldigt de berekening 0.15 × (frequency − $baselineFreq) / $baselineFreq om k ongeveer $kFactor te bereiken voor de huidige band $frequencyMHz MHz, wat de gebogen radiohorizon-grens definieert.'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index b8e2705..29bbbc4 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2864,22 +2864,22 @@ class AppLocalizationsPl extends AppLocalizations { 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horyzont radiowy'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linia widoczności'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Teren'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Częstotliwość'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego'; @override String losFrequencyDialogDescription( @@ -2888,7 +2888,7 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenie mnoży 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby osiągnąć k około $kFactor dla bieżącego pasma $frequencyMHz MHz, co definiuje zakrzywioną granicę horyzontu radiowego.'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b492c33..e3673f5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2867,22 +2867,22 @@ class AppLocalizationsPt extends AppLocalizations { 'Dados de elevação: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horizonte de rádio'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linha de visada'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequência'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; @override String losFrequencyDialogDescription( @@ -2891,7 +2891,7 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Partindo de k=$baselineK a $baselineFreq MHz, o cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para atingir k aprox $kFactor para a banda atual $frequencyMHz MHz, o que define o limite curvo do horizonte de rádio.'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 383bf09..44ccf7c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2870,22 +2870,22 @@ class AppLocalizationsRu extends AppLocalizations { 'Данные о высоте: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Радиогоризонт'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Линия прямой видимости'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Рельеф'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Расчёт радиогоризонта'; @override String losFrequencyDialogDescription( @@ -2894,7 +2894,7 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Начиная с k=$baselineK при $baselineFreq MHz, расчёт умножает 0.15 × (frequency − $baselineFreq) / $baselineFreq, чтобы получить k примерно $kFactor для текущего диапазона $frequencyMHz MHz, что определяет изогнутую границу радиогоризонта.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 474cf32..39c277b 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2852,22 +2852,22 @@ class AppLocalizationsSk extends AppLocalizations { 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Rádiový horizont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Priama viditeľnosť'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terén'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frekvencia'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; @override String losFrequencyDialogDescription( @@ -2876,7 +2876,7 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Vychádzajúc z k=$baselineK pri $baselineFreq MHz výpočet násobí 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby dosiahol k približne $kFactor pre aktuálne pásmo $frequencyMHz MHz, čo definuje zakrivenú hranicu rádiového horizontu.'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 6662bc1..db973e8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2855,22 +2855,22 @@ class AppLocalizationsSl extends AppLocalizations { 'Podatki o višini: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Radijski horizont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linija vidnosti'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Teren'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frekvenca'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; @override String losFrequencyDialogDescription( @@ -2879,7 +2879,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Začenši z k=$baselineK pri $baselineFreq MHz izračun množi 0.15 × (frequency − $baselineFreq) / $baselineFreq, da doseže k približno $kFactor za trenutni pas $frequencyMHz MHz, kar določa ukrivljeno mejo radijskega horizonta.'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 35a532b..8a3f29f 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2838,22 +2838,22 @@ class AppLocalizationsSv extends AppLocalizations { String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Radiohorisont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Siktlinje'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terräng'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frekvens'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten'; @override String losFrequencyDialogDescription( @@ -2862,7 +2862,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Med utgångspunkt från k=$baselineK vid $baselineFreq MHz multiplicerar beräkningen 0.15 × (frequency − $baselineFreq) / $baselineFreq för att nå k cirka $kFactor för det aktuella bandet $frequencyMHz MHz, vilket definierar den krökta radiohorisontgränsen.'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 4e7130b..1d38ca0 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2878,22 +2878,22 @@ class AppLocalizationsUk extends AppLocalizations { 'Дані про висоту: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Радіогоризонт'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Лінія прямої видимості'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Рельєф'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту'; @override String losFrequencyDialogDescription( @@ -2902,7 +2902,7 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Починаючи з k=$baselineK при $baselineFreq MHz, розрахунок множить 0.15 × (frequency − $baselineFreq) / $baselineFreq, щоб досягти k приблизно $kFactor для поточного діапазону $frequencyMHz MHz, що визначає вигнуту межу радіогоризонту.'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2ecf38e..fa1a34a 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2716,22 +2716,22 @@ class AppLocalizationsZh extends AppLocalizations { String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => '无线电地平线'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => '视距波束'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => '地形'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => '频率'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => '查看计算详情'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => '无线电地平线计算'; @override String losFrequencyDialogDescription( @@ -2740,7 +2740,7 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return '从 $baselineFreq MHz 的 k=$baselineK 开始,计算将 0.15 × (frequency − $baselineFreq) / $baselineFreq 相乘,以在当前频段 $frequencyMHz MHz 下得到约 k=$kFactor,从而定义弯曲的无线电地平线边界。'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 2f39fdf..f47a3b9 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1718,5 +1718,21 @@ "losPointName": "Puntnaam", "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", - "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorizon", + "losLegendLosBeam": "Zichtlijn", + "losLegendTerrain": "Terrein", + "losFrequencyLabel": "Frequentie", + "losFrequencyInfoTooltip": "Bekijk details van de berekening", + "losFrequencyDialogTitle": "Berekening van de radiohorizon", + "losFrequencyDialogDescription": "Uitgaande van k={baselineK} bij {baselineFreq} MHz vermenigvuldigt de berekening 0.15 × (frequency − {baselineFreq}) / {baselineFreq} om k ongeveer {kFactor} te bereiken voor de huidige band {frequencyMHz} MHz, wat de gebogen radiohorizon-grens definieert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0432f8f..3b56900 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nazwa punktu", "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", - "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horyzont radiowy", + "losLegendLosBeam": "Linia widoczności", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Częstotliwość", + "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", + "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenie mnoży 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby osiągnąć k około {kFactor} dla bieżącego pasma {frequencyMHz} MHz, co definiuje zakrzywioną granicę horyzontu radiowego.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 01c5a83..a058bff 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nome do ponto", "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", - "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte de rádio", + "losLegendLosBeam": "Linha de visada", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequência", + "losFrequencyInfoTooltip": "Ver detalhes do cálculo", + "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", + "losFrequencyDialogDescription": "Partindo de k={baselineK} a {baselineFreq} MHz, o cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para atingir k aprox {kFactor} para a banda atual {frequencyMHz} MHz, o que define o limite curvo do horizonte de rádio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b8a20d9..f439754 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -958,5 +958,21 @@ "losPointName": "Имя точки", "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", - "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиогоризонт", + "losLegendLosBeam": "Линия прямой видимости", + "losLegendTerrain": "Рельеф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Просмотреть детали расчёта", + "losFrequencyDialogTitle": "Расчёт радиогоризонта", + "losFrequencyDialogDescription": "Начиная с k={baselineK} при {baselineFreq} MHz, расчёт умножает 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, чтобы получить k примерно {kFactor} для текущего диапазона {frequencyMHz} MHz, что определяет изогнутую границу радиогоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 3245282..0801b8d 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1718,5 +1718,21 @@ "losPointName": "Názov bodu", "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", - "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádiový horizont", + "losLegendLosBeam": "Priama viditeľnosť", + "losLegendTerrain": "Terén", + "losFrequencyLabel": "Frekvencia", + "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", + "losFrequencyDialogTitle": "Výpočet rádiového horizontu", + "losFrequencyDialogDescription": "Vychádzajúc z k={baselineK} pri {baselineFreq} MHz výpočet násobí 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby dosiahol k približne {kFactor} pre aktuálne pásmo {frequencyMHz} MHz, čo definuje zakrivenú hranicu rádiového horizontu.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index c560c31..d7e9ab3 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1718,5 +1718,21 @@ "losPointName": "Ime točke", "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", - "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radijski horizont", + "losLegendLosBeam": "Linija vidnosti", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Frekvenca", + "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", + "losFrequencyDialogTitle": "Izračun radijskega horizonta", + "losFrequencyDialogDescription": "Začenši z k={baselineK} pri {baselineFreq} MHz izračun množi 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, da doseže k približno {kFactor} za trenutni pas {frequencyMHz} MHz, kar določa ukrivljeno mejo radijskega horizonta.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b93c5ca..dcb4069 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1718,5 +1718,21 @@ "losPointName": "Punktnamn", "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", - "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorisont", + "losLegendLosBeam": "Siktlinje", + "losLegendTerrain": "Terräng", + "losFrequencyLabel": "Frekvens", + "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", + "losFrequencyDialogTitle": "Beräkning av radiohorisonten", + "losFrequencyDialogDescription": "Med utgångspunkt från k={baselineK} vid {baselineFreq} MHz multiplicerar beräkningen 0.15 × (frequency − {baselineFreq}) / {baselineFreq} för att nå k cirka {kFactor} för det aktuella bandet {frequencyMHz} MHz, vilket definierar den krökta radiohorisontgränsen.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 235e4ed..d339ec2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1718,5 +1718,21 @@ "losPointName": "Назва точки", "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", - "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радіогоризонт", + "losLegendLosBeam": "Лінія прямої видимості", + "losLegendTerrain": "Рельєф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", + "losFrequencyDialogTitle": "Розрахунок радіогоризонту", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} при {baselineFreq} MHz, розрахунок множить 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, щоб досягти k приблизно {kFactor} для поточного діапазону {frequencyMHz} MHz, що визначає вигнуту межу радіогоризонту.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 72f48ad..626cbac 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1718,5 +1718,21 @@ "losPointName": "点名称", "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", - "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "无线电地平线", + "losLegendLosBeam": "视距波束", + "losLegendTerrain": "地形", + "losFrequencyLabel": "频率", + "losFrequencyInfoTooltip": "查看计算详情", + "losFrequencyDialogTitle": "无线电地平线计算", + "losFrequencyDialogDescription": "从 {baselineFreq} MHz 的 k={baselineK} 开始,计算将 0.15 × (frequency − {baselineFreq}) / {baselineFreq} 相乘,以在当前频段 {frequencyMHz} MHz 下得到约 k={kFactor},从而定义弯曲的无线电地平线边界。", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/untranslated.json b/untranslated.json index f9183cb..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,141 +1 @@ -{ - "bg": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "de": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "es": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "fr": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "it": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "nl": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "pl": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "pt": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "ru": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "sk": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "sl": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "sv": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "uk": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "zh": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ] -} +{} \ No newline at end of file From 78f1a7b28e5e79fdeb5dae7e44fa4f85470d775b Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:12:32 -0500 Subject: [PATCH 179/421] fix: normalize stored frequency --- lib/screens/line_of_sight_map_screen.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 785dfe5..efbdf34 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -993,11 +993,9 @@ class _LineOfSightMapScreenState extends State { ); } - double? _normalizeFrequencyMHz(int? frequencyHz) { - if (frequencyHz == null || frequencyHz <= 0) return null; - if (frequencyHz >= 1000000) return frequencyHz / 1e6; - if (frequencyHz >= 1000) return frequencyHz / 1e3; - return frequencyHz.toDouble(); + double? _normalizeFrequencyMHz(int? frequencyKHz) { + if (frequencyKHz == null || frequencyKHz <= 0) return null; + return frequencyKHz / 1000.0; } } From e2585c099289157145e98c1cfe09d2a62eb47a52 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:44:21 -0500 Subject: [PATCH 180/421] fix: reduce rebuilds in los panel --- lib/screens/line_of_sight_map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index efbdf34..c8b5a88 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -428,7 +428,7 @@ class _LineOfSightMapScreenState extends State { Widget _buildControlPanel(bool isImperial) { _sanitizeSelection(); final segment = _primarySegmentResult(); - final connector = context.watch(); + final connector = context.read(); final reportedFrequencyMHz = _normalizeFrequencyMHz( connector.currentFreqHz, ); From ea2f35ec2ebd52e3043fa8f04532eb68618e07f9 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:59:18 -0500 Subject: [PATCH 181/421] fix: keep los metadata on failure --- lib/services/line_of_sight_service.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index 61e9e27..7f056c8 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -50,13 +50,13 @@ class LineOfSightResult { const LineOfSightResult.error({ required this.totalDistanceMeters, required this.errorMessage, + this.usedKFactor = 4.0 / 3.0, + this.frequencyMHz, }) : hasData = false, isClear = false, maxObstructionMeters = 0, firstObstructionDistanceMeters = null, - samples = const [], - usedKFactor = 4.0 / 3.0, - frequencyMHz = null; + samples = const []; } class LineOfSightPathSegment { @@ -203,6 +203,8 @@ class LineOfSightService { return LineOfSightResult.error( totalDistanceMeters: totalDistanceMeters, errorMessage: errorElevationUnavailable, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } @@ -227,9 +229,11 @@ class LineOfSightService { double obstructionToleranceMeters = 0.0, }) { if (points.length < 2 || elevations.length != points.length) { - return const LineOfSightResult.error( + return LineOfSightResult.error( totalDistanceMeters: 0, errorMessage: errorInvalidInput, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } From 74e29a6c0f6f8f2e257de6f07134bb76468e1185 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:12:04 -0500 Subject: [PATCH 182/421] fix: clamp los profile bounds --- lib/l10n/app_en.arb | 2 +- lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- lib/screens/line_of_sight_map_screen.dart | 14 ++++++++------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 99ce9e2..49f2d34 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1673,7 +1673,7 @@ "losFrequencyLabel": "Frequency", "losFrequencyInfoTooltip": "View calculation details", "losFrequencyDialogTitle": "Radio horizon calculation", - "losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.", + "losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1996ad6..5f0cd5e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5043,7 +5043,7 @@ abstract class AppLocalizations { /// Explain how the calculation uses the baseline frequency and derived k-factor. /// /// In en, this message translates to: - /// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'** + /// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'** String losFrequencyDialogDescription( double baselineK, double baselineFreq, diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5c7cf36..98fee85 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2849,7 +2849,7 @@ class AppLocalizationsEn extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; } @override diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index c8b5a88..be164e3 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -1065,13 +1065,15 @@ class _LosProfilePainter extends CustomPainter { samples.last.terrainMeters, ); - double distanceForCanvasX(double x) => - ((x - horizontalPadding) / chartWidth) * maxDist; + double distanceForCanvasX(double x) { + final normalized = ((x - horizontalPadding) / chartWidth).clamp(0.0, 1.0); + return normalized * maxDist; + } - double elevationToPixel(double elevation) => - size.height - - verticalPadding - - ((elevation - minY) / ySpan) * chartHeight; + double elevationToPixel(double elevation) { + final normalized = ((elevation - minY) / ySpan).clamp(0.0, 1.0); + return size.height - verticalPadding - normalized * chartHeight; + } double extrapolateTerrain(double distance, bool isLeft) { final samplesForSlope = isLeft From 1a9b7b0d55597d8302d44665527678faa39b1a54 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:18:02 -0500 Subject: [PATCH 183/421] chore: remove 0.15 text --- translate_arb.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 translate_arb.py diff --git a/translate_arb.py b/translate_arb.py new file mode 100644 index 0000000..737c059 --- /dev/null +++ b/translate_arb.py @@ -0,0 +1,104 @@ +import json +import time +from pathlib import Path + +import requests + + +SOURCE_PATH = Path("lib/l10n/app_en.arb") +L10N_DIR = Path("lib/l10n") +API_URL = "https://libretranslate.de/translate" +DELAY_SECONDS = 0.5 + + +def load_json(path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def save_json(path: Path, data: dict) -> None: + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + +def translate_text(text: str, target_locale: str) -> str | None: + payload = { + "q": text, + "source": "en", + "target": target_locale, + "format": "text", + } + try: + response = requests.post(API_URL, json=payload, timeout=30) + response.raise_for_status() + translated = response.json().get("translatedText") + return translated + except requests.RequestException as exc: + print(f"[{target_locale}] Translation failed: {exc}") + except ValueError: + print(f"[{target_locale}] Invalid response from translation service") + return None + + +def translate_locale( + locale: str, + target_path: Path, + english_data: dict, +) -> None: + print(f"Processing locale '{locale}'") + target_data = load_json(target_path) + updated = False + missing_keys = [] + + for key, value in english_data.items(): + if key.startswith("@"): + continue + if not isinstance(value, str): + continue + target_value = target_data.get(key) + if target_value is None or (isinstance(target_value, str) and target_value.strip() == ""): + missing_keys.append((key, value)) + + if not missing_keys: + print(f" -> No missing entries for {locale}") + return + + print(f" -> Translating {len(missing_keys)} entries") + for key, english_text in missing_keys: + time.sleep(DELAY_SECONDS) + translated = translate_text(english_text, locale) + if translated: + target_data[key] = translated + updated = True + else: + print(f" → [{locale}] Keeping English text for {key}") + target_data[key] = english_text + + metadata_key = f"@{key}" + if metadata_key not in target_data: + target_data[metadata_key] = {"description": ""} + updated = True + + if updated: + save_json(target_path, target_data) + print(f" → Saved translations for {locale}") + else: + print(f" → No updates written for {locale}") + + +def main() -> None: + english_data = load_json(SOURCE_PATH) + if not english_data: + print("English source not found or empty") + return + + locales = sorted(L10N_DIR.glob("app_*.arb")) + for path in locales: + if path.name == SOURCE_PATH.name: + continue + locale = path.name.split("_", 1)[1].split(".")[0] + translate_locale(locale, path, english_data) + + +if __name__ == "__main__": + main() From 2188b4972689e058cd35c7ded607f6a45c9dd8b5 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:55:43 -0500 Subject: [PATCH 184/421] fix: refresh los localization --- lib/l10n/app_bg.arb | 20 ++++++++++++++------ lib/l10n/app_de.arb | 20 ++++++++++++++------ lib/l10n/app_en.arb | 18 +++++++++++++----- lib/l10n/app_es.arb | 20 ++++++++++++++------ lib/l10n/app_fr.arb | 20 ++++++++++++++------ lib/l10n/app_it.arb | 20 ++++++++++++++------ lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- 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 ++++++++++++++------ 29 files changed, 223 insertions(+), 103 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c245101..83c35e7 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Честота", "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", - "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението умножава 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, за да достигне k приблизително {kFactor} за текущата лента {frequencyMHz} MHz, което определя извитата граница на радиохоризонта.", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {frequencyMHz} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 1f67898..e5243bf 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1753,14 +1753,22 @@ "losFrequencyLabel": "Frequenz", "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", - "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz multipliziert die Berechnung 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, um k etwa {kFactor} für das aktuelle Band {frequencyMHz} MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {frequencyMHz} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 49f2d34..8f231e7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1677,10 +1677,18 @@ "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } }, "contacts_pathTrace": "Path Trace", @@ -1763,4 +1771,4 @@ "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", "snrIndicator_lastSeen": "Last seen" -} +} \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9721f6b..d0bd732 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1753,14 +1753,22 @@ "losFrequencyLabel": "Frecuencia", "losFrequencyInfoTooltip": "Ver detalles del cálculo", "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", - "losFrequencyDialogDescription": "Partiendo de k={baselineK} a {baselineFreq} MHz, el cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para alcanzar k aprox {kFactor} para la banda actual {frequencyMHz} MHz, lo que define el límite curvo del horizonte radioeléctrico.", + "losFrequencyDialogDescription": "A partir de k={baselineK} en {frequencyMHz} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c6d5de3..81cffc3 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Fréquence", "losFrequencyInfoTooltip": "Voir les détails du calcul", "losFrequencyDialogTitle": "Calcul de l’horizon radio", - "losFrequencyDialogDescription": "En partant de k={baselineK} à {baselineFreq} MHz, le calcul multiplie 0.15 × (frequency − {baselineFreq}) / {baselineFreq} pour atteindre k env {kFactor} pour la bande actuelle {frequencyMHz} MHz, ce qui définit la limite courbe de l’horizon radio.", + "losFrequencyDialogDescription": "À partir de k={baselineK} à {frequencyMHz} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a51cb6b..25e3918 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frequenza", "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", - "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo moltiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} per raggiungere k circa {kFactor} per la banda corrente {frequencyMHz} MHz, che definisce il limite curvo dell’orizzonte radio.", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {frequencyMHz} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 52c4246..c300e5e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението умножава 0.15 × (frequency − $baselineFreq) / $baselineFreq, за да достигне k приблизително $kFactor за текущата лента $frequencyMHz MHz, което определя извитата граница на радиохоризонта.'; + return 'Започвайки от k=$baselineK при $frequencyMHz MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e5eb247..a6107e5 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2898,7 +2898,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Ausgehend von k=$baselineK bei $baselineFreq MHz multipliziert die Berechnung 0.15 × (frequency − $baselineFreq) / $baselineFreq, um k etwa $kFactor für das aktuelle Band $frequencyMHz MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.'; + return 'Ausgehend von k=$baselineK bei $frequencyMHz MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index f411b18..8bd50c7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partiendo de k=$baselineK a $baselineFreq MHz, el cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para alcanzar k aprox $kFactor para la banda actual $frequencyMHz MHz, lo que define el límite curvo del horizonte radioeléctrico.'; + return 'A partir de k=$baselineK en $frequencyMHz MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 2269ea1..d22ede1 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2907,7 +2907,7 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'En partant de k=$baselineK à $baselineFreq MHz, le calcul multiplie 0.15 × (frequency − $baselineFreq) / $baselineFreq pour atteindre k env $kFactor pour la bande actuelle $frequencyMHz MHz, ce qui définit la limite courbe de l’horizon radio.'; + return 'À partir de k=$baselineK à $frequencyMHz MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 91d7c35..0135108 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsIt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo moltiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq per raggiungere k circa $kFactor per la banda corrente $frequencyMHz MHz, che definisce il limite curvo dell’orizzonte radio.'; + return 'Partendo da k=$baselineK a $frequencyMHz MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 8e03b53..3e9bc0a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2882,7 +2882,7 @@ class AppLocalizationsNl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Uitgaande van k=$baselineK bij $baselineFreq MHz vermenigvuldigt de berekening 0.15 × (frequency − $baselineFreq) / $baselineFreq om k ongeveer $kFactor te bereiken voor de huidige band $frequencyMHz MHz, wat de gebogen radiohorizon-grens definieert.'; + return 'Beginnend met k=$baselineK bij $frequencyMHz MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 29bbbc4..c0e75fd 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2888,7 +2888,7 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenie mnoży 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby osiągnąć k około $kFactor dla bieżącego pasma $frequencyMHz MHz, co definiuje zakrzywioną granicę horyzontu radiowego.'; + return 'Zaczynając od k=$baselineK przy $frequencyMHz MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index e3673f5..de53c86 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2891,7 +2891,7 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partindo de k=$baselineK a $baselineFreq MHz, o cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para atingir k aprox $kFactor para a banda atual $frequencyMHz MHz, o que define o limite curvo do horizonte de rádio.'; + return 'Começando em k=$baselineK em $frequencyMHz MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 44ccf7c..c32e663 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2894,7 +2894,7 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Начиная с k=$baselineK при $baselineFreq MHz, расчёт умножает 0.15 × (frequency − $baselineFreq) / $baselineFreq, чтобы получить k примерно $kFactor для текущего диапазона $frequencyMHz MHz, что определяет изогнутую границу радиогоризонта.'; + return 'Начиная с k=$baselineK на частоте $frequencyMHz МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 39c277b..2326e11 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2876,7 +2876,7 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Vychádzajúc z k=$baselineK pri $baselineFreq MHz výpočet násobí 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby dosiahol k približne $kFactor pre aktuálne pásmo $frequencyMHz MHz, čo definuje zakrivenú hranicu rádiového horizontu.'; + return 'Počnúc od k=$baselineK pri $frequencyMHz MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index db973e8..181a894 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2879,7 +2879,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Začenši z k=$baselineK pri $baselineFreq MHz izračun množi 0.15 × (frequency − $baselineFreq) / $baselineFreq, da doseže k približno $kFactor za trenutni pas $frequencyMHz MHz, kar določa ukrivljeno mejo radijskega horizonta.'; + return 'Začenši od k=$baselineK pri $frequencyMHz MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 8a3f29f..a71a8a5 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2862,7 +2862,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Med utgångspunkt från k=$baselineK vid $baselineFreq MHz multiplicerar beräkningen 0.15 × (frequency − $baselineFreq) / $baselineFreq för att nå k cirka $kFactor för det aktuella bandet $frequencyMHz MHz, vilket definierar den krökta radiohorisontgränsen.'; + return 'Med start från k=$baselineK vid $frequencyMHz MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1d38ca0..ffdf835 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2902,7 +2902,7 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Починаючи з k=$baselineK при $baselineFreq MHz, розрахунок множить 0.15 × (frequency − $baselineFreq) / $baselineFreq, щоб досягти k приблизно $kFactor для поточного діапазону $frequencyMHz MHz, що визначає вигнуту межу радіогоризонту.'; + return 'Починаючи з k=$baselineK на $frequencyMHz МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index fa1a34a..1842466 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2740,7 +2740,7 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return '从 $baselineFreq MHz 的 k=$baselineK 开始,计算将 0.15 × (frequency − $baselineFreq) / $baselineFreq 相乘,以在当前频段 $frequencyMHz MHz 下得到约 k=$kFactor,从而定义弯曲的无线电地平线边界。'; + return '从 $frequencyMHz MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index f47a3b9..17e4b3c 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frequentie", "losFrequencyInfoTooltip": "Bekijk details van de berekening", "losFrequencyDialogTitle": "Berekening van de radiohorizon", - "losFrequencyDialogDescription": "Uitgaande van k={baselineK} bij {baselineFreq} MHz vermenigvuldigt de berekening 0.15 × (frequency − {baselineFreq}) / {baselineFreq} om k ongeveer {kFactor} te bereiken voor de huidige band {frequencyMHz} MHz, wat de gebogen radiohorizon-grens definieert.", + "losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {frequencyMHz} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3b56900..db45f75 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Częstotliwość", "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", - "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenie mnoży 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby osiągnąć k około {kFactor} dla bieżącego pasma {frequencyMHz} MHz, co definiuje zakrzywioną granicę horyzontu radiowego.", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {frequencyMHz} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index a058bff..c3557e5 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frequência", "losFrequencyInfoTooltip": "Ver detalhes do cálculo", "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", - "losFrequencyDialogDescription": "Partindo de k={baselineK} a {baselineFreq} MHz, o cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para atingir k aprox {kFactor} para a banda atual {frequencyMHz} MHz, o que define o limite curvo do horizonte de rádio.", + "losFrequencyDialogDescription": "Começando em k={baselineK} em {frequencyMHz} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index f439754..7c8a325 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -965,14 +965,22 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Просмотреть детали расчёта", "losFrequencyDialogTitle": "Расчёт радиогоризонта", - "losFrequencyDialogDescription": "Начиная с k={baselineK} при {baselineFreq} MHz, расчёт умножает 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, чтобы получить k примерно {kFactor} для текущего диапазона {frequencyMHz} MHz, что определяет изогнутую границу радиогоризонта.", + "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {frequencyMHz} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 0801b8d..c3d7547 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frekvencia", "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", "losFrequencyDialogTitle": "Výpočet rádiového horizontu", - "losFrequencyDialogDescription": "Vychádzajúc z k={baselineK} pri {baselineFreq} MHz výpočet násobí 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby dosiahol k približne {kFactor} pre aktuálne pásmo {frequencyMHz} MHz, čo definuje zakrivenú hranicu rádiového horizontu.", + "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {frequencyMHz} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index d7e9ab3..1350f79 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frekvenca", "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", "losFrequencyDialogTitle": "Izračun radijskega horizonta", - "losFrequencyDialogDescription": "Začenši z k={baselineK} pri {baselineFreq} MHz izračun množi 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, da doseže k približno {kFactor} za trenutni pas {frequencyMHz} MHz, kar določa ukrivljeno mejo radijskega horizonta.", + "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {frequencyMHz} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index dcb4069..2394f87 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frekvens", "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", "losFrequencyDialogTitle": "Beräkning av radiohorisonten", - "losFrequencyDialogDescription": "Med utgångspunkt från k={baselineK} vid {baselineFreq} MHz multiplicerar beräkningen 0.15 × (frequency − {baselineFreq}) / {baselineFreq} för att nå k cirka {kFactor} för det aktuella bandet {frequencyMHz} MHz, vilket definierar den krökta radiohorisontgränsen.", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {frequencyMHz} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index d339ec2..5ead3a2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", "losFrequencyDialogTitle": "Розрахунок радіогоризонту", - "losFrequencyDialogDescription": "Починаючи з k={baselineK} при {baselineFreq} MHz, розрахунок множить 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, щоб досягти k приблизно {kFactor} для поточного діапазону {frequencyMHz} MHz, що визначає вигнуту межу радіогоризонту.", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {frequencyMHz} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 626cbac..5ecdebf 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "频率", "losFrequencyInfoTooltip": "查看计算详情", "losFrequencyDialogTitle": "无线电地平线计算", - "losFrequencyDialogDescription": "从 {baselineFreq} MHz 的 k={baselineK} 开始,计算将 0.15 × (frequency − {baselineFreq}) / {baselineFreq} 相乘,以在当前频段 {frequencyMHz} MHz 下得到约 k={kFactor},从而定义弯曲的无线电地平线边界。", + "losFrequencyDialogDescription": "从 {frequencyMHz} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file From ddc87f3a274234d22c2c54c7c3717490a45929ec Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:14:00 -0500 Subject: [PATCH 185/421] chore: remove translation script --- translate_arb.py | 104 ----------------------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 translate_arb.py diff --git a/translate_arb.py b/translate_arb.py deleted file mode 100644 index 737c059..0000000 --- a/translate_arb.py +++ /dev/null @@ -1,104 +0,0 @@ -import json -import time -from pathlib import Path - -import requests - - -SOURCE_PATH = Path("lib/l10n/app_en.arb") -L10N_DIR = Path("lib/l10n") -API_URL = "https://libretranslate.de/translate" -DELAY_SECONDS = 0.5 - - -def load_json(path: Path) -> dict: - if not path.exists(): - return {} - return json.loads(path.read_text(encoding="utf-8")) - - -def save_json(path: Path, data: dict) -> None: - path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") - - -def translate_text(text: str, target_locale: str) -> str | None: - payload = { - "q": text, - "source": "en", - "target": target_locale, - "format": "text", - } - try: - response = requests.post(API_URL, json=payload, timeout=30) - response.raise_for_status() - translated = response.json().get("translatedText") - return translated - except requests.RequestException as exc: - print(f"[{target_locale}] Translation failed: {exc}") - except ValueError: - print(f"[{target_locale}] Invalid response from translation service") - return None - - -def translate_locale( - locale: str, - target_path: Path, - english_data: dict, -) -> None: - print(f"Processing locale '{locale}'") - target_data = load_json(target_path) - updated = False - missing_keys = [] - - for key, value in english_data.items(): - if key.startswith("@"): - continue - if not isinstance(value, str): - continue - target_value = target_data.get(key) - if target_value is None or (isinstance(target_value, str) and target_value.strip() == ""): - missing_keys.append((key, value)) - - if not missing_keys: - print(f" -> No missing entries for {locale}") - return - - print(f" -> Translating {len(missing_keys)} entries") - for key, english_text in missing_keys: - time.sleep(DELAY_SECONDS) - translated = translate_text(english_text, locale) - if translated: - target_data[key] = translated - updated = True - else: - print(f" → [{locale}] Keeping English text for {key}") - target_data[key] = english_text - - metadata_key = f"@{key}" - if metadata_key not in target_data: - target_data[metadata_key] = {"description": ""} - updated = True - - if updated: - save_json(target_path, target_data) - print(f" → Saved translations for {locale}") - else: - print(f" → No updates written for {locale}") - - -def main() -> None: - english_data = load_json(SOURCE_PATH) - if not english_data: - print("English source not found or empty") - return - - locales = sorted(L10N_DIR.glob("app_*.arb")) - for path in locales: - if path.name == SOURCE_PATH.name: - continue - locale = path.name.split("_", 1)[1].split(".")[0] - translate_locale(locale, path, english_data) - - -if __name__ == "__main__": - main() From faefef14ff2a0f22c3a6db24cbd9783bd6939613 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:29:36 -0500 Subject: [PATCH 186/421] fix: restore baseline freq in los text --- lib/l10n/app_bg.arb | 4 ++-- lib/l10n/app_de.arb | 4 ++-- lib/l10n/app_es.arb | 4 ++-- lib/l10n/app_fr.arb | 4 ++-- lib/l10n/app_it.arb | 4 ++-- lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/l10n/app_nl.arb | 4 ++-- lib/l10n/app_pl.arb | 4 ++-- lib/l10n/app_pt.arb | 4 ++-- lib/l10n/app_ru.arb | 4 ++-- lib/l10n/app_sk.arb | 4 ++-- lib/l10n/app_sl.arb | 4 ++-- lib/l10n/app_sv.arb | 4 ++-- lib/l10n/app_uk.arb | 4 ++-- lib/l10n/app_zh.arb | 4 ++-- 28 files changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 83c35e7..b94b2cb 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Честота", "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", - "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {frequencyMHz} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5243bf..3963e31 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1753,7 +1753,7 @@ "losFrequencyLabel": "Frequenz", "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", - "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {frequencyMHz} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1771,4 +1771,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index d0bd732..d194093 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1753,7 +1753,7 @@ "losFrequencyLabel": "Frecuencia", "losFrequencyInfoTooltip": "Ver detalles del cálculo", "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", - "losFrequencyDialogDescription": "A partir de k={baselineK} en {frequencyMHz} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", + "losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1771,4 +1771,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 81cffc3..f3e9ea8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Fréquence", "losFrequencyInfoTooltip": "Voir les détails du calcul", "losFrequencyDialogTitle": "Calcul de l’horizon radio", - "losFrequencyDialogDescription": "À partir de k={baselineK} à {frequencyMHz} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", + "losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 25e3918..8b095f5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frequenza", "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", - "losFrequencyDialogDescription": "Partendo da k={baselineK} a {frequencyMHz} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index c300e5e..91e5a94 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Започвайки от k=$baselineK при $frequencyMHz MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a6107e5..4c591e5 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2898,7 +2898,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Ausgehend von k=$baselineK bei $frequencyMHz MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8bd50c7..b868aad 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'A partir de k=$baselineK en $frequencyMHz MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; + return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d22ede1..a939e09 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2907,7 +2907,7 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'À partir de k=$baselineK à $frequencyMHz MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; + return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 0135108..d4cda69 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsIt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partendo da k=$baselineK a $frequencyMHz MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.'; + return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3e9bc0a..5c38227 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2882,7 +2882,7 @@ class AppLocalizationsNl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Beginnend met k=$baselineK bij $frequencyMHz MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.'; + return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index c0e75fd..6f68786 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2888,7 +2888,7 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Zaczynając od k=$baselineK przy $frequencyMHz MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; + return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index de53c86..cedf400 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2891,7 +2891,7 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Começando em k=$baselineK em $frequencyMHz MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; + return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c32e663..d35b174 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2894,7 +2894,7 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Начиная с k=$baselineK на частоте $frequencyMHz МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; + return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 2326e11..c4f9a92 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2876,7 +2876,7 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Počnúc od k=$baselineK pri $frequencyMHz MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; + return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 181a894..a012ef1 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2879,7 +2879,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Začenši od k=$baselineK pri $frequencyMHz MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; + return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a71a8a5..abfd89c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2862,7 +2862,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Med start från k=$baselineK vid $frequencyMHz MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; + return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index ffdf835..c6b8ff2 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2902,7 +2902,7 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Починаючи з k=$baselineK на $frequencyMHz МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; + return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 1842466..5677c09 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2740,7 +2740,7 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return '从 $frequencyMHz MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; + return '从 $baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 17e4b3c..a94560b 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frequentie", "losFrequencyInfoTooltip": "Bekijk details van de berekening", "losFrequencyDialogTitle": "Berekening van de radiohorizon", - "losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {frequencyMHz} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.", + "losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index db45f75..2af1058 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Częstotliwość", "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", - "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {frequencyMHz} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index c3557e5..c9d3724 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frequência", "losFrequencyInfoTooltip": "Ver detalhes do cálculo", "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", - "losFrequencyDialogDescription": "Começando em k={baselineK} em {frequencyMHz} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", + "losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 7c8a325..e1a2066 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -965,7 +965,7 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Просмотреть детали расчёта", "losFrequencyDialogTitle": "Расчёт радиогоризонта", - "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {frequencyMHz} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", + "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -983,4 +983,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index c3d7547..34e5933 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frekvencia", "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", "losFrequencyDialogTitle": "Výpočet rádiového horizontu", - "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {frequencyMHz} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", + "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 1350f79..1371f97 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frekvenca", "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", "losFrequencyDialogTitle": "Izračun radijskega horizonta", - "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {frequencyMHz} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", + "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 2394f87..2bdaec4 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frekvens", "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", "losFrequencyDialogTitle": "Beräkning av radiohorisonten", - "losFrequencyDialogDescription": "Med start från k={baselineK} vid {frequencyMHz} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 5ead3a2..13d9362 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", "losFrequencyDialogTitle": "Розрахунок радіогоризонту", - "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {frequencyMHz} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5ecdebf..b2dc330 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "频率", "losFrequencyInfoTooltip": "查看计算详情", "losFrequencyDialogTitle": "无线电地平线计算", - "losFrequencyDialogDescription": "从 {frequencyMHz} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", + "losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} From 6065059241c3c884e559c45c4393db42a6615880 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:35:51 -0500 Subject: [PATCH 187/421] fix: keep los panel reactive --- lib/screens/line_of_sight_map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index be164e3..196cd2e 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -111,7 +111,7 @@ class _LineOfSightMapScreenState extends State { }); try { - final connector = context.read(); + final connector = context.watch(); final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz); final result = await _lineOfSightService.analyzePath( [start.point, end.point], From 0f17e2382cb2dea3176eae9372b23dfff5205d60 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:41:32 -0500 Subject: [PATCH 188/421] feat(chat): add global pinch-to-zoom text scaling via ChatTextScaleService --- lib/main.dart | 8 ++ lib/screens/channel_chat_screen.dart | 118 +++++++++++++-------- lib/screens/chat_screen.dart | 123 +++++++++++++--------- lib/services/chat_text_scale_service.dart | 45 ++++++++ lib/widgets/chat_zoom_wrapper.dart | 45 ++++++++ 5 files changed, 247 insertions(+), 92 deletions(-) create mode 100644 lib/services/chat_text_scale_service.dart create mode 100644 lib/widgets/chat_zoom_wrapper.dart diff --git a/lib/main.dart b/lib/main.dart index 3650a7e..5a11188 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'services/ble_debug_log_service.dart'; import 'services/app_debug_log_service.dart'; import 'services/background_service.dart'; import 'services/map_tile_cache_service.dart'; +import 'services/chat_text_scale_service.dart'; import 'storage/prefs_manager.dart'; import 'utils/app_logger.dart'; @@ -34,6 +35,7 @@ void main() async { final appDebugLogService = AppDebugLogService(); final backgroundService = BackgroundService(); final mapTileCacheService = MapTileCacheService(); + final chatTextScaleService = ChatTextScaleService(); // Load settings await appSettingsService.loadSettings(); @@ -50,6 +52,8 @@ void main() async { await backgroundService.initialize(); _registerThirdPartyLicenses(); + await chatTextScaleService.initialize(); + // Wire up connector with services connector.initialize( retryService: retryService, @@ -78,6 +82,7 @@ void main() async { bleDebugLogService: bleDebugLogService, appDebugLogService: appDebugLogService, mapTileCacheService: mapTileCacheService, + chatTextScaleService: chatTextScaleService, ), ); } @@ -112,6 +117,7 @@ class MeshCoreApp extends StatelessWidget { final BleDebugLogService bleDebugLogService; final AppDebugLogService appDebugLogService; final MapTileCacheService mapTileCacheService; + final ChatTextScaleService chatTextScaleService; const MeshCoreApp({ super.key, @@ -123,6 +129,7 @@ class MeshCoreApp extends StatelessWidget { required this.bleDebugLogService, required this.appDebugLogService, required this.mapTileCacheService, + required this.chatTextScaleService, }); @override @@ -135,6 +142,7 @@ class MeshCoreApp extends StatelessWidget { ChangeNotifierProvider.value(value: appSettingsService), ChangeNotifierProvider.value(value: bleDebugLogService), ChangeNotifierProvider.value(value: appDebugLogService), + ChangeNotifierProvider.value(value: chatTextScaleService), Provider.value(value: storage), Provider.value(value: mapTileCacheService), ], diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9df91c3..79d30e5 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -18,7 +18,9 @@ import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; import '../services/app_settings_service.dart'; +import '../services/chat_text_scale_service.dart'; import '../utils/emoji_utils.dart'; +import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; @@ -219,37 +221,50 @@ class _ChannelChatScreenState extends State { return Stack( children: [ - ListView.builder( - reverse: true, // List grows from bottom up - controller: _scrollController, - padding: const EdgeInsets.all(8), - itemCount: itemCount, - itemBuilder: (context, index) { - // Loading indicator now appears at end (bottom) of reversed list - if (_isLoadingOlder && index == itemCount - 1) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, + ChatZoomWrapper( + child: ListView.builder( + reverse: true, // List grows from bottom up + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: itemCount, + itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), ), ), + ); + } + final messageIndex = index; + final message = reversedMessages[messageIndex]; + if (!_messageKeys.containsKey(message.messageId)) { + _messageKeys[message.messageId] = GlobalKey(); + } + return Container( + key: _messageKeys[message.messageId]!, + child: Builder( + builder: (context) { + final textScale = context + .select( + (service) => service.scale, + ); + return _buildMessageBubble( + message, + textScale, + ); + }, ), ); - } - final messageIndex = index; - final message = reversedMessages[messageIndex]; - if (!_messageKeys.containsKey(message.messageId)) { - _messageKeys[message.messageId] = GlobalKey(); - } - return Container( - key: _messageKeys[message.messageId]!, - child: _buildMessageBubble(message), - ); - }, + }, + ), ), JumpToBottomButton(scrollController: _scrollController), ], @@ -264,7 +279,7 @@ class _ChannelChatScreenState extends State { ); } - Widget _buildMessageBubble(ChannelMessage message) { + Widget _buildMessageBubble(ChannelMessage message, double textScale) { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; @@ -278,6 +293,7 @@ class _ChannelChatScreenState extends State { const maxSwipeOffset = 64.0; const replySwipeThreshold = 64.0; + const bodyFontSize = 14.0; final messageBody = Column( crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end @@ -334,7 +350,7 @@ class _ChannelChatScreenState extends State { if (gifId == null) const SizedBox(height: 4), ], if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message), + _buildReplyPreview(message, textScale), const SizedBox(height: 8), ], if (poi != null) @@ -342,6 +358,7 @@ class _ChannelChatScreenState extends State { context, poi, isOutgoing, + textScale, trailing: (!enableTracing && isOutgoing) ? Padding( padding: const EdgeInsets.only(bottom: 2), @@ -415,9 +432,11 @@ class _ChannelChatScreenState extends State { Flexible( child: Linkify( text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, + style: TextStyle( + fontSize: bodyFontSize * textScale, + ), + linkStyle: TextStyle( + fontSize: bodyFontSize * textScale, color: Colors.green, decoration: TextDecoration.underline, ), @@ -595,7 +614,7 @@ class _ChannelChatScreenState extends State { ); } - Widget _buildReplyPreview(ChannelMessage message) { + Widget _buildReplyPreview(ChannelMessage message, double textScale) { final connector = context.read(); final isOwnNode = message.replyToSenderName == connector.selfName; final replyText = message.replyToText ?? ''; @@ -623,7 +642,7 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 4), Text( context.l10n.chat_location, - style: TextStyle(fontSize: 12, color: previewTextColor), + style: TextStyle(fontSize: 12 * textScale, color: previewTextColor), ), ], ); @@ -633,7 +652,7 @@ class _ChannelChatScreenState extends State { maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 12, + fontSize: 12 * textScale, color: previewTextColor, fontStyle: FontStyle.italic, ), @@ -657,7 +676,7 @@ class _ChannelChatScreenState extends State { Text( context.l10n.chat_replyTo(message.replyToSenderName ?? ''), style: TextStyle( - fontSize: 11, + fontSize: 11 * textScale, fontWeight: FontWeight.bold, color: isOwnNode ? Theme.of(context).colorScheme.primary @@ -736,7 +755,8 @@ class _ChannelChatScreenState extends State { Widget _buildPoiMessage( BuildContext context, _PoiInfo poi, - bool isOutgoing, { + bool isOutgoing, + double textScale, { Widget? trailing, }) { final colorScheme = Theme.of(context).colorScheme; @@ -774,12 +794,16 @@ class _ChannelChatScreenState extends State { children: [ Text( context.l10n.chat_poiShared, - style: TextStyle(color: textColor, fontWeight: FontWeight.w600), + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w600, + fontSize: 14 * textScale, + ), ), if (poi.label.isNotEmpty) Text( poi.label, - style: TextStyle(color: metaColor, fontSize: 12), + style: TextStyle(color: metaColor, fontSize: 12 * textScale), ), ], ), @@ -849,7 +873,7 @@ class _ChannelChatScreenState extends State { return colors[hash.abs() % colors.length]; } - Widget _buildReplyBanner() { + Widget _buildReplyBanner(double textScale) { final message = _replyingToMessage!; return Container( width: double.infinity, @@ -875,7 +899,7 @@ class _ChannelChatScreenState extends State { Text( context.l10n.chat_replyingTo(message.senderName), style: TextStyle( - fontSize: 12, + fontSize: 12 * textScale, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSecondaryContainer, ), @@ -885,7 +909,7 @@ class _ChannelChatScreenState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 11, + fontSize: 11 * textScale, color: Theme.of( context, ).colorScheme.onSecondaryContainer.withValues(alpha: 0.7), @@ -912,7 +936,15 @@ class _ChannelChatScreenState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - if (_replyingToMessage != null) _buildReplyBanner(), + if (_replyingToMessage != null) + Builder( + builder: (context) { + final textScale = context.select( + (service) => service.scale, + ); + return _buildReplyBanner(textScale); + }, + ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 3556d6d..d623f33 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -22,7 +22,9 @@ import '../models/contact.dart'; import '../models/message.dart'; import '../models/path_history.dart'; import '../services/app_settings_service.dart'; +import '../services/chat_text_scale_service.dart'; import '../services/path_history_service.dart'; +import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -270,52 +272,62 @@ class _ChatScreenState extends State { _scrollController.scrollToBottomIfAtBottom(); }); - return ListView.builder( - reverse: true, // List grows from bottom up - controller: _scrollController, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - itemCount: itemCount, - itemBuilder: (context, index) { - // Loading indicator now appears at end (bottom) of reversed list - if (_isLoadingOlder && index == itemCount - 1) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), + return ChatZoomWrapper( + child: ListView.builder( + reverse: true, // List grows from bottom up + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + itemCount: itemCount, + itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), - ), - ); - } - final messageIndex = index; - Contact contact = widget.contact; - final message = reversedMessages[messageIndex]; - String fourByteHex = ''; - if (widget.contact.type == advTypeRoom) { - contact = _resolveContactFrom4Bytes( - connector, - message.fourByteRoomContactKey.isEmpty - ? Uint8List.fromList([0, 0, 0, 0]) - : message.fourByteRoomContactKey, - ); - fourByteHex = message.fourByteRoomContactKey - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join() - .toUpperCase(); - } + ); + } + final messageIndex = index; + Contact contact = widget.contact; + final message = reversedMessages[messageIndex]; + String fourByteHex = ''; + if (widget.contact.type == advTypeRoom) { + contact = _resolveContactFrom4Bytes( + connector, + message.fourByteRoomContactKey.isEmpty + ? Uint8List.fromList([0, 0, 0, 0]) + : message.fourByteRoomContactKey, + ); + fourByteHex = message.fourByteRoomContactKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join() + .toUpperCase(); + } - return _MessageBubble( - message: message, - senderName: widget.contact.type == advTypeRoom - ? "${contact.name} [$fourByteHex]" - : contact.name, - isRoomServer: widget.contact.type == advTypeRoom, - onTap: () => _openMessagePath(message, contact), - onLongPress: () => _showMessageActions(message, contact), - ); - }, + return Builder( + builder: (context) { + final textScale = context.select( + (service) => service.scale, + ); + return _MessageBubble( + message: message, + senderName: widget.contact.type == advTypeRoom + ? "${contact.name} [$fourByteHex]" + : contact.name, + isRoomServer: widget.contact.type == advTypeRoom, + textScale: textScale, + onTap: () => _openMessagePath(message, contact), + onLongPress: () => _showMessageActions(message, contact), + ); + }, + ); + }, + ), ); } @@ -1163,11 +1175,13 @@ class _MessageBubble extends StatelessWidget { final bool isRoomServer; final VoidCallback? onTap; final VoidCallback? onLongPress; + final double textScale; const _MessageBubble({ required this.message, required this.senderName, required this.isRoomServer, + required this.textScale, this.onTap, this.onLongPress, }); @@ -1190,6 +1204,7 @@ class _MessageBubble extends StatelessWidget { ? colorScheme.onErrorContainer : (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface); final metaColor = textColor.withValues(alpha: 0.7); + const bodyFontSize = 14.0; String messageText = message.text; if (isRoomServer && !isOutgoing) { messageText = message.text.substring(4.clamp(0, message.text.length)); @@ -1258,6 +1273,7 @@ class _MessageBubble extends StatelessWidget { poi, textColor, metaColor, + textScale, trailing: (!enableTracing && isOutgoing) ? Padding( padding: const EdgeInsets.only(bottom: 2), @@ -1321,10 +1337,14 @@ class _MessageBubble extends StatelessWidget { Flexible( child: Linkify( text: messageText, - style: TextStyle(color: textColor), - linkStyle: const TextStyle( + style: TextStyle( + color: textColor, + fontSize: bodyFontSize * textScale, + ), + linkStyle: TextStyle( color: Colors.green, decoration: TextDecoration.underline, + fontSize: bodyFontSize * textScale, ), options: const LinkifyOptions( humanize: false, @@ -1464,7 +1484,8 @@ class _MessageBubble extends StatelessWidget { BuildContext context, _PoiInfo poi, Color textColor, - Color metaColor, { + Color metaColor, + double textScale, { Widget? trailing, }) { return Row( @@ -1493,12 +1514,16 @@ class _MessageBubble extends StatelessWidget { children: [ Text( context.l10n.chat_poiShared, - style: TextStyle(color: textColor, fontWeight: FontWeight.w600), + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w600, + fontSize: 14 * textScale, + ), ), if (poi.label.isNotEmpty) Text( poi.label, - style: TextStyle(color: metaColor, fontSize: 12), + style: TextStyle(color: metaColor, fontSize: 12 * textScale), ), ], ), diff --git a/lib/services/chat_text_scale_service.dart b/lib/services/chat_text_scale_service.dart new file mode 100644 index 0000000..0257a56 --- /dev/null +++ b/lib/services/chat_text_scale_service.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; + +import '../storage/prefs_manager.dart'; + +/// Client-side accessibility/UI service that exposes a persistent shared text scale +/// factor. No MeshCoreConnector/RoomServer or protocol interaction occurs, and the +/// value is saved locally via SharedPreferences so it can be reused in Markdown +/// viewers, log panels, or other text-heavy widgets without redundant network +/// dependencies. +/// +/// Widgets should scope rebuilds using the snippet below so only the scaled text +/// is rebuilt instead of the entire chat list: +/// ```dart +/// context.select( +/// (service) => service.scale, +/// ) +/// ``` +class ChatTextScaleService extends ChangeNotifier { + static const _prefKey = 'chat_text_scale'; + static const double _minScale = 0.8; + static const double _maxScale = 1.8; + + double _scale = 1.0; + + double get scale => _scale; + + Future initialize() async { + final stored = PrefsManager.instance.getDouble(_prefKey); + if (stored != null) { + _scale = _clamp(stored); + } + } + + void setScale(double value) { + final next = _clamp(value); + if (next == _scale) return; + _scale = next; + PrefsManager.instance.setDouble(_prefKey, _scale); + notifyListeners(); + } + + void reset() => setScale(1.0); + + double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble(); +} diff --git a/lib/widgets/chat_zoom_wrapper.dart b/lib/widgets/chat_zoom_wrapper.dart new file mode 100644 index 0000000..e18662d --- /dev/null +++ b/lib/widgets/chat_zoom_wrapper.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../services/chat_text_scale_service.dart'; + +/// Gesture wrapper that exposes two-finger pinch-to-zoom for chat scrollables. +/// Double-tap resets the scale. Only the wrapper itself listens to gestures; +/// child scrollables keep their normal touch handling. +class ChatZoomWrapper extends StatelessWidget { + ChatZoomWrapper({super.key, required this.child, this.onDoubleTap}); + + final Widget child; + final VoidCallback? onDoubleTap; + final _ZoomGestureState _state = _ZoomGestureState(); + + @override + Widget build(BuildContext context) { + final service = context.read(); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: () { + service.reset(); + onDoubleTap?.call(); + }, + onScaleStart: (details) { + if (details.pointerCount != 2) return; + _state.startScale = service.scale; + }, + onScaleUpdate: (details) { + if (details.pointerCount != 2) return; + final baseScale = _state.startScale ?? service.scale; + service.setScale(baseScale * details.scale); + }, + onScaleEnd: (_) { + _state.startScale = null; + }, + child: child, + ); + } +} + +class _ZoomGestureState { + double? startScale; +} From 8a160246424023f76969f33e5bbd6e937e7c47d1 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:06:27 -0500 Subject: [PATCH 189/421] fix(chat): stabilize pinch-to-zoom scaling --- lib/services/chat_text_scale_service.dart | 33 ++++++++++++++++++++--- lib/widgets/chat_zoom_wrapper.dart | 28 ++++++++++--------- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/lib/services/chat_text_scale_service.dart b/lib/services/chat_text_scale_service.dart index 0257a56..21d6a5f 100644 --- a/lib/services/chat_text_scale_service.dart +++ b/lib/services/chat_text_scale_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import '../storage/prefs_manager.dart'; @@ -21,6 +23,7 @@ class ChatTextScaleService extends ChangeNotifier { static const double _maxScale = 1.8; double _scale = 1.0; + Timer? _saveTimer; double get scale => _scale; @@ -31,15 +34,39 @@ class ChatTextScaleService extends ChangeNotifier { } } - void setScale(double value) { + void setScale(double value, {bool persistImmediately = false}) { final next = _clamp(value); if (next == _scale) return; _scale = next; - PrefsManager.instance.setDouble(_prefKey, _scale); notifyListeners(); + if (persistImmediately) { + _commitScale(); + } else { + _scheduleSave(); + } } - void reset() => setScale(1.0); + void reset() { + setScale(1.0, persistImmediately: true); + } + + void persist() => _commitScale(); + + @override + void dispose() { + _saveTimer?.cancel(); + super.dispose(); + } + + void _scheduleSave() { + _saveTimer?.cancel(); + _saveTimer = Timer(const Duration(milliseconds: 250), _commitScale); + } + + void _commitScale() { + _saveTimer?.cancel(); + PrefsManager.instance.setDouble(_prefKey, _scale); + } double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble(); } diff --git a/lib/widgets/chat_zoom_wrapper.dart b/lib/widgets/chat_zoom_wrapper.dart index e18662d..f0c6815 100644 --- a/lib/widgets/chat_zoom_wrapper.dart +++ b/lib/widgets/chat_zoom_wrapper.dart @@ -6,12 +6,18 @@ import '../services/chat_text_scale_service.dart'; /// Gesture wrapper that exposes two-finger pinch-to-zoom for chat scrollables. /// Double-tap resets the scale. Only the wrapper itself listens to gestures; /// child scrollables keep their normal touch handling. -class ChatZoomWrapper extends StatelessWidget { - ChatZoomWrapper({super.key, required this.child, this.onDoubleTap}); +class ChatZoomWrapper extends StatefulWidget { + const ChatZoomWrapper({super.key, required this.child, this.onDoubleTap}); final Widget child; final VoidCallback? onDoubleTap; - final _ZoomGestureState _state = _ZoomGestureState(); + + @override + State createState() => _ChatZoomWrapperState(); +} + +class _ChatZoomWrapperState extends State { + double? _startScale; @override Widget build(BuildContext context) { @@ -21,25 +27,23 @@ class ChatZoomWrapper extends StatelessWidget { behavior: HitTestBehavior.translucent, onDoubleTap: () { service.reset(); - onDoubleTap?.call(); + service.persist(); + widget.onDoubleTap?.call(); }, onScaleStart: (details) { if (details.pointerCount != 2) return; - _state.startScale = service.scale; + _startScale = service.scale; }, onScaleUpdate: (details) { if (details.pointerCount != 2) return; - final baseScale = _state.startScale ?? service.scale; + final baseScale = _startScale ?? service.scale; service.setScale(baseScale * details.scale); }, onScaleEnd: (_) { - _state.startScale = null; + _startScale = null; + service.persist(); }, - child: child, + child: widget.child, ); } } - -class _ZoomGestureState { - double? startScale; -} From 2a7cc28a3a5410417d1310fce0e204115048511f Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:46:25 -0500 Subject: [PATCH 190/421] fix --- lib/l10n/app_bg.arb | 2 +- lib/l10n/app_de.arb | 2 +- lib/l10n/app_es.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_it.arb | 2 +- lib/l10n/app_nl.arb | 2 +- lib/l10n/app_pl.arb | 2 +- lib/l10n/app_pt.arb | 2 +- lib/l10n/app_ru.arb | 2 +- lib/l10n/app_sk.arb | 2 +- lib/l10n/app_sl.arb | 2 +- lib/l10n/app_sv.arb | 2 +- lib/l10n/app_uk.arb | 2 +- lib/l10n/app_zh.arb | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 96db350..b59d407 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "bg", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 3898e76..409bdbb 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "de", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e07b48b..9e03138 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "es", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f3c79bf..78c155c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "fr", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2266641..a6871a1 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "it", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index ab57355..57797f9 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "nl", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d16052d..908892e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pl", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8d4e827..d221ae5 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pt", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 3d17889..eb6627c 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "ru", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index c87ef14..2e7a06b 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sk", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index b0fc948..5b0cbfb 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sl", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 7806ed7..7cec9b7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sv", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index b990b14..2ad9dde 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "uk", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 295274d..43adb81 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "zh", From c880c2d107c6cbc85c4ba39fb25d4d452a62f5ff Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:02:10 -0500 Subject: [PATCH 191/421] fix channel actions context --- lib/screens/channels_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index d2a2e3d..582fee7 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -522,7 +522,7 @@ class _ChannelsScreenState extends State : context.l10n.channels_muteChannel, ), onTap: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); if (isMuted) { await settingsService.unmuteChannel(channel.name); } else { From 515b9c1f295220b51641a7e8587c514ce27948de Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:51:58 -0500 Subject: [PATCH 192/421] fix los init localization --- lib/screens/line_of_sight_map_screen.dart | 12 +++++++++++- pubspec.lock | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 0fbc21b..402b417 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -73,6 +73,7 @@ class _LineOfSightMapScreenState extends State { bool _showMarkerLabels = true; bool _didReceivePositionUpdate = false; int _losRequestNonce = 0; + bool _initialLosScheduled = false; @override void initState() { @@ -83,7 +84,16 @@ class _LineOfSightMapScreenState extends State { _end = widget.candidates[1]; } } - _runLos(); + _scheduleInitialRun(); + } + + void _scheduleInitialRun() { + if (_initialLosScheduled) return; + _initialLosScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _runLos(); + }); } @override diff --git a/pubspec.lock b/pubspec.lock index aa8819e..fa23b27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1140,4 +1140,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" \ No newline at end of file + flutter: ">=3.38.4" From 31db565ebfa5ccd967fb1f82b4b7a0749b65097f Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:54:03 -0500 Subject: [PATCH 193/421] PR Combined #228 #220 #219 #201 --- lib/screens/line_of_sight_map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 402b417..ec8a391 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -122,7 +122,7 @@ class _LineOfSightMapScreenState extends State { }); try { - final connector = context.watch(); + final connector = context.read(); final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz); final result = await _lineOfSightService.analyzePath( [start.point, end.point], From 5a70ed48cf3e1143f0263140b8320408522a8166 Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 24 Feb 2026 23:56:30 +0100 Subject: [PATCH 194/421] favorites handling only --- .github/workflows/build.yml | 5 ++ lib/connector/meshcore_connector.dart | 68 +++++++++++++++++++ lib/connector/meshcore_protocol.dart | 1 + lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 3 +- lib/l10n/app_localizations.dart | 6 ++ lib/l10n/app_localizations_bg.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_sk.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_localizations_sv.dart | 3 + lib/l10n/app_localizations_uk.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/models/contact.dart | 7 ++ lib/screens/contacts_screen.dart | 26 +++++++ lib/storage/contact_store.dart | 2 + lib/widgets/list_filter_widget.dart | 21 ++++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + untranslated.json | 54 ++++++++++++++- 27 files changed, 233 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05c82de..c376a4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,11 @@ jobs: ${{ runner.os }}-gradle- - run: flutter pub get - run: flutter build apk --release --no-pub + - name: Upload Debug APK + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: build/app/outputs/flutter-apk/app-release.apk ios: runs-on: macos-latest diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index afd1626..ef19f02 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -669,6 +669,7 @@ class MeshCoreConnector extends ChangeNotifier { publicKey: contact.publicKey, name: contact.name, type: contact.type, + flags: contact.flags, pathLength: selection.hopCount >= 0 ? selection.hopCount : contact.pathLength, @@ -1185,11 +1186,78 @@ class MeshCoreConnector extends ChangeNotifier { customPath, pathLen, type: contact.type, + flags: contact.flags, name: contact.name, ), ); } + Future setContactFavorite(Contact contact, bool isFavorite) async { + if (!isConnected) return; + final latestContact = + await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact; + final updatedFlags = isFavorite + ? (latestContact.flags | contactFlagFavorite) + : (latestContact.flags & ~contactFlagFavorite); + + await sendFrame( + buildUpdateContactPathFrame( + latestContact.publicKey, + latestContact.path, + latestContact.pathLength, + type: latestContact.type, + flags: updatedFlags, + name: latestContact.name, + ), + ); + + final index = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + if (index >= 0) { + _contacts[index] = _contacts[index].copyWith( + type: latestContact.type, + name: latestContact.name, + pathLength: latestContact.pathLength, + path: latestContact.path, + flags: updatedFlags, + ); + notifyListeners(); + unawaited(_persistContacts()); + } + } + + Future _fetchContactSnapshotFromDevice( + Uint8List pubKey, { + Duration timeout = const Duration(seconds: 3), + }) async { + if (!isConnected) return null; + final expectedKeyHex = pubKeyToHex(pubKey); + final completer = Completer(); + + void finish(Contact? result) { + if (!completer.isCompleted) { + completer.complete(result); + } + } + + final subscription = receivedFrames.listen((frame) { + if (frame.isEmpty || frame[0] != respCodeContact) return; + final parsed = Contact.fromFrame(frame); + if (parsed == null || parsed.publicKeyHex != expectedKeyHex) return; + finish(parsed); + }); + + final timer = Timer(timeout, () => finish(null)); + try { + await getContactByKey(pubKey); + return await completer.future; + } finally { + timer.cancel(); + await subscription.cancel(); + } + } + /// Set path override for a contact (persists across contact refreshes) /// pathLen: -1 = force flood, null = auto (use device path), >= 0 = specific path Future setPathOverride( diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 2933e80..d5ce9ee 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -290,6 +290,7 @@ int _minPositive(int a, int b) { const int contactPubKeyOffset = 1; const int contactTypeOffset = 33; const int contactFlagsOffset = 34; +const int contactFlagFavorite = 0x01; const int contactPathLenOffset = 35; const int contactPathOffset = 36; const int contactNameOffset = 100; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index a1647fa..f596ae4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1343,6 +1343,7 @@ "listFilter_az": "A-Z", "listFilter_filters": "Filtere", "listFilter_all": "Alle", + "listFilter_favorites": "Favoriten", "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index eec03bc..5b15b65 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1555,6 +1555,7 @@ "listFilter_az": "A-Z", "listFilter_filters": "Filters", "listFilter_all": "All", + "listFilter_favorites": "Favorites", "listFilter_users": "Users", "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", @@ -1779,4 +1780,4 @@ "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", "snrIndicator_lastSeen": "Last seen" -} \ No newline at end of file +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 54e9cdc..5d6a6a2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4772,6 +4772,12 @@ abstract class AppLocalizations { /// **'All'** String get listFilter_all; + /// No description provided for @listFilter_favorites. + /// + /// In en, this message translates to: + /// **'Favorites'** + String get listFilter_favorites; + /// No description provided for @listFilter_users. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 21a6e79..513cdc3 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2728,6 +2728,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get listFilter_all => 'Всички'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Потребители'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e5a3a49..bada9f6 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2733,6 +2733,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get listFilter_all => 'Alle'; + @override + String get listFilter_favorites => 'Favoriten'; + @override String get listFilter_users => 'Benutzer'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a56e217..b090c45 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2686,6 +2686,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get listFilter_all => 'All'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Users'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 98cd658..ac51330 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2726,6 +2726,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get listFilter_all => 'Todas'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Usuarios'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index a52ff00..3ca86e6 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2742,6 +2742,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get listFilter_all => 'Tout'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Utilisateurs'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index aebea2f..c9900c7 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2726,6 +2726,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get listFilter_all => 'Tutti'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Utenti'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 5460d29..8c537f2 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2717,6 +2717,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get listFilter_all => 'Alles'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Gebruikers'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 7286033..2c11c82 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2724,6 +2724,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get listFilter_all => 'Wszystko'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Użytkownicy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 025c81c..c2fac6c 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2727,6 +2727,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get listFilter_all => 'Tudo'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Usuários'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 399b158..9fde3b8 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2730,6 +2730,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get listFilter_all => 'Все'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Пользователи'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 5138311..5d31462 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2712,6 +2712,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get listFilter_all => 'Všetko'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Používatelia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f42e8e0..19934e8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2715,6 +2715,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get listFilter_all => 'Vse'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Uporabniki'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index ba99455..04ae835 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2700,6 +2700,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get listFilter_all => 'Alla'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Användare'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e2bbbe8..1c3442d 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2737,6 +2737,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get listFilter_all => 'Все'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Користувачі'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4da17ea..6a9881e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2582,6 +2582,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get listFilter_all => '全部'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => '用户'; diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 143a62a..5e532e6 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -5,6 +5,7 @@ class Contact { final Uint8List publicKey; final String name; final int type; + final int flags; final int pathLength; // -1 = flood, 0+ = direct hops (from device) final Uint8List path; // Path bytes from device final int? @@ -19,6 +20,7 @@ class Contact { required this.publicKey, required this.name, required this.type, + this.flags = 0, required this.pathLength, required this.path, this.pathOverride, @@ -58,11 +60,13 @@ class Contact { } bool get hasLocation => latitude != null && longitude != null; + bool get isFavorite => (flags & contactFlagFavorite) != 0; Contact copyWith({ Uint8List? publicKey, String? name, int? type, + int? flags, int? pathLength, Uint8List? path, int? pathOverride, @@ -77,6 +81,7 @@ class Contact { publicKey: publicKey ?? this.publicKey, name: name ?? this.name, type: type ?? this.type, + flags: flags ?? this.flags, pathLength: pathLength ?? this.pathLength, path: path ?? this.path, pathOverride: clearPathOverride @@ -167,6 +172,7 @@ class Contact { data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), ); final type = data[contactTypeOffset]; + final flags = data[contactFlagsOffset]; final pathLen = data[contactPathLenOffset].toSigned(8); final safePathLen = pathLen > 0 ? (pathLen > maxPathSize ? maxPathSize : pathLen) @@ -191,6 +197,7 @@ class Contact { publicKey: pubKey, name: name.isEmpty ? 'Unknown' : name, type: type, + flags: flags, pathLength: pathLen, path: pathBytes, latitude: lat, diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index c3f783c..08e3e14 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -13,6 +13,7 @@ import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; import '../models/contact_group.dart'; import '../storage/contact_group_store.dart'; +import '../storage/contact_settings_store.dart'; import '../utils/contact_search.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; @@ -481,6 +482,7 @@ class _ContactsScreenState extends State contact: contact, lastSeen: _resolveLastSeen(contact), unreadCount: unreadCount, + isFavorite: contact.isFavorite, onTap: () => _openChat(context, contact), onLongPress: () => _showContactOptions(context, connector, contact), @@ -517,6 +519,7 @@ class _ContactsScreenState extends State }) .where((group) { if (_typeFilter == ContactTypeFilter.all) return true; + if (_typeFilter == ContactTypeFilter.favorites) return false; for (final key in group.memberKeys) { final contact = contactsByKey[key]; if (contact != null && _matchesTypeFilter(contact)) return true; @@ -591,6 +594,8 @@ class _ContactsScreenState extends State switch (_typeFilter) { case ContactTypeFilter.all: return true; + case ContactTypeFilter.favorites: + return contact.isFavorite; case ContactTypeFilter.users: return contact.type == advTypeChat; case ContactTypeFilter.repeaters: @@ -981,6 +986,7 @@ class _ContactsScreenState extends State ) { final isRepeater = contact.type == advTypeRepeater; final isRoom = contact.type == advTypeRoom; + final isFavorite = contact.isFavorite; showModalBottomSheet( context: context, @@ -1087,6 +1093,21 @@ class _ContactsScreenState extends State }, ), ], + ListTile( + leading: Icon( + isFavorite ? Icons.star : Icons.star_border, + color: Colors.amber[700], + ), + title: Text( + isFavorite + ? '${context.l10n.common_remove} ${context.l10n.listFilter_favorites}' + : '${context.l10n.common_add} ${context.l10n.listFilter_favorites}', + ), + onTap: () async { + Navigator.pop(sheetContext); + await connector.setContactFavorite(contact, !isFavorite); + }, + ), ListTile( leading: const Icon(Icons.copy), title: Text(context.l10n.contacts_ShareContact), @@ -1155,6 +1176,7 @@ class _ContactTile extends StatelessWidget { final Contact contact; final DateTime lastSeen; final int unreadCount; + final bool isFavorite; final VoidCallback onTap; final VoidCallback onLongPress; @@ -1162,6 +1184,7 @@ class _ContactTile extends StatelessWidget { required this.contact, required this.lastSeen, required this.unreadCount, + required this.isFavorite, required this.onTap, required this.onLongPress, }); @@ -1214,6 +1237,9 @@ class _ContactTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ + if (isFavorite) + Icon(Icons.star, size: 14, color: Colors.amber[700]), + if (isFavorite && contact.hasLocation) const SizedBox(width: 2), if (contact.hasLocation) Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 08d158b..504ff16 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -33,6 +33,7 @@ class ContactStore { 'publicKey': base64Encode(contact.publicKey), 'name': contact.name, 'type': contact.type, + 'flags': contact.flags, 'pathLength': contact.pathLength, 'path': base64Encode(contact.path), 'pathOverride': contact.pathOverride, @@ -53,6 +54,7 @@ class ContactStore { publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', type: json['type'] as int? ?? 0, + flags: json['flags'] as int? ?? 0, pathLength: json['pathLength'] as int? ?? -1, path: json['path'] != null ? Uint8List.fromList(base64Decode(json['path'] as String)) diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index e9c0d9e..473a3df 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -3,7 +3,7 @@ import '../l10n/l10n.dart'; enum ContactSortOption { lastSeen, recentMessages, name } -enum ContactTypeFilter { all, users, repeaters, rooms } +enum ContactTypeFilter { all, favorites, users, repeaters, rooms } class SortFilterMenuOption { final int value; @@ -94,11 +94,12 @@ const int _actionSortRecentMessages = 1; const int _actionSortName = 2; const int _actionSortLastSeen = 3; const int _actionFilterAll = 4; -const int _actionFilterUsers = 5; -const int _actionFilterRepeaters = 6; -const int _actionFilterRooms = 7; -const int _actionToggleUnreadOnly = 8; -const int _actionNewGroup = 9; +const int _actionFilterFavorites = 5; +const int _actionFilterUsers = 6; +const int _actionFilterRepeaters = 7; +const int _actionFilterRooms = 8; +const int _actionToggleUnreadOnly = 9; +const int _actionNewGroup = 10; class ContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; @@ -154,6 +155,11 @@ class ContactsFilterMenu extends StatelessWidget { label: l10n.listFilter_all, checked: typeFilter == ContactTypeFilter.all, ), + SortFilterMenuOption( + value: _actionFilterFavorites, + label: l10n.listFilter_favorites, + checked: typeFilter == ContactTypeFilter.favorites, + ), SortFilterMenuOption( value: _actionFilterUsers, label: l10n.listFilter_users, @@ -198,6 +204,9 @@ class ContactsFilterMenu extends StatelessWidget { case _actionFilterUsers: onTypeFilterChanged(ContactTypeFilter.users); break; + case _actionFilterFavorites: + onTypeFilterChanged(ContactTypeFilter.favorites); + break; case _actionFilterRepeaters: onTypeFilterChanged(ContactTypeFilter.repeaters); break; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e..4084d9b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/untranslated.json b/untranslated.json index 9e26dfe..a6d2937 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,53 @@ -{} \ No newline at end of file +{ + "bg": [ + "listFilter_favorites" + ], + + "es": [ + "listFilter_favorites" + ], + + "fr": [ + "listFilter_favorites" + ], + + "it": [ + "listFilter_favorites" + ], + + "nl": [ + "listFilter_favorites" + ], + + "pl": [ + "listFilter_favorites" + ], + + "pt": [ + "listFilter_favorites" + ], + + "ru": [ + "listFilter_favorites" + ], + + "sk": [ + "listFilter_favorites" + ], + + "sl": [ + "listFilter_favorites" + ], + + "sv": [ + "listFilter_favorites" + ], + + "uk": [ + "listFilter_favorites" + ], + + "zh": [ + "listFilter_favorites" + ] +} From a26055c93f648a70ae6065033190bd91e94f07da Mon Sep 17 00:00:00 2001 From: ericz Date: Wed, 25 Feb 2026 00:49:41 +0100 Subject: [PATCH 195/421] resolved analyte code failure: unused import --- lib/screens/contacts_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 08e3e14..28e7aa5 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -13,7 +13,6 @@ import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; import '../models/contact_group.dart'; import '../storage/contact_group_store.dart'; -import '../storage/contact_settings_store.dart'; import '../utils/contact_search.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; From a2d1cb2a998fde7dba403bd727f854d73d637416 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 19:42:12 -0700 Subject: [PATCH 196/421] add pubspec.lock to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2d9a3fc..157c7ec 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ migrate_working_dir/ .flutter-plugins-dependencies .pub-cache/ .pub/ +pubspec.lock /build/ /coverage/ From 190fd3b353a5b4594ac1d653fb329095fe16d3b8 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 19:44:15 -0700 Subject: [PATCH 197/421] Remove pubspec.lock from version control --- pubspec.lock | 1143 -------------------------------------------------- 1 file changed, 1143 deletions(-) delete mode 100644 pubspec.lock diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index fa23b27..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,1143 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.dev" - source: hosted - version: "4.0.9" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - bluez: - dependency: transitive - description: - name: bluez - sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" - url: "https://pub.dev" - source: hosted - version: "0.8.3" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - characters: - dependency: "direct main" - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" - source: hosted - version: "0.3.5+2" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - dart_earcut: - dependency: transitive - description: - name: dart_earcut - sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dart_polylabel2: - dependency: transitive - description: - name: dart_polylabel2 - sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - dbus: - dependency: transitive - description: - name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.dev" - source: hosted - version: "0.7.12" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_blue_plus: - dependency: "direct main" - description: - name: flutter_blue_plus - sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - flutter_blue_plus_android: - dependency: transitive - description: - name: flutter_blue_plus_android - sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_darwin: - dependency: transitive - description: - name: flutter_blue_plus_darwin - sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" - url: "https://pub.dev" - source: hosted - version: "8.1.1" - flutter_blue_plus_linux: - dependency: transitive - description: - name: flutter_blue_plus_linux - sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242 - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_platform_interface: - dependency: transitive - description: - name: flutter_blue_plus_platform_interface - sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_web: - dependency: transitive - description: - name: flutter_blue_plus_web - sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_winrt: - dependency: transitive - description: - name: flutter_blue_plus_winrt - sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 - url: "https://pub.dev" - source: hosted - version: "0.0.18" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - flutter_foreground_task: - dependency: "direct main" - description: - name: flutter_foreground_task - sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" - url: "https://pub.dev" - source: hosted - version: "9.2.0" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" - source: hosted - version: "0.14.4" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" - url: "https://pub.dev" - source: hosted - version: "20.1.0" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 - url: "https://pub.dev" - source: hosted - version: "7.0.0" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - flutter_local_notifications_windows: - dependency: transitive - description: - name: flutter_local_notifications_windows - sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a - url: "https://pub.dev" - source: hosted - version: "2.0.1" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_map: - dependency: "direct main" - description: - name: flutter_map - sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" - url: "https://pub.dev" - source: hosted - version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - gpx: - dependency: "direct main" - description: - name: gpx - sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e - url: "https://pub.dev" - source: hosted - version: "2.3.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.dev" - source: hosted - version: "4.8.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 - url: "https://pub.dev" - source: hosted - version: "4.11.0" - latlong2: - dependency: "direct main" - description: - name: latlong2 - sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" - url: "https://pub.dev" - source: hosted - version: "0.9.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - linkify: - dependency: transitive - description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - lists: - dependency: transitive - description: - name: lists - sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - logger: - dependency: transitive - description: - name: logger - sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 - url: "https://pub.dev" - source: hosted - version: "2.6.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" - url: "https://pub.dev" - source: hosted - version: "0.12.18" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - material_symbols_icons: - dependency: "direct main" - description: - name: material_symbols_icons - sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 - url: "https://pub.dev" - source: hosted - version: "4.2906.0" - meta: - dependency: transitive - description: - name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" - url: "https://pub.dev" - source: hosted - version: "1.18.0" - mgrs_dart: - dependency: transitive - description: - name: mgrs_dart - sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce - url: "https://pub.dev" - source: hosted - version: "7.2.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.dev" - source: hosted - version: "9.0.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pointycastle: - dependency: "direct main" - description: - name: pointycastle - sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - posix: - dependency: transitive - description: - name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" - source: hosted - version: "6.5.0" - proj4dart: - dependency: transitive - description: - name: proj4dart - sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e - url: "https://pub.dev" - source: hosted - version: "2.1.0" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - qr: - dependency: transitive - description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - qr_flutter: - dependency: "direct main" - description: - name: qr_flutter - sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.dev" - source: hosted - version: "12.0.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f - url: "https://pub.dev" - source: hosted - version: "2.4.20" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 - url: "https://pub.dev" - source: hosted - version: "2.4.2+2" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" - url: "https://pub.dev" - source: hosted - version: "0.7.9" - timezone: - dependency: transitive - description: - name: timezone - sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - unicode: - dependency: transitive - description: - name: unicode - sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.dev" - source: hosted - version: "6.3.28" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.dev" - source: hosted - version: "2.4.2" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" - source: hosted - version: "4.5.3" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - wakelock_plus: - dependency: "direct main" - description: - name: wakelock_plus - sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - web: - dependency: "direct main" - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - wkt_parser: - dependency: transitive - description: - name: wkt_parser - sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" From 50af2e0bc9c2ddedc4bdbafcc8709071e055b886 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 20:07:15 -0700 Subject: [PATCH 198/421] Fix review issues: dedicated l10n keys, remove unrelated CI/macOS changes, translate all locales - Replace concatenated favorite toggle strings with dedicated listFilter_addToFavorites/removeFromFavorites keys - Remove unrelated CI artifact upload step from build.yml - Revert unrelated macOS GeneratedPluginRegistrant.swift change - Add comment explaining groups hidden under favorites filter - Translate new keys across all 14 locales --- .github/workflows/build.yml | 5 -- lib/l10n/app_bg.arb | 13 ++++- lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 13 ++++- lib/l10n/app_fr.arb | 13 ++++- lib/l10n/app_it.arb | 13 ++++- lib/l10n/app_localizations.dart | 12 +++++ lib/l10n/app_localizations_bg.dart | 8 ++- lib/l10n/app_localizations_de.dart | 6 +++ lib/l10n/app_localizations_en.dart | 6 +++ lib/l10n/app_localizations_es.dart | 8 ++- lib/l10n/app_localizations_fr.dart | 8 ++- lib/l10n/app_localizations_it.dart | 8 ++- lib/l10n/app_localizations_nl.dart | 8 ++- lib/l10n/app_localizations_pl.dart | 8 ++- lib/l10n/app_localizations_pt.dart | 8 ++- lib/l10n/app_localizations_ru.dart | 8 ++- lib/l10n/app_localizations_sk.dart | 8 ++- lib/l10n/app_localizations_sl.dart | 8 ++- lib/l10n/app_localizations_sv.dart | 8 ++- lib/l10n/app_localizations_uk.dart | 8 ++- lib/l10n/app_localizations_zh.dart | 8 ++- 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/contacts_screen.dart | 5 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - untranslated.json | 54 +------------------ 35 files changed, 266 insertions(+), 101 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c376a4a..05c82de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,11 +30,6 @@ jobs: ${{ runner.os }}-gradle- - run: flutter pub get - run: flutter build apk --release --no-pub - - name: Upload Debug APK - uses: actions/upload-artifact@v4 - with: - name: app-debug - path: build/app/outputs/flutter-apk/app-release.apk ios: runs-on: macos-latest diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index b88e3ba..3613e85 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "bg", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Премахване от списъка с любими", + "listFilter_addToFavorites": "Добави към любими", + "listFilter_favorites": "Любими" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f596ae4..64e8cd3 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1344,6 +1344,8 @@ "listFilter_filters": "Filtere", "listFilter_all": "Alle", "listFilter_favorites": "Favoriten", + "listFilter_addToFavorites": "Zu Favoriten hinzufügen", + "listFilter_removeFromFavorites": "Aus Favoriten entfernen", "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5b15b65..8d9f385 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1556,6 +1556,8 @@ "listFilter_filters": "Filters", "listFilter_all": "All", "listFilter_favorites": "Favorites", + "listFilter_addToFavorites": "Add to favorites", + "listFilter_removeFromFavorites": "Remove from favorites", "listFilter_users": "Users", "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 2f62b54..dd8ce6c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "es", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", @@ -1772,5 +1778,8 @@ "type": "double" } } - } + }, + "listFilter_favorites": "Favoritos", + "listFilter_removeFromFavorites": "Eliminar de las favoritas", + "listFilter_addToFavorites": "Añadir a favoritos" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 9eb66e7..fe738f7 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "fr", "appTitle": "MeshCore Open", "nav_contacts": "Contacts", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Ajouter à mes favoris", + "listFilter_removeFromFavorites": "Supprimer des favoris", + "listFilter_favorites": "Préférences" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index e08dd67..d6c02f0 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "it", "appTitle": "MeshCore Open", "nav_contacts": "Contatti", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Aggiungi ai preferiti", + "listFilter_removeFromFavorites": "Rimuovi dai preferiti", + "listFilter_favorites": "Preferiti" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5d6a6a2..d64cdb0 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4778,6 +4778,18 @@ abstract class AppLocalizations { /// **'Favorites'** String get listFilter_favorites; + /// No description provided for @listFilter_addToFavorites. + /// + /// In en, this message translates to: + /// **'Add to favorites'** + String get listFilter_addToFavorites; + + /// No description provided for @listFilter_removeFromFavorites. + /// + /// In en, this message translates to: + /// **'Remove from favorites'** + String get listFilter_removeFromFavorites; + /// No description provided for @listFilter_users. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 513cdc3..f9637aa 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2729,7 +2729,13 @@ class AppLocalizationsBg extends AppLocalizations { String get listFilter_all => 'Всички'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Любими'; + + @override + String get listFilter_addToFavorites => 'Добави към любими'; + + @override + String get listFilter_removeFromFavorites => 'Премахване от списъка с любими'; @override String get listFilter_users => 'Потребители'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index bada9f6..1574281 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2736,6 +2736,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get listFilter_favorites => 'Favoriten'; + @override + String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen'; + + @override + String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen'; + @override String get listFilter_users => 'Benutzer'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b090c45..039ad22 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2689,6 +2689,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get listFilter_favorites => 'Favorites'; + @override + String get listFilter_addToFavorites => 'Add to favorites'; + + @override + String get listFilter_removeFromFavorites => 'Remove from favorites'; + @override String get listFilter_users => 'Users'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ac51330..8961b18 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2727,7 +2727,13 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_all => 'Todas'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favoritos'; + + @override + String get listFilter_addToFavorites => 'Añadir a favoritos'; + + @override + String get listFilter_removeFromFavorites => 'Eliminar de las favoritas'; @override String get listFilter_users => 'Usuarios'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3ca86e6..3737f46 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2743,7 +2743,13 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_all => 'Tout'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Préférences'; + + @override + String get listFilter_addToFavorites => 'Ajouter à mes favoris'; + + @override + String get listFilter_removeFromFavorites => 'Supprimer des favoris'; @override String get listFilter_users => 'Utilisateurs'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index c9900c7..b64ec6d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2727,7 +2727,13 @@ class AppLocalizationsIt extends AppLocalizations { String get listFilter_all => 'Tutti'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Preferiti'; + + @override + String get listFilter_addToFavorites => 'Aggiungi ai preferiti'; + + @override + String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti'; @override String get listFilter_users => 'Utenti'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 8c537f2..523cd6e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2718,7 +2718,13 @@ class AppLocalizationsNl extends AppLocalizations { String get listFilter_all => 'Alles'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favorieten'; + + @override + String get listFilter_addToFavorites => 'Toevoegen aan favorieten'; + + @override + String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten'; @override String get listFilter_users => 'Gebruikers'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2c11c82..1639600 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2725,7 +2725,13 @@ class AppLocalizationsPl extends AppLocalizations { String get listFilter_all => 'Wszystko'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Ulubione'; + + @override + String get listFilter_addToFavorites => 'Dodaj do ulubionych'; + + @override + String get listFilter_removeFromFavorites => 'Usuń z ulubionych'; @override String get listFilter_users => 'Użytkownicy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index c2fac6c..c59a4f5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2728,7 +2728,13 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_all => 'Tudo'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favoritos'; + + @override + String get listFilter_addToFavorites => 'Adicionar aos favoritos'; + + @override + String get listFilter_removeFromFavorites => 'Remover da lista de favoritos'; @override String get listFilter_users => 'Usuários'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9fde3b8..69d6044 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2731,7 +2731,13 @@ class AppLocalizationsRu extends AppLocalizations { String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Избранное'; + + @override + String get listFilter_addToFavorites => 'Добавить в избранное'; + + @override + String get listFilter_removeFromFavorites => 'Удалить из избранного'; @override String get listFilter_users => 'Пользователи'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 5d31462..17cbd7b 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2713,7 +2713,13 @@ class AppLocalizationsSk extends AppLocalizations { String get listFilter_all => 'Všetko'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Obľúbené'; + + @override + String get listFilter_addToFavorites => 'Pridaj do obľúbených'; + + @override + String get listFilter_removeFromFavorites => 'Odstrániť z označení'; @override String get listFilter_users => 'Používatelia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 19934e8..6478b2b 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2716,7 +2716,13 @@ class AppLocalizationsSl extends AppLocalizations { String get listFilter_all => 'Vse'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Priljubljene'; + + @override + String get listFilter_addToFavorites => 'Dodaj v priljubljene'; + + @override + String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih'; @override String get listFilter_users => 'Uporabniki'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 04ae835..54b998d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2701,7 +2701,13 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_all => 'Alla'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favoriter'; + + @override + String get listFilter_addToFavorites => 'Lägg till i favoriter'; + + @override + String get listFilter_removeFromFavorites => 'Ta bort från favoriter'; @override String get listFilter_users => 'Användare'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1c3442d..e3564f1 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2738,7 +2738,13 @@ class AppLocalizationsUk extends AppLocalizations { String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Улюблені'; + + @override + String get listFilter_addToFavorites => 'Додати до улюблених'; + + @override + String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених'; @override String get listFilter_users => 'Користувачі'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6a9881e..42f9130 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2583,7 +2583,13 @@ class AppLocalizationsZh extends AppLocalizations { String get listFilter_all => '全部'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => '收藏'; + + @override + String get listFilter_addToFavorites => '添加到收藏'; + + @override + String get listFilter_removeFromFavorites => '从收藏中移除'; @override String get listFilter_users => '用户'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index cb690fb..fee1f22 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "nl", "appTitle": "MeshCore Open", "nav_contacts": "Contacten", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Verwijderen uit favorieten", + "listFilter_favorites": "Favorieten", + "listFilter_addToFavorites": "Toevoegen aan favorieten" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 2161983..8096bb4 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "pl", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Usuń z ulubionych", + "listFilter_addToFavorites": "Dodaj do ulubionych", + "listFilter_favorites": "Ulubione" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8a02a4d..53f8f93 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "pt", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Adicionar aos favoritos", + "listFilter_removeFromFavorites": "Remover da lista de favoritos", + "listFilter_favorites": "Favoritos" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index ab2ed36..b4d7b59 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "ru", "appTitle": "MeshCore Open", "nav_contacts": "Контакты", @@ -984,5 +990,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Добавить в избранное", + "listFilter_favorites": "Избранное", + "listFilter_removeFromFavorites": "Удалить из избранного" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index ff95897..23a0aea 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "sk", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Odstrániť z označení", + "listFilter_addToFavorites": "Pridaj do obľúbených", + "listFilter_favorites": "Obľúbené" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index bd431d4..6b64f68 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "sl", "appTitle": "MeshCore Open", "nav_contacts": "Stiki", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_favorites": "Priljubljene", + "listFilter_removeFromFavorites": "Odstrani iz priljubljenih", + "listFilter_addToFavorites": "Dodaj v priljubljene" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 86da858..3b96df7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "sv", "appTitle": "MeshCore Open", "nav_contacts": "Kontakter", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Ta bort från favoriter", + "listFilter_addToFavorites": "Lägg till i favoriter", + "listFilter_favorites": "Favoriter" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 691aa8d..78b4a90 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "uk", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Видалити зі списку улюблених", + "listFilter_addToFavorites": "Додати до улюблених", + "listFilter_favorites": "Улюблені" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f6bb526..259e29b 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "zh", "appTitle": "MeshCore Open", "nav_contacts": "联系方式", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_favorites": "收藏", + "listFilter_addToFavorites": "添加到收藏", + "listFilter_removeFromFavorites": "从收藏中移除" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 28e7aa5..e9018a7 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -518,6 +518,7 @@ class _ContactsScreenState extends State }) .where((group) { if (_typeFilter == ContactTypeFilter.all) return true; + // Groups don't have a favorite flag, so hide them under favorites filter if (_typeFilter == ContactTypeFilter.favorites) return false; for (final key in group.memberKeys) { final contact = contactsByKey[key]; @@ -1099,8 +1100,8 @@ class _ContactsScreenState extends State ), title: Text( isFavorite - ? '${context.l10n.common_remove} ${context.l10n.listFilter_favorites}' - : '${context.l10n.common_add} ${context.l10n.listFilter_favorites}', + ? context.l10n.listFilter_removeFromFavorites + : context.l10n.listFilter_addToFavorites, ), onTap: () async { Navigator.pop(sheetContext); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4084d9b..d2ea57e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus -import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -21,7 +20,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/untranslated.json b/untranslated.json index a6d2937..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,53 +1 @@ -{ - "bg": [ - "listFilter_favorites" - ], - - "es": [ - "listFilter_favorites" - ], - - "fr": [ - "listFilter_favorites" - ], - - "it": [ - "listFilter_favorites" - ], - - "nl": [ - "listFilter_favorites" - ], - - "pl": [ - "listFilter_favorites" - ], - - "pt": [ - "listFilter_favorites" - ], - - "ru": [ - "listFilter_favorites" - ], - - "sk": [ - "listFilter_favorites" - ], - - "sl": [ - "listFilter_favorites" - ], - - "sv": [ - "listFilter_favorites" - ], - - "uk": [ - "listFilter_favorites" - ], - - "zh": [ - "listFilter_favorites" - ] -} +{} \ No newline at end of file From ea379ce50b3fd98341d4b39710536603569c4200 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 20:11:56 -0700 Subject: [PATCH 199/421] Fix dart format line length in contacts_screen.dart --- lib/screens/contacts_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index e9018a7..6c683cc 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1239,7 +1239,8 @@ class _ContactTile extends StatelessWidget { children: [ if (isFavorite) Icon(Icons.star, size: 14, color: Colors.amber[700]), - if (isFavorite && contact.hasLocation) const SizedBox(width: 2), + if (isFavorite && contact.hasLocation) + const SizedBox(width: 2), if (contact.hasLocation) Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], From 96371c03ae38306079cd8f1b57a6b45fb3a51ef1 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 24 Feb 2026 21:17:24 -0800 Subject: [PATCH 200/421] pub lock upate --- pubspec.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index fa23b27..838513d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -529,10 +529,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: From d88786bb0f6a2be72cfc1bd74b2bf5548baeebeb Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 24 Feb 2026 22:41:03 -0800 Subject: [PATCH 201/421] ble filtering --- lib/connector/meshcore_connector.dart | 220 ++++++++------------------ 1 file changed, 67 insertions(+), 153 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e4eaaf6..ba62232 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -30,7 +30,6 @@ import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; import '../utils/battery_utils.dart'; -import '../utils/platform_info.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -686,111 +685,59 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.scanning) return; - try { - _scanResults.clear(); - _setState(MeshCoreConnectionState.scanning); + _scanResults.clear(); + _setState(MeshCoreConnectionState.scanning); - // Ensure any previous scan is fully stopped - try { - await FlutterBluePlus.stopScan(); - } catch (_) {} + // Ensure any previous scan is fully stopped + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); - try { - await _scanSubscription?.cancel(); - } catch (_) {} - _scanSubscription = null; - - // On iOS/macOS, wait for Bluetooth to be powered on before scanning - if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { - // Wait for adapter state to be powered on - final adapterState = await FlutterBluePlus.adapterState.first; - if (adapterState != BluetoothAdapterState.on) { - // Wait for the adapter to turn on, with timeout - await FlutterBluePlus.adapterState - .firstWhere((state) => state == BluetoothAdapterState.on) - .timeout( - const Duration(seconds: 5), - onTimeout: () { - _setState(MeshCoreConnectionState.disconnected); - throw Exception('Bluetooth adapter not available'); - }, - ); - } - - // Add a small delay to allow BLE stack to fully initialize - await Future.delayed(const Duration(milliseconds: 300)); + // On iOS/macOS, wait for Bluetooth to be powered on before scanning + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + // Wait for adapter state to be powered on + final adapterState = await FlutterBluePlus.adapterState.first; + if (adapterState != BluetoothAdapterState.on) { + // Wait for the adapter to turn on, with timeout + await FlutterBluePlus.adapterState + .firstWhere((state) => state == BluetoothAdapterState.on) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + _setState(MeshCoreConnectionState.disconnected); + throw Exception('Bluetooth adapter not available'); + }, + ); } - _scanSubscription = FlutterBluePlus.scanResults.listen( - (results) { - _scanResults.clear(); - for (var result in results) { - if (result.device.platformName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("Whisper-")) { - _scanResults.add(result); - } - } - notifyListeners(); - }, - onError: (Object e) { - debugPrint("scanResults stream error: $e"); - stopScan(); - }, - ); - - if (PlatformInfo.isWeb) { - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - ); - // On web, the chooser returns once a device is picked, but the scanResults - // stream might take a moment to emit the last result. Wait briefly so the - // device appears in the UI before stopScan() clears the list. - await Future.delayed(const Duration(milliseconds: 500)); - } else { - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); - - await Future.delayed(timeout); - } - } catch (e) { - debugPrint("Scan error: $e"); - // On web, suppress common cancellation and chooser errors - if (kIsWeb) return; - - if (!PlatformInfo.isWeb) { - rethrow; - } - } finally { - await stopScan(); + // Add a small delay to allow BLE stack to fully initialize + await Future.delayed(const Duration(milliseconds: 300)); } + + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { + _scanResults.clear(); + _scanResults.addAll(results); + notifyListeners(); + }); + + await FlutterBluePlus.startScan( + withKeywords: ["MeshCore-", "Whisper-"], + webOptionalServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); + + await Future.delayed(timeout); + await stopScan(); } Future stopScan() async { - if (_state == MeshCoreConnectionState.scanning) { - _setState(MeshCoreConnectionState.disconnected); - } - - try { - await FlutterBluePlus.stopScan(); - } catch (e) { - debugPrint("stopScan error: $e"); - } - - try { - if (_scanSubscription != null) { - await _scanSubscription!.cancel(); - } - } catch (_) {} + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); _scanSubscription = null; - // On web, don't clear results immediately so the picked device remains visible - if (!PlatformInfo.isWeb) { - _scanResults.clear(); - notifyListeners(); + if (_state == MeshCoreConnectionState.scanning) { + _setState(MeshCoreConnectionState.disconnected); } } @@ -818,17 +765,11 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); try { - _connectionSubscription = device.connectionState.listen( - (state) { - if (state == BluetoothConnectionState.disconnected && isConnected) { - _handleDisconnection(); - } - }, - onError: (Object e) { - debugPrint("connectionState stream error: $e"); - if (isConnected) _handleDisconnection(); - }, - ); + _connectionSubscription = device.connectionState.listen((state) { + if (state == BluetoothConnectionState.disconnected && isConnected) { + _handleDisconnection(); + } + }); await device.connect( timeout: const Duration(seconds: 15), @@ -837,17 +778,11 @@ class MeshCoreConnector extends ChangeNotifier { ); // Request larger MTU for sending larger frames - if (!PlatformInfo.isWeb) { - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); - } - } else { - // On Chrome Web Bluetooth, give the GATT connection a moment to settle - // before discovering services, which is a common quirk to avoid timeouts. - await Future.delayed(const Duration(milliseconds: 500)); + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); } List services = await device.discoverServices(); @@ -877,44 +812,23 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - // Setup listener BEFORE enabling notifications so we don't miss anything - _notifySubscription = _txCharacteristic!.onValueReceived.listen( - _handleFrame, - onError: (Object e) { - debugPrint("onValueReceived stream error: $e"); - }, - ); - - debugPrint('Starting setNotifyValue(true)'); - if (PlatformInfo.isWeb) { - // On Web, setNotifyValue often hangs indefinitely on the Promise resolution. - // We trigger it but don't await its completion to avoid blocking the connection flow. - debugPrint('Web: Calling setNotifyValue(true) without awaiting'); - // ignore: unawaited_futures - _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { - debugPrint('Web setNotifyValue error (ignoring): $e'); - return false; // catchError must return a bool to match Future - }); - // Give the browser a moment to process the underlying startNotifications call - await Future.delayed(const Duration(milliseconds: 500)); - } else { - // Native platforms handle setNotifyValue blockingly with CCCD descriptors - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); - } - debugPrint('Calling setNotifyValue(true), attempt ${attempt + 1}'); - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; + // Retry setNotifyValue with increasing delays + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } } - debugPrint('setNotifyValue(true) configuration completed'); + _notifySubscription = _txCharacteristic!.onValueReceived.listen( + _handleFrame, + ); _setState(MeshCoreConnectionState.connected); From 2a62390903e3b9327e9df5499f7a0d38396c7da3 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Wed, 25 Feb 2026 21:58:35 -0800 Subject: [PATCH 202/421] Implement debounced notification listener updates in MeshCoreConnector --- lib/connector/meshcore_connector.dart | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef19f02..4c7dbd8 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -127,10 +127,13 @@ class MeshCoreConnector extends ChangeNotifier { StreamSubscription>? _scanSubscription; StreamSubscription? _connectionSubscription; StreamSubscription>? _notifySubscription; + Timer? _notifyListenersTimer; Timer? _selfInfoRetryTimer; Timer? _reconnectTimer; Timer? _batteryPollTimer; int _reconnectAttempts = 0; + bool _notifyListenersDirty = false; + static const Duration _notifyListenersDebounce = Duration(milliseconds: 50); final StreamController _receivedFramesController = StreamController.broadcast(); @@ -3648,11 +3651,46 @@ class MeshCoreConnector extends ChangeNotifier { } } + void markNotifyDirty() { + if (_notifyListenersDirty && _notifyListenersTimer != null) { + return; + } + + _notifyListenersDirty = true; + _notifyListenersTimer ??= Timer( + _notifyListenersDebounce, + _flushBatchedNotify, + ); + } + + void _flushBatchedNotify() { + _notifyListenersTimer = null; + if (!_notifyListenersDirty) { + return; + } + + _notifyListenersDirty = false; + super.notifyListeners(); + + if (_notifyListenersDirty && _notifyListenersTimer == null) { + _notifyListenersTimer = Timer( + _notifyListenersDebounce, + _flushBatchedNotify, + ); + } + } + + @override + void notifyListeners() { + markNotifyDirty(); + } + @override void dispose() { _scanSubscription?.cancel(); _connectionSubscription?.cancel(); _notifySubscription?.cancel(); + _notifyListenersTimer?.cancel(); _reconnectTimer?.cancel(); _batteryPollTimer?.cancel(); _receivedFramesController.close(); From e7a8c36bc4a9753ad6d4f0475e99a3411e7976b9 Mon Sep 17 00:00:00 2001 From: ZIER Date: Thu, 26 Feb 2026 08:51:57 +0100 Subject: [PATCH 203/421] more aesthetically pleasing display of Companionname --- lib/widgets/app_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index e1cda77..ba40e66 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -40,7 +40,7 @@ class AppBarTitle extends StatelessWidget { Text(title, maxLines: 1, overflow: TextOverflow.ellipsis), if (showSubtitle) Text( - '($selfName)', + '$selfName', style: TextStyle(fontSize: 14, color: Colors.grey[600]), maxLines: 1, overflow: TextOverflow.ellipsis, From 64428294c91437cd9c0029ac18db4e5a45c501e8 Mon Sep 17 00:00:00 2001 From: ZIER Date: Thu, 26 Feb 2026 08:59:58 +0100 Subject: [PATCH 204/421] =?UTF-8?q?info=20=E2=80=A2=20Unnecessary=20use=20?= =?UTF-8?q?of=20string=20interpolation=20=E2=80=A2=20lib/widgets/app=5Fbar?= =?UTF-8?q?.dart:43:23=20=E2=80=A2=20unnecessary=5Fstring=5Finterpolations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/widgets/app_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index ba40e66..1afc531 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -40,7 +40,7 @@ class AppBarTitle extends StatelessWidget { Text(title, maxLines: 1, overflow: TextOverflow.ellipsis), if (showSubtitle) Text( - '$selfName', + selfName, style: TextStyle(fontSize: 14, color: Colors.grey[600]), maxLines: 1, overflow: TextOverflow.ellipsis, From e139383335cfe71cf5057ef9ef0e8aaaeb64ee7a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Feb 2026 22:53:52 -0800 Subject: [PATCH 205/421] Add localized search functionality for contacts (#244) - Introduced new localization keys for searching contacts, users, favorites, repeaters, and room servers in multiple languages. - Updated localization files for Italian, Bulgarian, German, English, Spanish, French, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese. - Enhanced the contacts screen to dynamically display search hints based on the selected contact type filter. - Modified the map screen to utilize the new search functionality for contacts without a number. --- lib/l10n/app_bg.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_de.arb | 56 +++++++++++++++++++++++++++-- lib/l10n/app_en.arb | 58 +++++++++++++++++++++++++++++- lib/l10n/app_es.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_fr.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_it.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_localizations.dart | 40 +++++++++++++++++++-- lib/l10n/app_localizations_bg.dart | 30 +++++++++++++++- lib/l10n/app_localizations_de.dart | 30 +++++++++++++++- lib/l10n/app_localizations_en.dart | 30 +++++++++++++++- lib/l10n/app_localizations_es.dart | 30 +++++++++++++++- lib/l10n/app_localizations_fr.dart | 30 +++++++++++++++- lib/l10n/app_localizations_it.dart | 30 +++++++++++++++- lib/l10n/app_localizations_nl.dart | 30 +++++++++++++++- lib/l10n/app_localizations_pl.dart | 30 +++++++++++++++- lib/l10n/app_localizations_pt.dart | 30 +++++++++++++++- lib/l10n/app_localizations_ru.dart | 30 +++++++++++++++- lib/l10n/app_localizations_sk.dart | 30 +++++++++++++++- lib/l10n/app_localizations_sl.dart | 30 +++++++++++++++- lib/l10n/app_localizations_sv.dart | 30 +++++++++++++++- lib/l10n/app_localizations_uk.dart | 30 +++++++++++++++- lib/l10n/app_localizations_zh.dart | 30 +++++++++++++++- lib/l10n/app_nl.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_pl.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_pt.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_ru.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_sk.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_sl.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_sv.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_uk.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_zh.arb | 48 ++++++++++++++++++++++++- lib/screens/contacts_screen.dart | 37 ++++++++++++++++++- lib/screens/map_screen.dart | 3 +- 33 files changed, 1233 insertions(+), 35 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 3613e85..975e067 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Премахване от списъка с любими", "listFilter_addToFavorites": "Добави към любими", - "listFilter_favorites": "Любими" + "listFilter_favorites": "Любими", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchFavorites": "Търсене на {number}{str} любими...", + "contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...", + "contacts_unread": "Непрочетено", + "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", + "contacts_searchContactsNoNumber": "Търси контакти...", + "contacts_searchUsers": "Търсене на {number}{str} потребители..." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 64e8cd3..74b6c05 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "de", "appTitle": "MeshCore Open", "nav_contacts": "Kontakte", @@ -1775,5 +1781,51 @@ "type": "double" } } - } + }, + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Ungelesen", + "contacts_searchContactsNoNumber": "Kontakte suchen...", + "contacts_searchRepeaters": "Suche {number}{str} Repeater...", + "contacts_searchFavorites": "Suche {number}{str} Favoriten...", + "contacts_searchUsers": "Suche {number}{str} Benutzer...", + "contacts_searchRoomServers": "Suche {number}{str} Raumserver..." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8d9f385..175c346 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -268,7 +268,63 @@ "contacts_title": "Contacts", "contacts_noContacts": "No contacts yet", "contacts_contactsWillAppear": "Contacts will appear when devices advertise", - "contacts_searchContacts": "Search contacts...", + "contacts_unread": "Unread", + "contacts_searchContactsNoNumber": "Search Contacts...", + "contacts_searchContacts": "Search {number}{str} Contacts...", + "@contacts_searchContacts": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchFavorites": "Search {number}{str} Favorites...", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchUsers": "Search {number}{str} Users...", + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRepeaters": "Search {number}{str} Repeaters...", + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRoomServers": "Search {number}{str} Room servers...", + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, "contacts_noUnreadContacts": "No unread contacts", "contacts_noContactsFound": "No contacts or groups found", "contacts_deleteContact": "Delete Contact", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index dd8ce6c..74339ff 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1781,5 +1781,51 @@ }, "listFilter_favorites": "Favoritos", "listFilter_removeFromFavorites": "Eliminar de las favoritas", - "listFilter_addToFavorites": "Añadir a favoritos" + "listFilter_addToFavorites": "Añadir a favoritos", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchContactsNoNumber": "Buscar contactos...", + "contacts_unread": "No leído", + "contacts_searchFavorites": "Buscar {number}{str} Favoritos...", + "contacts_searchUsers": "Buscar {number}{str} Usuarios...", + "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", + "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index fe738f7..0697aee 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1753,5 +1753,51 @@ }, "listFilter_addToFavorites": "Ajouter à mes favoris", "listFilter_removeFromFavorites": "Supprimer des favoris", - "listFilter_favorites": "Préférences" + "listFilter_favorites": "Préférences", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Non lu", + "contacts_searchFavorites": "Rechercher {number}{str} Favoris...", + "contacts_searchUsers": "Rechercher {number}{str} utilisateurs...", + "contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...", + "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", + "contacts_searchContactsNoNumber": "Rechercher des contacts..." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d6c02f0..4798d26 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1753,5 +1753,51 @@ }, "listFilter_addToFavorites": "Aggiungi ai preferiti", "listFilter_removeFromFavorites": "Rimuovi dai preferiti", - "listFilter_favorites": "Preferiti" + "listFilter_favorites": "Preferiti", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchUsers": "Cerca {number}{str} Utenti...", + "contacts_searchContactsNoNumber": "Cerca Contatti...", + "contacts_searchFavorites": "Cerca {number}{str} Preferiti...", + "contacts_unread": "Non letti", + "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", + "contacts_searchRoomServers": "Cerca {number}{str} server Room..." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d64cdb0..ff2c726 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1336,11 +1336,47 @@ abstract class AppLocalizations { /// **'Contacts will appear when devices advertise'** String get contacts_contactsWillAppear; + /// No description provided for @contacts_unread. + /// + /// In en, this message translates to: + /// **'Unread'** + String get contacts_unread; + + /// No description provided for @contacts_searchContactsNoNumber. + /// + /// In en, this message translates to: + /// **'Search Contacts...'** + String get contacts_searchContactsNoNumber; + /// No description provided for @contacts_searchContacts. /// /// In en, this message translates to: - /// **'Search contacts...'** - String get contacts_searchContacts; + /// **'Search {number}{str} Contacts...'** + String contacts_searchContacts(int number, String str); + + /// No description provided for @contacts_searchFavorites. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Favorites...'** + String contacts_searchFavorites(int number, String str); + + /// No description provided for @contacts_searchUsers. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Users...'** + String contacts_searchUsers(int number, String str); + + /// No description provided for @contacts_searchRepeaters. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Repeaters...'** + String contacts_searchRepeaters(int number, String str); + + /// No description provided for @contacts_searchRoomServers. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Room servers...'** + String contacts_searchRoomServers(int number, String str); /// No description provided for @contacts_noUnreadContacts. /// diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index f9637aa..da7ddc9 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -681,7 +681,35 @@ class AppLocalizationsBg extends AppLocalizations { 'Контактите ще се появят, когато устройствата рекламират.'; @override - String get contacts_searchContacts => 'Търсене на контакти...'; + String get contacts_unread => 'Непрочетено'; + + @override + String get contacts_searchContactsNoNumber => 'Търси контакти...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Търсене на контакти...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Търсене на $number$str любими...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Търсене на $number$str потребители...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Търсене на $number$str повтарящи се...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Търсене на $number$str сървъри в стаята...'; + } @override String get contacts_noUnreadContacts => 'Няма непрочетени контакти'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1574281..228ffe7 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -677,7 +677,35 @@ class AppLocalizationsDe extends AppLocalizations { 'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.'; @override - String get contacts_searchContacts => 'Suche Kontakte...'; + String get contacts_unread => 'Ungelesen'; + + @override + String get contacts_searchContactsNoNumber => 'Kontakte suchen...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Suche Kontakte...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Suche $number$str Favoriten...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Suche $number$str Benutzer...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Suche $number$str Repeater...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Suche $number$str Raumserver...'; + } @override String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 039ad22..70b3393 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -670,7 +670,35 @@ class AppLocalizationsEn extends AppLocalizations { 'Contacts will appear when devices advertise'; @override - String get contacts_searchContacts => 'Search contacts...'; + String get contacts_unread => 'Unread'; + + @override + String get contacts_searchContactsNoNumber => 'Search Contacts...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Search $number$str Contacts...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Search $number$str Favorites...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Search $number$str Users...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Search $number$str Repeaters...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Search $number$str Room servers...'; + } @override String get contacts_noUnreadContacts => 'No unread contacts'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8961b18..876666b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -678,7 +678,35 @@ class AppLocalizationsEs extends AppLocalizations { 'Los contactos aparecerán cuando los dispositivos anuncien.'; @override - String get contacts_searchContacts => 'Buscar contactos...'; + String get contacts_unread => 'No leído'; + + @override + String get contacts_searchContactsNoNumber => 'Buscar contactos...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Buscar contactos...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Buscar $number$str Favoritos...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Buscar $number$str Usuarios...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Buscar $number$str Repetidores...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Buscar $number$str servidores de sala...'; + } @override String get contacts_noUnreadContacts => 'No contactos sin leer'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3737f46..0c11eac 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -681,7 +681,35 @@ class AppLocalizationsFr extends AppLocalizations { 'Les contacts apparaîtront lorsque les appareils font leur annonce.'; @override - String get contacts_searchContacts => 'Rechercher des contacts...'; + String get contacts_unread => 'Non lu'; + + @override + String get contacts_searchContactsNoNumber => 'Rechercher des contacts...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Rechercher des contacts...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Rechercher $number$str Favoris...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Rechercher $number$str utilisateurs...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Rechercher $number$str Répéteurs...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Rechercher $number$str serveurs de salle...'; + } @override String get contacts_noUnreadContacts => 'Aucun contact non lu'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b64ec6d..8a8fe71 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -677,7 +677,35 @@ class AppLocalizationsIt extends AppLocalizations { 'I contatti appariranno quando i dispositivi pubblicizzano.'; @override - String get contacts_searchContacts => 'Cerca contatti...'; + String get contacts_unread => 'Non letti'; + + @override + String get contacts_searchContactsNoNumber => 'Cerca Contatti...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Cerca contatti...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Cerca $number$str Preferiti...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Cerca $number$str Utenti...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Cerca $number$str Ripetitori...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Cerca $number$str server Room...'; + } @override String get contacts_noUnreadContacts => 'Nessun contatto non letto'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 523cd6e..8b4eee5 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -674,7 +674,35 @@ class AppLocalizationsNl extends AppLocalizations { 'Contacten verschijnen wanneer apparaten zich aanbieden.'; @override - String get contacts_searchContacts => 'Zoek contacten...'; + String get contacts_unread => 'Ongelezen'; + + @override + String get contacts_searchContactsNoNumber => 'Zoek contacten...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Zoek contacten...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Zoek $number$str favorieten...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Zoek $number$str gebruikers...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Zoek $number$str Repeaters...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Zoek $number$str Room servers...'; + } @override String get contacts_noUnreadContacts => 'Geen ongelezen contacten'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 1639600..cff6010 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -678,7 +678,35 @@ class AppLocalizationsPl extends AppLocalizations { 'Kontakty będą wyświetlane, gdy urządzenia reklamują się.'; @override - String get contacts_searchContacts => 'Wyszukaj kontakty...'; + String get contacts_unread => 'Nieprzeczytane'; + + @override + String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Wyszukaj kontakty...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Wyszukaj $number$str ulubione...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Wyszukaj $number$str Użytkowników...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Wyszukaj $number$str powtórników...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Wyszukaj $number$str serwerów Room...'; + } @override String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index c59a4f5..831d47a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -679,7 +679,35 @@ class AppLocalizationsPt extends AppLocalizations { 'Os contatos serão exibidos quando os dispositivos anunciarem.'; @override - String get contacts_searchContacts => 'Pesquisar contatos...'; + String get contacts_unread => 'Não lido'; + + @override + String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Pesquisar contatos...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Pesquisar $number$str Favoritos...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Pesquisar $number$str Usuários...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Pesquisar $number$str Repetidores...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Pesquisar $number$str servidores de sala...'; + } @override String get contacts_noUnreadContacts => 'Sem contatos não lidos.'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 69d6044..5c73e3e 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -678,7 +678,35 @@ class AppLocalizationsRu extends AppLocalizations { 'Контакты появятся, когда устройства начнут рассылать оповещения'; @override - String get contacts_searchContacts => 'Поиск контактов...'; + String get contacts_unread => 'Непрочитанное'; + + @override + String get contacts_searchContactsNoNumber => 'Поиск контактов...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Поиск контактов...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Поиск $number$str избранного...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Поиск $number$str пользователей...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Поиск $number$str ретрансляторов...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Поиск $number$str серверов комнат...'; + } @override String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 17cbd7b..b4f28fb 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -671,7 +671,35 @@ class AppLocalizationsSk extends AppLocalizations { 'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.'; @override - String get contacts_searchContacts => 'Vyhľadávajte kontakty...'; + String get contacts_unread => 'Neprečítané'; + + @override + String get contacts_searchContactsNoNumber => 'Hľadať kontakty...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Vyhľadávajte kontakty...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Hľadať $number$str obľúbené...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Hľadať $number$str používateľov...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Hľadať $number$str opakovače...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Hľadaj $number$str serverov miestností...'; + } @override String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 6478b2b..e015e45 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -672,7 +672,35 @@ class AppLocalizationsSl extends AppLocalizations { 'Stiki se bodo prikazali, ko se naprave oglasijo.'; @override - String get contacts_searchContacts => 'Iskanje stikov...'; + String get contacts_unread => 'Neprebrano'; + + @override + String get contacts_searchContactsNoNumber => 'Iskanje stikov...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Iskanje stikov...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Iskanje $number$str priljubljenih...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Išči $number$str uporabnikov...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Išči $number$str ponavljalnike...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Išči $number$str strežnikov sob...'; + } @override String get contacts_noUnreadContacts => 'Ne prebrani stiki.'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 54b998d..3a25c3c 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -667,7 +667,35 @@ class AppLocalizationsSv extends AppLocalizations { 'Kontakter kommer att visas när enheter annonserar.'; @override - String get contacts_searchContacts => 'Sök kontakter...'; + String get contacts_unread => 'Oläst'; + + @override + String get contacts_searchContactsNoNumber => 'Sök kontakter...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Sök kontakter...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Sök $number$str Favoriter...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Sök $number$str användare...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Sök $number$str upprepningsenheter...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Sök $number$str Room-servrar...'; + } @override String get contacts_noUnreadContacts => 'Inga oinlästa kontakter'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e3564f1..cd820cf 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -676,7 +676,35 @@ class AppLocalizationsUk extends AppLocalizations { 'Контакти з\'являться, коли пристрої надішлють оголошення.'; @override - String get contacts_searchContacts => 'Пошук контактів...'; + String get contacts_unread => 'Непрочитане'; + + @override + String get contacts_searchContactsNoNumber => 'Пошук контактів...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Пошук контактів...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Пошук $number$str улюблених...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Пошук $number$str користувачів...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Пошук $number$str ретрансляторів...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Пошук $number$str серверів кімнат...'; + } @override String get contacts_noUnreadContacts => 'Немає непрочитаних контактів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b55c376..b63f714 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -636,7 +636,35 @@ class AppLocalizationsZh extends AppLocalizations { String get contacts_contactsWillAppear => '当设备发送广播时,联系人将显示。'; @override - String get contacts_searchContacts => '搜索联系人...'; + String get contacts_unread => '未读'; + + @override + String get contacts_searchContactsNoNumber => '搜索联系人...'; + + @override + String contacts_searchContacts(int number, String str) { + return '搜索联系人...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return '搜索 $number$str 收藏...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return '搜索 $number$str 位用户...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return '搜索 $number$str 重复器...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return '搜索 $number$str 房间服务器...'; + } @override String get contacts_noUnreadContacts => '没有未读内容'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index fee1f22..69ebecc 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Verwijderen uit favorieten", "listFilter_favorites": "Favorieten", - "listFilter_addToFavorites": "Toevoegen aan favorieten" + "listFilter_addToFavorites": "Toevoegen aan favorieten", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Ongelezen", + "contacts_searchRepeaters": "Zoek {number}{str} Repeaters...", + "contacts_searchContactsNoNumber": "Zoek contacten...", + "contacts_searchUsers": "Zoek {number}{str} gebruikers...", + "contacts_searchFavorites": "Zoek {number}{str} favorieten...", + "contacts_searchRoomServers": "Zoek {number}{str} Room servers..." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8096bb4..75e1d34 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Usuń z ulubionych", "listFilter_addToFavorites": "Dodaj do ulubionych", - "listFilter_favorites": "Ulubione" + "listFilter_favorites": "Ulubione", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Nieprzeczytane", + "contacts_searchContactsNoNumber": "Wyszukaj kontakty...", + "contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...", + "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", + "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", + "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 53f8f93..f6ada19 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1753,5 +1753,51 @@ }, "listFilter_addToFavorites": "Adicionar aos favoritos", "listFilter_removeFromFavorites": "Remover da lista de favoritos", - "listFilter_favorites": "Favoritos" + "listFilter_favorites": "Favoritos", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...", + "contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...", + "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", + "contacts_searchContactsNoNumber": "Pesquisar Contatos...", + "contacts_unread": "Não lido", + "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b4d7b59..9aef298 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -993,5 +993,51 @@ }, "listFilter_addToFavorites": "Добавить в избранное", "listFilter_favorites": "Избранное", - "listFilter_removeFromFavorites": "Удалить из избранного" + "listFilter_removeFromFavorites": "Удалить из избранного", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...", + "contacts_searchContactsNoNumber": "Поиск контактов...", + "contacts_unread": "Непрочитанное", + "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", + "contacts_searchFavorites": "Поиск {number}{str} избранного...", + "contacts_searchUsers": "Поиск {number}{str} пользователей..." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 23a0aea..672b7d7 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Odstrániť z označení", "listFilter_addToFavorites": "Pridaj do obľúbených", - "listFilter_favorites": "Obľúbené" + "listFilter_favorites": "Obľúbené", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...", + "contacts_searchFavorites": "Hľadať {number}{str} obľúbené...", + "contacts_searchRepeaters": "Hľadať {number}{str} opakovače...", + "contacts_searchUsers": "Hľadať {number}{str} používateľov...", + "contacts_searchContactsNoNumber": "Hľadať kontakty...", + "contacts_unread": "Neprečítané" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 6b64f68..09359a1 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1753,5 +1753,51 @@ }, "listFilter_favorites": "Priljubljene", "listFilter_removeFromFavorites": "Odstrani iz priljubljenih", - "listFilter_addToFavorites": "Dodaj v priljubljene" + "listFilter_addToFavorites": "Dodaj v priljubljene", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Neprebrano", + "contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...", + "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", + "contacts_searchContactsNoNumber": "Iskanje stikov...", + "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", + "contacts_searchUsers": "Išči {number}{str} uporabnikov..." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3b96df7..a923cc9 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Ta bort från favoriter", "listFilter_addToFavorites": "Lägg till i favoriter", - "listFilter_favorites": "Favoriter" + "listFilter_favorites": "Favoriter", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Oläst", + "contacts_searchContactsNoNumber": "Sök kontakter...", + "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", + "contacts_searchFavorites": "Sök {number}{str} Favoriter...", + "contacts_searchUsers": "Sök {number}{str} användare...", + "contacts_searchRoomServers": "Sök {number}{str} Room-servrar..." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 78b4a90..c86f11c 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Видалити зі списку улюблених", "listFilter_addToFavorites": "Додати до улюблених", - "listFilter_favorites": "Улюблені" + "listFilter_favorites": "Улюблені", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...", + "contacts_searchUsers": "Пошук {number}{str} користувачів...", + "contacts_searchFavorites": "Пошук {number}{str} улюблених...", + "contacts_searchContactsNoNumber": "Пошук контактів...", + "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", + "contacts_unread": "Непрочитане" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6b8fc09..63c02a5 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1758,5 +1758,51 @@ }, "listFilter_favorites": "收藏", "listFilter_addToFavorites": "添加到收藏", - "listFilter_removeFromFavorites": "从收藏中移除" + "listFilter_removeFromFavorites": "从收藏中移除", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchUsers": "搜索 {number}{str} 位用户...", + "contacts_unread": "未读", + "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", + "contacts_searchContactsNoNumber": "搜索联系人...", + "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", + "contacts_searchFavorites": "搜索 {number}{str} 收藏..." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 6c683cc..eeecfb9 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -402,6 +402,41 @@ class _ContactsScreenState extends State ? const [] : _filterAndSortGroups(_groups, contacts); + String hintText = ""; + + switch (_typeFilter) { + case ContactTypeFilter.all: + hintText = context.l10n.contacts_searchContacts( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.users: + hintText = context.l10n.contacts_searchUsers( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.repeaters: + hintText = context.l10n.contacts_searchRepeaters( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.rooms: + hintText = context.l10n.contacts_searchRoomServers( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.favorites: + hintText = context.l10n.contacts_searchFavorites( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + } + return Column( children: [ Padding( @@ -409,7 +444,7 @@ class _ContactsScreenState extends State child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: context.l10n.contacts_searchContacts, + hintText: hintText, prefixIcon: const Icon(Icons.search), suffixIcon: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index b688a30..2ec71a0 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1301,7 +1301,8 @@ class _MapScreenState extends State { padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: TextField( decoration: InputDecoration( - hintText: context.l10n.contacts_searchContacts, + hintText: + context.l10n.contacts_searchContactsNoNumber, prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), From 57ea30cae966ecbefc3562a7b936eab57688c124 Mon Sep 17 00:00:00 2001 From: Specter242 Date: Fri, 27 Feb 2026 14:30:15 -0500 Subject: [PATCH 206/421] Unify signal indicator UI --- docs/screenshots/signal-ui-consistency.png | Bin 0 -> 106337 bytes lib/widgets/device_tile.dart | 34 +++++++++--------- lib/widgets/signal_ui.dart | 38 +++++++++++++++++++++ lib/widgets/snr_indicator.dart | 32 +++++++---------- 4 files changed, 66 insertions(+), 38 deletions(-) create mode 100644 docs/screenshots/signal-ui-consistency.png create mode 100644 lib/widgets/signal_ui.dart diff --git a/docs/screenshots/signal-ui-consistency.png b/docs/screenshots/signal-ui-consistency.png new file mode 100644 index 0000000000000000000000000000000000000000..2575945a21d3122a7851322942d81e91221133a9 GIT binary patch literal 106337 zcmeFZRag{o*DpGxh@^rbU4npssB|f9Al)V1B`6(&bfZW}NJ*E{9Rh+N-QC^Yd(HcO z-*e8nI{RkdZ5}Upm~m$K$6CKy1j@aU!o?=TMj#NlFQmm45D0W>1Ogj@g$X}Nl_d#6 zAW&G%#KhzjEzI-~hzF7G5hAaepOLnDe)aau@>yAlC=OO2uF85v9Q;OsUM;cj=?mX7 zKZXn}YY!BWPcs`2L``mBJhvqh@#d{)Wx5%~eke3mJuP%>>$rW(_a0FT_Q9EaPu6hA z=cR1U%l0plWM3)IUgK4IOg-5i?+~eb!8!gxRC7J%C*_Krwpmb2O^CqF^2c>Aok&k3 zPZ(z#EH?sdT8wN$Gz~1zpU-*TwK=Rp6S(HNpXuqA{$94SlYRNqi9>P4@0kePuJ@ZR zI03)K_~`gkBmCZ&(|vvxR!2l4^}4TqBXAvmgsR2vBG%#teMMo@gjhi|k6sG8EJx+n zFtVKp45+G z%KB~aO!R+Vg$nKVf4>IHE@=e8e?3_~u+8h!e?R-B;Q#BTSIu2f#l>3^pV4^L)d_cY zcD~iO#G%xtl+Jh@&|)V2`nBl0caIGp$CYKi_|F?8d%*|jmBR8Bt*ER_(NyJlYBPmn z^s>6KukSWpBvxEpob=0=lBQ}83UuoOeQw~-4q16hGu*b4{RZzYZ9v9l9uPR+w&e0} z`xZ%a_qI73v5}xFA8NgD?Q$s=h_-xjhl9EzQEw;I-i9*-e>AQU< z-iYT6fl7f!=+2HUTUR{1qHD(&W;FcpprT);zM-KqL)YVD4Sh|hcDuU)K_Q|_4o_24 zQwi=7hnjRJ(bLd8%eN<3T^G}35LHl!(r6nj`N|gGd^~aLmdHwkO-qCw85x-}tM0Cq zCW&h=qBlF28iy4bk6e8Wf4mKe&y1mq;m6dx0kiayo(FCzasl6XF64eVn4NN3XiCYv zk)Ic*BV?4J2q$dz7iGA;xK5_2sTo2lOoY2O+}!Y7jF5;TZ&v*_A)%RWZPojk$$Zwa z5;I(q7zKnL%hq;62~y~8mxGfRG2p8CkHiT5?B$XyTB!o~HAjm3IQ zIzsr2k#TX^v-gbSc=faz|53xkd^xWRPP7M4qP}%Rv!vL~`>9o1i=`CUOxIDyyHBEr zg@rxh;%Yn}P^!7*>~Aq_Gc%LYtU>upiP_{}dzfU4n6+tO0KJ!!v%+JZvCL{5rC3cX zt1{9Ym6hCIe_DM<(Q`yOj@#%li)uCwA|N=p%{Q2&*kYKfG&K2}th>}+X>?5X2Ik4HvSd$@8$Y<&FORH9~4(IZutE&F^LLM-&1 zts&x9F%LgQM)op=mO4(Ogi!LJFf%io$5Rp#x`&CMojp)2!_j&f?X4s1I5b3U7>CBnI76?id=93crkT6L>w)kxv@U z#s7Df)IUNoNGm{rf~e0Oo>?7a>$ZS1C0A&zjfIYSGF=(GvSJ$guUJ0KkN2Hvy+*4> zku9f(Zjm&bfzhw>@;0971M#+_O}SbZF4*UW%iJ6cvNX+M_E=s$ijEkL<6e7IAMZyt zQ`;lu*Uuuu`M!JVUhrFBzJC4sOVTYYtcLRGQ2AGf#74|zD1rPQBO*Tx+Kxu{j+3$5 zP^ao7Hu-S}9@nv~ISE~!9W|cVi8POnuBI)2^7l8`jWRmfFYHR@H(FU)`L5CxPx{!9 zhK$=3At0c+vSwbk@_?QmjgT<1!ex`6`|^}zYoaoD*RaNN^rz``0WOP5o>vTq?rhI2 zJr*8b?~0JN*2|RO7^bV^`aP$k4Zb%Cg84^W6&M-0xxoRC7L90gP;Uze2nf2nU8fni z7oY_C`tluZjD6H7HNSf@R#NhGqHZ&sMLDNoVgjeVy?sT*7K>e{CjDdFZNlE=hTp%@ zb&?W$iw#^y#z&vWc5}p}$HifV2Q zdm^%xa*CaE&h|DGv*v|Tr5Qxa3$%MOf|fKXLJlX((;iu^ah#ro z&_}=Iw7Eiks9jd`r`2+}443=5_J#0QC6<=+<89)msG=g<66Q;GE>+gLaqw1oyr9aBG%LJ;%W%qM><`67B3* z!vAP?++nWM_2lv9YlYodZNJE-tnoK8X4sSw%+DHlBYucDh&y z)YwW3`t|EhP4)Zd)55iS7ACT3 z_#|!Um0113qO@|khOd5Uy~cbnXKq2HZ6Q2EffXCA(k>`C(F-dW?=hL)5M2&!t*5uQai4_4 zcKM$+Zr39l)Q<=;Kb)ozGezObs-NGFttKi6FW7Y(@^qJHy6d^c7|&0Rt1@0yr$E!% zYs=Rtd$!mSZF{^`n;L8V(Ty~0Ato}CaARX*A>7K!@~yeK_oq+Z98w*Hso1~w($s(b zSm~W4XVWb83N1Zk3LmaRQE$J$E?PDo`_leE@kfZ{cFNZ9{=Te%k{>;s?|{%?pVDYzXdJ{z z>%D0L-rhc(*X0<#Rhe~lROUl@VJi8WO@IDi+1c$@%q{!xFLTh)(h{2Kb3Ps%;M0^+ zC4oBs@L^|fsX_x#3=FnlK%4X_EhW}ZH9$5vcAcc6 z^77`X+HIB>*Jz(Sxl_zpTvS#j+U$2|cetjUkHK(u=0f_^ z6ycTEKo1YQxa9ox@)b=HwFy%wFe*L^Czg0DE7LbP*it?n*5Rl)WC*#<0;X4@#iL*0r@d1YfwCMkCZCzBy)rV%wnw!VAn3$M2 zv$U2|>LpG?Fq28`*}(CciDh$1MF|#Zcl@IA5!aLEpGG;;HUO4zC?8AD`ecTM;rZcH zdfU`fr}bh7d$U;b(h*?ItgWRIl-xu|t*EM)k)Qjl)ttuBm=Ro;bug%EPHEqIV1~Wz zI9=}?dG8TkjG@u4Sp59_Z|6SQ6%`jhUsqx4Jy~D!UE5UbGZmug{f&R# z_B2Zu|MKj3a7Ad~(#0|_fA`Wc=r4I+Utf{w?&Cb|a;og?>_<VdlLlNzUP8TPfUmvRbG07%pYu(Dp&AkPOFhHEH#^q3B zF?R89jF^T7SyMB;QdkOLzv| zDDPV&9CQo|lx}V{y*28Jyd&ey?d^o;N7Ukmh6_xSZU6q^P1aU7RcoqNRI7?ZeLUKl z#AjlXDzP5Cy<}|6GE96o8s+j_>;Vxf;%HOA%X(5+2lpNF%0S1xMRMU6mRR-d;oatm z*OmC~%dJgKsE6x=Y&~3a2M!lU=c4fYqdbnovC=4Ssw(yTgT^4k@q(W2>^5vMDJkhQ zU0r-A4EOHENs|NzD{qH3_4Nj2qUQbNdQ83GB2${9+%s9tibSqL+O=-S zY`vf2p8S}twvjU1yc+!^O8Y}54(FD=eP;rf=BhJb$b*g9G>3`5xlcS2pOlY06nbFE zi;6fqJF|7LmE+%K_2_F(}Enpy~xt+Y}lJb{+W?t&!_yxr+bSFjfRGX3{*lMd5``@Q^?qC zZfx$P=$dqS!KQfSJ@cKFzB0X>tgiALv43K-Wf^dAILN=`4@ae@hQGP9^Y!dVP_*ZR zJjGWsGA(oW|6Z~6l5vPwTUv&|eOCMR!Tsa5g{38q{eIp#^aUq!(tsQI)b0mvaKkPN z-JBh^CT=SJc%ypu;des=VmgeAn$TVo`D>7w4tFP+jsApl4{R)W*VDCC(|@a zb9Pp@Lf=pdH1zhO;8P1DAG5ni_MDmTr|kO1H8@uOcyy`ze7mc)spZw|<|Nnf{`FK1 z@i{&PkcQich8}5qPf6rE%8hm5=4+OV!glTma}m$Z&IW}1hjKJEH%BHUgbWNQrKhLU z++VD1sxq{&V0D)M!p6day*^~)ouAKDw>qdeTi!$gI4h6x`27h(#lG{!15jv zRbHMSJMHd8H|^iN8Cc;yPHpn7XNK}b6rwRdRfF7cHrm87I35VWRs9u!z4g>#EP zW@jbLEp!y^tLNL2$fEgcx$`si9V~QG-tWnM=SDQSE{-ETw6Q`aNkgm;m3--+H#XY7 zw`(6h%y`L{o43_9HrT4HqyC)@-tuJmcRQVW)c)Bad4hl^74lMsZ%qM8ll$1VGBo&V z)=o{0SEW!rb8iBdoM(2tm!6pHPPtXT1~ZNPaV+1oZf5RE#^6_i2nY@TK1=v_X{$p+0_+qmobOnUKWJymL1RBO`2hfAGfO1N~yP zBz_OmuCc?D4+qk|G(U3E9U)Vv>OMXZ#?BJE_T{qlBq-emSCgy>$;{SyW|@=cb={f zkiqN(Ph(@o)U-505|Z%PSWWSu;G8d7JA7&5O;G!^*FP` zIXFB-R?PdaVhlFxXASy)$UO3#qTuCc@%$wwE*|9Y{=J@sg^wgId8+R8RF-0SSRaWR zARVX6Gu#gw6TXxJA7~l5RbKt-s>i-&4X<-|Nz?`-g?)lso8Ga_er6qhmZiMvZ&$Hy)e$hL1jg<*P27;1v0iZL{Lz`?i%| z+HVUvtshzWK1$QzT9hnWqk3|Gv7RGA*}dJy@cn~EHk?}dJpQR>(NHbAvuq7 zQs&mMfqBk}70_fHeS>+Wq;GJm0U7&xi7Uq^Q3KS$x^>H6obF>%64iv3wtU9B`&!i= zVvDD+h)@c|p4i$PX1jJ1t&&EM$#us z0!q0m#0uF&<<{q7>4nJNI5Iwt_9y4v&+_sZ5zL=M_KZwS^B~)#=RR00(5}9#QD%xa zZNFTszeo)-f)HZ}b#Tk8uX>wfhXk3KX;$Bc)Qa71=5bAk1O}|U`VRDIZ}IOe;2|%^ z@`Al*nk>IF7?-v zTUuHQpI!C+{!J<@JT+3XT9_J}8WrwKpL?a|1H#Fo1rC#x)XMs*dkMBA=p+?-`SfF3w|3w6e>JBp1ipwJu3PW%dHs|_Bn!))Iiselw5rYR%*Fx@ zb_+z76g{6a>|_qjrL3p7x{5JhO1=pVMm2+_#^}0Mu-J<@WytMql zHKyG#R>+lVti(EcdtI~4G;DL6`|sZ|mlLP7Tu1zgs`r|!TJP(V78Zzn+c5nqw?FOA z?o;QU{CQ<=&Z1N6;lpEtDp#zJ@r8ZUZ>j6s?7zh7mloc>qO?%?cV!Bb9%%n~3dc`5 zjF|wd=a&?|<#h3#-rnAqxW7O*Fzrpf2S5WC0k@iKs^;c14ML>|(tGz(p8K5r`P10| z(03M?Cq9*+xaI4Isi|qbEr0KDnJf9{C$ep9ZWh~If7Ei#cXC`0vT-^5^!amIO`M9P zA9{-3YzqrTHk=Ol5GaV^VmsfdDK*5VN^B zv*bjrD?X3Sq$JcoSVLbCm3*&^jI$~9JB04pP=>X%v>tg(bno?T1N?v;r4yv1DAcL- z1A>KAOyKoE-k|9lmWnCR0aO}$@K-5X?}JCXp@KBSJ#%$)y|-^82BnpCzbL0<`A*V? zgoL~`uu2?6a0}(4c=3nwS1nY~5U zN}cstor<6}*=S8(1p>Hd&X}LZwB~YG*9hPD_xJm>+@LjZa@xAY6WQ=LH8#d{b#*

AVbAxEifRDLyEOz`(#* zZg=AX`~P&pr&c`;SEgeJ7N_%{nPg`{0mHzSu5#E_QrnNI?GMcH5}(|fuE(FL|M}d4 zl@)NktkHd`=#FT)j#2elHJ#9gcl!&wceH98ZUQ&;jgBTJdzuB3M(zVEPYa;>0N6;m z-l6B^Jrv`c27uY_oKdLt?m6wNSGSNz zdq_nExwqSu_E^|>tv^JrMNLgfk>?#w9Vw^VgM*r^M!Xy6WV>;bxMMXW097o|!+9276aVP*Rat|M$)Lo%-u&e`#H6 z?P?cV7XGu(WtO3#!Oidnd$mg3Jl56)D_fJeK=^6-_%Jlftj`xXZ{_H>V6RLUa=d;W zvU_Aj0|X!dWa!*OD(LeolbIr6ltNiYQ(hR6U%q@2c0bR>No@^H&hovtD5E`#g%ET; zQ_}8NsNPtM><+B0P3&_$sHtn{tem+2Eqc(C{DrChYXrbDD!h$xTzYzXq(q{5b)~o( z4}{vFrDG7`#b@u~J6nHAu3DgxXhM98`8*XZY9l}NXG3dC<*|Tawpt1* zTv+51v#MSYkddjZoRatfw_6*^M`LAWh1xo|Qaq1!)7k&>1|A(R1G>-LTu-X=lY^aA zJRVh5%gHjd%Bb>9?pxj-ks-ekLfJMhbgL)I+Ka_cnN zX%uty90LoBU}Kqj$*y=Vf;;5Fzv}8FsDcFe`H`QxsO)FcWc9J_Zl4ii4Obd51E`<$ zsUa!IkSPP5H4mgS6mX~>Q{Ynz{X1XK7fqa%wmYs=4o7%p1TES`g-rM;DwDsCUBtrW z;$)0CJRbVq*j(h)i~5CSG0))&q%_jYNqlg4en&K-^=9dzAsQ~v@rw*s-(~DG+vqwx zdwf0|x3`FfD0(+YjpcM+)-a!^^XqO`ygQps&HT3KjrGA1CsrO{nNX7Ms^n|1+x$}m z)N+!<7YH^6Es%5yO3I$nV3LVR){eqgvN3Sz;*tjQ+Knh%#w?;;Rz^)Q_ z?XZuxME;cO2L4!CU2TN(IPETg4dMgRCQ1@^^~+T)1m_G@K2m^lF-4K2rKq?1MqQFOKDTn}Hl!6<9XOIucy9s4rf;Xq^65 zp6Y)7!PfqL%bwAz$S(<>($g6jn3(7vKgRy}@j3W}4dV^ZKKc66Gc(`Z-MtAEOBA#X zXlx+2H-l^_SE+D6!xaRcw=F>#74O%%&4;q^1Oyg02ZR&)fiKw$Wj+Am?gvrW!olWB zq|?p}E$fQ_Y5LbPGVTLdH(oIW%1sWNMyM*Wj&k~ZtzI-7< z$H36*3^%Yl8?cKGEy-R<5b-3mU%vD+Q}krsbL~#N7WkJ~FSl^a_E35D&+m4{BD=Ib zp6g@*k^0l+X5D^fz%1FusG`ih80AIg8_I?I2?)?Qg&u0w{gg8R4v;4Cw=7GimMmR? z`Qmar%Od{t-OLOvRQR_`A!d6&azO(+HyvJISTMZccP-E$+k+*Xzc{^Ry0cy0nK3!h zQ|rnzQR_$zz8nFT*YDp9z75ihpP8d$jXZOs+y$5M9vmGV?dD)6e4q(En^qt^H_BU<&%iWxozaRW=X_*fnv?z94 zy&YYxrLL)-0w~y-Ju#}?kk@{Q1iUS?xy8F3&~EnLaUMVSh#kzZE zd_bY@1LH;I=>8}1p}ScsOn>wzab?mk+eVwc;|S_IJ4bA-va#u{aoInzZ5NPaiA`7e zg?qBb!Yez$>MR>Y+>S(Xy2gLn)y4KPYs)5G3G5s=Bk;ZG6IGU2j>j~doScn$as7++ zIKOh4g?+{8n4^;Dsed{DIJS$)hPUceub{>S8)Lz0uag=$PVk zz&;=VTa4j$V=t`*NiiHeXAZ&?IV>hACN}|>bhqPb_+S+dtB=E|1j3NVQt*SI2bI88 zn!)l?TfYCh`v1uSa84UsR3z)Yjd1**(R(wqV(A$fja^;1pt;Ph%pl@k`~6idn8}&uVqh?}DLli!M@E*a*NjN?AMgve?pyXPUa^p&+9gw8_YwYAO8|I8{i2KCXWfAgg(-tu$5R z?4#{^q~Ea+h@rFg)NO0Sgy=f1VH{N&+&PP8IZ3MN#ufA}N2J`T$u3GpvL zSw%+$t~q8E)Sc zZ4N54(bXBGztg&Gb8EgkjW52s)EKI}S?NmDyctIIQ%%nT?$u zwBU6eBAF#51Qqw$`cQt)cMlFFhWRA^EH-s)($Nwgx!eVuMhZv%?6#c zsjZD1WS&pOk%asXrvANYl37|jF^a&AKw0x2F4Prc=@Q4MwgIjzCM$~%)r0rl0;x_t zvj_FV!-IpF*{!fuMbGEVCMEzdi55G)w#+<76EV~JdXX~?f;eZ9r$|M`gKgPlauA*` zD$Rt>@F_yTNzgwFQFl5!xYv4hp;%YO)A{F5LmjNE$<&2l`TKrM&ual^KWB+rw}^n3 zVId)}87|KKv{Y4Pr+T216PI6Z(>;8sRd-8VRFn?nX1012kjnY_E9;HADm6a=tZwq0 z4qwU1=0IFBJ$;G;3RF{M^Ep@F7ah7tAYad&j@h5;E;HS~ueblzidx&H&iSv3Z^q=> z4;BjN{iRS1*D!!{G`zf|i+`gPPu>1hJF<{_mfCZvE|nt*ObGZBLSQ z9ygaX>0-4IfTQy#yLU)=i$wDB^Lw)uZzS-)Cksy&Y)KM2@2S(){K8Rq#<69If?`wb zxb5+qiQ)UZ#g~9vDB_mWUUvt&64LJ;E4R3XkM9T;I%qNgDxR}MBOy`@ zyKd_hn!oM(kku)7bW7J#m*zf>G`G!E{QADQxNmg)8P)UWjR=In9fN@!wEpK60NEH|! zuF8K@yp~{@!xCg*a5YZ&>W;xuhIw=cDYwHT1av(4DFyxZISlOCKS-z;DGfTykaY?% zt4c!-@!y^*t63KT_tm;AFGDcQvNSS&Uf9`6V}YK+w!QLA@f&wOF;xt6zSGp#{Bh!= z??Yd~4`&gq^O*mkz#C;Xn|j~0FTL2!l?j0q(=||U)2vS~_s(B`2Y(kSZE0z1J+hsF zmElGMRehvlm7_DB>oU#r9w>svcKy0oIN07FKcZmbt>A;R2Nn|;XJl<*$!y$b5@dn{7A6sDr%KYIvPnLWd|JcLy^BvTPS&^C5MD={<9hwA?;-S+X52-#GPf8g2Jq|L<9-VXaYP6ipVn+~Zg8-u zgoMt!B-f?9!XdQVApR@06P_T{_dB|>6*GxZqp*puZomehW8CoEb9?;c$?nFe{l-M; z4FJLDPo6x1d$Sp!zyXidGFpmwOV5~FxBO=bo&5`o)`?i1KB*c zshkdJuiMV1@@HzK|LBhOy@5}q4}fXUFYoHgvB-H}#gAS*U4DDIz743{4ghH|K|!>m zH-4QQ6!1q{(B(j_?ynj*+(8Qqi#)YDC8{tWk-plyd;JzHxnmr;KN%ERd~w#Y8IZj2 zbEa%$WZXwM5Wm}6nkSmH*bJ6c-LwQDSOf0l69^b6uoCx!VT9xeaM^#!Qmupb;;VC) zBl>0WZg+i8xsAp2}E zR719TZ2WLJwXnqDcWd}D*8cud50q*x^gAbTsGcIg8AeA;)L$cBePzFa0GUZaC(h%% ztX_};#p@2!o4JG4CrF%wfa_IwFCt6MO}IC!F-ApHy+BJn!$w4OKdT7qro5XDgg`7S zf12Nv_R3qlW%4gUB3Fe?u3kqQWN?Gu0eThh&$*+bqKeE9$?~*oU#5(bAwV#AJGjOL zYXba)&kvWJ&gRGa<0%tki6rejGX;PZhvEa7z{1V&A zskVfqB<57@EeZ1+mk%Vj@bFrPtOVW~n+G`rqI2lfiNetwc>KYm*69=*BkoY!T8`ov)y6R{)YNF|G1 z`B&e%gfPs|6+8jB*|RGvSTwnDRtgF~RhCjaBE`;w-4CV9%5A1u(21h!&RB-`>-TI` zoi5G}*Xiyl5+fzh?zb*g=!%(L5h5ng6Rwv*=>AgRez+EWk^HM1gHb|P=~+~K{8x*c zOUsf!=yuCWN;(e7SXp0X7_5!7GiG{~+Y~$R-&M-iy4l*=nx#;AyUNK~|L@Qy0C;S6 zovM%zR^tz}?6)p3kf3*Ub#>)ZMi%*OI(K*EmhOMePwsk*aps1sw+6%3CnrwJ>T_8R z`D!6%{U&qm{h2+3gA&ui({DlHN9KP*;(kHUgbNpvrz?M>!CL#A`DXSQBN!QKgz^l+ zN(e@McUw*6FG4D+j}<3QU)U5i>Tm@jl!=|jr>TAXas0ss0)q~$I3m=j1kK82%k}s3 ztum0q5={xrviYds^? zr*3!!I@Mu}GGu?LhoO}ly^K*gjYE>CQVUkh?^ljnciUrgAhK5_I05vK1fI<9$-W%3 zt`N7^Eor>u**5Z9qttuTN4@j?%0l3eE{(-?PteSmwaBTHzwcc1XLgqyuw3o$4O(O@ z;^LAG{5_0s4JBt5{MXs}d*5Vjl0@kKeT;JJ>5to$`=G}c{ruT9(}<>LV&V@f2J|bX zlvL=+JRMD{GWm ze*AE1T5-+?sgs*S1>p!&C5bcMhO{k5uNW)!w6} zJ4=9E&uq8Ib!(HvE26MW6s0svdXyY!(GbuGKE;Zhr47Krf8PH? z*COLuTEmnk2ZRMa7ZpVXXShU&vXb8aX?66^ zs!gCQv)e=v=7Wv#>#zs*UT|>ScSI==Y}kokYSmAsUG0SOC#U0^kV!$)KA(@I;+@5Y z$0ojkjE;_H3GVX6^3Tgy&WglrwcIEm+;P#n|BcH68?*%2fstP(90k)A>MPDms?g-A zgq^Y?*vr6uhX|wH$shTQ{qb@ef4t3cRtnd%je9`W2ZYNVw?&JL*14aWO^`w=7$O0U zAS3RcdrKfmLYdWcAcUY%5y)>yIDU0hLhK2LN0kI{0}VSn)wgm>F=^?5vE>+D>H=qW zbJP?9(UhV;OY*fE%9ayFcR?Y5YE#i+KUAoR2CXK#`hK=Tp*5xDSOwK&)p0{G3CEz& zqAWy0imWd&fl%Q^vnqECbAj#Cy2hn`9R?0Fr}=^{FYgFFyxkq9t0rAW$W%OF01N@~E5uh>=hhe}lp|w_RVpP?-nDlk`(Ini)1oGNX}`TL2KJnd#CjjV`)T z;~WgorU5e6pp~$^S}xtZI1P>!*yeft?d(-uVH|vyAW~R2r2`s>gH zS*!?R77PpwWV%|7?f5bw2QFvONm^T2TD2q3X)zw<*lf?oS|obVsdK|%Vq!X2>7`^7 z8If3D$nJN#)ZiQF{~b$xcACivW$4tge8@QTMab(Nw-NoAHO+FDIJY$hB)-XNKxEd(8TfXchIgkdRQG_Wljf8A6|! zxD(#JiwSYp64PEBA(xW}Ru)xOno}3r=RCp@0M>8cH^ZG}J*76%`ZPh3X^dyoVka*Qb9- z`-lex+E8@gaQ^YM$~Tbu)#1J6hjieysk!y+99q4lnZm@(u_}oFX?gk*8GVP7zb|+U zL0x2UVQd*Rs(6hUJfcin22HRVleW!*`j%CMqO8tBbbG&hxceIC$izQYQDfXs0X*QF zL@mK-{H9qdr1cB)XPQ0|y=~#eq<4^qY51bc*aS%&0CgkvjyzWvu1_F~(hC}Sj?%xg zCb@@q4TiUbPOD5_GYLMEV&^ z%gV|s?Dp?Ie*CyZC^~!$vMy&wtD*5+7H$U=AR7HLaktxWQZWjJ+>FQ7>J8YoiE2k` zi0`tfXwL-(2s;92TU}KUmz13AjQh1W-8`xsO|4k~B^sIUIypFD>~=-R#2`Mso5lB{ z?>@NHXO}Ar3!jhy6v&xZ>KH&kZJU{?J>psFk}z33xB0=bt*z}dhy@F~{DUyeBlrPX z<#mh(UzDO9Hm(48d!x$XhQR>=&#piE6rUE#JW%f7>)r>h!D+kmC_{$xsgUuTkI$cz z3hN(8CCfr`?I5mRe{nAZ+5aIdvU7e>`Er>}v+l7jG3$Lov?!SLak%*`vf%Xm%0D>* zf<$1BcdF8{!?C|^pmn=$4kt^DQDTvhvwbvFpcxM7zUK_Lks|TRVD61F>v39719~Ft z-fBmV67x}1y2u9>IX_WasG9x@%RqNKZ3R9Ib=k)A#?%(q|Ux?C2{{SFU5ykTVb zb3{Z07FZ(~^<83dkBtb($^EBE9yV1wrbq}{z$JvHOuT}Y{F+frOG^;CWx{eLUpeNG z-%JDnVC6J@Uu>9-uyt_wb6g5E01Fpa?5$h`rb(6ucy*xi{64ieQL?r_+tPjjE{NI& zt4Xj0ufl{|!K`!{bjCEJ+%jZXgx_%s%YtA0MUhTu~mH?-6F=r5or%R{yUp2u!0%0uJxT~&Jm5K_10HI2){<(ehy_}1aeqIV* zC(<;jP~_QB+Uke|NC(!O0YpMT$1j&&BBoCEf-2qo%bsZ0uX?^31X?Ig_@lUKf6}VrIYkso>z4f~?wp7mtUURajgWLtb!R#*Y$-lGdjS!!Kkqp3yXHX^;07*(v&# zhWcP0`75zRA3p&`%Ke5y1VnHzXkrah|Lz+7{o2xeewC&3D{RaYh3GdJd%TWYf@9@2 znFk4=rFJOi!VDM+0^}Gz$Y_eOM9=-{HK>4ec&=1HKv1N&AIsPta3`e92wMb$R9*JY zGG(11FqkGex6(V`qCfBD9TXTUu`PUQx3<6uLDxUwnHd==j>%U6fuUWC=9}jOw!d2j zG43#h{N1nV7Seiv$N2gT;|}9u{dOn|OyQ6USfet^BzZ%r2B)^8d1Qq2=FOW}PUHcv zPO%Jv^*g5GJ^lR>Z{-O7CG4^t&8C;#%(J98DG;GPI5FL45&k~bOqzWmF7l6@R@b-)^`APPdyD$+@urQbZ=^_v{&W9AxTX@%9@!-;-AYgQa`oo70 z$S6XRj(TV&4lXX??E_5MjFX&qD$SYqgGuzCUDqKel=we%tQB3J%xWW3oa5WAZY7+S z^4UOW%B&{y&W!7)}wsCtO#n5Va zd6X}gj@HEFlvy!zzOh*f5}Oeqx{!$@H_{GyK!}#oQ<^QktP@g6|gs zaf^ga{&fFv!Yr3bNV{I_sXE0Y~<|ObA z9B%94(|wPFlbd1J`}ltN?-5+cSN{sYG0?{#F3U}Q&3PS0`m=8 zlipIN*@Mlc9|R%j=;&`-^F~2-i5mJ4+Ai}Uduv&)AQum=AQb^n#`*GTk|h_9qqF4% z&mZeU1(*oP?WuZLw*Auv`HvG)FRV{*3&ZjD!z0fg8kQr4qD;m(4+!Z$ zWIShk)AHEr{zkI~>j=3%gPc5*Y$E&5408y;>*na1bW1=7F5Y(T`6=V%A^S+n*`ma&+(<7}y9PlYhzeS#cza2ow|)nAJlw?v4-a>_iT?g2>$1 zI!5-5yTB(p`aV)q*uBF@%f==%-hZEt?rekj*cKh()}5$oE-9-{YAN@k$?MaAB-q_T z+TJU%q!ic7=T^Ix^j$RpWFMq$Kfdg@3jmI$U#qrT#>Uk}K0BFk||5 z;13~;10q8PyV3qPe(Q5W^MGjT&;Ja+DjkfZM`xk8kERcfX5c_(ZFi?m1PJO52!c<| z`(96_ABq9EIV5}92>Dgp#Xe`G4*w?OCF_CQDrk=~IK?1jpjLO5#~d zPO?;6*k-YH7d6C_8({tlvhDbgG~5{XB$#Jzix}oJ(iXdMDV`ERp+)>8+b!E(3FNVvYJjO(=AbWlXMIC|(-v0gq zpgivYepkv?$yM{EN7$YnsCOnkp@yyN86K9>SGbKw%@^dn?_t0zihN%s)MD@dXL?vA z;{R```2XO%^8bD5fPnwc0Qdi0kpH~}sTxZ{5E9DUf!P4za%*PFTib}c#{aol z@xPJt@hi8!!`&kxA%O%5%y1H6(z&O{=6R7=j5 zpvv*sPdqAR*BOU`m%zY-G;Oc)kbKAhvS4J%XxTCFa%4|Uy;RsVQ(k;#gkX64;e|5H}~i@&nR@+9Tm%WbGv*4ALgJw}0@^wC=cCJre=L_`Dz zr0ycy($4EZ0GHI7|=8{45r1W5k{ zGxLU&6jSI_bHCy~EsmK^#>?oPQ=fEN6hP1lA__FO^9q5MgQwzT4cE>Cd0} zLSsj!(5C2J%S+JwbB$^u=_TK8O*lV#%&qd*364WDng`c2aV4A*vwkut@=E^iz`cBYQ3F`c>ay#>Pr5|)+}>3ukAYHCem zV`O0ZOPn8Vf-;)vV70E2523F|@7I)I5>cie=YNir5{{Frt3g{BWrQ??Id%1W3qSf~ z0mxEb2w(F=&;A*-P%VBt>mAOm?G8GC|=`ZFe|M~Oao&n>(to$`4qMMLnmr{UwvHXkwXV#C!wQD6E|Py*6^23maMw!#)-B&H-GQ9an4 zlv+L65^}W#y-zfp{DhkT5@xFWIw_(OB`}B3GY@|(!X58FclSAT<)iKCLI`b3~sRrF?hzmF1z8B49BoQi~2i_UOq{^9!C z*ipHKw&Sq2Ko$*F77gWdbkSQ~h!kIjXIS??efkt}cJ6ckaiwEaoT2FLr9(GCr-$}i zwaZiOpQ@*h6ZZKw$G;V*?N7OGuWyV_kR2u_x=Zdabw4Qyr+lKawe{se?sjV!rIqR5 zf5K_uB3Biy?uFXbDt~3R?vzjQOR%%Eb8S&0yv~BIQBl44XEqlzAh@x5G;ULFTEE-+ z;%qIKLmAHfygd%*P0gjwxU`y@8nR<6nKVhgpuj*CH8lfgSLZc1y_A0md}$#eIF!O} z6_WH+dArB>a+X?oqTW8l1Ozmys;Z02$^F2RzeL)l*N_@ny|c5ES9175ry>4;msbrw z2EQw}Z0655s|_E~ItrJ%-5@CFS5|WHG+*OTQHDc3s>Jg2-6Z!ScW~JcnQtuI+;89t z=6|T~YWl{UD)EVo3;T0is zlOOB91LLpjVxps0gID{xOlN3M@t0?4Z z0FPqf;!?hU|2`gHcwAM5s&+?9iv%cvK>@ZKLTCuNC+G|axf_DDHD3~Hq{w-!OCJ0i z6RfE$s6Eoihb)5RrK_{E+1&3Jen%~@?tUA3m~|5pBDfU1(gtlI^4&?Ty?7)>#>Q*@ z7$T-u7uyaCe`d)N``g;2hP54Q;Ueb?*Pbg?Z5K>dm(KW8ml&=p7#Stz^g6R*V`25g zavC_y|9KOw)A;F-?QyXyRqa6)bGEK~ZLU^z7ACMg zP-dmMxU}>`o?dG0pIJW<@jatr*`d{bGG1FRnPo7+`XC>`^`TR()O@gG%>#Nu{Pm<8 zn_5v(YnqGYV5PkYY!BI(=Q}xD#uwF5QD35>Qb$JAf{%cXDU(GwIXg4!xqC~D&)p9` z?0nKKT}6O%uraDgFBvlM{Uh4q=BCw{?AfeCSE7LG{!-Uq+!H=7Hy)CKquP$i}sdg>fL2(z&%Fwa}Nx5w(Tnj!M- zYC}oISWIM#OvJ<6*z<{0`1S7Y%VxqC=Hq4gNgiuv?uUahuyZp;ao&BU7OMY?z3&W) zYWu!KQS_;R36Ly;fvn^ppdylU&H^GyG7XY300IJ{5+vu`v`CYafPjKz8YDDG&N(-9 zGrQlfW@;SFZM*U&a<90c}eg@tPE@WAGC9d`4fCfanA4tupZ+Xhilbcs}^hfc*f104NQW+ zucoE;{X#?U05P}u57;^6@04WEmak$VNU4MmA1>DJE*5A@IvMbr|2Rv2?d}7AuUT58 zNo~G?uhI0MCNDWWf>ptd93LhS@DstfE`Q0w z%`IdIPg3=@VIlkB`Sq>kv67ML(WIF*=BIdcDSz(oL>uT>Ct&HFv zpkri2p1^b4={~cxv{@N8Ws^@%7CbKp3%RpD6=~@E_s*R=YQ06qI2(*As3Lt@P+DI2jfjSk zQ6^M}Et4dT7dD_PY9RHYwttw!u{j0`muE-UaItT-i_7mEc?hnSlFG*i=6hPwGj?BYG`BTf%G(TJgS&OC z$CqK8*p{|!K*u6KMF*dmB<`MD@uw!D&3A9Cz{W$R1mCza4O^eb`2C4rb-ss;entIS zTOWk@dh0M{u&g^1^-==lvfsXa`|LIU0h2N(tGILo<{EOiY3a=B`uW z!izBpj`z@O@?#mtpmH<()huIk)6cux$zx9q;zdFG;iMq;dw7TW9QxbJy|Jdg+6$tF zrM6kby__b&&#!sB|CiaI+#w0>kE;x5B{o0*HdL+Ed6Q>6Z;50^?fmnPLD>tN=(X~6 zNnby2N&t^>M_XMx?Zc&QKSc~o=-KYxePCQ|g0%4%O^Zkpqs8sx*Zff_eg^tB+)>%L zO3imyJ&vTE5HG#F7H6dD(eZ{yyb2Q%5|X_(Z4mXdbWLS5 zoH0vFsQQ5jMsaSYNniX?e}NUR#Qt~EATdTt$^BFzhv7^{@iN_|TCH24QnLL}LU89p zQ&;pCPbKKsimtD(m+}5|OS#m$+}Tau5=x#1ggs{eBXudR{fFWn1Oq>mlZOWwe(rwM zIC+(QoBg@-=lQocmifCl?l~9m@kI=~b%J01^2L45ac^&`HPR0Ee!-yRd3Xea=w$c* z^mbcYbB@-|P%w1!)*qP{OWd!~FU{r#=VR0RBP3||ree(JV*>tO9-zQB$lzp83o-!#uo5gS%vuOFB z&%dFRyel!=LhsoB?h@;T72f*I&}6Sg_M5kE(LE8Fo}0004xOnwO!h{*wo)_|tKKKP zMD|BiR17Q13=LT^_!Uh~nlnl{7*VwrJ}BgNadFvRXQf=KoJ-Ms^yqbSZF)t8o{dc& zZZeG5ZDgg~1}TqZG&MDqSpQBclvGf(svWJ5wvKYB+|lWXyC)5TClvp269K1476T$9 zQO!#S;~m1w7CN$-M0Z=ZpRMyz2WgV^`Sa(^L8>L?Kg0kGHr59-ftJ^S=XdtX$9gMW z=Qt$la&z?@YYwUgP^OMOtKc})GYTn__I7qK^XeHFRvc^bF$+zGGEmg&+2XzLXdM|I zu3hu%>{&x#hAN`!cJsfLlyG8Esi_bA4@bt8m`$ce%2C^TaPQKFe#sU&gIcm~N@ykl zPe?Im2fgN!JYK|~MEl{rBo3yp1kDt(vfe-(_KzSLv`e9b35ysM;5MoGGn@5RTAF^L z;S7@ix_)DpK3>?-AsWF9MD4uR6rbL8T94&FJ?4QNvUwu$Q|Yf?pA1e>{P?Zwn!g9> z$=|3n4oZom4=fjiT)C#=l6`k+S4J!QzS{TyjEbffiC$ba(f-0r&DDKeyX`NASbUas zh?!14L6R$a@-j&-sh?jAWj){#+RvY7%ZvmN@GCslBJPFvw7y6ki-!#|VYqsSp~his zIPr=XEN$)Hn78%-WkoKm<1ucp{=&*sYj`=`sDQ)hA2+IyA3q)^bh67sZE3ioNAJfs z&)a0P#f za#s51pP2F2US8TLvR`X{!b9D=%`_>HG~~-V8`*+bz;IQEi_H*t?_3Mnt%<4R(iYSHMckvPdH&WyWBE3O63Bs_EGC6gJAeOU66GW@OgEvYm3hc`$!tNx zLL$LTI{4?f)b}F4eDg9T#KVUVk(g`|k4_Q3W^5B7&j!e%WruOK-k+4$!YrOd7NLK- z7-I_xJ-1MfblF>4Sz>z^NFH=t`atr4F;DNfaruND7(O9?Ls7SP&O70b&;2c4Iw3pD z>j5EB_y6Q*uUmBVYqgGtp}etsAYYra&$lDH7~Dy|rQ#MI`9;#V%At0LI!ub+suNv6 zDL-7+L1;{{Qp#q3tCx3EJq58k+I%;D?q?6{@K=^J1ALOWhl=YQt)0X+^WE;%I!VGb zYjxzp0J7jDDJl0b6o=4Fnb~sM;1AKbISjS^>MB!cNU`HpJ?;EzoQAQfv{{;c#%&IR zDz}g{Zit?g6X?RODI&a4-?AsQ7!8fs;pw1jPmUxtLW_YTB_<{xkP`zmcw((u@4cdL zypE2J%87Ryx*e#iw5(2r40W%?eGy2S)A)b7@i^tR zyPN~yfc|hCE@G?8sr_Z;& zt!;Z}(3F1IdwV`jpWkY6z|cV*y5~z#GcI?%Q{50qAFs%vdkBe_sj?mxJ?bTD-53PJTZWE9`59s6_L>$yDusTe71xj2h*tYfb|$ZqP?EWYlltk1cB0w>Zh8?h1tXDi~TTq=%@x0XMTm6`uX0yUE9$gHQ51f z6C?e?V?M@^CsyIzw=ADlBUbW3WF)3uWwc7$ZlJISG$_#Cdk%e$V@oBj2QAw2K8#0S z?J1d=nfowrU0iGlYJQ8KolnWDeb{Iijq>sFCEt;?m3|sv(PbuteO$GE(68{|@>4Fp zgakX&hMxGgU%&35_0K0S;!IAHSpSJLCnZ4u57wt*`{T{oW&>ZJ-?Ee!Xw8i_rV@rq zkUUlkv(Np7R;LUPDQD~I>ascA;90c_J;r=xJ+Qv9m&+ZH-|FFh7va%+wt2Ic67ZD{qbR}#JIIQ8Zf2%+442QwzDY@`w>hkI6*M*3UDlr>bqaX%CNT6qk}Gy;eM`&Y zhmNI+Rf>>3D*VAOEY=t>d47{7n=QLsZcopa86;_8A%zz6Dsk1rh^zkP{re09h#6!I zL9o_qZ`I-tP`{y!sDifU`1@PjeZJ;WDQIaD*;;d5)wfFb1PL8e_8*c565Dvnal8HX32J+Al6|4)8f2jr z=z??`4VRv^ruEavqmvdA@4F-qmBM1V+~++N6cz1gmrYi0j2I!R5#(%veVOVD326W-%vt)QO@~Q%9oT|hb;iP>+>i6A?cR}Y+hYKA=I2# z{|1Del7JnR=()Qj$~zB0udm3Fm&zBVJ&`yUH9&hPq+)ABLqnrKe+C?lts@y9J~}#j zm5M5h$GFON|Et*|R6-`=oTpmYZwt#pX6MdmR192p4W$vovKM*A)$XFp_t$Fh5dhp< z_Uxt{6J(EZgY{g)oilRk&g~I=`!?v>FyL~U8IlDqyDyUeg|tkZ zD^~;si32teAS~o7pg75X>}_O}h}&JUE0z!y)gLL-_Lo<*`=&x4W-a8gzL^yP_nZz1 za7Ti$?%tROYPD|8VP(Y8s0y)YuL&@s=^0(?1n8Y-yr(ZzX4PeK%Ud@Rg+f(%)VB~V z69bEyj!rgd>|0kFcpaE|IzXEHvs`JUy>)a}1}g7xb7P`dTcU1I#kLnkmN^I#=+_2H z3l=z}F*A#c`B0#_HavK=8P?Qk##yq5n0dXVvx);MgIecC_ z4!uahXls&KDx_m4uPi8e*A$CPXzH4>wzRYyuJJ5ctv^1I#jGOyf?34rDWg%eJy=Y% zTVC@+84Y8Gu9DV%zk#Zn3a5n9Y3+1PDwl;gt0m{TbFEN|gH3zH(IY{`!FZh!QD@>{cIFN40#I|HDSAf-(;1P|6K2PxFI>Jk zKYldTVg}xaxqfvNOoK4}@LJuWgJ-|fmtjarl&?uObW6fr0YII=RAN8kF1;}TAgZx% zsdIb;H>BnQ4$C6a-u9<`slG@kmD}_UpdFNbw*UpBe-a09_5u_akj~?SCL@}xvJnF4 zuKi8Lf_i#wX9T|~BAbKs44rHo1N+9-xGmxKH@%6L$)^{hJ%u5Ep~2e zb5j7FBw|MR08&5@E0=Uh3Zl#HMpP&z*HbZBD)!Fp+~qN`v=Q0%1yFj7aN zU|%=kFMdy#^DC`{*ZbkJ(!_L4HMLZkFox>McYR8@z=*-0P%_~(ki7~B3g?UyfICAM;;XovfH`_RwwcE-c=FVKtCEGBV) zq*yPv@c0Yse_Jq7rb(~Zga7=zW$fGI7w_b;QNO7jbf=`Mi&A5 z;m7{RhvVBnn<$q$gpY0a>d$-o;udn+e*cDibieUDhVEWZp}uA;%Y}LW8mvvA|T>{v6t#(h)XFt&K^VHG?~(xIAaHf=n<4W3Ln^*9k6p*r+*7KI;z? zBtI3P>I@w5c2vE85wq9H{sb8~HbecYpr^94(VFs^ zMrdUmRWErk&k9^P@4gCnr8~VTrhM4l%@ostl;fjs$_1Mh)zxYBN4hnZLhl!mrc;1za1n9sHyejnyhzRxa%tpY%jRy~pj*$LoztUC5xf`lAKa{)9n ze6hE_4gyX{$o5}i-KN5W{64v_^gj8dt-qLI(aN{8?AoVmX%eJ%7XK3oX z6Ai!4M5DEmr2OlAydOLW0CXd*2n3-g--bYmK056f^5fe5$uOb8uLpo%_T%I2E|iL8 zV?~uEs`k3KY56~Y_-X>-@XQF&85S9fhhMw*NYf$7e0Z&$w?4hFP*ceGO$O0qK!T$S zCGo4!HBxL3C7!C;_=24tS!O#tt~BV3E3rc8fHi<}-9Se@zshO-5(gy4fHvaehYzW` zrrye35{HFmBXK<$f-mOPxb>^F0Cv;x!kJlGrRb=uN1c3gl34L5s>9KRGe{cs=sbD@ zSok79A_zGWdMetiNGY5iv+q$cAaCj0+X7*Jcsyx;jw&-&&^~@)ZDC`ob!WjUWyr1Q z-?Kp{36|5E#Ls9u0~7EJ;uQ$Q1+lZ%P)3Bh+PGRqx7yBpPm_55|-gkMU2yOe`$qoms6nr5yFI2LO2CY1;`ZDylrsmledKUrxUq z%_1n>%t&#Hi;rY)^*7g^Bk5iz0w1Jb0n!8rO%slezzf5g>CLO?nmgi$@0kv#EW9>D zZVNgjI-kD0&?uc%8vGtYcCoD5laI@2qI7pDU6UUaV?0^sAIk0z~mp0D>92tXlIdy6mMdMse~ z#`v$MqqPBJBW#TyTesPlYjZMG-hoL1{x6p)y>?0mKWm5I%#P(T%>PxNU^F-JQ-+pS zcIUe^Os- zGlO~sz}3~!AHI$iO+k$AwkP3XColNs?|+__=Lzv?i*0u}X6EJ$xjLZXVN|R-8QeOz zX`PbYz)@ktn=`~*ZV_AHk=4p}^QO%~=nM|(=9v5rVof$u5{IJZI(i6AT77>Tq&XPn zS&ZWxz5(3HZCs{Q4W-E)^RifJKfgN3!(~n1W4y;Akhy~>Xw7ram_LVN3$f~DQ0Eia zWb1ns;FHe8A!1G8Nd!Y0)Y$Ts=`GT_6j8n)a$~SR^~ip#rgF@e$4|Joyg3fVmloU-%Yj`-5;|Q(LM(*5Fs126bOp>;?QIM$G*hW3%5s~+8`aI$B00v z1MaI+Xhf7;upTM1=iuf>M%#_l`=>B@?VB0<<99%`D-MV4Rs|$;ru-&5-l>X64gv0n ze*d?;i2tnKt=R6b&v%yUmB2UYfUr$^hDCp-{1xC;$bEEi?cxv?mJOx^TC)b?EfE=o zo?)Pr0WwiDGdY=lbmS`Ly<6DZ*B2e-j2@GCI~er$?^7su#T+ji8yjgk)u|mEc((n% zUqs|t@S?m}RhBVaHD7>H0+fQgz)H!_L~B)6S|OJI+ykT2n`&DxM-#JVzfr$8HMNqJ zK4}ib1diLny6k5t2lh|Yl?A?id*=q9hBBRSOk`vvNOvNN{?>q}j)v(o@HcWZGk>tm zZEkLAG~!N4AI241kSod^9-g@)#G~`@)hjScAH%}JTwHiYIpNeYx+_0loFt}L#}M0- zko>*43o#E#zi|Kc6Y#$zBr1^*Are0#5@7I&pTFDxev^bGppW%`^Gn19w?z0}`uBPy zh!Cis6F=VoDu#>mFh7liL~D?W_5P~2}a6WD^l zpW6jvFwJM2W=~|CJnJU$72Hbjx-zL{`S&|vv2SU3a}c_~xSy>7(|()A3NMm;Vf|x0E!4WF(aaYFQ8hXeg2fA^Wao#oJ{gp~FZdtV9e8T@;YVw^Zfn6L z<8V%JFi@4~)4L>7)KTZkDQE>Aq%cUP19?&$Z3-N~W{`uhY!?xvbj<$n_bGsWcMgBk zW@Y6peSdWoh=V_D)BYGbCKtwVTPT$^Z^%B^o76n31d0I{aAitu`V{O(&`|o&JWY0r7}ja@^+u!Z%6t1LN4Cdq z{sKl7@T)WmVw`P@_dL~po_l?rRA87*+(U(&a?Q%!^aF4lrW2y{EantzD5yN<065z8` z19!AepPq~MJbLTCCO?oY@k?lr6y8lL!S2I}5SYn#6H#9s94T}Z=m-Vb@I;xjE?_h9QT?DkJyPsV+LDEMT(%{?5m56T;upn18fvMqvvfHUn zxJBC-rAW*_Cim~1%e$zi2bbi;dbKMDIc28%2^;0W+%v5zfChAzJcN}6-m+31;P$Qi z-mD<=~d3_-b!)NLo7hLcUwd{QMo|N=uj0 zBCwDo5Aaje488MNRdr{*cNVak$_YYwr3CzuPpO@U3(}|E{uT#^M)AF#85BTirO-$Z zagJV1SBqKrwp}kzQ~5zdL&M@|UWUvFENS-mhv2|Kn>OxGGYbp)V>(7gg|G{3M}Q~f zbXNwB;kom6T6I`Jb&897aDq1FGAVgB3&Utpb$VfYb!g9`y3Aly$33H#|J+AZ5&o^G zMcMFkLV{s<%KPcHwH)?xf(pUUZ^r)Ls)BDY2vOv{VGm}Lho*{HhA9;AXZzSMM)(}l z7$dZ6WS-JocA1#_fWV>4USNx`-XDSbV?-eM!KPs!?}GXd+>ye;xOWAS$XojU&>pAp z^g#enlmR{Uv>&O{tX~~QqS0;u69CPyd-4xZ_3W`_*zO!RvxE76rdssSNs>uOXilFW z%*2YhOy?fq@zZllhIJA=LQjG<`iN{|Y5$0QpU$NAK~U@|b?&3SxrVjcHzt_33_@@& zWfnL!-R1P9q9r6ymtxmqKYh|9>}Allay?3h|1$7# z=Xk7{P_y3qrjj!7Ti?}fb2@LcjGSI5&vj0PPTcFUry-d8Qg^IZnAepEzvF2Aa=$k^ z$46n!O-HlZ>%QJ5M;7dl;I&dOo91tdc2>jW?*M zP4?? zss&ubWR%M}fB&Z1?hJxOZEn`5*2b>}2U1UbmpMa`X+4piMQx8ot!97!-oEF$v9;AF zh^^8?a~st3Pfi@BuYf6SAF!vk2g_IX(pPl+7{oO%urhY0NTER1Tn`O+;V&pvmBk4k zpJ|dy?CsS+8rDAqz9{tybVX_q%@8*;_J%wpG&@r_+MOO(UDV(*EmjyBst>;w5)z>S zXODZ=b$d>e5t^+RM;AE4%1%r1l%193QBWGqHvQxtUGENLxFyBVB?TG+w%l=WAl?4g zw`gj}q0dleTQ_BQ2v+(K*I-BP&3QTKh{+%Ob%LXS*;y1opbVe**KTH2tpNmKt%4^C z%`>k(z%!YBVke`yd2>Z$;AX}Ho}!B`mC&NA*B>C_yqT$?@t!iD1~S;zNkp@lVC)h; zLHSM9{ItBqwW*mIZOfF8FjPpiV5OBW(h%+GO@9az-;$b_lnpNf4A)K9yn|IrH2%3Gp zNC!?Xfq}cu9w@KJS}Ih6M&~%*f)d|0AfoLR*TTyB`_T%Azw__P9&WGTMVKF6^f92|u`8)Yg}dGE{1 zXL@TQj(qZ)FqqPo($^4@PS4Kj5UM8oGjUfx6aQeYF*(n5QD_bM(rV|A`10N&+TX&z z5)eC^g>O{U_O;%|1@GQb3o$zCl>CT^)-1Z(Ng!ac*!;1)S3u-uPjT1j^ZQNUk#|Wp zB__q+7X&NVg@~2RQoHhaom{HFa6gIjPKX1<& zh)HBeGXiXUH`SKR4H1b1Sb%Glwho>h3mrO=4zdM*lrq zn}pp#@=4OAR$Cpo0TNr&Hy7mi_#^EUC(8^LssWZ?x+2uCFdpFqh4L8W{;zc8q(;_& zQI*kGw$l8s%l0xwMMY4b3zsqUy4gM{X3$b>L5;GoNar*j6XD`gy3!KKe(c(GQ^r0k zXpg|i&daMR%g^PX41*5~)l>dG1T!CIzBNK~W5K}XKQxv)dOVfd?G*~TomFT*A}|pU zgnUTG_7UxF{0RAeQ?HumVUCKWXpN~XZfDR~o5#xb`LSPn-WgrQJ6(vckjvbqpdJz4 zC`H=qTEUuVw|o1ay*c+r&5THSax9s*nP zhZ?;OVS}|^yJ{IvqET&qnerCU=FueJ=CnP>DsO4oV)>f0fZyfs)6nK62);@k$2%7Z zA0gL3g2pOs=&nL<6l+Yu^8UdAyX2Ucf0CH7_Ch6bVFeh-g|7eV>>Q|B^K-IJ0j;5K zI{HP={!%dTAY9WsInhro8^Iv>KK+`o`{xB|muJu3f->&&p0b$)zU|F6dQ};HZ0TeQt~5+5|ck3o&mQ* z2s*(e@S&|3#Iu3U5)(`V@!=w*nZRcSj56~bHMiSJ>7e!e-&fS*UsE7jgM)N;MYC#M z`B0m2IJTbR?7A+{s^CyurN4sYw{p=^dwTpX<@awTV@#T~XR#JQ(+E&VfG+xT6m?Fu zwce%$C-9D6&pz8_Tf+<^{)wHzn-YV~J6~bKuccO8r%P0Ibv16c*eF@lc}b|r<=dr8 z-2Iy$yLLhHVz~D9&|+7fW@`bLZGN1wG(4P(iz}wicw|!u{*bXNLRZlCD)4{&fj;;c z_Ip2w=wS-1A~zqE-MiNVKiBv4j44%Oz#n`DqM>nh+2-CMGFcuHOMnTqw$`89j%F4| zFqJaQI18|9CW*P0~@fJ4ZpG^Frx8h(Trf4H}Y3(%gLUN9*?9zZjul-(Q{L zygNXK2V_OSZ`nEw!?j$zHE4`;?)gZzr&eO)nm#!-QsDWLKOs`oc`~yuX)o!P@-ideXiHA*orGj{cR_7`)9D$52^E~Abj58Pz;*T5vi#amG!tTVP z7M}0KfzIqaG_J+-Oc@%k)u-+646>aj+qZ+sIn*mOIkkHH9)vwHhi^4y8B)v(jdHOK zu|_GevAUlAuoZh}J3^=rfds5kQlEr^x#b2&1Q}UbcAS5`|MSa24g)Yzfo>Sa2c*d; z#@Ot4H&CxUm&(x0`IeuUO~hPN&8)v_zThc^gcZLx)wRqs>P3u|llE$7{r9BboR`gn z&Y&0`L$f_+5IY*A;jbGoqdyn1iY+%a{k`9|j`Wq8>!g|w4h$=FK*>-1Mx(dS4|-=V zE@2@LGVO}jwDFA}etAs(t7?aSd#6xT%y05p^kK4yvqp;Qu>v$jwJ%D74J>;h-=Eem z&}Tp37fHdS)MI7(yW5XGW?#aeP(#gSR5;VFYd~%TT|ns&$Q6s3D}3zjrCe%9Sq*Kw z5+6hDSaAOm8;zxvmEA;SSgEICI*eh;+TY(LA{1uzz|mS6E72vI9O!Y;(zC{&$YqgJ zR2&RaPC(f2*)5B$u1~Pyd~mv<;o<6#=-n`Q%gH99cQY$XED2!{9Lazoke-Q-iE-Fr z`Iau>{c)$~c1nTboC>7eaKr*&#~`@)``o_hdp~=|&^KplR51p~4b@oTP~Z|bdrw{= z7i*Y%#&M_=0WBB#pQZDL>yHDJ zOC1;v4xrot-3}#~EXCV^@F*zLneKZeLM8~n43fzM+9G`=T+43UFt{Tm^aPLy*ybNo zP$)Qzh-JNO_)@922XwfL>&_t9ZaGAq-w*rmTmY~N!Tl_CwScr>L<*zMb*2m!TeOzm z7S)GHOB;MWVE&j)XT4}V(K+Xi8<|fI8+oewgT*PSuTKG%IuT@oUqef)rpee48S}r0 zBne4tmHCqoT3X`94Gk(wkT|}SHfOX8GPW$Ioi0k!#x29U)_@gLA(o2mOjpTv4s^p&&78XX8&OXh+Xw+zidDU<*7SFvHUd&7<+FK7YUrtRAqFm%N9P@Momgwat@ zLY_dtls1FPjJvEV}0ETY6nFDBoJXE6wYW;aEN-ME?+6PhVcl%#?4=9Z@wvcv=_YClMyU4 z0$tsoO;?^(1A+w?-9wsmL4n~@S*S_tYM#7^!DE6~&@;7BmCAFWV_^enHiPzxAD zME(&BSj~dIQN3kNv!*U%tBf@lw9pN2f{D$du^XWze_ihMHP7(? zJ$*5pWamBX7$PbL=*F>frCF7-@AtEp<8Cli`m{vSn<7)2k{6i1nu0j@_U{zJGwWmE zM^qie{322WKLdpZ$i=qsR*0=)&}@%rP4bv;I`**^SuL{hVCUgkbukj-xTevpg}#>| z%X#;MjT%W28ypGgzcVv8bqCK!PQPc%V?HbQ^nr+rCyPQWg>jZXfECpHnlYy%co`09h-Yl@IWn{$A z_t3;cl=nP|?WKvc-5)M~?B`V~esvmNVXU8sF-#AEYKU~(NDu{MKBho-XjB$D(YQ=< zvU1pgO#bGKcD*%>LkNRcjoBJbUUWwYms*cKg25kRF5B;5q0lh@=O$aB=JQ$S>cibV z%5KEqj`m;?_A2O8tUvG2GBP?k9;#mZdZ!3Q`F9+-J?0&CgF60(`I@m(ah9$!*Mh{o zdm*=Gl?6kJoByQC``IqjRhvs^o#p7g`q1mriKsHJLqaj%-QPNYvVI=ir;HT$7&dly zx9m*z=5g8D{E!kGtQgBPY5x1>)oB`Z)p2-DZ7s+=-vOo3hQ7WlBp;GKz1iOvVGwn> zD(sMH*FUab^4xhime(`_!s|S&Dd^k7mWk`V(LNXT(`Mhk`CNa0f35^sWUKVrSnVar z>kKe~3#OD3XNGQWZhc7j6!xb=@3HB-Cpy`)n*DXqeE1RwSo-#)tYqnBWz^96`DnUd z?(N%CAi?~PFhAdV8>j2_sQ!ZGjJhIPV_J~d0R_2&l|3oH;7=v|P*jR5WB&x;Zo?aU zF_;ManVh?<7i_yDzbt2*H+c(Xf@sf%0(nUNmYSa-qOt? zJ+U-L9AE)+RB3D$Y~DrG1pu2eJ^c~>Fyvx7`MHRTy;MC;37v0Fvb}xS5VdOtxANx9 zvBhb*TPkq@DSo(rnwu9L*RwIGF}1!kTXX+R!C+wns1SMIrK~kKGsBzNTQ=K7*_A zN}%!;nN+^6d-X~w0fec2@JCKNZB)^RI2_JwqVY7$c)4bkR9PwNh1>fv-;;5IWTeco z6(mRv7~kIIGt1`_-D%Q=B=V0RpMt_j z)R!-LJEHqOlAv%CJZNHgmd0vz&T7g|$TrjG-295T`%~c3-{G#!*r^2N@otom&*#xz+DhWiPKx>+7#9hJ(qc#wzagmOEaD8kC+r zlwRJZsV|)?3knJ{pTa5AS*Bd0qnq3rG-(25Dzg{n=`Mz8YVTnGGN%CLxbg418@#pq zmtkllDGUM$FLQkM^y#}UNxYJoQ)ZT~spn}`DLlH(-hf*FG*SQ726LN4uBMIr?aPYl zf>Em<@1p)}&ZV4ld`9&})PhW?aQsK*C5+ayX#o|d%&2c%s=mhjRkJHA;U`GRz7B(R zK1Cwpy~|~F7^RSXQb&gE@+5S3{}67EVZRV@htC4%R;XV~CnQw1DD04?x+V$oQp|b{ zQBTOp$gaZiOY`~qA0Hat3a-zGlKpp7k;DJqUHT_?i2}c>T*Y`hCh0T2{4(>+Z@xTO?T#Q zoaB3^H^L_V(lbJRIjXINpxv?pGgx4TBngac4RAZ%WbAeN)Zc zFLTAQhCr66J^DLnXQ|6ZQB_w;ASVl0Y)|*!nH8oGsbW}35FeAm@3UITy%Ph*pkA#l zIh+_Y^sYJ9Ww|A#(>wT@@K*^bJp4Wd&H5w(M@RP)BPF{G_I|SVp_4m3I}zb_PYHe*UoTFr59X&$G+SjKWVBN8j{k( z$X9x?3n*?M^6c#F-|qZYzrO?L1x5m&4#XSX^*>R7%+Ei^?7#UBaEv=fe{LN3fKu4s zcHX*Ymf;r|uDvpAdh&PQdF7i>XR<^X(%)Xjz(8zqaq%h4riXqki?ITfOBtiE0~heP zf7;A#yqu@K_}!+fYi?{hU(?XAVB_m$J3D%xs(F5u!{{H6j|i<|Fjtr=I$9ehOFVkH zPr_yN^&$+x`0WmJ3ihX`Wb`VWPQdlitDGd$voklgbZ)WjT>dj6?&MIh>GkNC@8k8#3}nXf-JH^CMAHSAVrM_(@IMm=u^n{ez17OX8dH`Gti) z;!EwEW|wJ4+1Ug2xL!v_^3HZ9h54nW724;>JTaeX8z+)rF`{Q#T*4bJNv6nxVPHBL zzBYx!dHsAw5{|Ypy?TA2<&H?N^Wz)$0Pc0ZdG8X4nJXQF&C@261EDxQ;Y29piujul zxGUsK+?AES@X`ADEnnS-#)e7Y!dhbcX{KqO0{;@E4x3uY>GeibQwSA}3E1!7>k9o& zes7DecRtr6Bp~n?HtN3D5TDO6)}s)?Dr-%1eQ0w~=cvXrMRwPh5uS#DU{wx;i>Gqp<^VqON2xF;Cwf+6kM_F#84M=V!Y@Z>$*vQy`nz zw1_E~SRD{}cDvBu|2>^xDD9KXs0DD*(v)@X@D-ob>Y?r_x` z#)VQ`4tc!b&giKuaoo6-__{cj$M~^A8{);-_U-%k_0*8kZsD~P$ORgD7;RlmJ@Z~2L3 zR!wSNVCAed6I?UdbCy=e(6{!~xZhq<@B82%KTf?ku;VO%rh$8jc}^$3t3335n?Ose zaCn8WqgEYLMrh!bIxa-7FG68Q&9tj7YIzWq(s=yhGr4xJ)hl(FLiIadGy$Wdsrd#~ zXVO*`WzQoe#SGmKf;sqKL#o#Jl=+dhnm5q;7~OEm*A%Rd16R4Eg4?#}04PCSg88Gr zY!moAzqK?4b8xIq_>Yto`*kLUXjI|zsOL@wJQZwqqWU82D4CwFl6N7(ZCbGOXl_Qq zsxL3MA{guK?mBF2c&g+qOMGg2;oJMa|2$Nsv+Xmv^Prbp-6?Y_L}v54oR-$rr4}sA zK7gw6xjkoh_l9>9ta|q0fzk&)lD93Ssg9E+L{HzOBzT-18Y|&B zO74*y{1U1)T%0j6?a_SbGKi; zy_Yq!mCQ}VU`zU(m?*o7OhwwO;1wPlSqphtC&T@mTD*}NLOU+vK zpm19jL~Ks8M=$YHDY3qIo^4FV!16Zq+vUx#Qv<9lE3ZznVK1DP6$@@*VaUqza9Wpg zU&OuLxN>!s-+bR`z{S9Tqevo-NpnQ%J;eUaHktyXsadaI`=~(Y#qFFG2v`!!0?$+b z1;wh@Fn-F4kwi;Z7ZxM*pl3R}wr7Gh7~|lYllivqx%Ho_FaB;w)s(u#G8i3Idid)Z zU*dh|#6G6f)Sytl4Q_{^=$<)23mcn|_KjFU+=0)=oNbrrv!53`@3LDqI|IK%bVx?i zu>SxAdgRW+u7Ze-PpZT21IcFBNlDYK<4}-H%<3T?n7p>fC%-?d6@&pqg)s4$rDNQR z4=E{qmc=+sj_qU$@9_;8>@n)Ga4wm=FgWDSrthf>vn$WJR9}fg=_ax_PHmi! zqbjoFgaV<8p-v2r>%@bN?wc@EJiLoL+IBZ_+qYxBx1U{8)V`-DbSaq_Dz`>3Tb;hu zbml@pdATs3)`jzSA+1wsvXsmh=&rsRtMz6TxueG`9pdo(4pUX{$Jp50Imp_gyaDSY z(27iFOjCO-ZFko5=Rbir`Z1VC1doCXp#;C1RnjLuI=p_(niMA2f3^Evc({y-A5&sd z;)$P9r=XY^YFr(kePR`xLzyG;e9MdMQJVXS%Zi-s18#;ame7QYG$A)M6z|-whCmPI zwV!mAHUPf@A=(5O`*)F%fsfNGBILaD@^y?-8oqxI_+*pHIiNF;SQaCF@3XqVR&eD< zuB0;SNV_LA#N6rWcM_eEs0>*(125SHNf3pad*%?%a8F~3Bm{+Gp6yC$r9GKLsjVGv zse=f#3%%GY)=?sBZB5QXDFwscnIb8_C_O0|Mw7PvS|S4nbRfX{g9B#r*wIv5L)^<& zz|#t_xQ5v|PL z7YcNdjX^u~m#BpvER?OJRpl#8e1^aRM}M;0<M7JXQEDQ2s$-u>4#$RvJ zHut2+OM7_Ru6zCj8|7;2Ifa<~;mj2$m7-5UaqcxdKDbh|7cbub{3)w9ay*#s5Rz_V zqaCGC*(65{3zwH)AR|a0EWDr!@bT&+Jwx&LXB6xFc)e861=`mjDC?ZfWZz%y;aF%~ zLT2iV7bG?wBTU_3UIq)yt?@nlW+x&dCdSFNnRh<4Zipu22VF;EhLghNVYJwSt}CwQ zIoO$tm+5*J>jW={ynUD0f@I!oS#tavJ~-aJsOM1RaF%=p<)UCNEbQTP=s({0Cyw#H zxcdjzgp$b$w>jo ztO<*E_M$|>_v9U37m}d0{v)?2)%1Xu4 zZ;`DkFF8jEGcxX%bKAEKyTeHe*hEgozAAq5rD`tL@BugDd_~5hvz$cfZ(3kxiuN%Q zbL2i)LcU7o16$%m#S+%QFlKTP4%m6QCDToSiswg0VYYoNAfNjtp`|#y)NRNbr2m!M zcL4!#DO}9Vhg>8oDnPy+#Qr=kN91_S52z|sc`L)q;wk%p)0m@wcwxs#7c$Caz9v%? ziT&&uvbebTgrc=IsmZT?JQYkB@1P*#(Vwr9Bx(6+q-TqNjz>>^$pzf(g3;9{Vvs>8 zB=T*O-9|dDz@gkK(#id`cIa0&2=qA-`YAbno7Fd6LgHeS?j5)`UAAx z_tMg+@df6}bcLHyLOVOwAB7(<=z@U-&SHy5c;xcxYTs|M{DJ~;AgXIJTY!;kVO{(& zL+PW`mnC>>P=n*#%h@;sftGJh&WqePiJ7BDp`C`n(okZV53m~*QX@O))yMEotRj~= zMJAtC*35Ts`-tO|W*9hNNxikr-UVElmbAt_hM3DmxA>}*sD$9k$}z6=-JNmzA3qI; zCfMxSU||s~)DR58oV8U|mhJ$G+rke=RJ$g?LhMzf`nz`lyT2zjt$^W>)}-2>*z)pE zz{oay^kJrLl1Ka%&hqKC%f++_@4nmRz6wvrGPrO`5>|!rpl@pGPVzQzU7jdqfB?yR z^LIL!j!#e17DXY>Gua2aby&{`GmnSv;7)nF!&7>T%%CBLWfXbx^Lqlo=2=T1t9l!G z5{E*<{+pl}A{1>{w&1YMB?UOgiVwH`P!+DnEk4_yu8^krvvP8RZ254isBjAsZB=78 z;MEAqf%s$%k5HZuX1v72wcOeP0Rgp+yLbgwJKyMRozy&a2#JWY^1L7gjNHuX%a(-u zAG;&Gc)1e{?r)8bS2{u3qFkR2Ja6es8hrDS%b$a-Bf~a5G}P_tHoaNX3b@c*0nJ4H zCk19QettLidQ+@Ck6k_OKf^7IKW5ax-r-=d^DRH6`s+Y$+k}L>yMWx`!pACb8yO(o zOR+kfTUH=y6E`_D4h` zBO~DY2&^THfWFLBtwGiZ1(SP|XK3Nyvoce66rak0N&+$3B$~sMVVIU^K1zc z?>9ln!Q!xm36kwEW~ZP8aA@Hr4KkT0VeWc9q?D8t%!y{<8XMXXFnL2&#n zudj#1>$)e7AMCAtnZ#onyW+{qR4@QFyjL}JjE#OTUN?92E~=H!!1(zd?J_t26qJ^f zgo|U+$^64nT$D3pk;^DWhp101;1b17ErSh2g5=IbYSqaV*b@>Eh*ByE|N2s0-^MA4_$TF!RJ#R8U)_b8C6B%tSN_56CEs45X>19QllMYWY%vL>5zR^VwCu87AtV!Dw5;^2ns32iM5 zM!?zWpf734nvP74m9 z3m`Sd(+&UqQ;(YVf8MLyC>kalSRvt4om%|W%os$-Q4R*+t@5m|eOOf;qf^rcZv~Ki z&%WT?H2fC}@Tm!qkOfZnDyHh}^Q2}s*Eg5HN~SLrl*D~%QnD#7EJOw5mG%;PwI8X| zS$}@3&>YX54pDG%sb%fY`n`GCK2fC2YCpj+xSQE~zV_kiiLO5Vj{i~brMG$ylCtG( zleE@t_9V1C064@?^E!6T&ZZ@6%CfkrH`b!!s1%-iKd)g0no6~WZe@UJ#Z)QTmUjk`$U}wf=4gv{Kwc{+J)PPDO=HgXQrhTcd+! zAMO!Y46hK4T_*>1_?pWsnV6aJfh%w824gbTgcs!I@(@8?ox7Ij*Z1mb_y9P#S|>rI z)&g|C>dcOP z=psF4ik)Cf1=7Fij-c(siH5iW? zGmV~Yc9Wx#Fy^uuKos@Y^Sp<1*2b}mmCFI9dNfbXg%G8D~Z&U$NvcSN(?ap8P*eLW@7wAEHOZq&upIc`Z z&ZyDv_Sl z)Q*D)OEMKXC~ax#?_*?ac=rX4g{^9-kejW_+pmDx7eNskk|KP^jed8I{gb|L_E&)f z!osZ`bBe%S;`;ewe|J^FD#=vVplF&X^xO9mH6XDMcx(Ij^4V_simS3ni{Wn=3Xjf+ zgj^lfvfU3#3l6Q94_a*R{m|`-Q9h)lfNDu;o=d0@& z>aFd}{uNA&#{!`!Y!nuz-Z|mecYg4qAM`s4oo&xsM{VwdW7CDhZr7c8Z?Q+lIX=d5 zPLO5rQP+z=#V~pMQvJP?`Dg)q|G=pp`IK{b8lF$fQ%=OwP0$}-z!f@CVcS35(M#cT zuF;LPgCIJ^#<0htyKJ4C*TGGkxaKeF#YXR(KH-409SqsFwW*B!Yo>)p7R#N}!s;K5 zM5P`BCMipZ5Y@$!mg7{5Qs>pjvYB>i#h}!RJW@9~88`Er!tZ8Yj(%)1e5|0{`t&Xx zga>RKuH5Ak5xjk}@+#uOPOX=RtJ5hg-rnzD*R^Z2I-IbVmfIW8_#()6{2?h&J=okB z#TWDoYRS8VO!4%Zbl0ia67kl=M1Q(QRmL95&{fWJ@PeVDVwjC-F?aKF(i?ldqrbTl z@crm`>p2_21=B**#3U6nJMQmerC|OFjPXl)9MVzx?_|2#H~1Cq=POv2R~MWRZ|9N+ zL{u__67RQDe6_0&jcjuYiXE|G($P3>&qh9n^lr|M*%-l|5&7s+P%Z5(qtNr?e~ZP- zq!z}`M>4Tvyr6z17skPd2ho_W`|jL~b%a8QN+iv#p~e3_`-n^Kgw2lefLeKF$TqDc zql~WUfn8ynRqxLB#VHT{KAY!*X?54V>r}9W;n>84a)ug7CPbD-5>zz7h(#U9zl8A@ zBh;nh;t+EQgZxhP(|)V1Q{<#4tHZdWc-Xnstp}8$pl|5s7uvd@RMI}<-v>&6nn=`| zYHJt9@uLw4XwaT4GZ}rPxZi;bPY=o{d)_9k*zG@EB2KQq7^peLL-~EtR^m%~c=%#t z;7Y@rs%~~R)}ZJRUQ(}X6@PT2%gLTG=xPO0L=wD^0(|t<5?r#(>Uz2LV}KnMTRSCd zoA~2r&p_iWe1=9AB&)d4`M@Z=v~o;>k(;6RhWwe))wfMng9$N2N%%rX>s*iJi7aNZ zKm|!jGpOp4Yhu|lnsFgLu^GRXo~g6bhyz%I-B+#ADOZ%LYx9R&)_-S>KqV}+XHALd z%FaBIUzcF3`RwDS^A85*aZ@kSbPOoHVBc9Y+S=5>`lMIyBL!RQT5IetI#LuT+PGQ7 z_E`rC_b|Cul;-Ff!W#-z2R~=X__Ibf_L|!tql$D)B`%%Ds`Ii`l=}QqB}uL(N?a?M7nu!}m@hXb5J~u+qZc>z-~3i= z3@@zgtF_yIw&~JNBOFv39a%P0Zll{7iz2P0qls?ehL1{cPVk1&GVy#=?;e_0^LI~U zqW?_NEB8iQhKhq#Txrd2zN<+^86Gc8?>jAW$iRmzeo)E>Mio!X0nCTwkmvjb%hpOv z)Q(krg566y181S zw;(>Z?NPWFJBq-yTJ=I(0}AGGiGmP~KcAMdh3v5gbDF!jdgxEH^$y5ltHsChi>t^FhEsFUFknaf##B8#cJN@JuQJDx2xb+y7l!qr-G zVRhE5S9|n+v*;_U-e6u2efBhuTWOa|LI?yC(Vu|p*zB%xg{p^;@NLW@4y12LF5qjB zaK(FD{?w{8Or1*jSAi7n_~c^SZe?*jj`0hstIytnUJ` ziJ`o~sn6mIlQ!p^fER)q_b92n70G#ED z-}CqqO=eEtTgE|+(AqBXXw3d%?Zb5rE43OhDFnN^%nQe<$IY5r?UeS)YcMTnFoY+Z zGQ@LBcM7ugc!!W8dWKZK>|0FvB!XAm+SBoVTIB_@!{xH`4LE)j{(JmLVAVrG`x`n% z55eafvzMFp(vyFSZ3Gh`;x$d|WK~adYQ$;Kqj6P!0Sq*R=CWA5p6SpD*V_V za7-d+mrZ4=Vv8VI)2hh7YHm|ROz++(7cZ=UlE9%#m=+=lUL-wHy}}Uqvh0y z)g+QVx;o2KTM>s+Ve?}?FPl8M%F=otQ5>6Hb)J4yv)N^-itc?^YGAxh!)X6V1UuZJ z#p}nB9pc;#c}+TFHK&yGOM2BI4%HO5*9E6onKm39xqkm*5C0!omM&t>$~y0UYjJT^ z4R0UIYoQ9}H|b@Frq|9A5GL*vD(<(oOuf06GAAJ97BmqTH=5v+wc<3_{?LUvpE-mu zp9PUc_!pCa?uA)mtik3UoKjdlwN;yuiCRznZx)&ibT&H|fpwK$u1W(JObU{Kz^O|1 ze=`;{v#zZHRUx@3i~qzX&!V!~Wa#Y~^7DzY7MO?IP5j&@={^JUr2gy?0sBlPdB+@I zM*fl+JWe^t(yyA_CEdCm1J(iAob&$GB)n-#c&V3|M1&dDN#!y!lcG>DP7?n)7cnyl zF$Peol#vDBM#UoMSEg}#SVnrYgu_rbiUk_g?AU5Ly0DNM&#zS_VezcDN6q zfA*P=+erK;LL(YeU6kI-pGPDUOf?HjID%zLnezE8o7(FUK*=f=ByUhvmGZ!~3-88j zyF{=zeZ@)b*!wtfzTcO2|AP0SviG?qUl#bro+ejZYrz{ft46UPircPnHIkCdO2p2( z=cV>$$Sid(7ajA4>z6`Cc|(3i6^S+x`&walg=1=|&HOP4Z<7Og^9hb6I3&Pee8blE zPnpV$IDr38kao6~NUho}deC-EQV0q8`io3Tm=7VS=05jRGYE0pX3|pk?Ral6Ew0r* z2^_@7w{^4;LiEiI!aUn4?Lcf8&|T^1(Ea_mLeol?m4?HUDOI+u?ok6O1}mDG;$coS zwxW;6^{QRimL>DTMt|TN6>z7%p7SE$wuc(UV|BudSy6bEQalCXh4wrFgjHk7PZ$r4 zDHW-eX(X`Nh}(E^TNt=zFa8Fm*#GroQ(=Ee zxp)T785K1*@cmKgiEZWu`9CGb9^?zFrS^l z;dO*(&2!4AFHh0rVqt3yJ1p@Ib7mf_79hp&Xp>rwm9)URzC$43_aM!avDn)dEUc%7 zx(_t!&$4gbMA4Xio|ItFmC{+b)B~mtEo1L;%BAjeH`=H?yd3 z?_dauqQLF+J`jTy_G5~`vNDIdw{TU}wwiuz!w!uQ;j`;!i5%^Nwc%clKFLJVJ&^tSM@d0IY8ZZ-lt3F& zkEM1afikDK2VX(&iSnMt<%=}eQ-s`s)^UVG_Zk!=6AW^y7o_KooGcaP;)_sOi2$4M zCgC1nfcOIM3ORC%sD)929xv%%3SQ%lavlG!87_E4eBbB*Js$fJMA}kY!*nVAX^#)r zv9Zx3Kgjt>z<{v#wNvSN2UH0n+q3KITl#t=H@&OXa~=jilroZ7z@-C%^mo2dq!Pf% ztR1L-GZ=`8Kc3_7TmHikCh|ewAA}9>4zv^OVUoSvQE+oYgMLk_sV@(uFShrQ6@C~W z-eTA1xIrZ-I;-_6xx%c8S*>(`ncBC`ersU14^FnGZgUJ$w&$(lFX`uP`i2h6eW}6~ z_H{52;z_r-1!BxV&|=VN%y{3p0+Z9+ZBa(UdJ-Ma>tW%rG<5juImSk|*+`}zYz%m(*W}%2 za#0{?{IJhaczpubkusN~M8@~)tJ}nK_a5bDlPMWe% z5APq)70Bkkq_v7K%@+t;63^+IehB3cnC|&4BPG}NJexYjC%alIa;W_NMW+=*bklg;BsIn|c#DPXYp9#iylOC2cR6=MX z8ubM2nJkdOAXl(Q;n<_*s}E-7{#3SFys)_@P0WwSV*7wR=Wz={cVcH`8!B+Q`t2>k zOd0_TZRz^#|AcGX5^J2eAEJJ*MFQxOSpph`zmkp)TH>1v^3gY>GFF-#@urc1Q-4Dp z%n>@yg);|RZbXVyg+98LZF@0pT9N;ReKIua>XV!%vK0PXx;i?kzR*QD6o255qyGJ& zENAXe&0Tc@=7@)qkJ^34|4h&?nGUY?bd>&(@OBFqjzTF6Q#>NEGt0gSFVG-Z*ty4N zaKW9f!ByA8M-D(lL=yakHyZiy0gjH)s&DmpX>~Ph!QFFC;(yG&?8k$Q?vQ-VXXC@(%*qvvVEWSZ6*lCDYC;pfk7xJ8I)dZ z@=0;zs;=Ie|Ap~VF)>uThON|T;1v*Ja};6L26CB*Kne~O8v3){#Ifss@}04n8QkmF z^q|6SaCH<7@qR=nZHZ${wmV!9th&9#Ao5n%O+`R(Ls0t_PzMLp1Tl%9g-%zbOE@@i za24tWf%bpuc_VW`eB9vhq{OvTo=PM_c*;7g2tk9@px_#^!p zDaQ2PeWz|Hk2hK-m?qtwFj6r+-*yqmF@m<^pPiJXpFe(xc=!+dbvu=Wm~!#DLfy z%xR{6w1BGjvZ>lgVDwV-kGdEqAaE^(6_L`U24m*v=xAzc3W=1~q`DCU7{i%Ap}5UU zJB~Ki4t*h^H-;6f|1>RCF#OXIMxS27;+rs;{!J`!-WES z$IpH@UAriDyJ~@h5#5`otj-4l6H!kcWnzI+jF1o{Fxy8h@Wg;$Xx!tOUz;O9mRVaL z1gCJJ0bC&*yf-f6H|)%S%#Me!N!0#1k6An zu(Z+{uiJLH=rNnS2~e~llZ)=jND%-EWHevD7@Zb}Jnh$9?cs6HVsb#gH4SV?jxJ^o zm&5x53Lo+__dat0*r|vJ^P4mQxK@e`h&Lb?w0F&*>v=W8f)0t*XKTh(V1cr|n+po^ zDn-aq?q|o8FGWrkTkoxQ#O>=nZJ_F}1+m0th@2DT#bL%NDEv7jrNR2r#vwNgShfjj zTUeCacamt_9Lg*IavfbZ=yvA1{&?L+(&n9$c>y5%z_m6S7_b|f0^A~oP zu7!pS7RgEIiXmCt2Rq-k4o=2Y==n(m?u2TouD#2=l6w9LJM^vQ_jDWI5OXT&aL^GX zfh+-9pho@LYgN^671hzdKghh6Z+-N*MT=v8#NF7~C@m`!o$_W$;q|DesxOKLa;X)* zQ~-C6@3cP`0@TgX_hR3BcnDh0_qLQY-=UI5Mk-}V!9ZRrrUBH(Lx6;6?od{w;Glqz z;|{gw{YT!smNW@U(X;E*s4>~2=@n)Oq&+aaWr9%~=rvC6JVX$2M?4?-nwlD(^SWti z?76I>V*AJoJphV$4a(stU|sho@L~%yKjx2b!mTCp0MjoK0RR2+oF5$pzX=_Jh=kV3 zJ|qwZs`oad$NXWWg6^1DtcczM?5m&$Kr%-q*3u|Mopk5so-Y@OG_(lkAn@+OSD}( zqA1ijMj6H9-B&u>9+jr)J~RdJazoO?>0f!;V;Z2a-W<1xe(J)Jj7Bl9q++L*)F_os z>nHIV8V2eM!y4VLv(2KolY7R9l?DAYJNHq50IEwdKC#;@%k3gxdQkoCvndzt7Uygt zNQ+G+fA8rb)#;nvX2FR7A|>8hb@FE3_2ZA2$)U?*vv_I;#!Dzyh>7dKKKW9~rKnQ; zb~+Ui78;-qHl%LUy5y^FIhwbtZIq}h>wwRd!ASP+p8C{)*ybW2oONQMO$Pc7J^c~S zNJzrxe>qieyD%{ogEIh%n3$x@`I2dGwniO)9u@+e3o7E|<>h&1H4?O0nr{Q)pO=sK ze;yqkemVhc?V=I13yX?ktqTsSKG``2gooeR=K?GwQarY58b2@Z^T`sL3?O3<@m~6_ zPb}>DW?g882XOOm&QZhx;vJ$tLlsFkP(8f4oBLWpVUU2|8XDqypZ*%3nRdQ9Yf;-j zs<;>gIFU42hDH7kD$is|Tm+mZ5F;sH@JT~GV|<`rIIt3HMH*HN4fb6*hakR)mAJ>$onJoH|QrJwm- zV&X%*FEQ1TqbjNLRYNE725rtCD5GTze&yxivv=l)7co*&QO!CM_`juX>j+UvSoeBZ z=pFlQTC~k>41AOjAz)K008zs;aKy|w^h+^%MLIgpnOU;4+ZceISla_V z4GAn%xW%{8U3y1&*#KZJI%8PXi3=NWSZMF)sHFNOiD*W1(#-Ax&=L2bHxt(}08P!g zJ~N=UL})l9pun5*8&q=Un>2!#oz6tiqimDv6-}%C$5xE$~vr@G!3l z4`o~9;X%ILTsfwMelBpxB(i$-c-HwW6&RKml74tY{X(Z5+&&8ztLWRenC>2T`c_7M z0NV&)#jUaQA%KsqlC0amiK4}@d%$~reajm8B8D$@t|hF~@C(^^{h;zY zS`7Tyw@I-Nw9R!`!LX1@j3}@GYXkV(sQR(uigSO|(TF}&J)7g{Y!F#@JGu%gmw^Zx(i6ehk%J~{yKjSW$s(;$p9qB}BKiqwB+?{HU{lCNfHcL|YKgT4 ziAxs`LRR}#$)|to{Tj`V3E$*FCUC+3I^gaK2Kq(_{@f7%fp_TBcpb7b?LADPy#HK_=IhnWH)4ywsn_6-3nyax?U6Q>FY!E+WPvo)>bS) zv$9la9o^X5+dEqOI>gd<3-Bpgi{eDsH1fs(8~owJ(#&`Pa?R*km3i8LzOixfpXw3?JBI2+hTN#NSXV#R-aGJ@fefG~|p)(9~6zJNh52AVwDA!QlZvm`cPY ztzQuC;9{WhG~Si>8li;yv)D%=NK=oW|6o#2DK83b5%Kn6z6`YgPBLn&|1vP30;z#A z<=cfk5m%Z#_j|ZWIl#_{>rZ`yc`T==<^^f7h(ejW<)<)r6%at+WqnQ;{$6|!Oz)s8 zFJ-&aY-?!#`4B+Q0f-6zO+A-rGvFW|KH}{Qn%Z%~+(x@`!m|REIh=Fzf(hM`!a^{) zxj$6`{lvwNftZp;<(83Jc|E+q zuh|E)(X##heOAvl*!~1oA_$-+guN?J6QNWhCLUj_dc|mk%y4sc)iySU6B|4Cyei4w z*5L!Gc9OQ~EeA);EW_&@yO+6f4%hYDejozN6+EG>aK9tq?iqbA-CTo6fq}iQNi8KM zMKZ}K7QDcLUS!5=4-jWt7;rL`A3iMLkR!Rz;e%7N@ci;{Y-#6!sHZ9014}4=DYqVq zTj8U4Fy0x-f3W~+CHG$tp|AxL^!+P~q0DHkS9O(e-&lTG;_gL*H!gDj?dm-xffxiK zf|b9t;d@{m=hR?${|21Qp<_lhO3`{q*hEw={`{>SU6|0UWLXP`7!X>yy0C_VP6&oA zYN>&F6|CS!*j@tk*t<`8Z}&s-nA}sZ;dN4hEd&KnT+Fg+zWRg&>kb7gMggqqmk$mG z$4aAQc1k~w%MtTCsqWiVHu~IN$Kw*({5rVgociS)+2)v*rrW>TwbTPxgQX^yOdxv9 z0t-JbPK|)W1reD3@q(7#)YQz%0QX`W;G+fKd|ppfn-RzZArkxOYKxC($6e!VkeCNfbM%L_)-P7|=36GG1Ia z?SvtBgKwvC{Jq;l8Y0C_1^{toh{}muA-c*aUC7-2-H{m461y4ws37`=M!`PJ=RQ*! z!O3|Xx9!g%Uo26FFO{w<(?;syKU5mR6TzUc-gR?&V0+z6_R-4H8^zfhqOf`%i%9-@ z#c>z@HfOijo*t`)6Yk^mjtzR2XVaI=UHO3b6-Fqm4;8tdIMnm4b+I9Docad7@)8^? z2!nt^gnXmDx1BgZ7DJI@YN3%cdF8AHYCz>!T zHmnwk7OCa>(7RWvYvrROedWDM36`j{q;a(kg1B|Zl2NL!4L*GI*8SSJ#M$#4f+;YH z;i@k8R!6I#8Uq?es4Z=~^9>pCr6Tgr15uHi;4i1-0K1G^4>T5|Eay zKSr~RBFfs@PvJNL0Rdnw#jl)gt2|Lht13D;l)1+b0afmcFt<$`09y?YfA@gbv`4@o zZt+LLrg#>zyt3je879>EMDd{#4F7Fx>)_Bf)+6&WYUIaB)6OzaYIN=-1$nwt9Va~c z7w@lG*bs77Rw!VxP{bFJJ9D#O-pA@Q@%S`<$2qdX1vEwym0=sgCqH$Ex)LMZ&29Rb4R4pMqHp;TY$_Hc|t=LDH0(}_hP$v$jUn=TqJ(?WZ4HI}1St3u1<7nq{MQ2)ImB#4aFm=w)%6B5r zpB##RpcWfKA}zh(=w5Oa4Zc3*V;kDAn4g?+bA02Sy(79jvq8-%&)qHdr~TBb=U;xz zCv3eoCfjcW0OK??H2xR6k%{akfFVT;PQ$>guT)K1^%8k>r#1~uZ?`S!lgt^MrcDQS zV&(imAv8^35LO1b-aR2)Ly`B(d2xU?5YJOmJaXRk@+IOfSz!ET6EuoF3Mvs7G^kcA zdR-PtiPMgZj%t*|YA^%_<>3QMb0Bjn4ayo{>Pq_k0&-qdTVGem553^#MgVqpU2Op+ zFp#@Dx0N6=Pcdt2bfB-U|M4T5&0+4eIgn2HQDF7!*mRkCg{qTs-B z6wXJ)Tbk74i{?VJO^L2W=+;W(xU$Jz<2clyhGdaa6)0F4SS~$- zGQd407aE${`+;pvVDuZQ&3du%1tRINNfLJhLLbVv4IR3!b*~Yvd+8bQ>v=#yJZ3wBj^-xdFSe~^;uVjP>~h65*Ks^bE)CrhuE#_n&C+6pN%E~5NVx89`aY=z0v7AqvdUS2 z0lG{#!|wG%xydx7Dv|z}Bb=ZQ_Quv9xx(p-b-L&$<`U#tJtaXDAFiLTlLNa5G=DI~ zt#c2O2zI+cL*%UED2zuPIb>$y)P{zQj2&DDJ#b#ew^6hZQi}WZ5>i|upQ-eDo4%ys zCwjcAz(v99sX)w?P-uU+zvUaZBs^E9bF0(!5M4-0GRKL>VQ(jS18Dqv8QR-s`N5%l zA%C*Ep&^0jL=g#G6RsgbUZi;nsU5(F7zzT^q=tWJ6!3xSCy1)mSzLNB{R92mIy-S8 zQHil3fStZHH_iS7u%dc;at*iulWEpnS8{KkB%vEP0)OTW5;}4bm^cK5JstCk6oAfJ z{DxK)JxR4JPQG>FwShgd-m_c92`7}#VsV?LsK6Nf29~8PI*)PUoh&Xo8WsT;8bb6> zL?;(iC$V9p6HtdfIb3b4x3Egf&$;ka5q%9~-`V>7wfg*=jM}^vGHi%EM}edmUsr4g zeZLWnG!4M2X8n-4)r>pK$xXT!0wRs7AH}^toNW(&!u56>)$Eh4{j@=m>?Kpgs^FPfc^m zhk59QQO4GfG(_cxhokw6Iej0V>hO0>Wne7g;4_wVRnbA0mPWwU-ia#+d7IqjWnpek9;;kUM4=<< z=*US(nEm#>b2beB1e%h!1Pbo;yC3dRoGY06Xt%l(h3A24hF9U%Nyarte9luL;U#dC z8$Hcdabq%e*5kOU8@t8?a>-mU5l3Ln5Xz#Ky>fM=@WF^`PNl&Vk$?A zV{n13$kta@#@aKWq|;JgaddXpHk{E8)f42E$}`=hB8M#&%-8Ej{hb_%LPz9uDR`xx z3Wvf>@tbpA0630|_v=DHgarjb)gkad!=48DKSEhFfe@9Bfgu$cX;iYtml%!<2DA;P zT!K4u%qz-k$uyv2gH0rEOg4B;jn?ye%$+M_1{obP z@Fx|2U-`MogX~x`SDdth0rS*g^&`?GCVwvDy2eX$dRo8iMnLHbs@j`xM?i{SeRnXL zC5^FR=PJo7_ZoLHY+Htvl{JBL7YWkR6P;D&G0TO6LyPel6_Kn*py|^KB^vfA(*mz4 z$j0&c?j(<+PH6G~>?+vL?Cnuhe*5q-#08^>Mf?mIs0zgEruhN!H7SD^o+c*v{VA+rnnIt~N_&lVS9v8-3H^hEMf*I)zm~%QBiq{rOLX=y1USJV@XHB_KDr4_#uv2r{Q}!M!kzJMMj3pZjrP8 z-S?jTi~Yy;{lY>nk_ujM;%Zi;x9k~10Sjs8RX`PfbrG}s!=j8A0YUmr3P+-*Q09h~ zN)EdK87O`hva(QmF)+{cx|K3u!~kMOa3?&;%xkgW6aX-*=L_*&k{RVvbn)+p zMt*O~h5S`+ne+fLC~bnpo<(Zk1F6?~QQX=PtK2AzqG0lmC5Kkb1lLmA;pCFgFxG)V zM5zO(_76J=|Iv|;%i%pn8K|!I^u;Qs4s4$O0gST-J{t;q@${IO zFI!uu$dTLI+X0ihUN9Hv=vU6&PJ0Vg6$AH`psn^piRySwWr)oRwDx#Na66}~b8`v3 ztnRVx^%4;vE_-F$m|>k$Ri}D5+1Ag_RufH#7yD;Qel8%parqqk7eyg%y(wyZR zQP=X{`V&|5*fRHCx5ThBR1egDGq}j?@BESa#~Q;yi^%r2+WoWI69B!gu57}q!jf(< zU7h$1Ff&6CW_%3LTxhhQnJCfgo*o>SCuzQ~P8xT5!75tN^mu=%1{!*F@3{>-WE$=V z-3qJM)^=&>>7UXsc1cFaQ|Aqk5tNgTj|P7oJ~j6GPx_b}zuR7*5NHvdn?w#E1yu)_-0ZT#Hcx&Wm%F_Cf(a@uwvfWi4waOCbY{8Tfu&s#(b zA|m6P^$m2g8w{_zG6l106)K%3>z^Q^S>0S0rqi(j&7P{}5+n0@_ls%Ds`hD|?XAnW zrl;Q4V4f>x{c+iy-nCIL2vJu-I+0nd0QgM`U)W?2w|&vc`26{u<&IBM>TEq(cAw%r zdhcQ2k3|INnZu?U;#5|F&>*q2XkdiwDMw@3?u^L-R`Tk&D@ww5Q6u;__Oh~*rJvT3 zKL{ftnn_h`W^bQ{LWL88DMUZ^Ir3{wIgJDY48Bc7K4u;13)Wxt~NTN zc1i=dT4y%tI=*|@pRH+~m_Wa{K%c6zmrgbg$S*3A6cr^64GoQoN%;cwguq`E{~0Kj zP>uwKvP*zp09cmNhe95J@rN-rJ(B^_Zr5v98HFZrU4ravTMPWEv7|}4mtQl=%bz(P z;84-f;McHOuiTtd1Ha=R_k!kG4VlD}0g_McaydD+m=YQaK|vDbeAQK>trJHJM!;nf zVB1$Rc3oVUFG44G%)P{;$i6jwP7SpdpiP8Q3u@Y`>XRZ2xt7nw&WiFEWMH)5#gY&J zR&|7T7?bEf$OTfdz3;!i$-XKIQhg-}#5@T7$al)6*7|S=w)_7Fj#2YRAlZM? z@P~X~_lcR$M>fz#7bC|MxXRE*91)bwIhlT3o2FFmSriY4!f^D#x?tN**%}T1i`KXk zcDp21KfUjUx*NYw8xeeUuA-Ewj8&&+_=#kro~Uc1d^(`cvYOQh--v*VovWrSJOkC9 zCcftd(t0tGWQ@b%r8M99{;i;+!eBm6$>ypZ>F({dF2-Vnhu-G>wYyAvaDgl&|9kd_ zSwqkf$|x;G6d{F(=;@^!Aeq5#zs){#mMJTf)Zje1TcRPv0zc7G&m!b0nQ5Ua)6n;{ zPT1e_oyrS&8)|+;f&{)idrwam_>BQyNJm8ohfWhd+c&y=J#=^bx50o$d9-$geJU)77}9An}(`{}MdAb=~3( zA^`C73QbOKrm9zvIPP8Cw>|9%H2@OW+V-Ty|BgA%m~&ZKOZ28wE~=a>-#oh937%?* z9iAHK#7Ia`Q{jI5RtdA-9usP{4gM8y96 zH(}(zwS$FaCh%`zf{Xv}hyQmpB>q3n4aJ&jX79h#R#&yN$^%LQdHCl(%6C5Bf3s20 z&k#qwN3^o)HOhDu^-Qq29v~>^^l529{ICbw|Lwcq$WbLKtEw_`;I?++1hlrcw#G&r zu>_L=Tut+>vZ^XIqC7!@Dg3`#tr}4NHrt)Njm2M=nvah!=F$Oprb_6ym99y4{JS7E zqBs@_$$sABM_REYNgyYBVejcF1o{5`I|M@iT9@PB&i;egfTc|iE0Zn9AS5k~6wukz z;a|LHW+{B-`taW!pM`)zIRc2~lGD;L8NWSg__yM+|MdD|>FDT)IzvXIc;haTVc5wB^P*J3} z{pK;7u>ZY8>j8%ZXU^GW*f{kN26$?7F8d7{7uOcZZ%0AB9iMOC#>~mI6&3M;`6_n` zAz#SEe1jd#J^We8MVV~U*RSlrxe5yt6M}$%z{AJW*58iY#mj{#-%dZTXI>=wWLI#yJkBw6^ z3yUl}Bj>Bz+t$fRd?%+1TL)WjP;WXAu8o|nu~{`2gC6XD-}3t_%@L4bz-{Q;u^9i! z8ON;s()4yUQkcc&p+T#jl2VylEgW-PK<%r59@xpoGlBfz3Q#Heaq-a9^boU*n4cCF zhSCKT|3G#nTg=VL836vx=eP$8sdd^XRJ%LFez>*40Fk))i85yzcmVjOJ2t5ZcU&hY zrps1-|| z1i&a!EIe<~fFO#5n%X|>i}_!U*j48T^F+c-+M)gZGiWx!GUvTPR6qc;%AHdpDG0n4 z1bAv#h^?79I)XZT-7a%lZBYMN17QQ@z;K?T-jlQNg)n39&dXzO@t>0=dS3krLTL5% zq45;I>go(08e9=EY=T@*Hz#MrOLOf$ed=m(JzW~F%vZj3JQzdTpAA^rwQ1_y7)Zuh z0FxIVn?|9n2a76}56J$}bv2)&*E{a}nvLc?!N}zH9}5{67%=hhUYc_Q{rlN!3P4~t z3D?EI%2(fhG%0?qCUuXvJpM{;f9b`GtU%zIx(PxDc6BC1;L!{}_ z)OW4h-JKnSn@Y3c@jtDB;Y2^@boKPeDuhyB6o}=cV_+yUXg1&&EfD&3bnuT0MU;b1 z!*`$>R>#D^@C6F#4@#D01D1z}hf_YqPZ+shbHMjUsEExtKDpi>3Q37^7kp_IG%#Ry zVgd4D?YK($z(HI#km~t!jIxiri#)C;)FY#ZS+%u@03ZnPw;~!PEG}+tDAhKL#e!16 z;VW1E5<3$ z-iUYa)BlSF_(oT2%ZkUkIi&u1P+hv|)$@q#oSfxDMlUL`Fu=m1rKR<+t9uU65yK$$ zt3*bAW_h@zANhPV�{fiF1Oaz*)nlZg&Q@#^*H^{#=7yg;a4s@L0eyqLPlWzCJmM ztX%yZ3w1uq(_$zGGSf!uy|HrY_&r1@eyZ(VU3jBH4;o{hTSpY`=i~2xw>kn1u;WQR zFN3ikN(8qP8m;wzvZRav8J}|@R!qf(+aeHspFen}Iq4D?2-q;}bj5x97MyUp35*TU z-FZzSO!uPfjJgf)u8uzqZLY3T)jc#fJ{mmSl|;umDONIZap8frEdKZa!vb_aTkDsW z^kL!PmamRiW1AG;+1lHSzYU1W%~T-aH#*IomHrAWBF$1Ds2Bi*?@(rd)6Z6JPR?!=QL2*6w&^* zR@P9z`YdLr)Ytz<;Cz!k5%7rGS}6Ke$jHHggMdN$_KE=btl}^%)_b4;d19%W;&HOd zcR)lJtP|7upPYBvH(5SLFk=NKJ{{Q-bM=lWK*g}-cr{u^zOmq}OkRFqxL61A?(PoA zd%z*Mx#8g8kh9Vu{g#ylGZt1sB2_}}lU!9v^S9(=B0lGxFTm|^8s=c1-x-s{gAl-z zM8(7!#3pZh=}UrV@R{EK7#=GG_EZ`b#++@hRIkr=NAX|BGbsJg0;n+*po(IOJBIA( z>1k_g>s?jF858q0%7q3iPa(anY_LlszUt@joqCgR>sw3Y=KQux-eR}F^M?xPWZvJLV^?yUjlgnS&tX~} z?(Dp}{#9-JN9==%3BAK0PRTP*R-0d-e<(>>*WyOkzCiklf96m?pQbsmA65DE$kEL@dX z@{)cnhdHbX4b9rx2Kli9Wij>p*0LU>Cq1mMG>SN%o56wpBnR_-KZ{P~Xwl0J^37cc zSUEuz`9d$R?V(ap8Bs6P`2l5;z4_*nUTYnRcSby7X99*VWanGG!HgMaF9`|`PW26n zP80mRbbHv`4Lr|;Wq@dDrcHX&|KaH?qpFOyc0r^Oq&uWLq+0|;K|s1&x}{sXL0Y7{ zySqUeq;u2V-Q2}F-?@&#AKh-=SZl5sPx#aVF`V>7rz$g@7_jHVyo{SSMAQ7m1)beo zGK?ch{`U8G$O=D)fl*EkOT@cPM!b-bF72`1u9BD9}dM z*Yo&~CM4b>z@r}E`oB+2m41tjopWLvyLn7X_J`mB7$3bn`$u@R*Vi7`M;TM7pMl^9 zK(Vin=hZnEVB$~F{A!jk9oFo*72}6kkoi3M=XnbnQ5WM7Mz0dO8<)Lrn-mn8) zz2fAT#WpXUvE|m58$H#KNLf1oAw&W1>9W3$WnDhg#i7r7ts>M zM(G|AWoX?|uV*?3X($`!Pegw8Jp3w+vcQ3`{OTG))rC&y3;(9GtZ2F&m?!P-!${`- zDaDJ3jlO2gFLm2eqok$^o>#9^Zr-22d$W(eNUeIJ-lo^GQQOMYy*l3N;fCEdo7bAm zk6Yrafvjyjk}P+}Gq9trXLfRrMncBNM*#eHf`Z;UBJUI91h`&yuZk%B;WQ6$FrSeM zppXZNrrzo`L`vCG2_SIbfG{n(KY;eA(h9DWBqrj6WF@dbAy+bh0vp(ktt?o#c5L*= zWE2%WTuTDMuHXDlrQdnv$ICOiSdE78az;g7e|Q^VL$8y$9diP~cdRRT1Cnwx5k0+k zwHA*WW7BoAr2f%srCLo0@xZvi2S5@#XH{|8O&H@S% z&)YH-5=OH&kRdwy1sX^=^Q=)dHaT-zhOlf8d8ekXp^<}UCvR3b$GY2#Kq z;w!*cnsfoMeoPwKpm}Y40X&cUi>TSgyNinpO5cRRgfd!>V$f0sc?^VzPV8(jc?Z6OfOyv%haJn%aVQxn-i31a<+aOl)`mQ8nBr z{`r$3YrP5sxgzF=f}-yIB&h{JmUKeT2khfr&LCXh+-|46#dSgilc@5_+K=~+hitZ; zV!!|V8K@-`FnJ61Lm(zica;_R8@&hqPJAKYoW$nQi$Wyi{`qPpv!n#5f$6%R-s1Ye z7fzS>g4HN-uvDZu)eB_t%B>#xG#fA?o0|nxH8e8ZuD>-4S|yt-94$5J0oC?QMEq+$LHNL_p`oJl@|vs$%0}amCBUcnE}X{igj!Hg;B!x3tis|a zg~Idl0u02KCcx;}4;I4mHY@?O>_b5{lm$Y*k7ZvQI|-N(l_{&lgVoWk8Po%`|8*_C z2PG4o*1zitM3Y$HvJ^jjiRo=kj^w;__>HcvCL3!$9lmf`);fAlz35#M;N+Z;498b0 zeArg7vSJ1dg0TVxnlP_z;_;vLz~!xHKu4M01NGn6oHiDKBLs<=HWXl`0uNnUX1Ljj zTq97{7Q(u^-qruud_R)LPec3W7|7z~K!35ZDPQqs2c=L{R1_3c%#Xtm1mt~KG&H@s zFbau?m99u3EP=^9$~;}*u~_eZi=_NapL}Jv(w;y?>X^iRjjl-p^>DK;KQ1aRCx;$E zA{f#Dma28z?Wgim1&Pg9(KY)sdgGn`KGrK$l_aZqY04|7>x}L<9f3ZFU{LWH?ghFV zjZLJc7V@T5O$$64A79^=^g|%o`TOovv7;Kr2aN{7p9CltwbX-hFXrVU|x-Z zV&~!BdJ{%L*&aYx8PyuG01v15qC+v|dZ-Fe5j$rfLlQtUb7gCby7@@E0>`|QmExKLr{Cp+gG)_Lz_4zvo)4IF&Z4)%WGxd>nVT>1 z;Q@X@z2~EATpy@RMURoN>4U!n|7J6wuB`N1M*{x~*RC}gri@y8MZQ$Mb76rHd|pyI zQ0v>T$YUJF4}fl%<4dcdrnY`)YaKdIBl9N+Jl?pj0v5UJu^=#KW+;ah22eMhu zf8D`BVFL2IP`%hCYE^JRzYE=6NYFFcA7FgfPuDHJrg2G^dkn@usosWm2GWEpUnLGF z<^^dJxw^X2($kB{$+7iO6zCo3iz*^tyt(+MsTo$XP>VQ!e|ZQG4$y}CmJ4~=jY%DEQQoVU-uqWa)HAH4 znL_n3qARc`jUAVzcWwGm!)GAd^YL!&8Zx7(#%6ncq&1Xu9Vb5%m&{0_IWzRN!CUY8 za3OEZajDTByXNt727;jxPX{(E=xU6EsxiGq+J=xwh!Hhrqu^kanf)ByYO$U`1zFy$_Yo`t06%W{LZEqU@0$@xnx{HjG#F+2RGi;{40 zaSs%;&ThpV6y_2TdWyNoUA;jULj;t>kfgn^QSku-*nZ^Bk$$|gVGBB^$19z+Ur$6R zCg}hE9re$H&h2IY9#lM$Pd3 z9>dVRWP`OAbh@^n?M&*%1|_Iv2%7Kq%qzLHv;<#*|BF~8Dv`j?iM%XyLN{ncM8r5{ zFWQe$W`5-TUW(mlJg~6;;>gw2=b@0|n&0=-H*#UrEH$gS7=EHFD3y|VLrCv_)cLVz zIi~R#-@*BY6%#b^y3@R#5Ur1m5D-m6frm&4y`Cf}w9uZI9@z{BuryoU)qW(o2b)DT zK9TJolS1VM$oSNP*B8{7636F$^R_n}TPBg1fFK00BqdX3CMQuqi_rvs3`0nJcxCfY z$=raFhleL{xNQ6GU8(b|An^R1#8FQ>f^XakW54Hf@*9vTW%vQl7F;~OqRDllto zbUjcPz3`csniAF4W|n-?qJ?|*_{IV{yEPwV%NWOa9QLq4&Azg;f{?x;TCUaPI9r)h ze6i4^#y-CC8`M+@kHx@*?t{J_-P3e!8afzka#4+H$WlET!spWh%GNQhE zxF&s(m)_Y?kbL8wk2)utEj9lJ1!Y6%n^4hR{F)V6Vxp3;Gc?swApQFUg`9Inv$a&Y z1mLfm%uC{*2Yz8mMU;m$NdeRjQtO7JUXyK^Qoc z_Sw2#Izi&`uTBK zNk#_M$b$=+T{tl~iF-3LP!GtLC%~h$pBWCG>=#afaxuEeQrbB>g^5?Xw4lJ*Oht(; z;pYBVq2e!cmlEniG&1mT3J_oRU3hp;G?mm?nHMRjEXAa3>%-lxjB<=+;= zzk`Cv3es9KX$!RiKzToAfS)oMII%&e>C=Y~P~bww3Ji?L?Q3(ITUt&hLa=Xs9BDq> zZX4y4iDQGPW_x>Pcyoi$2U}6+BxY9V3WpO{hy!AjipdsHKV2|YAyH}^$fd)=P;PGT zmG0*Y_gA~V7OP((Gi!wa&a@QlexuWV#O7c^Xl|Xy4J*twslZz$&-!|fk3$n|-g_7> z8NDM2o4G!4lDUYbW>9mWLz!vr-|-FDb(gyin*qAaM|pXSRDu3@hr_+ODw-|Hq26pW z0Hn|4dXzLmDf;{OGf}{IV+J7^jHtp`LEH*gzt1tTW#sQJ^*?}#HATbX1ghe;Yb%Zf~#8Rv7Sr z-WU#}DirWQ;Q$@567`x-kz?P+MW4cUC!&7+`Uy(m{7u)wA0RW8`>=L&6k)eLLhq2= zf7}w-;j4pQ?ZJC3H7>gK2*1{b1{@n2CbHsbIR6^v5)3?<-=)e0fEM5o!&keM-@k+0 zuCb7-0bv%Mgd_rtKM>~u4)Gn*XTW%{x!94rKt;x^c5`!^>HnGSqx`vz(hNL?aKTSDCH}n4%E- zBOzO6!59q)6#nqYG@CCS52jyt0t>AkNxBU>5YMrh8Hq@eP9X06RHbyb@XatgH#cZr zQ)?-8_jkyPQg4m<3_x{wt5pe!9}Bgd9y6cT&4~sylUFzm*ORd=nQzOa#0o_KS!t9v zZ(z&1#ZG|8EWFaRS?Y5`iv@Z1T^J#@}-Aqv}q z=f&BLLY(JU%6oVL0c*@yXRV36KQ@ABvqA4bY^HRrRi>6rB@y1EA1k2BrE_*h`Y{=H2*?VC(#gBdjFn8e_~5I^C}pVx^uREuR|TAv!{DS zWg+u*mJnvOJyJ|NZ4T6a245ZT@NmnV?bFLkO#8uCKWArW&BBr8u^pD#MSd6O|Q%K*5TytH?Eg`0iN;Wwjwt`Y+n?xa`Hem3!E5& z&_eeRB1CbpHQinT#+I?DEIDu=IScj4Cb%Ka3c*g77EcQ_s0sI%`#QTq>gwuveD*>4 zf@iAP1Ery#(O(|VPbErDZ?8r%hrg+UO|XO5ZB%!_e^fCVN7(&$FvRm&Yf{K~OEp1k*N5S-l1~efi7{6eiBiQQ>C{ ze;jXU=--^?IE)O5f0V$H2OyBx;c{|6hJ>lE`-j{qT~~l6N-q#Wpo9;64Mj+(;r`w( zH{WCF&ViO511(37b?J%Yaaew%6mEoBLqj)Pdgi zx$0fC#WGgrP#~Rw!=jDi<@HQo*5I%|jSy2Wq1XcvhLU-c^4byr=QAP0g%9^-_4gX! zkfy4WZed78s*^CuUo=oT5X!egN9Pw$qceaIgw|Rv3@jM~{q>#M4A311%pnl`e)BO$ z_By~MYpE$vf39-VdHb*q8j6G@Rkd9A-QEcxi>#^)^$@AJ0s+t(|R3{TQt?oiG~bK_uq4?>-|CKzs; zoOg2)PWH=+#IP~OjYo7LtsYbEeM+F=orMH(zYfpWPz0%Zh06r>*EAs*fC>!+{rt%? zYyZ2;k}&|!_6xXbkM5CT1x5+ur=WeOVfy=5bacq5(DT>HqscMEwqjslur(}dVuI%E z%H<8#4Pe%$1Jg?cWaO?+f8>gTKErY#lXS?!nPTB!e@uOJgjvDj|fUOC+0Kr!&YsKLYv-uoyVGx_WVuJe^uBqZ|cB5vdiLARpJ8J2T9@q6$sW zJ(#t%ad7aDib4n6sqt!4g`wnIM4^Wj(ebO$1jbKb_LHCTS!=-4`j$*gj7o_zhZOn1 zFv~R=GzwAaXldI)K7aDGaV@^z*+pUV?AbUl;_&RrXEgHgTKKv8<4Jfd82y9xEAvYW zHmP{p8QV)`~WyMyZ|V?e#xC#O5zKL+MVtVgDs@2u1?UZ zQJy|><(thG7R7cf`$0gn%mQyMyHS>@y`au+&~g zP+T3B-z|Y#rO8YwF_%*|C0WO=i~3ttA05p%%e{(IBpSC-K| z@V(B)56*Hb(Bco5*ASp6lKR~#0<=Vj{%}fhctU@Zw;iLXh6kl06Mn?Y&F@XpeUAF= zkeQ2(iwjhtNKb(igYrEdXq5MaC9-4xaoGW>i?j9F_nao2oZMXfgZc_dr?h-jhb8tx zn~lHd&74+@R+2+V+4=i3Wd`71kw%*jl0NDM` zdVRS4Wv~?BaPxHUZ&pvAIS?xd#YXflQ@*o9-TYep1&yEZDjf@GN%o9u_rZVc$Y+luN&sZmkhPRlNY zU|oxdceb$>e6)`WCxCT7jz-wyRUfim-4_E7b zNXJfKIvIw?ixG&*Fr8vH7|-PVI6Py!8~t#^Uug4@5axeafZS5IPuO6n)>n?+b$U6Y z6WR!)L`|{&4u?g}M@7f&%}lA5!d@~MT=)D;xL>b>Ow+oT!BEA?GkB>5ivX*KG_Tr1 z3y!;Gy%upW#jw~zrJnEY#cOe{*A z&Se&^17!J&s|efk{%xjhK=vXST9e8q1;9d>zU>_+e8#?65fwW-ITsO}?a7T8f;Sv_s}9tm?&Y=$n- z-F-_)7@8$PfllfrZh3sF*3+lc0p_A;Kw<>o2RcSa(NG<)Ojb5tYY`J?=jE;M)f5GV zHu-@OYj=0D1hAZ|8NFO~JI;8CjY#gU%m9frKTl3ahaL<@<{o=kGRXqLetrXaa!R-_ zEBK`&1`s2mlT_848Bsi>DbHm+&9!2N+9g4jf^wvDZAM-0u} z=g*%%0E4X;p9@eGLA8T1#aOdA^4!zQw0%=FkraE1vGVU|qWH?)-k`^U!uY&8Ng7?Uai%;eywGH- zuGaWU5gzZ)-bxH-%0_7!J=twqB<%9T*F~+2NzlQu5%~C2S3nF`pMGz1TO|mvABdE_ zp?Ne^FQDd2+5$#~fcW@Y^}cNv?Zn9{za}RJXAW!Rrhw9X>;2arFcja77RAqooi*FA z!lkw&RoC)S zK+Hk%Dfd!rKm6-{M`;GX`$Fq$sG!9f=+*tUzL?@bPLG1@SENj{aPYXYb7zGv$15Kv zm(`my8#A(fUh6Vvs{bEgZx3&kBbD)n*-(BIgJSs+HI->zx#d*B@?+qd*RuD`Vbk?5 zcLwcn6$!CX{{KS@I{dEXV)zWz-u@RaOIRBY7fpD)kzt$MmSqzzBs4@em zmr}!erkw55SM8sq!fx1+q(b@qgVge&l>?RkP9=CygXvlS3jr^Tk))RZJ4|d;Wf|L_ zqVIVjY$+*$jDksg%<8*-1XqUWxcSX}B^SIgn%|q}w_XoCS)WQ1J}(WxnNcaNal*VgnFr3CMCTZ`;o2CBj@ zhH2QzS1O#K(fslaih;3SW8>3C{Q^+cNAJItyR+=_JuLy*Uhz0DH&6Q+zUqBWjCihy zN-hYOV8!;RVcgO7~KZ5cVRn>ZS-ePCUzHPnz7Atcae~8b6K*K#D9{y@k zjWMjHLcmQrnJ^mH8_n@dCE7nXHU5?`+*mj-Hl~K%;-uZXT??lhwz(T@T1mCt-MssT zgKRX8>p-*W71Uz$tFdM!r+Ti-z?u|x65)aNk{df7FMTw9*+U6E5zn0Cx`W2>@oGZ} z=UWHAtWbVNO9HZWPx^n+)+=Xo6!kV{!takEN;|4hx~3CQ%S`;8S>hV1bNn5dt;GN2 zt=}>2!Z9Z=M@o$ciXYF6&0^Vk2{T>)w)T9;I@nH98POfDeutzp8v03AmK4mhiSm{F z{|_%cCDPOLvy{1MW(O=vCI6orHr?tQLN4-~t+y@>K|z1rn?)&UL=lU87n7bv;f_*dY#5o# z#81X!kD%Fc`Qm-6;N8_7`lPx@bo=IyZS;@RWb-oObF=ayQT58Q`WP_Ni;T%s zx4-Yf7BAoSTLMeLnvv@?Qw)1@zC&D+~1$nToa9RS|Sdb(J>};;u%Y)C; zi}G|fPc;`cR>J8*#lgpgN<`85oImC)_+1c1a0_cwBT3M|=c?*nsoPGsd#>BC$^w_# zVIV9q8-yn2WsRLUQ`eVoIyhc<-Y!{3QAk((-<_1nH(07?(D6f05Bi_H6CE>W#CLrTy11k|gc zp)@1;{iTc}$%JX(l_N$>=1RFy!iT>vG&tXMREaOp7tvp!gpJj!nM1WV{MD+Dvmt?6 zxo9&wv0P9nx9k(jvY~z8?mtx*@L14q-;ss2ygpv;-Su5KIq{_bI<()_ro}SLX`fJX z%=zH4*!%+y5mzQ8VkTq89N~cD!>BJyh+YQibK|AX*5#0Mw(>)v)OiEV6e z7G4lpx`p{oJiC7%1m<@yk?pPB#~T|@lRkpxtEX!l`^)H>H?I5ajK9w+Kd!qMKd(|G z3sq~432uJ7vHs2DRzk*la`8qkf8op4Bi6%vuUA$y-3arepFXWzM&cmPYl3Z?*tH7k zOHm3Gg8PQ_L(Pr08-v@g8v>q`<_p)OEIZG=DAkBegvVL;>8F`LyXQOb1p`}XOcttK z=p3!JLjX3r!_D*Z5!U7$=@k?>Dla3xpL z|L>yAS^@-ZZXR|MoB>&^-N*LhaPK*eU|lqEw@|gYZ}MYTkP1 z%wX%!aoY_xKUidl7pV!F*0t9hh~`h5(o3Qwko`+NhJE@ofrkKi1>t_keSR})rLVFT z&!?BJ`ohuHYGCoK}o zpMq+;A#j+%uj8w39yBmGNVr!1@+uEXMeu(I2^Q&3C@qgw9vpH1km0|Xv@^L+K6~Nd zz1#kHPWw?O+ln3eSBvnq<+8U{p}^>qWzj~dRHMU}atqU?^nrEg_TjVh8^6FI4=DWkX*mxYl6wASbMMuL7kuHqot2Vg@Gay z=9V~WY$jSoVW+;_6P40v8ZZrGh&kV=(4G4`D^_ly`~9^nq}Hdl$W|_nq-t%JMycK}a3%+~#5iQId*TEM{+Oex9DVpbyX}Z;&WN6wwsM@lktv1xGtaqa z&OJn`xpeY2Xfho73$ffU6+eSjr^4lL5f>ZD#`Djg4n!A&;+XRQW)Tp(L`>Xx`$6Qq52<_m2Hl%z+MW#K%igYLUZ636~!9@YcVcBL&}>^mV_#0-JS1NDmuPQ22jG8ooNM z@HYmj9q-tr1aPxtzlE;6q+zg8KZD9gj-cpjK5!Y8;;e^3x zEL6|%hoG)>FdC0kOa<~*4Q-;xsX2^8E~%n`3I@Zq6tsi?!#$c( z5;7DMt~+n92p;DB`?az2_~Cwb`UrGXMlkb>{JQ&J=9hI7ZWFAbGP2gaJnU0Drb3-@ z9fYPw$yOAe5rxQiN$d}uC5&F3CfJDC&&`ZD|D&?X#-@)zMB|y;IK9A&f83VIl9kfacw?wyoiqVWQGLOuwqVE6i%Hj|gR z&|WA0uD?L^8KIZTbiNX%@yZK%e!esDH`qO_CPR#P2}vTftDS6%Z49Y#=W+q%4Bsfw zgg2Krki)c26eCXwUtXphac_aAR3WBg@gq*SfQR19nAY@i!84v;GsmlnJk*tr7BLg^ z{r1jzXz7Rfj`qJ|HeqhTGvyAKn*Hmf%eRDo>W)GJp3kA7cJT4^hQi|NK~OI3O%)T- zX0{-xCoeY;mLsb&y=BDO+WtaZJ2L3%OzKto@QMO*FEecfl53$GBjgbh#J(D-aoWFw z-ibMH+uN2gfx?zOG&i`Yk4vDN_9~W&by)}p_KiM2;yx(ldXKO zYr2l`?5LMHkeWh>n^mtW;QMXh(3!(>PNa4*W0Wvo1%gu$JoDlbaA@8dzbK0dD7Q)o z1;6u!82%T;@nE7cBH7u?^{-5tuR1=~#}<)~v9tPEFQmpDCArc-w=w+=8@hL*n%NfW z?>!C`>f9+1?#vaLBP$v9X-rr9hTG7$RK8&VGLDiB{sN5=qGjmNyQ`hYJqaI2$S>`a zH@j3$Z8f1nSbh{as1_QqO>obmgQ$i}v6=!8uf2@1Emhwu9i^@NE8LG4KupGK;*g#n zHNuOtX#s~rMXy&^ObGz>UczQRD!s?ecz2+%%lm)w<@zId2Y2i6SR32))uS22<|kbj z8PRCxn@dv75r#U=-t2)+iY`MN8Y<;$q1+O#aUJ^U^gRp9>ht?AqqL7<6paNOh!iHktrvdISmobk00 z>w7K<*FC1iFk!Sehgza$nM#oP)H*u4EFCicPfnt;i%NDgM?1-nJSVG@MX%CxdR z@d|L90Al|G!2SUX;3ykT=G$3Nd`1`bSS!#Qpw#;pRR_ASkDnB1D4szzI+& zGR?(>$6>?j+Bzv!Jp%Y+%F4lcdv&>j-Eu_1hEf%WBt0*QP1r355^R;dI@Y@_|I`r*%Rpu9&R z(Mo;$!5WC&IXR)(?~EZPCMK#@xncw6TjT&~<(oI*W9MLSF|hIuV9leNQMv1$Gt6hc zeHZo&i(~NW1VhxceGF@4WGR3>deJ^3=CgkVbr6OCrY3Ex<5`6;>GDTMBzSCVN?@oK zIt>i9yGEu_pPrc5?00McyM{=(L^T3{0}2m3RMj*L4rc4d8y(8imtOt!ZGm}sP}c{_ zDmRZndbIVB6i{10>}j--#fWmxnRvqakIccZs=2qO<)BE25YGJcg6#eb<|sgzJ`bRW zajJz~qQhfjv3@7b_{PP>6)(-r;h(oXp$;anh>JNp*NiSS*nzHLP3hH>JQ8wfAvUUs zDKu!z#WD>ddjSjWligw#T|k{Om-^cL!YTt36#pcRFDCjx%tKb_5k<#?6TXj}BCeIh zu-Y1;U4F2?jYeZ^yx8g>Rj~d-*%@v7P2q<2;mc-sO%bAVbPNn3At9LGqNZ*|}So92X9f|z`|8c3Sqf~+IrCISqfqNCIE%4iUL!=%5m?k@G8SZ(>{NO^wS@ot*j zlnQ}&Z=fgm$;!@-*^e=W*-M=ZuFEGXs-n1LTdHMjtRGRwlb3KVl2pazU?in!rKO)V zXrBE&$qMjl1-67Blwt&oEG%e%5;FEr ze=4Z)Wf?;=Gl!q_95htI=RG_KSS*&4M9B`WNReuTdm77hu|vV<(^TeVvs!e{rpzPb zyQ}`U4aU4(Nm+_If@d3FxLWV8%yX{I0s4iJj2DwQ{DvVxeBtGpemQPoL6&&Idkn-0 zQ#?tkslbA@8|Grp_=m$33Cj8TG4kXn9C2z-J{`gC+;wNRpenz`GKp(8U+hC>PL6+Y z40S(=80lhLWSYv8UkptI@&MK0!2z57td0HAA}rJi81|vNy@LZTwWBvOlJrDExAr)I zb@$z}2_Sg|pASEMbZ;*9YAP)y`NMH4rh43JGLg&lGrBOb$w{F??~%`c$QaWl}&1d)yYInO9VV4DgT_y9>C02m=H45-k9S&|>~VBB$?gwjzd%=!>*;N5JL2 z*pdv;)JDg`@{6L-2?SKkm-PXdRR(*LXlVt}Vj3ER2EAc1{x96ba`!p;WC47t{Rm^A z^1a$6ktvl>3iT5j29Eq`0GmkARqX8df1=JnP}3Y1wk;6`IlYtwUTi)NOtkTLqXIN1 z$eqc72C4EDb@>n0-p((5zv40$xidWaT*>pBfCwo|+1oUNqM2w}`Eq5N)4?qDZHXfQXzLl9!~aJ?a%<>GlVjww`vdk+;v+ABVG$l39RY)o z_Vfj@1lrjN=oCZ9a5xXZ83KSeo;=50TCfbd%f-P-}upF-f*ChrvAes z{-T=WAS~zucm`V|Dex~LR83=jYDT-S4nj-H7;iQ`1P`bgmDg;>BNmzs?qABK=Ud!V zE1l6pkdVhp+|Z!fgQCh-CwCS84x@V+&G@@T2Vp75OUs{80$&q&;Nqd)N={yA3Si|2 z+F3@)J-L6X9jeC)C19HCc<`Q$k6XL48IOC=yqwg2F%gGr?4}H<+ zr5?}!@>P&VF020wCm2%R=9o^-8vJ1k?$w53(1m>sn=J(&MeZDbFl-himU1cY_luFJMT8N(eg zktebnVhg`(qzS{1Y5C=KF&=2}a6?0I-JPWN3=LDcFT|)`&6@kwrDmR&NxeE~V)US^33vbR#biq zRbSG5Aof{R2lx~!>;5JUYioz6vj&kykrLs2m29$2pY9Lqyprv%Br5y3mz+gQD3Ax@ zVq?40p6#7-kXIEPHJy)#C8ea+$0dF!%@yJv9)n(D0bxE^8)7*!U9~AtD+EomQl+wQ- zi~PbQ^$tc@mXm3V5J-&6rA%RwD*qBdj!tqQ)ns2L3 zqDYOk-}Ats9#0460PL>m+uwk?8^*otEa;}WAigr>9__9C zW)KYsp6pB^r5=tus}D#d@7kH)i%b2!!L2{`xu);|R#teUD> z_Z##XS&ydh(~!6wRKN)yXJ%$P-(Vu^45EcZ(HJh967$wtH{b5@H?8j;V#A@#vVb39x5Zh1X&uka%>&k3QB*-n^%^s! z^=WPF&CSgoV<9`+g9TeiP*D9Q8h0;1;_NlcD)kYu^YLGG4{vCOq~nQQK|ex*ouU+H;N#BeZVL5YegP z;yhFz%M=g28JKxuxIcgD#MX&maet~b+s4MK4#Sik(%KL{?KTLX|Kxgiu^Sh0f3=TW zWjrO|lfnDg*C(7xoMSh)nHGtw9VlRWlkRc#J8Efk)7g(|^Ot(@$7Ms1Qh4#Lkutkrj}R2y@X3$Oo& z1$cRka|-}H5)7yooO@3%8zag3&f}`yVc%o#4P{@15OdSpJwL>CgR_nP@Vh_g=(kMt zXv~A7kcQCoio9e3SlC1zZnTH{Cv_)e(*>@&roX-6Z`=Ys`=fj27L@8dZ| z7+Co4a`Z@aT|WIMZ~J#PMtDyOWD zW0zYR`~|HIdl1(mr1_zl$5}A}&(ZyON7w}NcW^YcZdsF5p_e5zk)>scan!}Bpnkc1 zzf3mr-@kvKmb{kcYfe{u(I&mtP(gASKFHi6U#J9yL>rhisJCR~EVrG8u((rAD5k4ornJkl_d)U$pINwe>ZSvpB%hN6MTn;^VC(@lJTw^{9U#`i!J?kN zyA60iDGC^$zOOEI(pY2Cpl8`zthH=PL85%)A^oc9(`{Sb0IvD)AVcWkLMVs|IG^k~ z?q{zKYr;w7K0CsCXQQiE4~5FnTmxmRnj-GLA*r|n^&1zPTQg-$8zJ zI`RByyRo^+Nms7OVKfw28LA<4x6@UzbvU0j!+*Sydh8948U`q&eSlE5@BTm{=w@W? zRCuzjC8yAq#zx9V=XSrFM}N%7X}2|`+N$qzu{Xycbm9%8Kb9`SR)n9MTx~J;9+mfE z?B*hmC0NMgeVa8fE>hW!O$UVXUT}!*JuJ^V{S7BkYxrT9l{<4F18D1EWzW|$e>Q7o z3zdp?{sDm#E(_IUuVvUdaS!9sKlolv+hv@~G{VBdjksH$UL`s+^}A+olpv!vT>)=i zjs=qmYW9IbYlu+XQ|n}Q^?*Ume4gp?d+yDbJzNt>Ucfhi;_OA6rmo;|7`1d6w*34$ z`qR$8D(PwncNN)^55D>O&cuChaGC(wm1)Yo!D4L<4@t}Zu=DPzVNHN9$YXd8Grh+% z#XT8Q>V&ji_0_5&b*D{`$qhfd>%rcj%*^QD7h{>KRqj&u^cKW@$#cT<4kk<4eZzyH zCKIh>s6^9LGHo~M2kXMu$Nmn>WfIS*<5^yT&QBfU6=tVR;|1lGD5Mh%d=BQAptbck z;!mP5NReG#9KfsU@$PEuY42i3c1U#{Pj^0?i>tU@3Se<>qS>8g!;RU~kTozS_P7|I za{yFJcOhVpl9=K-l+r?Gxzs3~6N;rY`gs0;H(dcp6J8o^Vn_wi^dr60Uah}40ZZ=swalofb zFVDko=Te;4zRa~DU*dBg_18bvYM2p+gNDrh!9{sc5EG|G+=Axb9Z6_tU0*DnLM6Wf zvE9&)^I^@*o_n)_Q?dqgnfEaV=Zraqi6>_`IUN%d+Z5b#UKokXF(PI7Sc$QfhaZK| zWL~e`m8sU-ZA~FcpoC>`M!_j-%DeHq6OTjSd}K4rX*f6<=#!wUmx^vH+b&964U#}E zv*;>xKOHTN;KwNUsCc^A`;s27J>#qGZ!3cg>B(aPEcf)IcgyJLP7fz>zu6ruubl6? zt{(n5oV?(pg$j^ePx)n>VF_0W_ z+19}WY-QlTXZCcP=6*&O$E?k5ZPtq5VDE1kM-xV>vmRf{(tPrf>sOedsl~(Qs8^xW zyr>dDgzJ^0-JhpD;axAD*BRZaDQm80W(N@aImTs(qM44lz`_n=1``^9-1+okLUyWw zLVAi*R?=w|vVJFUy;wE(;l~}maqonzY!GP8PCFm9d&8^`>fEa5L|R&}ey+Gh~kbTH}44Hl#TB=Tl@5MU(v5!b7bUgdCMOx4ie+^+?6V@MQXs{yA-= zSVg(#xRs5r^}gTsG-gx?6O&Xs_Hi*#n4Ol-p{(^$Zvho{&R?}M%SviCP9i0J}^Qg0gxo1H#ODixyYfLeWVsmu;5W4|+4wqB|_*!A(CiE5Rm|Klg9F)Vs` zzNghqJnIIALgJ>TU#^bpsLz(ac)tD;IC9^8t3#^1zD?3#=UA-zyJ^L2s_~3;M#qzl zBBai6S$v_sl$NB`QpWypjtiA9lNG3^8JzsJnwlQPJFj@mG)S1Pur#*~Yclq%@?;Ub z+x8hbNH8ZU)Rve36Ha@X$nPP|n3O;m(&pNqJDX=Il5BJIG=AOgvEEq}*w^MMOrh;r zd5XK9#Em6>fEQI|bs2A#2C1dGMs*S?YN=btR>AXG&g?0@9p*BdjG8O?spD}Oho{p8 zGRCebiHHzrm@81#aT||@>`v^bqJFaik`A^L(%iiXtru}YL3s9KD>pqXPdbcRjg-p8 zs^}^FPQhr3vtQQZRJ0?|Dc)zLESDO1#Povl;Pb`)3=8@DCbZ%F${tH`OzN=Fh-jYM zY=yNsV*>7)-9OgaaV;rnX{6vgBHoDx_RqIZKwDlO&0YlCy**!%A3kkg(*1I}1Ly>b~!n|E+qf?uRR7 zm9TrxoH;!`-TmwC=`PX+5Uyx%L)Mi{MH>^ST4rK!r!FoatnYeZ)2H1zl@k@jX1Z$8 zR|mHqjz&;QYFg!F{a%@C9Y}&@HJQ!&zP;JgR7bfRI1@~L$HBp2V=D=(U1rRzBrZA2 zF~iIosHCWP7lD{M*qg$4yNhEMa6YkxW5}6rFi-6cyteG=&Pb*W9+~xPTSepzmRCXe zu79#tuw3TvRiSQ$TH!(p=s}unUZf$&>>Z^0SgWyLz!8}_NW#CVHR4qG+|HZ6uw|25 zz95frI?|$&rf;{t{f5{M>G1s^ep7^xkNqZ#mF`4N#=5&gIOnP*9WQ5op~F?IpXkjo zUJlynkwfA}J-7I1>^kxqgKnR&)O7cx*_-VM-ll-ejMoSfhC1hH;95OUN)?exs`Fiu-W4B?wMK5^HR@tFCh=Y+#Z`Hmi6HSE?V6^ z0&x)(<=gZpz|o=C70a=)!rs$}RM92{A~Dd%2UJ#RevnjK+7?VsP@Mg9A=YnMBJCXj ze>jQOgkN`3`*@OdV&VIYj1MT$p~Png65L0uXlX}F3k2By)IKxnj@zgOA^a%jF6pTi z;QGO%7N7;ydjP7rBnLF1-MEGN_@;MU$vh~tQzx-Zv)T^OwKY-2VW;MEw80SFxgD7Y z51K}}b>9w7(CrgIbML`};4XP@*T3IWB0)Y~ON^~}uTf3UMV2C*Ij*b5^k%MUkuPH~ zA|j%>oGIO+mDO{k$1WcXJ^!Gb z;^B0}EKSUsnRC^}-GBmj6u?PQPF*sb)$Q?IMlCp15-vjq!uTzeZZFH2zDj{bMJ_4z zI<-WEfg@{m^F1XwAa%N#tL|9aV!OAcIg3t^1bes}yhAzK0uEs6E=8zJ#~39A8c0Pq z|Nc?A>Wi|+1|}R=bzx&K+RE&I%16^lwu^`B57M;-gMtnxMgl7~<>nJ)L=BmqYA7dH zE6$%@9F+vr3027F&cz0v#@y-^vy*M>JYO|e>^{u<_`K_-73{9neV3q}>pa?XQ$;$c zi=3>y;AfO{rGJ9$;b2XeL(e%%vTaq&$RAttN8X&P`LSAYO>i{xFg{hhT1^Ej-4kB0 zF&ez7L-}3NkjY?YbA3I}ih=~1V7Hn7OKw;7yb2>kTvi2HXDv)nIwK|gqzw*@s zq7#+6$Qw^QyzBy1nwbi1WrT;Qv!%4O&=?s`v!@XyoLlo~F*2LgL8cwl$pu!pR#$4U ziDm13@5UiSs2aWw@OORxeI#or$o2JSQ%|BkI0!gmgAYzyQ-B_7+sub=fb8v9Tf)JI zFB6~5Y;$=x(!%WySa0#I6xm~t?PKTX(P#s^ZC@W%gt=ydf?a#QYG^fy8LD7WK@S{J zqln`&1M?ge6=}Q=3@Jq#5JCxHC_FOU*=uJwsc!2rkd?Q1ivqG~kfg$}$C0ku><`Y( zQE@aiCwB9#;C@>1Mz0n7n5Jy(vtqUL62cL11xf<2FZ**`T8;FS`J!ieiTK6U?qRM^LtoH7F7n2hYsNX%6Cl zurutPiMdysZ)FQ#%#E(;v4N$W?}mG zL+U_)`^UX4?N0ov=N&XM1`J*Qu&^aJDuv9s>6f6_mfmb@D655TgR6MOd$rr*%BqiT zc2*LC?QgQ`w}z}tH-R==`K5=_qSaf#(hVM^lK{(cFN;LnWGvlpHwYCKxVrk4Z#u?}$>)xs ztB{C@hV|+n`?X`wjqY})-b(Q}?#+n0K*t&dW(txCM-`GNRF3F2UzP$MKU+mqbIC4C zK;Uu!O|~>B@_2NJ+BLA#=UHB{!WD%o{xJ2)aeb`TDVT(BCt_ve^TrH=3$E}EYxc)` z#smbQB%Til3)BEB;P=PF8KA5Oejjow;@r?_*Xf=%ul}U8xyAjvp8KTc#;NLAl|q(B*DQt->>7c$fYw+|O|3J5i|f`{abnvGkIP6W);FrE zUjj{mb*{299&}$;U_*p61k^Np+#JAD-2H?MjkCB(Gn0T^oZe^w9@<@T5=x$>)|FMR z$uDv<2ht#L?hmT^wRj6y>xziPyZ!KFwHYnFj3o`A;GqY8*l>Q=x0|zu_JcOd!xpIT zvti@sb9Rrh@M0Gy%%74+IM1n^1%KEkEKKfUSUpvqBSl*-TY?ycL*YnMM(CuAk@`4h z=1H-k!Cb=9+ZIgF$=+tNMRSu!$#Pidml`&z8cf&3=27DQP>TIbdZ(Z&@Zafmwg}vw z*J6DpuwC4Fv>U@xX!1u7vtM6lF=&L}eQY->>OtQWdXHM0LUuyo#M5;t(cxfmrMd!` z4;z6Dg+bta*?>b5kekg!+DG!X)!m?(TVt9;Ahl9y2^k-11GvCDCaDBn$C=erakfsQ z9by-|!iRD@IYdq8i~28Bs!X6{!SYQ<6lWR+CpntDOik=x87V1Ar3c69r#-}#v^(@I zJ(H$@R=figZnmwAsF54A;?A$>CjVtT)0_KhT}gs0RG{a;%ErBd@3dZ&0yW)-v-9&U z4)aOc#*|T@i)z|rA#&WdX?!d-m?xv}Sp=x|g>+o2-JJ}l{K279e{9i2q#ZDrzf2W5 z1i;MqBP$jC;N?TUUJ8^R$`v~uYlhYHj~O7)RPZ2Zw!3((b?tu0vmcw6;-=Y^C#2 zO-Zi24~}>INsG<4)<#Th4$J%Y#(InzaX@3POZ841ipdWt#nrrp&lo^n;Cw$raA@1^ za8k2^X)?B!GqDo>V*F_J-pp~Fp}56H!5Gs#R(E-j$pX9X8dv*$359SwI=Y7l2$!WI z$N7D&&Btt4r#+~j)4OgW1Ow@U08)&RdyF_nvc%Q=GFAWy7y<>yb7dPt2rPfyLD`9) za{iLGPlf4FL1f0z^Mlp~oA?QPFj4^SRuxTb*WU4NAu#aS{HYKJOAO$?qss0>&e=m! zfg_)V@dxZSIzSw;+O$WA@gqP+1_{@i@5;t-aV9v?iq$B`RoI$$vlQOqGQ)>D zbPb@e(Z74QG|yUV$b4eDBwCYdJ{}vo(y~@}*!||-9{=31-c0$}8i;VzrBD90*qyLz zN)e6m9|eW1Kp1u}mHXt=jnttaQCLSAkafadc)oBA2PNm}JM#vDt{~9Z{OQux@)22= zF#@|2tbMepVlgz~78LBfJ#OgG3D~&NVLsvBkjB>PN=p>8CSlJ&u~zqV#iU;xypGk$ z`DjCLx@y_=E}d9F-j3taXuf*9<&yD6Nzw^hm2KCGW6AsdZy=n-XX0|y44knhz&}Ot z%>IM(9Q(Z%xT}>5-UKV9N!J)0{DIBo9U>Z0DwNQ%Yt?)esjaFM00p&t?@8$3hxpQ| zoBGlb8%#`WR#`7EUcWbaLD>6NplJa%$wW>coEa{5ou~qCL@C>;2FtOEdrj-EyKyG{ zrm@p0vMxD46yD45tu|OR-9yClAGgUC-9SZv-c(^*W-uQwbtn!KUB=V<# zVAH*0{O2Fs#vMDTO==&n4I$h#++3JxoiPmBql;fQF>npiNAZ7>S2ogO1nE>`~KbZtRBfH`3g|3m3>3I-z z&Qq0hDPGHoJp^7@Eus^xC{*_me9jgC*Hook7uyyEk69ujGyjy7Rb+|}_qX|{qy&?6 zg@|KE4IAXpjNr*u%8IAGh{(EDt!Yht48N)2_=g6}`eHSvEPH*(8s{C!yS~mS_@^G* zWjQqffDegA12H&k+wF@u!NNCw&bCQAUa4N%RRD=&i$g-~N zf3Hv}3|e=c9|P7~Phha?w%|8~N)UIK*YkOE*v)4#BhB^9q$^G{^KD`7X3`56P~?-u zZqmYuQN6tM#k}C&?+eBD8`FCOnJhscFR-h>0=<{%cTmNFa3$cO2p)r1bASvy^_h9P z*d6xqikUiAeHdnSdI$vN$DP*saly`F2df5RCiAr+Xps||*sN=U8S$H*fq`1$8~_QgCrrVVcDaD^=F?9QCI3aou48yakjTngIzBdVhXM$Gs&U$P=q`9%D z7u@m(yzKB)L#j&QByhp?440d!gEHN;b6pAK?1ry`K@C!yOuMTlIe=pl7Y6g6R@to+ zoE*&87MmzF49VrtZ@Dnx8oM{;#ju0asBBidwRkb@1=%=Gj)J-+a*Nhv@8&-Z-Yp+5 zToD6K!W5T7+hR}yPPO#I+s;IOhK!m66OZ#o3a(xthJ`~Ks@3M2jN0Xl#GOghC8&5J zP_z-=cfP6%Xi8f|PAD{Glepf&0e9)(J7oJi!eOMOq+e>*yTc$Z483=V-wP3n5%w>- z;1Kt^aO8y#BS8t~mM7}2GQc+YO!&?)=BfPM2M=t5$$W2%??A;uAdvdQ*XG+WktQa0 z2J=ht(f8yOBh)(8pg0>TpTI(Yo%Zw^Wry2O5D9AjP%G)E8xPT@;a_YoNUIEGh5BS+ zIGt{N1cBnoq4oyY>T;OAfEJac->d46Vp(KjOVk|l`(OCIh0a4D*RovNr9~W`w#7%s z*eXy-(!N4`%)!kbbZ-X@4pN?f8sIX4S}_K!v|S{GKwx4`y-)|Gj8Vnfgc0KJd5kIG zx;Lfoif06)Z>MFaK}-B~4NGAX`p35-uLK|bgrW6zUb$AEg9z#71V8l6dJ>`E&EyXv zo1n*7pic_G?8ux*)t7`OL9}Q$Z?f`oA32Z~iRc9(Zi&dDhuKUKBD1Ti!yHz8DbDk8 zs|0dUlxlo!}1V})T|bYeMf0ks}DdJK?CvQhWs zez5pLt`=4aU(-Va=atZgH%b|EvIWz)Ba6EHt?pufz83 zMVeIQF?vKE66lsOrJlR7+UOzE)zGv9mzrNFeB5?E4Fz>tg|$ol#X4Fi@w5Yukc-mlRGd7 z#Qk!6F0qJo1meG30O6dL;L%Al?^=JFFe=>2ze~ya0Zo~{N=PR@+>hjn^eVjyg+PSZ zbBy0gx$|ouuepM^g+Qv~aBmHP%F>(z|AjpQj0J(T;T{2TpS=d|fCz2kW)7ioz)cMj z@*Fof1eW;kwtpn~$0h$Z&Of2>Pfq?*9sktgKTF}C@$}Cy{AViz9{2|z01^KGf~ZcC zDEV%K_l4Y^h4Z(15e9UR2` zcMHyf`bRN9gntYJh~WNjzW^%y6BmF8|D+`#0_2~v07UqwD*+MynF{|*g@2aPKhyf3 zq4*D2{j-z*!Rmi7;moP>TF;sFzg&QS4wiqA;U8l9hnW5W#(#kE|5LnNW~be25q~9d z=LC5zySLRBO7h~V-(3l`j#`R{11O~!#pLwKc4Y&-7GE;NH#xzsf?E5$9*iy!KL}>La!Fk=t?{uW9^{%k)Uvx+cvbXH@<^Z<*RQ}OkbY0@uZcvmWStYk{B}W| zX0#2TroZ8K|G!`U4-z4s^({w}{Jv4G9(he(->wRw6PgD3>@ctz*N{Gc5j`ONUoRXtVVNX49VJ9K6w}V$h zcvhapeI`A95Sv%w@j1gl2x+5Ci3Ei*KJMWV==9Z)?3(4>CxwQE$Oagn;c4KiC?dD$ zVqj1cU(pUB8Xqy{XOyIcg(|>j8bLVYo}zWr@x94iphwmSj(uqNqtVAW&OYfkhCBM z_VRRAA%>wAX(8O_^GMVA8V;{^iHEJ+d~ll82{C>~Ee-{*!jbh(++Og$RY9P>(cfpn z!dtn+z>wVY=UbI|>P#xHGdwV``|nGS1@50K1Ujp+nkIAx@c%w7Zsn#eF1=jjF_O+H zKfCKzPIEeaSTeY}kF1`Gn7J%8!bY~@?5$wggOn1CbdlmS1kS8shWc8D}OeLXg zPUWO$L}g(Kr;GKL2GfjxrwMJ+ep51B z>Im8SHY@;*@jK(p^l>oTAwWW6{wU)T*x8GQ6%91!#aNYGfVBOkQ}$2HVLsJ)cRaZw z^Q39d{#UW0f0vLp#v=Rzq~pcef__bp8iLg8vbhnTae&ZuRX3{(5Qe{ByEhsJf3JD| z<8plsg*N6>5@igf{25gs_3|-Aoqvhp`IC!dMfN;R90AUm0Ac*Nc}~aR+uNAGFB?Xf zKp9q9Apl`ZKz6)8o3gSyngcR;`E*4<8gTV1ZNr&wj<~xaY-+l}yP|17Uly7R7KxK{ z4ZPF&pt|m2`38#>R>r>}L~*uW9U?qxuVjLx_{z-&r>bumH&g%WHjCT-DOH#g3_IUw z5F7UPOIU!?$@{2b+^DJqhF==zEvl`z)*M|9$+eXo9Ey<*=h#H%u)oiUCKT&Q*JgYA zov4E^_)dAm8Lw^jtK=!E+?9TVn{{?=UFIuUa=(3oo!2Kjn|&T(MMpZedTBdhKJj4~ zg!C^_2sRRR?g)gm^`qG%$IyLs31heml$8v!!O$NlPZwRt@3j;mQ4rMT!2k5C7~G5` zxoZlJd+wYK-Z*8=qg0*zrh;mmfi&A`J8KL*Ak9~?SdDUigH%F4sb&sYoHI%^P*!|! zxo<$6iuUY9=Q6neCAiO3~hXh=V1BV zvQkqb@RAVJNULtwNf{rfr0Yk2w{FlQbmM?n_3hn|;HW6i%_~(U&taeoa@g7`t+Y*O zew1mQp3lBiyT4yKTAeDRZaUOe%PGK)ziHuwrX`zmQ)^1JtdIry__Wg{3qV^B&>r^oNPt~ zB?sS{OZ)8|ZM}`N5tqHh2PXKw=2B`_ec1Plee;HcmDL#Y>#f4s#!`L7OD93|wR??9 z(`?=EDl}}?l2=jfP0Fe|XEPqu>+QQ447X)}(4T+=rn2QyJEH^{H(SV#S*%eZaHt%*{F{mM$YOowqTqQjceH`Ou~YpDG4Zu8o$jVCVF(|L}EmzhYoSAtFj z8)TzXGz~a&-vmNQ+NL2D>zrUyMeC_eqGcKO=a)G-sd-O#*~!hQ*I(d zLnh%lrE^T9ukI;2*Lex36&^f?3-*lJpz%HjT~jsqFz73>%4oBJ4HU zI)~XMsGMM7)&~}3STpQ>Es9EWeQOOP^4RV4!?iC? z`9)ua_?9_yt1|^-3KTtO&uXm&E&#SPeMDvoj33!?J z;VigE~0h@4Ny0E*~J!e0Vjn)~YO8Ps8{VaiH7#{Ju z&W&G3w4>X)x_X6oCo{T+5NkqD)4b+P4-kSUHl1B6T_M?1y{wp9c#FTJ`68p^=o+5R zT5}JK@MUE%+R!5 zk0@?Ed_+gr(7N!sThpYsJaGAe1P@ikph{%!R~qe^nL7-}^5N(YB=pQqBI_nFqqQ#nobW6; z3AtpTP%e9mTbI?U8?k5&Zm`b}!ygFWJ~aSn)TBfRqx+wzByM>PQCRYX2h+uw3@s)* z!G|6hbuac?-6Uwq%rG37B(21Iz}1r4zA0a(dK>mKBYWc#yJ)kS$_ev*?6D|{ zi=RDpSAZuqYH;U|&C3kOD_**Z>WZ`dW>TCUx_0ACc+SEp4Y)PIl(~zgyo8Og_BN4%L2}yjJbvDj43j zNXk)Wo0XsHNYcQ`4m#d+im)@@Cqv*XGLIiIss=yI%MtPCF$jszWIiC1i==r%; zv}I1lw^vO9g=DdXCc3)xy6<-<9AF$o23BtPO6YXnqH3*=Izp8E*dy`8&cC^yW-_K?8&n8WN@c_uFur)=ez>RJ!Em1?qY?5g;}G672R-N$Aj4q+@Q2D zP4E~~QiTvtC6(s#g7V4lKjUa;fx<+G?Q;_~3^j~~z=HJ5U^u`~3k>kJ$SgM+%_Dy~ zOY7-{=P)uvatX@HWGV<0s?g2cF?nA_##-Yeq)S5I+S4CdF5~iHGj9^RS1RK+nI)z2 zhJ%wQd$Ug$t*On4w&o?e?rFWb;z@MWWmNwD+Fu0c9;y0$y!n~+VOMpuvy|P*SFK~D z`a#2$N5p}2ZnpeEopn*+Gs-VsBzk$#M(6Bd*~!SLlx!Tb)Bc3N-Q1#$w$6)s{WvYC zo2quR+HN~iuu2=4UzR+Sqos9{J^U6T_u>k&llODp@KqsuCo+$J+y|x%1BuUM|BTqB zeFxrBcr<50ZrEK{=RYI<(vBFr^(^$8>X$!0aPvAP%So$@ZX?C0DRSCE<0N3d3voMXl*}5CV+a#Djb(X5p6y zXAnXseU9p!vx{sPcPKQJd-LcMH(Lk#1HGUimx*wX(BrhW#YLa3>x|c-^7bA`y-a~{ zC}rItv01j}Be-kgICHf5Xt9#ha>YAc6Upc|qXT_|*|}y3CdQ^Jvg<Sn-4f;2*Ew*y4Ellw!+KmGaEwf?euYG*T&a-Jh!-Ew@N$m-0`QAx(!GC)PwD0TfVshwm-wWE~j$jl?fA)?}8|`L)Qnn8e;IEj?NJ zCE|8eRT6yV=8Dn=G3|c-`XpZ0H#Y(7Ssg{>%FKi-;Pyl}=d;D}rjDhBrNDjeiP2d4 zNTMFWk&JzlYQQnN-2&GO+#cS^P3xJYWw~6ZX#xx?JAr^8=mp^h$C-te(bt*)EX2_TUGjJ#EhBD@M=xYJoy)R>wY<03u6uhszM6qmSdJ{d}mygoMhl`A70)gi%q3USJza_piQz{>v8d*v!FH@AMDKshgh@53;y(fHaz-~ z^^3J*>Oh*L={Wmp`%|Wx~Qbre14xAI*4peEj+_BM%>10?-gMm$rg}RPN*x#@DL{CP3N0 zz{GLo7sfrMtcE^tQ*IIwKV~s+ zVUCH}5bwA3+CF8Sm6P(yd%aT0j^ew-GJS*W*K5GMPx>jiP-0c z#n+*QAJ(pM%I{f}d3c19R~8U^H?UH(27I`Ct;vU^qKl=jPG2<`d8E!9FF2G(XG)b^^Mx?wpxSkhuBo=FaK|o`g}2 zYMEV>1I#>H;eNm3dVP`B{)8N`nq=tc^sMH|ypr?dxNgJVyPpBHwl}&xp}-1{ao66z zv3vQN=j`s4IVuG{1}*5d%N|t&&PpuSm_|n$KTE5I;KBqVHt9gwSJ`l##|N`YA9Ucg zbsKxaVL#ojL>FlE8_lykx$QF4+sp*t#E_&_xD>mjgQt0=jshh8oCyl0q(s7mp6ZG$7E7ilhq+RWLj>6S7WW4F6q{^~w6Jp<_7$F#EZL1Yab*Lj>!{j=AN2?tN z1fR3rgW>dql4wko(}A7<@pp5De5&tJb@FLMQc@;AUxlbZ@gtI7kvj!G3S+$0$dQRhCm0{O>H^6_J2MqEW4T!^s8@0r zVUqczdLg!YhL`UT1VSk%A@V|Tsm#iB?-~blrWE^7L#VOj$4kKkw$&QIx1eHfZr*6b zOK}T8r=>*oU6#Z3iz6Oq?(h(wi!-gDU|hlGH_=3UT}l30kAp=-?S0^6aWWsdXTd%B zDvW&J`BIg!_{4aUa7K1rGJ&SKv1aqVXtG)tVaJ);U`!|7E`uPK3Xgfk8n1QubLME7 zB)FoHECO^OL*wtEt*aClx`v`gF;(m3*iDBUP|s*u%uT%Im3=9KAu0-r-Vy(3rHV>r z;1dF9+`8psEr4&lR&{d14RmbM*~2PX*y@Vqow(iB`I|jQ3u~0G`y)9617%@h=RdlG zMV>E6n6bcFLM~I?7l2MA*7BOM&8{gG!5KFVb|}jR6%$4`4^@4}GtOgt;LAM^4^e;x?NrqS7)lOfxrL1uiZcQ}Wx& zBZ&;Eu=P|#4maj#+mc=QZ1QW>>GWn|V-urVR+ZHxLUCr>;lWB+h%pW-tUEhP=5uw@ zuu3Z!0gUKk6Io-2N}2w7UMVtqA(Uk@y_T;$r`k>^xv3qloCZ)X`W;TEN5NyM9!|@2r#>yyVE!y%soDwh zl5P=#oeNA31&d^ruxC%7r`$D0!_f=u*=JEV|#0jE4u@^wwjSZcvR#~_^xxU^QcUG+dDX@W7TqL=Yo@>vPN2sMzHPskt|AePO*4$ zFq7Sz3k~5(i&$A1(dP%;cduusf(qny8wH8HmW3v!Ilw3aO^$HZH(deiQ?{Ip+||)y zgH*rk4J8`!GTzq;IRx|e9Q{oSix&)9ei){f7)O#aGapC`j0TW&3vyHrCZ%%j!GO>(vu=WiNgs~eZt zBs+4$51B6mv7~dyFy84{wdGai^<2gBWtgLP*&94z4kh%#OeTQ!pgz6h&W5+mUV-q9 zZtA`sEjJ*)4vWXlqjy%bhbj=qX*unZw3Fmly{wD({apWqs|*ZIQo8xv5{@$+S}suE zS;SPJhJO8mjc?`u24EIL+^Qauc~s+!p8Jlr|7;*9<~`5k-G|g=cJVrlBAH@)9yA^E;d^~DFDelYdeclUTX~HF|3IZfDodK&cZ4o#-DzCW!9=np|0DODzobm zyk+~q0@^Y&m|V+uFOY7Y#oH(dN+b?R!#lI0bgy1M&pY(%`8^3`LUCwztNK?Dr=`?s zAHc4aQcP3aeIwPa)#uX4rQYMzmkvQm=7YT^*6zZBi zKVDxt_w+NP^0xTX+rP>NWi(A!UoF~P`0=%ygLB!Nr8cFzpe{m-Txj#J?b&|*$5#RP zyFq*{XHm_GMgp2UXWk_pqWudm01Kz##sz9OpX!$T^51(Tk3PENu#M+EZ8^IhcMwh~ zg8VhgKMLOd1+ebHlt1FN?D|(+5az;PR1yQe%Dl(C>=~_fKL|p47HR__%^CcKPEV=?{S@)18KBAds>D`7f}T`gavbVMxe}4DqTws+T{{UKCQl^S}HePal^tZ~;~r z6S|^iD}L&Yg+R*BehHBu*(TS6EOLa4l5?HbiqQ$x`Q8VOvzDiAGT1qAu({~}g1XmGkcmBVJuu1+YiRxM z;Zcvza4*!^8RK#NbhLW5#-RJfPyH_zdGC+V#R#fHiO=3!l;p{}r zG;~7Y&p8qQ0J;r5*hBHNFrK@Cq;=}x{+;u56J*9+$Ju+3pYmAjd*q$>(6dQ=moJtx zO|ys^g_!U341rZFPIXsu6T+j0 zEmbQ|e9o4kC=ICT~uG>3BP5PwrVLn&WS906mTR1+5Zn z10&jBd>Zm`ZU#22YoWh5L~LY35ekh?Pr3zGmqv<&Ex^76ri++wCZ&@zA}J|>{haTf z{Jd`h*P48KE=>`&k7^g}bHU2CHU3K1?s;~W6A|(?{k+7{0}fx18UU>*l=NKQEt$Ps z1lx9{+lU?%q4IzJggj~pcII%orY=LHwt`Xp2tB}j!W_m(pU?Z&szz5rIH3nE9-`Cg za_6Q!<5%c|i4s_fw%uXQ?328Q5fJ0@Jq44&;}rs5{!?Ff+Qg9-bjvA7q>jpgant4m zX4eQWx%NQFL>k}Dq!imvQX-Z%JKcKI2>RMz{(09cPPIeCP7v0-u@JRX zvnlku<6aSu)|a_=#OHd>BtokrLojq>T4LKN+HE^v^%2k);ZHyH&&sl>n8ph7+W?gg4k)ZIta5>caiA77t`K4J@CEK4)osc8xl zr4HU|BR$SPi})A46g$r);42C~sdlFkC3LBJWf>~shlwC(I#82p3G=-As8yLEkkGd| zLa`~dv+%!!lZe(-w}yEpN{O_{&tabH>W_Cg+*cyU@dDA_p$l7t*xxIA0%r>aD^?T@ zYqc`$37-}XN;mqf-(l@WQx>M?T2f=cRg4PfM}0Ws9OI%I7%dZj9JJVquII{%5s%ad zc`j4v*z{jH5yIIfLinV31iCffmB_$kbNo`|PM%WRXaB~@>UcG}t@VzA|8`HBQc5V) zd9ANR0z6jNGpq?^HRE*d(C6eGN(5C>KJ(v*wrygBPby!txgG!9?WBC$clG_lk(PsI zH~4fhUEF_H714+JLZ`OwXiKs?9p|-%6IZK2<7wj3r`tF-3ZvhXNUZV4_?*&DC-O6$ zZ^XNJm~BreIqKg2WLC+yL#FYhm@W*FjSLtpd>9;+xJpWp8No*qBtNSm? z)wLp2jqm!_$n1EBs-4jzm(f``ay%?d_Zd$yhOLCf zweET~q2Qlg#B@}oSnjJYu7sBniJu90h*Ve4!%$^H$uw0d!{M()rkc}Ei+POi^3YG& z{LhyB0;3T;Om*nH+=_qk3x>hEfg|cl%Ac)gu+kl`xNijNugy-T04q{Xd!!*E)>sBI zuFhT~;0~!|nl+s()hjFd64@EOle&B2HJ_;N26 z-Q(*a_Vqk_zW;40ceLMR{FleyZl7tLC_hRmLjq|XyW*rlk-KVVc40l)ZI}Lg46(4! zck-AvsMVzLBrc!H4>F>|xu?#;5)Xd7d2vyK=yYlj^63mhA*eSJ)U3q)6p@`?`+qQ> z@c&NEOq{Z;l+L|!np_Ccd2p6w_y5Y)`gif^8vT!~Q!6!oP@si@g=GkIi&Iio&M7D` zPvExrxpe=W&?|u?SsNRhob2q-x19*{Sa~C(OlIx6ywFgB<;zzgkPiuemx_i}NlD3Z zF2<nGU^F7O*G3=X%*jx?`ki5Zjk&xzw{c|e*g9SQX%UJ*^k9XiFGJ(Kp_>3O%Wgg>* zNoV2D16_L6d3bm%JX&>M^$%=smj+Ss=TuBOs_W_L8IwGLFy^PFiGVn3l#8pjcGBf5 zSC|f+>kF&Xu4n6E^_U1{2yJ04X6GHmA@0PXf??>HXs&nwA=9u0J7WEk35Tc^F%huogC=EZ>S znM>K3iSnSg;OMju6wt^RRp+v4KW_5=Jv0oM<)Gb|7CD!BsUryiXnCxtshK*{60FXD zx1QME?`Fu0Ta>(Mh0f?jBJAAHRol!DB9PmwrrL)&xGM9dUZI{K8VLHB%x>6kZ+4tW zLeGU*0!FH~TmR-r!n!(HD+Rj0gM6KDj*shO0j^;aQc{XcbXwy<4#6uNA46^raUu5O z*DB|!qy!HAwU1KbWiquK7c)f&Ay3L&oaTYj+NyYE?m+$U<=o z6%xCPVu|*Li$h{o7c&t!9vSOn1r?WTW8>ooatn{cVCQKJo>K)Qg?#|e_blCql-E;| z>VzsXW}b6L%4=&EUZHmWfQ&*mZh@XVi$6X6tL!&YkitM3nff$nBQ95pFd%woA$YzXnt(uwv)NJ@2rduGL7@@1B+SNw`Ha`HCP~}!5hD0 z(fSQP1q3!6E-R2dJ*}JTO^}?G1;eqM;NeJ*nh6lE@*w>;?|f80q!omlBCEE#`SPg5 zUhSgX5R)#u{5E!WU?G42zI+?z{%m^j8RXkU1HNE9tCXoZ;KN(guI`zmU=u}vu!Ql` z>iD^jSEzpDu8JC3Mw(Sjm|&iQ;_e02+gnK|FM%8Hbe{S=Yvb;B^>}W&mra+CnRo2* zhXbPDqI#YcEO5}elnlmW1S=)~8T|}07<+JV5TL15^6MCTh(l94O;@4y*Ks_zGo0%9 z`{Nb1W3GHi$m52miYgUdOK+k2W99j5cpfgm%&zwXvnJBPJt(KDN)mKdP{yabuT`jm zL@F(i`)JP;ug>gDfy3pDxisV+m(lw@Rrkisy(A@vX<^7iu( z_fBB;g()g4H;cJL25ZL0pXYjqM7$?wfk0ZH<8H=cu>jmc`pivC`j+lPAB;8M02!?y z#TO;W`^4%e* z^(1v$tHnM8+;AOiyu2AREwW$&-(`{nQe$FBK|VeNG6*?;ErfH*fXi)*>qHZDJ*2yN zbK|NcWMm#}Ewg6k65tF+?03QQzyd;4>A-Z~bg@+L%bqM2s@h(lS>E2>UKlP8&Ih|= zy>_?G(a8z7CZ|`{=je>wbYH=gBw-n641e6j$6t28`egu1t{dfhv%>9u=d~W zFo)=0>W=^-O%ywYzplEuFCJW%dr$kwmrXw%-<@L?bS?voUNAVQoa+r4{?tG9!L?`U z1>`DA_2SCPFCfcUGW(OwKx5ry$wxBrisItpCf-*dR(J`5ZZaUX1!(Ntoosl`hL`K{ zRuTbl1U4-!WZ^dR&z~=Iy$RD(Q$1p=>gmC}Nt<3jB^dxsMXjeBC6EJo3Jwlx@+$UN z%n%1JZ*0cKIU!Zy22sE}?{?QF+S}SB#^&(=DZK@qUjT|1{A4AC^ra#Y+Cb~(;?b43 z4jyd@p1?8N+knIBiNS)2u{{$I$k1yE%C4>!x3L)`tU5Qvg$Gd%POhud0?kfwc0+bm z6+JrX!_t7U3uu(0;^Y+PLkH>0uc%O&@6rAR>c-D*?dMT}%JaP2_$Pk^k;B99Fs~tF zPc@ji!j(P-7}7s_q!Rpm(z30mH$t7AZqV?mz@xxs-(jvB+Y3S{h!(cGEm6dAY;dgEX{n-NBHctR-nn?4baAu1zM@KWc~{{`N|tnmN< literal 0 HcmV?d00001 diff --git a/lib/widgets/device_tile.dart b/lib/widgets/device_tile.dart index 9dd6d5b..3f30b38 100644 --- a/lib/widgets/device_tile.dart +++ b/lib/widgets/device_tile.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import '../l10n/l10n.dart'; +import 'signal_ui.dart'; /// A reusable tile widget for displaying a MeshCore device in a list class DeviceTile extends StatelessWidget { @@ -33,28 +34,25 @@ class DeviceTile extends StatelessWidget { } Widget _buildSignalIcon(int rssi) { - IconData icon; - Color color; - - if (rssi >= -60) { - icon = Icons.signal_cellular_4_bar; - color = Colors.green; - } else if (rssi >= -70) { - icon = Icons.signal_cellular_alt; - color = Colors.lightGreen; - } else if (rssi >= -80) { - icon = Icons.signal_cellular_alt_2_bar; - color = Colors.orange; - } else { - icon = Icons.signal_cellular_alt_1_bar; - color = Colors.red; - } + final tier = rssi >= -60 + ? 0 + : rssi >= -70 + ? 1 + : rssi >= -80 + ? 2 + : rssi >= -90 + ? 3 + : 4; + final signalUi = signalUiForStrengthTier(tier); return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(icon, color: color), - Text('$rssi dBm', style: TextStyle(fontSize: 10, color: color)), + Icon(signalUi.icon, color: signalUi.color), + Text( + '$rssi dBm', + style: TextStyle(fontSize: 10, color: signalUi.color), + ), ], ); } diff --git a/lib/widgets/signal_ui.dart b/lib/widgets/signal_ui.dart new file mode 100644 index 0000000..e0e0511 --- /dev/null +++ b/lib/widgets/signal_ui.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class SignalUi { + final IconData icon; + final Color color; + + const SignalUi({required this.icon, required this.color}); +} + +SignalUi signalUiForStrengthTier(int tier) { + switch (tier) { + case 0: + return const SignalUi( + icon: Icons.signal_cellular_4_bar, + color: Colors.green, + ); + case 1: + return const SignalUi( + icon: Icons.signal_cellular_alt, + color: Colors.lightGreen, + ); + case 2: + return const SignalUi( + icon: Icons.signal_cellular_alt_2_bar, + color: Colors.amber, + ); + case 3: + return const SignalUi( + icon: Icons.signal_cellular_alt_1_bar, + color: Colors.orange, + ); + default: + return const SignalUi( + icon: Icons.signal_cellular_alt_1_bar, + color: Colors.red, + ); + } +} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index db4fb8e..f3becea 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import 'signal_ui.dart'; class SNRUi { final IconData icon; @@ -38,28 +39,19 @@ SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) { final snrLevels = getSNRfromSF(spreadingFactor); - IconData icon; - Color color; String text = '${snr.toStringAsFixed(1)} dB'; + final tier = snr >= snrLevels[0] + ? 0 + : snr >= snrLevels[1] + ? 1 + : snr >= snrLevels[2] + ? 2 + : snr >= snrLevels[3] + ? 3 + : 4; + final signalUi = signalUiForStrengthTier(tier); - if (snr >= snrLevels[0]) { - icon = Icons.signal_cellular_alt; - color = Colors.green; - } else if (snr >= snrLevels[1]) { - icon = Icons.signal_cellular_alt; - color = Colors.lightGreen; - } else if (snr >= snrLevels[2]) { - icon = Icons.signal_cellular_alt; - color = Colors.yellow; - } else if (snr >= snrLevels[3]) { - icon = Icons.signal_cellular_alt_2_bar; - color = Colors.orange; - } else { - icon = Icons.signal_cellular_alt_1_bar; - color = Colors.red; - } - - return SNRUi(icon, color, text); + return SNRUi(signalUi.icon, signalUi.color, text); } class SNRIndicator extends StatefulWidget { From 75610695c2fed89dd5688ef62d35d7418ff5a4c4 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 28 Feb 2026 19:11:11 -0800 Subject: [PATCH 207/421] Add contact settings and discovery features - Implemented contact settings in localization files for Swedish, Ukrainian, and Chinese. - Added new DiscoveryContact model to handle discovered contacts. - Created DiscoveryScreen to display discovered contacts with filtering and sorting options. - Integrated contact discovery storage to persist discovered contacts. - Updated settings screen to include options for automatic contact addition. - Enhanced app bar and list filter widgets for better user experience. - Fixed variable naming inconsistencies in contact model. --- lib/connector/meshcore_connector.dart | 119 ++++++-- lib/connector/meshcore_protocol.dart | 52 +++- lib/l10n/app_en.arb | 22 +- lib/l10n/app_localizations.dart | 108 +++++++ lib/l10n/app_localizations_bg.dart | 61 ++++ lib/l10n/app_localizations_de.dart | 61 ++++ lib/l10n/app_localizations_en.dart | 61 ++++ lib/l10n/app_localizations_es.dart | 61 ++++ lib/l10n/app_localizations_fr.dart | 61 ++++ lib/l10n/app_localizations_it.dart | 61 ++++ lib/l10n/app_localizations_nl.dart | 61 ++++ lib/l10n/app_localizations_pl.dart | 61 ++++ lib/l10n/app_localizations_pt.dart | 61 ++++ lib/l10n/app_localizations_ru.dart | 61 ++++ lib/l10n/app_localizations_sk.dart | 61 ++++ lib/l10n/app_localizations_sl.dart | 61 ++++ lib/l10n/app_localizations_sv.dart | 61 ++++ lib/l10n/app_localizations_uk.dart | 61 ++++ lib/l10n/app_localizations_zh.dart | 61 ++++ lib/models/contact.dart | 2 +- lib/models/discovery_contact.dart | 137 +++++++++ lib/screens/contacts_screen.dart | 16 ++ lib/screens/discovery_screen.dart | 347 +++++++++++++++++++++++ lib/screens/settings_screen.dart | 113 +++++++- lib/services/ble_debug_log_service.dart | 4 +- lib/storage/contact_discovery_store.dart | 59 ++++ lib/widgets/app_bar.dart | 15 +- lib/widgets/list_filter_widget.dart | 90 ++++++ 28 files changed, 1958 insertions(+), 41 deletions(-) create mode 100644 lib/models/discovery_contact.dart create mode 100644 lib/screens/discovery_screen.dart create mode 100644 lib/storage/contact_discovery_store.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ef19f02..c142f84 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart' as crypto; +import 'package:meshcore_open/models/discovery_contact.dart'; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -24,6 +25,7 @@ import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; import '../storage/channel_store.dart'; +import '../storage/contact_discovery_store.dart'; import '../storage/contact_settings_store.dart'; import '../storage/contact_store.dart'; import '../storage/message_store.dart'; @@ -111,6 +113,7 @@ class MeshCoreConnector extends ChangeNotifier { final List _scanResults = []; final List _contacts = []; + final List _discoveredContacts = []; final List _channels = []; final Map> _conversations = {}; final Map> _channelMessages = {}; @@ -155,6 +158,12 @@ class MeshCoreConnector extends ChangeNotifier { bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _preserveContactsOnRefresh = false; + bool _autoAddUsers = false; + bool _autoAddRepeaters = false; + bool _autoAddRoomServers = false; + bool _autoAddSensors = false; + bool _overwriteOldest = false; + static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; int _maxContacts = _defaultMaxContacts; @@ -195,6 +204,7 @@ class MeshCoreConnector extends ChangeNotifier { final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore(); final ContactSettingsStore _contactSettingsStore = ContactSettingsStore(); final ContactStore _contactStore = ContactStore(); + final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore(); final ChannelStore _channelStore = ChannelStore(); final UnreadStore _unreadStore = UnreadStore(); List _cachedChannels = []; @@ -242,6 +252,10 @@ class MeshCoreConnector extends ChangeNotifier { ); } + List get discoveredContacts { + return List.unmodifiable(_discoveredContacts); + } + List get channels => List.unmodifiable(_channels); bool get isConnected => _state == MeshCoreConnectionState.connected; bool get isLoadingContacts => _isLoadingContacts; @@ -258,6 +272,11 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + bool? get autoAddUsers => _autoAddUsers; + bool? get autoAddRepeaters => _autoAddRepeaters; + bool? get autoAddRoomServers => _autoAddRoomServers; + bool? get autoAddSensors => _autoAddSensors; + bool? get autoAddOverwriteOldest => _overwriteOldest; bool? get clientRepeat => _clientRepeat; int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; @@ -602,6 +621,13 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future loadDiscoveredContactCache() async { + final cached = await _discoveryContactStore.loadContacts(); + _discoveredContacts + ..clear() + ..addAll(cached); + } + Future loadChannelSettings({int? maxChannels}) async { _channelSmazEnabled.clear(); final channelCount = maxChannels ?? _maxChannels; @@ -852,6 +878,9 @@ class MeshCoreConnector extends ChangeNotifier { // Fetch channels so we can track unread counts for incoming messages unawaited(getChannels()); + + // Load discovered contacts from storage + unawaited(loadDiscoveredContactCache()); } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -972,6 +1001,7 @@ class MeshCoreConnector extends ChangeNotifier { _deviceDisplayName = null; _deviceId = null; _contacts.clear(); + _discoveredContacts.clear(); _conversations.clear(); _loadedConversationKeys.clear(); _selfPublicKey = null; @@ -1064,6 +1094,7 @@ class MeshCoreConnector extends ChangeNotifier { await requestBatteryStatus(force: true); await sendFrame(buildGetRadioSettingsFrame()); await sendFrame(buildGetCustomVarsFrame()); + await sendFrame(buildGetAutoAddFlagsFrame()); _scheduleSelfInfoRetry(); } @@ -1074,7 +1105,7 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildAppStartFrame()); await sendFrame(buildGetCustomVarsFrame()); await requestBatteryStatus(); - + await sendFrame(buildGetAutoAddFlagsFrame()); _scheduleSelfInfoRetry(); } @@ -1903,8 +1934,8 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeChannelInfo: _handleChannelInfo(frame); break; - case respCodeRadioSettings: - _handleRadioSettings(frame); + case respCodeAutoAddConfig: + _handleAutoAddConfig(frame); break; case respCodeBattAndStorage: _handleBatteryAndStorage(frame); @@ -1985,6 +2016,10 @@ class MeshCoreConnector extends ChangeNotifier { _selfLatitude = readInt32LE(frame, 36) / 1000000.0; _selfLongitude = readInt32LE(frame, 40) / 1000000.0; + if (frame.length >= 47 && frame[47] == 0x00) { + sendFrame(buildSetOtherParamsFrame(0, 0, 0)); + } + // Radio settings (if frame is long enough) if (frame.length >= 58) { _currentFreqHz = readUint32LE(frame, 48); @@ -1992,7 +2027,6 @@ class MeshCoreConnector extends ChangeNotifier { _currentSf = frame[56]; _currentCr = frame[57]; } - // Node name starts at offset 58 if frame is long enough if (frame.length > 58) { _selfName = readCString(frame, 58, frame.length - 58); @@ -2056,25 +2090,6 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_requestNextQueuedMessage()); } - void _handleRadioSettings(Uint8List frame) { - // Frame format from C++: - // [0] = RESP_CODE_RADIO_SETTINGS - // [1-4] = freq (uint32 LE, in Hz) - // [5-8] = bw (uint32 LE, in Hz) - // [9] = sf - // [10] = cr - if (frame.length >= 11) { - _currentFreqHz = readUint32LE(frame, 1); - _currentBwHz = readUint32LE(frame, 5); - _currentSf = frame[9]; - _currentCr = frame[10]; - debugPrint( - 'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr', - ); - notifyListeners(); - } - } - void _handleBatteryAndStorage(Uint8List frame) { // Frame format from C++: // [0] = RESP_CODE_BATT_AND_STORAGE @@ -2275,6 +2290,10 @@ class MeshCoreConnector extends ChangeNotifier { await _contactStore.saveContacts(_contacts); } + Future _persistDiscoveredContacts() async { + await _discoveryContactStore.saveContacts(_discoveredContacts); + } + int _latestContactLastmod() { if (_contacts.isEmpty) return 0; var latest = 0; @@ -3739,6 +3758,7 @@ class MeshCoreConnector extends ChangeNotifier { return; } + //We ignore our own adverts if (listEquals(publicKey, _selfPublicKey)) { return; } @@ -3759,7 +3779,14 @@ class MeshCoreConnector extends ChangeNotifier { longitude: longitude, lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), ); - _handleContactAdvert(newContact); + if ((_autoAddUsers && type == advTypeChat) || + (_autoAddRepeaters && type == advTypeRepeater) || + (_autoAddRoomServers && type == advTypeRoom) || + (_autoAddSensors && type == advTypeSensor)) { + _handleContactAdvert(newContact); + } else { + _handleDiscovery(newContact); + } _updateDirectRepeater(newContact, snr, path); return; } @@ -3847,6 +3874,50 @@ class MeshCoreConnector extends ChangeNotifier { } notifyListeners(); } + + void _handleAutoAddConfig(Uint8List frame) { + final reader = BufferReader(frame); + try { + reader.skipBytes(1); // Skip the response code byte + final flags = reader.readByte(); + _autoAddUsers = flags & autoAddChatFlag != 0; + _autoAddRepeaters = flags & autoAddRepeaterFlag != 0; + _autoAddRoomServers = flags & autoAddRoomServerFlag != 0; + _autoAddSensors = flags & autoAddSensorFlag != 0; + _overwriteOldest = flags & autoAddOverwriteOldestFlag != 0; + } catch (e) { + appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector'); + } + } + + void _handleDiscovery(Contact contact) { + debugPrint('Discovered new contact: ${contact.name}'); + final disContact = DiscoveryContact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + latitude: contact.latitude, + longitude: contact.longitude, + lastSeen: contact.lastSeen, + ); + _discoveredContacts.add(disContact); + + unawaited(_persistDiscoveredContacts()); + + // Show notification for new contact (advertisement) + if (_appSettingsService != null) { + final settings = _appSettingsService!.settings; + if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { + _notificationService.showAdvertNotification( + contactName: contact.name, + contactType: contact.typeLabel, + contactId: contact.publicKeyHex, + ); + } + } + } } const int _phRouteMask = 0x03; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index d5ce9ee..36c13e2 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -167,6 +167,8 @@ const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; const int cmdSendBinaryReq = 50; +const int cmdSetAutoAddConfig = 58; +const int cmdGetAutoAddConfig = 59; // Text message types const int txtTypePlain = 0; @@ -200,8 +202,8 @@ const int respCodeDeviceInfo = 13; const int respCodeContactMsgRecvV3 = 16; const int respCodeChannelMsgRecvV3 = 17; const int respCodeChannelInfo = 18; -const int respCodeRadioSettings = 25; const int respCodeCustomVars = 21; +const int respCodeAutoAddConfig = 25; // Push codes (async from device) const int pushCodeAdvert = 0x80; @@ -247,6 +249,18 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet const int payloadTypeRawCustom = 0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc +//auto-add flags +const int autoAddOverwriteOldestFlag = + 1 << 0; // 0x01 - overwrite oldest non-favourite when full +const int autoAddChatFlag = + 1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT) +const int autoAddRepeaterFlag = + 1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER) +const int autoAddRoomServerFlag = + 1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) +const int autoAddSensorFlag = + 1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) + // Sizes const int pubKeySize = 32; const int maxPathSize = 64; @@ -297,7 +311,7 @@ const int contactNameOffset = 100; const int contactTimestampOffset = 132; const int contactLatOffset = 136; const int contactLonOffset = 140; -const int contactLastmodOffset = 144; +const int contactLastModOffset = 144; const int contactFrameSize = 148; // Message frame offsets @@ -685,6 +699,10 @@ Uint8List buildGetCustomVarsFrame() { return Uint8List.fromList([cmdGetCustomVar]); } +Uint8List buildGetAutoAddFlagsFrame() { + return Uint8List.fromList([cmdGetAutoAddConfig]); +} + // Calculate LoRa airtime for a packet // Based on Semtech SX127x datasheet formula // Returns airtime in milliseconds @@ -826,20 +844,40 @@ Uint8List buildZeroHopContact(Uint8List pubKey) { } // Build CMD_SET_OTHER_PARAMS frame -// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks] +// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks] Uint8List buildSetOtherParamsFrame( - bool allowAutoAddContacts, int allowTelemetryFlags, int advertLocationPolicy, int multiAcks, ) { final writer = BufferWriter(); writer.writeByte(cmdSetOtherParams); - writer.writeByte( - allowAutoAddContacts ? 0x00 : 0x01, - ); // Allow Auto Add Contacts + //Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags + //Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled). + writer.writeByte(0x01); writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags writer.writeByte(advertLocationPolicy); // Advertisement Location Policy writer.writeByte(multiAcks); // Multi Acknowledgements return writer.toBytes(); } + +// Build CMD_SET_AUTO_ADD_CONFIG frame +// Format: [cmd][flags] +Uint8List buildSetAutoAddConfigFrame({ + required bool autoAddChat, + required bool autoAddRepeater, + required bool autoAddRoomServer, + required bool autoAddSensor, + required bool overwriteOldest, +}) { + final writer = BufferWriter(); + writer.writeByte(cmdSetAutoAddConfig); + int flags = 0; + if (autoAddChat) flags |= autoAddChatFlag; + if (autoAddRepeater) flags |= autoAddRepeaterFlag; + if (autoAddRoomServer) flags |= autoAddRoomServerFlag; + if (autoAddSensor) flags |= autoAddSensorFlag; + if (overwriteOldest) flags |= autoAddOverwriteOldestFlag; + writer.writeByte(flags); + return writer.toBytes(); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 175c346..3f92d0e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -98,6 +98,8 @@ "settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", + "settings_contactSettings": "Contact Settings", + "settings_contactSettingsSubtitle": "Settings for how contacts are added.", "settings_privacyMode": "Privacy Mode", "settings_privacyModeSubtitle": "Hide name/location in advertisements", "settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.", @@ -1837,5 +1839,21 @@ "settings_gpxExportShareText": "Map data exported from meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", - "snrIndicator_lastSeen": "Last seen" -} + "snrIndicator_lastSeen": "Last seen", + "contactsSettings_title": "Contacts settings", + "contactsSettings_autoAddTitle": "Automatic Discovery", + "contactsSettings_otherTitle": "Other contact related settings", + "contactsSettings_autoAddUsersTitle": "Auto-add users", + "contactsSettings_autoAddUsersSubtitle": "Allow the companion to automatically add discovered users.", + "contactsSettings_autoAddRepeatersTitle": "Auto-add repeaters", + "contactsSettings_autoAddRepeatersSubtitle": "Allow the companion to automatically add discovered repeaters.", + "contactsSettings_autoAddRoomServersTitle": "Auto-add room servers", + "contactsSettings_autoAddRoomServersSubtitle": "Allow the companion to automatically add discovered room servers.", + "contactsSettings_autoAddSensorsTitle": "Auto-add sensors", + "contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.", + "contactsSettings_overwriteOldestTitle": "Overwrite Oldest", + "contactsSettings_overwriteOldestSubtitle": "When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.", + "discoveredContacts_Title": "Discovered Contacts", + "discoveredContacts_noMatching": "No matching contacts", + "discoveredContacts_searchHint": "Search discovered contacts" +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ff2c726..333147f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -544,6 +544,18 @@ abstract class AppLocalizations { /// **'Longitude'** String get settings_longitude; + /// No description provided for @settings_contactSettings. + /// + /// In en, this message translates to: + /// **'Contact Settings'** + String get settings_contactSettings; + + /// No description provided for @settings_contactSettingsSubtitle. + /// + /// In en, this message translates to: + /// **'Settings for how contacts are added.'** + String get settings_contactSettingsSubtitle; + /// No description provided for @settings_privacyMode. /// /// In en, this message translates to: @@ -5380,6 +5392,102 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Last seen'** String get snrIndicator_lastSeen; + + /// No description provided for @contactsSettings_title. + /// + /// In en, this message translates to: + /// **'Contacts settings'** + String get contactsSettings_title; + + /// No description provided for @contactsSettings_autoAddTitle. + /// + /// In en, this message translates to: + /// **'Automatic Discovery'** + String get contactsSettings_autoAddTitle; + + /// No description provided for @contactsSettings_otherTitle. + /// + /// In en, this message translates to: + /// **'Other contact related settings'** + String get contactsSettings_otherTitle; + + /// No description provided for @contactsSettings_autoAddUsersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add users'** + String get contactsSettings_autoAddUsersTitle; + + /// No description provided for @contactsSettings_autoAddUsersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered users.'** + String get contactsSettings_autoAddUsersSubtitle; + + /// No description provided for @contactsSettings_autoAddRepeatersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add repeaters'** + String get contactsSettings_autoAddRepeatersTitle; + + /// No description provided for @contactsSettings_autoAddRepeatersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered repeaters.'** + String get contactsSettings_autoAddRepeatersSubtitle; + + /// No description provided for @contactsSettings_autoAddRoomServersTitle. + /// + /// In en, this message translates to: + /// **'Auto-add room servers'** + String get contactsSettings_autoAddRoomServersTitle; + + /// No description provided for @contactsSettings_autoAddRoomServersSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered room servers.'** + String get contactsSettings_autoAddRoomServersSubtitle; + + /// No description provided for @contactsSettings_autoAddSensorsTitle. + /// + /// In en, this message translates to: + /// **'Auto-add sensors'** + String get contactsSettings_autoAddSensorsTitle; + + /// No description provided for @contactsSettings_autoAddSensorsSubtitle. + /// + /// In en, this message translates to: + /// **'Allow the companion to automatically add discovered sensors.'** + String get contactsSettings_autoAddSensorsSubtitle; + + /// No description provided for @contactsSettings_overwriteOldestTitle. + /// + /// In en, this message translates to: + /// **'Overwrite Oldest'** + String get contactsSettings_overwriteOldestTitle; + + /// No description provided for @contactsSettings_overwriteOldestSubtitle. + /// + /// In en, this message translates to: + /// **'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'** + String get contactsSettings_overwriteOldestSubtitle; + + /// No description provided for @discoveredContacts_Title. + /// + /// In en, this message translates to: + /// **'Discovered Contacts'** + String get discoveredContacts_Title; + + /// No description provided for @discoveredContacts_noMatching. + /// + /// In en, this message translates to: + /// **'No matching contacts'** + String get discoveredContacts_noMatching; + + /// No description provided for @discoveredContacts_searchHint. + /// + /// In en, this message translates to: + /// **'Search discovered contacts'** + String get discoveredContacts_searchHint; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index da7ddc9..c26b5b2 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -234,6 +234,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_longitude => 'Дължина'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Режим на поверителност'; @@ -3112,4 +3119,58 @@ class AppLocalizationsBg extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Последно видян'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 228ffe7..6d36471 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -233,6 +233,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_longitude => 'Längengrad'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privatsphäreeinstellung'; @@ -3121,4 +3128,58 @@ class AppLocalizationsDe extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Zuletzt gesehen'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 70b3393..c40b7df 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -232,6 +232,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privacy Mode'; @@ -3065,4 +3072,58 @@ class AppLocalizationsEn extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Last seen'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 876666b..203916b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -233,6 +233,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_longitude => 'Longitud'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Modo Privacidad'; @@ -3113,4 +3120,58 @@ class AppLocalizationsEs extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Visto por última vez'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 0c11eac..7ae1b25 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -234,6 +234,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Mode de confidentialité'; @@ -3135,4 +3142,58 @@ class AppLocalizationsFr extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Dernière fois vu'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 8a8fe71..be88ecb 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -233,6 +233,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_longitude => 'Longitudine'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Modalità Privacy'; @@ -3116,4 +3123,58 @@ class AppLocalizationsIt extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Ultimo accesso'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 8b4eee5..ad1f624 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -233,6 +233,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_longitude => 'Lengtegraad'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privacy Mode'; @@ -3103,4 +3110,58 @@ class AppLocalizationsNl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Laatst gezien'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index cff6010..a97599c 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -235,6 +235,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_longitude => 'Długość'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Tryb Prywatny'; @@ -3116,4 +3123,58 @@ class AppLocalizationsPl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Ostatnio widziany'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 831d47a..e1f89e2 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -234,6 +234,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_longitude => 'Longitude'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Modo de Privacidade'; @@ -3111,4 +3118,58 @@ class AppLocalizationsPt extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Visto pela última vez'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5c73e3e..b2f3718 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -232,6 +232,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_longitude => 'Долгота'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Режим конфиденциальности'; @@ -3123,4 +3130,58 @@ class AppLocalizationsRu extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Последний раз видели'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index b4f28fb..773b338 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -233,6 +233,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_longitude => 'Dĺžka'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Režim ochrany súkromia'; @@ -3098,4 +3105,58 @@ class AppLocalizationsSk extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Naposledy videný'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e015e45..0f38b94 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -233,6 +233,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_longitude => 'Dolžina'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Zasebnost'; @@ -3103,4 +3110,58 @@ class AppLocalizationsSl extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Zadnjič videno'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 3a25c3c..4e67267 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -232,6 +232,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_longitude => 'Längdgrad'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Privatläge'; @@ -3081,4 +3088,58 @@ class AppLocalizationsSv extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Senast sedd'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index cd820cf..40a52ca 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -232,6 +232,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_longitude => 'Довгота'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => 'Режим приватності'; @@ -3130,4 +3137,58 @@ class AppLocalizationsUk extends AppLocalizations { @override String get snrIndicator_lastSeen => 'Останній раз бачили'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b63f714..4aec80b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -226,6 +226,13 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_longitude => '经度'; + @override + String get settings_contactSettings => 'Contact Settings'; + + @override + String get settings_contactSettingsSubtitle => + 'Settings for how contacts are added.'; + @override String get settings_privacyMode => '隐私模式'; @@ -2890,4 +2897,58 @@ class AppLocalizationsZh extends AppLocalizations { @override String get snrIndicator_lastSeen => '最近访问'; + + @override + String get contactsSettings_title => 'Contacts settings'; + + @override + String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + + @override + String get contactsSettings_otherTitle => 'Other contact related settings'; + + @override + String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + + @override + String get contactsSettings_autoAddUsersSubtitle => + 'Allow the companion to automatically add discovered users.'; + + @override + String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + + @override + String get contactsSettings_autoAddRepeatersSubtitle => + 'Allow the companion to automatically add discovered repeaters.'; + + @override + String get contactsSettings_autoAddRoomServersTitle => + 'Auto-add room servers'; + + @override + String get contactsSettings_autoAddRoomServersSubtitle => + 'Allow the companion to automatically add discovered room servers.'; + + @override + String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + + @override + String get contactsSettings_autoAddSensorsSubtitle => + 'Allow the companion to automatically add discovered sensors.'; + + @override + String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + + @override + String get contactsSettings_overwriteOldestSubtitle => + 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + + @override + String get discoveredContacts_Title => 'Discovered Contacts'; + + @override + String get discoveredContacts_noMatching => 'No matching contacts'; + + @override + String get discoveredContacts_searchHint => 'Search discovered contacts'; } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 5e532e6..7d8e011 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -183,7 +183,7 @@ class Contact { ) : Uint8List(0); final name = readCString(data, contactNameOffset, maxNameSize); - final lastmod = readUint32LE(data, contactLastmodOffset); + final lastmod = readUint32LE(data, contactLastModOffset); double? lat, lon; final latRaw = readInt32LE(data, contactLatOffset); diff --git a/lib/models/discovery_contact.dart b/lib/models/discovery_contact.dart new file mode 100644 index 0000000..54c2937 --- /dev/null +++ b/lib/models/discovery_contact.dart @@ -0,0 +1,137 @@ +import 'dart:typed_data'; +import '../connector/meshcore_protocol.dart'; + +class DiscoveryContact { + final Uint8List publicKey; + final String name; + final int type; + final int pathLength; // -1 = flood, 0+ = direct hops (from device) + final Uint8List path; // Path bytes from device + final double? latitude; + final double? longitude; + final DateTime lastSeen; + + DiscoveryContact({ + required this.publicKey, + required this.name, + required this.type, + required this.pathLength, + required this.path, + this.latitude, + this.longitude, + required this.lastSeen, + }); + + String get publicKeyHex => pubKeyToHex(publicKey); + + String get typeLabel { + switch (type) { + case advTypeChat: + return 'Chat'; + case advTypeRepeater: + return 'Repeater'; + case advTypeRoom: + return 'Room'; + case advTypeSensor: + return 'Sensor'; + default: + return 'Unknown'; + } + } + + String get pathLabel { + if (pathLength < 0) return 'Flood'; + if (pathLength == 0) return 'Direct'; + return '$pathLength hops'; + } + + bool get hasLocation => latitude != null && longitude != null; + + DiscoveryContact copyWith({ + Uint8List? publicKey, + String? name, + int? type, + int? pathLength, + Uint8List? path, + double? latitude, + double? longitude, + DateTime? lastSeen, + }) { + return DiscoveryContact( + publicKey: publicKey ?? this.publicKey, + name: name ?? this.name, + type: type ?? this.type, + pathLength: pathLength ?? this.pathLength, + path: path ?? this.path, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + lastSeen: lastSeen ?? this.lastSeen, + ); + } + + String get pathIdList { + final pathBytes = path; + if (pathBytes.isEmpty) return ''; + final parts = []; + final groupSize = pathHashSize; + for (int i = 0; i < pathBytes.length; i += groupSize) { + final end = (i + groupSize) <= pathBytes.length + ? (i + groupSize) + : pathBytes.length; + final chunk = pathBytes.sublist(i, end); + parts.add( + chunk + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(), + ); + } + return parts.join(','); + } + + String get shortPubKeyHex { + return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; + } + + Uint8List? get traceRouteBytes { + final pathBytes = path; + Uint8List? traceBytes; + + if (pathBytes.isEmpty) { + traceBytes = Uint8List(1); + traceBytes[0] = publicKey[0]; + return traceBytes; + } + + if (type == advTypeRepeater || type == advTypeRoom) { + final len = (pathBytes.length + pathBytes.length + 1); + traceBytes = Uint8List(len); + traceBytes[pathBytes.length] = publicKey[0]; + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + } else { + if (pathBytes.length < 2) { + return pathBytes[0] == 0 ? null : pathBytes; + } + final len = (pathBytes.length + pathBytes.length - 1); + traceBytes = Uint8List(len); + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length - 1) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + } + return traceBytes; + } + + @override + bool operator ==(Object other) => + other is DiscoveryContact && publicKeyHex == other.publicKeyHex; + + @override + int get hashCode => publicKeyHex.hashCode; +} diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index eeecfb9..6e9f841 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -26,6 +26,7 @@ import '../widgets/room_login_dialog.dart'; import '../widgets/unread_badge.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; +import 'discovery_screen.dart'; import 'map_screen.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; @@ -318,6 +319,21 @@ class _ContactsScreenState extends State ), onTap: () => _disconnect(context, connector), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.person_add_rounded), + const SizedBox(width: 8), + Text("Discovered Contacts"), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DiscoveryScreen(), + ), + ), + ), PopupMenuItem( child: Row( children: [ diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart new file mode 100644 index 0000000..b8f49fa --- /dev/null +++ b/lib/screens/discovery_screen.dart @@ -0,0 +1,347 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:meshcore_open/models/contact.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../connector/meshcore_protocol.dart'; +import '../l10n/l10n.dart'; +import '../models/discovery_contact.dart'; +import '../utils/contact_search.dart'; +import '../widgets/app_bar.dart'; +import '../widgets/list_filter_widget.dart'; + +enum DiscoverySortOption { lastSeen, name, type } + +class DiscoveryScreen extends StatefulWidget { + const DiscoveryScreen({super.key}); + + @override + State createState() => _DiscoveryScreenState(); +} + +class _DiscoveryScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + String searchQuery = ''; + ContactSortOption sortOption = ContactSortOption.lastSeen; + bool showUnreadOnly = false; + ContactTypeFilter typeFilter = ContactTypeFilter.all; + DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen; + Timer? _searchDebounce; + + @override + void dispose() { + _searchController.dispose(); + _searchDebounce?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final connector = context.watch(); + + final discoveredContacts = connector.discoveredContacts; + final filteredAndSorted = _filterAndSortContacts( + discoveredContacts, + connector, + ); + + return Scaffold( + appBar: AppBar( + title: AppBarTitle( + l10n.discoveredContacts_Title, + indicators: false, + subtitle: false, + ), + centerTitle: true, + ), + body: Column( + children: [ + _buildFilters(filteredAndSorted, connector), + Expanded( + child: discoveredContacts.isEmpty + ? Center(child: Text(l10n.contacts_noContacts)) + : filteredAndSorted.isEmpty + ? Center(child: Text(l10n.discoveredContacts_noMatching)) + : ListView.builder( + itemCount: filteredAndSorted.length, + itemBuilder: (context, index) { + final contact = filteredAndSorted[index]; + return ListTile( + leading: CircleAvatar( + backgroundColor: _getTypeColor(contact.type), + child: Icon( + _getTypeIcon(contact.type), + color: Colors.white, + size: 20, + ), + ), + title: Text( + contact.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + contact.shortPubKeyHex, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Text( + _formatLastSeen(context, contact.lastSeen), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildFilters(filteredAndSorted, connector) { + final l10n = context.l10n; + + String hintText = ""; + switch (typeFilter) { + case ContactTypeFilter.all: + hintText = context.l10n.contacts_searchContacts( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.users: + hintText = context.l10n.contacts_searchUsers( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.repeaters: + hintText = context.l10n.contacts_searchRepeaters( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.rooms: + hintText = context.l10n.contacts_searchRoomServers( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.favorites: + hintText = context.l10n.contacts_searchFavorites( + filteredAndSorted.length, + showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: hintText, + prefixIcon: const Icon(Icons.search), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (searchQuery.isNotEmpty) + IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + setState(() { + searchQuery = ''; + }); + }, + ), + _buildFilterButton(context, connector), + ], + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + _searchDebounce?.cancel(); + _searchDebounce = Timer(const Duration(milliseconds: 300), () { + if (!mounted) return; + setState(() { + searchQuery = value.toLowerCase(); + }); + }); + }, + ), + ), + ], + ); + } + + Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) { + return DiscoveryContactsFilterMenu( + sortOption: sortOption, + typeFilter: typeFilter, + onSortChanged: (value) { + setState(() { + sortOption = value; + }); + }, + onTypeFilterChanged: (value) { + setState(() { + typeFilter = value; + }); + }, + ); + } + + List _filterAndSortContacts( + List contacts, + MeshCoreConnector connector, + ) { + var filtered = contacts.where((contact) { + if (searchQuery.isEmpty) return true; + return matchesContactQuery( + Contact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + lastSeen: contact.lastSeen, + ), + searchQuery, + ); + }).toList(); + + // Filter out own node from the list + if (connector.selfPublicKey != null) { + final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!); + filtered = filtered.where((contact) { + return contact.publicKeyHex != selfPubKeyHex; + }).toList(); + } + + if (typeFilter != ContactTypeFilter.all) { + filtered = filtered.where(_matchesTypeFilter).toList(); + } + + if (showUnreadOnly) { + filtered = filtered.where((contact) { + return connector.getUnreadCountForContact( + Contact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + lastSeen: contact.lastSeen, + ), + ) > + 0; + }).toList(); + } + + switch (sortOption) { + case ContactSortOption.lastSeen: + filtered.sort( + (a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)), + ); + break; + case ContactSortOption.name: + filtered.sort( + (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), + ); + break; + default: + break; + } + + return filtered; + } + + bool _matchesTypeFilter(DiscoveryContact contact) { + switch (typeFilter) { + case ContactTypeFilter.all: + return true; + case ContactTypeFilter.users: + return contact.type == advTypeChat; + case ContactTypeFilter.repeaters: + return contact.type == advTypeRepeater; + case ContactTypeFilter.rooms: + return contact.type == advTypeRoom; + default: + return false; + } + } + + DateTime _resolveLastSeen(DiscoveryContact contact) { + if (contact.type != advTypeChat) return contact.lastSeen; + return contact.lastSeen.isAfter(contact.lastSeen) + ? contact.lastSeen + : contact.lastSeen; + } + + IconData _getTypeIcon(int type) { + switch (type) { + case advTypeChat: + return Icons.chat; + case advTypeRepeater: + return Icons.cell_tower; + case advTypeRoom: + return Icons.group; + case advTypeSensor: + return Icons.sensors; + default: + return Icons.device_unknown; + } + } + + Color _getTypeColor(int type) { + switch (type) { + case advTypeChat: + return Colors.blue; + case advTypeRepeater: + return Colors.orange; + case advTypeRoom: + return Colors.purple; + case advTypeSensor: + return Colors.green; + default: + return Colors.grey; + } + } + + String _formatLastSeen(BuildContext context, DateTime lastSeen) { + final now = DateTime.now(); + final diff = now.difference(lastSeen); + + if (diff.isNegative || diff.inMinutes < 5) { + return context.l10n.contacts_lastSeenNow; + } + if (diff.inMinutes < 60) { + return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes); + } + if (diff.inHours < 24) { + final hours = diff.inHours; + return hours == 1 + ? context.l10n.contacts_lastSeenHourAgo + : context.l10n.contacts_lastSeenHoursAgo(hours); + } + final days = diff.inDays; + return days == 1 + ? context.l10n.contacts_lastSeenDayAgo + : context.l10n.contacts_lastSeenDaysAgo(days); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index a198f99..fe893f8 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; import '../widgets/adaptive_app_bar_title.dart'; +import '../widgets/app_bar.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -43,8 +44,11 @@ class _SettingsScreenState extends State { final l10n = context.l10n; return Scaffold( appBar: AppBar( - title: AdaptiveAppBarTitle(l10n.settings_title), - centerTitle: true, + title: AppBarTitle( + l10n.settings_title, + indicators: false, + subtitle: false, + ), ), body: SafeArea( top: false, @@ -274,6 +278,14 @@ class _SettingsScreenState extends State { onTap: () => _editLocation(context, connector), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.group_add_outlined), + title: Text(l10n.settings_contactSettings), + subtitle: Text(l10n.settings_contactSettingsSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => _editAutoAddConfig(context, connector), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.visibility_off_outlined), title: Text(l10n.settings_privacyMode), @@ -849,6 +861,103 @@ class _SettingsScreenState extends State { ), ); } + + void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + bool autoAddChat = false; + bool autoAddRepeater = false; + bool autoAddRoomServer = false; + bool autoAddSensor = false; + bool overwriteOldest = false; + + final connector = context.read(); + autoAddChat = connector.autoAddUsers ?? false; + autoAddRepeater = connector.autoAddRepeaters ?? false; + autoAddRoomServer = connector.autoAddRoomServers ?? false; + autoAddSensor = connector.autoAddSensors ?? false; + overwriteOldest = connector.autoAddOverwriteOldest ?? false; + + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: Text(l10n.contactsSettings_autoAddTitle), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + FeatureToggleRow( + title: l10n.contactsSettings_autoAddUsersTitle, + subtitle: l10n.contactsSettings_autoAddUsersSubtitle, + value: autoAddChat, + onChanged: (value) { + setDialogState(() => autoAddChat = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddRepeatersTitle, + subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle, + value: autoAddRepeater, + onChanged: (value) { + setDialogState(() => autoAddRepeater = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddRoomServersTitle, + subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle, + value: autoAddRoomServer, + onChanged: (value) { + setDialogState(() => autoAddRoomServer = value); + }, + ), + SizedBox(height: 8), + FeatureToggleRow( + title: l10n.contactsSettings_autoAddSensorsTitle, + subtitle: l10n.contactsSettings_autoAddSensorsSubtitle, + value: autoAddSensor, + onChanged: (value) { + setDialogState(() => autoAddSensor = value); + }, + ), + Divider(height: 4), + FeatureToggleRow( + title: l10n.contactsSettings_overwriteOldestTitle, + subtitle: l10n.contactsSettings_overwriteOldestSubtitle, + value: overwriteOldest, + onChanged: (value) { + setDialogState(() => overwriteOldest = value); + }, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () async { + final frame = buildSetAutoAddConfigFrame( + autoAddChat: autoAddChat, + autoAddRepeater: autoAddRepeater, + autoAddRoomServer: autoAddRoomServer, + autoAddSensor: autoAddSensor, + overwriteOldest: overwriteOldest, + ); + await connector.sendFrame(frame); + await connector.sendFrame(buildGetAutoAddFlagsFrame()); + Navigator.pop(context); + }, + child: Text(l10n.common_save), + ), + ], + ), + ), + ); + } } class _RadioSettingsDialog extends StatefulWidget { diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index bc46b59..d923d6b 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -215,8 +215,8 @@ class BleDebugLogService extends ChangeNotifier { return 'RESP_CODE_CHANNEL_MSG_RECV_V3'; case respCodeChannelInfo: return 'RESP_CODE_CHANNEL_INFO'; - case respCodeRadioSettings: - return 'RESP_CODE_RADIO_SETTINGS'; + case respCodeAutoAddConfig: + return 'RESP_CODE_AUTO_ADD_CONFIG'; case pushCodeTraceData: return 'PUSH_CODE_TRACE_DATA'; default: diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart new file mode 100644 index 0000000..84f7807 --- /dev/null +++ b/lib/storage/contact_discovery_store.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import '../models/discovery_contact.dart'; +import 'prefs_manager.dart'; + +class ContactDiscoveryStore { + static const String _key = 'discovered_contacts'; + + Future> loadContacts() async { + final prefs = PrefsManager.instance; + final jsonStr = prefs.getString(_key); + if (jsonStr == null) return []; + + try { + final jsonList = jsonDecode(jsonStr) as List; + return jsonList + .map((entry) => _fromJson(entry as Map)) + .toList(); + } catch (_) { + return []; + } + } + + Future saveContacts(List contacts) async { + final prefs = PrefsManager.instance; + final jsonList = contacts.map(_toJson).toList(); + await prefs.setString(_key, jsonEncode(jsonList)); + } + + Map _toJson(DiscoveryContact contact) { + return { + 'publicKey': base64Encode(contact.publicKey), + 'name': contact.name, + 'type': contact.type, + 'pathLength': contact.pathLength, + 'path': base64Encode(contact.path), + 'latitude': contact.latitude, + 'longitude': contact.longitude, + 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + }; + } + + DiscoveryContact _fromJson(Map json) { + final lastSeenMs = json['lastSeen'] as int? ?? 0; + return DiscoveryContact( + publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), + name: json['name'] as String? ?? 'Unknown', + type: json['type'] as int? ?? 0, + pathLength: json['pathLength'] as int? ?? -1, + path: json['path'] != null + ? Uint8List.fromList(base64Decode(json['path'] as String)) + : Uint8List(0), + latitude: (json['latitude'] as num?)?.toDouble(), + longitude: (json['longitude'] as num?)?.toDouble(), + lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + ); + } +} diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index e1cda77..7324481 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -9,7 +9,16 @@ class AppBarTitle extends StatelessWidget { final String title; final Widget? leading; final Widget? trailing; - const AppBarTitle(this.title, {this.leading, this.trailing, super.key}); + final bool indicators; + final bool subtitle; + const AppBarTitle( + this.title, { + this.leading, + this.trailing, + this.indicators = true, + this.subtitle = true, + super.key, + }); @override Widget build(BuildContext context) { @@ -23,10 +32,10 @@ class AppBarTitle extends StatelessWidget { : MediaQuery.sizeOf(context).width; final compact = availableWidth < 240; final showSubtitle = - !compact && connector.isConnected && selfName != null; + !compact && connector.isConnected && selfName != null && subtitle; final showBattery = availableWidth >= 60; final showSnr = availableWidth >= 110; - final showIndicators = showBattery || showSnr; + final showIndicators = (showBattery || showSnr) && indicators; return Row( mainAxisAlignment: MainAxisAlignment.start, diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 473a3df..ee6fcd4 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -224,3 +224,93 @@ class ContactsFilterMenu extends StatelessWidget { ); } } + +class DiscoveryContactsFilterMenu extends StatelessWidget { + final ContactSortOption sortOption; + final ContactTypeFilter typeFilter; + final ValueChanged onSortChanged; + final ValueChanged onTypeFilterChanged; + + const DiscoveryContactsFilterMenu({ + super.key, + required this.sortOption, + required this.typeFilter, + required this.onSortChanged, + required this.onTypeFilterChanged, + }); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + return SortFilterMenu( + tooltip: l10n.listFilter_tooltip, + sections: [ + SortFilterMenuSection( + title: l10n.listFilter_sortBy, + options: [ + SortFilterMenuOption( + value: _actionSortLastSeen, + label: l10n.listFilter_heardRecently, + checked: sortOption == ContactSortOption.lastSeen, + ), + SortFilterMenuOption( + value: _actionSortName, + label: l10n.listFilter_az, + checked: sortOption == ContactSortOption.name, + ), + ], + ), + SortFilterMenuSection( + title: l10n.listFilter_filters, + options: [ + SortFilterMenuOption( + value: _actionFilterAll, + label: l10n.listFilter_all, + checked: typeFilter == ContactTypeFilter.all, + ), + SortFilterMenuOption( + value: _actionFilterUsers, + label: l10n.listFilter_users, + checked: typeFilter == ContactTypeFilter.users, + ), + SortFilterMenuOption( + value: _actionFilterRepeaters, + label: l10n.listFilter_repeaters, + checked: typeFilter == ContactTypeFilter.repeaters, + ), + SortFilterMenuOption( + value: _actionFilterRooms, + label: l10n.listFilter_roomServers, + checked: typeFilter == ContactTypeFilter.rooms, + ), + ], + ), + ], + onSelected: (action) { + switch (action) { + case _actionSortName: + onSortChanged(ContactSortOption.name); + break; + case _actionSortLastSeen: + onSortChanged(ContactSortOption.lastSeen); + break; + case _actionFilterAll: + onTypeFilterChanged(ContactTypeFilter.all); + break; + case _actionFilterUsers: + onTypeFilterChanged(ContactTypeFilter.users); + break; + case _actionFilterFavorites: + onTypeFilterChanged(ContactTypeFilter.favorites); + break; + case _actionFilterRepeaters: + onTypeFilterChanged(ContactTypeFilter.repeaters); + break; + case _actionFilterRooms: + onTypeFilterChanged(ContactTypeFilter.rooms); + break; + } + }, + ); + } +} From 92d8e7cd0b282f4627c8d7dc8d3974004937cb7f Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 28 Feb 2026 19:14:22 -0800 Subject: [PATCH 208/421] Refactor contact search functionality to use DiscoveryContact model and simplify query matching --- lib/screens/discovery_screen.dart | 29 ++--------------------------- lib/utils/contact_search.dart | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index b8f49fa..10dc016 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:meshcore_open/models/contact.dart'; @@ -212,17 +213,7 @@ class _DiscoveryScreenState extends State { ) { var filtered = contacts.where((contact) { if (searchQuery.isEmpty) return true; - return matchesContactQuery( - Contact( - publicKey: contact.publicKey, - name: contact.name, - type: contact.type, - pathLength: contact.pathLength, - path: contact.path, - lastSeen: contact.lastSeen, - ), - searchQuery, - ); + return matchesDiscoveryContactQuery(contact, searchQuery); }).toList(); // Filter out own node from the list @@ -237,22 +228,6 @@ class _DiscoveryScreenState extends State { filtered = filtered.where(_matchesTypeFilter).toList(); } - if (showUnreadOnly) { - filtered = filtered.where((contact) { - return connector.getUnreadCountForContact( - Contact( - publicKey: contact.publicKey, - name: contact.name, - type: contact.type, - pathLength: contact.pathLength, - path: contact.path, - lastSeen: contact.lastSeen, - ), - ) > - 0; - }).toList(); - } - switch (sortOption) { case ContactSortOption.lastSeen: filtered.sort( diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 31def4e..6cbd0b2 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,3 +1,5 @@ +import 'package:meshcore_open/models/discovery_contact.dart'; + import '../models/contact.dart'; bool matchesContactQuery(Contact contact, String query) { @@ -14,6 +16,20 @@ bool matchesContactQuery(Contact contact, String query) { return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); } +bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) { + final normalizedQuery = query.trim().toLowerCase(); + if (normalizedQuery.isEmpty) return true; + + if (contact.name.toLowerCase().contains(normalizedQuery)) { + return true; + } + + final hexPrefix = _extractHexPrefix(normalizedQuery); + if (hexPrefix == null) return false; + + return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); +} + String? _extractHexPrefix(String query) { var cleaned = query; if (cleaned.startsWith('0x')) { From 12bf46bba15cf7b9ce06e6a65b4faa67b2d200c9 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 1 Mar 2026 10:13:17 -0800 Subject: [PATCH 209/421] feat(localization): update contact settings translations for multiple languages - Translated contact settings and related strings in Slovenian, Swedish, Ukrainian, Chinese, Dutch, Polish, Portuguese, Russian, and Slovak. - Added new strings for discovered contacts actions such as adding, copying, and deleting contacts. - Enhanced the DiscoveryContact model to include a rawPacket field for better data handling. - Updated the contacts screen to support new actions in the context menu for discovered contacts. - Improved the contact discovery store to handle the serialization of the new rawPacket field. --- lib/connector/meshcore_connector.dart | 97 ++++++++++++++++++++++-- lib/connector/meshcore_protocol.dart | 38 +++++----- lib/l10n/app_bg.arb | 24 +++++- lib/l10n/app_de.arb | 24 +++++- lib/l10n/app_en.arb | 6 +- lib/l10n/app_es.arb | 24 +++++- lib/l10n/app_fr.arb | 24 +++++- lib/l10n/app_it.arb | 24 +++++- lib/l10n/app_localizations.dart | 24 ++++++ lib/l10n/app_localizations_bg.dart | 52 ++++++++----- lib/l10n/app_localizations_de.dart | 54 ++++++++----- lib/l10n/app_localizations_en.dart | 12 +++ lib/l10n/app_localizations_es.dart | 55 +++++++++----- lib/l10n/app_localizations_fr.dart | 54 ++++++++----- lib/l10n/app_localizations_it.dart | 53 ++++++++----- lib/l10n/app_localizations_nl.dart | 52 ++++++++----- lib/l10n/app_localizations_pl.dart | 52 ++++++++----- lib/l10n/app_localizations_pt.dart | 54 ++++++++----- lib/l10n/app_localizations_ru.dart | 54 ++++++++----- lib/l10n/app_localizations_sk.dart | 52 ++++++++----- lib/l10n/app_localizations_sl.dart | 51 ++++++++----- lib/l10n/app_localizations_sv.dart | 52 ++++++++----- lib/l10n/app_localizations_uk.dart | 54 ++++++++----- lib/l10n/app_localizations_zh.dart | 54 +++++++------ lib/l10n/app_nl.arb | 24 +++++- lib/l10n/app_pl.arb | 24 +++++- lib/l10n/app_pt.arb | 24 +++++- lib/l10n/app_ru.arb | 24 +++++- lib/l10n/app_sk.arb | 24 +++++- lib/l10n/app_sl.arb | 24 +++++- lib/l10n/app_sv.arb | 24 +++++- lib/l10n/app_uk.arb | 24 +++++- lib/l10n/app_zh.arb | 24 +++++- lib/models/discovery_contact.dart | 40 +--------- lib/screens/contacts_screen.dart | 3 +- lib/screens/discovery_screen.dart | 71 ++++++++++++++++- lib/storage/contact_discovery_store.dart | 2 + 37 files changed, 1034 insertions(+), 338 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c142f84..c7ebdaf 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -283,6 +283,7 @@ class MeshCoreConnector extends ChangeNotifier { int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; + Set get knownContactKeys => Set.unmodifiable(_knownContactKeys); bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -1534,6 +1535,50 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } + Future removeDiscoveredContact(DiscoveryContact contact) async { + if (!isConnected) return; + _discoveredContacts.removeWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + unawaited(_persistDiscoveredContacts()); + notifyListeners(); + } + + Future importDiscoveredContact(DiscoveryContact contact) async { + if (!isConnected) return; + await sendFrame( + buildSetAutoAddConfigFrame( + autoAddChat: true, + autoAddRepeater: true, + autoAddRoomServer: true, + autoAddSensor: true, + overwriteOldest: _overwriteOldest, + ), + ); + await sendFrame(buildImportContactFrame(contact.rawPacket)); + await sendFrame( + buildSetAutoAddConfigFrame( + autoAddChat: _autoAddUsers, + autoAddRepeater: _autoAddRepeaters, + autoAddRoomServer: _autoAddRoomServers, + autoAddSensor: _autoAddSensors, + overwriteOldest: _overwriteOldest, + ), + ); + + _handleContactAdvert( + Contact( + publicKey: contact.publicKey, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + lastSeen: DateTime.now(), + ), + ); + notifyListeners(); + } + Future clearContactPath(Contact contact) async { if (!isConnected) return; @@ -3705,22 +3750,29 @@ class MeshCoreConnector extends ChangeNotifier { appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); return; } - + final rawPacket = frame.sublist(3); switch (payloadType) { case payloadTypeADVERT: - _handlePayloadAdvertReceived(payload, pathBytes, routeType, snr); + _handlePayloadAdvertReceived( + rawPacket, + payload, + pathBytes, + routeType, + snr, + ); break; default: } } void _handlePayloadAdvertReceived( - Uint8List frame, + Uint8List rawPacket, + Uint8List payload, Uint8List path, int routeType, double snr, ) { - final advert = BufferReader(frame); + final advert = BufferReader(payload); double latitude = 0.0; double longitude = 0.0; String name = ''; @@ -3785,7 +3837,7 @@ class MeshCoreConnector extends ChangeNotifier { (_autoAddSensors && type == advTypeSensor)) { _handleContactAdvert(newContact); } else { - _handleDiscovery(newContact); + _handleDiscovery(newContact, rawPacket); } _updateDirectRepeater(newContact, snr, path); return; @@ -3890,9 +3942,42 @@ class MeshCoreConnector extends ChangeNotifier { } } - void _handleDiscovery(Contact contact) { + void _handleDiscovery(Contact contact, Uint8List rawPacket) { debugPrint('Discovered new contact: ${contact.name}'); + + final existingIndex = _discoveredContacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + final existingContactsIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + + if (existingContactsIndex >= 0) { + if (existingIndex >= 0) { + removeDiscoveredContact(_discoveredContacts[existingIndex]); + unawaited(_persistDiscoveredContacts()); + } + return; + } + + // Update existing contact + if (existingIndex >= 0) { + _discoveredContacts[existingIndex] = _discoveredContacts[existingIndex] + .copyWith( + rawPacket: rawPacket, + name: contact.name, + type: contact.type, + pathLength: contact.pathLength, + path: contact.path, + latitude: contact.latitude, + longitude: contact.longitude, + lastSeen: contact.lastSeen, + ); + return; + } + final disContact = DiscoveryContact( + rawPacket: rawPacket, publicKey: contact.publicKey, name: contact.name, type: contact.type, diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 36c13e2..4f50ec3 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -114,25 +114,27 @@ class BufferWriter { } void writeHex(String hex) { - // Validate hex string length is even and not empty - if (hex.isEmpty || hex.length % 2 != 0) { - throw FormatException('Invalid hex string length: ${hex.length}'); - } - List result = []; - for (int i = 0; i < hex.length ~/ 2; i++) { - final hexByte = hex.substring(i * 2, i * 2 + 2); - final byte = int.tryParse(hexByte, radix: 16); - if (byte == null) { - throw FormatException( - 'Invalid hex characters at position $i: $hexByte', - ); - } - result.add(byte); - } - writeBytes(Uint8List.fromList(result)); + writeBytes(hex2Uint8List(hex)); } } +Uint8List hex2Uint8List(String hex) { + // Validate hex string length is even and not empty + if (hex.isEmpty || hex.length % 2 != 0) { + throw FormatException('Invalid hex string length: ${hex.length}'); + } + List result = []; + for (int i = 0; i < hex.length ~/ 2; i++) { + final hexByte = hex.substring(i * 2, i * 2 + 2); + final byte = int.tryParse(hexByte, radix: 16); + if (byte == null) { + throw FormatException('Invalid hex characters at position $i: $hexByte'); + } + result.add(byte); + } + return Uint8List.fromList(result); +} + // Command codes (to device) const int cmdAppStart = 1; const int cmdSendTxtMsg = 2; @@ -827,10 +829,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) { // Build a import contact frame // [cmd][contact_frame x98+] -Uint8List buildImportContactFrame(String contactFrame) { +Uint8List buildImportContactFrame(Uint8List contactFrame) { final writer = BufferWriter(); writer.writeByte(cmdImportContact); - writer.writeHex(contactFrame); + writer.writeBytes(contactFrame); return writer.toBytes(); } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 975e067..eed1897 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1799,5 +1799,27 @@ "contacts_unread": "Непрочетено", "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", "contacts_searchContactsNoNumber": "Търси контакти...", - "contacts_searchUsers": "Търсене на {number}{str} потребители..." + "contacts_searchUsers": "Търсене на {number}{str} потребители...", + "contactsSettings_title": "Настройки на контактите", + "contactsSettings_autoAddTitle": "Автоматично откриване", + "contactsSettings_autoAddUsersTitle": "Автоматично добавяне на потребители", + "contactsSettings_otherTitle": "Други настройки свързани с контакти", + "settings_contactSettingsSubtitle": "Настройки за добавяне на контакти.", + "settings_contactSettings": "Настройки за контакти", + "contactsSettings_autoAddSensorsTitle": "Автоматично добавяне на датчици", + "contactsSettings_autoAddRoomServersTitle": "Автоматично добавяне на сървъри на стаите", + "contactsSettings_autoAddRoomServersSubtitle": "Позволи на спътника да добавя автоматично откритите сървъри на стаите.", + "contactsSettings_autoAddRepeatersTitle": "Автоматично добавяне на повтарящи се елементи", + "contactsSettings_autoAddUsersSubtitle": "Позволи на спътника да добавя автоматично откритите потребители.", + "contactsSettings_autoAddRepeatersSubtitle": "Позволи на спътника да добавя автоматично откритите повтарящи се устройства.", + "contactsSettings_autoAddSensorsSubtitle": "Позволи на спътника да добавя автоматично откритите датчици.", + "contactsSettings_overwriteOldestTitle": "Премахни най-старото", + "discoveredContacts_Title": "Открити контакти", + "discoveredContacts_searchHint": "Търсене на открити контакти", + "discoveredContacts_noMatching": "Няма съвпадащи контакти", + "contactsSettings_overwriteOldestSubtitle": "Когато е активиран, компаньонът ще презапише най-стария контакт, който не е отбелязан като любим, когато списъкът с контакти е пълен.", + "discoveredContacts_contactAdded": "Контакт добавен", + "discoveredContacts_copyContact": "Копирай контакт в клипборда", + "discoveredContacts_deleteContact": "Изтрий контакт", + "discoveredContacts_addContact": "Добави контакт" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 74b6c05..4fe662f 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1827,5 +1827,27 @@ "contacts_searchRepeaters": "Suche {number}{str} Repeater...", "contacts_searchFavorites": "Suche {number}{str} Favoriten...", "contacts_searchUsers": "Suche {number}{str} Benutzer...", - "contacts_searchRoomServers": "Suche {number}{str} Raumserver..." + "contacts_searchRoomServers": "Suche {number}{str} Raumserver...", + "settings_contactSettings": "Kontakteinstellungen", + "contactsSettings_otherTitle": "Weitere Einstellungen zu Kontakten", + "contactsSettings_title": "Kontakteinstellungen", + "contactsSettings_autoAddTitle": "Automatische Erkennung", + "contactsSettings_autoAddUsersTitle": "Automatische Hinzufügung von Benutzern", + "settings_contactSettingsSubtitle": "Einstellungen für das Hinzufügen von Kontakten", + "contactsSettings_autoAddSensorsTitle": "Automatisch Sensoren hinzufügen", + "contactsSettings_autoAddUsersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen", + "contactsSettings_autoAddRoomServersTitle": "Automatisch Raumservers hinzufügen", + "contactsSettings_autoAddRoomServersSubtitle": "Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen", + "contactsSettings_autoAddRepeatersTitle": "Automatisch Repeater hinzufügen", + "contactsSettings_autoAddRepeatersSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.", + "discoveredContacts_noMatching": "Keine passenden Kontakte", + "discoveredContacts_searchHint": "Entdeckte Kontakte suchen", + "discoveredContacts_addContact": "Kontakt hinzufügen", + "discoveredContacts_contactAdded": "Kontakt hinzugefügt", + "discoveredContacts_deleteContact": "Kontakt löschen", + "discoveredContacts_Title": "Entdeckte Kontakte", + "discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren", + "contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten", + "contactsSettings_overwriteOldestSubtitle": "Wenn aktiviert, überschreibt der Begleiter den ältesten nicht favorisierten Kontakt, wenn die Kontaktliste voll ist.", + "contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3f92d0e..8d5e528 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1855,5 +1855,9 @@ "contactsSettings_overwriteOldestSubtitle": "When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.", "discoveredContacts_Title": "Discovered Contacts", "discoveredContacts_noMatching": "No matching contacts", - "discoveredContacts_searchHint": "Search discovered contacts" + "discoveredContacts_searchHint": "Search discovered contacts", + "discoveredContacts_contactAdded": "Contact added", + "discoveredContacts_addContact": "Add Contact", + "discoveredContacts_copyContact": "Copy Contact to clipboard", + "discoveredContacts_deleteContact": "Delete Contact" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 74339ff..5a2e1c8 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1827,5 +1827,27 @@ "contacts_searchFavorites": "Buscar {number}{str} Favoritos...", "contacts_searchUsers": "Buscar {number}{str} Usuarios...", "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", - "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..." + "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...", + "contactsSettings_autoAddTitle": "Detección automática", + "settings_contactSettings": "Configuración de contacto", + "contactsSettings_autoAddUsersTitle": "Agregar usuarios automáticamente", + "contactsSettings_otherTitle": "Otras configuraciones relacionadas con el contacto", + "contactsSettings_autoAddUsersSubtitle": "Permitir que el compañero agregue automáticamente a los usuarios descubiertos.", + "contactsSettings_autoAddRepeatersSubtitle": "Permitir que el compañero agregue automáticamente los repetidores descubiertos.", + "contactsSettings_autoAddRoomServersSubtitle": "Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.", + "contactsSettings_autoAddSensorsTitle": "Agregar sensores automáticamente", + "contactsSettings_title": "Configuración de contactos", + "settings_contactSettingsSubtitle": "Configuración de cómo se agregan los contactos.", + "contactsSettings_autoAddSensorsSubtitle": "Permitir que el compañero agregue automáticamente los sensores descubiertos.", + "contactsSettings_autoAddRepeatersTitle": "Agregar repetidores automáticamente", + "contactsSettings_overwriteOldestTitle": "Sobreescribir el más antiguo", + "contactsSettings_autoAddRoomServersTitle": "Agregar automáticamente servidores de sala", + "discoveredContacts_noMatching": "No se encontraron contactos coincidentes", + "discoveredContacts_contactAdded": "Contacto agregado", + "discoveredContacts_copyContact": "Copiar contacto al portapapeles", + "discoveredContacts_deleteContact": "Eliminar contacto", + "discoveredContacts_Title": "Contactos descubiertos", + "contactsSettings_overwriteOldestSubtitle": "Cuando se habilita, el compañero sobrescribirá el contacto más antiguo no favorito cuando la lista de contactos esté llena.", + "discoveredContacts_searchHint": "Buscar contactos descubiertos", + "discoveredContacts_addContact": "Agregar contacto" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 0697aee..e222f53 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1799,5 +1799,27 @@ "contacts_searchUsers": "Rechercher {number}{str} utilisateurs...", "contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...", "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", - "contacts_searchContactsNoNumber": "Rechercher des contacts..." + "contacts_searchContactsNoNumber": "Rechercher des contacts...", + "settings_contactSettings": "Paramètres de contact", + "settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts", + "contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs", + "contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts", + "contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle", + "contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts", + "contactsSettings_otherTitle": "Autres paramètres liés aux contacts", + "contactsSettings_title": "Paramètres des contacts", + "contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs", + "contactsSettings_autoAddTitle": "Découverte automatique", + "contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs", + "contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts", + "discoveredContacts_noMatching": "Aucun contact correspondant", + "discoveredContacts_contactAdded": "Contact ajouté", + "discoveredContacts_addContact": "Ajouter un contact", + "discoveredContacts_copyContact": "Copier le contact dans le presse-papiers", + "discoveredContacts_deleteContact": "Supprimer le contact", + "contactsSettings_overwriteOldestTitle": "Écraser le plus ancien", + "contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.", + "contactsSettings_overwriteOldestSubtitle": "Lorsqu'il est activé, le compagnon écrasera l'ancien contact non favori lorsque la liste de contacts est pleine.", + "discoveredContacts_Title": "Contacts découverts", + "discoveredContacts_searchHint": "Rechercher des contacts découverts" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4798d26..53c3737 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1799,5 +1799,27 @@ "contacts_searchFavorites": "Cerca {number}{str} Preferiti...", "contacts_unread": "Non letti", "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", - "contacts_searchRoomServers": "Cerca {number}{str} server Room..." + "contacts_searchRoomServers": "Cerca {number}{str} server Room...", + "contactsSettings_title": "Impostazioni dei contatti", + "settings_contactSettings": "Impostazioni di contatto", + "contactsSettings_otherTitle": "Altre impostazioni relative ai contatti", + "contactsSettings_autoAddUsersSubtitle": "Consenti al compagno di aggiungere automaticamente gli utenti scoperti.", + "contactsSettings_autoAddRepeatersTitle": "Aggiungere ripetitori automaticamente", + "contactsSettings_autoAddRoomServersSubtitle": "Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.", + "contactsSettings_autoAddSensorsTitle": "Aggiungere automaticamente i sensori", + "settings_contactSettingsSubtitle": "Impostazioni per l'aggiunta dei contatti", + "contactsSettings_autoAddUsersTitle": "Aggiungere utenti automaticamente", + "contactsSettings_autoAddTitle": "Scoperta automatica", + "contactsSettings_autoAddSensorsSubtitle": "Consenti al compagno di aggiungere automaticamente i sensori scoperti", + "discoveredContacts_noMatching": "Nessun contatto corrispondente", + "contactsSettings_autoAddRepeatersSubtitle": "Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.", + "discoveredContacts_searchHint": "Cerca contatti scoperti", + "contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze", + "discoveredContacts_addContact": "Aggiungi contatto", + "contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio", + "contactsSettings_overwriteOldestSubtitle": "Quando abilitato, il companion sovrascriverà il contatto più vecchio non preferito quando l'elenco dei contatti è pieno.", + "discoveredContacts_Title": "Contatti scoperti", + "discoveredContacts_contactAdded": "Contatto aggiunto", + "discoveredContacts_deleteContact": "Elimina Contatto", + "discoveredContacts_copyContact": "Copia contatto negli appunti" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 333147f..a7c458f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5488,6 +5488,30 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Search discovered contacts'** String get discoveredContacts_searchHint; + + /// No description provided for @discoveredContacts_contactAdded. + /// + /// In en, this message translates to: + /// **'Contact added'** + String get discoveredContacts_contactAdded; + + /// No description provided for @discoveredContacts_addContact. + /// + /// In en, this message translates to: + /// **'Add Contact'** + String get discoveredContacts_addContact; + + /// No description provided for @discoveredContacts_copyContact. + /// + /// In en, this message translates to: + /// **'Copy Contact to clipboard'** + String get discoveredContacts_copyContact; + + /// No description provided for @discoveredContacts_deleteContact. + /// + /// In en, this message translates to: + /// **'Delete Contact'** + String get discoveredContacts_deleteContact; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index c26b5b2..f9a858f 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -235,11 +235,11 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_longitude => 'Дължина'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Настройки за контакти'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Настройки за добавяне на контакти.'; @override String get settings_privacyMode => 'Режим на поверителност'; @@ -3121,56 +3121,72 @@ class AppLocalizationsBg extends AppLocalizations { String get snrIndicator_lastSeen => 'Последно видян'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Настройки на контактите'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Автоматично откриване'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Други настройки свързани с контакти'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Автоматично добавяне на потребители'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Позволи на спътника да добавя автоматично откритите потребители.'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Автоматично добавяне на повтарящи се елементи'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Позволи на спътника да добавя автоматично откритите повтарящи се устройства.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Автоматично добавяне на сървъри на стаите'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Позволи на спътника да добавя автоматично откритите сървъри на стаите.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Автоматично добавяне на датчици'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Позволи на спътника да добавя автоматично откритите датчици.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Премахни най-старото'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Когато е активиран, компаньонът ще презапише най-стария контакт, който не е отбелязан като любим, когато списъкът с контакти е пълен.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Открити контакти'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Няма съвпадащи контакти'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Търсене на открити контакти'; + + @override + String get discoveredContacts_contactAdded => 'Контакт добавен'; + + @override + String get discoveredContacts_addContact => 'Добави контакт'; + + @override + String get discoveredContacts_copyContact => 'Копирай контакт в клипборда'; + + @override + String get discoveredContacts_deleteContact => 'Изтрий контакт'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 6d36471..77b2a4b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -234,11 +234,11 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_longitude => 'Längengrad'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Kontakteinstellungen'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Einstellungen für das Hinzufügen von Kontakten'; @override String get settings_privacyMode => 'Privatsphäreeinstellung'; @@ -3130,56 +3130,74 @@ class AppLocalizationsDe extends AppLocalizations { String get snrIndicator_lastSeen => 'Zuletzt gesehen'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Kontakteinstellungen'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Automatische Erkennung'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Weitere Einstellungen zu Kontakten'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Automatische Hinzufügung von Benutzern'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Ermöglichen Sie dem Begleiter, automatisch entdeckte Benutzer hinzuzufügen'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Automatisch Repeater hinzufügen'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Ermöglichen Sie dem Begleiter, automatisch entdeckte Repeater hinzuzufügen.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Automatisch Raumservers hinzufügen'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Ermöglichen Sie dem Begleiter, entdeckte Raumserver automatisch hinzuzufügen'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Automatisch Sensoren hinzufügen'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => + 'Überschreiben des Ältesten'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Wenn aktiviert, überschreibt der Begleiter den ältesten nicht favorisierten Kontakt, wenn die Kontaktliste voll ist.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Entdeckte Kontakte'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Keine passenden Kontakte'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Entdeckte Kontakte suchen'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt hinzugefügt'; + + @override + String get discoveredContacts_addContact => 'Kontakt hinzufügen'; + + @override + String get discoveredContacts_copyContact => + 'Kontakt in die Zwischenablage kopieren'; + + @override + String get discoveredContacts_deleteContact => 'Kontakt löschen'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c40b7df..08132ac 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3126,4 +3126,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get discoveredContacts_searchHint => 'Search discovered contacts'; + + @override + String get discoveredContacts_contactAdded => 'Contact added'; + + @override + String get discoveredContacts_addContact => 'Add Contact'; + + @override + String get discoveredContacts_copyContact => 'Copy Contact to clipboard'; + + @override + String get discoveredContacts_deleteContact => 'Delete Contact'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 203916b..0d2167f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -234,11 +234,11 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_longitude => 'Longitud'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Configuración de contacto'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Configuración de cómo se agregan los contactos.'; @override String get settings_privacyMode => 'Modo Privacidad'; @@ -3122,56 +3122,75 @@ class AppLocalizationsEs extends AppLocalizations { String get snrIndicator_lastSeen => 'Visto por última vez'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Configuración de contactos'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Detección automática'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Otras configuraciones relacionadas con el contacto'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Agregar usuarios automáticamente'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Agregar repetidores automáticamente'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Permitir que el compañero agregue automáticamente los repetidores descubiertos.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Agregar automáticamente servidores de sala'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Agregar sensores automáticamente'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Permitir que el compañero agregue automáticamente los sensores descubiertos.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => + 'Sobreescribir el más antiguo'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Cuando se habilita, el compañero sobrescribirá el contacto más antiguo no favorito cuando la lista de contactos esté llena.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Contactos descubiertos'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => + 'No se encontraron contactos coincidentes'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Buscar contactos descubiertos'; + + @override + String get discoveredContacts_contactAdded => 'Contacto agregado'; + + @override + String get discoveredContacts_addContact => 'Agregar contacto'; + + @override + String get discoveredContacts_copyContact => + 'Copiar contacto al portapapeles'; + + @override + String get discoveredContacts_deleteContact => 'Eliminar contacto'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7ae1b25..29f3af8 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -235,11 +235,11 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_longitude => 'Longitude'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Paramètres de contact'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Paramètres pour l\'ajout de contacts'; @override String get settings_privacyMode => 'Mode de confidentialité'; @@ -3144,56 +3144,74 @@ class AppLocalizationsFr extends AppLocalizations { String get snrIndicator_lastSeen => 'Dernière fois vu'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Paramètres des contacts'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Découverte automatique'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Autres paramètres liés aux contacts'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Ajouter automatiquement les utilisateurs'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Ajouter automatiquement les répéteurs'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Autoriser le compagnon à ajouter automatiquement les répéteurs découverts'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Ajouter automatiquement les serveurs de salle'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Ajouter automatiquement les capteurs'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Autoriser le compagnon à ajouter automatiquement les capteurs découverts.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Écraser le plus ancien'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Lorsqu\'il est activé, le compagnon écrasera l\'ancien contact non favori lorsque la liste de contacts est pleine.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Contacts découverts'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Aucun contact correspondant'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => + 'Rechercher des contacts découverts'; + + @override + String get discoveredContacts_contactAdded => 'Contact ajouté'; + + @override + String get discoveredContacts_addContact => 'Ajouter un contact'; + + @override + String get discoveredContacts_copyContact => + 'Copier le contact dans le presse-papiers'; + + @override + String get discoveredContacts_deleteContact => 'Supprimer le contact'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index be88ecb..1fb4eb1 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -234,11 +234,11 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_longitude => 'Longitudine'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Impostazioni di contatto'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Impostazioni per l\'aggiunta dei contatti'; @override String get settings_privacyMode => 'Modalità Privacy'; @@ -3125,56 +3125,73 @@ class AppLocalizationsIt extends AppLocalizations { String get snrIndicator_lastSeen => 'Ultimo accesso'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Impostazioni dei contatti'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Scoperta automatica'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Altre impostazioni relative ai contatti'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Aggiungere utenti automaticamente'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Aggiungere ripetitori automaticamente'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Aggiungere automaticamente i server delle stanze'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Aggiungere automaticamente i sensori'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Consenti al compagno di aggiungere automaticamente i sensori scoperti'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => + 'Sostituisci il più vecchio'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Quando abilitato, il companion sovrascriverà il contatto più vecchio non preferito quando l\'elenco dei contatti è pieno.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Contatti scoperti'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Nessun contatto corrispondente'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Cerca contatti scoperti'; + + @override + String get discoveredContacts_contactAdded => 'Contatto aggiunto'; + + @override + String get discoveredContacts_addContact => 'Aggiungi contatto'; + + @override + String get discoveredContacts_copyContact => 'Copia contatto negli appunti'; + + @override + String get discoveredContacts_deleteContact => 'Elimina Contatto'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index ad1f624..6ed3a84 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -234,11 +234,11 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_longitude => 'Lengtegraad'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Contactinstellingen'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Instellingen voor het toevoegen van contacten'; @override String get settings_privacyMode => 'Privacy Mode'; @@ -3112,56 +3112,72 @@ class AppLocalizationsNl extends AppLocalizations { String get snrIndicator_lastSeen => 'Laatst gezien'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Instellingen voor contacten'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Automatische detectie'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Andere instellingen voor contactgerelateerde zaken'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Gebruikers automatisch toevoegen'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Automatisch herhalingstoestellen toevoegen'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Sta toe dat de companion automatisch ontdekte repeaters toevoegt'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Automatisch kamerservers toevoegen'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Automatisch sensoren toevoegen'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Sta toe dat de companion automatisch ontdekte sensoren toevoegt'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Wanneer ingeschakeld, overschrijft de companion het oudste contact dat niet is gemarkeerd als favoriet wanneer de contactenlijst vol is.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Ontdekte contacten'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Geen overeenkomende contacten'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken'; + + @override + String get discoveredContacts_contactAdded => 'Contact toegevoegd'; + + @override + String get discoveredContacts_addContact => 'Contact toevoegen'; + + @override + String get discoveredContacts_copyContact => 'Kopieer contact naar klembord'; + + @override + String get discoveredContacts_deleteContact => 'Contact verwijderen'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index a97599c..703e281 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -236,11 +236,11 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_longitude => 'Długość'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Ustawienia kontaktowe'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Ustawienia dotyczące sposobu dodawania kontaktów'; @override String get settings_privacyMode => 'Tryb Prywatny'; @@ -3125,56 +3125,72 @@ class AppLocalizationsPl extends AppLocalizations { String get snrIndicator_lastSeen => 'Ostatnio widziany'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Ustawienia kontaktów'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Automatyczne odnajdywanie'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Inne ustawienia związane z kontaktami'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Automatycznie dodaj użytkowników'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Automatyczne dodawanie powtarzalników'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Automatycznie dodaj serwery pokojowe'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Automatycznie dodaj czujniki'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Nadpisz najstarszy'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Gdy jest włączone, companion zastępuje najstarszy kontakt nie ulubiony, gdy lista kontaktów jest pełna.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Odkryte Kontakty'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Brak pasujących kontaktów'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Wyszukaj odkryte kontakty'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt dodany'; + + @override + String get discoveredContacts_addContact => 'Dodaj kontakt'; + + @override + String get discoveredContacts_copyContact => 'Kopiuj kontakt do schowka'; + + @override + String get discoveredContacts_deleteContact => 'Usuń kontakt'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index e1f89e2..442b41e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -235,11 +235,11 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_longitude => 'Longitude'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Configurações de Contato'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Configurações para como os contatos são adicionados'; @override String get settings_privacyMode => 'Modo de Privacidade'; @@ -3120,56 +3120,74 @@ class AppLocalizationsPt extends AppLocalizations { String get snrIndicator_lastSeen => 'Visto pela última vez'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Configurações de contatos'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Descoberta Automática'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Outras configurações relacionadas a contatos'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Adicionar usuários automaticamente'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Permitir que o companheiro adicione automaticamente os usuários descobertos.'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Adicionar repetidores automaticamente'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Permitir que o companheiro adicione automaticamente os repetidores descobertos.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Adicionar automaticamente servidores de sala'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Adicionar sensores automaticamente'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Permitir que o companheiro adicione automaticamente sensores descobertos.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => + 'Sobrescrever o Mais Antigo'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Quando ativado, o acompanhante substituirá o contato mais antigo não favoritado quando a lista de contatos estiver cheia.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Contatos Descobertos'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Nenhum contato correspondente'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Pesquisar contatos descobertos'; + + @override + String get discoveredContacts_contactAdded => 'Contato adicionado'; + + @override + String get discoveredContacts_addContact => 'Adicionar Contato'; + + @override + String get discoveredContacts_copyContact => + 'Copiar Contato para a área de transferência'; + + @override + String get discoveredContacts_deleteContact => 'Excluir Contato'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index b2f3718..90f8ed6 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -233,11 +233,11 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_longitude => 'Долгота'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Настройки контактов'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Настройки добавления контактов'; @override String get settings_privacyMode => 'Режим конфиденциальности'; @@ -3132,56 +3132,74 @@ class AppLocalizationsRu extends AppLocalizations { String get snrIndicator_lastSeen => 'Последний раз видели'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Настройки контактов'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Автоматическое обнаружение'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Другие настройки, связанные с контактами'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Автоматически добавлять пользователей'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Разрешить компаньону автоматически добавлять обнаруженных пользователей'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Автоматически добавлять ретрансляторы'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Автоматически добавлять серверы комнат'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Автоматически добавлять датчики'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Разрешить компаньону автоматически добавлять обнаруженные датчики'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => + 'Перезаписать самое старое'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'При включении компаньон будет перезаписывать самый старый контакт, не отмеченный как любимый, когда список контактов полон.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Обнаруженные контакты'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Нет совпадающих контактов'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Найденные контакты поиска'; + + @override + String get discoveredContacts_contactAdded => 'Контакт добавлен'; + + @override + String get discoveredContacts_addContact => 'Добавить контакт'; + + @override + String get discoveredContacts_copyContact => + 'Копировать контакт в буфер обмена'; + + @override + String get discoveredContacts_deleteContact => 'Удалить контакт'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 773b338..944bc6c 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -234,11 +234,11 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_longitude => 'Dĺžka'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Nastavenia kontaktov'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Nastavenia pre pridávanie kontaktov.'; @override String get settings_privacyMode => 'Režim ochrany súkromia'; @@ -3107,56 +3107,72 @@ class AppLocalizationsSk extends AppLocalizations { String get snrIndicator_lastSeen => 'Naposledy videný'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Nastavenia kontaktov'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Automatické zisťovanie'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Ďalšie nastavenia súvisiace s kontaktami'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Automaticky pridávať užívateľov'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Povoliť spoločníkovi automaticky pridávať objavených užívateľov.'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Automaticky pridávať opakovače'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Povoliť spoločníkovi automaticky pridávať objavené repeater.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Automaticky pridávať server miestnosti'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Automaticky pridávať senzory'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Povoliť spoločníkovi automaticky pridávať objavené senzory.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Prepísať najstaršie'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Keď je povolené, spoločník prepíše najstarší kontakt, ktorý nie je označený ako obľúbený, keď je zoznam kontaktov plný.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Objavené kontakty'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Žiadne zhodné kontakty'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Vyhľadať objavené kontakty'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt bol pridaný'; + + @override + String get discoveredContacts_addContact => 'Pridať kontakt'; + + @override + String get discoveredContacts_copyContact => 'Kopírovať kontakt do schránky'; + + @override + String get discoveredContacts_deleteContact => 'Zmazať kontakt'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 0f38b94..0fd67ce 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -234,11 +234,11 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_longitude => 'Dolžina'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Nastavitve stika'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Nastavitve za dodajanje stikov.'; @override String get settings_privacyMode => 'Zasebnost'; @@ -3112,56 +3112,71 @@ class AppLocalizationsSl extends AppLocalizations { String get snrIndicator_lastSeen => 'Zadnjič videno'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Nastavitve stikov'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Avtomatsko odkrivanje'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => 'Druge nastavitve v zvezi s stiki'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Avtomatsko dodaj uporabnike'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Avtomatsko dodaj ponovitelje'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Avtomatsko dodaj strežnike sob'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Avtomatsko dodaj senzorje'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Dovoli spremljevalcu, da samodejno doda odkrite senzorje.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Prepiši najstarejše'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Ko je omogočen, bo spremljevalec prepisal najstarejši stik, ki ni označen kot najljubši, ko je seznam stikov poln.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Odkriti stiki'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Ni ujemajočih stikov'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Najdeni stiki po iskanju'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt dodan'; + + @override + String get discoveredContacts_addContact => 'Dodaj stik'; + + @override + String get discoveredContacts_copyContact => 'Kopiraj stik v odložišče'; + + @override + String get discoveredContacts_deleteContact => 'Izbriši stik'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 4e67267..86ba9fd 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -233,11 +233,11 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_longitude => 'Längdgrad'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Kontaktinställningar'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Inställningar för hur kontakter läggs till.'; @override String get settings_privacyMode => 'Privatläge'; @@ -3090,56 +3090,72 @@ class AppLocalizationsSv extends AppLocalizations { String get snrIndicator_lastSeen => 'Senast sedd'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Kontaktinställningar'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Automatisk upptäckt'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Andra inställningar relaterade till kontakt'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Lägg till användare automatiskt'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Tillåt kompanjonen att automatiskt lägga till upptäckta användare'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Lägg till upprepande enheter automatiskt'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Lägg automatiskt till rumsservrar'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Lägg till sensorer automatiskt'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Skriv över äldst'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'När den är aktiverad kommer medhjälparen att skriva över den äldsta kontakten som inte är favoritmarkerad när kontaktnamnslistan är full'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Upptäckta kontakter'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => 'Inga matchande kontakter'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Sök uppfunna kontakter'; + + @override + String get discoveredContacts_contactAdded => 'Kontakt tillagd'; + + @override + String get discoveredContacts_addContact => 'Lägg till kontakt'; + + @override + String get discoveredContacts_copyContact => 'Kopiera kontakt till urklipp'; + + @override + String get discoveredContacts_deleteContact => 'Ta bort kontakt'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 40a52ca..abeb6c6 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -233,11 +233,11 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_longitude => 'Довгота'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => 'Налаштування контактів'; @override String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + 'Налаштування для додавання контактів'; @override String get settings_privacyMode => 'Режим приватності'; @@ -3139,56 +3139,74 @@ class AppLocalizationsUk extends AppLocalizations { String get snrIndicator_lastSeen => 'Останній раз бачили'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => 'Налаштування контактів'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => 'Автоматичне виявлення'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => + 'Інші налаштування, пов\'язані з контактами'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => + 'Автоматично додавати користувачів'; @override String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + 'Дозволити супутникові автоматично додавати виявлених користувачів'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => + 'Автоматично додавати повторювачі'; @override String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + 'Дозволити супутнику автоматично додавати виявлені ретранслятори'; @override String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + 'Автоматично додавати сервери кімнат'; @override String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + 'Дозволити супровіднику автоматично додавати виявлені сервери кімнат.'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => + 'Автоматично додавати датчики'; @override String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + 'Дозволити супровіднику автоматично додавати виявлені сенсори'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => 'Перезаписати найстаріше'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'Коли увімкнено, супровід переганяє найстарший контакт, який не доданий до улюблених, коли список контактів повний.'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => 'Виявлені контакти'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => + 'Відповідних контактів не знайдено'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => 'Знайти виявлені контакти'; + + @override + String get discoveredContacts_contactAdded => 'Контакт додано'; + + @override + String get discoveredContacts_addContact => 'Додати контакт'; + + @override + String get discoveredContacts_copyContact => + 'Копіювати контакт у буфер обміну'; + + @override + String get discoveredContacts_deleteContact => 'Видалити контакт'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4aec80b..72babcc 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -227,11 +227,10 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_longitude => '经度'; @override - String get settings_contactSettings => 'Contact Settings'; + String get settings_contactSettings => '联系人设置'; @override - String get settings_contactSettingsSubtitle => - 'Settings for how contacts are added.'; + String get settings_contactSettingsSubtitle => '添加联系人的设置'; @override String get settings_privacyMode => '隐私模式'; @@ -2899,56 +2898,63 @@ class AppLocalizationsZh extends AppLocalizations { String get snrIndicator_lastSeen => '最近访问'; @override - String get contactsSettings_title => 'Contacts settings'; + String get contactsSettings_title => '联系人设置'; @override - String get contactsSettings_autoAddTitle => 'Automatic Discovery'; + String get contactsSettings_autoAddTitle => '自动发现'; @override - String get contactsSettings_otherTitle => 'Other contact related settings'; + String get contactsSettings_otherTitle => '其他联系人相关设置'; @override - String get contactsSettings_autoAddUsersTitle => 'Auto-add users'; + String get contactsSettings_autoAddUsersTitle => '自动添加用户'; @override - String get contactsSettings_autoAddUsersSubtitle => - 'Allow the companion to automatically add discovered users.'; + String get contactsSettings_autoAddUsersSubtitle => '允许伴侣自动添加发现的用户'; @override - String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters'; + String get contactsSettings_autoAddRepeatersTitle => '自动添加重复器'; @override - String get contactsSettings_autoAddRepeatersSubtitle => - 'Allow the companion to automatically add discovered repeaters.'; + String get contactsSettings_autoAddRepeatersSubtitle => '允许伴侣自动添加发现的重复器'; @override - String get contactsSettings_autoAddRoomServersTitle => - 'Auto-add room servers'; + String get contactsSettings_autoAddRoomServersTitle => '自动添加房间服务器'; @override - String get contactsSettings_autoAddRoomServersSubtitle => - 'Allow the companion to automatically add discovered room servers.'; + String get contactsSettings_autoAddRoomServersSubtitle => '允许伴侣自动添加发现的房间服务器'; @override - String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors'; + String get contactsSettings_autoAddSensorsTitle => '自动添加传感器'; @override - String get contactsSettings_autoAddSensorsSubtitle => - 'Allow the companion to automatically add discovered sensors.'; + String get contactsSettings_autoAddSensorsSubtitle => '允许伴侣自动添加发现的传感器'; @override - String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest'; + String get contactsSettings_overwriteOldestTitle => '覆盖最旧的'; @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + '启用时,伴侣将在联系人列表满时覆盖最老的未收藏的联系人。'; @override - String get discoveredContacts_Title => 'Discovered Contacts'; + String get discoveredContacts_Title => '已发现的联系人'; @override - String get discoveredContacts_noMatching => 'No matching contacts'; + String get discoveredContacts_noMatching => '没有匹配的联系人'; @override - String get discoveredContacts_searchHint => 'Search discovered contacts'; + String get discoveredContacts_searchHint => '搜索已发现的联系人'; + + @override + String get discoveredContacts_contactAdded => '联系人已添加'; + + @override + String get discoveredContacts_addContact => '添加联系人'; + + @override + String get discoveredContacts_copyContact => '复制联系人到剪贴板'; + + @override + String get discoveredContacts_deleteContact => '删除联系人'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 69ebecc..7d57cb8 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1799,5 +1799,27 @@ "contacts_searchContactsNoNumber": "Zoek contacten...", "contacts_searchUsers": "Zoek {number}{str} gebruikers...", "contacts_searchFavorites": "Zoek {number}{str} favorieten...", - "contacts_searchRoomServers": "Zoek {number}{str} Room servers..." + "contacts_searchRoomServers": "Zoek {number}{str} Room servers...", + "contactsSettings_autoAddUsersTitle": "Gebruikers automatisch toevoegen", + "contactsSettings_title": "Instellingen voor contacten", + "settings_contactSettings": "Contactinstellingen", + "contactsSettings_otherTitle": "Andere instellingen voor contactgerelateerde zaken", + "contactsSettings_autoAddRepeatersSubtitle": "Sta toe dat de companion automatisch ontdekte repeaters toevoegt", + "contactsSettings_autoAddRoomServersTitle": "Automatisch kamerservers toevoegen", + "contactsSettings_autoAddRoomServersSubtitle": "Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.", + "contactsSettings_autoAddSensorsTitle": "Automatisch sensoren toevoegen", + "settings_contactSettingsSubtitle": "Instellingen voor het toevoegen van contacten", + "contactsSettings_autoAddTitle": "Automatische detectie", + "contactsSettings_autoAddSensorsSubtitle": "Sta toe dat de companion automatisch ontdekte sensoren toevoegt", + "contactsSettings_autoAddUsersSubtitle": "Sta toe dat de companion automatisch ontdekte gebruikers toevoegt", + "contactsSettings_autoAddRepeatersTitle": "Automatisch herhalingstoestellen toevoegen", + "contactsSettings_overwriteOldestTitle": "Overschrijf Oudste", + "discoveredContacts_noMatching": "Geen overeenkomende contacten", + "discoveredContacts_addContact": "Contact toevoegen", + "discoveredContacts_copyContact": "Kopieer contact naar klembord", + "discoveredContacts_deleteContact": "Contact verwijderen", + "discoveredContacts_Title": "Ontdekte contacten", + "contactsSettings_overwriteOldestSubtitle": "Wanneer ingeschakeld, overschrijft de companion het oudste contact dat niet is gemarkeerd als favoriet wanneer de contactenlijst vol is.", + "discoveredContacts_contactAdded": "Contact toegevoegd", + "discoveredContacts_searchHint": "Ontdekte contacten zoeken" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 75e1d34..d20ffee 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1799,5 +1799,27 @@ "contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...", "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", - "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..." + "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...", + "contactsSettings_title": "Ustawienia kontaktów", + "settings_contactSettingsSubtitle": "Ustawienia dotyczące sposobu dodawania kontaktów", + "contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.", + "contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie powtarzalników", + "contactsSettings_autoAddRepeatersSubtitle": "Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.", + "contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe", + "contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników", + "settings_contactSettings": "Ustawienia kontaktowe", + "contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami", + "contactsSettings_autoAddTitle": "Automatyczne odnajdywanie", + "contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.", + "contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki", + "discoveredContacts_searchHint": "Wyszukaj odkryte kontakty", + "discoveredContacts_contactAdded": "Kontakt dodany", + "discoveredContacts_addContact": "Dodaj kontakt", + "discoveredContacts_copyContact": "Kopiuj kontakt do schowka", + "contactsSettings_overwriteOldestTitle": "Nadpisz najstarszy", + "discoveredContacts_Title": "Odkryte Kontakty", + "contactsSettings_overwriteOldestSubtitle": "Gdy jest włączone, companion zastępuje najstarszy kontakt nie ulubiony, gdy lista kontaktów jest pełna.", + "contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.", + "discoveredContacts_noMatching": "Brak pasujących kontaktów", + "discoveredContacts_deleteContact": "Usuń kontakt" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index f6ada19..421ddd3 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1799,5 +1799,27 @@ "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", "contacts_searchContactsNoNumber": "Pesquisar Contatos...", "contacts_unread": "Não lido", - "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..." + "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...", + "settings_contactSettings": "Configurações de Contato", + "contactsSettings_otherTitle": "Outras configurações relacionadas a contatos", + "contactsSettings_title": "Configurações de contatos", + "contactsSettings_autoAddTitle": "Descoberta Automática", + "settings_contactSettingsSubtitle": "Configurações para como os contatos são adicionados", + "contactsSettings_autoAddUsersTitle": "Adicionar usuários automaticamente", + "contactsSettings_autoAddRepeatersSubtitle": "Permitir que o companheiro adicione automaticamente os repetidores descobertos.", + "contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala", + "contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo", + "contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente", + "contactsSettings_overwriteOldestSubtitle": "Quando ativado, o acompanhante substituirá o contato mais antigo não favoritado quando a lista de contatos estiver cheia.", + "discoveredContacts_Title": "Contatos Descobertos", + "contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.", + "contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente", + "discoveredContacts_noMatching": "Nenhum contato correspondente", + "contactsSettings_autoAddRoomServersSubtitle": "Permitir que o companheiro adicione automaticamente os servidores de salas descobertos.", + "discoveredContacts_searchHint": "Pesquisar contatos descobertos", + "contactsSettings_autoAddSensorsSubtitle": "Permitir que o companheiro adicione automaticamente sensores descobertos.", + "discoveredContacts_copyContact": "Copiar Contato para a área de transferência", + "discoveredContacts_deleteContact": "Excluir Contato", + "discoveredContacts_contactAdded": "Contato adicionado", + "discoveredContacts_addContact": "Adicionar Contato" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 9aef298..b5910b7 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1039,5 +1039,27 @@ "contacts_unread": "Непрочитанное", "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", "contacts_searchFavorites": "Поиск {number}{str} избранного...", - "contacts_searchUsers": "Поиск {number}{str} пользователей..." + "contacts_searchUsers": "Поиск {number}{str} пользователей...", + "settings_contactSettings": "Настройки контактов", + "settings_contactSettingsSubtitle": "Настройки добавления контактов", + "contactsSettings_autoAddTitle": "Автоматическое обнаружение", + "contactsSettings_title": "Настройки контактов", + "contactsSettings_otherTitle": "Другие настройки, связанные с контактами", + "contactsSettings_autoAddUsersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженных пользователей", + "contactsSettings_autoAddRoomServersTitle": "Автоматически добавлять серверы комнат", + "contactsSettings_autoAddSensorsTitle": "Автоматически добавлять датчики", + "contactsSettings_autoAddSensorsSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные датчики", + "contactsSettings_autoAddUsersTitle": "Автоматически добавлять пользователей", + "contactsSettings_overwriteOldestTitle": "Перезаписать самое старое", + "contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы", + "contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы", + "contactsSettings_overwriteOldestSubtitle": "При включении компаньон будет перезаписывать самый старый контакт, не отмеченный как любимый, когда список контактов полон.", + "contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.", + "discoveredContacts_noMatching": "Нет совпадающих контактов", + "discoveredContacts_searchHint": "Найденные контакты поиска", + "discoveredContacts_contactAdded": "Контакт добавлен", + "discoveredContacts_copyContact": "Копировать контакт в буфер обмена", + "discoveredContacts_addContact": "Добавить контакт", + "discoveredContacts_Title": "Обнаруженные контакты", + "discoveredContacts_deleteContact": "Удалить контакт" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 672b7d7..4fb52ce 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1799,5 +1799,27 @@ "contacts_searchRepeaters": "Hľadať {number}{str} opakovače...", "contacts_searchUsers": "Hľadať {number}{str} používateľov...", "contacts_searchContactsNoNumber": "Hľadať kontakty...", - "contacts_unread": "Neprečítané" + "contacts_unread": "Neprečítané", + "settings_contactSettingsSubtitle": "Nastavenia pre pridávanie kontaktov.", + "contactsSettings_autoAddUsersTitle": "Automaticky pridávať užívateľov", + "contactsSettings_autoAddUsersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavených užívateľov.", + "contactsSettings_autoAddRepeatersTitle": "Automaticky pridávať opakovače", + "contactsSettings_autoAddRoomServersTitle": "Automaticky pridávať server miestnosti", + "contactsSettings_autoAddRoomServersSubtitle": "Povoliť spoločníkovi automaticky pridať objavené serverové miestnosti.", + "contactsSettings_autoAddTitle": "Automatické zisťovanie", + "contactsSettings_title": "Nastavenia kontaktov", + "contactsSettings_otherTitle": "Ďalšie nastavenia súvisiace s kontaktami", + "settings_contactSettings": "Nastavenia kontaktov", + "contactsSettings_autoAddSensorsTitle": "Automaticky pridávať senzory", + "discoveredContacts_noMatching": "Žiadne zhodné kontakty", + "discoveredContacts_searchHint": "Vyhľadať objavené kontakty", + "contactsSettings_autoAddRepeatersSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené repeater.", + "discoveredContacts_contactAdded": "Kontakt bol pridaný", + "discoveredContacts_copyContact": "Kopírovať kontakt do schránky", + "discoveredContacts_deleteContact": "Zmazať kontakt", + "contactsSettings_overwriteOldestSubtitle": "Keď je povolené, spoločník prepíše najstarší kontakt, ktorý nie je označený ako obľúbený, keď je zoznam kontaktov plný.", + "contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.", + "discoveredContacts_Title": "Objavené kontakty", + "contactsSettings_overwriteOldestTitle": "Prepísať najstaršie", + "discoveredContacts_addContact": "Pridať kontakt" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 09359a1..ea75b76 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1799,5 +1799,27 @@ "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", "contacts_searchContactsNoNumber": "Iskanje stikov...", "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", - "contacts_searchUsers": "Išči {number}{str} uporabnikov..." + "contacts_searchUsers": "Išči {number}{str} uporabnikov...", + "settings_contactSettings": "Nastavitve stika", + "contactsSettings_autoAddTitle": "Avtomatsko odkrivanje", + "contactsSettings_autoAddUsersTitle": "Avtomatsko dodaj uporabnike", + "contactsSettings_autoAddRepeatersTitle": "Avtomatsko dodaj ponovitelje", + "contactsSettings_autoAddRepeatersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite ponovitelje.", + "contactsSettings_autoAddRoomServersTitle": "Avtomatsko dodaj strežnike sob", + "contactsSettings_autoAddRoomServersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite strežnike sob.", + "contactsSettings_otherTitle": "Druge nastavitve v zvezi s stiki", + "settings_contactSettingsSubtitle": "Nastavitve za dodajanje stikov.", + "contactsSettings_title": "Nastavitve stikov", + "contactsSettings_autoAddSensorsTitle": "Avtomatsko dodaj senzorje", + "contactsSettings_autoAddUsersSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite uporabnike.", + "discoveredContacts_noMatching": "Ni ujemajočih stikov", + "contactsSettings_autoAddSensorsSubtitle": "Dovoli spremljevalcu, da samodejno doda odkrite senzorje.", + "discoveredContacts_addContact": "Dodaj stik", + "discoveredContacts_contactAdded": "Kontakt dodan", + "discoveredContacts_copyContact": "Kopiraj stik v odložišče", + "contactsSettings_overwriteOldestTitle": "Prepiši najstarejše", + "contactsSettings_overwriteOldestSubtitle": "Ko je omogočen, bo spremljevalec prepisal najstarejši stik, ki ni označen kot najljubši, ko je seznam stikov poln.", + "discoveredContacts_Title": "Odkriti stiki", + "discoveredContacts_searchHint": "Najdeni stiki po iskanju", + "discoveredContacts_deleteContact": "Izbriši stik" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index a923cc9..b4bcfb5 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1799,5 +1799,27 @@ "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", "contacts_searchFavorites": "Sök {number}{str} Favoriter...", "contacts_searchUsers": "Sök {number}{str} användare...", - "contacts_searchRoomServers": "Sök {number}{str} Room-servrar..." + "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", + "settings_contactSettingsSubtitle": "Inställningar för hur kontakter läggs till.", + "settings_contactSettings": "Kontaktinställningar", + "contactsSettings_autoAddTitle": "Automatisk upptäckt", + "contactsSettings_otherTitle": "Andra inställningar relaterade till kontakt", + "contactsSettings_autoAddUsersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta användare", + "contactsSettings_autoAddRepeatersTitle": "Lägg till upprepande enheter automatiskt", + "contactsSettings_autoAddRoomServersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta rumsservrar.", + "contactsSettings_autoAddSensorsTitle": "Lägg till sensorer automatiskt", + "contactsSettings_autoAddUsersTitle": "Lägg till användare automatiskt", + "contactsSettings_title": "Kontaktinställningar", + "contactsSettings_autoAddSensorsSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta sensorer.", + "contactsSettings_overwriteOldestTitle": "Skriv över äldst", + "contactsSettings_autoAddRepeatersSubtitle": "Tillåt kompanjonen att automatiskt lägga till upptäckta repeater.", + "contactsSettings_autoAddRoomServersTitle": "Lägg automatiskt till rumsservrar", + "discoveredContacts_noMatching": "Inga matchande kontakter", + "discoveredContacts_searchHint": "Sök uppfunna kontakter", + "discoveredContacts_deleteContact": "Ta bort kontakt", + "discoveredContacts_Title": "Upptäckta kontakter", + "contactsSettings_overwriteOldestSubtitle": "När den är aktiverad kommer medhjälparen att skriva över den äldsta kontakten som inte är favoritmarkerad när kontaktnamnslistan är full", + "discoveredContacts_contactAdded": "Kontakt tillagd", + "discoveredContacts_addContact": "Lägg till kontakt", + "discoveredContacts_copyContact": "Kopiera kontakt till urklipp" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index c86f11c..733e4e5 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1799,5 +1799,27 @@ "contacts_searchFavorites": "Пошук {number}{str} улюблених...", "contacts_searchContactsNoNumber": "Пошук контактів...", "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", - "contacts_unread": "Непрочитане" + "contacts_unread": "Непрочитане", + "settings_contactSettingsSubtitle": "Налаштування для додавання контактів", + "settings_contactSettings": "Налаштування контактів", + "contactsSettings_autoAddUsersSubtitle": "Дозволити супутникові автоматично додавати виявлених користувачів", + "contactsSettings_autoAddRepeatersTitle": "Автоматично додавати повторювачі", + "contactsSettings_autoAddRepeatersSubtitle": "Дозволити супутнику автоматично додавати виявлені ретранслятори", + "contactsSettings_autoAddRoomServersTitle": "Автоматично додавати сервери кімнат", + "contactsSettings_otherTitle": "Інші налаштування, пов'язані з контактами", + "contactsSettings_autoAddTitle": "Автоматичне виявлення", + "contactsSettings_autoAddUsersTitle": "Автоматично додавати користувачів", + "contactsSettings_title": "Налаштування контактів", + "contactsSettings_autoAddRoomServersSubtitle": "Дозволити супровіднику автоматично додавати виявлені сервери кімнат.", + "contactsSettings_autoAddSensorsTitle": "Автоматично додавати датчики", + "discoveredContacts_searchHint": "Знайти виявлені контакти", + "discoveredContacts_contactAdded": "Контакт додано", + "contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори", + "contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше", + "contactsSettings_overwriteOldestSubtitle": "Коли увімкнено, супровід переганяє найстарший контакт, який не доданий до улюблених, коли список контактів повний.", + "discoveredContacts_Title": "Виявлені контакти", + "discoveredContacts_noMatching": "Відповідних контактів не знайдено", + "discoveredContacts_deleteContact": "Видалити контакт", + "discoveredContacts_copyContact": "Копіювати контакт у буфер обміну", + "discoveredContacts_addContact": "Додати контакт" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 63c02a5..d0abb87 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1804,5 +1804,27 @@ "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", "contacts_searchContactsNoNumber": "搜索联系人...", "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", - "contacts_searchFavorites": "搜索 {number}{str} 收藏..." + "contacts_searchFavorites": "搜索 {number}{str} 收藏...", + "settings_contactSettings": "联系人设置", + "contactsSettings_title": "联系人设置", + "contactsSettings_autoAddUsersTitle": "自动添加用户", + "contactsSettings_otherTitle": "其他联系人相关设置", + "contactsSettings_autoAddUsersSubtitle": "允许伴侣自动添加发现的用户", + "contactsSettings_autoAddRepeatersSubtitle": "允许伴侣自动添加发现的重复器", + "contactsSettings_autoAddSensorsTitle": "自动添加传感器", + "contactsSettings_autoAddRoomServersSubtitle": "允许伴侣自动添加发现的房间服务器", + "contactsSettings_autoAddRepeatersTitle": "自动添加重复器", + "contactsSettings_autoAddTitle": "自动发现", + "settings_contactSettingsSubtitle": "添加联系人的设置", + "contactsSettings_overwriteOldestTitle": "覆盖最旧的", + "contactsSettings_autoAddSensorsSubtitle": "允许伴侣自动添加发现的传感器", + "discoveredContacts_searchHint": "搜索已发现的联系人", + "contactsSettings_autoAddRoomServersTitle": "自动添加房间服务器", + "discoveredContacts_contactAdded": "联系人已添加", + "discoveredContacts_deleteContact": "删除联系人", + "discoveredContacts_addContact": "添加联系人", + "discoveredContacts_noMatching": "没有匹配的联系人", + "discoveredContacts_Title": "已发现的联系人", + "contactsSettings_overwriteOldestSubtitle": "启用时,伴侣将在联系人列表满时覆盖最老的未收藏的联系人。", + "discoveredContacts_copyContact": "复制联系人到剪贴板" } diff --git a/lib/models/discovery_contact.dart b/lib/models/discovery_contact.dart index 54c2937..f6c6a52 100644 --- a/lib/models/discovery_contact.dart +++ b/lib/models/discovery_contact.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import '../connector/meshcore_protocol.dart'; class DiscoveryContact { + final Uint8List rawPacket; final Uint8List publicKey; final String name; final int type; @@ -12,6 +13,7 @@ class DiscoveryContact { final DateTime lastSeen; DiscoveryContact({ + required this.rawPacket, required this.publicKey, required this.name, required this.type, @@ -48,6 +50,7 @@ class DiscoveryContact { bool get hasLocation => latitude != null && longitude != null; DiscoveryContact copyWith({ + Uint8List? rawPacket, Uint8List? publicKey, String? name, int? type, @@ -58,6 +61,7 @@ class DiscoveryContact { DateTime? lastSeen, }) { return DiscoveryContact( + rawPacket: rawPacket ?? this.rawPacket, publicKey: publicKey ?? this.publicKey, name: name ?? this.name, type: type ?? this.type, @@ -92,42 +96,6 @@ class DiscoveryContact { return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; } - Uint8List? get traceRouteBytes { - final pathBytes = path; - Uint8List? traceBytes; - - if (pathBytes.isEmpty) { - traceBytes = Uint8List(1); - traceBytes[0] = publicKey[0]; - return traceBytes; - } - - if (type == advTypeRepeater || type == advTypeRoom) { - final len = (pathBytes.length + pathBytes.length + 1); - traceBytes = Uint8List(len); - traceBytes[pathBytes.length] = publicKey[0]; - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length) { - traceBytes[len - 1 - i] = pathBytes[i]; - } - } - } else { - if (pathBytes.length < 2) { - return pathBytes[0] == 0 ? null : pathBytes; - } - final len = (pathBytes.length + pathBytes.length - 1); - traceBytes = Uint8List(len); - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length - 1) { - traceBytes[len - 1 - i] = pathBytes[i]; - } - } - } - return traceBytes; - } - @override bool operator ==(Object other) => other is DiscoveryContact && publicKeyHex == other.publicKeyHex; diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 6e9f841..5753785 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -219,7 +219,8 @@ class _ContactsScreenState extends State } final hexString = text.substring('meshcore://'.length); try { - final importContactFrame = buildImportContactFrame(hexString); + final bytes = hex2Uint8List(hexString); + final importContactFrame = buildImportContactFrame(bytes); _pendingOperations.add(ContactOperationType.import); await connector.sendFrame(importContactFrame, expectsGenericAck: true); } catch (e) { diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 10dc016..5cc9fa8 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -1,8 +1,7 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:meshcore_open/models/contact.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -96,6 +95,11 @@ class _DiscoveryScreenState extends State { color: Colors.grey[600], ), ), + onTap: () { + connector.importDiscoveredContact(contact); + }, + onLongPress: () => + _showContactContextMenu(contact, connector), ); }, ), @@ -105,9 +109,64 @@ class _DiscoveryScreenState extends State { ); } - Widget _buildFilters(filteredAndSorted, connector) { - final l10n = context.l10n; + Future _showContactContextMenu( + DiscoveryContact contact, + MeshCoreConnector connector, + ) async { + final action = await showModalBottomSheet( + context: context, + showDragHandle: true, + builder: (sheetContext) { + final l10n = context.l10n; + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.add_reaction_sharp), + title: Text(l10n.discoveredContacts_addContact), + onTap: () => Navigator.of(sheetContext).pop('import_contact'), + ), + ListTile( + leading: const Icon(Icons.copy), + title: Text(l10n.discoveredContacts_copyContact), + onTap: () => Navigator.of(sheetContext).pop('copy_contact'), + ), + ListTile( + leading: const Icon(Icons.delete), + title: Text(l10n.discoveredContacts_deleteContact), + onTap: () => Navigator.of(sheetContext).pop('delete_contact'), + ), + ], + ), + ); + }, + ); + if (!mounted || action == null) return; + + switch (action) { + case 'import_contact': + connector.importDiscoveredContact(contact); + break; + case 'copy_contact': + final hexString = pubKeyToHex(contact.rawPacket); + Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), + ); + break; + case 'delete_contact': + connector.removeDiscoveredContact(contact); + break; + } + } + + Widget _buildFilters( + List filteredAndSorted, + MeshCoreConnector connector, + ) { String hintText = ""; switch (typeFilter) { case ContactTypeFilter.all: @@ -216,6 +275,10 @@ class _DiscoveryScreenState extends State { return matchesDiscoveryContactQuery(contact, searchQuery); }).toList(); + filtered = filtered.where((contact) { + return !connector.knownContactKeys.contains(contact.publicKeyHex); + }).toList(); + // Filter out own node from the list if (connector.selfPublicKey != null) { final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!); diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index 84f7807..37bfbb4 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -30,6 +30,7 @@ class ContactDiscoveryStore { Map _toJson(DiscoveryContact contact) { return { + 'rawPacket': base64Encode(contact.rawPacket), 'publicKey': base64Encode(contact.publicKey), 'name': contact.name, 'type': contact.type, @@ -44,6 +45,7 @@ class ContactDiscoveryStore { DiscoveryContact _fromJson(Map json) { final lastSeenMs = json['lastSeen'] as int? ?? 0; return DiscoveryContact( + rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)), publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', type: json['type'] as int? ?? 0, From 128e99e3e7cd6cab5e0e4b5161ece27e96afb810 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 1 Mar 2026 10:35:32 -0800 Subject: [PATCH 210/421] refactor(settings): remove unused import for adaptive_app_bar_title --- lib/screens/settings_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index fe893f8..0d429f3 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,7 +8,6 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; -import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/app_bar.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; From b02225c02e4a8ad0ea1cfb756e38b30a78013f79 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 1 Mar 2026 10:41:31 -0800 Subject: [PATCH 211/421] refactor(connector): remove unused radio settings frame and update command constant --- lib/connector/meshcore_connector.dart | 1 - lib/connector/meshcore_protocol.dart | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c7ebdaf..52f2a77 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1093,7 +1093,6 @@ class MeshCoreConnector extends ChangeNotifier { await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildAppStartFrame()); await requestBatteryStatus(force: true); - await sendFrame(buildGetRadioSettingsFrame()); await sendFrame(buildGetCustomVarsFrame()); await sendFrame(buildGetAutoAddFlagsFrame()); _scheduleSelfInfoRetry(); diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 4f50ec3..db7e02a 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -164,7 +164,7 @@ const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; const int cmdSetOtherParams = 38; -const int cmdGetRadioSettings = 57; +const int cmdSendAnonReq = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; const int cmdSetCustomVar = 41; @@ -691,11 +691,6 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) { return writer.toBytes(); } -// Build CMD_GET_RADIO_SETTINGS frame -Uint8List buildGetRadioSettingsFrame() { - return Uint8List.fromList([cmdGetRadioSettings]); -} - //Build CMD_GET_CUSTOM_VARS frame Uint8List buildGetCustomVarsFrame() { return Uint8List.fromList([cmdGetCustomVar]); From d2640e12940d7e9a13174428dda33d07dda9333a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 1 Mar 2026 10:52:19 -0800 Subject: [PATCH 212/421] feat(localization): update 'overwrite oldest contact' subtitle for multiple languages --- lib/l10n/app_bg.arb | 4 ++-- lib/l10n/app_de.arb | 4 ++-- lib/l10n/app_en.arb | 2 +- lib/l10n/app_es.arb | 4 ++-- lib/l10n/app_fr.arb | 4 ++-- lib/l10n/app_it.arb | 4 ++-- lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/l10n/app_nl.arb | 4 ++-- lib/l10n/app_pl.arb | 4 ++-- lib/l10n/app_pt.arb | 4 ++-- lib/l10n/app_ru.arb | 4 ++-- lib/l10n/app_sk.arb | 4 ++-- lib/l10n/app_sl.arb | 4 ++-- lib/l10n/app_sv.arb | 4 ++-- lib/l10n/app_uk.arb | 4 ++-- lib/l10n/app_zh.arb | 4 ++-- 31 files changed, 45 insertions(+), 45 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index eed1897..cf33d3d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1817,9 +1817,9 @@ "discoveredContacts_Title": "Открити контакти", "discoveredContacts_searchHint": "Търсене на открити контакти", "discoveredContacts_noMatching": "Няма съвпадащи контакти", - "contactsSettings_overwriteOldestSubtitle": "Когато е активиран, компаньонът ще презапише най-стария контакт, който не е отбелязан като любим, когато списъкът с контакти е пълен.", "discoveredContacts_contactAdded": "Контакт добавен", "discoveredContacts_copyContact": "Копирай контакт в клипборда", "discoveredContacts_deleteContact": "Изтрий контакт", - "discoveredContacts_addContact": "Добави контакт" + "discoveredContacts_addContact": "Добави контакт", + "contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 4fe662f..0029f4c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1848,6 +1848,6 @@ "discoveredContacts_Title": "Entdeckte Kontakte", "discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren", "contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten", - "contactsSettings_overwriteOldestSubtitle": "Wenn aktiviert, überschreibt der Begleiter den ältesten nicht favorisierten Kontakt, wenn die Kontaktliste voll ist.", - "contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen" + "contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen", + "contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8d5e528..66cf2a2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1852,7 +1852,7 @@ "contactsSettings_autoAddSensorsTitle": "Auto-add sensors", "contactsSettings_autoAddSensorsSubtitle": "Allow the companion to automatically add discovered sensors.", "contactsSettings_overwriteOldestTitle": "Overwrite Oldest", - "contactsSettings_overwriteOldestSubtitle": "When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.", + "contactsSettings_overwriteOldestSubtitle": "When the contact list is full, the oldest non-favorited contact will be replaced.", "discoveredContacts_Title": "Discovered Contacts", "discoveredContacts_noMatching": "No matching contacts", "discoveredContacts_searchHint": "Search discovered contacts", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5a2e1c8..54b9c4d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1847,7 +1847,7 @@ "discoveredContacts_copyContact": "Copiar contacto al portapapeles", "discoveredContacts_deleteContact": "Eliminar contacto", "discoveredContacts_Title": "Contactos descubiertos", - "contactsSettings_overwriteOldestSubtitle": "Cuando se habilita, el compañero sobrescribirá el contacto más antiguo no favorito cuando la lista de contactos esté llena.", "discoveredContacts_searchHint": "Buscar contactos descubiertos", - "discoveredContacts_addContact": "Agregar contacto" + "discoveredContacts_addContact": "Agregar contacto", + "contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e222f53..bdfc3e6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1819,7 +1819,7 @@ "discoveredContacts_deleteContact": "Supprimer le contact", "contactsSettings_overwriteOldestTitle": "Écraser le plus ancien", "contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.", - "contactsSettings_overwriteOldestSubtitle": "Lorsqu'il est activé, le compagnon écrasera l'ancien contact non favori lorsque la liste de contacts est pleine.", "discoveredContacts_Title": "Contacts découverts", - "discoveredContacts_searchHint": "Rechercher des contacts découverts" + "discoveredContacts_searchHint": "Rechercher des contacts découverts", + "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 53c3737..ad4a80b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1817,9 +1817,9 @@ "contactsSettings_autoAddRoomServersTitle": "Aggiungere automaticamente i server delle stanze", "discoveredContacts_addContact": "Aggiungi contatto", "contactsSettings_overwriteOldestTitle": "Sostituisci il più vecchio", - "contactsSettings_overwriteOldestSubtitle": "Quando abilitato, il companion sovrascriverà il contatto più vecchio non preferito quando l'elenco dei contatti è pieno.", "discoveredContacts_Title": "Contatti scoperti", "discoveredContacts_contactAdded": "Contatto aggiunto", "discoveredContacts_deleteContact": "Elimina Contatto", - "discoveredContacts_copyContact": "Copia contatto negli appunti" + "discoveredContacts_copyContact": "Copia contatto negli appunti", + "contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a7c458f..8b73b19 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5468,7 +5468,7 @@ abstract class AppLocalizations { /// No description provided for @contactsSettings_overwriteOldestSubtitle. /// /// In en, this message translates to: - /// **'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'** + /// **'When the contact list is full, the oldest non-favorited contact will be replaced.'** String get contactsSettings_overwriteOldestSubtitle; /// No description provided for @discoveredContacts_Title. diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index f9a858f..16712d2 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -3167,7 +3167,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Когато е активиран, компаньонът ще презапише най-стария контакт, който не е отбелязан като любим, когато списъкът с контакти е пълен.'; + 'Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.'; @override String get discoveredContacts_Title => 'Открити контакти'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 77b2a4b..64c75cb 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -3177,7 +3177,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Wenn aktiviert, überschreibt der Begleiter den ältesten nicht favorisierten Kontakt, wenn die Kontaktliste voll ist.'; + 'Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.'; @override String get discoveredContacts_Title => 'Entdeckte Kontakte'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 08132ac..031dd44 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -3116,7 +3116,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'When enabled, the companion will overwrite the oldest contact not favoriteited when the contact list is full.'; + 'When the contact list is full, the oldest non-favorited contact will be replaced.'; @override String get discoveredContacts_Title => 'Discovered Contacts'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 0d2167f..c516d96 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -3169,7 +3169,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Cuando se habilita, el compañero sobrescribirá el contacto más antiguo no favorito cuando la lista de contactos esté llena.'; + 'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.'; @override String get discoveredContacts_Title => 'Contactos descubiertos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 29f3af8..1ff8aca 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -3190,7 +3190,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Lorsqu\'il est activé, le compagnon écrasera l\'ancien contact non favori lorsque la liste de contacts est pleine.'; + 'Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.'; @override String get discoveredContacts_Title => 'Contacts découverts'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 1fb4eb1..a4e6057 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -3172,7 +3172,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Quando abilitato, il companion sovrascriverà il contatto più vecchio non preferito quando l\'elenco dei contatti è pieno.'; + 'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.'; @override String get discoveredContacts_Title => 'Contatti scoperti'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6ed3a84..3deb4c7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -3158,7 +3158,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Wanneer ingeschakeld, overschrijft de companion het oudste contact dat niet is gemarkeerd als favoriet wanneer de contactenlijst vol is.'; + 'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.'; @override String get discoveredContacts_Title => 'Ontdekte contacten'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 703e281..78c1f8a 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -3171,7 +3171,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Gdy jest włączone, companion zastępuje najstarszy kontakt nie ulubiony, gdy lista kontaktów jest pełna.'; + 'Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.'; @override String get discoveredContacts_Title => 'Odkryte Kontakty'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 442b41e..58f2284 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -3167,7 +3167,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Quando ativado, o acompanhante substituirá o contato mais antigo não favoritado quando a lista de contatos estiver cheia.'; + 'Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.'; @override String get discoveredContacts_Title => 'Contatos Descobertos'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 90f8ed6..0d5dea5 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -3179,7 +3179,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'При включении компаньон будет перезаписывать самый старый контакт, не отмеченный как любимый, когда список контактов полон.'; + 'Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.'; @override String get discoveredContacts_Title => 'Обнаруженные контакты'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 944bc6c..fd4db23 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -3153,7 +3153,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Keď je povolené, spoločník prepíše najstarší kontakt, ktorý nie je označený ako obľúbený, keď je zoznam kontaktov plný.'; + 'Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.'; @override String get discoveredContacts_Title => 'Objavené kontakty'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 0fd67ce..339f30a 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -3157,7 +3157,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Ko je omogočen, bo spremljevalec prepisal najstarejši stik, ki ni označen kot najljubši, ko je seznam stikov poln.'; + 'Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.'; @override String get discoveredContacts_Title => 'Odkriti stiki'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 86ba9fd..9e32ec8 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -3136,7 +3136,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'När den är aktiverad kommer medhjälparen att skriva över den äldsta kontakten som inte är favoritmarkerad när kontaktnamnslistan är full'; + 'När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.'; @override String get discoveredContacts_Title => 'Upptäckta kontakter'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index abeb6c6..fae7e99 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -3185,7 +3185,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - 'Коли увімкнено, супровід переганяє найстарший контакт, який не доданий до улюблених, коли список контактів повний.'; + 'Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.'; @override String get discoveredContacts_Title => 'Виявлені контакти'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 72babcc..8a3a3a3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2935,7 +2935,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contactsSettings_overwriteOldestSubtitle => - '启用时,伴侣将在联系人列表满时覆盖最老的未收藏的联系人。'; + '当联系人列表已满时,将替换最老的非收藏联系人。'; @override String get discoveredContacts_Title => '已发现的联系人'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7d57cb8..68e6fc6 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1819,7 +1819,7 @@ "discoveredContacts_copyContact": "Kopieer contact naar klembord", "discoveredContacts_deleteContact": "Contact verwijderen", "discoveredContacts_Title": "Ontdekte contacten", - "contactsSettings_overwriteOldestSubtitle": "Wanneer ingeschakeld, overschrijft de companion het oudste contact dat niet is gemarkeerd als favoriet wanneer de contactenlijst vol is.", "discoveredContacts_contactAdded": "Contact toegevoegd", - "discoveredContacts_searchHint": "Ontdekte contacten zoeken" + "discoveredContacts_searchHint": "Ontdekte contacten zoeken", + "contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d20ffee..66d55aa 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1818,8 +1818,8 @@ "discoveredContacts_copyContact": "Kopiuj kontakt do schowka", "contactsSettings_overwriteOldestTitle": "Nadpisz najstarszy", "discoveredContacts_Title": "Odkryte Kontakty", - "contactsSettings_overwriteOldestSubtitle": "Gdy jest włączone, companion zastępuje najstarszy kontakt nie ulubiony, gdy lista kontaktów jest pełna.", "contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.", "discoveredContacts_noMatching": "Brak pasujących kontaktów", - "discoveredContacts_deleteContact": "Usuń kontakt" + "discoveredContacts_deleteContact": "Usuń kontakt", + "contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 421ddd3..bbf067b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1810,7 +1810,6 @@ "contactsSettings_autoAddRoomServersTitle": "Adicionar automaticamente servidores de sala", "contactsSettings_overwriteOldestTitle": "Sobrescrever o Mais Antigo", "contactsSettings_autoAddSensorsTitle": "Adicionar sensores automaticamente", - "contactsSettings_overwriteOldestSubtitle": "Quando ativado, o acompanhante substituirá o contato mais antigo não favoritado quando a lista de contatos estiver cheia.", "discoveredContacts_Title": "Contatos Descobertos", "contactsSettings_autoAddUsersSubtitle": "Permitir que o companheiro adicione automaticamente os usuários descobertos.", "contactsSettings_autoAddRepeatersTitle": "Adicionar repetidores automaticamente", @@ -1821,5 +1820,6 @@ "discoveredContacts_copyContact": "Copiar Contato para a área de transferência", "discoveredContacts_deleteContact": "Excluir Contato", "discoveredContacts_contactAdded": "Contato adicionado", - "discoveredContacts_addContact": "Adicionar Contato" + "discoveredContacts_addContact": "Adicionar Contato", + "contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b5910b7..21a9cda 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1053,7 +1053,6 @@ "contactsSettings_overwriteOldestTitle": "Перезаписать самое старое", "contactsSettings_autoAddRepeatersTitle": "Автоматически добавлять ретрансляторы", "contactsSettings_autoAddRepeatersSubtitle": "Разрешить спутнику автоматически добавлять обнаруженные ретрансляторы", - "contactsSettings_overwriteOldestSubtitle": "При включении компаньон будет перезаписывать самый старый контакт, не отмеченный как любимый, когда список контактов полон.", "contactsSettings_autoAddRoomServersSubtitle": "Разрешить компаньону автоматически добавлять обнаруженные сервера комнат.", "discoveredContacts_noMatching": "Нет совпадающих контактов", "discoveredContacts_searchHint": "Найденные контакты поиска", @@ -1061,5 +1060,6 @@ "discoveredContacts_copyContact": "Копировать контакт в буфер обмена", "discoveredContacts_addContact": "Добавить контакт", "discoveredContacts_Title": "Обнаруженные контакты", - "discoveredContacts_deleteContact": "Удалить контакт" + "discoveredContacts_deleteContact": "Удалить контакт", + "contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 4fb52ce..19b217e 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1817,9 +1817,9 @@ "discoveredContacts_contactAdded": "Kontakt bol pridaný", "discoveredContacts_copyContact": "Kopírovať kontakt do schránky", "discoveredContacts_deleteContact": "Zmazať kontakt", - "contactsSettings_overwriteOldestSubtitle": "Keď je povolené, spoločník prepíše najstarší kontakt, ktorý nie je označený ako obľúbený, keď je zoznam kontaktov plný.", "contactsSettings_autoAddSensorsSubtitle": "Povoliť spoločníkovi automaticky pridávať objavené senzory.", "discoveredContacts_Title": "Objavené kontakty", "contactsSettings_overwriteOldestTitle": "Prepísať najstaršie", - "discoveredContacts_addContact": "Pridať kontakt" + "discoveredContacts_addContact": "Pridať kontakt", + "contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index ea75b76..aefa817 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1818,8 +1818,8 @@ "discoveredContacts_contactAdded": "Kontakt dodan", "discoveredContacts_copyContact": "Kopiraj stik v odložišče", "contactsSettings_overwriteOldestTitle": "Prepiši najstarejše", - "contactsSettings_overwriteOldestSubtitle": "Ko je omogočen, bo spremljevalec prepisal najstarejši stik, ki ni označen kot najljubši, ko je seznam stikov poln.", "discoveredContacts_Title": "Odkriti stiki", "discoveredContacts_searchHint": "Najdeni stiki po iskanju", - "discoveredContacts_deleteContact": "Izbriši stik" + "discoveredContacts_deleteContact": "Izbriši stik", + "contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b4bcfb5..b8f29d7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1818,8 +1818,8 @@ "discoveredContacts_searchHint": "Sök uppfunna kontakter", "discoveredContacts_deleteContact": "Ta bort kontakt", "discoveredContacts_Title": "Upptäckta kontakter", - "contactsSettings_overwriteOldestSubtitle": "När den är aktiverad kommer medhjälparen att skriva över den äldsta kontakten som inte är favoritmarkerad när kontaktnamnslistan är full", "discoveredContacts_contactAdded": "Kontakt tillagd", "discoveredContacts_addContact": "Lägg till kontakt", - "discoveredContacts_copyContact": "Kopiera kontakt till urklipp" + "discoveredContacts_copyContact": "Kopiera kontakt till urklipp", + "contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 733e4e5..275cb13 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1816,10 +1816,10 @@ "discoveredContacts_contactAdded": "Контакт додано", "contactsSettings_autoAddSensorsSubtitle": "Дозволити супровіднику автоматично додавати виявлені сенсори", "contactsSettings_overwriteOldestTitle": "Перезаписати найстаріше", - "contactsSettings_overwriteOldestSubtitle": "Коли увімкнено, супровід переганяє найстарший контакт, який не доданий до улюблених, коли список контактів повний.", "discoveredContacts_Title": "Виявлені контакти", "discoveredContacts_noMatching": "Відповідних контактів не знайдено", "discoveredContacts_deleteContact": "Видалити контакт", "discoveredContacts_copyContact": "Копіювати контакт у буфер обміну", - "discoveredContacts_addContact": "Додати контакт" + "discoveredContacts_addContact": "Додати контакт", + "contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d0abb87..11bbd67 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1825,6 +1825,6 @@ "discoveredContacts_addContact": "添加联系人", "discoveredContacts_noMatching": "没有匹配的联系人", "discoveredContacts_Title": "已发现的联系人", - "contactsSettings_overwriteOldestSubtitle": "启用时,伴侣将在联系人列表满时覆盖最老的未收藏的联系人。", - "discoveredContacts_copyContact": "复制联系人到剪贴板" + "discoveredContacts_copyContact": "复制联系人到剪贴板", + "contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。" } From fcab69f9f088bea3a68f4d38387e53ef302a451d Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 1 Mar 2026 13:05:57 -0800 Subject: [PATCH 213/421] refactor(connector): adjust frame length check and simplify contact handling logic refactor(settings): extract settings sending logic into a separate method refactor(ble_debug_log_service): remove unused command case for radio settings refactor(app_bar): update compact width threshold for app bar display --- lib/connector/meshcore_connector.dart | 14 ++-------- lib/screens/settings_screen.dart | 36 ++++++++++++++++++------- lib/services/ble_debug_log_service.dart | 2 -- lib/widgets/app_bar.dart | 2 +- 4 files changed, 30 insertions(+), 24 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 52f2a77..ad2a074 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2060,7 +2060,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfLatitude = readInt32LE(frame, 36) / 1000000.0; _selfLongitude = readInt32LE(frame, 40) / 1000000.0; - if (frame.length >= 47 && frame[47] == 0x00) { + if (frame.length >= 48 && frame[47] == 0x00) { sendFrame(buildSetOtherParamsFrame(0, 0, 0)); } @@ -3947,17 +3947,6 @@ class MeshCoreConnector extends ChangeNotifier { final existingIndex = _discoveredContacts.indexWhere( (c) => c.publicKeyHex == contact.publicKeyHex, ); - final existingContactsIndex = _contacts.indexWhere( - (c) => c.publicKeyHex == contact.publicKeyHex, - ); - - if (existingContactsIndex >= 0) { - if (existingIndex >= 0) { - removeDiscoveredContact(_discoveredContacts[existingIndex]); - unawaited(_persistDiscoveredContacts()); - } - return; - } // Update existing contact if (existingIndex >= 0) { @@ -3972,6 +3961,7 @@ class MeshCoreConnector extends ChangeNotifier { longitude: contact.longitude, lastSeen: contact.lastSeen, ); + notifyListeners(); return; } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 0d429f3..0423517 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -938,16 +938,15 @@ class _SettingsScreenState extends State { child: Text(l10n.common_cancel), ), TextButton( - onPressed: () async { - final frame = buildSetAutoAddConfigFrame( - autoAddChat: autoAddChat, - autoAddRepeater: autoAddRepeater, - autoAddRoomServer: autoAddRoomServer, - autoAddSensor: autoAddSensor, - overwriteOldest: overwriteOldest, + onPressed: () { + _sendSettings( + connector, + autoAddChat, + autoAddRepeater, + autoAddRoomServer, + autoAddSensor, + overwriteOldest, ); - await connector.sendFrame(frame); - await connector.sendFrame(buildGetAutoAddFlagsFrame()); Navigator.pop(context); }, child: Text(l10n.common_save), @@ -957,6 +956,25 @@ class _SettingsScreenState extends State { ), ); } + + void _sendSettings( + MeshCoreConnector connector, + autoAddChat, + autoAddRepeater, + autoAddRoomServer, + autoAddSensor, + overwriteOldest, + ) async { + final frame = buildSetAutoAddConfigFrame( + autoAddChat: autoAddChat, + autoAddRepeater: autoAddRepeater, + autoAddRoomServer: autoAddRoomServer, + autoAddSensor: autoAddSensor, + overwriteOldest: overwriteOldest, + ); + await connector.sendFrame(frame); + await connector.sendFrame(buildGetAutoAddFlagsFrame()); + } } class _RadioSettingsDialog extends StatefulWidget { diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index d923d6b..df2822b 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -172,8 +172,6 @@ class BleDebugLogService extends ChangeNotifier { return 'CMD_GET_CHANNEL'; case cmdSetChannel: return 'CMD_SET_CHANNEL'; - case cmdGetRadioSettings: - return 'CMD_GET_RADIO_SETTINGS'; case cmdSetCustomVar: return 'CMD_SET_CUSTOM_VAR'; case cmdSendTracePath: diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index 7324481..9f2494e 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -30,7 +30,7 @@ class AppBarTitle extends StatelessWidget { final availableWidth = constraints.hasBoundedWidth ? constraints.maxWidth : MediaQuery.sizeOf(context).width; - final compact = availableWidth < 240; + final compact = availableWidth < 170; final showSubtitle = !compact && connector.isConnected && selfName != null && subtitle; final showBattery = availableWidth >= 60; From 8d736025099de6a08b16ca528a8d613b5f7bba94 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 1 Mar 2026 14:36:04 -0800 Subject: [PATCH 214/421] add flags for manual contact addition and telemetry mode handling --- lib/connector/meshcore_connector.dart | 79 ++++++++++++++++++++------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ad2a074..14d62dd 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -163,6 +163,12 @@ class MeshCoreConnector extends ChangeNotifier { bool _autoAddRoomServers = false; bool _autoAddSensors = false; bool _overwriteOldest = false; + bool _manualAddContacts = false; + int _telemetryModeBase = 0; + int _telemetryModeLoc = 0; + int _telemetryModeEnv = 0; + int _advertLocPolicy = 0; + int _multiAcks = 0; static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; @@ -1095,6 +1101,7 @@ class MeshCoreConnector extends ChangeNotifier { await requestBatteryStatus(force: true); await sendFrame(buildGetCustomVarsFrame()); await sendFrame(buildGetAutoAddFlagsFrame()); + _scheduleSelfInfoRetry(); } @@ -1980,6 +1987,7 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeAutoAddConfig: _handleAutoAddConfig(frame); + _checkManualAddContacts(); break; case respCodeBattAndStorage: _handleBatteryAndStorage(frame); @@ -2052,28 +2060,35 @@ class MeshCoreConnector extends ChangeNotifier { // [56] = sf // [57] = cr // [58+] = node_name - if (frame.length < 4 + pubKeySize) return; + final reader = BufferReader(frame); + try { + reader.skipBytes(2); + _currentTxPower = reader.readByte(); + _maxTxPower = reader.readByte(); + _selfPublicKey = reader.readBytes(pubKeySize); + _selfLatitude = reader.readInt32LE() / 1000000.0; + _selfLongitude = reader.readInt32LE() / 1000000.0; + _multiAcks = reader.readByte(); + _advertLocPolicy = reader.readByte(); - _currentTxPower = frame[2]; - _maxTxPower = frame[3]; - _selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize)); - _selfLatitude = readInt32LE(frame, 36) / 1000000.0; - _selfLongitude = readInt32LE(frame, 40) / 1000000.0; + final telemetryFlag = reader.readByte(); + _telemetryModeBase = telemetryFlag & 0x03; + _telemetryModeEnv = telemetryFlag >> 2 & 0x03; + _telemetryModeLoc = telemetryFlag >> 4 & 0x03; - if (frame.length >= 48 && frame[47] == 0x00) { - sendFrame(buildSetOtherParamsFrame(0, 0, 0)); - } + _manualAddContacts = reader.readByte() & 0x01 == 0x00; - // Radio settings (if frame is long enough) - if (frame.length >= 58) { - _currentFreqHz = readUint32LE(frame, 48); - _currentBwHz = readUint32LE(frame, 52); - _currentSf = frame[56]; - _currentCr = frame[57]; - } - // Node name starts at offset 58 if frame is long enough - if (frame.length > 58) { - _selfName = readCString(frame, 58, frame.length - 58); + _currentFreqHz = reader.readUInt32LE(); + _currentBwHz = reader.readUInt32LE(); + _currentSf = reader.readByte(); + _currentCr = reader.readByte(); + + _selfName = reader.readString(); + } catch (e) { + _appDebugLogService?.error( + 'Error parsing SELF_INFO frame: $e', + tag: 'Connector', + ); } _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); @@ -2151,6 +2166,32 @@ class MeshCoreConnector extends ChangeNotifier { } } + void _checkManualAddContacts() async { + // If manual add contacts is enabled, set auto add config and other params. + // and disable it after + if (_manualAddContacts) { + await sendFrame( + buildSetAutoAddConfigFrame( + autoAddChat: true, + autoAddRepeater: true, + autoAddRoomServer: true, + autoAddSensor: true, + overwriteOldest: _overwriteOldest, + ), + ); + await sendFrame( + buildSetOtherParamsFrame( + (_telemetryModeEnv << 4) | + (_telemetryModeLoc << 2) | + (_telemetryModeBase), + _advertLocPolicy, + _multiAcks, + ), + ); + _manualAddContacts = false; + } + } + /// Calculate timeout for a message based on radio settings and path length /// Returns timeout in milliseconds, considering number of hops int calculateTimeout({required int pathLength, int messageBytes = 100}) { From ddeb1edc2e27b277e25389bc09e0c2616f6115d8 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 1 Mar 2026 14:40:26 -0800 Subject: [PATCH 215/421] refactor(discovery): simplify sorting logic for last seen contacts --- lib/screens/discovery_screen.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 5cc9fa8..dadf632 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -293,9 +293,7 @@ class _DiscoveryScreenState extends State { switch (sortOption) { case ContactSortOption.lastSeen: - filtered.sort( - (a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)), - ); + filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); break; case ContactSortOption.name: filtered.sort( @@ -324,13 +322,6 @@ class _DiscoveryScreenState extends State { } } - DateTime _resolveLastSeen(DiscoveryContact contact) { - if (contact.type != advTypeChat) return contact.lastSeen; - return contact.lastSeen.isAfter(contact.lastSeen) - ? contact.lastSeen - : contact.lastSeen; - } - IconData _getTypeIcon(int type) { switch (type) { case advTypeChat: From 38856c67e5a9acee38c8013bfadc426259f7a290 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 2 Mar 2026 10:23:14 -0800 Subject: [PATCH 216/421] feat: Add functionality to delete all discovered contacts - Implemented a new method to remove all discovered contacts from the list. - Added confirmation dialog for deleting all discovered contacts in the discovery screen. - Updated localization files to include new strings for deleting all discovered contacts. - Refactored contact import logic to streamline the process. - Enhanced the discovery handling to notify users appropriately based on settings. --- lib/connector/meshcore_connector.dart | 109 +++++++++++++++++++++----- lib/l10n/app_bg.arb | 5 +- lib/l10n/app_de.arb | 5 +- lib/l10n/app_en.arb | 5 +- lib/l10n/app_es.arb | 5 +- lib/l10n/app_fr.arb | 5 +- lib/l10n/app_it.arb | 5 +- lib/l10n/app_localizations.dart | 20 ++++- lib/l10n/app_localizations_bg.dart | 11 +++ lib/l10n/app_localizations_de.dart | 11 +++ lib/l10n/app_localizations_en.dart | 13 ++- lib/l10n/app_localizations_es.dart | 11 +++ lib/l10n/app_localizations_fr.dart | 11 +++ lib/l10n/app_localizations_it.dart | 11 +++ lib/l10n/app_localizations_nl.dart | 11 +++ lib/l10n/app_localizations_pl.dart | 11 +++ lib/l10n/app_localizations_pt.dart | 11 +++ lib/l10n/app_localizations_ru.dart | 11 +++ lib/l10n/app_localizations_sk.dart | 11 +++ lib/l10n/app_localizations_sl.dart | 11 +++ lib/l10n/app_localizations_sv.dart | 11 +++ lib/l10n/app_localizations_uk.dart | 11 +++ lib/l10n/app_localizations_zh.dart | 9 +++ lib/l10n/app_nl.arb | 5 +- lib/l10n/app_pl.arb | 5 +- lib/l10n/app_pt.arb | 5 +- lib/l10n/app_ru.arb | 5 +- lib/l10n/app_sk.arb | 5 +- lib/l10n/app_sl.arb | 5 +- lib/l10n/app_sv.arb | 5 +- lib/l10n/app_uk.arb | 5 +- lib/l10n/app_zh.arb | 5 +- lib/screens/contacts_screen.dart | 2 +- lib/screens/discovery_screen.dart | 43 ++++++++++ 34 files changed, 378 insertions(+), 36 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 14d62dd..d71cc75 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1527,6 +1527,8 @@ class MeshCoreConnector extends ChangeNotifier { Future removeContact(Contact contact) async { if (!isConnected) return; + _handleDiscovery(contact, Uint8List(0), noNotify: true); + await sendFrame(buildRemoveContactFrame(contact.publicKey)); _contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex); _knownContactKeys.remove(contact.publicKeyHex); @@ -1552,23 +1554,15 @@ class MeshCoreConnector extends ChangeNotifier { Future importDiscoveredContact(DiscoveryContact contact) async { if (!isConnected) return; + await sendFrame( - buildSetAutoAddConfigFrame( - autoAddChat: true, - autoAddRepeater: true, - autoAddRoomServer: true, - autoAddSensor: true, - overwriteOldest: _overwriteOldest, - ), - ); - await sendFrame(buildImportContactFrame(contact.rawPacket)); - await sendFrame( - buildSetAutoAddConfigFrame( - autoAddChat: _autoAddUsers, - autoAddRepeater: _autoAddRepeaters, - autoAddRoomServer: _autoAddRoomServers, - autoAddSensor: _autoAddSensors, - overwriteOldest: _overwriteOldest, + buildUpdateContactPathFrame( + contact.publicKey, + contact.path, + contact.pathLength, + type: contact.type, + flags: 0, + name: contact.name, ), ); @@ -3805,6 +3799,76 @@ class MeshCoreConnector extends ChangeNotifier { } } + void importContact(Uint8List frame) { + final packet = BufferReader(frame); + int payloadType = 0; + Uint8List pathBytes = Uint8List(0); + try { + packet.skipBytes(1); // Skip frame type byte + packet.skipBytes(1); // Skip SNR byte + packet.skipBytes(1); // Skip RSSI byte + final header = packet.readByte(); + payloadType = (header >> 2) & 0x0F; + //final payloadVer = (header >> 6) & 0x03; + final pathLen = packet.readByte(); + pathBytes = packet.readBytes(pathLen); + } catch (e) { + appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); + return; + } + double latitude = 0.0; + double longitude = 0.0; + String name = ''; + Uint8List publicKey = Uint8List(0); + int type = 0; + int timestamp = 0; + bool hasLocation = false; + bool hasName = false; + if (payloadType != payloadTypeADVERT) { + appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector'); + return; + } + try { + publicKey = packet.readBytes(32); + timestamp = packet.readInt32LE(); + //TODO add signature verification + packet.skipBytes(64); // Skip signature for now + final flags = packet.readByte(); + type = flags & 0x0F; + hasLocation = (flags & 0x10) != 0; + // For future use: + //final hasFeature1 = (flags & 0x20) != 0; + //final hasFeature2 = (flags & 0x40) != 0; + hasName = (flags & 0x80) != 0; + if (hasLocation && packet.remaining >= 8) { + latitude = packet.readInt32LE() / 1e6; + longitude = packet.readInt32LE() / 1e6; + } + if (hasName && packet.remaining > 0) { + name = packet.readString(); + } + } catch (e) { + appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); + return; + } + + importDiscoveredContact( + DiscoveryContact( + rawPacket: frame, + publicKey: publicKey, + name: name, + type: type, + pathLength: pathBytes.length, + path: Uint8List.fromList( + pathBytes.reversed.toList(), + ), // Store path in reverse for easier use in outgoing messages + latitude: latitude, + longitude: longitude, + lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + ), + ); + } + void _handlePayloadAdvertReceived( Uint8List rawPacket, Uint8List payload, @@ -3982,7 +4046,11 @@ class MeshCoreConnector extends ChangeNotifier { } } - void _handleDiscovery(Contact contact, Uint8List rawPacket) { + void _handleDiscovery( + Contact contact, + Uint8List rawPacket, { + bool noNotify = false, + }) { debugPrint('Discovered new contact: ${contact.name}'); final existingIndex = _discoveredContacts.indexWhere( @@ -4022,7 +4090,7 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(_persistDiscoveredContacts()); // Show notification for new contact (advertisement) - if (_appSettingsService != null) { + if (_appSettingsService != null && !noNotify) { final settings = _appSettingsService!.settings; if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { _notificationService.showAdvertNotification( @@ -4033,6 +4101,11 @@ class MeshCoreConnector extends ChangeNotifier { } } } + + void removeAllDiscoveredContacts() { + _discoveredContacts.clear(); + notifyListeners(); + } } const int _phRouteMask = 0x03; diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index cf33d3d..d8c6821 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_copyContact": "Копирай контакт в клипборда", "discoveredContacts_deleteContact": "Изтрий контакт", "discoveredContacts_addContact": "Добави контакт", - "contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен." + "contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.", + "discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти", + "discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?", + "common_deleteAll": "Изтрий всичко" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0029f4c..690dc6c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1849,5 +1849,8 @@ "discoveredContacts_copyContact": "Kontakt in die Zwischenablage kopieren", "contactsSettings_overwriteOldestTitle": "Überschreiben des Ältesten", "contactsSettings_autoAddSensorsSubtitle": "Ermöglichen Sie dem Begleiter, automatisch entdeckte Sensoren hinzuzufügen", - "contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt." + "contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.", + "common_deleteAll": "Alles löschen", + "discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?", + "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 66cf2a2..0eb6173 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -10,6 +10,7 @@ "common_unknownDevice": "Unknown Device", "common_save": "Save", "common_delete": "Delete", + "common_deleteAll": "Delete All", "common_close": "Close", "common_edit": "Edit", "common_add": "Add", @@ -1859,5 +1860,7 @@ "discoveredContacts_contactAdded": "Contact added", "discoveredContacts_addContact": "Add Contact", "discoveredContacts_copyContact": "Copy Contact to clipboard", - "discoveredContacts_deleteContact": "Delete Contact" + "discoveredContacts_deleteContact": "Delete Discovered Contact", + "discoveredContacts_deleteContactAll": "Delete All Discovered Contacts", + "discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 54b9c4d..3eb91d9 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1849,5 +1849,8 @@ "discoveredContacts_Title": "Contactos descubiertos", "discoveredContacts_searchHint": "Buscar contactos descubiertos", "discoveredContacts_addContact": "Agregar contacto", - "contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo." + "contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.", + "common_deleteAll": "Eliminar todo", + "discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos", + "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bdfc3e6..faf8b8b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1821,5 +1821,8 @@ "contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.", "discoveredContacts_Title": "Contacts découverts", "discoveredContacts_searchHint": "Rechercher des contacts découverts", - "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé." + "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.", + "common_deleteAll": "Supprimer tout", + "discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts", + "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index ad4a80b..11de963 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_contactAdded": "Contatto aggiunto", "discoveredContacts_deleteContact": "Elimina Contatto", "discoveredContacts_copyContact": "Copia contatto negli appunti", - "contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito." + "contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.", + "common_deleteAll": "Elimina tutto", + "discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?", + "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8b73b19..f44ef17 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -184,6 +184,12 @@ abstract class AppLocalizations { /// **'Delete'** String get common_delete; + /// No description provided for @common_deleteAll. + /// + /// In en, this message translates to: + /// **'Delete All'** + String get common_deleteAll; + /// No description provided for @common_close. /// /// In en, this message translates to: @@ -5510,8 +5516,20 @@ abstract class AppLocalizations { /// No description provided for @discoveredContacts_deleteContact. /// /// In en, this message translates to: - /// **'Delete Contact'** + /// **'Delete Discovered Contact'** String get discoveredContacts_deleteContact; + + /// No description provided for @discoveredContacts_deleteContactAll. + /// + /// In en, this message translates to: + /// **'Delete All Discovered Contacts'** + String get discoveredContacts_deleteContactAll; + + /// No description provided for @discoveredContacts_deleteContactAllContent. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete all discovered contacts?'** + String get discoveredContacts_deleteContactAllContent; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 16712d2..05ce277 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -38,6 +38,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get common_delete => 'Изтрий'; + @override + String get common_deleteAll => 'Изтрий всичко'; + @override String get common_close => 'Затвори'; @@ -3189,4 +3192,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Изтрий контакт'; + + @override + String get discoveredContacts_deleteContactAll => + 'Изтриване на Всички Открити Контакти'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Сигурни ли сте, че искате да изтриете всички открити контакти?'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 64c75cb..47377ee 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -38,6 +38,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get common_delete => 'Löschen'; + @override + String get common_deleteAll => 'Alles löschen'; + @override String get common_close => 'Schließen'; @@ -3200,4 +3203,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Kontakt löschen'; + + @override + String get discoveredContacts_deleteContactAll => + 'Alle entdeckten Kontakte löschen'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 031dd44..8f1fa3d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -38,6 +38,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get common_delete => 'Delete'; + @override + String get common_deleteAll => 'Delete All'; + @override String get common_close => 'Close'; @@ -3137,5 +3140,13 @@ class AppLocalizationsEn extends AppLocalizations { String get discoveredContacts_copyContact => 'Copy Contact to clipboard'; @override - String get discoveredContacts_deleteContact => 'Delete Contact'; + String get discoveredContacts_deleteContact => 'Delete Discovered Contact'; + + @override + String get discoveredContacts_deleteContactAll => + 'Delete All Discovered Contacts'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Are you sure you want to delete all discovered contacts?'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index c516d96..ddd9ff9 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -38,6 +38,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get common_delete => 'Eliminar'; + @override + String get common_deleteAll => 'Eliminar todo'; + @override String get common_close => 'Cerrar'; @@ -3193,4 +3196,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Eliminar contacto'; + + @override + String get discoveredContacts_deleteContactAll => + 'Eliminar Todos los Contactos Descubiertos'; + + @override + String get discoveredContacts_deleteContactAllContent => + '¿Está seguro de que desea eliminar todos los contactos descubiertos!'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1ff8aca..52bf0a3 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -38,6 +38,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get common_delete => 'Supprimer'; + @override + String get common_deleteAll => 'Supprimer tout'; + @override String get common_close => 'Fermer'; @@ -3214,4 +3217,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Supprimer le contact'; + + @override + String get discoveredContacts_deleteContactAll => + 'Supprimer tous les contacts découverts'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a4e6057..8730115 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -38,6 +38,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get common_delete => 'Elimina'; + @override + String get common_deleteAll => 'Elimina tutto'; + @override String get common_close => 'Chiudi'; @@ -3194,4 +3197,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Elimina Contatto'; + + @override + String get discoveredContacts_deleteContactAll => + 'Eliminare tutti i contatti scoperti'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Sei sicuro di voler eliminare tutti i contatti scoperti?'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3deb4c7..93d7f97 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -38,6 +38,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get common_delete => 'Verwijderen'; + @override + String get common_deleteAll => 'Alles verwijderen'; + @override String get common_close => 'Sluiten'; @@ -3180,4 +3183,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Contact verwijderen'; + + @override + String get discoveredContacts_deleteContactAll => + 'Verwijder alle ontdekte contacten'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 78c1f8a..3d5bf54 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -38,6 +38,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get common_delete => 'Usuń'; + @override + String get common_deleteAll => 'Usuń wszystko'; + @override String get common_close => 'Zamknąć'; @@ -3193,4 +3196,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Usuń kontakt'; + + @override + String get discoveredContacts_deleteContactAll => + 'Usuń wszystkie odkryte kontakty'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 58f2284..393f369 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -38,6 +38,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get common_delete => 'Excluir'; + @override + String get common_deleteAll => 'Excluir Tudo'; + @override String get common_close => 'Fechar'; @@ -3190,4 +3193,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Excluir Contato'; + + @override + String get discoveredContacts_deleteContactAll => + 'Excluir Todos os Contatos Descobertos'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Tem certeza de que deseja excluir todos os contatos descobertos?'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 0d5dea5..192cb7f 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -38,6 +38,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get common_delete => 'Удалить'; + @override + String get common_deleteAll => 'Удалить все'; + @override String get common_close => 'Закрыть'; @@ -3202,4 +3205,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Удалить контакт'; + + @override + String get discoveredContacts_deleteContactAll => + 'Удалить Все Обнаруженные Контакты'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Вы уверены, что хотите удалить все обнаруженные контакты?'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index fd4db23..f1076b8 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -38,6 +38,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get common_delete => 'Odstrániť'; + @override + String get common_deleteAll => 'Zmazať všetko'; + @override String get common_close => 'Zavrieť'; @@ -3175,4 +3178,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Zmazať kontakt'; + + @override + String get discoveredContacts_deleteContactAll => + 'Zmazať všetky objavené kontakty'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Ste si istí, že chcete zmazať všetky objavené kontakty?'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 339f30a..469abdb 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -38,6 +38,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get common_delete => 'Izbrisati'; + @override + String get common_deleteAll => 'Izbriši vse'; + @override String get common_close => 'Zapri'; @@ -3179,4 +3182,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Izbriši stik'; + + @override + String get discoveredContacts_deleteContactAll => + 'Izbriši vse odkrite kontakte'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Ste prepričani, da želite izbrisati vse odkrite kontakte?'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 9e32ec8..718d7ad 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -38,6 +38,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get common_delete => 'Radera'; + @override + String get common_deleteAll => 'Ta bort alla'; + @override String get common_close => 'Stänga'; @@ -3158,4 +3161,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Ta bort kontakt'; + + @override + String get discoveredContacts_deleteContactAll => + 'Ta bort alla upptäckta kontakter'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Är du säker på att du vill ta bort alla upptäckta kontakter?'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index fae7e99..9541219 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -38,6 +38,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get common_delete => 'Видалити'; + @override + String get common_deleteAll => 'Видалити все'; + @override String get common_close => 'Закрити'; @@ -3209,4 +3212,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get discoveredContacts_deleteContact => 'Видалити контакт'; + + @override + String get discoveredContacts_deleteContactAll => + 'Видалити всі виявлені контакти'; + + @override + String get discoveredContacts_deleteContactAllContent => + 'Ви впевнені, що хочете видалити всі виявлені контакти?'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 8a3a3a3..17f84ce 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -38,6 +38,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get common_delete => '删除'; + @override + String get common_deleteAll => '删除全部'; + @override String get common_close => '关闭'; @@ -2957,4 +2960,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get discoveredContacts_deleteContact => '删除联系人'; + + @override + String get discoveredContacts_deleteContactAll => '删除所有发现的联系人'; + + @override + String get discoveredContacts_deleteContactAllContent => '您确定要删除所有发现的联系人吗?'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 68e6fc6..67ef891 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_Title": "Ontdekte contacten", "discoveredContacts_contactAdded": "Contact toegevoegd", "discoveredContacts_searchHint": "Ontdekte contacten zoeken", - "contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen." + "contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.", + "common_deleteAll": "Alles verwijderen", + "discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten", + "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 66d55aa..d090cb5 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1821,5 +1821,8 @@ "contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.", "discoveredContacts_noMatching": "Brak pasujących kontaktów", "discoveredContacts_deleteContact": "Usuń kontakt", - "contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony." + "contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.", + "common_deleteAll": "Usuń wszystko", + "discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?", + "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index bbf067b..ceecc40 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_deleteContact": "Excluir Contato", "discoveredContacts_contactAdded": "Contato adicionado", "discoveredContacts_addContact": "Adicionar Contato", - "contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído." + "contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.", + "common_deleteAll": "Excluir Tudo", + "discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos", + "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 21a9cda..fbed74e 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1061,5 +1061,8 @@ "discoveredContacts_addContact": "Добавить контакт", "discoveredContacts_Title": "Обнаруженные контакты", "discoveredContacts_deleteContact": "Удалить контакт", - "contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном." + "contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.", + "common_deleteAll": "Удалить все", + "discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?", + "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 19b217e..85d7bec 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_Title": "Objavené kontakty", "contactsSettings_overwriteOldestTitle": "Prepísať najstaršie", "discoveredContacts_addContact": "Pridať kontakt", - "contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt." + "contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.", + "discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty", + "common_deleteAll": "Zmazať všetko", + "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index aefa817..83bbfa2 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_Title": "Odkriti stiki", "discoveredContacts_searchHint": "Najdeni stiki po iskanju", "discoveredContacts_deleteContact": "Izbriši stik", - "contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan." + "contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.", + "common_deleteAll": "Izbriši vse", + "discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?", + "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b8f29d7..12996e4 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_contactAdded": "Kontakt tillagd", "discoveredContacts_addContact": "Lägg till kontakt", "discoveredContacts_copyContact": "Kopiera kontakt till urklipp", - "contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten." + "contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.", + "common_deleteAll": "Ta bort alla", + "discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?", + "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 275cb13..4b74e37 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1821,5 +1821,8 @@ "discoveredContacts_deleteContact": "Видалити контакт", "discoveredContacts_copyContact": "Копіювати контакт у буфер обміну", "discoveredContacts_addContact": "Додати контакт", - "contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений." + "contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.", + "common_deleteAll": "Видалити все", + "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", + "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 11bbd67..be178bc 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1826,5 +1826,8 @@ "discoveredContacts_noMatching": "没有匹配的联系人", "discoveredContacts_Title": "已发现的联系人", "discoveredContacts_copyContact": "复制联系人到剪贴板", - "contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。" + "contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。", + "common_deleteAll": "删除全部", + "discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?", + "discoveredContacts_deleteContactAll": "删除所有发现的联系人" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 5753785..47bac9c 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -222,7 +222,7 @@ class _ContactsScreenState extends State final bytes = hex2Uint8List(hexString); final importContactFrame = buildImportContactFrame(bytes); _pendingOperations.add(ContactOperationType.import); - await connector.sendFrame(importContactFrame, expectsGenericAck: true); + connector.importContact(importContactFrame); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index dadf632..f122654 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -56,6 +56,25 @@ class _DiscoveryScreenState extends State { subtitle: false, ), centerTitle: true, + actions: [ + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.delete, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.discoveredContacts_deleteContactAll), + ], + ), + onTap: () { + _deleteContacts(context, connector); + }, + ), + ], + icon: const Icon(Icons.more_vert), + ), + ], ), body: Column( children: [ @@ -163,6 +182,30 @@ class _DiscoveryScreenState extends State { } } + void _deleteContacts(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.common_deleteAll), + content: Text(l10n.discoveredContacts_deleteContactAllContent), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + connector.removeAllDiscoveredContacts(); + }, + child: Text(l10n.common_deleteAll), + ), + ], + ), + ); + } + Widget _buildFilters( List filteredAndSorted, MeshCoreConnector connector, From b7d5ee5754f3941db3ba31d79ee51aac7c6a7985 Mon Sep 17 00:00:00 2001 From: ericz Date: Mon, 2 Mar 2026 21:35:16 +0100 Subject: [PATCH 217/421] Allow search for prefix as Displayed in contact list. --- lib/utils/contact_search.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 31def4e..2359793 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -19,6 +19,9 @@ String? _extractHexPrefix(String query) { if (cleaned.startsWith('0x')) { cleaned = cleaned.substring(2); } + if (cleaned.startsWith('<')) { + cleaned = cleaned.substring(1).replaceAll(">", ""); + } cleaned = cleaned.replaceAll(' ', ''); if (cleaned.length < 2) return null; if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; From 647fe1523ef4b0275b0ca63a24a6911fbb267c25 Mon Sep 17 00:00:00 2001 From: ericz Date: Mon, 2 Mar 2026 21:42:44 +0100 Subject: [PATCH 218/421] make it that even combination <0x90 is allowed. --- lib/utils/contact_search.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 2359793..318f307 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -16,12 +16,12 @@ bool matchesContactQuery(Contact contact, String query) { String? _extractHexPrefix(String query) { var cleaned = query; - if (cleaned.startsWith('0x')) { - cleaned = cleaned.substring(2); - } if (cleaned.startsWith('<')) { cleaned = cleaned.substring(1).replaceAll(">", ""); } + if (cleaned.startsWith('0x')) { + cleaned = cleaned.substring(2); + } cleaned = cleaned.replaceAll(' ', ''); if (cleaned.length < 2) return null; if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; From a0efbbe4bd7446e0eb2fa6c6b66269300c039a8d Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 3 Mar 2026 17:44:28 -0800 Subject: [PATCH 219/421] Persist Discovered Contacts when updated --- lib/connector/meshcore_connector.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d71cc75..bc5e654 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4051,7 +4051,7 @@ class MeshCoreConnector extends ChangeNotifier { Uint8List rawPacket, { bool noNotify = false, }) { - debugPrint('Discovered new contact: ${contact.name}'); + appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector'); final existingIndex = _discoveredContacts.indexWhere( (c) => c.publicKeyHex == contact.publicKeyHex, @@ -4071,6 +4071,7 @@ class MeshCoreConnector extends ChangeNotifier { lastSeen: contact.lastSeen, ); notifyListeners(); + unawaited(_persistDiscoveredContacts()); return; } From d53465d13b5b6cfe9d2ae489a2d54e1f2042292e Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 3 Mar 2026 17:57:56 -0800 Subject: [PATCH 220/421] persist discovered contacts when all are removed --- lib/connector/meshcore_connector.dart | 1 + lib/screens/settings_screen.dart | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index bc5e654..76496a8 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4105,6 +4105,7 @@ class MeshCoreConnector extends ChangeNotifier { void removeAllDiscoveredContacts() { _discoveredContacts.clear(); + unawaited(_persistDiscoveredContacts()); notifyListeners(); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 0423517..d6118f5 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -959,11 +959,11 @@ class _SettingsScreenState extends State { void _sendSettings( MeshCoreConnector connector, - autoAddChat, - autoAddRepeater, - autoAddRoomServer, - autoAddSensor, - overwriteOldest, + bool autoAddChat, + bool autoAddRepeater, + bool autoAddRoomServer, + bool autoAddSensor, + bool overwriteOldest, ) async { final frame = buildSetAutoAddConfigFrame( autoAddChat: autoAddChat, From e1253181374f03ad79006ef15749381298da5d7b Mon Sep 17 00:00:00 2001 From: ericz Date: Wed, 4 Mar 2026 21:41:51 +0100 Subject: [PATCH 221/421] Shorten lastSeen for en,de,es,fr --- lib/l10n/app_de.arb | 12 ++++++------ lib/l10n/app_en.arb | 12 ++++++------ lib/l10n/app_es.arb | 10 +++++----- lib/l10n/app_fr.arb | 10 +++++----- lib/l10n/app_localizations.dart | 12 ++++++------ lib/l10n/app_localizations_de.dart | 12 ++++++------ lib/l10n/app_localizations_en.dart | 12 ++++++------ lib/l10n/app_localizations_es.dart | 10 +++++----- lib/l10n/app_localizations_fr.dart | 10 +++++----- 9 files changed, 50 insertions(+), 50 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 07190a9..3e7a485 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -296,8 +296,8 @@ "contacts_filterContacts": "Filtert Kontakte...", "contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter", "contacts_noMembers": "Keine Mitglieder", - "contacts_lastSeenNow": "gerade gesehen", - "contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.", + "contacts_lastSeenNow": "kürzlich", + "contacts_lastSeenMinsAgo": "~ {minutes} Min.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.", - "contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.", + "contacts_lastSeenHourAgo": "~ 1 Std.", + "contacts_lastSeenHoursAgo": "~ {hours} Std.", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag", - "contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor", + "contacts_lastSeenDayAgo": "~ 1 Tag", + "contacts_lastSeenDaysAgo": "~ {days} Tage", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f0b0587..e8b530a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -366,8 +366,8 @@ "contacts_filterContacts": "Filter contacts...", "contacts_noContactsMatchFilter": "No contacts match your filter", "contacts_noMembers": "No members", - "contacts_lastSeenNow": "Last seen now", - "contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago", + "contacts_lastSeenNow": "recently", + "contacts_lastSeenMinsAgo": "- {minutes} min.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -375,8 +375,8 @@ } } }, - "contacts_lastSeenHourAgo": "Last seen 1 hour ago", - "contacts_lastSeenHoursAgo": "Last seen {hours} hours ago", + "contacts_lastSeenHourAgo": "~ 1 hour", + "contacts_lastSeenHoursAgo": "~ {hours} hours", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -384,8 +384,8 @@ } } }, - "contacts_lastSeenDayAgo": "Last seen 1 day ago", - "contacts_lastSeenDaysAgo": "Last seen {days} days ago", + "contacts_lastSeenDayAgo": "~ 1 day", + "contacts_lastSeenDaysAgo": "~ {days} days", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 47765c6..883e9ce 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -297,7 +297,7 @@ "contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro", "contacts_noMembers": "No miembros", "contacts_lastSeenNow": "Última vez que se vio ahora", - "contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.", + "contacts_lastSeenMinsAgo": "~ {minutes} min.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora", - "contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.", + "contacts_lastSeenHourAgo": "~ 1 hora", + "contacts_lastSeenHoursAgo": "~ {hours} horas", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día", - "contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.", + "contacts_lastSeenDayAgo": "~ 1 día", + "contacts_lastSeenDaysAgo": "~ {days} días", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b742dc9..8341221 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -297,7 +297,7 @@ "contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.", "contacts_noMembers": "Aucun membre", "contacts_lastSeenNow": "Vu maintenant", - "contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes", + "contacts_lastSeenMinsAgo": "~ {minutes} min.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Vu il y a 1 heure", - "contacts_lastSeenHoursAgo": "Vu il y a {hours} heures", + "contacts_lastSeenHourAgo": "~ 1 heure", + "contacts_lastSeenHoursAgo": "~ {hours} heures", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Vu il y a 1 jour", - "contacts_lastSeenDaysAgo": "Vu il y a {days} jours", + "contacts_lastSeenDayAgo": "~ 1 jour", + "contacts_lastSeenDaysAgo": "~ {days} jours", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c48994c..bbee680 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1501,37 +1501,37 @@ abstract class AppLocalizations { /// No description provided for @contacts_lastSeenNow. /// /// In en, this message translates to: - /// **'Last seen now'** + /// **'recently'** String get contacts_lastSeenNow; /// No description provided for @contacts_lastSeenMinsAgo. /// /// In en, this message translates to: - /// **'Last seen {minutes} mins ago'** + /// **'- {minutes} min.'** String contacts_lastSeenMinsAgo(int minutes); /// No description provided for @contacts_lastSeenHourAgo. /// /// In en, this message translates to: - /// **'Last seen 1 hour ago'** + /// **'~ 1 hour'** String get contacts_lastSeenHourAgo; /// No description provided for @contacts_lastSeenHoursAgo. /// /// In en, this message translates to: - /// **'Last seen {hours} hours ago'** + /// **'~ {hours} hours'** String contacts_lastSeenHoursAgo(int hours); /// No description provided for @contacts_lastSeenDayAgo. /// /// In en, this message translates to: - /// **'Last seen 1 day ago'** + /// **'~ 1 day'** String get contacts_lastSeenDayAgo; /// No description provided for @contacts_lastSeenDaysAgo. /// /// In en, this message translates to: - /// **'Last seen {days} days ago'** + /// **'~ {days} days'** String contacts_lastSeenDaysAgo(int days); /// No description provided for @channels_title. diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c7eb927..afa7b56 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -777,27 +777,27 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_noMembers => 'Keine Mitglieder'; @override - String get contacts_lastSeenNow => 'gerade gesehen'; + String get contacts_lastSeenNow => 'kürzlich'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Letzte Sichtung vor $minutes Minuten.'; + return '~ $minutes Min.'; } @override - String get contacts_lastSeenHourAgo => 'Letzte Sichtung vor 1 Stunde.'; + String get contacts_lastSeenHourAgo => '~ 1 Std.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Letzte Sichtung vor $hours Stunden.'; + return '~ $hours Std.'; } @override - String get contacts_lastSeenDayAgo => 'Letzte Sichtung vor 1 Tag'; + String get contacts_lastSeenDayAgo => '~ 1 Tag'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Letzte Sichtung $days Tage zuvor'; + return '~ $days Tage'; } @override diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4458062..be8712d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -768,27 +768,27 @@ class AppLocalizationsEn extends AppLocalizations { String get contacts_noMembers => 'No members'; @override - String get contacts_lastSeenNow => 'Last seen now'; + String get contacts_lastSeenNow => 'recently'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Last seen $minutes mins ago'; + return '- $minutes min.'; } @override - String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago'; + String get contacts_lastSeenHourAgo => '~ 1 hour'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Last seen $hours hours ago'; + return '~ $hours hours'; } @override - String get contacts_lastSeenDayAgo => 'Last seen 1 day ago'; + String get contacts_lastSeenDayAgo => '~ 1 day'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Last seen $days days ago'; + return '~ $days days'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ce4b615..ae57c96 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -782,23 +782,23 @@ class AppLocalizationsEs extends AppLocalizations { @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Última vez visto hace $minutes minutos.'; + return '~ $minutes min.'; } @override - String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora'; + String get contacts_lastSeenHourAgo => '~ 1 hora'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Última vez visto hace $hours horas.'; + return '~ $hours horas'; } @override - String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día'; + String get contacts_lastSeenDayAgo => '~ 1 día'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Última vez visto hace $days días.'; + return '~ $days días'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6118444..d3adcfa 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -784,23 +784,23 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Vu il y a $minutes minutes'; + return '~ $minutes min.'; } @override - String get contacts_lastSeenHourAgo => 'Vu il y a 1 heure'; + String get contacts_lastSeenHourAgo => '~ 1 heure'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Vu il y a $hours heures'; + return '~ $hours heures'; } @override - String get contacts_lastSeenDayAgo => 'Vu il y a 1 jour'; + String get contacts_lastSeenDayAgo => '~ 1 jour'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Vu il y a $days jours'; + return '~ $days jours'; } @override From 3502559fae1d716c3a1fbccda0448050855a2ca0 Mon Sep 17 00:00:00 2001 From: ericz Date: Wed, 4 Mar 2026 22:49:20 +0100 Subject: [PATCH 222/421] minus to tilde --- lib/l10n/app_en.arb | 2 +- lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e8b530a..42337ba 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -367,7 +367,7 @@ "contacts_noContactsMatchFilter": "No contacts match your filter", "contacts_noMembers": "No members", "contacts_lastSeenNow": "recently", - "contacts_lastSeenMinsAgo": "- {minutes} min.", + "contacts_lastSeenMinsAgo": "~ {minutes} min.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bbee680..64d9f76 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1507,7 +1507,7 @@ abstract class AppLocalizations { /// No description provided for @contacts_lastSeenMinsAgo. /// /// In en, this message translates to: - /// **'- {minutes} min.'** + /// **'~ {minutes} min.'** String contacts_lastSeenMinsAgo(int minutes); /// No description provided for @contacts_lastSeenHourAgo. diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index be8712d..1e93c41 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -772,7 +772,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String contacts_lastSeenMinsAgo(int minutes) { - return '- $minutes min.'; + return '~ $minutes min.'; } @override diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e..4084d9b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From 7d8e0497451b02ce0f163bf19158360925c44b62 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Wed, 4 Mar 2026 22:56:39 -0800 Subject: [PATCH 223/421] Enhance message parsing and error handling in MeshCoreConnector (#260) * Enhance readString method to include Latin-1 fallback for decoding errors * Refactor _parseContactMessage to improve error handling and message parsing logic * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 137 +++++++++++++++----------- lib/connector/meshcore_protocol.dart | 10 +- 2 files changed, 88 insertions(+), 59 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index cf147bd..c57a85a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2459,70 +2459,93 @@ class MeshCoreConnector extends ChangeNotifier { } Message? _parseContactMessage(Uint8List frame) { - if (frame.isEmpty) return null; - final code = frame[0]; - if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) { + if (frame.isEmpty) { + appLogger.warn('Received empty frame, ignoring'); return null; } + final reader = BufferReader(frame); - // Companion radio layout: - // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...] - final prefixOffset = code == respCodeContactMsgRecvV3 ? 4 : 1; - const prefixLen = 6; - final pathLenOffset = prefixOffset + prefixLen; - final txtTypeOffset = pathLenOffset + 1; - final timestampOffset = txtTypeOffset + 1; - final baseTextOffset = timestampOffset + 4; + try { + final code = reader.readByte(); + if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) { + appLogger.warn( + 'Unexpected message code: $code, expected contact message receive codes', + ); + return null; + } - if (frame.length <= baseTextOffset) return null; - final fourBytePubMSG = frame.sublist(baseTextOffset, baseTextOffset + 4); - final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen); - final flags = frame[txtTypeOffset]; - final shiftedType = flags >> 2; - final rawType = flags; - final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain; - final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData; - if (!isPlain && !isCli) { - return null; - } + // Companion radio layout: + // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...] + // double snr = 0; + if (code == respCodeContactMsgRecvV3) { + // Older firmware layout with SNR as a signed byte after the code + // snr = reader.readInt8().toDouble() * 4; // SNR in dB, scaled by 4 + reader.skipBytes(1); // Skip SNR byte + reader.skipBytes(2); // Skip reserved bytes + } - // Try base text offset; if empty and there is room for the optional 4-byte extra - // (used by signed/plain variants), try again skipping those bytes. - var text = readCString( - frame, - baseTextOffset, - frame.length - baseTextOffset, - ); - if (text.isEmpty && frame.length > baseTextOffset + 4) { - text = readCString( - frame, - baseTextOffset + 4, - frame.length - (baseTextOffset + 4), + final senderPrefix = reader.readBytes(6); + final pathLength = reader.readByte(); + final txtType = reader.readByte(); + final timestampRaw = reader.readUInt32LE(); + final timestamp = DateTime.fromMillisecondsSinceEpoch( + timestampRaw * 1000, ); + + if (txtType == 2) { + reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants + } + + final msgText = reader.readString(); + + final flags = txtType; + final shiftedType = flags >> 2; + final rawType = flags; + final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain; + final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData; + if (!isPlain && !isCli) { + appLogger.warn( + 'Unknown message type received: txtType=$txtType, shifted=$shiftedType, raw=$rawType', + ); + return null; + } + + if (msgText.isEmpty) { + appLogger.warn('Received message with empty text, ignoring'); + return null; + } + final decodedText = isCli + ? msgText + : (Smaz.tryDecodePrefixed(msgText) ?? msgText); + + final contact = _contacts.cast().firstWhere( + (c) => c != null && _matchesPrefix(c.publicKey, senderPrefix), + orElse: () => null, + ); + if (contact == null) { + appLogger.warn( + 'Received message from unknown contact with prefix: ${senderPrefix.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join('')}', + ); + return null; + } + + return Message( + senderKey: contact.publicKey, + text: decodedText, + timestamp: timestamp, + isOutgoing: false, + isCli: isCli, + status: MessageStatus.delivered, + pathLength: pathLength == 0xFF ? 0 : pathLength, + pathBytes: Uint8List(0), + fourByteRoomContactKey: msgText.length >= 4 + ? Uint8List.fromList(msgText.substring(0, 4).codeUnits) + : null, + ); + } catch (e) { + appLogger.warn('Error parsing contact direct message: $e'); + return null; } - if (text.isEmpty) return null; - final decodedText = isCli ? text : (Smaz.tryDecodePrefixed(text) ?? text); - - final timestampRaw = readUint32LE(frame, timestampOffset); - final pathLenByte = frame[pathLenOffset]; - - final contact = _contacts.cast().firstWhere( - (c) => c != null && _matchesPrefix(c.publicKey, senderPrefix), - orElse: () => null, - ); - if (contact == null) return null; - - return Message( - senderKey: contact.publicKey, - text: decodedText, - timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), - isOutgoing: false, - isCli: isCli, - status: MessageStatus.delivered, - pathLength: pathLenByte == 0xFF ? 0 : pathLenByte, - pathBytes: Uint8List(0), - fourByteRoomContactKey: fourBytePubMSG, - ); } bool _matchesPrefix(Uint8List fullKey, Uint8List prefix) { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index d5ce9ee..938a274 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -34,8 +34,14 @@ class BufferReader { Uint8List readRemainingBytes() => readBytes(remaining); - String readString() => - utf8.decode(readRemainingBytes(), allowMalformed: true); + String readString() { + final value = readRemainingBytes(); + try { + return utf8.decode(Uint8List.fromList(value), allowMalformed: true); + } catch (e) { + return String.fromCharCodes(value); // Latin-1 fallback + } + } String readCString(int maxLength) { final value = []; From 22a53439b1b7b0a103795165ec5636159b2e86c5 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:08:51 -0500 Subject: [PATCH 224/421] Initialize USB Supoport for Andriod and Desktop --- android/app/build.gradle.kts | 3 +- android/app/src/main/AndroidManifest.xml | 1 + .../meshcore/meshcore_open/MainActivity.kt | 310 +++++++++++- android/build.gradle.kts | 1 + lib/connector/meshcore_connector.dart | 151 +++++- lib/l10n/app_bg.arb | 11 +- lib/l10n/app_de.arb | 11 +- lib/l10n/app_en.arb | 9 + lib/l10n/app_es.arb | 11 +- lib/l10n/app_fr.arb | 11 +- lib/l10n/app_it.arb | 11 +- lib/l10n/app_localizations.dart | 54 +++ lib/l10n/app_localizations_bg.dart | 31 ++ lib/l10n/app_localizations_de.dart | 32 ++ lib/l10n/app_localizations_en.dart | 31 ++ lib/l10n/app_localizations_es.dart | 32 ++ lib/l10n/app_localizations_fr.dart | 32 ++ lib/l10n/app_localizations_it.dart | 32 ++ lib/l10n/app_localizations_nl.dart | 31 ++ lib/l10n/app_localizations_pl.dart | 31 ++ lib/l10n/app_localizations_pt.dart | 31 ++ lib/l10n/app_localizations_ru.dart | 32 ++ lib/l10n/app_localizations_sk.dart | 31 ++ lib/l10n/app_localizations_sl.dart | 31 ++ lib/l10n/app_localizations_sv.dart | 31 ++ lib/l10n/app_localizations_uk.dart | 32 ++ lib/l10n/app_localizations_zh.dart | 27 ++ lib/l10n/app_nl.arb | 11 +- lib/l10n/app_pl.arb | 11 +- lib/l10n/app_pt.arb | 11 +- lib/l10n/app_ru.arb | 11 +- lib/l10n/app_sk.arb | 11 +- lib/l10n/app_sl.arb | 11 +- lib/l10n/app_sv.arb | 11 +- lib/l10n/app_uk.arb | 11 +- lib/l10n/app_zh.arb | 11 +- lib/main.dart | 4 +- lib/screens/connection_choice_screen.dart | 201 ++++++++ lib/screens/scanner_screen.dart | 24 +- lib/screens/usb_screen.dart | 456 ++++++++++++++++++ lib/services/usb_serial_service.dart | 284 +++++++++++ linux/flutter/generated_plugins.cmake | 1 + pubspec.yaml | 1 + windows/CMakeLists.txt | 8 +- windows/flutter/generated_plugins.cmake | 1 + 45 files changed, 2083 insertions(+), 47 deletions(-) create mode 100644 lib/screens/connection_choice_screen.dart create mode 100644 lib/screens/usb_screen.dart create mode 100644 lib/services/usb_serial_service.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e0a8029..2e8f47f 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) { android { namespace = "com.meshcore.meshcore_open" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "29.0.14206865" compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -84,4 +84,5 @@ flutter { dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") + implementation("com.github.mik3y:usb-serial-for-android:3.9.0") } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b8dd623..4ff626f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + + when (call.method) { + "listPorts" -> result.success(listUsbPorts()) + "connect" -> handleUsbConnect(call, result) + "write" -> handleUsbWrite(call, result) + "disconnect" -> { + closeUsbConnection() + result.success(null) + } + else -> result.notImplemented() + } + } + + EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName) + .setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + }, + ) + } + + override fun onDestroy() { + closeUsbConnection() + unregisterReceiver(permissionReceiver) + super.onDestroy() + } + + private fun registerUsbPermissionReceiver() { + val filter = IntentFilter(usbPermissionAction) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(permissionReceiver, filter, RECEIVER_NOT_EXPORTED) + } else { + @Suppress("DEPRECATION") + registerReceiver(permissionReceiver, filter) + } + } + + private fun listUsbPorts(): List { + val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager) + return drivers.map { driver -> + val device = driver.device + val productName = device.productName ?: "USB Serial Device" + val vendorProduct = + String.format( + Locale.US, + "VID:%04X PID:%04X", + device.vendorId, + device.productId, + ) + "${device.deviceName} - $productName - $vendorProduct" + } + } + + private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) { + val portName = call.argument("portName") + val baudRate = call.argument("baudRate") ?: 115200 + if (portName.isNullOrBlank()) { + result.error("usb_invalid_port", "Port name is required", null) + return + } + + val device = findUsbDevice(portName) + if (device == null) { + result.error("usb_device_missing", "USB device not found for $portName", null) + return + } + + if (usbManager.hasPermission(device)) { + openUsbDevice(device, baudRate, result) + return + } + + if (pendingConnectResult != null) { + result.error("usb_busy", "Another USB permission request is already pending", null) + return + } + + pendingConnectResult = result + pendingConnectPortName = portName + pendingConnectBaudRate = baudRate + + val permissionIntent = PendingIntent.getBroadcast( + this, + 0, + Intent(usbPermissionAction).setPackage(packageName), + pendingIntentFlags(), + ) + usbManager.requestPermission(device, permissionIntent) + } + + private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) { + val data = call.argument("data") + val port = usbPort + if (data == null) { + result.error("usb_invalid_data", "Data is required", null) + return + } + if (port == null) { + result.error("usb_not_connected", "USB serial port is not connected", null) + return + } + + try { + port.write(data, 1000) + result.success(null) + } catch (error: Exception) { + result.error("usb_write_failed", error.message, null) + } + } + + private fun findUsbDevice(portName: String): UsbDevice? { + return usbManager.deviceList.values.firstOrNull { it.deviceName == portName } + } + + private fun openUsbDevice( + device: UsbDevice, + baudRate: Int, + result: MethodChannel.Result, + ) { + try { + closeUsbConnection() + + val driver = UsbSerialProber.getDefaultProber().probeDevice(device) + if (driver == null) { + result.error("usb_driver_missing", "No USB serial driver for ${device.deviceName}", null) + return + } + + val connection = usbManager.openDevice(device) + if (connection == null) { + result.error( + "usb_open_failed", + "UsbManager could not open ${device.deviceName}", + null, + ) + return + } + + val port = firstPort(driver) + if (port == null) { + connection.close() + result.error("usb_port_missing", "No USB serial port exposed by ${device.deviceName}", null) + return + } + + port.open(connection) + port.setParameters( + baudRate, + 8, + UsbSerialPort.STOPBITS_1, + UsbSerialPort.PARITY_NONE, + ) + port.rts = false + port.dtr = true + + usbConnection = connection + usbPort = port + + ioManager = + SerialInputOutputManager( + port, + object : SerialInputOutputManager.Listener { + override fun onNewData(data: ByteArray) { + mainHandler.post { + eventSink?.success(data) + } + } + + override fun onRunError(e: Exception) { + mainHandler.post { + eventSink?.error( + "usb_io_error", + e.message ?: "USB serial I/O error", + null, + ) + } + closeUsbConnection() + } + }, + ).also { manager -> + manager.start() + } + + result.success(null) + } catch (error: Exception) { + closeUsbConnection() + result.error("usb_connect_failed", error.message, null) + } + } + + private fun firstPort(driver: UsbSerialDriver): UsbSerialPort? { + return driver.ports.firstOrNull() + } + + private fun closeUsbConnection() { + try { + ioManager?.stop() + } catch (_: Exception) { + } + ioManager = null + + try { + usbPort?.close() + } catch (_: Exception) { + } + usbPort = null + + try { + usbConnection?.close() + } catch (_: Exception) { + } + usbConnection = null + } + + private fun pendingIntentFlags(): Int { + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_MUTABLE + } + return flags + } +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbee657..eeea458 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -2,6 +2,7 @@ allprojects { repositories { google() mavenCentral() + maven(url = "https://jitpack.io") } } diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c57a85a..f514f15 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -20,6 +20,7 @@ import '../services/path_history_service.dart'; import '../services/app_settings_service.dart'; import '../services/background_service.dart'; import '../services/notification_service.dart'; +import '../services/usb_serial_service.dart'; import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; @@ -82,6 +83,8 @@ enum MeshCoreConnectionState { disconnecting, } +enum MeshCoreTransportType { bluetooth, usb } + class RepeaterBatterySnapshot { final int millivolts; final DateTime updatedAt; @@ -108,6 +111,10 @@ class MeshCoreConnector extends ChangeNotifier { String? _lastDeviceId; String? _lastDeviceDisplayName; bool _manualDisconnect = false; + final UsbSerialService _usbSerialService = UsbSerialService(); + StreamSubscription? _usbFrameSubscription; + MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; + String? _activeUsbPort; final List _scanResults = []; final List _contacts = []; @@ -154,6 +161,8 @@ class MeshCoreConnector extends ChangeNotifier { bool _hasLoadedChannels = false; bool _batteryRequested = false; bool _awaitingSelfInfo = false; + bool _hasReceivedDeviceInfo = false; + bool _pendingInitialChannelSync = false; bool _preserveContactsOnRefresh = false; static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; @@ -217,6 +226,12 @@ class MeshCoreConnector extends ChangeNotifier { String? get deviceId => _deviceId; String get deviceIdLabel => _deviceId ?? 'Unknown'; + MeshCoreTransportType get activeTransport => _activeTransport; + String? get activeUsbPort => _activeUsbPort; + bool get isUsbTransportConnected => + _state == MeshCoreConnectionState.connected && + _activeTransport == MeshCoreTransportType.usb; + String get deviceDisplayName { if (_selfName != null && _selfName!.isNotEmpty) { return _selfName!; @@ -742,12 +757,17 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future> listUsbPorts() => _usbSerialService.listPorts(); + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { return; } + _activeTransport = MeshCoreTransportType.bluetooth; + _activeUsbPort = null; + await stopScan(); _setState(MeshCoreConnectionState.connecting); _device = device; @@ -832,6 +852,8 @@ class MeshCoreConnector extends ChangeNotifier { ); _setState(MeshCoreConnectionState.connected); + _hasReceivedDeviceInfo = false; + _pendingInitialChannelSync = true; await _requestDeviceInfo(); _startBatteryPolling(); @@ -845,9 +867,6 @@ class MeshCoreConnector extends ChangeNotifier { // Keep device clock aligned on every connection. await syncTime(); - - // Fetch channels so we can track unread counts for incoming messages - unawaited(getChannels()); } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -855,6 +874,63 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future connectUsb({ + required String portName, + int baudRate = 115200, + }) async { + if (_state == MeshCoreConnectionState.connecting || + _state == MeshCoreConnectionState.connected) { + return; + } + + _activeTransport = MeshCoreTransportType.bluetooth; + _activeUsbPort = null; + + await stopScan(); + _cancelReconnectTimer(); + _manualDisconnect = false; + _activeTransport = MeshCoreTransportType.usb; + _activeUsbPort = portName; + unawaited(_backgroundService?.start()); + _setState(MeshCoreConnectionState.connecting); + + try { + await _usbFrameSubscription?.cancel(); + _usbFrameSubscription = null; + await _usbSerialService.connect(portName: portName, baudRate: baudRate); + await Future.delayed(const Duration(milliseconds: 200)); + _usbFrameSubscription = _usbSerialService.frameStream.listen( + _handleFrame, + onError: (error, stackTrace) { + debugPrint('USB transport error: $error'); + unawaited(disconnect(manual: false)); + }, + onDone: () { + unawaited(disconnect(manual: false)); + }, + ); + + _setState(MeshCoreConnectionState.connected); + _hasReceivedDeviceInfo = false; + _pendingInitialChannelSync = true; + await _requestDeviceInfo(); + _startBatteryPolling(); + final gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + } + + await syncTime(); + } catch (error) { + debugPrint('USB connection error: $error'); + await disconnect(manual: false); + rethrow; + } + } + Future _waitForSelfInfo({required Duration timeout}) async { if (_selfPublicKey != null) return true; if (!isConnected) return false; @@ -886,7 +962,10 @@ class MeshCoreConnector extends ChangeNotifier { return result; } - bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null; + bool get _shouldAutoReconnect => + !_manualDisconnect && + _lastDeviceId != null && + _activeTransport == MeshCoreTransportType.bluetooth; void _cancelReconnectTimer() { _reconnectTimer?.cancel(); @@ -930,6 +1009,7 @@ class MeshCoreConnector extends ChangeNotifier { Future disconnect({bool manual = true}) async { if (_state == MeshCoreConnectionState.disconnecting) return; + final transportAtDisconnect = _activeTransport; if (manual) { _manualDisconnect = true; @@ -941,6 +1021,10 @@ class MeshCoreConnector extends ChangeNotifier { _setState(MeshCoreConnectionState.disconnecting); _stopBatteryPolling(); + await _usbFrameSubscription?.cancel(); + _usbFrameSubscription = null; + await _usbSerialService.disconnect(); + await _notifySubscription?.cancel(); _notifySubscription = null; @@ -980,6 +1064,8 @@ class MeshCoreConnector extends ChangeNotifier { _repeaterBatterySnapshots.clear(); _batteryRequested = false; _awaitingSelfInfo = false; + _hasReceivedDeviceInfo = false; + _pendingInitialChannelSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; @@ -993,8 +1079,11 @@ class MeshCoreConnector extends ChangeNotifier { _pendingGenericAckQueue.clear(); _reactionSendQueueSequence = 0; + _activeTransport = MeshCoreTransportType.bluetooth; + _activeUsbPort = null; + _setState(MeshCoreConnectionState.disconnected); - if (!manual) { + if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) { _scheduleReconnect(); } } @@ -1004,24 +1093,29 @@ class MeshCoreConnector extends ChangeNotifier { String? channelSendQueueId, bool expectsGenericAck = false, }) async { - if (!isConnected || _rxCharacteristic == null) { + if (!isConnected) { throw Exception("Not connected to a MeshCore device"); } - _bleDebugLogService?.logFrame(data, outgoing: true); - // Prefer write without response when supported; fall back to write with response. - final properties = _rxCharacteristic!.properties; - final canWriteWithoutResponse = properties.writeWithoutResponse; - final canWriteWithResponse = properties.write; - if (!canWriteWithoutResponse && !canWriteWithResponse) { - throw Exception("MeshCore RX characteristic does not support write"); + if (_activeTransport == MeshCoreTransportType.usb) { + await _usbSerialService.write(data); + } else { + if (_rxCharacteristic == null) { + throw Exception("MeshCore RX characteristic does not support write"); + } + // Prefer write without response when supported; fall back to write with response. + final properties = _rxCharacteristic!.properties; + final canWriteWithoutResponse = properties.writeWithoutResponse; + final canWriteWithResponse = properties.write; + if (!canWriteWithoutResponse && !canWriteWithResponse) { + throw Exception("MeshCore RX characteristic does not support write"); + } + await _rxCharacteristic!.write( + data.toList(), + withoutResponse: canWriteWithoutResponse, + ); } - - await _rxCharacteristic!.write( - data.toList(), - withoutResponse: canWriteWithoutResponse, - ); _trackPendingGenericAck( data, channelSendQueueId: channelSendQueueId, @@ -2000,10 +2094,12 @@ class MeshCoreConnector extends ChangeNotifier { // Auto-fetch contacts after getting self info getContacts(); + _maybeStartInitialChannelSync(); } void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; + _hasReceivedDeviceInfo = true; _firmwareVerCode = frame[1]; // Parse client_repeat from firmware v9+ (byte 80) @@ -2027,12 +2123,25 @@ class MeshCoreConnector extends ChangeNotifier { if (nextMaxChannels > previousMaxChannels) { unawaited(loadChannelSettings(maxChannels: nextMaxChannels)); unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels)); - if (isConnected) { + if (isConnected && !_pendingInitialChannelSync) { unawaited(getChannels(maxChannels: nextMaxChannels)); } } } notifyListeners(); + _maybeStartInitialChannelSync(); + } + + void _maybeStartInitialChannelSync() { + if (!_pendingInitialChannelSync || !isConnected) { + return; + } + if (_selfPublicKey == null || !_hasReceivedDeviceInfo) { + return; + } + + _pendingInitialChannelSync = false; + unawaited(getChannels(maxChannels: _maxChannels)); } void _handleNoMoreMessages() { @@ -3591,6 +3700,8 @@ class MeshCoreConnector extends ChangeNotifier { _txCharacteristic = null; // Preserve deviceId and displayName for UI display during reconnection // They're only cleared on manual disconnect via disconnect() method + _hasReceivedDeviceInfo = false; + _pendingInitialChannelSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; @@ -3671,10 +3782,12 @@ class MeshCoreConnector extends ChangeNotifier { void dispose() { _scanSubscription?.cancel(); _connectionSubscription?.cancel(); + _usbFrameSubscription?.cancel(); _notifySubscription?.cancel(); _reconnectTimer?.cancel(); _batteryPollTimer?.cancel(); _receivedFramesController.close(); + _usbSerialService.dispose(); // Flush pending unread writes before disposal _unreadStore.flush(); diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 2dbcf5e..0d64508 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1801,5 +1801,14 @@ "contacts_unread": "Непрочетено", "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", "contacts_searchContactsNoNumber": "Търси контакти...", - "contacts_searchUsers": "Търсене на {number}{str} потребители..." + "contacts_searchUsers": "Търсене на {number}{str} потребители...", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceTitle": "Изберете метода на връзка.", + "connectionChoiceSubtitle": "Изберете как искате да получите вашия устройство MeshCore.", + "usbScreenTitle": "Връзката чрез USB ще бъде налична скоро.", + "usbScreenSubtitle": "Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.", + "usbScreenStatus": "Ще бъде достъпно скоро", + "usbScreenNote": "След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.", + "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 07190a9..0c49f8d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1829,5 +1829,14 @@ "contacts_searchRepeaters": "Suche {number}{str} Repeater...", "contacts_searchFavorites": "Suche {number}{str} Favoriten...", "contacts_searchUsers": "Suche {number}{str} Benutzer...", - "contacts_searchRoomServers": "Suche {number}{str} Raumserver..." + "contacts_searchRoomServers": "Suche {number}{str} Raumserver...", + "connectionChoiceSubtitle": "Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceTitle": "Wählen Sie Ihre bevorzugte Verbindungsmethode.", + "usbScreenTitle": "Die USB-Verbindung wird bald verfügbar sein.", + "usbScreenSubtitle": "Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.", + "usbScreenStatus": "Bald verfügbar", + "usbScreenNote": "Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.", + "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f0b0587..9719c7c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -46,6 +46,15 @@ } }, "scanner_title": "MeshCore Open", + "connectionChoiceTitle": "Choose your connection method", + "connectionChoiceSubtitle": "Select how you would like to reach your MeshCore device.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenTitle": "Connect over USB", + "usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.", + "usbScreenStatus": "Select a USB device", + "usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.", + "usbScreenEmptyState": "No USB devices found. Plug one in and refresh.", "scanner_scanning": "Scanning for devices...", "scanner_connecting": "Connecting...", "scanner_disconnecting": "Disconnecting...", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 47765c6..48431e3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1829,5 +1829,14 @@ "contacts_searchFavorites": "Buscar {number}{str} Favoritos...", "contacts_searchUsers": "Buscar {number}{str} Usuarios...", "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", - "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..." + "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...", + "connectionChoiceTitle": "Seleccione su método de conexión.", + "connectionChoiceSubtitle": "Seleccione la forma en que desea acceder a su dispositivo MeshCore.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "La conexión USB estará disponible próximamente.", + "usbScreenSubtitle": "Estamos creando una conexión en serie para dispositivos Android y de escritorio.", + "usbScreenStatus": "Próximamente", + "usbScreenNote": "Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.", + "usbScreenEmptyState": "No se detectaron dispositivos USB. Conecte uno y vuelva a intentar." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b742dc9..61f6551 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1801,5 +1801,14 @@ "contacts_searchUsers": "Rechercher {number}{str} utilisateurs...", "contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...", "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", - "contacts_searchContactsNoNumber": "Rechercher des contacts..." + "contacts_searchContactsNoNumber": "Rechercher des contacts...", + "connectionChoiceTitle": "Choisissez votre méthode de connexion.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceSubtitle": "Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.", + "usbScreenTitle": "La connexion USB sera disponible prochainement.", + "usbScreenSubtitle": "Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.", + "usbScreenStatus": "Bientôt", + "usbScreenNote": "Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.", + "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Connectez-en un et rafraîchissez." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 82adad8..827d1e7 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1801,5 +1801,14 @@ "contacts_searchFavorites": "Cerca {number}{str} Preferiti...", "contacts_unread": "Non letti", "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", - "contacts_searchRoomServers": "Cerca {number}{str} server Room..." + "contacts_searchRoomServers": "Cerca {number}{str} server Room...", + "connectionChoiceTitle": "Scegli il metodo di connessione che preferisci.", + "connectionChoiceSubtitle": "Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "La connessione USB sarà disponibile a breve.", + "usbScreenSubtitle": "Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.", + "usbScreenStatus": "Arriverà presto", + "usbScreenNote": "Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.", + "usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e riavviare." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c48994c..162b760 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -316,6 +316,60 @@ abstract class AppLocalizations { /// **'MeshCore Open'** String get scanner_title; + /// No description provided for @connectionChoiceTitle. + /// + /// In en, this message translates to: + /// **'Choose your connection method'** + String get connectionChoiceTitle; + + /// No description provided for @connectionChoiceSubtitle. + /// + /// In en, this message translates to: + /// **'Select how you would like to reach your MeshCore device.'** + String get connectionChoiceSubtitle; + + /// No description provided for @connectionChoiceUsbLabel. + /// + /// In en, this message translates to: + /// **'USB'** + String get connectionChoiceUsbLabel; + + /// No description provided for @connectionChoiceBluetoothLabel. + /// + /// In en, this message translates to: + /// **'Bluetooth'** + String get connectionChoiceBluetoothLabel; + + /// No description provided for @usbScreenTitle. + /// + /// In en, this message translates to: + /// **'Connect over USB'** + String get usbScreenTitle; + + /// No description provided for @usbScreenSubtitle. + /// + /// In en, this message translates to: + /// **'Choose a detected serial device and connect directly to your MeshCore node.'** + String get usbScreenSubtitle; + + /// No description provided for @usbScreenStatus. + /// + /// In en, this message translates to: + /// **'Select a USB device'** + String get usbScreenStatus; + + /// No description provided for @usbScreenNote. + /// + /// In en, this message translates to: + /// **'USB serial is active on supported Android devices and desktop platforms.'** + String get usbScreenNote; + + /// No description provided for @usbScreenEmptyState. + /// + /// In en, this message translates to: + /// **'No USB devices found. Plug one in and refresh.'** + String get usbScreenEmptyState; + /// No description provided for @scanner_scanning. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index c168b7c..861bf6a 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -108,6 +108,37 @@ class AppLocalizationsBg extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Изберете метода на връзка.'; + + @override + String get connectionChoiceSubtitle => + 'Изберете как искате да получите вашия устройство MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'Връзката чрез USB ще бъде налична скоро.'; + + @override + String get usbScreenSubtitle => + 'Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.'; + + @override + String get usbScreenStatus => 'Ще бъде достъпно скоро'; + + @override + String get usbScreenNote => + 'След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Няма открити USB устройства. Включете едно и опитайте отново.'; + @override String get scanner_scanning => 'Сканиране за устройства...'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c7eb927..f768dbb 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -108,6 +108,38 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => + 'Wählen Sie Ihre bevorzugte Verbindungsmethode.'; + + @override + String get connectionChoiceSubtitle => + 'Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'Die USB-Verbindung wird bald verfügbar sein.'; + + @override + String get usbScreenSubtitle => + 'Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.'; + + @override + String get usbScreenStatus => 'Bald verfügbar'; + + @override + String get usbScreenNote => + 'Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.'; + + @override + String get usbScreenEmptyState => + 'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.'; + @override String get scanner_scanning => 'Scannen nach Geräten...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4458062..2c287f7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -108,6 +108,37 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Choose your connection method'; + + @override + String get connectionChoiceSubtitle => + 'Select how you would like to reach your MeshCore device.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'Connect over USB'; + + @override + String get usbScreenSubtitle => + 'Choose a detected serial device and connect directly to your MeshCore node.'; + + @override + String get usbScreenStatus => 'Select a USB device'; + + @override + String get usbScreenNote => + 'USB serial is active on supported Android devices and desktop platforms.'; + + @override + String get usbScreenEmptyState => + 'No USB devices found. Plug one in and refresh.'; + @override String get scanner_scanning => 'Scanning for devices...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ce4b615..32680ab 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -108,6 +108,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Seleccione su método de conexión.'; + + @override + String get connectionChoiceSubtitle => + 'Seleccione la forma en que desea acceder a su dispositivo MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => + 'La conexión USB estará disponible próximamente.'; + + @override + String get usbScreenSubtitle => + 'Estamos creando una conexión en serie para dispositivos Android y de escritorio.'; + + @override + String get usbScreenStatus => 'Próximamente'; + + @override + String get usbScreenNote => + 'Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.'; + + @override + String get usbScreenEmptyState => + 'No se detectaron dispositivos USB. Conecte uno y vuelva a intentar.'; + @override String get scanner_scanning => 'Escaneando dispositivos...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6118444..dae3478 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -108,6 +108,38 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Choisissez votre méthode de connexion.'; + + @override + String get connectionChoiceSubtitle => + 'Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => + 'La connexion USB sera disponible prochainement.'; + + @override + String get usbScreenSubtitle => + 'Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.'; + + @override + String get usbScreenStatus => 'Bientôt'; + + @override + String get usbScreenNote => + 'Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Aucun périphérique USB n\'a été trouvé. Connectez-en un et rafraîchissez.'; + @override String get scanner_scanning => 'Recherche de périphériques...'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 20873b9..b138671 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -108,6 +108,38 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => + 'Scegli il metodo di connessione che preferisci.'; + + @override + String get connectionChoiceSubtitle => + 'Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'La connessione USB sarà disponibile a breve.'; + + @override + String get usbScreenSubtitle => + 'Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.'; + + @override + String get usbScreenStatus => 'Arriverà presto'; + + @override + String get usbScreenNote => + 'Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Nessun dispositivo USB rilevato. Collegare uno e riavviare.'; + @override String get scanner_scanning => 'Scansione in corso per i dispositivi...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 323981d..f582abc 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -108,6 +108,37 @@ class AppLocalizationsNl extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Kies uw verbindingsmethode'; + + @override + String get connectionChoiceSubtitle => + 'Kies hoe u uw MeshCore-apparaat wilt bereiken.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'USB-verbinding is binnenkort beschikbaar.'; + + @override + String get usbScreenSubtitle => + 'We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.'; + + @override + String get usbScreenStatus => 'Komende week'; + + @override + String get usbScreenNote => + 'Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.'; + + @override + String get usbScreenEmptyState => + 'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.'; + @override String get scanner_scanning => 'Scannen naar apparaten...'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index e033359..2f103b5 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -108,6 +108,37 @@ class AppLocalizationsPl extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Wybierz metodę połączenia.'; + + @override + String get connectionChoiceSubtitle => + 'Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'Połączenie USB będzie dostępne wkrótce.'; + + @override + String get usbScreenSubtitle => + 'Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.'; + + @override + String get usbScreenStatus => 'Wkrótce'; + + @override + String get usbScreenNote => + 'Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.'; + @override String get scanner_scanning => 'Skanowanie urządzeń...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index dd19452..283bada 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -108,6 +108,37 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Escolha o método de conexão desejado.'; + + @override + String get connectionChoiceSubtitle => + 'Selecione a forma como você deseja acessar seu dispositivo MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'A conexão USB estará disponível em breve.'; + + @override + String get usbScreenSubtitle => + 'Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.'; + + @override + String get usbScreenStatus => 'Em breve'; + + @override + String get usbScreenNote => + 'Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Nenhum dispositivo USB encontrado. Conecte um e atualize.'; + @override String get scanner_scanning => 'Procurando por dispositivos...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5f5591d..1687782 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -108,6 +108,38 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Выберите способ подключения'; + + @override + String get connectionChoiceSubtitle => + 'Выберите, каким способом вы хотите получить свой устройство MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => + 'Подключение через USB будет доступно в ближайшее время.'; + + @override + String get usbScreenSubtitle => + 'Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.'; + + @override + String get usbScreenStatus => 'Скоро'; + + @override + String get usbScreenNote => + 'Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Не обнаружено никаких устройств USB. Подключите одно из них и обновите список.'; + @override String get scanner_scanning => 'Поиск устройств...'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 82e4d06..8949090 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -108,6 +108,37 @@ class AppLocalizationsSk extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Vyberte si metódu prepojenia.'; + + @override + String get connectionChoiceSubtitle => + 'Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'Pripojenie cez USB bude k dispozícii čoskoro.'; + + @override + String get usbScreenSubtitle => + 'Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.'; + + @override + String get usbScreenStatus => 'Čoskoro'; + + @override + String get usbScreenNote => + 'Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.'; + @override String get scanner_scanning => 'Skrívania zariadení...'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 9b1bfc9..bd0c4d5 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -108,6 +108,37 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Izberite svoj način povezave.'; + + @override + String get connectionChoiceSubtitle => + 'Izberite, kako želite dostopati do svojega naprave MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'Vnos preko USB-ja bo v kratkem na voljo.'; + + @override + String get usbScreenSubtitle => + 'Gradimo pot za serijsko povezavo za Android in računalnike.'; + + @override + String get usbScreenStatus => 'Čez kratko časa'; + + @override + String get usbScreenNote => + 'Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite.'; + @override String get scanner_scanning => 'Skeniram za naprave...'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index ddfcf41..183250d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -108,6 +108,37 @@ class AppLocalizationsSv extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Välj din anslutningsmetod'; + + @override + String get connectionChoiceSubtitle => + 'Välj hur du vill komma åt din MeshCore-enhet.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => 'USB-anslutning kommer snart'; + + @override + String get usbScreenSubtitle => + 'Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.'; + + @override + String get usbScreenStatus => 'Kommer snart'; + + @override + String get usbScreenNote => + 'När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.'; + + @override + String get usbScreenEmptyState => + 'Inga USB-enheter hittades. Anslut en och uppdatera.'; + @override String get scanner_scanning => 'Söker efter enheter...'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index b44a7cb..19feaac 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -108,6 +108,38 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; + @override + String get connectionChoiceTitle => 'Виберіть спосіб зв\'язку'; + + @override + String get connectionChoiceSubtitle => + 'Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => 'Bluetooth'; + + @override + String get usbScreenTitle => + 'Підключення через USB буде доступне найближчим часом.'; + + @override + String get usbScreenSubtitle => + 'Ми створюємо серійний шлях з\'єднання для Android та десктопних комп\'ютерів.'; + + @override + String get usbScreenStatus => 'Скоро'; + + @override + String get usbScreenNote => + 'Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.'; + + @override + String get usbScreenEmptyState => + 'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.'; + @override String get scanner_scanning => 'Пошук пристроїв...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b3d85ed..fdb9f1d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -108,6 +108,33 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scanner_title => '连接设备'; + @override + String get connectionChoiceTitle => '选择您的连接方式'; + + @override + String get connectionChoiceSubtitle => '请选择您希望如何访问 MeshCore 设备的选项。'; + + @override + String get connectionChoiceUsbLabel => 'USB'; + + @override + String get connectionChoiceBluetoothLabel => '蓝牙'; + + @override + String get usbScreenTitle => 'USB 连接即将推出'; + + @override + String get usbScreenSubtitle => '我们正在构建一个基于串行的连接路径,用于Android和桌面设备。'; + + @override + String get usbScreenStatus => '即将推出'; + + @override + String get usbScreenNote => '一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。'; + + @override + String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。'; + @override String get scanner_scanning => '正在搜索设备...'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 35cb375..2f09405 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1801,5 +1801,14 @@ "contacts_searchContactsNoNumber": "Zoek contacten...", "contacts_searchUsers": "Zoek {number}{str} gebruikers...", "contacts_searchFavorites": "Zoek {number}{str} favorieten...", - "contacts_searchRoomServers": "Zoek {number}{str} Room servers..." + "contacts_searchRoomServers": "Zoek {number}{str} Room servers...", + "connectionChoiceTitle": "Kies uw verbindingsmethode", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenTitle": "USB-verbinding is binnenkort beschikbaar.", + "usbScreenSubtitle": "We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.", + "usbScreenStatus": "Komende week", + "usbScreenNote": "Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.", + "usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 23c4cbc..8988e02 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1801,5 +1801,14 @@ "contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...", "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", - "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..." + "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceSubtitle": "Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.", + "connectionChoiceTitle": "Wybierz metodę połączenia.", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "Połączenie USB będzie dostępne wkrótce.", + "usbScreenSubtitle": "Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.", + "usbScreenStatus": "Wkrótce", + "usbScreenNote": "Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.", + "usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 05792c6..296590f 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1801,5 +1801,14 @@ "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", "contacts_searchContactsNoNumber": "Pesquisar Contatos...", "contacts_unread": "Não lido", - "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..." + "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...", + "connectionChoiceSubtitle": "Selecione a forma como você deseja acessar seu dispositivo MeshCore.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceTitle": "Escolha o método de conexão desejado.", + "usbScreenTitle": "A conexão USB estará disponível em breve.", + "usbScreenSubtitle": "Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.", + "usbScreenStatus": "Em breve", + "usbScreenNote": "Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.", + "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 9460f44..65b3792 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1041,5 +1041,14 @@ "contacts_unread": "Непрочитанное", "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", "contacts_searchFavorites": "Поиск {number}{str} избранного...", - "contacts_searchUsers": "Поиск {number}{str} пользователей..." + "contacts_searchUsers": "Поиск {number}{str} пользователей...", + "connectionChoiceSubtitle": "Выберите, каким способом вы хотите получить свой устройство MeshCore.", + "connectionChoiceTitle": "Выберите способ подключения", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenTitle": "Подключение через USB будет доступно в ближайшее время.", + "usbScreenSubtitle": "Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.", + "usbScreenStatus": "Скоро", + "usbScreenNote": "Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.", + "usbScreenEmptyState": "Не обнаружено никаких устройств USB. Подключите одно из них и обновите список." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 5bc00c6..7178166 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1801,5 +1801,14 @@ "contacts_searchRepeaters": "Hľadať {number}{str} opakovače...", "contacts_searchUsers": "Hľadať {number}{str} používateľov...", "contacts_searchContactsNoNumber": "Hľadať kontakty...", - "contacts_unread": "Neprečítané" + "contacts_unread": "Neprečítané", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceTitle": "Vyberte si metódu prepojenia.", + "connectionChoiceSubtitle": "Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.", + "usbScreenTitle": "Pripojenie cez USB bude k dispozícii čoskoro.", + "usbScreenSubtitle": "Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.", + "usbScreenStatus": "Čoskoro", + "usbScreenNote": "Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.", + "usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 226a715..416106a 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1801,5 +1801,14 @@ "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", "contacts_searchContactsNoNumber": "Iskanje stikov...", "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", - "contacts_searchUsers": "Išči {number}{str} uporabnikov..." + "contacts_searchUsers": "Išči {number}{str} uporabnikov...", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceTitle": "Izberite svoj način povezave.", + "connectionChoiceSubtitle": "Izberite, kako želite dostopati do svojega naprave MeshCore.", + "usbScreenTitle": "Vnos preko USB-ja bo v kratkem na voljo.", + "usbScreenSubtitle": "Gradimo pot za serijsko povezavo za Android in računalnike.", + "usbScreenStatus": "Čez kratko časa", + "usbScreenNote": "Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.", + "usbScreenEmptyState": "Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index bfccfff..9bbcd31 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1801,5 +1801,14 @@ "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", "contacts_searchFavorites": "Sök {number}{str} Favoriter...", "contacts_searchUsers": "Sök {number}{str} användare...", - "contacts_searchRoomServers": "Sök {number}{str} Room-servrar..." + "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceSubtitle": "Välj hur du vill komma åt din MeshCore-enhet.", + "connectionChoiceTitle": "Välj din anslutningsmetod", + "usbScreenTitle": "USB-anslutning kommer snart", + "usbScreenSubtitle": "Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.", + "usbScreenStatus": "Kommer snart", + "usbScreenNote": "När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.", + "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 6063bc8..9a9919c 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1801,5 +1801,14 @@ "contacts_searchFavorites": "Пошук {number}{str} улюблених...", "contacts_searchContactsNoNumber": "Пошук контактів...", "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", - "contacts_unread": "Непрочитане" + "contacts_unread": "Непрочитане", + "connectionChoiceSubtitle": "Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceTitle": "Виберіть спосіб зв'язку", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenTitle": "Підключення через USB буде доступне найближчим часом.", + "usbScreenSubtitle": "Ми створюємо серійний шлях з'єднання для Android та десктопних комп'ютерів.", + "usbScreenStatus": "Скоро", + "usbScreenNote": "Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.", + "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index db4953e..660b221 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1806,5 +1806,14 @@ "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", "contacts_searchContactsNoNumber": "搜索联系人...", "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", - "contacts_searchFavorites": "搜索 {number}{str} 收藏..." + "contacts_searchFavorites": "搜索 {number}{str} 收藏...", + "connectionChoiceSubtitle": "请选择您希望如何访问 MeshCore 设备的选项。", + "connectionChoiceBluetoothLabel": "蓝牙", + "connectionChoiceTitle": "选择您的连接方式", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "USB 连接即将推出", + "usbScreenSubtitle": "我们正在构建一个基于串行的连接路径,用于Android和桌面设备。", + "usbScreenStatus": "即将推出", + "usbScreenNote": "一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。", + "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。" } diff --git a/lib/main.dart b/lib/main.dart index 9e53e21..dd503fd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart'; import 'utils/platform_info.dart'; import 'connector/meshcore_connector.dart'; -import 'screens/scanner_screen.dart'; +import 'screens/connection_choice_screen.dart'; import 'services/storage_service.dart'; import 'services/message_retry_service.dart'; import 'services/path_history_service.dart'; @@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget { }, home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) ? const ChromeRequiredScreen() - : const ScannerScreen(), + : const ConnectionChoiceScreen(), ); }, ), diff --git a/lib/screens/connection_choice_screen.dart b/lib/screens/connection_choice_screen.dart new file mode 100644 index 0000000..a2ea183 --- /dev/null +++ b/lib/screens/connection_choice_screen.dart @@ -0,0 +1,201 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import '../l10n/l10n.dart'; +import 'scanner_screen.dart'; +import 'usb_screen.dart'; + +/// Entry point that lets the user choose between USB or Bluetooth. +class ConnectionChoiceScreen extends StatelessWidget { + const ConnectionChoiceScreen({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: FittedBox( + fit: BoxFit.scaleDown, + child: Text(l10n.appTitle, textAlign: TextAlign.center), + ), + centerTitle: true, + automaticallyImplyLeading: false, + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + child: LayoutBuilder( + builder: (context, constraints) { + final availableHeight = constraints.maxHeight.isFinite + ? constraints.maxHeight + : 600.0; + final gap = math.max( + 8.0, + math.min(20.0, availableHeight * 0.035), + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + flex: 3, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + l10n.connectionChoiceTitle, + textAlign: TextAlign.center, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + ), + SizedBox(height: math.max(4.0, gap * 0.5)), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + l10n.connectionChoiceSubtitle, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ], + ), + ), + ), + SizedBox(height: gap), + Expanded( + flex: 4, + child: _ConnectionMethodButton( + icon: Icons.usb, + label: l10n.connectionChoiceUsbLabel, + color: theme.colorScheme.primaryContainer, + iconColor: theme.colorScheme.onPrimaryContainer, + onPressed: () { + debugPrint( + 'ConnectionChoiceScreen: USB selected, opening UsbScreen', + ); + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const UsbScreen()), + ); + }, + ), + ), + SizedBox(height: gap), + Expanded( + flex: 4, + child: _ConnectionMethodButton( + icon: Icons.bluetooth, + label: l10n.connectionChoiceBluetoothLabel, + color: theme.colorScheme.surfaceContainerHighest, + iconColor: theme.colorScheme.onSurfaceVariant, + onPressed: () { + debugPrint( + 'ConnectionChoiceScreen: Bluetooth selected, opening ScannerScreen', + ); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const ScannerScreen(), + ), + ); + }, + ), + ), + ], + ); + }, + ), + ), + ), + ); + } +} + +class _ConnectionMethodButton extends StatelessWidget { + const _ConnectionMethodButton({ + required this.icon, + required this.label, + required this.onPressed, + required this.color, + required this.iconColor, + }); + + final IconData icon; + final String label; + final VoidCallback onPressed; + final Color color; + final Color iconColor; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: color, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), + minimumSize: const Size.fromHeight(0), + ), + onPressed: onPressed, + child: LayoutBuilder( + builder: (context, constraints) { + final availableHeight = constraints.maxHeight.isFinite + ? constraints.maxHeight + : 200.0; + final availableWidth = constraints.maxWidth.isFinite + ? constraints.maxWidth + : 320.0; + final isCompact = availableHeight < 72.0 || availableWidth < 180.0; + final baseGap = isCompact ? 8.0 : 12.0; + final content = Flex( + direction: isCompact ? Axis.horizontal : Axis.vertical, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: isCompact ? 24.0 : 60.0, color: iconColor), + SizedBox( + width: isCompact ? baseGap : 0, + height: isCompact ? 0 : baseGap, + ), + Text( + label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.visible, + style: + (isCompact + ? theme.textTheme.titleMedium + : theme.textTheme.titleLarge) + ?.copyWith(fontWeight: FontWeight.w600), + ), + ], + ); + + return Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: math.max(0, availableWidth - 12), + maxHeight: math.max(0, availableHeight - 12), + ), + child: content, + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 4017408..713239f 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -20,6 +20,7 @@ class ScannerScreen extends StatefulWidget { class _ScannerScreenState extends State { bool _changedNavigation = false; + late final MeshCoreConnector _connector; late final VoidCallback _connectionListener; BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; late StreamSubscription _bluetoothStateSubscription; @@ -27,12 +28,12 @@ class _ScannerScreenState extends State { @override void initState() { super.initState(); - final connector = Provider.of(context, listen: false); + _connector = Provider.of(context, listen: false); _connectionListener = () { - if (connector.state == MeshCoreConnectionState.disconnected) { + if (_connector.state == MeshCoreConnectionState.disconnected) { _changedNavigation = false; - } else if (connector.state == MeshCoreConnectionState.connected && + } else if (_connector.state == MeshCoreConnectionState.connected && !_changedNavigation) { _changedNavigation = true; if (mounted) { @@ -43,7 +44,7 @@ class _ScannerScreenState extends State { } }; - connector.addListener(_connectionListener); + _connector.addListener(_connectionListener); _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen( (state) { @@ -53,7 +54,7 @@ class _ScannerScreenState extends State { }); // Cancel scan if Bluetooth turns off while scanning if (state != BluetoothAdapterState.on) { - unawaited(connector.stopScan()); + unawaited(_connector.stopScan()); } } }, @@ -65,16 +66,25 @@ class _ScannerScreenState extends State { @override void dispose() { - final connector = Provider.of(context, listen: false); - connector.removeListener(_connectionListener); + _connector.removeListener(_connectionListener); unawaited(_bluetoothStateSubscription.cancel()); super.dispose(); } @override Widget build(BuildContext context) { + final canPop = Navigator.of(context).canPop(); return Scaffold( appBar: AppBar( + leading: canPop + ? IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + debugPrint('ScannerScreen: back button pressed'); + Navigator.of(context).maybePop(); + }, + ) + : null, title: AdaptiveAppBarTitle(context.l10n.scanner_title), centerTitle: true, automaticallyImplyLeading: false, diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart new file mode 100644 index 0000000..e542d61 --- /dev/null +++ b/lib/screens/usb_screen.dart @@ -0,0 +1,456 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; +import 'contacts_screen.dart'; + +class UsbScreen extends StatefulWidget { + const UsbScreen({super.key}); + + @override + State createState() => _UsbScreenState(); +} + +class _UsbScreenState extends State { + final List _ports = []; + bool _isLoadingPorts = true; + bool _isConnecting = false; + bool _navigatedToContacts = false; + String? _selectedPort; + String? _errorText; + late final MeshCoreConnector _connector; + late final VoidCallback _connectionListener; + + @override + void initState() { + super.initState(); + _connector = context.read(); + _connectionListener = () { + if (!mounted) return; + if (_connector.state == MeshCoreConnectionState.disconnected) { + _navigatedToContacts = false; + if (_isConnecting) { + setState(() { + _isConnecting = false; + }); + } + } + if (_connector.state == MeshCoreConnectionState.connected && + _connector.isUsbTransportConnected && + !_navigatedToContacts) { + _navigatedToContacts = true; + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const ContactsScreen()), + ); + } + }; + _connector.addListener(_connectionListener); + unawaited(_loadPorts()); + } + + @override + void dispose() { + _connector.removeListener(_connectionListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = context.l10n; + + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + debugPrint('UsbScreen: back button pressed'); + Navigator.of(context).maybePop(); + }, + ), + title: Text( + l10n.connectionChoiceUsbLabel, + style: theme.textTheme.titleLarge, + ), + centerTitle: true, + ), + body: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final availableHeight = constraints.maxHeight.isFinite + ? constraints.maxHeight + : 600.0; + final availableWidth = constraints.maxWidth.isFinite + ? constraints.maxWidth + : 800.0; + final gap = math.max(8.0, math.min(16.0, availableHeight * 0.025)); + final iconSize = math.max( + 28.0, + math.min(72.0, availableHeight * 0.12), + ); + final isNarrow = availableWidth < 460.0; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + flex: 3, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.usb, + size: iconSize, + color: theme.colorScheme.primary, + ), + SizedBox(height: gap), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + l10n.usbScreenTitle, + textAlign: TextAlign.center, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + ), + SizedBox(height: math.max(4.0, gap * 0.5)), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + l10n.usbScreenSubtitle, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + SizedBox(height: gap), + FittedBox( + fit: BoxFit.scaleDown, + child: Chip( + label: Text( + _selectedPort == null + ? l10n.usbScreenStatus + : _friendlyPortName(_selectedPort!), + overflow: TextOverflow.ellipsis, + ), + backgroundColor: + theme.colorScheme.surfaceContainerHighest, + ), + ), + ], + ), + ), + ), + SizedBox(height: gap), + Expanded(child: _buildPortList(context)), + if (_errorText != null) ...[ + SizedBox(height: gap), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + _errorText!, + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.error, + ), + ), + ), + ), + ], + SizedBox(height: gap), + if (isNarrow) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OutlinedButton.icon( + onPressed: _isLoadingPorts || _isConnecting + ? null + : () { + debugPrint( + 'UsbScreen: refresh ports pressed', + ); + _loadPorts(); + }, + icon: const Icon(Icons.refresh), + label: Text(l10n.repeater_refresh), + ), + SizedBox(height: gap), + FilledButton.icon( + onPressed: _canConnect + ? () { + final rawPortName = _normalizedPortName( + _selectedPort!, + ); + debugPrint( + 'UsbScreen: connect pressed for $_selectedPort (raw: $rawPortName)', + ); + _connectSelectedPort(); + } + : null, + icon: _isConnecting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.usb), + label: Text(l10n.common_connect), + ), + ], + ) + else + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _isLoadingPorts || _isConnecting + ? null + : () { + debugPrint( + 'UsbScreen: refresh ports pressed', + ); + _loadPorts(); + }, + icon: const Icon(Icons.refresh), + label: Text(l10n.repeater_refresh), + ), + ), + SizedBox(width: gap), + Expanded( + child: FilledButton.icon( + onPressed: _canConnect + ? () { + final rawPortName = _normalizedPortName( + _selectedPort!, + ); + debugPrint( + 'UsbScreen: connect pressed for $_selectedPort (raw: $rawPortName)', + ); + _connectSelectedPort(); + } + : null, + icon: _isConnecting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.usb), + label: Text(l10n.common_connect), + ), + ), + ], + ), + SizedBox(height: math.max(4.0, gap * 0.75)), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + l10n.usbScreenNote, + textAlign: TextAlign.center, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + ); + } + + bool get _canConnect => + !_isLoadingPorts && + !_isConnecting && + _selectedPort != null && + _selectedPort!.isNotEmpty; + + Widget _buildPortList(BuildContext context) { + final theme = Theme.of(context); + final l10n = context.l10n; + + if (_isLoadingPorts) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 12), + Text(l10n.common_loading), + ], + ), + ); + } + + if (_ports.isEmpty) { + return Center( + child: Text( + l10n.usbScreenEmptyState, + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ); + } + + return ListView.separated( + itemCount: _ports.length, + itemBuilder: (context, index) { + final port = _ports[index]; + final isSelected = port == _selectedPort; + final displayName = _friendlyPortName(port); + final rawName = _normalizedPortName(port); + final showRawName = rawName != displayName; + return Material( + color: isSelected + ? theme.colorScheme.primaryContainer + : theme.colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(16), + child: ListTile( + onTap: _isConnecting + ? null + : () { + setState(() { + _selectedPort = port; + _errorText = null; + }); + debugPrint('UsbScreen: selected port $port'); + }, + leading: Icon( + Icons.usb, + color: isSelected + ? theme.colorScheme.onPrimaryContainer + : theme.colorScheme.onSurfaceVariant, + ), + title: Text( + displayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleMedium?.copyWith( + color: isSelected ? theme.colorScheme.onPrimaryContainer : null, + ), + ), + subtitle: showRawName + ? Text( + rawName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: isSelected + ? theme.colorScheme.onPrimaryContainer + : theme.colorScheme.onSurfaceVariant, + ), + ) + : null, + trailing: isSelected + ? Icon( + Icons.check_circle, + color: theme.colorScheme.onPrimaryContainer, + ) + : null, + ), + ); + }, + separatorBuilder: (context, index) => const SizedBox(height: 10), + ); + } + + Future _loadPorts() async { + if (!mounted) return; + + setState(() { + _isLoadingPorts = true; + _errorText = null; + }); + + try { + final ports = await _connector.listUsbPorts(); + if (!mounted) return; + setState(() { + _ports + ..clear() + ..addAll(ports); + if (_ports.isEmpty) { + _selectedPort = null; + } else if (!_ports.contains(_selectedPort)) { + _selectedPort = _ports.first; + } + _isLoadingPorts = false; + }); + } catch (error) { + if (!mounted) return; + setState(() { + _ports.clear(); + _selectedPort = null; + _errorText = error.toString(); + _isLoadingPorts = false; + }); + } + } + + Future _connectSelectedPort() async { + final selectedPort = _selectedPort; + if (selectedPort == null || selectedPort.isEmpty) { + return; + } + final rawPortName = _normalizedPortName(selectedPort); + + setState(() { + _isConnecting = true; + _errorText = null; + }); + + try { + await _connector.connectUsb(portName: rawPortName); + } catch (error) { + if (!mounted) return; + setState(() { + _isConnecting = false; + _errorText = error.toString(); + }); + } + } + + String _normalizedPortName(String portLabel) { + final separatorIndex = portLabel.indexOf(' - '); + final normalized = separatorIndex >= 0 + ? portLabel.substring(0, separatorIndex) + : portLabel; + return normalized.trim(); + } + + String _friendlyPortName(String portLabel) { + final separatorIndex = portLabel.indexOf(' - '); + if (separatorIndex < 0) { + return portLabel.trim(); + } + final friendlyName = portLabel.substring(separatorIndex + 3).trim(); + if (friendlyName.isEmpty) { + return _normalizedPortName(portLabel); + } + return friendlyName; + } +} diff --git a/lib/services/usb_serial_service.dart b/lib/services/usb_serial_service.dart new file mode 100644 index 0000000..7e9027d --- /dev/null +++ b/lib/services/usb_serial_service.dart @@ -0,0 +1,284 @@ +import 'dart:async'; + +import 'package:flserial/flserial.dart'; +import 'package:flserial/flserial_exception.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// Wraps the native flserial plugin to expose a stream of raw bytes for the +/// MeshCore connector to consume. +class UsbSerialService { + UsbSerialService(); + + static const MethodChannel _androidMethodChannel = MethodChannel( + 'meshcore_open/android_usb_serial', + ); + static const EventChannel _androidEventChannel = EventChannel( + 'meshcore_open/android_usb_serial_events', + ); + static const int _serialTxFrameStart = 0x3c; + static const int _serialRxFrameStart = 0x3e; + static const int _serialHeaderLength = 3; + + final StreamController _frameController = + StreamController.broadcast(); + final FlSerial _serial = FlSerial(); + final List _rxBuffer = []; + StreamSubscription? _androidDataSubscription; + StreamSubscription? _dataSubscription; + UsbSerialStatus _status = UsbSerialStatus.disconnected; + String? _connectedPortName; + + UsbSerialStatus get status => _status; + String? get activePortName => _connectedPortName; + Stream get frameStream => _frameController.stream; + bool get _useAndroidUsbHost => + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + + bool get isConnected { + if (_useAndroidUsbHost) { + return _status == UsbSerialStatus.connected; + } + return _status == UsbSerialStatus.connected && + _serial.isOpen() == FlOpenStatus.open; + } + + Future> listPorts() async { + if (_useAndroidUsbHost) { + final ports = await _androidMethodChannel.invokeListMethod( + 'listPorts', + ); + return ports ?? []; + } + return Future.value(FlSerial.listPorts()); + } + + Future connect({ + required String portName, + int baudRate = 115200, + }) async { + if (_status == UsbSerialStatus.connected || + _status == UsbSerialStatus.connecting) { + throw StateError('USB serial transport is already active'); + } + + _status = UsbSerialStatus.connecting; + final normalizedPortName = _normalizePortName(portName); + + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('connect', { + 'portName': normalizedPortName, + 'baudRate': baudRate, + }); + debugPrint( + 'USB serial opened port=$normalizedPortName on Android via USB host bridge', + ); + } on PlatformException catch (error) { + _status = UsbSerialStatus.disconnected; + throw StateError(error.message ?? error.code); + } + } else { + _serial.init(); + + try { + final status = _serial.openPort(normalizedPortName, baudRate); + if (status != FlOpenStatus.open) { + throw StateError( + 'Failed to open USB port $normalizedPortName ($status)', + ); + } + _serial.setByteSize8(); + _serial.setBitParityNone(); + _serial.setStopBits1(); + _serial.setFlowControlNone(); + _serial.setRTS(false); + _serial.setDTR(true); + debugPrint( + 'USB serial opened port=$normalizedPortName cts=${_serial.getCTS()} dsr=${_serial.getDSR()} dtr=true rts=false', + ); + } on FlSerialException catch (error) { + _serial.free(); + _status = UsbSerialStatus.disconnected; + throw StateError( + 'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})', + ); + } catch (error) { + _serial.free(); + _status = UsbSerialStatus.disconnected; + rethrow; + } + } + + _connectedPortName = normalizedPortName; + if (_useAndroidUsbHost) { + _androidDataSubscription = _androidEventChannel + .receiveBroadcastStream() + .listen( + _handleAndroidData, + onError: _handleSerialError, + onDone: _handleSerialDone, + ); + } else { + _dataSubscription = _serial.onSerialData.stream.listen( + _handleSerialData, + onError: _handleSerialError, + onDone: _handleSerialDone, + ); + } + _status = UsbSerialStatus.connected; + } + + Future write(Uint8List data) async { + if (!isConnected) { + throw StateError('USB serial port is not open'); + } + final packet = Uint8List(_serialHeaderLength + data.length); + packet[0] = _serialTxFrameStart; + packet[1] = data.length & 0xff; + packet[2] = (data.length >> 8) & 0xff; + packet.setRange(_serialHeaderLength, packet.length, data); + _logFrameSummary('USB TX frame', data); + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('write', { + 'data': packet, + }); + } on PlatformException catch (error) { + throw StateError(error.message ?? error.code); + } + } else { + _serial.write(packet); + } + } + + Future disconnect() async { + if (_status == UsbSerialStatus.disconnected) return; + + _status = UsbSerialStatus.disconnecting; + _connectedPortName = null; + await _androidDataSubscription?.cancel(); + _androidDataSubscription = null; + await _dataSubscription?.cancel(); + _dataSubscription = null; + + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('disconnect'); + } catch (_) { + // Ignore errors while closing. + } + } else { + try { + if (_serial.isOpen() == FlOpenStatus.open) { + _serial.closePort(); + } + } catch (_) { + // Ignore errors while closing. + } + + _serial.free(); + } + _status = UsbSerialStatus.disconnected; + } + + void dispose() { + unawaited(disconnect()); + unawaited(_frameController.close()); + } + + void _handleSerialData(FlSerialEventArgs event) { + try { + final bytes = event.serial.readList(); + if (bytes.isNotEmpty) { + _ingestRawBytes(Uint8List.fromList(bytes)); + } + } catch (error, stack) { + _frameController.addError(error, stack); + } + } + + void _handleAndroidData(dynamic data) { + if (data is Uint8List) { + _ingestRawBytes(data); + return; + } + if (data is ByteData) { + _ingestRawBytes(data.buffer.asUint8List()); + return; + } + _frameController.addError( + StateError('Unexpected Android USB event payload: ${data.runtimeType}'), + ); + } + + void _handleSerialError(Object error, [StackTrace? stackTrace]) { + _frameController.addError(error, stackTrace); + } + + void _handleSerialDone() { + unawaited(disconnect()); + } + + String _normalizePortName(String portName) { + final separatorIndex = portName.indexOf(' - '); + final normalized = separatorIndex >= 0 + ? portName.substring(0, separatorIndex) + : portName; + return normalized.trim(); + } + + void _ingestRawBytes(Uint8List bytes) { + if (bytes.isEmpty) { + return; + } + _rxBuffer.addAll(bytes); + _drainRxBuffer(); + } + + void _drainRxBuffer() { + while (true) { + if (_rxBuffer.isEmpty) { + return; + } + + if (_rxBuffer.first != _serialRxFrameStart && + _rxBuffer.first != _serialTxFrameStart) { + _rxBuffer.removeAt(0); + continue; + } + + if (_rxBuffer.length < _serialHeaderLength) { + return; + } + + final payloadLength = _rxBuffer[1] | (_rxBuffer[2] << 8); + final packetLength = _serialHeaderLength + payloadLength; + if (_rxBuffer.length < packetLength) { + return; + } + + final frameStart = _rxBuffer.first; + final payload = Uint8List.fromList( + _rxBuffer.sublist(_serialHeaderLength, packetLength), + ); + _rxBuffer.removeRange(0, packetLength); + if (frameStart != _serialRxFrameStart) { + debugPrint( + 'USB ignored packet start=0x${frameStart.toRadixString(16).padLeft(2, '0')} len=${payload.length}', + ); + } + _frameController.add(payload); + } + } + + void _logFrameSummary(String prefix, Uint8List bytes) { + if (bytes.isEmpty) { + debugPrint('$prefix len=0'); + return; + } + debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); + } +} + +enum UsbSerialStatus { disconnected, connecting, connected, disconnecting } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f16b4c3..379e36f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flserial ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/pubspec.yaml b/pubspec.yaml index f85530f..9e82770 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 flutter_blue_plus: ^2.1.0 + flserial: ^0.3.5 provider: ^6.1.5+1 shared_preferences: ^2.2.2 uuid: ^4.3.3 diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index daf32c2..97c813c 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -89,9 +89,11 @@ endif() # Copy the native assets provided by the build.dart from all packages. set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) +if(EXISTS "${NATIVE_ASSETS_DIR}") + install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4c358e7..f02857f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flserial flutter_local_notifications_windows ) From c23a1da430ad785c30ee865a3184803cafa6f8f7 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 00:27:49 -0500 Subject: [PATCH 225/421] Add web serial support and USB tests --- lib/connector/meshcore_connector.dart | 15 +- lib/screens/connection_choice_screen.dart | 71 ++-- lib/screens/usb_screen.dart | 37 +- lib/services/usb_serial_frame_codec.dart | 70 ++++ lib/services/usb_serial_service.dart | 286 +------------ lib/services/usb_serial_service_native.dart | 247 +++++++++++ lib/services/usb_serial_service_web.dart | 385 ++++++++++++++++++ lib/utils/usb_port_labels.dart | 57 +++ .../services/usb_serial_frame_codec_test.dart | 84 ++++ test/utils/usb_port_labels_test.dart | 76 ++++ 10 files changed, 997 insertions(+), 331 deletions(-) create mode 100644 lib/services/usb_serial_frame_codec.dart create mode 100644 lib/services/usb_serial_service_native.dart create mode 100644 lib/services/usb_serial_service_web.dart create mode 100644 lib/utils/usb_port_labels.dart create mode 100644 test/services/usb_serial_frame_codec_test.dart create mode 100644 test/utils/usb_port_labels_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index f514f15..d7731f1 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -31,6 +31,7 @@ import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; import '../utils/battery_utils.dart'; +import '../utils/platform_info.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -744,7 +745,9 @@ class MeshCoreConnector extends ChangeNotifier { ); await Future.delayed(timeout); - await stopScan(); + if (!PlatformInfo.isWeb) { + await stopScan(); + } } Future stopScan() async { @@ -898,6 +901,9 @@ class MeshCoreConnector extends ChangeNotifier { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; await _usbSerialService.connect(portName: portName, baudRate: baudRate); + if (PlatformInfo.isWeb) { + await stopScan(); + } await Future.delayed(const Duration(milliseconds: 200)); _usbFrameSubscription = _usbSerialService.frameStream.listen( _handleFrame, @@ -2087,6 +2093,13 @@ class MeshCoreConnector extends ChangeNotifier { if (frame.length > 58) { _selfName = readCString(frame, 58, frame.length - 58); } + final selfName = _selfName?.trim(); + if (_activeTransport == MeshCoreTransportType.usb && + selfName != null && + selfName.isNotEmpty) { + _usbSerialService.updateConnectedLabel(selfName); + _activeUsbPort = _usbSerialService.activePortName ?? _activeUsbPort; + } _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); _selfInfoRetryTimer = null; diff --git a/lib/screens/connection_choice_screen.dart b/lib/screens/connection_choice_screen.dart index a2ea183..e4abe7f 100644 --- a/lib/screens/connection_choice_screen.dart +++ b/lib/screens/connection_choice_screen.dart @@ -157,30 +157,55 @@ class _ConnectionMethodButton extends StatelessWidget { ? constraints.maxWidth : 320.0; final isCompact = availableHeight < 72.0 || availableWidth < 180.0; - final baseGap = isCompact ? 8.0 : 12.0; - final content = Flex( - direction: isCompact ? Axis.horizontal : Axis.vertical, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: isCompact ? 24.0 : 60.0, color: iconColor), - SizedBox( - width: isCompact ? baseGap : 0, - height: isCompact ? 0 : baseGap, - ), - Text( - label, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.visible, - style: - (isCompact + final useTightVertical = !isCompact && availableHeight < 120.0; + final baseGap = isCompact + ? 8.0 + : (useTightVertical + ? math.max(4.0, math.min(8.0, availableHeight * 0.06)) + : 12.0); + final labelStyle = + (isCompact + ? theme.textTheme.titleMedium + : (useTightVertical ? theme.textTheme.titleMedium - : theme.textTheme.titleLarge) - ?.copyWith(fontWeight: FontWeight.w600), - ), - ], - ); + : theme.textTheme.titleLarge)) + ?.copyWith(fontWeight: FontWeight.w600); + final verticalIconSize = useTightVertical + ? math.max(32.0, math.min(48.0, availableHeight * 0.42)) + : 60.0; + final content = isCompact + ? Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 24.0, color: iconColor), + SizedBox(width: baseGap), + Flexible( + child: Text( + label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: labelStyle, + ), + ), + ], + ) + : Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: verticalIconSize, color: iconColor), + SizedBox(height: baseGap), + Text( + label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.visible, + style: labelStyle, + ), + ], + ); return Center( child: FittedBox( diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index e542d61..c3c1066 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../utils/usb_port_labels.dart'; import 'contacts_screen.dart'; class UsbScreen extends StatefulWidget { @@ -31,6 +32,14 @@ class _UsbScreenState extends State { _connector = context.read(); _connectionListener = () { if (!mounted) return; + final activeUsbPort = _connector.activeUsbPort; + if (activeUsbPort != null && + activeUsbPort.isNotEmpty && + activeUsbPort != _selectedPort) { + setState(() { + _selectedPort = activeUsbPort; + }); + } if (_connector.state == MeshCoreConnectionState.disconnected) { _navigatedToContacts = false; if (_isConnecting) { @@ -192,7 +201,7 @@ class _UsbScreenState extends State { FilledButton.icon( onPressed: _canConnect ? () { - final rawPortName = _normalizedPortName( + final rawPortName = normalizeUsbPortName( _selectedPort!, ); debugPrint( @@ -236,7 +245,7 @@ class _UsbScreenState extends State { child: FilledButton.icon( onPressed: _canConnect ? () { - final rawPortName = _normalizedPortName( + final rawPortName = normalizeUsbPortName( _selectedPort!, ); debugPrint( @@ -322,7 +331,7 @@ class _UsbScreenState extends State { final port = _ports[index]; final isSelected = port == _selectedPort; final displayName = _friendlyPortName(port); - final rawName = _normalizedPortName(port); + final rawName = normalizeUsbPortName(port); final showRawName = rawName != displayName; return Material( color: isSelected @@ -416,7 +425,7 @@ class _UsbScreenState extends State { if (selectedPort == null || selectedPort.isEmpty) { return; } - final rawPortName = _normalizedPortName(selectedPort); + final rawPortName = normalizeUsbPortName(selectedPort); setState(() { _isConnecting = true; @@ -434,23 +443,5 @@ class _UsbScreenState extends State { } } - String _normalizedPortName(String portLabel) { - final separatorIndex = portLabel.indexOf(' - '); - final normalized = separatorIndex >= 0 - ? portLabel.substring(0, separatorIndex) - : portLabel; - return normalized.trim(); - } - - String _friendlyPortName(String portLabel) { - final separatorIndex = portLabel.indexOf(' - '); - if (separatorIndex < 0) { - return portLabel.trim(); - } - final friendlyName = portLabel.substring(separatorIndex + 3).trim(); - if (friendlyName.isEmpty) { - return _normalizedPortName(portLabel); - } - return friendlyName; - } + String _friendlyPortName(String portLabel) => friendlyUsbPortName(portLabel); } diff --git a/lib/services/usb_serial_frame_codec.dart b/lib/services/usb_serial_frame_codec.dart new file mode 100644 index 0000000..ee4a17c --- /dev/null +++ b/lib/services/usb_serial_frame_codec.dart @@ -0,0 +1,70 @@ +import 'dart:typed_data'; + +const int usbSerialTxFrameStart = 0x3c; +const int usbSerialRxFrameStart = 0x3e; +const int usbSerialHeaderLength = 3; + +Uint8List wrapUsbSerialTxFrame(Uint8List payload) { + final packet = Uint8List(usbSerialHeaderLength + payload.length); + packet[0] = usbSerialTxFrameStart; + packet[1] = payload.length & 0xff; + packet[2] = (payload.length >> 8) & 0xff; + packet.setRange(usbSerialHeaderLength, packet.length, payload); + return packet; +} + +class UsbSerialDecodedPacket { + const UsbSerialDecodedPacket({ + required this.frameStart, + required this.payload, + }); + + final int frameStart; + final Uint8List payload; + + bool get isRxFrame => frameStart == usbSerialRxFrameStart; +} + +class UsbSerialFrameDecoder { + final List _rxBuffer = []; + + List ingest(Uint8List bytes) { + if (bytes.isEmpty) { + return const []; + } + + _rxBuffer.addAll(bytes); + final packets = []; + + while (true) { + if (_rxBuffer.isEmpty) { + return packets; + } + + if (_rxBuffer.first != usbSerialRxFrameStart && + _rxBuffer.first != usbSerialTxFrameStart) { + _rxBuffer.removeAt(0); + continue; + } + + if (_rxBuffer.length < usbSerialHeaderLength) { + return packets; + } + + final payloadLength = _rxBuffer[1] | (_rxBuffer[2] << 8); + final packetLength = usbSerialHeaderLength + payloadLength; + if (_rxBuffer.length < packetLength) { + return packets; + } + + final frameStart = _rxBuffer.first; + final payload = Uint8List.fromList( + _rxBuffer.sublist(usbSerialHeaderLength, packetLength), + ); + _rxBuffer.removeRange(0, packetLength); + packets.add( + UsbSerialDecodedPacket(frameStart: frameStart, payload: payload), + ); + } + } +} diff --git a/lib/services/usb_serial_service.dart b/lib/services/usb_serial_service.dart index 7e9027d..343d0ea 100644 --- a/lib/services/usb_serial_service.dart +++ b/lib/services/usb_serial_service.dart @@ -1,284 +1,2 @@ -import 'dart:async'; - -import 'package:flserial/flserial.dart'; -import 'package:flserial/flserial_exception.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -/// Wraps the native flserial plugin to expose a stream of raw bytes for the -/// MeshCore connector to consume. -class UsbSerialService { - UsbSerialService(); - - static const MethodChannel _androidMethodChannel = MethodChannel( - 'meshcore_open/android_usb_serial', - ); - static const EventChannel _androidEventChannel = EventChannel( - 'meshcore_open/android_usb_serial_events', - ); - static const int _serialTxFrameStart = 0x3c; - static const int _serialRxFrameStart = 0x3e; - static const int _serialHeaderLength = 3; - - final StreamController _frameController = - StreamController.broadcast(); - final FlSerial _serial = FlSerial(); - final List _rxBuffer = []; - StreamSubscription? _androidDataSubscription; - StreamSubscription? _dataSubscription; - UsbSerialStatus _status = UsbSerialStatus.disconnected; - String? _connectedPortName; - - UsbSerialStatus get status => _status; - String? get activePortName => _connectedPortName; - Stream get frameStream => _frameController.stream; - bool get _useAndroidUsbHost => - !kIsWeb && defaultTargetPlatform == TargetPlatform.android; - - bool get isConnected { - if (_useAndroidUsbHost) { - return _status == UsbSerialStatus.connected; - } - return _status == UsbSerialStatus.connected && - _serial.isOpen() == FlOpenStatus.open; - } - - Future> listPorts() async { - if (_useAndroidUsbHost) { - final ports = await _androidMethodChannel.invokeListMethod( - 'listPorts', - ); - return ports ?? []; - } - return Future.value(FlSerial.listPorts()); - } - - Future connect({ - required String portName, - int baudRate = 115200, - }) async { - if (_status == UsbSerialStatus.connected || - _status == UsbSerialStatus.connecting) { - throw StateError('USB serial transport is already active'); - } - - _status = UsbSerialStatus.connecting; - final normalizedPortName = _normalizePortName(portName); - - if (_useAndroidUsbHost) { - try { - await _androidMethodChannel.invokeMethod('connect', { - 'portName': normalizedPortName, - 'baudRate': baudRate, - }); - debugPrint( - 'USB serial opened port=$normalizedPortName on Android via USB host bridge', - ); - } on PlatformException catch (error) { - _status = UsbSerialStatus.disconnected; - throw StateError(error.message ?? error.code); - } - } else { - _serial.init(); - - try { - final status = _serial.openPort(normalizedPortName, baudRate); - if (status != FlOpenStatus.open) { - throw StateError( - 'Failed to open USB port $normalizedPortName ($status)', - ); - } - _serial.setByteSize8(); - _serial.setBitParityNone(); - _serial.setStopBits1(); - _serial.setFlowControlNone(); - _serial.setRTS(false); - _serial.setDTR(true); - debugPrint( - 'USB serial opened port=$normalizedPortName cts=${_serial.getCTS()} dsr=${_serial.getDSR()} dtr=true rts=false', - ); - } on FlSerialException catch (error) { - _serial.free(); - _status = UsbSerialStatus.disconnected; - throw StateError( - 'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})', - ); - } catch (error) { - _serial.free(); - _status = UsbSerialStatus.disconnected; - rethrow; - } - } - - _connectedPortName = normalizedPortName; - if (_useAndroidUsbHost) { - _androidDataSubscription = _androidEventChannel - .receiveBroadcastStream() - .listen( - _handleAndroidData, - onError: _handleSerialError, - onDone: _handleSerialDone, - ); - } else { - _dataSubscription = _serial.onSerialData.stream.listen( - _handleSerialData, - onError: _handleSerialError, - onDone: _handleSerialDone, - ); - } - _status = UsbSerialStatus.connected; - } - - Future write(Uint8List data) async { - if (!isConnected) { - throw StateError('USB serial port is not open'); - } - final packet = Uint8List(_serialHeaderLength + data.length); - packet[0] = _serialTxFrameStart; - packet[1] = data.length & 0xff; - packet[2] = (data.length >> 8) & 0xff; - packet.setRange(_serialHeaderLength, packet.length, data); - _logFrameSummary('USB TX frame', data); - if (_useAndroidUsbHost) { - try { - await _androidMethodChannel.invokeMethod('write', { - 'data': packet, - }); - } on PlatformException catch (error) { - throw StateError(error.message ?? error.code); - } - } else { - _serial.write(packet); - } - } - - Future disconnect() async { - if (_status == UsbSerialStatus.disconnected) return; - - _status = UsbSerialStatus.disconnecting; - _connectedPortName = null; - await _androidDataSubscription?.cancel(); - _androidDataSubscription = null; - await _dataSubscription?.cancel(); - _dataSubscription = null; - - if (_useAndroidUsbHost) { - try { - await _androidMethodChannel.invokeMethod('disconnect'); - } catch (_) { - // Ignore errors while closing. - } - } else { - try { - if (_serial.isOpen() == FlOpenStatus.open) { - _serial.closePort(); - } - } catch (_) { - // Ignore errors while closing. - } - - _serial.free(); - } - _status = UsbSerialStatus.disconnected; - } - - void dispose() { - unawaited(disconnect()); - unawaited(_frameController.close()); - } - - void _handleSerialData(FlSerialEventArgs event) { - try { - final bytes = event.serial.readList(); - if (bytes.isNotEmpty) { - _ingestRawBytes(Uint8List.fromList(bytes)); - } - } catch (error, stack) { - _frameController.addError(error, stack); - } - } - - void _handleAndroidData(dynamic data) { - if (data is Uint8List) { - _ingestRawBytes(data); - return; - } - if (data is ByteData) { - _ingestRawBytes(data.buffer.asUint8List()); - return; - } - _frameController.addError( - StateError('Unexpected Android USB event payload: ${data.runtimeType}'), - ); - } - - void _handleSerialError(Object error, [StackTrace? stackTrace]) { - _frameController.addError(error, stackTrace); - } - - void _handleSerialDone() { - unawaited(disconnect()); - } - - String _normalizePortName(String portName) { - final separatorIndex = portName.indexOf(' - '); - final normalized = separatorIndex >= 0 - ? portName.substring(0, separatorIndex) - : portName; - return normalized.trim(); - } - - void _ingestRawBytes(Uint8List bytes) { - if (bytes.isEmpty) { - return; - } - _rxBuffer.addAll(bytes); - _drainRxBuffer(); - } - - void _drainRxBuffer() { - while (true) { - if (_rxBuffer.isEmpty) { - return; - } - - if (_rxBuffer.first != _serialRxFrameStart && - _rxBuffer.first != _serialTxFrameStart) { - _rxBuffer.removeAt(0); - continue; - } - - if (_rxBuffer.length < _serialHeaderLength) { - return; - } - - final payloadLength = _rxBuffer[1] | (_rxBuffer[2] << 8); - final packetLength = _serialHeaderLength + payloadLength; - if (_rxBuffer.length < packetLength) { - return; - } - - final frameStart = _rxBuffer.first; - final payload = Uint8List.fromList( - _rxBuffer.sublist(_serialHeaderLength, packetLength), - ); - _rxBuffer.removeRange(0, packetLength); - if (frameStart != _serialRxFrameStart) { - debugPrint( - 'USB ignored packet start=0x${frameStart.toRadixString(16).padLeft(2, '0')} len=${payload.length}', - ); - } - _frameController.add(payload); - } - } - - void _logFrameSummary(String prefix, Uint8List bytes) { - if (bytes.isEmpty) { - debugPrint('$prefix len=0'); - return; - } - debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); - } -} - -enum UsbSerialStatus { disconnected, connecting, connected, disconnecting } +export 'usb_serial_service_native.dart' + if (dart.library.js_interop) 'usb_serial_service_web.dart'; diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart new file mode 100644 index 0000000..f69c6fc --- /dev/null +++ b/lib/services/usb_serial_service_native.dart @@ -0,0 +1,247 @@ +import 'dart:async'; + +import 'package:flserial/flserial.dart'; +import 'package:flserial/flserial_exception.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import '../utils/usb_port_labels.dart'; +import 'usb_serial_frame_codec.dart'; + +/// Wraps the native flserial plugin to expose a stream of raw bytes for the +/// MeshCore connector to consume. +class UsbSerialService { + UsbSerialService(); + + static const MethodChannel _androidMethodChannel = MethodChannel( + 'meshcore_open/android_usb_serial', + ); + static const EventChannel _androidEventChannel = EventChannel( + 'meshcore_open/android_usb_serial_events', + ); + final StreamController _frameController = + StreamController.broadcast(); + final FlSerial _serial = FlSerial(); + final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder(); + StreamSubscription? _androidDataSubscription; + StreamSubscription? _dataSubscription; + UsbSerialStatus _status = UsbSerialStatus.disconnected; + String? _connectedPortName; + + UsbSerialStatus get status => _status; + String? get activePortName => _connectedPortName; + Stream get frameStream => _frameController.stream; + bool get _useAndroidUsbHost => + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + + bool get isConnected { + if (_useAndroidUsbHost) { + return _status == UsbSerialStatus.connected; + } + return _status == UsbSerialStatus.connected && + _serial.isOpen() == FlOpenStatus.open; + } + + Future> listPorts() async { + if (_useAndroidUsbHost) { + final ports = await _androidMethodChannel.invokeListMethod( + 'listPorts', + ); + return ports ?? []; + } + return Future.value(FlSerial.listPorts()); + } + + Future connect({ + required String portName, + int baudRate = 115200, + }) async { + if (_status == UsbSerialStatus.connected || + _status == UsbSerialStatus.connecting) { + throw StateError('USB serial transport is already active'); + } + + _status = UsbSerialStatus.connecting; + final normalizedPortName = normalizeUsbPortName(portName); + + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('connect', { + 'portName': normalizedPortName, + 'baudRate': baudRate, + }); + debugPrint( + 'USB serial opened port=$normalizedPortName on Android via USB host bridge', + ); + } on PlatformException catch (error) { + _status = UsbSerialStatus.disconnected; + throw StateError(error.message ?? error.code); + } + } else { + _serial.init(); + + try { + final status = _serial.openPort(normalizedPortName, baudRate); + if (status != FlOpenStatus.open) { + throw StateError( + 'Failed to open USB port $normalizedPortName ($status)', + ); + } + _serial.setByteSize8(); + _serial.setBitParityNone(); + _serial.setStopBits1(); + _serial.setFlowControlNone(); + _serial.setRTS(false); + _serial.setDTR(true); + debugPrint( + 'USB serial opened port=$normalizedPortName cts=${_serial.getCTS()} dsr=${_serial.getDSR()} dtr=true rts=false', + ); + } on FlSerialException catch (error) { + _serial.free(); + _status = UsbSerialStatus.disconnected; + throw StateError( + 'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})', + ); + } catch (error) { + _serial.free(); + _status = UsbSerialStatus.disconnected; + rethrow; + } + } + + _connectedPortName = normalizedPortName; + if (_useAndroidUsbHost) { + _androidDataSubscription = _androidEventChannel + .receiveBroadcastStream() + .listen( + _handleAndroidData, + onError: _handleSerialError, + onDone: _handleSerialDone, + ); + } else { + _dataSubscription = _serial.onSerialData.stream.listen( + _handleSerialData, + onError: _handleSerialError, + onDone: _handleSerialDone, + ); + } + _status = UsbSerialStatus.connected; + } + + Future write(Uint8List data) async { + if (!isConnected) { + throw StateError('USB serial port is not open'); + } + final packet = wrapUsbSerialTxFrame(data); + _logFrameSummary('USB TX frame', data); + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('write', { + 'data': packet, + }); + } on PlatformException catch (error) { + throw StateError(error.message ?? error.code); + } + } else { + _serial.write(packet); + } + } + + Future disconnect() async { + if (_status == UsbSerialStatus.disconnected) return; + + _status = UsbSerialStatus.disconnecting; + _connectedPortName = null; + await _androidDataSubscription?.cancel(); + _androidDataSubscription = null; + await _dataSubscription?.cancel(); + _dataSubscription = null; + + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('disconnect'); + } catch (_) { + // Ignore errors while closing. + } + } else { + try { + if (_serial.isOpen() == FlOpenStatus.open) { + _serial.closePort(); + } + } catch (_) { + // Ignore errors while closing. + } + + _serial.free(); + } + _status = UsbSerialStatus.disconnected; + } + + void updateConnectedLabel(String label) { + final trimmed = label.trim(); + if (trimmed.isEmpty) { + return; + } + _connectedPortName = trimmed; + } + + void dispose() { + unawaited(disconnect()); + unawaited(_frameController.close()); + } + + void _handleSerialData(FlSerialEventArgs event) { + try { + final bytes = event.serial.readList(); + if (bytes.isNotEmpty) { + _ingestRawBytes(Uint8List.fromList(bytes)); + } + } catch (error, stack) { + _frameController.addError(error, stack); + } + } + + void _handleAndroidData(dynamic data) { + if (data is Uint8List) { + _ingestRawBytes(data); + return; + } + if (data is ByteData) { + _ingestRawBytes(data.buffer.asUint8List()); + return; + } + _frameController.addError( + StateError('Unexpected Android USB event payload: ${data.runtimeType}'), + ); + } + + void _handleSerialError(Object error, [StackTrace? stackTrace]) { + _frameController.addError(error, stackTrace); + } + + void _handleSerialDone() { + unawaited(disconnect()); + } + + void _ingestRawBytes(Uint8List bytes) { + for (final packet in _frameDecoder.ingest(bytes)) { + if (!packet.isRxFrame) { + debugPrint( + 'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}', + ); + continue; + } + _frameController.add(packet.payload); + } + } + + void _logFrameSummary(String prefix, Uint8List bytes) { + if (bytes.isEmpty) { + debugPrint('$prefix len=0'); + return; + } + debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); + } +} + +enum UsbSerialStatus { disconnected, connecting, connected, disconnecting } diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart new file mode 100644 index 0000000..67844df --- /dev/null +++ b/lib/services/usb_serial_service_web.dart @@ -0,0 +1,385 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:flutter/foundation.dart'; +import 'package:web/web.dart' as web; + +import '../utils/usb_port_labels.dart'; +import 'usb_serial_frame_codec.dart'; + +class UsbSerialService { + UsbSerialService(); + + static const Map _knownUsbNames = { + '2886:1667': 'Seeed Wio Tracker L1', + }; + static final Map _deviceNamesByPortKey = {}; + + final StreamController _frameController = + StreamController.broadcast(); + final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder(); + + UsbSerialStatus _status = UsbSerialStatus.disconnected; + JSObject? _port; + JSObject? _reader; + JSObject? _writer; + String? _connectedPortName; + String? _connectedPortKey; + + UsbSerialStatus get status => _status; + String? get activePortName => _connectedPortName; + Stream get frameStream => _frameController.stream; + bool get isConnected => _status == UsbSerialStatus.connected; + + JSObject get _navigator => JSObject.fromInteropObject(web.window.navigator); + bool get _isSupported => _navigator.has('serial'); + JSObject? get _serial { + if (!_isSupported) { + return null; + } + final serial = _navigator['serial']; + return serial == null ? null : serial as JSObject; + } + + Future> listPorts() async { + if (!_isSupported) { + return const []; + } + + final ports = await _getAuthorizedPorts(); + if (ports.isEmpty) { + return const [usbRequestPortLabel]; + } + return ports.map(_displayLabelForPort).toList(growable: false); + } + + Future connect({ + required String portName, + int baudRate = 115200, + }) async { + if (_status == UsbSerialStatus.connected || + _status == UsbSerialStatus.connecting) { + throw StateError('USB serial transport is already active'); + } + if (!_isSupported) { + throw UnsupportedError('Web Serial is not supported by this browser.'); + } + + _status = UsbSerialStatus.connecting; + + try { + final requestedPortName = normalizeUsbPortName(portName); + final authorizedPorts = await _getAuthorizedPorts(); + _port = _selectPort(authorizedPorts, requestedPortName); + + _port ??= await _requestPort(); + if (_port == null) { + throw StateError('No USB serial device selected'); + } + + await _openPort(_port!, baudRate); + _connectedPortKey = _portKeyFor(_port!); + _connectedPortName = _buildDisplayLabel(_connectedPortKey!); + _writer = _getWriter(_port!); + _reader = _getReader(_port!); + _status = UsbSerialStatus.connected; + unawaited(_pumpReads()); + + debugPrint('USB serial opened port=$_connectedPortName via Web Serial'); + } catch (error) { + _status = UsbSerialStatus.disconnected; + _connectedPortName = null; + rethrow; + } + } + + Future write(Uint8List data) async { + if (!isConnected || _writer == null) { + throw StateError('USB serial port is not open'); + } + + final packet = wrapUsbSerialTxFrame(data); + _logFrameSummary('USB TX frame', data); + + final promise = _writer!.callMethod>( + 'write'.toJS, + packet.toJS, + ); + await promise.toDart; + } + + Future disconnect() async { + if (_status == UsbSerialStatus.disconnected) return; + + _status = UsbSerialStatus.disconnecting; + final reader = _reader; + final writer = _writer; + final port = _port; + + _reader = null; + _writer = null; + _port = null; + _connectedPortName = null; + _connectedPortKey = null; + + if (reader != null) { + try { + await reader.callMethod>('cancel'.toJS).toDart; + } catch (_) { + // Ignore errors while closing. + } + _releaseLock(reader); + } + + if (writer != null) { + _releaseLock(writer); + } + + if (port != null) { + try { + await port.callMethod>('close'.toJS).toDart; + } catch (_) { + // Ignore errors while closing. + } + } + + _status = UsbSerialStatus.disconnected; + } + + void updateConnectedLabel(String label) { + final trimmed = label.trim(); + final portKey = _connectedPortKey; + if (trimmed.isEmpty || portKey == null) { + return; + } + _deviceNamesByPortKey[portKey] = trimmed; + _connectedPortName = _buildDisplayLabel(portKey); + } + + void dispose() { + unawaited(disconnect()); + unawaited(_frameController.close()); + } + + Future> _getAuthorizedPorts() async { + final serial = _serial; + if (serial == null) { + return const []; + } + final result = await serial + .callMethod>('getPorts'.toJS) + .toDart; + return _toObjectList(result); + } + + Future _requestPort() async { + final serial = _serial; + if (serial == null) { + return null; + } + final result = await serial + .callMethod>('requestPort'.toJS) + .toDart; + return result == null ? null : result as JSObject; + } + + JSObject? _selectPort(List ports, String requestedPortName) { + if (ports.isEmpty) { + return null; + } + if (requestedPortName.isEmpty || requestedPortName == usbRequestPortLabel) { + return ports.first; + } + for (final port in ports) { + final description = _describePort(port); + if (description == requestedPortName) { + return port; + } + } + return null; + } + + Future _openPort(JSObject port, int baudRate) { + final options = JSObject()..['baudRate'] = baudRate.toJS; + return port.callMethod>('open'.toJS, options).toDart; + } + + JSObject? _getReader(JSObject port) { + final readable = port.getProperty('readable'.toJS); + if (readable == null) { + throw StateError('Web Serial port is not readable'); + } + final readableObject = readable as JSObject; + return readableObject.callMethod('getReader'.toJS) as JSObject; + } + + JSObject? _getWriter(JSObject port) { + final writable = port.getProperty('writable'.toJS); + if (writable == null) { + throw StateError('Web Serial port is not writable'); + } + final writableObject = writable as JSObject; + return writableObject.callMethod('getWriter'.toJS) as JSObject; + } + + Future _pumpReads() async { + final reader = _reader; + if (reader == null) return; + + try { + while (_status == UsbSerialStatus.connected && + identical(reader, _reader)) { + final result = await reader + .callMethod>('read'.toJS) + .toDart; + if (result == null) { + break; + } + final resultObject = result as JSObject; + + final doneValue = resultObject.getProperty('done'.toJS); + final done = doneValue != null && doneValue.dartify() == true; + if (done) { + break; + } + + final value = resultObject.getProperty('value'.toJS); + final bytes = _coerceBytes(value); + if (bytes != null && bytes.isNotEmpty) { + _ingestRawBytes(bytes); + } + } + } catch (error, stackTrace) { + if (_status == UsbSerialStatus.connected) { + _frameController.addError(error, stackTrace); + } + } finally { + _releaseLock(reader); + if (_status == UsbSerialStatus.connected && identical(reader, _reader)) { + _frameController.addError(StateError('USB serial connection closed')); + } + } + } + + Uint8List? _coerceBytes(JSAny? value) { + if (value == null) return null; + try { + return (value as JSUint8Array).toDart; + } catch (_) { + // Fall back to array-like coercion below. + } + + final object = value as JSObject; + if (object.has('length')) { + final lengthValue = object.getProperty('length'.toJS)?.dartify(); + if (lengthValue is num) { + final length = lengthValue.toInt(); + final bytes = Uint8List(length); + for (var i = 0; i < length; i++) { + final item = object.getProperty(i.toString().toJS)?.dartify(); + if (item is num) { + bytes[i] = item.toInt(); + } + } + return bytes; + } + } + + return null; + } + + List _toObjectList(JSAny? value) { + if (value == null) { + return const []; + } + final object = value as JSObject; + if (!object.has('length')) { + return const []; + } + + final lengthValue = object.getProperty('length'.toJS)?.dartify(); + if (lengthValue is! num) { + return const []; + } + + final length = lengthValue.toInt(); + final items = []; + for (var i = 0; i < length; i++) { + final item = object.getProperty(i.toString().toJS); + if (item != null) { + items.add(item as JSObject); + } + } + return items; + } + + String _describePort(JSObject port) { + try { + final info = port.callMethod('getInfo'.toJS); + if (info == null) { + return usbRequestPortLabel; + } + final infoObject = info as JSObject; + + final vendorId = infoObject + .getProperty('usbVendorId'.toJS) + ?.dartify(); + final productId = infoObject + .getProperty('usbProductId'.toJS) + ?.dartify(); + final hasVendor = vendorId is num; + final hasProduct = productId is num; + + return describeWebUsbPort( + vendorId: hasVendor ? vendorId.toInt() : null, + productId: hasProduct ? productId.toInt() : null, + knownUsbNames: _knownUsbNames, + ); + } catch (_) { + return usbRequestPortLabel; + } + } + + String _portKeyFor(JSObject port) => _describePort(port); + + String _displayLabelForPort(JSObject port) => + _buildDisplayLabel(_portKeyFor(port)); + + String _buildDisplayLabel(String portKey) { + return buildUsbDisplayLabel( + basePortLabel: portKey, + deviceName: _deviceNamesByPortKey[portKey], + ); + } + + void _releaseLock(JSObject resource) { + try { + resource.callMethod('releaseLock'.toJS); + } catch (_) { + // Ignore lock release failures. + } + } + + void _ingestRawBytes(Uint8List bytes) { + for (final packet in _frameDecoder.ingest(bytes)) { + if (!packet.isRxFrame) { + debugPrint( + 'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}', + ); + continue; + } + _frameController.add(packet.payload); + } + } + + void _logFrameSummary(String prefix, Uint8List bytes) { + if (bytes.isEmpty) { + debugPrint('$prefix len=0'); + return; + } + debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); + } +} + +enum UsbSerialStatus { disconnected, connecting, connected, disconnecting } diff --git a/lib/utils/usb_port_labels.dart b/lib/utils/usb_port_labels.dart new file mode 100644 index 0000000..6430f95 --- /dev/null +++ b/lib/utils/usb_port_labels.dart @@ -0,0 +1,57 @@ +const String usbRequestPortLabel = 'Choose USB Device'; + +String normalizeUsbPortName(String portLabel) { + final separatorIndex = portLabel.indexOf(' - '); + final normalized = separatorIndex >= 0 + ? portLabel.substring(0, separatorIndex) + : portLabel; + return normalized.trim(); +} + +String friendlyUsbPortName(String portLabel) { + final separatorIndex = portLabel.indexOf(' - '); + if (separatorIndex < 0) { + return portLabel.trim(); + } + final friendlyName = portLabel.substring(separatorIndex + 3).trim(); + if (friendlyName.isEmpty) { + return normalizeUsbPortName(portLabel); + } + return friendlyName; +} + +String describeWebUsbPort({ + required int? vendorId, + required int? productId, + Map knownUsbNames = const {}, +}) { + if (vendorId == null && productId == null) { + return usbRequestPortLabel; + } + + final vendorHex = vendorId?.toRadixString(16).padLeft(4, '0').toUpperCase(); + final productHex = productId?.toRadixString(16).padLeft(4, '0').toUpperCase(); + final knownName = (vendorHex != null && productHex != null) + ? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}'] + : null; + + final parts = [knownName ?? 'Web Serial Device']; + if (vendorHex != null) { + parts.add('VID:$vendorHex'); + } + if (productHex != null) { + parts.add('PID:$productHex'); + } + return '${parts.first} (${parts.skip(1).join(' ')})'; +} + +String buildUsbDisplayLabel({ + required String basePortLabel, + String? deviceName, +}) { + final trimmedName = deviceName?.trim() ?? ''; + if (trimmedName.isEmpty) { + return basePortLabel; + } + return '$basePortLabel - $trimmedName'; +} diff --git a/test/services/usb_serial_frame_codec_test.dart b/test/services/usb_serial_frame_codec_test.dart new file mode 100644 index 0000000..f0ce186 --- /dev/null +++ b/test/services/usb_serial_frame_codec_test.dart @@ -0,0 +1,84 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/services/usb_serial_frame_codec.dart'; + +void main() { + test('wrapUsbSerialTxFrame prefixes tx header and payload length', () { + final packet = wrapUsbSerialTxFrame(Uint8List.fromList([0x16, 0x03])); + + expect( + packet, + orderedEquals([usbSerialTxFrameStart, 0x02, 0x00, 0x16, 0x03]), + ); + }); + + test('UsbSerialFrameDecoder buffers partial frames until complete', () { + final decoder = UsbSerialFrameDecoder(); + + final firstChunk = decoder.ingest( + Uint8List.fromList([usbSerialRxFrameStart, 0x03]), + ); + final secondChunk = decoder.ingest( + Uint8List.fromList([0x00, 0x05, 0x06, 0x07]), + ); + + expect(firstChunk, isEmpty); + expect(secondChunk, hasLength(1)); + expect(secondChunk.single.isRxFrame, isTrue); + expect(secondChunk.single.payload, orderedEquals([0x05, 0x06, 0x07])); + }); + + test( + 'UsbSerialFrameDecoder drops leading noise and parses multiple frames', + () { + final decoder = UsbSerialFrameDecoder(); + + final packets = decoder.ingest( + Uint8List.fromList([ + 0x00, + 0x01, + usbSerialRxFrameStart, + 0x01, + 0x00, + 0x55, + usbSerialRxFrameStart, + 0x02, + 0x00, + 0x66, + 0x77, + ]), + ); + + expect(packets, hasLength(2)); + expect(packets[0].payload, orderedEquals([0x55])); + expect(packets[1].payload, orderedEquals([0x66, 0x77])); + }, + ); + + test( + 'UsbSerialFrameDecoder preserves tx packets so caller can ignore them', + () { + final decoder = UsbSerialFrameDecoder(); + + final packets = decoder.ingest( + Uint8List.fromList([ + usbSerialTxFrameStart, + 0x01, + 0x00, + 0x22, + usbSerialRxFrameStart, + 0x01, + 0x00, + 0x33, + ]), + ); + + expect(packets, hasLength(2)); + expect(packets[0].isRxFrame, isFalse); + expect(packets[0].payload, orderedEquals([0x22])); + expect(packets[1].isRxFrame, isTrue); + expect(packets[1].payload, orderedEquals([0x33])); + }, + ); +} diff --git a/test/utils/usb_port_labels_test.dart b/test/utils/usb_port_labels_test.dart new file mode 100644 index 0000000..4fef509 --- /dev/null +++ b/test/utils/usb_port_labels_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/utils/usb_port_labels.dart'; + +void main() { + test('normalizeUsbPortName strips friendly suffix from composite label', () { + expect( + normalizeUsbPortName( + 'COM6 - USB Serial Device (COM6) - USB\\VID_2886&PID_1667', + ), + 'COM6', + ); + }); + + test('friendlyUsbPortName prefers suffix when present', () { + expect( + friendlyUsbPortName( + 'COM6 - USB Serial Device (COM6) - USB\\VID_2886&PID_1667', + ), + 'USB Serial Device (COM6) - USB\\VID_2886&PID_1667', + ); + }); + + test( + 'friendlyUsbPortName falls back to normalized port when suffix is empty', + () { + expect(friendlyUsbPortName('COM6 - '), 'COM6'); + }, + ); + + test('describeWebUsbPort uses known VID/PID names when available', () { + expect( + describeWebUsbPort( + vendorId: 0x2886, + productId: 0x1667, + knownUsbNames: const { + '2886:1667': 'Seeed Wio Tracker L1', + }, + ), + 'Seeed Wio Tracker L1 (VID:2886 PID:1667)', + ); + }); + + test('describeWebUsbPort falls back to generic label for unknown device', () { + expect( + describeWebUsbPort(vendorId: 0x1234, productId: 0x5678), + 'Web Serial Device (VID:1234 PID:5678)', + ); + }); + + test('describeWebUsbPort returns chooser label when no usb ids exist', () { + expect( + describeWebUsbPort(vendorId: null, productId: null), + usbRequestPortLabel, + ); + }); + + test('buildUsbDisplayLabel appends device-reported name when available', () { + expect( + buildUsbDisplayLabel( + basePortLabel: 'Seeed Wio Tracker L1 (VID:2886 PID:1667)', + deviceName: 'KD3CGK mesh-utility.org', + ), + 'Seeed Wio Tracker L1 (VID:2886 PID:1667) - KD3CGK mesh-utility.org', + ); + }); + + test('buildUsbDisplayLabel keeps base label when custom name is blank', () { + expect( + buildUsbDisplayLabel( + basePortLabel: 'Seeed Wio Tracker L1 (VID:2886 PID:1667)', + deviceName: ' ', + ), + 'Seeed Wio Tracker L1 (VID:2886 PID:1667)', + ); + }); +} From 5f4333398e0bc9cfe8d91e2fc6441cb6830abc45 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 00:41:52 -0500 Subject: [PATCH 226/421] Enhance Bluetooth scanning and notification handling for web platform --- lib/connector/meshcore_connector.dart | 36 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d7731f1..44c5170 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -738,7 +738,7 @@ class MeshCoreConnector extends ChangeNotifier { }); await FlutterBluePlus.startScan( - withKeywords: ["MeshCore-", "Whisper-"], + withKeywords: ["MeshCore-", "Whisper-", "Wismesh-", "WisCore-"], webOptionalServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, @@ -836,18 +836,30 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - // Retry setNotifyValue with increasing delays - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); + if (PlatformInfo.isWeb) { + debugPrint('Starting setNotifyValue(true)'); + debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + unawaited(() async { + try { + await _txCharacteristic!.setNotifyValue(true); + } catch (error) { + debugPrint('Web setNotifyValue error (ignoring): $error'); + } + }()); + debugPrint('setNotifyValue(true) configuration completed'); + } else { + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); + } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; } } _notifySubscription = _txCharacteristic!.onValueReceived.listen( From f462815775073eeaef81438fb125af45c8ae9d57 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 00:57:28 -0500 Subject: [PATCH 227/421] Refactor USB connection handling and improve notification setup --- lib/connector/meshcore_connector.dart | 84 +++++++++++++++------------ 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 44c5170..4b5fe4f 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -738,16 +738,14 @@ class MeshCoreConnector extends ChangeNotifier { }); await FlutterBluePlus.startScan( - withKeywords: ["MeshCore-", "Whisper-", "Wismesh-", "WisCore-"], + withKeywords: ["MeshCore-", "Whisper-"], webOptionalServices: [Guid(MeshCoreUuids.service)], timeout: timeout, androidScanMode: AndroidScanMode.lowLatency, ); await Future.delayed(timeout); - if (!PlatformInfo.isWeb) { - await stopScan(); - } + await stopScan(); } Future stopScan() async { @@ -836,30 +834,17 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - if (PlatformInfo.isWeb) { - debugPrint('Starting setNotifyValue(true)'); - debugPrint('Web: Calling setNotifyValue(true) without awaiting'); - unawaited(() async { - try { - await _txCharacteristic!.setNotifyValue(true); - } catch (error) { - debugPrint('Web setNotifyValue error (ignoring): $error'); - } - }()); - debugPrint('setNotifyValue(true) configuration completed'); - } else { - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); - } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } } _notifySubscription = _txCharacteristic!.onValueReceived.listen( @@ -867,8 +852,6 @@ class MeshCoreConnector extends ChangeNotifier { ); _setState(MeshCoreConnectionState.connected); - _hasReceivedDeviceInfo = false; - _pendingInitialChannelSync = true; await _requestDeviceInfo(); _startBatteryPolling(); @@ -882,6 +865,9 @@ class MeshCoreConnector extends ChangeNotifier { // Keep device clock aligned on every connection. await syncTime(); + + // Fetch channels so we can track unread counts for incoming messages + unawaited(getChannels()); } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -904,6 +890,7 @@ class MeshCoreConnector extends ChangeNotifier { await stopScan(); _cancelReconnectTimer(); _manualDisconnect = false; + _resetConnectionHandshakeState(); _activeTransport = MeshCoreTransportType.usb; _activeUsbPort = portName; unawaited(_backgroundService?.start()); @@ -929,16 +916,20 @@ class MeshCoreConnector extends ChangeNotifier { ); _setState(MeshCoreConnectionState.connected); - _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = true; await _requestDeviceInfo(); _startBatteryPolling(); - final gotSelfInfo = await _waitForSelfInfo( + var gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), ); if (!gotSelfInfo) { await refreshDeviceInfo(); - await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + } + if (!gotSelfInfo) { + throw StateError('Timed out waiting for SELF_INFO during connect'); } await syncTime(); @@ -980,6 +971,19 @@ class MeshCoreConnector extends ChangeNotifier { return result; } + void _resetConnectionHandshakeState() { + _selfPublicKey = null; + _selfName = null; + _selfLatitude = null; + _selfLongitude = null; + _awaitingSelfInfo = false; + _selfInfoRetryTimer?.cancel(); + _selfInfoRetryTimer = null; + _hasReceivedDeviceInfo = false; + _pendingInitialChannelSync = false; + _hasReceivedDeviceInfo = false; + } + bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null && @@ -2119,12 +2123,16 @@ class MeshCoreConnector extends ChangeNotifier { // Auto-fetch contacts after getting self info getContacts(); - _maybeStartInitialChannelSync(); + if (_activeTransport == MeshCoreTransportType.usb) { + _maybeStartInitialChannelSync(); + } } void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; - _hasReceivedDeviceInfo = true; + if (_activeTransport == MeshCoreTransportType.usb) { + _hasReceivedDeviceInfo = true; + } _firmwareVerCode = frame[1]; // Parse client_repeat from firmware v9+ (byte 80) @@ -2148,13 +2156,17 @@ class MeshCoreConnector extends ChangeNotifier { if (nextMaxChannels > previousMaxChannels) { unawaited(loadChannelSettings(maxChannels: nextMaxChannels)); unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels)); - if (isConnected && !_pendingInitialChannelSync) { + if (isConnected && + (_activeTransport != MeshCoreTransportType.usb || + !_pendingInitialChannelSync)) { unawaited(getChannels(maxChannels: nextMaxChannels)); } } } notifyListeners(); - _maybeStartInitialChannelSync(); + if (_activeTransport == MeshCoreTransportType.usb) { + _maybeStartInitialChannelSync(); + } } void _maybeStartInitialChannelSync() { From 98f7c3b088d9daae2a9ca8b7595665388f280726 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 01:24:33 -0500 Subject: [PATCH 228/421] Refactor USB handling to improve connection management and error cleanup --- .../meshcore/meshcore_open/MainActivity.kt | 20 ++++++--- lib/connector/meshcore_connector.dart | 1 - lib/services/usb_serial_frame_codec.dart | 43 ++++++++++++++----- lib/services/usb_serial_service_web.dart | 33 ++++++++++++++ 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index b327b06..11bca61 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -21,6 +21,8 @@ import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors class MainActivity : FlutterActivity() { private val usbMethodChannelName = "meshcore_open/android_usb_serial" @@ -29,6 +31,7 @@ class MainActivity : FlutterActivity() { private lateinit var usbManager: UsbManager private val mainHandler = Handler(Looper.getMainLooper()) + private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor() private var eventSink: EventChannel.EventSink? = null private var usbConnection: UsbDeviceConnection? = null @@ -112,6 +115,7 @@ class MainActivity : FlutterActivity() { override fun onDestroy() { closeUsbConnection() + usbIoExecutor.shutdownNow() unregisterReceiver(permissionReceiver) super.onDestroy() } @@ -191,11 +195,17 @@ class MainActivity : FlutterActivity() { return } - try { - port.write(data, 1000) - result.success(null) - } catch (error: Exception) { - result.error("usb_write_failed", error.message, null) + usbIoExecutor.execute { + try { + port.write(data, 1000) + mainHandler.post { + result.success(null) + } + } catch (error: Exception) { + mainHandler.post { + result.error("usb_write_failed", error.message, null) + } + } } } diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 4b5fe4f..392be9a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -981,7 +981,6 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; - _hasReceivedDeviceInfo = false; } bool get _shouldAutoReconnect => diff --git a/lib/services/usb_serial_frame_codec.dart b/lib/services/usb_serial_frame_codec.dart index ee4a17c..f2ddbb6 100644 --- a/lib/services/usb_serial_frame_codec.dart +++ b/lib/services/usb_serial_frame_codec.dart @@ -27,6 +27,7 @@ class UsbSerialDecodedPacket { class UsbSerialFrameDecoder { final List _rxBuffer = []; + int _startIndex = 0; List ingest(Uint8List bytes) { if (bytes.isEmpty) { @@ -37,34 +38,56 @@ class UsbSerialFrameDecoder { final packets = []; while (true) { - if (_rxBuffer.isEmpty) { + if (_startIndex >= _rxBuffer.length) { + _rxBuffer.clear(); + _startIndex = 0; return packets; } - if (_rxBuffer.first != usbSerialRxFrameStart && - _rxBuffer.first != usbSerialTxFrameStart) { - _rxBuffer.removeAt(0); + if (_rxBuffer[_startIndex] != usbSerialRxFrameStart && + _rxBuffer[_startIndex] != usbSerialTxFrameStart) { + _startIndex++; + _compactBufferIfNeeded(); continue; } - if (_rxBuffer.length < usbSerialHeaderLength) { + final availableLength = _rxBuffer.length - _startIndex; + if (availableLength < usbSerialHeaderLength) { + _compactBufferIfNeeded(force: true); return packets; } - final payloadLength = _rxBuffer[1] | (_rxBuffer[2] << 8); + final payloadLength = + _rxBuffer[_startIndex + 1] | (_rxBuffer[_startIndex + 2] << 8); final packetLength = usbSerialHeaderLength + payloadLength; - if (_rxBuffer.length < packetLength) { + if (availableLength < packetLength) { + _compactBufferIfNeeded(force: true); return packets; } - final frameStart = _rxBuffer.first; + final frameStart = _rxBuffer[_startIndex]; final payload = Uint8List.fromList( - _rxBuffer.sublist(usbSerialHeaderLength, packetLength), + _rxBuffer.sublist( + _startIndex + usbSerialHeaderLength, + _startIndex + packetLength, + ), ); - _rxBuffer.removeRange(0, packetLength); + _startIndex += packetLength; + _compactBufferIfNeeded(); packets.add( UsbSerialDecodedPacket(frameStart: frameStart, payload: payload), ); } } + + void _compactBufferIfNeeded({bool force = false}) { + if (_startIndex == 0) { + return; + } + if (!force && _startIndex < 1024 && _startIndex < (_rxBuffer.length ~/ 2)) { + return; + } + _rxBuffer.removeRange(0, _startIndex); + _startIndex = 0; + } } diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 67844df..1f0fcb9 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -88,8 +88,10 @@ class UsbSerialService { debugPrint('USB serial opened port=$_connectedPortName via Web Serial'); } catch (error) { + await _cleanupFailedConnect(); _status = UsbSerialStatus.disconnected; _connectedPortName = null; + _connectedPortKey = null; rethrow; } } @@ -205,6 +207,37 @@ class UsbSerialService { return port.callMethod>('open'.toJS, options).toDart; } + Future _cleanupFailedConnect() async { + final reader = _reader; + final writer = _writer; + final port = _port; + + _reader = null; + _writer = null; + _port = null; + + if (reader != null) { + try { + await reader.callMethod>('cancel'.toJS).toDart; + } catch (_) { + // Ignore cleanup errors after a failed connect. + } + _releaseLock(reader); + } + + if (writer != null) { + _releaseLock(writer); + } + + if (port != null) { + try { + await port.callMethod>('close'.toJS).toDart; + } catch (_) { + // Ignore cleanup errors after a failed connect. + } + } + } + JSObject? _getReader(JSObject port) { final readable = port.getProperty('readable'.toJS); if (readable == null) { From ee3af52c0fd99d5444553d2e846671c0cc83eeee Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 01:50:38 -0500 Subject: [PATCH 229/421] Add initial contacts sync handling for web Bluetooth transport --- lib/connector/meshcore_connector.dart | 128 +++++++++++++++++++------- 1 file changed, 95 insertions(+), 33 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 392be9a..4b39068 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -164,6 +164,7 @@ class MeshCoreConnector extends ChangeNotifier { bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; bool _pendingInitialChannelSync = false; + bool _pendingInitialContactsSync = false; bool _preserveContactsOnRefresh = false; static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; @@ -783,6 +784,9 @@ class MeshCoreConnector extends ChangeNotifier { _lastDeviceDisplayName = _deviceDisplayName; _manualDisconnect = false; _cancelReconnectTimer(); + if (PlatformInfo.isWeb) { + _resetConnectionHandshakeState(); + } unawaited(_backgroundService?.start()); notifyListeners(); @@ -799,12 +803,14 @@ class MeshCoreConnector extends ChangeNotifier { license: License.free, ); - // Request larger MTU for sending larger frames - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); + // Request larger MTU only on native platforms; web does not support it. + if (!PlatformInfo.isWeb) { + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); + } } List services = await device.discoverServices(); @@ -834,17 +840,30 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); + if (PlatformInfo.isWeb) { + debugPrint('Starting setNotifyValue(true)'); + debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + unawaited(() async { + try { + await _txCharacteristic!.setNotifyValue(true); + } catch (error) { + debugPrint('Web setNotifyValue error (ignoring): $error'); + } + }()); + debugPrint('setNotifyValue(true) configuration completed'); + } else { + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); + } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; } } _notifySubscription = _txCharacteristic!.onValueReceived.listen( @@ -852,22 +871,34 @@ class MeshCoreConnector extends ChangeNotifier { ); _setState(MeshCoreConnectionState.connected); + if (_shouldGateInitialChannelSync) { + _hasReceivedDeviceInfo = false; + _pendingInitialChannelSync = true; + } await _requestDeviceInfo(); _startBatteryPolling(); - final gotSelfInfo = await _waitForSelfInfo( - timeout: const Duration(seconds: 3), - ); - if (!gotSelfInfo) { - await refreshDeviceInfo(); - await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + // Chrome's Web Bluetooth stack commonly delays incoming notifications + // until the non-blocking notify setup settles. Avoid stacking extra + // startup writes while that is happening. + } else { + final gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + } + + unawaited(syncTime()); } - // Keep device clock aligned on every connection. - await syncTime(); - // Fetch channels so we can track unread counts for incoming messages - unawaited(getChannels()); + if (!_shouldGateInitialChannelSync) { + unawaited(getChannels()); + } } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -981,6 +1012,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; } bool get _shouldAutoReconnect => @@ -988,6 +1020,11 @@ class MeshCoreConnector extends ChangeNotifier { _lastDeviceId != null && _activeTransport == MeshCoreTransportType.bluetooth; + bool get _shouldGateInitialChannelSync => + _activeTransport == MeshCoreTransportType.usb || + (_activeTransport == MeshCoreTransportType.bluetooth && + PlatformInfo.isWeb); + void _cancelReconnectTimer() { _reconnectTimer?.cancel(); _reconnectTimer = null; @@ -1087,6 +1124,7 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; @@ -1191,6 +1229,10 @@ class MeshCoreConnector extends ChangeNotifier { void _scheduleSelfInfoRetry() { _selfInfoRetryTimer?.cancel(); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + return; + } _selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), ( timer, ) { @@ -1974,6 +2016,12 @@ class MeshCoreConnector extends ChangeNotifier { _preserveContactsOnRefresh = false; notifyListeners(); unawaited(_persistContacts()); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + _isSyncingChannels && + !_channelSyncInFlight) { + unawaited(_requestNextChannel()); + } if (!_didInitialQueueSync || _pendingQueueSync) { _didInitialQueueSync = true; _pendingQueueSync = false; @@ -2120,16 +2168,22 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; notifyListeners(); - // Auto-fetch contacts after getting self info - getContacts(); - if (_activeTransport == MeshCoreTransportType.usb) { + // Auto-fetch contacts after getting self info. On web BLE, defer this + // until after channel 0 so startup writes stay serialized. + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + _pendingInitialContactsSync = true; + } else { + getContacts(); + } + if (_shouldGateInitialChannelSync) { _maybeStartInitialChannelSync(); } } void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; - if (_activeTransport == MeshCoreTransportType.usb) { + if (_shouldGateInitialChannelSync) { _hasReceivedDeviceInfo = true; } _firmwareVerCode = frame[1]; @@ -2156,14 +2210,13 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(loadChannelSettings(maxChannels: nextMaxChannels)); unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels)); if (isConnected && - (_activeTransport != MeshCoreTransportType.usb || - !_pendingInitialChannelSync)) { + (!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) { unawaited(getChannels(maxChannels: nextMaxChannels)); } } } notifyListeners(); - if (_activeTransport == MeshCoreTransportType.usb) { + if (_shouldGateInitialChannelSync) { _maybeStartInitialChannelSync(); } } @@ -3075,6 +3128,14 @@ class MeshCoreConnector extends ChangeNotifier { // Move to next channel _nextChannelIndexToRequest++; + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + channel.index == 0 && + _pendingInitialContactsSync) { + _pendingInitialContactsSync = false; + unawaited(getContacts()); + return; + } unawaited(_requestNextChannel()); return; } else { @@ -3738,6 +3799,7 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; From 2d1160d992de1c845d7dbb13477b21c089fba59a Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 02:34:46 -0500 Subject: [PATCH 230/421] Enhance BLE connection handling and improve USB connection messaging - Wrapped BLE scan and connection methods in try-catch blocks to handle errors gracefully and provide debug output. - Added retry logic for service discovery on web platforms after transient disconnections. - Updated USB connection messages in multiple languages to reflect active support on Android and desktop platforms. - Improved loading indicators for contacts screen to show a spinner during data loading. --- .../meshcore/meshcore_open/MainActivity.kt | 59 +++++++++++++++- lib/connector/meshcore_connector.dart | 68 +++++++++++++++---- lib/l10n/app_bg.arb | 8 +-- lib/l10n/app_de.arb | 8 +-- lib/l10n/app_es.arb | 10 +-- lib/l10n/app_fr.arb | 10 +-- lib/l10n/app_it.arb | 8 +-- lib/l10n/app_localizations_bg.dart | 8 +-- lib/l10n/app_localizations_de.dart | 8 +-- lib/l10n/app_localizations_es.dart | 11 ++- lib/l10n/app_localizations_fr.dart | 11 ++- lib/l10n/app_localizations_it.dart | 8 +-- lib/l10n/app_localizations_nl.dart | 8 +-- lib/l10n/app_localizations_pl.dart | 8 +-- lib/l10n/app_localizations_pt.dart | 8 +-- lib/l10n/app_localizations_ru.dart | 11 ++- lib/l10n/app_localizations_sk.dart | 8 +-- lib/l10n/app_localizations_sl.dart | 10 +-- lib/l10n/app_localizations_sv.dart | 8 +-- lib/l10n/app_localizations_uk.dart | 9 ++- lib/l10n/app_localizations_zh.dart | 8 +-- lib/l10n/app_nl.arb | 8 +-- lib/l10n/app_pl.arb | 8 +-- lib/l10n/app_pt.arb | 8 +-- lib/l10n/app_ru.arb | 10 +-- lib/l10n/app_sk.arb | 8 +-- lib/l10n/app_sl.arb | 10 +-- lib/l10n/app_sv.arb | 8 +-- lib/l10n/app_uk.arb | 8 +-- lib/l10n/app_zh.arb | 8 +-- lib/screens/contacts_screen.dart | 9 ++- 31 files changed, 240 insertions(+), 140 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index 11bca61..dec4e28 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -37,6 +37,7 @@ class MainActivity : FlutterActivity() { private var usbConnection: UsbDeviceConnection? = null private var usbPort: UsbSerialPort? = null private var ioManager: SerialInputOutputManager? = null + private var connectedDeviceName: String? = null private var pendingConnectResult: MethodChannel.Result? = null private var pendingConnectPortName: String? = null @@ -45,7 +46,19 @@ class MainActivity : FlutterActivity() { private val permissionReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action != usbPermissionAction) { + when (intent?.action) { + UsbManager.ACTION_USB_DEVICE_DETACHED -> { + handleUsbDetached(intent) + return + } + usbPermissionAction -> { + } + else -> { + return + } + } + + if (intent.action != usbPermissionAction) { return } @@ -116,12 +129,19 @@ class MainActivity : FlutterActivity() { override fun onDestroy() { closeUsbConnection() usbIoExecutor.shutdownNow() - unregisterReceiver(permissionReceiver) + try { + unregisterReceiver(permissionReceiver) + } catch (_: IllegalArgumentException) { + } super.onDestroy() } private fun registerUsbPermissionReceiver() { - val filter = IntentFilter(usbPermissionAction) + val filter = + IntentFilter().apply { + addAction(usbPermissionAction) + addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(permissionReceiver, filter, RECEIVER_NOT_EXPORTED) } else { @@ -256,6 +276,7 @@ class MainActivity : FlutterActivity() { usbConnection = connection usbPort = port + connectedDeviceName = device.deviceName ioManager = SerialInputOutputManager( @@ -311,6 +332,38 @@ class MainActivity : FlutterActivity() { } catch (_: Exception) { } usbConnection = null + connectedDeviceName = null + } + + private fun handleUsbDetached(intent: Intent) { + val detachedDevice = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + } + + val detachedName = detachedDevice?.deviceName ?: return + + if (pendingConnectPortName == detachedName) { + pendingConnectResult?.error( + "usb_device_detached", + "USB device was removed before the connection completed", + null, + ) + pendingConnectResult = null + pendingConnectPortName = null + } + + if (connectedDeviceName == detachedName) { + closeUsbConnection() + eventSink?.error( + "usb_device_detached", + "USB device was disconnected", + null, + ) + } } private fun pendingIntentFlags(): Int { diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 4b39068..9e0eff6 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -738,12 +738,18 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); }); - await FlutterBluePlus.startScan( - withKeywords: ["MeshCore-", "Whisper-"], - webOptionalServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); + try { + await FlutterBluePlus.startScan( + withKeywords: ["MeshCore-", "Whisper-"], + webOptionalServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); + } catch (error) { + debugPrint('[BLE Scan] Scan/picker failure: $error'); + _setState(MeshCoreConnectionState.disconnected); + rethrow; + } await Future.delayed(timeout); await stopScan(); @@ -791,17 +797,24 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); try { + final connectLabel = _deviceDisplayName ?? _deviceId; + debugPrint('[BLE Connect] Starting connect to $connectLabel'); _connectionSubscription = device.connectionState.listen((state) { if (state == BluetoothConnectionState.disconnected && isConnected) { _handleDisconnection(); } }); - await device.connect( - timeout: const Duration(seconds: 15), - mtu: null, - license: License.free, - ); + try { + await device.connect( + timeout: const Duration(seconds: 15), + mtu: null, + license: License.free, + ); + } catch (error) { + debugPrint('[BLE Connect] device.connect() failure: $error'); + rethrow; + } // Request larger MTU only on native platforms; web does not support it. if (!PlatformInfo.isWeb) { @@ -813,7 +826,27 @@ class MeshCoreConnector extends ChangeNotifier { } } - List services = await device.discoverServices(); + late final List services; + try { + services = await device.discoverServices(); + } catch (error) { + debugPrint('[BLE Connect] service discovery failure: $error'); + if (PlatformInfo.isWeb && + error.toString().contains('GATT Server is disconnected')) { + debugPrint( + '[BLE Connect] retrying service discovery after transient web disconnect', + ); + await Future.delayed(const Duration(milliseconds: 300)); + await device.connect( + timeout: const Duration(seconds: 15), + mtu: null, + license: License.free, + ); + services = await device.discoverServices(); + } else { + rethrow; + } + } BluetoothService? uartService; for (var service in services) { @@ -847,6 +880,7 @@ class MeshCoreConnector extends ChangeNotifier { try { await _txCharacteristic!.setNotifyValue(true); } catch (error) { + debugPrint('[BLE Connect] notify failure (web, ignored): $error'); debugPrint('Web setNotifyValue error (ignoring): $error'); } }()); @@ -861,6 +895,7 @@ class MeshCoreConnector extends ChangeNotifier { await _txCharacteristic!.setNotifyValue(true); notifySet = true; } catch (e) { + debugPrint('[BLE Connect] notify failure: $e'); debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); if (attempt == 2) rethrow; } @@ -1231,6 +1266,15 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer?.cancel(); if (PlatformInfo.isWeb && _activeTransport == MeshCoreTransportType.bluetooth) { + _selfInfoRetryTimer = Timer(const Duration(seconds: 10), () { + if (!isConnected || !_awaitingSelfInfo) { + return; + } + if (_isLoadingContacts || _isSyncingChannels || _channelSyncInFlight) { + return; + } + unawaited(sendFrame(buildAppStartFrame())); + }); return; } _selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), ( diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 0d64508..8c99338 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1806,9 +1806,9 @@ "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceTitle": "Изберете метода на връзка.", "connectionChoiceSubtitle": "Изберете как искате да получите вашия устройство MeshCore.", - "usbScreenTitle": "Връзката чрез USB ще бъде налична скоро.", - "usbScreenSubtitle": "Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.", - "usbScreenStatus": "Ще бъде достъпно скоро", - "usbScreenNote": "След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.", + "usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.", + "usbScreenStatus": "Изберете USB устройство", + "usbScreenTitle": "Свързване чрез USB", + "usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.", "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0c49f8d..44eca06 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1834,9 +1834,9 @@ "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceTitle": "Wählen Sie Ihre bevorzugte Verbindungsmethode.", - "usbScreenTitle": "Die USB-Verbindung wird bald verfügbar sein.", - "usbScreenSubtitle": "Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.", - "usbScreenStatus": "Bald verfügbar", - "usbScreenNote": "Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.", + "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", + "usbScreenNote": "USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", + "usbScreenTitle": "Über USB verbinden", + "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie." } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 48431e3..a69503a 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1834,9 +1834,9 @@ "connectionChoiceSubtitle": "Seleccione la forma en que desea acceder a su dispositivo MeshCore.", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "La conexión USB estará disponible próximamente.", - "usbScreenSubtitle": "Estamos creando una conexión en serie para dispositivos Android y de escritorio.", - "usbScreenStatus": "Próximamente", - "usbScreenNote": "Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.", - "usbScreenEmptyState": "No se detectaron dispositivos USB. Conecte uno y vuelva a intentar." + "usbScreenStatus": "Seleccione un dispositivo USB", + "usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.", + "usbScreenTitle": "Conecte mediante USB", + "usbScreenSubtitle": "Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.", + "usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a cargar." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 61f6551..2e22ee5 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1806,9 +1806,9 @@ "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", "connectionChoiceSubtitle": "Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.", - "usbScreenTitle": "La connexion USB sera disponible prochainement.", - "usbScreenSubtitle": "Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.", - "usbScreenStatus": "Bientôt", - "usbScreenNote": "Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.", - "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Connectez-en un et rafraîchissez." + "usbScreenStatus": "Sélectionnez un périphérique USB", + "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.", + "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.", + "usbScreenTitle": "Connectez via USB", + "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 827d1e7..14c37fa 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1806,9 +1806,9 @@ "connectionChoiceSubtitle": "Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "La connessione USB sarà disponibile a breve.", - "usbScreenSubtitle": "Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.", - "usbScreenStatus": "Arriverà presto", - "usbScreenNote": "Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.", + "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", + "usbScreenStatus": "Seleziona un dispositivo USB", + "usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.", + "usbScreenTitle": "Connessione tramite USB", "usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e riavviare." } diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 861bf6a..4eeedae 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -122,18 +122,18 @@ class AppLocalizationsBg extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Връзката чрез USB ще бъде налична скоро.'; + String get usbScreenTitle => 'Свързване чрез USB'; @override String get usbScreenSubtitle => - 'Създаваме път за комуникация, базиран на последователно предаване на данни, за Android и настолни компютри.'; + 'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.'; @override - String get usbScreenStatus => 'Ще бъде достъпно скоро'; + String get usbScreenStatus => 'Изберете USB устройство'; @override String get usbScreenNote => - 'След като бъде внедрена поддръжката за USB, ще изберете сериен порт и ще се свържете директно към вашето устройство MeshCore.'; + 'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index f768dbb..a3ab54d 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -123,18 +123,18 @@ class AppLocalizationsDe extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Die USB-Verbindung wird bald verfügbar sein.'; + String get usbScreenTitle => 'Über USB verbinden'; @override String get usbScreenSubtitle => - 'Wir entwickeln eine Verbindung, die sowohl für Android- als auch für Desktop-Geräte geeignet ist und auf einer seriellen Schnittstelle basiert.'; + 'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.'; @override - String get usbScreenStatus => 'Bald verfügbar'; + String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus'; @override String get usbScreenNote => - 'Sobald die USB-Unterstützung implementiert ist, wählen Sie einen seriellen Anschluss und verbinden Sie ihn direkt mit Ihrem MeshCore-Gerät.'; + 'USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 32680ab..cc7261a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -122,23 +122,22 @@ class AppLocalizationsEs extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => - 'La conexión USB estará disponible próximamente.'; + String get usbScreenTitle => 'Conecte mediante USB'; @override String get usbScreenSubtitle => - 'Estamos creando una conexión en serie para dispositivos Android y de escritorio.'; + 'Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.'; @override - String get usbScreenStatus => 'Próximamente'; + String get usbScreenStatus => 'Seleccione un dispositivo USB'; @override String get usbScreenNote => - 'Una vez que se implemente el soporte para USB, seleccionará un puerto serie y se conectará directamente a su dispositivo MeshCore.'; + 'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.'; @override String get usbScreenEmptyState => - 'No se detectaron dispositivos USB. Conecte uno y vuelva a intentar.'; + 'No se encontraron dispositivos USB. Conecte uno y vuelva a cargar.'; @override String get scanner_scanning => 'Escaneando dispositivos...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index dae3478..5f7d70f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -122,23 +122,22 @@ class AppLocalizationsFr extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => - 'La connexion USB sera disponible prochainement.'; + String get usbScreenTitle => 'Connectez via USB'; @override String get usbScreenSubtitle => - 'Nous mettons en place un chemin de connexion basé sur une série pour les appareils Android et les ordinateurs de bureau.'; + 'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.'; @override - String get usbScreenStatus => 'Bientôt'; + String get usbScreenStatus => 'Sélectionnez un périphérique USB'; @override String get usbScreenNote => - 'Une fois que le support USB sera disponible, vous sélectionnerez un port série et vous connecterez directement à votre appareil MeshCore.'; + 'La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.'; @override String get usbScreenEmptyState => - 'Aucun périphérique USB n\'a été trouvé. Connectez-en un et rafraîchissez.'; + 'Aucun périphérique USB n\'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.'; @override String get scanner_scanning => 'Recherche de périphériques...'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b138671..aef53e1 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -123,18 +123,18 @@ class AppLocalizationsIt extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'La connessione USB sarà disponibile a breve.'; + String get usbScreenTitle => 'Connessione tramite USB'; @override String get usbScreenSubtitle => - 'Stiamo sviluppando un percorso di connessione basato su serie per Android e per i desktop.'; + 'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.'; @override - String get usbScreenStatus => 'Arriverà presto'; + String get usbScreenStatus => 'Seleziona un dispositivo USB'; @override String get usbScreenNote => - 'Una volta che il supporto USB sarà disponibile, selezionerete una porta seriale e vi connetterete direttamente al vostro dispositivo MeshCore.'; + 'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index f582abc..b1c3452 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -122,18 +122,18 @@ class AppLocalizationsNl extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'USB-verbinding is binnenkort beschikbaar.'; + String get usbScreenTitle => 'Verbind via USB'; @override String get usbScreenSubtitle => - 'We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.'; + 'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; @override - String get usbScreenStatus => 'Komende week'; + String get usbScreenStatus => 'Selecteer een USB-apparaat'; @override String get usbScreenNote => - 'Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.'; + 'USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2f103b5..5b1712f 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -122,18 +122,18 @@ class AppLocalizationsPl extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Połączenie USB będzie dostępne wkrótce.'; + String get usbScreenTitle => 'Połącz przez USB'; @override String get usbScreenSubtitle => - 'Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.'; + 'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.'; @override - String get usbScreenStatus => 'Wkrótce'; + String get usbScreenStatus => 'Wybierz urządzenie USB'; @override String get usbScreenNote => - 'Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.'; + 'Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 283bada..1bf5647 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -122,18 +122,18 @@ class AppLocalizationsPt extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'A conexão USB estará disponível em breve.'; + String get usbScreenTitle => 'Conecte via USB'; @override String get usbScreenSubtitle => - 'Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.'; + 'Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.'; @override - String get usbScreenStatus => 'Em breve'; + String get usbScreenStatus => 'Selecione um dispositivo USB'; @override String get usbScreenNote => - 'Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.'; + 'A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1687782..6a25aa2 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -122,23 +122,22 @@ class AppLocalizationsRu extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => - 'Подключение через USB будет доступно в ближайшее время.'; + String get usbScreenTitle => 'Подключение через USB'; @override String get usbScreenSubtitle => - 'Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.'; + 'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.'; @override - String get usbScreenStatus => 'Скоро'; + String get usbScreenStatus => 'Выберите USB-устройство'; @override String get usbScreenNote => - 'Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.'; + 'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.'; @override String get usbScreenEmptyState => - 'Не обнаружено никаких устройств USB. Подключите одно из них и обновите список.'; + 'Не обнаружено устройств USB. Подключите одно из них и обновите список.'; @override String get scanner_scanning => 'Поиск устройств...'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8949090..a120a7d 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -122,18 +122,18 @@ class AppLocalizationsSk extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Pripojenie cez USB bude k dispozícii čoskoro.'; + String get usbScreenTitle => 'Pripojte cez USB'; @override String get usbScreenSubtitle => - 'Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.'; + 'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.'; @override - String get usbScreenStatus => 'Čoskoro'; + String get usbScreenStatus => 'Vyberte USB zariadenie'; @override String get usbScreenNote => - 'Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.'; + 'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index bd0c4d5..8969898 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -122,22 +122,22 @@ class AppLocalizationsSl extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Vnos preko USB-ja bo v kratkem na voljo.'; + String get usbScreenTitle => 'Povežite preko USB'; @override String get usbScreenSubtitle => - 'Gradimo pot za serijsko povezavo za Android in računalnike.'; + 'Izberite zaznano serijsko napravo in se neposredno povežite z vašim MeshCore-om.'; @override - String get usbScreenStatus => 'Čez kratko časa'; + String get usbScreenStatus => 'Izberite USB naprave.'; @override String get usbScreenNote => - 'Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.'; + 'USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.'; @override String get usbScreenEmptyState => - 'Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite.'; + 'Niti en USB naprave niso najdeni. Povežite eno in posodobite.'; @override String get scanner_scanning => 'Skeniram za naprave...'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 183250d..2cd731b 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -122,18 +122,18 @@ class AppLocalizationsSv extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'USB-anslutning kommer snart'; + String get usbScreenTitle => 'Anslut via USB'; @override String get usbScreenSubtitle => - 'Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.'; + 'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.'; @override - String get usbScreenStatus => 'Kommer snart'; + String get usbScreenStatus => 'Välj en USB-enhet'; @override String get usbScreenNote => - 'När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.'; + 'USB-seriell kommunikation är aktiv på stöderliga Android-enheter och skrivbordsplattformar.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 19feaac..08c2c0f 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -122,19 +122,18 @@ class AppLocalizationsUk extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => - 'Підключення через USB буде доступне найближчим часом.'; + String get usbScreenTitle => 'Підключити через USB'; @override String get usbScreenSubtitle => - 'Ми створюємо серійний шлях з\'єднання для Android та десктопних комп\'ютерів.'; + 'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.'; @override - String get usbScreenStatus => 'Скоро'; + String get usbScreenStatus => 'Виберіть пристрій USB'; @override String get usbScreenNote => - 'Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.'; + 'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index fdb9f1d..678c63b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -121,16 +121,16 @@ class AppLocalizationsZh extends AppLocalizations { String get connectionChoiceBluetoothLabel => '蓝牙'; @override - String get usbScreenTitle => 'USB 连接即将推出'; + String get usbScreenTitle => '通过USB连接'; @override - String get usbScreenSubtitle => '我们正在构建一个基于串行的连接路径,用于Android和桌面设备。'; + String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。'; @override - String get usbScreenStatus => '即将推出'; + String get usbScreenStatus => '选择一个 USB 设备'; @override - String get usbScreenNote => '一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。'; + String get usbScreenNote => '在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。'; @override String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 2f09405..338a3a2 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1806,9 +1806,9 @@ "connectionChoiceUsbLabel": "USB", "connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenTitle": "USB-verbinding is binnenkort beschikbaar.", - "usbScreenSubtitle": "We ontwikkelen een verbindingspad op basis van seriële communicatie, zowel voor Android als voor desktop-computers.", - "usbScreenStatus": "Komende week", - "usbScreenNote": "Zodra de USB-ondersteuning is geïnstalleerd, selecteert u een seriële poort en verbindt u direct met uw MeshCore-apparaat.", + "usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", + "usbScreenStatus": "Selecteer een USB-apparaat", + "usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.", + "usbScreenTitle": "Verbind via USB", "usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8988e02..6b47ad5 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1806,9 +1806,9 @@ "connectionChoiceSubtitle": "Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.", "connectionChoiceTitle": "Wybierz metodę połączenia.", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "Połączenie USB będzie dostępne wkrótce.", - "usbScreenSubtitle": "Tworzymy ścieżkę połączenia opartą na protokole szeregowym, przeznaczoną zarówno dla urządzeń z systemem Android, jak i dla komputerów stacjonarnych.", - "usbScreenStatus": "Wkrótce", - "usbScreenNote": "Po wdrożeniu wsparcia dla USB, wybierzesz port szeregowy i połączysz się bezpośrednio z urządzeniem MeshCore.", + "usbScreenTitle": "Połącz przez USB", + "usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.", + "usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.", + "usbScreenStatus": "Wybierz urządzenie USB", "usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 296590f..d6d50e8 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1806,9 +1806,9 @@ "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceTitle": "Escolha o método de conexão desejado.", - "usbScreenTitle": "A conexão USB estará disponível em breve.", - "usbScreenSubtitle": "Estamos criando um caminho de conexão baseado em série para dispositivos Android e de desktop.", - "usbScreenStatus": "Em breve", - "usbScreenNote": "Assim que o suporte USB for implementado, você poderá selecionar uma porta serial e conectar-se diretamente ao seu dispositivo MeshCore.", + "usbScreenNote": "A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.", + "usbScreenSubtitle": "Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.", + "usbScreenStatus": "Selecione um dispositivo USB", + "usbScreenTitle": "Conecte via USB", "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 65b3792..2b8ab81 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1046,9 +1046,9 @@ "connectionChoiceTitle": "Выберите способ подключения", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenTitle": "Подключение через USB будет доступно в ближайшее время.", - "usbScreenSubtitle": "Мы создаем последовательную схему подключения для устройств на базе Android и настольных компьютеров.", - "usbScreenStatus": "Скоро", - "usbScreenNote": "Как только появится поддержка USB, вы сможете выбрать последовательный порт и напрямую подключиться к вашему устройству MeshCore.", - "usbScreenEmptyState": "Не обнаружено никаких устройств USB. Подключите одно из них и обновите список." + "usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.", + "usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.", + "usbScreenStatus": "Выберите USB-устройство", + "usbScreenTitle": "Подключение через USB", + "usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 7178166..46fe7e7 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1806,9 +1806,9 @@ "connectionChoiceUsbLabel": "USB", "connectionChoiceTitle": "Vyberte si metódu prepojenia.", "connectionChoiceSubtitle": "Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.", - "usbScreenTitle": "Pripojenie cez USB bude k dispozícii čoskoro.", - "usbScreenSubtitle": "Vytvárajeme komunikačný systém založený na sériovej komunikácii pre Android a stolné počítače.", - "usbScreenStatus": "Čoskoro", - "usbScreenNote": "Po implementácii podpory pre USB, budete môcť vybrať sériový port a priamo sa pripojiť k vašmu zariadeniu MeshCore.", + "usbScreenStatus": "Vyberte USB zariadenie", + "usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.", + "usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.", + "usbScreenTitle": "Pripojte cez USB", "usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 416106a..1d8943e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1806,9 +1806,9 @@ "connectionChoiceUsbLabel": "USB", "connectionChoiceTitle": "Izberite svoj način povezave.", "connectionChoiceSubtitle": "Izberite, kako želite dostopati do svojega naprave MeshCore.", - "usbScreenTitle": "Vnos preko USB-ja bo v kratkem na voljo.", - "usbScreenSubtitle": "Gradimo pot za serijsko povezavo za Android in računalnike.", - "usbScreenStatus": "Čez kratko časa", - "usbScreenNote": "Ko bo podpora za USB na voljo, boste izbrali serijsko vrata in se neposredno povezali z vašim napravem MeshCore.", - "usbScreenEmptyState": "Niti en USB naprave niso bilo najdeno. Povežite eno in posodobite." + "usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašim MeshCore-om.", + "usbScreenTitle": "Povežite preko USB", + "usbScreenStatus": "Izberite USB naprave.", + "usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.", + "usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 9bbcd31..5a55bcb 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1806,9 +1806,9 @@ "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceSubtitle": "Välj hur du vill komma åt din MeshCore-enhet.", "connectionChoiceTitle": "Välj din anslutningsmetod", - "usbScreenTitle": "USB-anslutning kommer snart", - "usbScreenSubtitle": "Vi skapar en seriebaserad anslutningsväg för både Android- och skrivbordsenheter.", - "usbScreenStatus": "Kommer snart", - "usbScreenNote": "När USB-stöd är implementerat, kommer du att välja en seriell port och ansluta direkt till din MeshCore-enhet.", + "usbScreenTitle": "Anslut via USB", + "usbScreenNote": "USB-seriell kommunikation är aktiv på stöderliga Android-enheter och skrivbordsplattformar.", + "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", + "usbScreenStatus": "Välj en USB-enhet", "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 9a9919c..5b0bde5 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1806,9 +1806,9 @@ "connectionChoiceUsbLabel": "USB", "connectionChoiceTitle": "Виберіть спосіб зв'язку", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenTitle": "Підключення через USB буде доступне найближчим часом.", - "usbScreenSubtitle": "Ми створюємо серійний шлях з'єднання для Android та десктопних комп'ютерів.", - "usbScreenStatus": "Скоро", - "usbScreenNote": "Після того, як буде реалізовано підтримку USB, ви виберете серійний порт і підключитесь безпосередньо до вашого пристрою MeshCore.", + "usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", + "usbScreenTitle": "Підключити через USB", + "usbScreenStatus": "Виберіть пристрій USB", + "usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.", "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 660b221..c35cbaa 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1811,9 +1811,9 @@ "connectionChoiceBluetoothLabel": "蓝牙", "connectionChoiceTitle": "选择您的连接方式", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "USB 连接即将推出", - "usbScreenSubtitle": "我们正在构建一个基于串行的连接路径,用于Android和桌面设备。", - "usbScreenStatus": "即将推出", - "usbScreenNote": "一旦USB支持功能上线,您就可以选择一个串口,并直接连接到您的MeshCore设备。", + "usbScreenTitle": "通过USB连接", + "usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。", + "usbScreenStatus": "选择一个 USB 设备", + "usbScreenNote": "在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。", "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index eeecfb9..8c4cec4 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -384,8 +384,15 @@ class _ContactsScreenState extends State Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) { final contacts = connector.contacts; + final shouldShowStartupSpinner = + contacts.isEmpty && + _groups.isEmpty && + connector.isConnected && + (connector.isLoadingContacts || + connector.isLoadingChannels || + connector.selfPublicKey == null); - if (contacts.isEmpty && connector.isLoadingContacts && _groups.isEmpty) { + if (shouldShowStartupSpinner) { return const Center(child: CircularProgressIndicator()); } From 9a0572e8e44d8262626816088536136cd2e850a8 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 02:46:50 -0500 Subject: [PATCH 231/421] Add payload length validation in USB frame decoder --- lib/services/usb_serial_frame_codec.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/services/usb_serial_frame_codec.dart b/lib/services/usb_serial_frame_codec.dart index f2ddbb6..eb4c41e 100644 --- a/lib/services/usb_serial_frame_codec.dart +++ b/lib/services/usb_serial_frame_codec.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; const int usbSerialTxFrameStart = 0x3c; const int usbSerialRxFrameStart = 0x3e; const int usbSerialHeaderLength = 3; +const int usbSerialMaxPayloadLength = 172; Uint8List wrapUsbSerialTxFrame(Uint8List payload) { final packet = Uint8List(usbSerialHeaderLength + payload.length); @@ -59,6 +60,11 @@ class UsbSerialFrameDecoder { final payloadLength = _rxBuffer[_startIndex + 1] | (_rxBuffer[_startIndex + 2] << 8); + if (payloadLength > usbSerialMaxPayloadLength) { + _startIndex++; + _compactBufferIfNeeded(); + continue; + } final packetLength = usbSerialHeaderLength + payloadLength; if (availableLength < packetLength) { _compactBufferIfNeeded(force: true); From 115689ad956dfa3edbddbf9b85ec797294f19442 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:09:50 -0500 Subject: [PATCH 232/421] Improve USB connection handling by preventing connection attempts when already connected --- .../meshcore/meshcore_open/MainActivity.kt | 152 ++++++++++-------- lib/screens/usb_screen.dart | 7 + 2 files changed, 93 insertions(+), 66 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index dec4e28..3d91e71 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -238,75 +238,95 @@ class MainActivity : FlutterActivity() { baudRate: Int, result: MethodChannel.Result, ) { - try { - closeUsbConnection() + usbIoExecutor.execute { + try { + closeUsbConnection() - val driver = UsbSerialProber.getDefaultProber().probeDevice(device) - if (driver == null) { - result.error("usb_driver_missing", "No USB serial driver for ${device.deviceName}", null) - return - } - - val connection = usbManager.openDevice(device) - if (connection == null) { - result.error( - "usb_open_failed", - "UsbManager could not open ${device.deviceName}", - null, - ) - return - } - - val port = firstPort(driver) - if (port == null) { - connection.close() - result.error("usb_port_missing", "No USB serial port exposed by ${device.deviceName}", null) - return - } - - port.open(connection) - port.setParameters( - baudRate, - 8, - UsbSerialPort.STOPBITS_1, - UsbSerialPort.PARITY_NONE, - ) - port.rts = false - port.dtr = true - - usbConnection = connection - usbPort = port - connectedDeviceName = device.deviceName - - ioManager = - SerialInputOutputManager( - port, - object : SerialInputOutputManager.Listener { - override fun onNewData(data: ByteArray) { - mainHandler.post { - eventSink?.success(data) - } - } - - override fun onRunError(e: Exception) { - mainHandler.post { - eventSink?.error( - "usb_io_error", - e.message ?: "USB serial I/O error", - null, - ) - } - closeUsbConnection() - } - }, - ).also { manager -> - manager.start() + val driver = UsbSerialProber.getDefaultProber().probeDevice(device) + if (driver == null) { + mainHandler.post { + result.error( + "usb_driver_missing", + "No USB serial driver for ${device.deviceName}", + null, + ) + } + return@execute } - result.success(null) - } catch (error: Exception) { - closeUsbConnection() - result.error("usb_connect_failed", error.message, null) + val connection = usbManager.openDevice(device) + if (connection == null) { + mainHandler.post { + result.error( + "usb_open_failed", + "UsbManager could not open ${device.deviceName}", + null, + ) + } + return@execute + } + + val port = firstPort(driver) + if (port == null) { + connection.close() + mainHandler.post { + result.error( + "usb_port_missing", + "No USB serial port exposed by ${device.deviceName}", + null, + ) + } + return@execute + } + + port.open(connection) + port.setParameters( + baudRate, + 8, + UsbSerialPort.STOPBITS_1, + UsbSerialPort.PARITY_NONE, + ) + port.rts = false + port.dtr = true + + usbConnection = connection + usbPort = port + connectedDeviceName = device.deviceName + + ioManager = + SerialInputOutputManager( + port, + object : SerialInputOutputManager.Listener { + override fun onNewData(data: ByteArray) { + mainHandler.post { + eventSink?.success(data) + } + } + + override fun onRunError(e: Exception) { + mainHandler.post { + eventSink?.error( + "usb_io_error", + e.message ?: "USB serial I/O error", + null, + ) + } + closeUsbConnection() + } + }, + ).also { manager -> + manager.start() + } + + mainHandler.post { + result.success(null) + } + } catch (error: Exception) { + closeUsbConnection() + mainHandler.post { + result.error("usb_connect_failed", error.message, null) + } + } } } diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index c3c1066..fb62b95 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -425,6 +425,13 @@ class _UsbScreenState extends State { if (selectedPort == null || selectedPort.isEmpty) { return; } + if (_connector.state != MeshCoreConnectionState.disconnected) { + setState(() { + _isConnecting = false; + _errorText = null; + }); + return; + } final rawPortName = normalizeUsbPortName(selectedPort); setState(() { From 3542adad1ddbfe888608ade435ca55b70ffe41cc Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:13:47 -0500 Subject: [PATCH 233/421] Update USB communication note for clarity in Swedish localization --- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_sv.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 2cd731b..9bfc64d 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -133,7 +133,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbScreenNote => - 'USB-seriell kommunikation är aktiv på stöderliga Android-enheter och skrivbordsplattformar.'; + 'USB-seriell kommunikation är aktiv på kompatibla Android-enheter och datorplattformar.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 5a55bcb..f3e7dbd 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1807,7 +1807,7 @@ "connectionChoiceSubtitle": "Välj hur du vill komma åt din MeshCore-enhet.", "connectionChoiceTitle": "Välj din anslutningsmetod", "usbScreenTitle": "Anslut via USB", - "usbScreenNote": "USB-seriell kommunikation är aktiv på stöderliga Android-enheter och skrivbordsplattformar.", + "usbScreenNote": "USB-seriell kommunikation är aktiv på kompatibla Android-enheter och datorplattformar.", "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", "usbScreenStatus": "Välj en USB-enhet", "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera." From 3cec3dc233b9c10a956ff7ff3b4b326a7e9425e0 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:24:19 -0500 Subject: [PATCH 234/421] Improve USB disconnection handling and add payload length validation for USB frames --- .../kotlin/com/meshcore/meshcore_open/MainActivity.kt | 8 ++++++-- lib/services/usb_serial_frame_codec.dart | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index 3d91e71..5955744 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -105,8 +105,12 @@ class MainActivity : FlutterActivity() { "connect" -> handleUsbConnect(call, result) "write" -> handleUsbWrite(call, result) "disconnect" -> { - closeUsbConnection() - result.success(null) + usbIoExecutor.execute { + closeUsbConnection() + mainHandler.post { + result.success(null) + } + } } else -> result.notImplemented() } diff --git a/lib/services/usb_serial_frame_codec.dart b/lib/services/usb_serial_frame_codec.dart index eb4c41e..ebe1733 100644 --- a/lib/services/usb_serial_frame_codec.dart +++ b/lib/services/usb_serial_frame_codec.dart @@ -6,6 +6,13 @@ const int usbSerialHeaderLength = 3; const int usbSerialMaxPayloadLength = 172; Uint8List wrapUsbSerialTxFrame(Uint8List payload) { + if (payload.length > usbSerialMaxPayloadLength) { + throw ArgumentError.value( + payload.length, + 'payload.length', + 'USB serial payload exceeds $usbSerialMaxPayloadLength bytes', + ); + } final packet = Uint8List(usbSerialHeaderLength + payload.length); packet[0] = usbSerialTxFrameStart; packet[1] = payload.length & 0xff; From 612612795a1c39e16b3f4ef7c2483975dc3290a1 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:24:35 -0500 Subject: [PATCH 235/421] Update French localization for connection choice subtitle --- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2e22ee5..aa33073 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1805,7 +1805,7 @@ "connectionChoiceTitle": "Choisissez votre méthode de connexion.", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", - "connectionChoiceSubtitle": "Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.", + "connectionChoiceSubtitle": "Choisissez la méthode de connexion que vous préférez pour votre appareil MeshCore.", "usbScreenStatus": "Sélectionnez un périphérique USB", "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.", "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.", diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 5f7d70f..aaca233 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -113,7 +113,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get connectionChoiceSubtitle => - 'Choisissez la méthode de livraison que vous préférez pour votre appareil MeshCore.'; + 'Choisissez la méthode de connexion que vous préférez pour votre appareil MeshCore.'; @override String get connectionChoiceUsbLabel => 'USB'; From c041e0597292ba67bc4daec12487ceff084388af Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:39:21 -0500 Subject: [PATCH 236/421] Improve error message for unavailable RX characteristic in USB communication --- lib/connector/meshcore_connector.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9e0eff6..39a34f2 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1196,7 +1196,7 @@ class MeshCoreConnector extends ChangeNotifier { await _usbSerialService.write(data); } else { if (_rxCharacteristic == null) { - throw Exception("MeshCore RX characteristic does not support write"); + throw Exception("MeshCore RX characteristic not available"); } // Prefer write without response when supported; fall back to write with response. final properties = _rxCharacteristic!.properties; From 47c4e0fb8252f9752995d1a8740c34d4df89e1cd Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:47:38 -0500 Subject: [PATCH 237/421] Fix USB permission receiver registration for compatibility with Android Tiramisu --- .../src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt | 2 +- lib/services/usb_serial_service_web.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index 5955744..7ce40d4 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -147,7 +147,7 @@ class MainActivity : FlutterActivity() { addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(permissionReceiver, filter, RECEIVER_NOT_EXPORTED) + registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) } else { @Suppress("DEPRECATION") registerReceiver(permissionReceiver, filter) diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 1f0fcb9..5ceb6eb 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:web/web.dart' as web; From 4b2450631066c4bf70790524ebbca6b5adedb58d Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 03:53:43 -0500 Subject: [PATCH 238/421] Remove unused import of 'dart:typed_data' in usb_serial_service_web.dart --- lib/services/usb_serial_service_web.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 5ceb6eb..1f0fcb9 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:web/web.dart' as web; From dcad5c586de7ec91f977f69a434d8448d38fe940 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:11:52 -0500 Subject: [PATCH 239/421] Refactor USB connection handling to use scheduled closure and improve error management in USB services --- .../meshcore/meshcore_open/MainActivity.kt | 32 ++++++++++++------- lib/connector/meshcore_connector.dart | 11 ++++++- lib/services/usb_serial_service_native.dart | 32 +++++++++++++++---- lib/services/usb_serial_service_web.dart | 30 ++++++++++++++--- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index 7ce40d4..e0e0723 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -105,11 +105,8 @@ class MainActivity : FlutterActivity() { "connect" -> handleUsbConnect(call, result) "write" -> handleUsbWrite(call, result) "disconnect" -> { - usbIoExecutor.execute { - closeUsbConnection() - mainHandler.post { - result.success(null) - } + scheduleCloseUsbConnection { + result.success(null) } } else -> result.notImplemented() @@ -315,7 +312,7 @@ class MainActivity : FlutterActivity() { null, ) } - closeUsbConnection() + scheduleCloseUsbConnection() } }, ).also { manager -> @@ -338,6 +335,16 @@ class MainActivity : FlutterActivity() { return driver.ports.firstOrNull() } + private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) { + usbIoExecutor.execute { + closeUsbConnection() + if (onComplete != null) { + mainHandler.post(onComplete) + } + } + } + + @Synchronized private fun closeUsbConnection() { try { ioManager?.stop() @@ -381,12 +388,13 @@ class MainActivity : FlutterActivity() { } if (connectedDeviceName == detachedName) { - closeUsbConnection() - eventSink?.error( - "usb_device_detached", - "USB device was disconnected", - null, - ) + scheduleCloseUsbConnection { + eventSink?.error( + "usb_device_detached", + "USB device was disconnected", + null, + ) + } } } diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 39a34f2..e3712ed 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1266,14 +1266,23 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer?.cancel(); if (PlatformInfo.isWeb && _activeTransport == MeshCoreTransportType.bluetooth) { - _selfInfoRetryTimer = Timer(const Duration(seconds: 10), () { + var attempts = 0; + const maxAttempts = 3; + _selfInfoRetryTimer = Timer.periodic(const Duration(seconds: 10), ( + timer, + ) { if (!isConnected || !_awaitingSelfInfo) { + timer.cancel(); return; } if (_isLoadingContacts || _isSyncingChannels || _channelSyncInFlight) { return; } + attempts += 1; unawaited(sendFrame(buildAppStartFrame())); + if (attempts >= maxAttempts) { + timer.cancel(); + } }); return; } diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index f69c6fc..7f6eb11 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -186,8 +186,7 @@ class UsbSerialService { } void dispose() { - unawaited(disconnect()); - unawaited(_frameController.close()); + unawaited(disconnect().whenComplete(_closeFrameController)); } void _handleSerialData(FlSerialEventArgs event) { @@ -197,7 +196,7 @@ class UsbSerialService { _ingestRawBytes(Uint8List.fromList(bytes)); } } catch (error, stack) { - _frameController.addError(error, stack); + _addFrameError(error, stack); } } @@ -210,13 +209,13 @@ class UsbSerialService { _ingestRawBytes(data.buffer.asUint8List()); return; } - _frameController.addError( + _addFrameError( StateError('Unexpected Android USB event payload: ${data.runtimeType}'), ); } void _handleSerialError(Object error, [StackTrace? stackTrace]) { - _frameController.addError(error, stackTrace); + _addFrameError(error, stackTrace); } void _handleSerialDone() { @@ -231,10 +230,31 @@ class UsbSerialService { ); continue; } - _frameController.add(packet.payload); + _addFrame(packet.payload); } } + void _addFrame(Uint8List payload) { + if (_frameController.isClosed) { + return; + } + _frameController.add(payload); + } + + void _addFrameError(Object error, [StackTrace? stackTrace]) { + if (_frameController.isClosed) { + return; + } + _frameController.addError(error, stackTrace); + } + + Future _closeFrameController() async { + if (_frameController.isClosed) { + return; + } + await _frameController.close(); + } + void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { debugPrint('$prefix len=0'); diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 1f0fcb9..71e5127 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -160,8 +160,7 @@ class UsbSerialService { } void dispose() { - unawaited(disconnect()); - unawaited(_frameController.close()); + unawaited(disconnect().whenComplete(_closeFrameController)); } Future> _getAuthorizedPorts() async { @@ -285,12 +284,12 @@ class UsbSerialService { } } catch (error, stackTrace) { if (_status == UsbSerialStatus.connected) { - _frameController.addError(error, stackTrace); + _addFrameError(error, stackTrace); } } finally { _releaseLock(reader); if (_status == UsbSerialStatus.connected && identical(reader, _reader)) { - _frameController.addError(StateError('USB serial connection closed')); + _addFrameError(StateError('USB serial connection closed')); } } } @@ -402,10 +401,31 @@ class UsbSerialService { ); continue; } - _frameController.add(packet.payload); + _addFrame(packet.payload); } } + void _addFrame(Uint8List payload) { + if (_frameController.isClosed) { + return; + } + _frameController.add(payload); + } + + void _addFrameError(Object error, [StackTrace? stackTrace]) { + if (_frameController.isClosed) { + return; + } + _frameController.addError(error, stackTrace); + } + + Future _closeFrameController() async { + if (_frameController.isClosed) { + return; + } + await _frameController.close(); + } + void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { debugPrint('$prefix len=0'); From ca5784f3f8c94149e572eff62459fc64ad2b5fd3 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:55:47 -0500 Subject: [PATCH 240/421] Add post-frame callback to ensure disconnection on dispose when navigation hasn't changed --- lib/screens/scanner_screen.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 713239f..69ddf6c 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -68,6 +68,11 @@ class _ScannerScreenState extends State { void dispose() { _connector.removeListener(_connectionListener); unawaited(_bluetoothStateSubscription.cancel()); + if (!_changedNavigation) { + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(_connector.disconnect(manual: true)); + }); + } super.dispose(); } From 781090243cf0b2155024830b9d55a66c201341fd Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 05:28:31 -0500 Subject: [PATCH 241/421] Enhance USB functionality by adding request port label management and platform support checks --- lib/connector/meshcore_connector.dart | 4 ++ lib/screens/connection_choice_screen.dart | 24 +++++---- lib/screens/usb_screen.dart | 8 +++ lib/services/usb_serial_service_native.dart | 58 ++++++++++++++------- lib/services/usb_serial_service_web.dart | 18 +++++-- lib/utils/platform_info.dart | 10 ++++ lib/utils/usb_port_labels.dart | 5 +- 7 files changed, 93 insertions(+), 34 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e3712ed..3447eee 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -767,6 +767,10 @@ class MeshCoreConnector extends ChangeNotifier { Future> listUsbPorts() => _usbSerialService.listPorts(); + void setUsbRequestPortLabel(String label) { + _usbSerialService.setRequestPortLabel(label); + } + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { diff --git a/lib/screens/connection_choice_screen.dart b/lib/screens/connection_choice_screen.dart index e4abe7f..16634c6 100644 --- a/lib/screens/connection_choice_screen.dart +++ b/lib/screens/connection_choice_screen.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; +import '../utils/platform_info.dart'; import 'scanner_screen.dart'; import 'usb_screen.dart'; @@ -14,6 +15,7 @@ class ConnectionChoiceScreen extends StatelessWidget { Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); + final usbSupported = PlatformInfo.supportsUsbSerial; return Scaffold( appBar: AppBar( title: FittedBox( @@ -82,14 +84,18 @@ class ConnectionChoiceScreen extends StatelessWidget { label: l10n.connectionChoiceUsbLabel, color: theme.colorScheme.primaryContainer, iconColor: theme.colorScheme.onPrimaryContainer, - onPressed: () { - debugPrint( - 'ConnectionChoiceScreen: USB selected, opening UsbScreen', - ); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const UsbScreen()), - ); - }, + onPressed: usbSupported + ? () { + debugPrint( + 'ConnectionChoiceScreen: USB selected, opening UsbScreen', + ); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const UsbScreen(), + ), + ); + } + : null, ), ), SizedBox(height: gap), @@ -133,7 +139,7 @@ class _ConnectionMethodButton extends StatelessWidget { final IconData icon; final String label; - final VoidCallback onPressed; + final VoidCallback? onPressed; final Color color; final Color iconColor; diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index fb62b95..160f463 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -61,6 +61,12 @@ class _UsbScreenState extends State { unawaited(_loadPorts()); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + } + @override void dispose() { _connector.removeListener(_connectionListener); @@ -389,6 +395,7 @@ class _UsbScreenState extends State { Future _loadPorts() async { if (!mounted) return; + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); setState(() { _isLoadingPorts = true; @@ -425,6 +432,7 @@ class _UsbScreenState extends State { if (selectedPort == null || selectedPort.isEmpty) { return; } + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); if (_connector.state != MeshCoreConnectionState.disconnected) { setState(() { _isConnecting = false; diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 7f6eb11..8467d3a 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -5,6 +5,7 @@ import 'package:flserial/flserial_exception.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'usb_serial_frame_codec.dart'; @@ -21,28 +22,38 @@ class UsbSerialService { ); final StreamController _frameController = StreamController.broadcast(); - final FlSerial _serial = FlSerial(); final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder(); StreamSubscription? _androidDataSubscription; StreamSubscription? _dataSubscription; UsbSerialStatus _status = UsbSerialStatus.disconnected; String? _connectedPortName; + FlSerial? _serial; UsbSerialStatus get status => _status; String? get activePortName => _connectedPortName; Stream get frameStream => _frameController.stream; bool get _useAndroidUsbHost => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + bool get _useDesktopFlSerial => + PlatformInfo.isWindows || PlatformInfo.isLinux; + bool get _isSupportedPlatform => _useAndroidUsbHost || _useDesktopFlSerial; + FlSerial get _nativeSerial => _serial ??= FlSerial(); bool get isConnected { + if (!_isSupportedPlatform) { + return false; + } if (_useAndroidUsbHost) { return _status == UsbSerialStatus.connected; } return _status == UsbSerialStatus.connected && - _serial.isOpen() == FlOpenStatus.open; + _serial?.isOpen() == FlOpenStatus.open; } Future> listPorts() async { + if (!_isSupportedPlatform) { + return const []; + } if (_useAndroidUsbHost) { final ports = await _androidMethodChannel.invokeListMethod( 'listPorts', @@ -60,6 +71,9 @@ class UsbSerialService { _status == UsbSerialStatus.connecting) { throw StateError('USB serial transport is already active'); } + if (!_isSupportedPlatform) { + throw UnsupportedError('USB serial is not supported on this platform.'); + } _status = UsbSerialStatus.connecting; final normalizedPortName = normalizeUsbPortName(portName); @@ -78,32 +92,35 @@ class UsbSerialService { throw StateError(error.message ?? error.code); } } else { - _serial.init(); + final serial = _nativeSerial; + serial.init(); try { - final status = _serial.openPort(normalizedPortName, baudRate); + final status = serial.openPort(normalizedPortName, baudRate); if (status != FlOpenStatus.open) { throw StateError( 'Failed to open USB port $normalizedPortName ($status)', ); } - _serial.setByteSize8(); - _serial.setBitParityNone(); - _serial.setStopBits1(); - _serial.setFlowControlNone(); - _serial.setRTS(false); - _serial.setDTR(true); + serial.setByteSize8(); + serial.setBitParityNone(); + serial.setStopBits1(); + serial.setFlowControlNone(); + serial.setRTS(false); + serial.setDTR(true); debugPrint( - 'USB serial opened port=$normalizedPortName cts=${_serial.getCTS()} dsr=${_serial.getDSR()} dtr=true rts=false', + 'USB serial opened port=$normalizedPortName cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false', ); } on FlSerialException catch (error) { - _serial.free(); + _serial?.free(); + _serial = null; _status = UsbSerialStatus.disconnected; throw StateError( 'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})', ); } catch (error) { - _serial.free(); + _serial?.free(); + _serial = null; _status = UsbSerialStatus.disconnected; rethrow; } @@ -119,7 +136,7 @@ class UsbSerialService { onDone: _handleSerialDone, ); } else { - _dataSubscription = _serial.onSerialData.stream.listen( + _dataSubscription = _nativeSerial.onSerialData.stream.listen( _handleSerialData, onError: _handleSerialError, onDone: _handleSerialDone, @@ -143,7 +160,7 @@ class UsbSerialService { throw StateError(error.message ?? error.code); } } else { - _serial.write(packet); + _nativeSerial.write(packet); } } @@ -165,18 +182,23 @@ class UsbSerialService { } } else { try { - if (_serial.isOpen() == FlOpenStatus.open) { - _serial.closePort(); + if (_serial?.isOpen() == FlOpenStatus.open) { + _serial?.closePort(); } } catch (_) { // Ignore errors while closing. } - _serial.free(); + _serial?.free(); + _serial = null; } _status = UsbSerialStatus.disconnected; } + void setRequestPortLabel(String label) { + // Native implementations do not use a synthetic chooser row. + } + void updateConnectedLabel(String label) { final trimmed = label.trim(); if (trimmed.isEmpty) { diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 71e5127..8c3900d 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -26,6 +26,7 @@ class UsbSerialService { JSObject? _writer; String? _connectedPortName; String? _connectedPortKey; + String _requestPortLabel = 'Choose USB Device'; UsbSerialStatus get status => _status; String? get activePortName => _connectedPortName; @@ -49,7 +50,7 @@ class UsbSerialService { final ports = await _getAuthorizedPorts(); if (ports.isEmpty) { - return const [usbRequestPortLabel]; + return [_requestPortLabel]; } return ports.map(_displayLabelForPort).toList(growable: false); } @@ -159,6 +160,14 @@ class UsbSerialService { _connectedPortName = _buildDisplayLabel(portKey); } + void setRequestPortLabel(String label) { + final trimmed = label.trim(); + if (trimmed.isEmpty) { + return; + } + _requestPortLabel = trimmed; + } + void dispose() { unawaited(disconnect().whenComplete(_closeFrameController)); } @@ -189,7 +198,7 @@ class UsbSerialService { if (ports.isEmpty) { return null; } - if (requestedPortName.isEmpty || requestedPortName == usbRequestPortLabel) { + if (requestedPortName.isEmpty || requestedPortName == _requestPortLabel) { return ports.first; } for (final port in ports) { @@ -350,7 +359,7 @@ class UsbSerialService { try { final info = port.callMethod('getInfo'.toJS); if (info == null) { - return usbRequestPortLabel; + return _requestPortLabel; } final infoObject = info as JSObject; @@ -366,10 +375,11 @@ class UsbSerialService { return describeWebUsbPort( vendorId: hasVendor ? vendorId.toInt() : null, productId: hasProduct ? productId.toInt() : null, + requestPortLabel: _requestPortLabel, knownUsbNames: _knownUsbNames, ); } catch (_) { - return usbRequestPortLabel; + return _requestPortLabel; } } diff --git a/lib/utils/platform_info.dart b/lib/utils/platform_info.dart index dc8e27e..e3fd428 100644 --- a/lib/utils/platform_info.dart +++ b/lib/utils/platform_info.dart @@ -33,4 +33,14 @@ class PlatformInfo { /// Whether the app is running on a desktop platform (macOS, Windows, or Linux). static bool get isDesktop => isMacOS || isWindows || isLinux; + + /// Whether the current platform supports a native USB serial backend. + static bool get supportsNativeUsbSerial => isAndroid || isWindows || isLinux; + + /// Whether the current browser supports the Web Serial backend. + static bool get supportsWebSerial => isWeb && isChrome; + + /// Whether USB serial is expected to be available on the current platform. + static bool get supportsUsbSerial => + supportsNativeUsbSerial || supportsWebSerial; } diff --git a/lib/utils/usb_port_labels.dart b/lib/utils/usb_port_labels.dart index 6430f95..ff32937 100644 --- a/lib/utils/usb_port_labels.dart +++ b/lib/utils/usb_port_labels.dart @@ -1,5 +1,3 @@ -const String usbRequestPortLabel = 'Choose USB Device'; - String normalizeUsbPortName(String portLabel) { final separatorIndex = portLabel.indexOf(' - '); final normalized = separatorIndex >= 0 @@ -23,10 +21,11 @@ String friendlyUsbPortName(String portLabel) { String describeWebUsbPort({ required int? vendorId, required int? productId, + String requestPortLabel = 'Choose USB Device', Map knownUsbNames = const {}, }) { if (vendorId == null && productId == null) { - return usbRequestPortLabel; + return requestPortLabel; } final vendorHex = vendorId?.toRadixString(16).padLeft(4, '0').toUpperCase(); From f39a22668e51bffdee1eef2abb5b8f1a94158a00 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 05:28:40 -0500 Subject: [PATCH 242/421] Add initial load scheduling and tests for USB screen and frame codec functionality --- lib/screens/usb_screen.dart | 6 +- test/screens/usb_flow_test.dart | 125 ++++++++++++++++++ .../services/usb_serial_frame_codec_test.dart | 39 ++++++ test/utils/usb_port_labels_test.dart | 13 +- 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 test/screens/usb_flow_test.dart diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 160f463..c99e10f 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -21,6 +21,7 @@ class _UsbScreenState extends State { bool _isLoadingPorts = true; bool _isConnecting = false; bool _navigatedToContacts = false; + bool _didScheduleInitialLoad = false; String? _selectedPort; String? _errorText; late final MeshCoreConnector _connector; @@ -58,13 +59,16 @@ class _UsbScreenState extends State { } }; _connector.addListener(_connectionListener); - unawaited(_loadPorts()); } @override void didChangeDependencies() { super.didChangeDependencies(); _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + if (!_didScheduleInitialLoad) { + _didScheduleInitialLoad = true; + unawaited(_loadPorts()); + } } @override diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart new file mode 100644 index 0000000..499f894 --- /dev/null +++ b/test/screens/usb_flow_test.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/l10n/app_localizations.dart'; +import 'package:meshcore_open/screens/connection_choice_screen.dart'; +import 'package:meshcore_open/screens/usb_screen.dart'; +import 'package:meshcore_open/utils/platform_info.dart'; + +class _FakeMeshCoreConnector extends MeshCoreConnector { + _FakeMeshCoreConnector({ + this.initialState = MeshCoreConnectionState.disconnected, + List? ports, + }) : _ports = ports ?? []; + + final MeshCoreConnectionState initialState; + final List _ports; + + String? requestPortLabel; + int connectUsbCalls = 0; + String? lastConnectPortName; + String? fakeActiveUsbPort; + bool fakeUsbTransportConnected = false; + + @override + MeshCoreConnectionState get state => initialState; + + @override + String? get activeUsbPort => fakeActiveUsbPort; + + @override + bool get isUsbTransportConnected => fakeUsbTransportConnected; + + @override + Future> listUsbPorts() async => List.from(_ports); + + @override + Future connectUsb({ + required String portName, + int baudRate = 115200, + }) async { + connectUsbCalls += 1; + lastConnectPortName = portName; + } + + @override + void setUsbRequestPortLabel(String label) { + requestPortLabel = label; + } +} + +Widget _buildTestApp({ + required MeshCoreConnector connector, + required Widget child, +}) { + return ChangeNotifierProvider.value( + value: connector, + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: child, + ), + ); +} + +void main() { + testWidgets('UsbScreen passes localized chooser label to connector', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + expect(connector.requestPortLabel, 'Select a USB device'); + }); + + testWidgets( + 'UsbScreen does not call connectUsb when connector is not disconnected', + (tester) async { + final connector = _FakeMeshCoreConnector( + initialState: MeshCoreConnectionState.connected, + ports: ['COM6 - USB Serial Device (COM6)'], + ); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pump(); + + expect(connector.connectUsbCalls, 0); + expect(find.byType(CircularProgressIndicator), findsNothing); + }, + ); + + testWidgets('ConnectionChoiceScreen USB button reflects platform support', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp( + connector: connector, + child: const ConnectionChoiceScreen(), + ), + ); + await tester.pumpAndSettle(); + + final usbButton = tester.widget( + find.widgetWithText(ElevatedButton, 'USB'), + ); + + if (PlatformInfo.supportsUsbSerial) { + expect(usbButton.onPressed, isNotNull); + } else { + expect(usbButton.onPressed, isNull); + } + }); +} diff --git a/test/services/usb_serial_frame_codec_test.dart b/test/services/usb_serial_frame_codec_test.dart index f0ce186..be4497e 100644 --- a/test/services/usb_serial_frame_codec_test.dart +++ b/test/services/usb_serial_frame_codec_test.dart @@ -13,6 +13,21 @@ void main() { ); }); + test('wrapUsbSerialTxFrame rejects payloads above protocol maximum', () { + final payload = Uint8List(usbSerialMaxPayloadLength + 1); + + expect( + () => wrapUsbSerialTxFrame(payload), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'payload.length', + ), + ), + ); + }); + test('UsbSerialFrameDecoder buffers partial frames until complete', () { final decoder = UsbSerialFrameDecoder(); @@ -81,4 +96,28 @@ void main() { expect(packets[1].payload, orderedEquals([0x33])); }, ); + + test( + 'UsbSerialFrameDecoder drops oversized frames and resyncs on the next valid packet', + () { + final decoder = UsbSerialFrameDecoder(); + + final packets = decoder.ingest( + Uint8List.fromList([ + usbSerialRxFrameStart, + 0xAD, + 0x00, + 0x99, + usbSerialRxFrameStart, + 0x01, + 0x00, + 0x44, + ]), + ); + + expect(packets, hasLength(1)); + expect(packets.single.isRxFrame, isTrue); + expect(packets.single.payload, orderedEquals([0x44])); + }, + ); } diff --git a/test/utils/usb_port_labels_test.dart b/test/utils/usb_port_labels_test.dart index 4fef509..e375005 100644 --- a/test/utils/usb_port_labels_test.dart +++ b/test/utils/usb_port_labels_test.dart @@ -50,7 +50,18 @@ void main() { test('describeWebUsbPort returns chooser label when no usb ids exist', () { expect( describeWebUsbPort(vendorId: null, productId: null), - usbRequestPortLabel, + 'Choose USB Device', + ); + }); + + test('describeWebUsbPort uses caller-provided chooser label', () { + expect( + describeWebUsbPort( + vendorId: null, + productId: null, + requestPortLabel: 'Select a USB device', + ), + 'Select a USB device', ); }); From a0feb129e15283a7beb483bc9cdc5c83c5be2a80 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:30:46 -0500 Subject: [PATCH 243/421] Add post-frame callback to disconnect USB transport on dispose if not navigated to contacts --- lib/screens/usb_screen.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index c99e10f..38a7c67 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -74,6 +74,13 @@ class _UsbScreenState extends State { @override void dispose() { _connector.removeListener(_connectionListener); + if (!_navigatedToContacts && + _connector.activeTransport == MeshCoreTransportType.usb && + _connector.state != MeshCoreConnectionState.disconnected) { + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(_connector.disconnect(manual: true)); + }); + } super.dispose(); } From 5216e00807b2499aac723e7665122d139df5a568 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:54:39 -0500 Subject: [PATCH 244/421] Refactor USB port handling to introduce display labels and improve state management --- lib/connector/meshcore_connector.dart | 22 +++++++++----- lib/screens/usb_screen.dart | 32 ++++++++++++--------- lib/services/usb_serial_service_native.dart | 18 ++++++++---- lib/services/usb_serial_service_web.dart | 3 +- test/screens/usb_flow_test.dart | 32 +++++++++++++++++++++ 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 3447eee..b91dd49 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -115,7 +115,8 @@ class MeshCoreConnector extends ChangeNotifier { final UsbSerialService _usbSerialService = UsbSerialService(); StreamSubscription? _usbFrameSubscription; MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; - String? _activeUsbPort; + String? _activeUsbPortKey; + String? _activeUsbPortLabel; final List _scanResults = []; final List _contacts = []; @@ -229,7 +230,9 @@ class MeshCoreConnector extends ChangeNotifier { String get deviceIdLabel => _deviceId ?? 'Unknown'; MeshCoreTransportType get activeTransport => _activeTransport; - String? get activeUsbPort => _activeUsbPort; + String? get activeUsbPort => _activeUsbPortKey; + String? get activeUsbPortDisplayLabel => + _activeUsbPortLabel ?? _activeUsbPortKey; bool get isUsbTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.usb; @@ -778,7 +781,8 @@ class MeshCoreConnector extends ChangeNotifier { } _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPort = null; + _activeUsbPortKey = null; + _activeUsbPortLabel = null; await stopScan(); _setState(MeshCoreConnectionState.connecting); @@ -955,14 +959,16 @@ class MeshCoreConnector extends ChangeNotifier { } _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPort = null; + _activeUsbPortKey = null; + _activeUsbPortLabel = null; await stopScan(); _cancelReconnectTimer(); _manualDisconnect = false; _resetConnectionHandshakeState(); _activeTransport = MeshCoreTransportType.usb; - _activeUsbPort = portName; + _activeUsbPortKey = portName; + _activeUsbPortLabel = portName; unawaited(_backgroundService?.start()); _setState(MeshCoreConnectionState.connecting); @@ -1178,7 +1184,8 @@ class MeshCoreConnector extends ChangeNotifier { _reactionSendQueueSequence = 0; _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPort = null; + _activeUsbPortKey = null; + _activeUsbPortLabel = null; _setState(MeshCoreConnectionState.disconnected); if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) { @@ -2218,7 +2225,8 @@ class MeshCoreConnector extends ChangeNotifier { selfName != null && selfName.isNotEmpty) { _usbSerialService.updateConnectedLabel(selfName); - _activeUsbPort = _usbSerialService.activePortName ?? _activeUsbPort; + _activeUsbPortLabel = + _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; } _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 38a7c67..69e95c4 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -23,6 +23,7 @@ class _UsbScreenState extends State { bool _navigatedToContacts = false; bool _didScheduleInitialLoad = false; String? _selectedPort; + String? _connectedPortDisplayLabel; String? _errorText; late final MeshCoreConnector _connector; late final VoidCallback _connectionListener; @@ -33,21 +34,19 @@ class _UsbScreenState extends State { _connector = context.read(); _connectionListener = () { if (!mounted) return; - final activeUsbPort = _connector.activeUsbPort; - if (activeUsbPort != null && - activeUsbPort.isNotEmpty && - activeUsbPort != _selectedPort) { - setState(() { - _selectedPort = activeUsbPort; - }); - } + final activeUsbPortDisplayLabel = _connector.activeUsbPortDisplayLabel; + final shouldUpdateDisplayLabel = + activeUsbPortDisplayLabel != _connectedPortDisplayLabel; if (_connector.state == MeshCoreConnectionState.disconnected) { _navigatedToContacts = false; - if (_isConnecting) { - setState(() { - _isConnecting = false; - }); - } + setState(() { + _isConnecting = false; + _connectedPortDisplayLabel = activeUsbPortDisplayLabel; + }); + } else if (shouldUpdateDisplayLabel) { + setState(() { + _connectedPortDisplayLabel = activeUsbPortDisplayLabel; + }); } if (_connector.state == MeshCoreConnectionState.connected && _connector.isUsbTransportConnected && @@ -167,7 +166,12 @@ class _UsbScreenState extends State { fit: BoxFit.scaleDown, child: Chip( label: Text( - _selectedPort == null + _connectedPortDisplayLabel != null && + _connectedPortDisplayLabel!.isNotEmpty + ? _friendlyPortName( + _connectedPortDisplayLabel!, + ) + : _selectedPort == null ? l10n.usbScreenStatus : _friendlyPortName(_selectedPort!), overflow: TextOverflow.ellipsis, diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 8467d3a..f6b879b 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -26,11 +26,14 @@ class UsbSerialService { StreamSubscription? _androidDataSubscription; StreamSubscription? _dataSubscription; UsbSerialStatus _status = UsbSerialStatus.disconnected; - String? _connectedPortName; + String? _connectedPortKey; + String? _connectedPortLabel; FlSerial? _serial; UsbSerialStatus get status => _status; - String? get activePortName => _connectedPortName; + String? get activePortKey => _connectedPortKey; + String? get activePortDisplayLabel => + _connectedPortLabel ?? _connectedPortKey; Stream get frameStream => _frameController.stream; bool get _useAndroidUsbHost => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; @@ -126,7 +129,8 @@ class UsbSerialService { } } - _connectedPortName = normalizedPortName; + _connectedPortKey = normalizedPortName; + _connectedPortLabel = normalizedPortName; if (_useAndroidUsbHost) { _androidDataSubscription = _androidEventChannel .receiveBroadcastStream() @@ -168,7 +172,8 @@ class UsbSerialService { if (_status == UsbSerialStatus.disconnected) return; _status = UsbSerialStatus.disconnecting; - _connectedPortName = null; + _connectedPortKey = null; + _connectedPortLabel = null; await _androidDataSubscription?.cancel(); _androidDataSubscription = null; await _dataSubscription?.cancel(); @@ -204,7 +209,10 @@ class UsbSerialService { if (trimmed.isEmpty) { return; } - _connectedPortName = trimmed; + _connectedPortLabel = buildUsbDisplayLabel( + basePortLabel: _connectedPortKey ?? trimmed, + deviceName: trimmed, + ); } void dispose() { diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 8c3900d..87fe8e9 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -29,7 +29,8 @@ class UsbSerialService { String _requestPortLabel = 'Choose USB Device'; UsbSerialStatus get status => _status; - String? get activePortName => _connectedPortName; + String? get activePortKey => _connectedPortKey; + String? get activePortDisplayLabel => _connectedPortName ?? _connectedPortKey; Stream get frameStream => _frameController.stream; bool get isConnected => _status == UsbSerialStatus.connected; diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 499f894..317dc92 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -21,6 +21,7 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { int connectUsbCalls = 0; String? lastConnectPortName; String? fakeActiveUsbPort; + String? fakeActiveUsbPortDisplayLabel; bool fakeUsbTransportConnected = false; @override @@ -29,6 +30,10 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { @override String? get activeUsbPort => fakeActiveUsbPort; + @override + String? get activeUsbPortDisplayLabel => + fakeActiveUsbPortDisplayLabel ?? fakeActiveUsbPort; + @override bool get isUsbTransportConnected => fakeUsbTransportConnected; @@ -99,6 +104,33 @@ void main() { }, ); + testWidgets( + 'UsbScreen keeps raw selection while showing connector USB display label', + (tester) async { + final connector = _FakeMeshCoreConnector( + ports: ['COM6 - USB Serial Device (COM6)'], + ); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + connector.fakeActiveUsbPortDisplayLabel = + 'COM6 - KD3CGK mesh-utility.org'; + connector.notifyListeners(); + await tester.pump(); + + expect(find.text('KD3CGK mesh-utility.org'), findsOneWidget); + + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pump(); + + expect(connector.connectUsbCalls, 1); + expect(connector.lastConnectPortName, 'COM6'); + }, + ); + testWidgets('ConnectionChoiceScreen USB button reflects platform support', ( tester, ) async { From 3cef9e81b6af56d9a56ef773a1d8b8a27fe8fee3 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:11:49 -0500 Subject: [PATCH 245/421] Remove unawaited background service start during USB connection initialization --- lib/connector/meshcore_connector.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b91dd49..50ae50c 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -969,7 +969,6 @@ class MeshCoreConnector extends ChangeNotifier { _activeTransport = MeshCoreTransportType.usb; _activeUsbPortKey = portName; _activeUsbPortLabel = portName; - unawaited(_backgroundService?.start()); _setState(MeshCoreConnectionState.connecting); try { From d6d11eaad2582e44669580c5c3e2036993b01d9c Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:20:41 -0500 Subject: [PATCH 246/421] Update active USB port key and label on connection, notify listeners --- lib/connector/meshcore_connector.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 50ae50c..b3aaeab 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -975,6 +975,10 @@ class MeshCoreConnector extends ChangeNotifier { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; await _usbSerialService.connect(portName: portName, baudRate: baudRate); + _activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey; + _activeUsbPortLabel = + _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; + notifyListeners(); if (PlatformInfo.isWeb) { await stopScan(); } From 98cdac4309f078c2d5723fb96b78c2bae881c973 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:49:04 -0500 Subject: [PATCH 247/421] Refactor MeshCoreConnector to streamline connection handling and remove web-specific logic for contact synchronization... Back to the way it was before.. For some reason the fix worked on my machine but wwhen i built web from upstream it didnt work --- lib/connector/meshcore_connector.dart | 156 +++++--------------------- 1 file changed, 30 insertions(+), 126 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b3aaeab..c5bb09f 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -165,7 +165,6 @@ class MeshCoreConnector extends ChangeNotifier { bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; bool _pendingInitialChannelSync = false; - bool _pendingInitialContactsSync = false; bool _preserveContactsOnRefresh = false; static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; @@ -798,9 +797,6 @@ class MeshCoreConnector extends ChangeNotifier { _lastDeviceDisplayName = _deviceDisplayName; _manualDisconnect = false; _cancelReconnectTimer(); - if (PlatformInfo.isWeb) { - _resetConnectionHandshakeState(); - } unawaited(_backgroundService?.start()); notifyListeners(); @@ -824,37 +820,15 @@ class MeshCoreConnector extends ChangeNotifier { rethrow; } - // Request larger MTU only on native platforms; web does not support it. - if (!PlatformInfo.isWeb) { - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); - } + // Request larger MTU for sending larger frames + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); } - late final List services; - try { - services = await device.discoverServices(); - } catch (error) { - debugPrint('[BLE Connect] service discovery failure: $error'); - if (PlatformInfo.isWeb && - error.toString().contains('GATT Server is disconnected')) { - debugPrint( - '[BLE Connect] retrying service discovery after transient web disconnect', - ); - await Future.delayed(const Duration(milliseconds: 300)); - await device.connect( - timeout: const Duration(seconds: 15), - mtu: null, - license: License.free, - ); - services = await device.discoverServices(); - } else { - rethrow; - } - } + List services = await device.discoverServices(); BluetoothService? uartService; for (var service in services) { @@ -881,32 +855,18 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - if (PlatformInfo.isWeb) { - debugPrint('Starting setNotifyValue(true)'); - debugPrint('Web: Calling setNotifyValue(true) without awaiting'); - unawaited(() async { - try { - await _txCharacteristic!.setNotifyValue(true); - } catch (error) { - debugPrint('[BLE Connect] notify failure (web, ignored): $error'); - debugPrint('Web setNotifyValue error (ignoring): $error'); - } - }()); - debugPrint('setNotifyValue(true) configuration completed'); - } else { - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); - } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('[BLE Connect] notify failure: $e'); - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; + // Retry setNotifyValue with increasing delays + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } } _notifySubscription = _txCharacteristic!.onValueReceived.listen( @@ -921,27 +881,19 @@ class MeshCoreConnector extends ChangeNotifier { await _requestDeviceInfo(); _startBatteryPolling(); - if (PlatformInfo.isWeb && - _activeTransport == MeshCoreTransportType.bluetooth) { - // Chrome's Web Bluetooth stack commonly delays incoming notifications - // until the non-blocking notify setup settles. Avoid stacking extra - // startup writes while that is happening. - } else { - final gotSelfInfo = await _waitForSelfInfo( - timeout: const Duration(seconds: 3), - ); - if (!gotSelfInfo) { - await refreshDeviceInfo(); - await _waitForSelfInfo(timeout: const Duration(seconds: 3)); - } - - unawaited(syncTime()); + final gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + await _waitForSelfInfo(timeout: const Duration(seconds: 3)); } + // Keep device clock aligned on every connection. + await syncTime(); + // Fetch channels so we can track unread counts for incoming messages - if (!_shouldGateInitialChannelSync) { - unawaited(getChannels()); - } + unawaited(getChannels()); } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -1060,7 +1012,6 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; - _pendingInitialContactsSync = false; } bool get _shouldAutoReconnect => @@ -1069,9 +1020,7 @@ class MeshCoreConnector extends ChangeNotifier { _activeTransport == MeshCoreTransportType.bluetooth; bool get _shouldGateInitialChannelSync => - _activeTransport == MeshCoreTransportType.usb || - (_activeTransport == MeshCoreTransportType.bluetooth && - PlatformInfo.isWeb); + _activeTransport == MeshCoreTransportType.usb; void _cancelReconnectTimer() { _reconnectTimer?.cancel(); @@ -1172,7 +1121,6 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; - _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; @@ -1278,28 +1226,6 @@ class MeshCoreConnector extends ChangeNotifier { void _scheduleSelfInfoRetry() { _selfInfoRetryTimer?.cancel(); - if (PlatformInfo.isWeb && - _activeTransport == MeshCoreTransportType.bluetooth) { - var attempts = 0; - const maxAttempts = 3; - _selfInfoRetryTimer = Timer.periodic(const Duration(seconds: 10), ( - timer, - ) { - if (!isConnected || !_awaitingSelfInfo) { - timer.cancel(); - return; - } - if (_isLoadingContacts || _isSyncingChannels || _channelSyncInFlight) { - return; - } - attempts += 1; - unawaited(sendFrame(buildAppStartFrame())); - if (attempts >= maxAttempts) { - timer.cancel(); - } - }); - return; - } _selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), ( timer, ) { @@ -2083,12 +2009,6 @@ class MeshCoreConnector extends ChangeNotifier { _preserveContactsOnRefresh = false; notifyListeners(); unawaited(_persistContacts()); - if (PlatformInfo.isWeb && - _activeTransport == MeshCoreTransportType.bluetooth && - _isSyncingChannels && - !_channelSyncInFlight) { - unawaited(_requestNextChannel()); - } if (!_didInitialQueueSync || _pendingQueueSync) { _didInitialQueueSync = true; _pendingQueueSync = false; @@ -2236,14 +2156,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; notifyListeners(); - // Auto-fetch contacts after getting self info. On web BLE, defer this - // until after channel 0 so startup writes stay serialized. - if (PlatformInfo.isWeb && - _activeTransport == MeshCoreTransportType.bluetooth) { - _pendingInitialContactsSync = true; - } else { - getContacts(); - } + getContacts(); if (_shouldGateInitialChannelSync) { _maybeStartInitialChannelSync(); } @@ -3196,14 +3109,6 @@ class MeshCoreConnector extends ChangeNotifier { // Move to next channel _nextChannelIndexToRequest++; - if (PlatformInfo.isWeb && - _activeTransport == MeshCoreTransportType.bluetooth && - channel.index == 0 && - _pendingInitialContactsSync) { - _pendingInitialContactsSync = false; - unawaited(getContacts()); - return; - } unawaited(_requestNextChannel()); return; } else { @@ -3867,7 +3772,6 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; - _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; From c2f544eebaac9e9f0d3a6cb94b60b8ea2950c76a Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:11:35 -0500 Subject: [PATCH 248/421] I restored the Web BLE behavior in [meshcore_connector.dart] to the earlier Windows/Chrome-working state aligned with the logic that was present around commit `fcef3de57837983a300634aa3e0a77622e945cc2`, What is back: - Web BLE resets handshake state before connect - skips `requestMtu()` on web - retries `discoverServices()` once on the transient web disconnect case - uses the non-blocking web `setNotifyValue(true)` workaround again - skips the immediate `SELF_INFO` wait/refresh stack on web BLE - defers contact loading on web BLE until after channel `0` - uses the Web-specific bounded `SELF_INFO` retry timer - re-enables initial channel-sync gating for web BLE --- lib/connector/meshcore_connector.dart | 156 +++++++++++++++++++++----- 1 file changed, 126 insertions(+), 30 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c5bb09f..b3aaeab 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -165,6 +165,7 @@ class MeshCoreConnector extends ChangeNotifier { bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; bool _pendingInitialChannelSync = false; + bool _pendingInitialContactsSync = false; bool _preserveContactsOnRefresh = false; static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; @@ -797,6 +798,9 @@ class MeshCoreConnector extends ChangeNotifier { _lastDeviceDisplayName = _deviceDisplayName; _manualDisconnect = false; _cancelReconnectTimer(); + if (PlatformInfo.isWeb) { + _resetConnectionHandshakeState(); + } unawaited(_backgroundService?.start()); notifyListeners(); @@ -820,15 +824,37 @@ class MeshCoreConnector extends ChangeNotifier { rethrow; } - // Request larger MTU for sending larger frames - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); + // Request larger MTU only on native platforms; web does not support it. + if (!PlatformInfo.isWeb) { + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); + } } - List services = await device.discoverServices(); + late final List services; + try { + services = await device.discoverServices(); + } catch (error) { + debugPrint('[BLE Connect] service discovery failure: $error'); + if (PlatformInfo.isWeb && + error.toString().contains('GATT Server is disconnected')) { + debugPrint( + '[BLE Connect] retrying service discovery after transient web disconnect', + ); + await Future.delayed(const Duration(milliseconds: 300)); + await device.connect( + timeout: const Duration(seconds: 15), + mtu: null, + license: License.free, + ); + services = await device.discoverServices(); + } else { + rethrow; + } + } BluetoothService? uartService; for (var service in services) { @@ -855,18 +881,32 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - // Retry setNotifyValue with increasing delays - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); + if (PlatformInfo.isWeb) { + debugPrint('Starting setNotifyValue(true)'); + debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + unawaited(() async { + try { + await _txCharacteristic!.setNotifyValue(true); + } catch (error) { + debugPrint('[BLE Connect] notify failure (web, ignored): $error'); + debugPrint('Web setNotifyValue error (ignoring): $error'); + } + }()); + debugPrint('setNotifyValue(true) configuration completed'); + } else { + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); + } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('[BLE Connect] notify failure: $e'); + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; } } _notifySubscription = _txCharacteristic!.onValueReceived.listen( @@ -881,19 +921,27 @@ class MeshCoreConnector extends ChangeNotifier { await _requestDeviceInfo(); _startBatteryPolling(); - final gotSelfInfo = await _waitForSelfInfo( - timeout: const Duration(seconds: 3), - ); - if (!gotSelfInfo) { - await refreshDeviceInfo(); - await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + // Chrome's Web Bluetooth stack commonly delays incoming notifications + // until the non-blocking notify setup settles. Avoid stacking extra + // startup writes while that is happening. + } else { + final gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + } + + unawaited(syncTime()); } - // Keep device clock aligned on every connection. - await syncTime(); - // Fetch channels so we can track unread counts for incoming messages - unawaited(getChannels()); + if (!_shouldGateInitialChannelSync) { + unawaited(getChannels()); + } } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -1012,6 +1060,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; } bool get _shouldAutoReconnect => @@ -1020,7 +1069,9 @@ class MeshCoreConnector extends ChangeNotifier { _activeTransport == MeshCoreTransportType.bluetooth; bool get _shouldGateInitialChannelSync => - _activeTransport == MeshCoreTransportType.usb; + _activeTransport == MeshCoreTransportType.usb || + (_activeTransport == MeshCoreTransportType.bluetooth && + PlatformInfo.isWeb); void _cancelReconnectTimer() { _reconnectTimer?.cancel(); @@ -1121,6 +1172,7 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; @@ -1226,6 +1278,28 @@ class MeshCoreConnector extends ChangeNotifier { void _scheduleSelfInfoRetry() { _selfInfoRetryTimer?.cancel(); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + var attempts = 0; + const maxAttempts = 3; + _selfInfoRetryTimer = Timer.periodic(const Duration(seconds: 10), ( + timer, + ) { + if (!isConnected || !_awaitingSelfInfo) { + timer.cancel(); + return; + } + if (_isLoadingContacts || _isSyncingChannels || _channelSyncInFlight) { + return; + } + attempts += 1; + unawaited(sendFrame(buildAppStartFrame())); + if (attempts >= maxAttempts) { + timer.cancel(); + } + }); + return; + } _selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), ( timer, ) { @@ -2009,6 +2083,12 @@ class MeshCoreConnector extends ChangeNotifier { _preserveContactsOnRefresh = false; notifyListeners(); unawaited(_persistContacts()); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + _isSyncingChannels && + !_channelSyncInFlight) { + unawaited(_requestNextChannel()); + } if (!_didInitialQueueSync || _pendingQueueSync) { _didInitialQueueSync = true; _pendingQueueSync = false; @@ -2156,7 +2236,14 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; notifyListeners(); - getContacts(); + // Auto-fetch contacts after getting self info. On web BLE, defer this + // until after channel 0 so startup writes stay serialized. + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + _pendingInitialContactsSync = true; + } else { + getContacts(); + } if (_shouldGateInitialChannelSync) { _maybeStartInitialChannelSync(); } @@ -3109,6 +3196,14 @@ class MeshCoreConnector extends ChangeNotifier { // Move to next channel _nextChannelIndexToRequest++; + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + channel.index == 0 && + _pendingInitialContactsSync) { + _pendingInitialContactsSync = false; + unawaited(getContacts()); + return; + } unawaited(_requestNextChannel()); return; } else { @@ -3772,6 +3867,7 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; From 4c7ee3b3b0300d8200019f8930df15a1ce3634d0 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:54:12 -0500 Subject: [PATCH 249/421] Enhance USB serial services with debug logging and reset functionality - Introduced debug logging in USB serial services for better traceability. - Added reset method to UsbSerialFrameDecoder to clear buffered data. - Updated tests to verify the reset functionality of the decoder. --- lib/connector/meshcore_connector.dart | 113 ++++++++++++++---- lib/services/usb_serial_frame_codec.dart | 5 + lib/services/usb_serial_service_native.dart | 24 +++- lib/services/usb_serial_service_web.dart | 23 +++- .../services/usb_serial_frame_codec_test.dart | 18 +++ 5 files changed, 153 insertions(+), 30 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b3aaeab..a589849 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -350,11 +350,38 @@ class MeshCoreConnector extends ChangeNotifier { ? allMessages.sublist(allMessages.length - _messageWindowSize) : allMessages; - _conversations[contactKeyHex] = windowedMessages; + final currentMessages = + _conversations[contactKeyHex] ?? const []; + final mergedMessages = [...windowedMessages]; + final existingKeys = { + for (final message in windowedMessages) _messageMergeKey(message), + }; + + for (final message in currentMessages) { + final key = _messageMergeKey(message); + if (existingKeys.add(key)) { + mergedMessages.add(message); + } + } + + mergedMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + final windowedMergedMessages = mergedMessages.length > _messageWindowSize + ? mergedMessages.sublist(mergedMessages.length - _messageWindowSize) + : mergedMessages; + + _conversations[contactKeyHex] = windowedMergedMessages; notifyListeners(); } } + String _messageMergeKey(Message message) { + final messageId = message.messageId; + if (messageId != null && messageId.isNotEmpty) { + return 'id:$messageId'; + } + return 'fallback:${message.isOutgoing}:${message.timestamp.millisecondsSinceEpoch}:${message.text}'; + } + /// Load older messages for a contact (pagination) Future> loadOlderMessages( String contactKeyHex, { @@ -590,6 +617,7 @@ class MeshCoreConnector extends ChangeNotifier { _bleDebugLogService = bleDebugLogService; _appDebugLogService = appDebugLogService; _backgroundService = backgroundService; + _usbSerialService.setDebugLogService(_appDebugLogService); // Initialize notification service _notificationService.initialize(); @@ -749,7 +777,7 @@ class MeshCoreConnector extends ChangeNotifier { androidScanMode: AndroidScanMode.lowLatency, ); } catch (error) { - debugPrint('[BLE Scan] Scan/picker failure: $error'); + _appDebugLogService?.warn('Scan/picker failure: $error', tag: 'BLE Scan'); _setState(MeshCoreConnectionState.disconnected); rethrow; } @@ -806,7 +834,10 @@ class MeshCoreConnector extends ChangeNotifier { try { final connectLabel = _deviceDisplayName ?? _deviceId; - debugPrint('[BLE Connect] Starting connect to $connectLabel'); + _appDebugLogService?.info( + 'Starting connect to $connectLabel', + tag: 'BLE Connect', + ); _connectionSubscription = device.connectionState.listen((state) { if (state == BluetoothConnectionState.disconnected && isConnected) { _handleDisconnection(); @@ -820,7 +851,10 @@ class MeshCoreConnector extends ChangeNotifier { license: License.free, ); } catch (error) { - debugPrint('[BLE Connect] device.connect() failure: $error'); + _appDebugLogService?.error( + 'device.connect() failure: $error', + tag: 'BLE Connect', + ); rethrow; } @@ -828,9 +862,12 @@ class MeshCoreConnector extends ChangeNotifier { if (!PlatformInfo.isWeb) { try { final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); + _appDebugLogService?.info('MTU set to: $mtu', tag: 'BLE Connect'); } catch (e) { - debugPrint('MTU request failed: $e, using default'); + _appDebugLogService?.warn( + 'MTU request failed: $e, using default', + tag: 'BLE Connect', + ); } } @@ -838,11 +875,15 @@ class MeshCoreConnector extends ChangeNotifier { try { services = await device.discoverServices(); } catch (error) { - debugPrint('[BLE Connect] service discovery failure: $error'); + _appDebugLogService?.error( + 'service discovery failure: $error', + tag: 'BLE Connect', + ); if (PlatformInfo.isWeb && error.toString().contains('GATT Server is disconnected')) { - debugPrint( - '[BLE Connect] retrying service discovery after transient web disconnect', + _appDebugLogService?.warn( + 'retrying service discovery after transient web disconnect', + tag: 'BLE Connect', ); await Future.delayed(const Duration(milliseconds: 300)); await device.connect( @@ -882,17 +923,32 @@ class MeshCoreConnector extends ChangeNotifier { } if (PlatformInfo.isWeb) { - debugPrint('Starting setNotifyValue(true)'); - debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + _appDebugLogService?.info( + 'Starting setNotifyValue(true)', + tag: 'BLE Connect', + ); + _appDebugLogService?.info( + 'Web: Calling setNotifyValue(true) without awaiting', + tag: 'BLE Connect', + ); unawaited(() async { try { await _txCharacteristic!.setNotifyValue(true); } catch (error) { - debugPrint('[BLE Connect] notify failure (web, ignored): $error'); - debugPrint('Web setNotifyValue error (ignoring): $error'); + _appDebugLogService?.warn( + 'notify failure (web, ignored): $error', + tag: 'BLE Connect', + ); + _appDebugLogService?.warn( + 'Web setNotifyValue error (ignoring): $error', + tag: 'BLE Connect', + ); } }()); - debugPrint('setNotifyValue(true) configuration completed'); + _appDebugLogService?.info( + 'setNotifyValue(true) configuration completed', + tag: 'BLE Connect', + ); } else { bool notifySet = false; for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { @@ -903,8 +959,11 @@ class MeshCoreConnector extends ChangeNotifier { await _txCharacteristic!.setNotifyValue(true); notifySet = true; } catch (e) { - debugPrint('[BLE Connect] notify failure: $e'); - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + _appDebugLogService?.warn('notify failure: $e', tag: 'BLE Connect'); + _appDebugLogService?.warn( + 'setNotifyValue attempt ${attempt + 1}/3 failed: $e', + tag: 'BLE Connect', + ); if (attempt == 2) rethrow; } } @@ -925,7 +984,19 @@ class MeshCoreConnector extends ChangeNotifier { _activeTransport == MeshCoreTransportType.bluetooth) { // Chrome's Web Bluetooth stack commonly delays incoming notifications // until the non-blocking notify setup settles. Avoid stacking extra - // startup writes while that is happening. + // startup writes while that is happening. Defer the clock sync until + // the connection has had time to settle. + unawaited( + Future(() async { + await Future.delayed(const Duration(seconds: 5)); + if (!isConnected || + !PlatformInfo.isWeb || + _activeTransport != MeshCoreTransportType.bluetooth) { + return; + } + await syncTime(); + }), + ); } else { final gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), @@ -943,7 +1014,7 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(getChannels()); } } catch (e) { - debugPrint("Connection error: $e"); + _appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect'); await disconnect(manual: false); rethrow; } @@ -986,7 +1057,7 @@ class MeshCoreConnector extends ChangeNotifier { _usbFrameSubscription = _usbSerialService.frameStream.listen( _handleFrame, onError: (error, stackTrace) { - debugPrint('USB transport error: $error'); + _appDebugLogService?.error('USB transport error: $error', tag: 'USB'); unawaited(disconnect(manual: false)); }, onDone: () { @@ -1013,7 +1084,7 @@ class MeshCoreConnector extends ChangeNotifier { await syncTime(); } catch (error) { - debugPrint('USB connection error: $error'); + _appDebugLogService?.error('USB connection error: $error', tag: 'USB'); await disconnect(manual: false); rethrow; } @@ -1149,7 +1220,7 @@ class MeshCoreConnector extends ChangeNotifier { // Skip queued BLE operations so disconnect doesn't get stuck behind them. await _device?.disconnect(queue: false); } catch (e) { - debugPrint("Disconnect error: $e"); + _appDebugLogService?.warn('Disconnect error: $e', tag: 'BLE Connect'); } _device = null; diff --git a/lib/services/usb_serial_frame_codec.dart b/lib/services/usb_serial_frame_codec.dart index ebe1733..59e4d4b 100644 --- a/lib/services/usb_serial_frame_codec.dart +++ b/lib/services/usb_serial_frame_codec.dart @@ -37,6 +37,11 @@ class UsbSerialFrameDecoder { final List _rxBuffer = []; int _startIndex = 0; + void reset() { + _rxBuffer.clear(); + _startIndex = 0; + } + List ingest(Uint8List bytes) { if (bytes.isEmpty) { return const []; diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index f6b879b..d79205b 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -5,6 +5,7 @@ import 'package:flserial/flserial_exception.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'app_debug_log_service.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'usb_serial_frame_codec.dart'; @@ -29,6 +30,7 @@ class UsbSerialService { String? _connectedPortKey; String? _connectedPortLabel; FlSerial? _serial; + AppDebugLogService? _debugLogService; UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; @@ -66,6 +68,10 @@ class UsbSerialService { return Future.value(FlSerial.listPorts()); } + void setDebugLogService(AppDebugLogService? service) { + _debugLogService = service; + } + Future connect({ required String portName, int baudRate = 115200, @@ -80,6 +86,7 @@ class UsbSerialService { _status = UsbSerialStatus.connecting; final normalizedPortName = normalizeUsbPortName(portName); + _frameDecoder.reset(); if (_useAndroidUsbHost) { try { @@ -87,8 +94,9 @@ class UsbSerialService { 'portName': normalizedPortName, 'baudRate': baudRate, }); - debugPrint( + _debugLogService?.info( 'USB serial opened port=$normalizedPortName on Android via USB host bridge', + tag: 'USB Serial', ); } on PlatformException catch (error) { _status = UsbSerialStatus.disconnected; @@ -111,8 +119,9 @@ class UsbSerialService { serial.setFlowControlNone(); serial.setRTS(false); serial.setDTR(true); - debugPrint( + _debugLogService?.info( 'USB serial opened port=$normalizedPortName cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false', + tag: 'USB Serial', ); } on FlSerialException catch (error) { _serial?.free(); @@ -174,6 +183,7 @@ class UsbSerialService { _status = UsbSerialStatus.disconnecting; _connectedPortKey = null; _connectedPortLabel = null; + _frameDecoder.reset(); await _androidDataSubscription?.cancel(); _androidDataSubscription = null; await _dataSubscription?.cancel(); @@ -255,8 +265,9 @@ class UsbSerialService { void _ingestRawBytes(Uint8List bytes) { for (final packet in _frameDecoder.ingest(bytes)) { if (!packet.isRxFrame) { - debugPrint( + _debugLogService?.info( 'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}', + tag: 'USB Serial', ); continue; } @@ -287,10 +298,13 @@ class UsbSerialService { void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { - debugPrint('$prefix len=0'); + _debugLogService?.info('$prefix len=0', tag: 'USB Serial'); return; } - debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); + _debugLogService?.info( + '$prefix code=${bytes[0]} len=${bytes.length}', + tag: 'USB Serial', + ); } } diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 87fe8e9..974928e 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -5,6 +5,7 @@ import 'dart:js_interop_unsafe'; import 'package:flutter/foundation.dart'; import 'package:web/web.dart' as web; +import 'app_debug_log_service.dart'; import '../utils/usb_port_labels.dart'; import 'usb_serial_frame_codec.dart'; @@ -27,6 +28,7 @@ class UsbSerialService { String? _connectedPortName; String? _connectedPortKey; String _requestPortLabel = 'Choose USB Device'; + AppDebugLogService? _debugLogService; UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; @@ -69,6 +71,7 @@ class UsbSerialService { } _status = UsbSerialStatus.connecting; + _frameDecoder.reset(); try { final requestedPortName = normalizeUsbPortName(portName); @@ -88,7 +91,10 @@ class UsbSerialService { _status = UsbSerialStatus.connected; unawaited(_pumpReads()); - debugPrint('USB serial opened port=$_connectedPortName via Web Serial'); + _debugLogService?.info( + 'USB serial opened port=$_connectedPortName via Web Serial', + tag: 'USB Serial', + ); } catch (error) { await _cleanupFailedConnect(); _status = UsbSerialStatus.disconnected; @@ -126,6 +132,7 @@ class UsbSerialService { _port = null; _connectedPortName = null; _connectedPortKey = null; + _frameDecoder.reset(); if (reader != null) { try { @@ -169,6 +176,10 @@ class UsbSerialService { _requestPortLabel = trimmed; } + void setDebugLogService(AppDebugLogService? service) { + _debugLogService = service; + } + void dispose() { unawaited(disconnect().whenComplete(_closeFrameController)); } @@ -407,8 +418,9 @@ class UsbSerialService { void _ingestRawBytes(Uint8List bytes) { for (final packet in _frameDecoder.ingest(bytes)) { if (!packet.isRxFrame) { - debugPrint( + _debugLogService?.info( 'USB ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}', + tag: 'USB Serial', ); continue; } @@ -439,10 +451,13 @@ class UsbSerialService { void _logFrameSummary(String prefix, Uint8List bytes) { if (bytes.isEmpty) { - debugPrint('$prefix len=0'); + _debugLogService?.info('$prefix len=0', tag: 'USB Serial'); return; } - debugPrint('$prefix code=${bytes[0]} len=${bytes.length}'); + _debugLogService?.info( + '$prefix code=${bytes[0]} len=${bytes.length}', + tag: 'USB Serial', + ); } } diff --git a/test/services/usb_serial_frame_codec_test.dart b/test/services/usb_serial_frame_codec_test.dart index be4497e..54165de 100644 --- a/test/services/usb_serial_frame_codec_test.dart +++ b/test/services/usb_serial_frame_codec_test.dart @@ -120,4 +120,22 @@ void main() { expect(packets.single.payload, orderedEquals([0x44])); }, ); + + test('UsbSerialFrameDecoder reset clears buffered partial data', () { + final decoder = UsbSerialFrameDecoder(); + + expect( + decoder.ingest(Uint8List.fromList([usbSerialRxFrameStart, 0x02])), + isEmpty, + ); + + decoder.reset(); + + final packets = decoder.ingest( + Uint8List.fromList([usbSerialRxFrameStart, 0x01, 0x00, 0x55]), + ); + + expect(packets, hasLength(1)); + expect(packets.single.payload, orderedEquals([0x55])); + }); } From f5154b003368bec7b35d12267505625456277782 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:33:09 -0500 Subject: [PATCH 250/421] Improve sender name resolution for room server messages by handling missing room-contact keys --- lib/connector/meshcore_connector.dart | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a589849..3f69d0a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3506,14 +3506,17 @@ class MeshCoreConnector extends ChangeNotifier { // For 1:1 chats, sender is implicit (null) String? senderName; if (isRoomServer && !msg.isOutgoing) { - // Resolve sender from the message's fourByteRoomContactKey - final senderContact = _contacts.cast().firstWhere( - (c) => - c != null && - _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), - orElse: () => null, - ); - senderName = senderContact?.name; + // Treat a missing room-contact key as unknown instead of matching every + // contact via an empty prefix. + if (msg.fourByteRoomContactKey.length == 4) { + final senderContact = _contacts.cast().firstWhere( + (c) => + c != null && + _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), + orElse: () => null, + ); + senderName = senderContact?.name; + } } else if (isRoomServer && msg.isOutgoing) { senderName = selfName; } From e6c9a3fea7802c57810747d1453f59267069dea4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 2 Mar 2026 19:21:06 -0800 Subject: [PATCH 251/421] wip --- lib/connector/meshcore_connector.dart | 24 +- lib/screens/usb_screen.dart | 259 ++++++++++++-------- lib/services/usb_serial_service_native.dart | 248 +++++++++++++++---- lib/utils/macos_usb_device_names.dart | 92 +++++++ lib/utils/platform_info.dart | 3 +- lib/utils/usb_port_labels.dart | 21 +- macos/Podfile.lock | 6 + macos/Runner/DebugProfile.entitlements | 6 + macos/Runner/Release.entitlements | 6 + test/utils/usb_port_labels_test.dart | 48 +++- 10 files changed, 533 insertions(+), 180 deletions(-) create mode 100644 lib/utils/macos_usb_device_names.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 3f69d0a..ecedb4e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -737,8 +737,15 @@ class MeshCoreConnector extends ChangeNotifier { _scanResults.clear(); _setState(MeshCoreConnectionState.scanning); - // Ensure any previous scan is fully stopped - await FlutterBluePlus.stopScan(); + // Ensure any previous scan is fully stopped. Guard with isScanningNow to + // avoid triggering stale native callbacks when no scan is active. + if (FlutterBluePlus.isScanningNow) { + try { + await FlutterBluePlus.stopScan(); + } catch (e) { + debugPrint('[FBP] stopScan error in startScan (ignored): $e'); + } + } await _scanSubscription?.cancel(); // On iOS/macOS, wait for Bluetooth to be powered on before scanning @@ -787,7 +794,18 @@ class MeshCoreConnector extends ChangeNotifier { } Future stopScan() async { - await FlutterBluePlus.stopScan(); + // Only call FlutterBluePlus.stopScan() when a scan is actually running. + // Calling it when idle triggers a native BLE completion callback even + // though no scan was started. After a hot restart Dart has already freed + // those callback handles, so the callback crashes with + // "Callback invoked after it has been deleted". + if (FlutterBluePlus.isScanningNow) { + try { + await FlutterBluePlus.stopScan(); + } catch (e) { + debugPrint('[FBP] stopScan error (ignored): $e'); + } + } await _scanSubscription?.cancel(); _scanSubscription = null; diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 69e95c4..03d9446 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'contacts_screen.dart'; @@ -25,9 +26,16 @@ class _UsbScreenState extends State { String? _selectedPort; String? _connectedPortDisplayLabel; String? _errorText; + Timer? _hotPlugTimer; late final MeshCoreConnector _connector; late final VoidCallback _connectionListener; + /// Whether the current platform supports dynamic hot-plug polling. + /// On desktop (macOS, Windows, Linux) we poll continuously so the user + /// never needs to hit Refresh manually. + bool get _supportsHotPlug => + PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS; + @override void initState() { super.initState(); @@ -58,6 +66,7 @@ class _UsbScreenState extends State { } }; _connector.addListener(_connectionListener); + _startHotPlugTimer(); } @override @@ -72,6 +81,8 @@ class _UsbScreenState extends State { @override void dispose() { + _hotPlugTimer?.cancel(); + _hotPlugTimer = null; _connector.removeListener(_connectionListener); if (!_navigatedToContacts && _connector.activeTransport == MeshCoreTransportType.usb && @@ -124,101 +135,77 @@ class _UsbScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Flexible( - flex: 3, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.usb, - size: iconSize, - color: theme.colorScheme.primary, - ), - SizedBox(height: gap), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - l10n.usbScreenTitle, - textAlign: TextAlign.center, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ), - ), - SizedBox(height: math.max(4.0, gap * 0.5)), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - l10n.usbScreenSubtitle, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ), - SizedBox(height: gap), - FittedBox( - fit: BoxFit.scaleDown, - child: Chip( - label: Text( - _connectedPortDisplayLabel != null && - _connectedPortDisplayLabel!.isNotEmpty - ? _friendlyPortName( - _connectedPortDisplayLabel!, - ) - : _selectedPort == null - ? l10n.usbScreenStatus - : _friendlyPortName(_selectedPort!), - overflow: TextOverflow.ellipsis, - ), - backgroundColor: - theme.colorScheme.surfaceContainerHighest, - ), - ), - ], + // ── Compact header ────────────────────────────────────── + Row( + children: [ + Icon( + Icons.usb, + size: iconSize.clamp(24.0, 40.0), + color: theme.colorScheme.primary, ), - ), + SizedBox(width: gap), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.usbScreenTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Text( + l10n.usbScreenSubtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], ), SizedBox(height: gap), + // ── Port list takes all remaining space ───────────────── Expanded(child: _buildPortList(context)), if (_errorText != null) ...[ - SizedBox(height: gap), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - _errorText!, - textAlign: TextAlign.center, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.error, - ), - ), + SizedBox(height: gap * 0.5), + Text( + _errorText!, + textAlign: TextAlign.center, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.error, ), ), ], SizedBox(height: gap), + // ── Action buttons ────────────────────────────────────── if (isNarrow) Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - OutlinedButton.icon( - onPressed: _isLoadingPorts || _isConnecting - ? null - : () { - debugPrint( - 'UsbScreen: refresh ports pressed', - ); - _loadPorts(); - }, - icon: const Icon(Icons.refresh), - label: Text(l10n.repeater_refresh), - ), - SizedBox(height: gap), + if (!_supportsHotPlug) ...[ + OutlinedButton.icon( + onPressed: _isLoadingPorts || _isConnecting + ? null + : () { + debugPrint( + 'UsbScreen: refresh ports pressed', + ); + _loadPorts(); + }, + icon: const Icon(Icons.refresh), + label: Text(l10n.repeater_refresh), + ), + SizedBox(height: gap), + ], FilledButton.icon( onPressed: _canConnect ? () { @@ -247,21 +234,23 @@ class _UsbScreenState extends State { else Row( children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: _isLoadingPorts || _isConnecting - ? null - : () { - debugPrint( - 'UsbScreen: refresh ports pressed', - ); - _loadPorts(); - }, - icon: const Icon(Icons.refresh), - label: Text(l10n.repeater_refresh), + if (!_supportsHotPlug) ...[ + Expanded( + child: OutlinedButton.icon( + onPressed: _isLoadingPorts || _isConnecting + ? null + : () { + debugPrint( + 'UsbScreen: refresh ports pressed', + ); + _loadPorts(); + }, + icon: const Icon(Icons.refresh), + label: Text(l10n.repeater_refresh), + ), ), - ), - SizedBox(width: gap), + SizedBox(width: gap), + ], Expanded( child: FilledButton.icon( onPressed: _canConnect @@ -289,17 +278,14 @@ class _UsbScreenState extends State { ), ], ), - SizedBox(height: math.max(4.0, gap * 0.75)), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - l10n.usbScreenNote, - textAlign: TextAlign.center, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), + SizedBox(height: math.max(4.0, gap * 0.5)), + Text( + l10n.usbScreenNote, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, ), ), ], @@ -317,6 +303,43 @@ class _UsbScreenState extends State { _selectedPort != null && _selectedPort!.isNotEmpty; + void _startHotPlugTimer() { + if (!_supportsHotPlug) return; + _hotPlugTimer?.cancel(); + _hotPlugTimer = Timer.periodic(const Duration(seconds: 2), (_) { + _pollHotPlug(); + }); + } + + Future _pollHotPlug() async { + // Don't interfere with an active connection attempt or initial load. + if (_isConnecting || _isLoadingPorts) return; + if (!mounted) return; + try { + final ports = await _connector.listUsbPorts(); + if (!mounted) return; + final added = ports.where((p) => !_ports.contains(p)).toList(); + final removed = _ports.where((p) => !ports.contains(p)).toList(); + if (added.isEmpty && removed.isEmpty) return; + setState(() { + _ports + ..clear() + ..addAll(ports); + if (_ports.isEmpty) { + _selectedPort = null; + } else if (added.isNotEmpty) { + // Auto-select the newly-connected device. + _selectedPort = added.first; + } else if (_selectedPort != null && !_ports.contains(_selectedPort)) { + // Previously-selected device was unplugged. + _selectedPort = _ports.isNotEmpty ? _ports.first : null; + } + }); + } catch (_) { + // Silent — hot-plug failures are non-critical. + } + } + Widget _buildPortList(BuildContext context) { final theme = Theme.of(context); final l10n = context.l10n; @@ -464,14 +487,32 @@ class _UsbScreenState extends State { try { await _connector.connectUsb(portName: rawPortName); - } catch (error) { + } catch (error, stackTrace) { + debugPrint( + 'UsbScreen: connect failed for $rawPortName: $error\n$stackTrace', + ); if (!mounted) return; setState(() { _isConnecting = false; - _errorText = error.toString(); + _errorText = _friendlyErrorMessage(error); }); + // Re-scan so stale or renamed port entries are cleared from the list. + unawaited(_loadPorts()); } } + /// Strips the Dart runtime prefix (e.g. "Bad state: ", "Exception: ") + /// from error messages before showing them in the UI. + String _friendlyErrorMessage(Object error) { + var msg = error.toString(); + // StateError surfaces as "Bad state: " + if (msg.startsWith('Bad state: ')) { + msg = msg.substring('Bad state: '.length); + } else if (msg.startsWith('Exception: ')) { + msg = msg.substring('Exception: '.length); + } + return msg; + } + String _friendlyPortName(String portLabel) => friendlyUsbPortName(portLabel); } diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index d79205b..9f442a1 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:flserial/flserial.dart'; import 'package:flserial/flserial_exception.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'app_debug_log_service.dart'; +import '../utils/macos_usb_device_names.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'usb_serial_frame_codec.dart'; @@ -32,6 +34,14 @@ class UsbSerialService { FlSerial? _serial; AppDebugLogService? _debugLogService; + /// Holds the last-opened native serial port across hot-restart boundaries. + /// On hot restart the Dart isolate is torn down without running [disconnect], + /// leaving the native SerialThread alive. The next [connect] call reads this + /// field and force-closes the orphaned port before creating a new one, which + /// causes the old native thread to unblock its blocking read and exit + /// naturally—before any new Dart FFI callbacks are registered. + static FlSerial? _lastSerial; + UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; String? get activePortDisplayLabel => @@ -40,19 +50,24 @@ class UsbSerialService { bool get _useAndroidUsbHost => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; bool get _useDesktopFlSerial => - PlatformInfo.isWindows || PlatformInfo.isLinux; + PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS; bool get _isSupportedPlatform => _useAndroidUsbHost || _useDesktopFlSerial; - FlSerial get _nativeSerial => _serial ??= FlSerial(); + // Always-fresh: do NOT use ??= here – a cached FlSerial retains stale + // native handle state (flh) from a prior failed open, causing subsequent + // open attempts to fail with "port not exist" even when the device is present. + FlSerial _freshSerial() => FlSerial(); bool get isConnected { if (!_isSupportedPlatform) { return false; } - if (_useAndroidUsbHost) { - return _status == UsbSerialStatus.connected; - } - return _status == UsbSerialStatus.connected && - _serial?.isOpen() == FlOpenStatus.open; + // Trust _status as the authoritative connection state. Polling + // _serial?.isOpen() via the native FL_CTRL_IS_PORT_OPEN query is + // unreliable during the brief USB re-enumeration window that many + // microcontrollers (e.g. NRF52) trigger in response to DTR assertion. + // Actual port drops are handled by the onDone / onError callbacks on the + // serial data stream subscription, which update _status correctly. + return _status == UsbSerialStatus.connected; } Future> listPorts() async { @@ -65,7 +80,34 @@ class UsbSerialService { ); return ports ?? []; } - return Future.value(FlSerial.listPorts()); + final rawPorts = FlSerial.listPorts(); + // On macOS, flserial's native device-name lookup is broken on macOS + // 10.15+ because the IOKit class name changed from IOUSBDevice to + // IOUSBHostDevice. We resolve names ourselves via ioreg and rewrite any + // "port - n/a" entries with the real product name. + if (Platform.isMacOS && rawPorts.isNotEmpty) { + return _annotateMacOsPorts(rawPorts); + } + return Future.value(rawPorts); + } + + /// Rewrites the flserial port list on macOS by substituting real USB device + /// names (obtained via [ioreg]) for the "n/a" placeholders that flserial + /// returns when it can't find the deprecated IOUSBDevice parent. + Future> _annotateMacOsPorts(List rawPorts) async { + final deviceNames = await queryMacOsUsbDeviceNames(); + if (deviceNames.isEmpty) return rawPorts; + return rawPorts.map((entry) { + // entry format from fl_ports: "port - description - hardware_id" + final port = normalizeUsbPortName(entry); // e.g. /dev/cu.usbmodem1101 + final knownName = deviceNames[port]; // e.g. "Nordic NRF52 DK" + if (knownName == null) return entry; // non-USB port, keep as-is + // Replace description field only; preserve hardware_id for device + // identity (used by normalizeUsbPortName). + final segments = entry.split(' - '); + final hardwareId = segments.length >= 3 ? segments.last : 'n/a'; + return '$port - $knownName - $hardwareId'; + }).toList(); } void setDebugLogService(AppDebugLogService? service) { @@ -85,7 +127,7 @@ class UsbSerialService { } _status = UsbSerialStatus.connecting; - final normalizedPortName = normalizeUsbPortName(portName); + var normalizedPortName = normalizeUsbPortName(portName); _frameDecoder.reset(); if (_useAndroidUsbHost) { @@ -100,41 +142,111 @@ class UsbSerialService { ); } on PlatformException catch (error) { _status = UsbSerialStatus.disconnected; - throw StateError(error.message ?? error.code); - } - } else { - final serial = _nativeSerial; - serial.init(); - - try { - final status = serial.openPort(normalizedPortName, baudRate); - if (status != FlOpenStatus.open) { - throw StateError( - 'Failed to open USB port $normalizedPortName ($status)', - ); - } - serial.setByteSize8(); - serial.setBitParityNone(); - serial.setStopBits1(); - serial.setFlowControlNone(); - serial.setRTS(false); - serial.setDTR(true); - _debugLogService?.info( - 'USB serial opened port=$normalizedPortName cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false', + final msg = error.message ?? error.code; + debugPrint('[USB Serial] Android connect failed: $msg'); + _debugLogService?.error( + 'Android connect failed: $msg', tag: 'USB Serial', ); - } on FlSerialException catch (error) { - _serial?.free(); - _serial = null; + throw StateError(msg); + } + } else { + // Force-close any native serial port left open by the previous Dart + // isolate (hot-restart case). The old SerialThread blocks on read(); once + // the port is closed here it unblocks and exits before we register any + // new Dart FFI callbacks, preventing the "callback invoked after deletion" + // crash. + final orphan = _lastSerial; + if (orphan != null) { + _lastSerial = null; + try { + if (orphan.isOpen() == FlOpenStatus.open) { + orphan.closePort(); + } + } catch (_) {} + try { + orphan.free(); + } catch (_) {} + // Give the native thread a moment to observe the port closure and exit. + await Future.delayed(const Duration(milliseconds: 100)); + } + + // On macOS, flserial lists both cu.* and tty.* device nodes. + // When a cu.* open fails with FL_ERROR_PORT_NOT_EXIST, try the tty.* + // variant as a fallback (and vice-versa) before giving up. + final candidates = _buildPortCandidates(normalizedPortName); + FlSerialException? lastError; + bool opened = false; + + for (final candidate in candidates) { + // Always create a fresh FlSerial instance — a cached instance retains + // a stale flh handle from prior failed opens, which causes the native + // fl_open() to mis-route the request and report port-not-exist even + // when the device node is physically present. + final serial = _freshSerial(); + serial.init(); + try { + final openStatus = serial.openPort(candidate, baudRate); + if (openStatus != FlOpenStatus.open) { + final msg = + 'Failed to open USB port $candidate (status: $openStatus)'; + debugPrint('[USB Serial] $msg'); + _debugLogService?.error(msg, tag: 'USB Serial'); + // Not a FlSerialException — treat as terminal failure + _status = UsbSerialStatus.disconnected; + throw StateError(msg); + } + serial.setByteSize8(); + serial.setBitParityNone(); + serial.setStopBits1(); + serial.setFlowControlNone(); + serial.setRTS(false); + serial.setDTR(true); + _serial = serial; + _lastSerial = serial; + // Update the normalized port name to whichever candidate succeeded. + normalizedPortName = candidate; + _debugLogService?.info( + 'USB serial opened port=$candidate cts=${serial.getCTS()} dsr=${serial.getDSR()} dtr=true rts=false', + tag: 'USB Serial', + ); + opened = true; + break; + } on FlSerialException catch (error) { + // Do NOT call fl_free() here — it destroys global native library + // state and makes subsequent fl_init() calls unreliable. The native + // fl_open() already called fl_close() on failure internally. + debugPrint( + '[USB Serial] Failed to open $candidate: ${error.msg} (code ${error.error})', + ); + _debugLogService?.warn( + 'Failed to open $candidate: ${error.msg} (code ${error.error})', + tag: 'USB Serial', + ); + lastError = error; + // Try next candidate + } catch (error, stackTrace) { + _status = UsbSerialStatus.disconnected; + debugPrint( + '[USB Serial] Unexpected error opening $candidate: $error\n$stackTrace', + ); + _debugLogService?.error( + 'Unexpected error opening $candidate: $error', + tag: 'USB Serial', + ); + rethrow; + } + } + + if (!opened) { _status = UsbSerialStatus.disconnected; - throw StateError( - 'Failed to open USB port $normalizedPortName: ${error.msg} (${error.error})', - ); - } catch (error) { - _serial?.free(); - _serial = null; - _status = UsbSerialStatus.disconnected; - rethrow; + final primary = candidates.first; + final msg = lastError != null + ? 'Failed to open USB port $primary: ${lastError.msg} (code ${lastError.error})' + : 'Failed to open USB port $primary'; + debugPrint('[USB Serial] $msg'); + _debugLogService?.error(msg, tag: 'USB Serial'); + throw StateError(msg); } } @@ -149,7 +261,7 @@ class UsbSerialService { onDone: _handleSerialDone, ); } else { - _dataSubscription = _nativeSerial.onSerialData.stream.listen( + _dataSubscription = _serial!.onSerialData.stream.listen( _handleSerialData, onError: _handleSerialError, onDone: _handleSerialDone, @@ -173,7 +285,7 @@ class UsbSerialService { throw StateError(error.message ?? error.code); } } else { - _nativeSerial.write(packet); + _serial!.write(packet); } } @@ -184,28 +296,40 @@ class UsbSerialService { _connectedPortKey = null; _connectedPortLabel = null; _frameDecoder.reset(); - await _androidDataSubscription?.cancel(); - _androidDataSubscription = null; - await _dataSubscription?.cancel(); - _dataSubscription = null; if (_useAndroidUsbHost) { + await _androidDataSubscription?.cancel(); + _androidDataSubscription = null; try { await _androidMethodChannel.invokeMethod('disconnect'); } catch (_) { // Ignore errors while closing. } } else { + // IMPORTANT: Close and free the native port FIRST, before cancelling the + // Dart subscription. The native SerialThread is blocked on a read(); once + // closePort() is called it unblocks and the thread exits. If we cancel + // the Dart subscription first (freeing the FFI callback pointer) and the + // thread fires one final callback before noticing the port is gone, Dart + // crashes with "Callback invoked after it has been deleted". + final serial = _serial; + _serial = null; + _lastSerial = null; try { - if (_serial?.isOpen() == FlOpenStatus.open) { - _serial?.closePort(); + if (serial?.isOpen() == FlOpenStatus.open) { + serial?.closePort(); } } catch (_) { // Ignore errors while closing. } + try { + serial?.free(); + } catch (_) {} - _serial?.free(); - _serial = null; + // Now it is safe to cancel the Dart subscription — the native thread has + // already seen the port close and will not fire any more callbacks. + await _dataSubscription?.cancel(); + _dataSubscription = null; } _status = UsbSerialStatus.disconnected; } @@ -306,6 +430,28 @@ class UsbSerialService { tag: 'USB Serial', ); } + + /// Returns an ordered list of port paths to try for [portName]. + /// + /// On macOS, USB serial devices appear as both `/dev/cu.*` (call-out, the + /// correct mode for outgoing serial connections) and `/dev/tty.*` (dial-in). + /// `flserial` may list one variant while only the other is actually openable + /// at a given moment. We prefer `cu.*` but automatically include the `tty.*` + /// sibling as a fallback, and vice-versa. + List _buildPortCandidates(String normalizedPort) { + if (!Platform.isMacOS) return [normalizedPort]; + const cuPrefix = '/dev/cu.'; + const ttyPrefix = '/dev/tty.'; + if (normalizedPort.startsWith(cuPrefix)) { + final suffix = normalizedPort.substring(cuPrefix.length); + return [normalizedPort, '$ttyPrefix$suffix']; + } + if (normalizedPort.startsWith(ttyPrefix)) { + final suffix = normalizedPort.substring(ttyPrefix.length); + return [normalizedPort, '$cuPrefix$suffix']; + } + return [normalizedPort]; + } } enum UsbSerialStatus { disconnected, connecting, connected, disconnecting } diff --git a/lib/utils/macos_usb_device_names.dart b/lib/utils/macos_usb_device_names.dart new file mode 100644 index 0000000..ad521f8 --- /dev/null +++ b/lib/utils/macos_usb_device_names.dart @@ -0,0 +1,92 @@ +import 'dart:io'; + +/// Queries the macOS IOKit registry via [ioreg] to build a map of serial port +/// callout device paths to human-readable USB device names. +/// +/// The [flserial] native library uses the deprecated [IOUSBDevice] IOKit class +/// to resolve device names, but macOS 10.15+ renamed it to [IOUSBHostDevice]. +/// As a result flserial always returns "n/a" for USB product/vendor info on +/// modern macOS. This utility bypasses that limitation by invoking ioreg +/// directly and parsing its output. +/// +/// Returns a Map of e.g. `"/dev/cu.usbmodem1101"` → `"Nordic NRF52 DK"`. +/// Devices without a USB product name are not included in the map. +Future> queryMacOsUsbDeviceNames() async { + assert(Platform.isMacOS); + try { + final result = await Process.run('ioreg', [ + '-r', + '-c', + 'IOUSBHostDevice', + '-l', + ], stdoutEncoding: const SystemEncoding()); + if (result.exitCode != 0) return const {}; + return _parseIoregOutput(result.stdout as String); + } catch (_) { + return const {}; + } +} + +Map _parseIoregOutput(String output) { + final lines = output.split('\n'); + final result = {}; + + // We accumulate the current device block's properties. + // A new block starts at a line beginning with "+-o " which indicates a + // top-level IOUSBHostDevice entry in the ioreg tree. + String? currentVendor; + String? currentProduct; + final List currentPorts = []; + + void flushBlock() { + if (currentPorts.isNotEmpty && + (currentVendor != null || currentProduct != null)) { + final parts = [ + if (currentVendor != null && currentVendor!.isNotEmpty) currentVendor!, + if (currentProduct != null && currentProduct!.isNotEmpty) + currentProduct!, + ]; + final name = parts.join(' '); + for (final port in currentPorts) { + result[port] = name; + } + } + currentVendor = null; + currentProduct = null; + currentPorts.clear(); + } + + for (final line in lines) { + // A new top-level device block begins here. + if (line.startsWith('+-o ')) { + flushBlock(); + continue; + } + // USB Product Name (appears at multiple depths in the tree, first wins) + final productMatch = _kProductName.firstMatch(line); + if (productMatch != null && currentProduct == null) { + currentProduct = productMatch.group(1)?.trim(); + continue; + } + // USB Vendor Name + final vendorMatch = _kVendorName.firstMatch(line); + if (vendorMatch != null && currentVendor == null) { + currentVendor = vendorMatch.group(1)?.trim(); + continue; + } + // IOCalloutDevice — the /dev/cu.xxx path our app uses + final calloutMatch = _kCalloutDevice.firstMatch(line); + if (calloutMatch != null) { + final port = calloutMatch.group(1)?.trim(); + if (port != null && port.isNotEmpty) { + currentPorts.add(port); + } + } + } + flushBlock(); + return result; +} + +final RegExp _kProductName = RegExp(r'"USB Product Name" = "([^"]*)"'); +final RegExp _kVendorName = RegExp(r'"USB Vendor Name" = "([^"]*)"'); +final RegExp _kCalloutDevice = RegExp(r'"IOCalloutDevice" = "([^"]*)"'); diff --git a/lib/utils/platform_info.dart b/lib/utils/platform_info.dart index e3fd428..a388932 100644 --- a/lib/utils/platform_info.dart +++ b/lib/utils/platform_info.dart @@ -35,7 +35,8 @@ class PlatformInfo { static bool get isDesktop => isMacOS || isWindows || isLinux; /// Whether the current platform supports a native USB serial backend. - static bool get supportsNativeUsbSerial => isAndroid || isWindows || isLinux; + static bool get supportsNativeUsbSerial => + isAndroid || isWindows || isLinux || isMacOS; /// Whether the current browser supports the Web Serial backend. static bool get supportsWebSerial => isWeb && isChrome; diff --git a/lib/utils/usb_port_labels.dart b/lib/utils/usb_port_labels.dart index ff32937..1eb8796 100644 --- a/lib/utils/usb_port_labels.dart +++ b/lib/utils/usb_port_labels.dart @@ -6,16 +6,25 @@ String normalizeUsbPortName(String portLabel) { return normalized.trim(); } +/// Returns a human-readable name for a serial port label. +/// +/// The native flserial library encodes port info as a ` - `-separated string: +/// `" - - "` +/// +/// This function extracts the *description* field (index 1) and discards the +/// raw hardware_id, which is not user-friendly. If the description is missing +/// or unhelpful (e.g. "n/a"), it falls back to the raw port name. String friendlyUsbPortName(String portLabel) { - final separatorIndex = portLabel.indexOf(' - '); - if (separatorIndex < 0) { + final parts = portLabel.split(' - '); + if (parts.length < 2) { return portLabel.trim(); } - final friendlyName = portLabel.substring(separatorIndex + 3).trim(); - if (friendlyName.isEmpty) { - return normalizeUsbPortName(portLabel); + // parts[0] = port name, parts[1] = description, parts[2+] = hardware id + final description = parts[1].trim(); + if (description.isEmpty || description.toLowerCase() == 'n/a') { + return parts[0].trim(); } - return friendlyName; + return description; } String describeWebUsbPort({ diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8224cfb..58b4d01 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - flserial (0.0.1): + - FlutterMacOS - flutter_blue_plus_darwin (0.0.2): - Flutter - FlutterMacOS @@ -24,6 +26,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - flserial (from `Flutter/ephemeral/.symlinks/plugins/flserial/macos`) - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) @@ -36,6 +39,8 @@ DEPENDENCIES: - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) EXTERNAL SOURCES: + flserial: + :path: Flutter/ephemeral/.symlinks/plugins/flserial/macos flutter_blue_plus_darwin: :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin flutter_local_notifications: @@ -58,6 +63,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos SPEC CHECKSUMS: + flserial: 3c161e076dfc73458ec5803e7a9a9d2bb85fadf6 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index f31e9af..17455ef 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -12,6 +12,12 @@ com.apple.security.device.bluetooth + com.apple.security.device.usb + + com.apple.security.temporary-exception.files.absolute-path.read-write + + /dev/ + com.apple.security.device.camera diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 29ef507..11bd5b8 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -8,6 +8,12 @@ com.apple.security.device.bluetooth + com.apple.security.device.usb + + com.apple.security.temporary-exception.files.absolute-path.read-write + + /dev/ + com.apple.security.device.camera diff --git a/test/utils/usb_port_labels_test.dart b/test/utils/usb_port_labels_test.dart index e375005..b2d88bd 100644 --- a/test/utils/usb_port_labels_test.dart +++ b/test/utils/usb_port_labels_test.dart @@ -11,19 +11,47 @@ void main() { ); }); - test('friendlyUsbPortName prefers suffix when present', () { - expect( - friendlyUsbPortName( - 'COM6 - USB Serial Device (COM6) - USB\\VID_2886&PID_1667', - ), - 'USB Serial Device (COM6) - USB\\VID_2886&PID_1667', - ); - }); + test( + 'friendlyUsbPortName returns only description, not hardware_id (3-part label)', + () { + expect( + friendlyUsbPortName( + 'COM6 - USB Serial Device (COM6) - USB\\VID_2886&PID_1667', + ), + 'USB Serial Device (COM6)', + ); + }, + ); test( - 'friendlyUsbPortName falls back to normalized port when suffix is empty', + 'friendlyUsbPortName works for macOS-style 3-part label with USB product name', () { - expect(friendlyUsbPortName('COM6 - '), 'COM6'); + expect( + friendlyUsbPortName( + '/dev/cu.usbmodem1101 - Nordic Semiconductor nRF52 DK - USB VID:PID=1915:520f SNR=ABCDEF', + ), + 'Nordic Semiconductor nRF52 DK', + ); + }, + ); + + test( + 'friendlyUsbPortName falls back to port name when description is n/a', + () { + expect( + friendlyUsbPortName('/dev/cu.Bluetooth-Incoming-Port - n/a - n/a'), + '/dev/cu.Bluetooth-Incoming-Port', + ); + }, + ); + + test( + 'friendlyUsbPortName handles 2-part label (no hardware_id) correctly', + () { + expect( + friendlyUsbPortName('COM6 - USB Serial Device (COM6)'), + 'USB Serial Device (COM6)', + ); }, ); From 3c0c0d1deacd662896a9ce50e43fa19d51ad3bc2 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 2 Mar 2026 19:31:35 -0800 Subject: [PATCH 252/421] wip --- lib/services/usb_serial_service_native.dart | 76 +++++++++++---------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 9f442a1..204f312 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -34,14 +34,6 @@ class UsbSerialService { FlSerial? _serial; AppDebugLogService? _debugLogService; - /// Holds the last-opened native serial port across hot-restart boundaries. - /// On hot restart the Dart isolate is torn down without running [disconnect], - /// leaving the native SerialThread alive. The next [connect] call reads this - /// field and force-closes the orphaned port before creating a new one, which - /// causes the old native thread to unblock its blocking read and exit - /// naturally—before any new Dart FFI callbacks are registered. - static FlSerial? _lastSerial; - UsbSerialStatus get status => _status; String? get activePortKey => _connectedPortKey; String? get activePortDisplayLabel => @@ -151,25 +143,23 @@ class UsbSerialService { throw StateError(msg); } } else { - // Force-close any native serial port left open by the previous Dart - // isolate (hot-restart case). The old SerialThread blocks on read(); once - // the port is closed here it unblocks and exits before we register any - // new Dart FFI callbacks, preventing the "callback invoked after deletion" - // crash. - final orphan = _lastSerial; - if (orphan != null) { - _lastSerial = null; - try { - if (orphan.isOpen() == FlOpenStatus.open) { - orphan.closePort(); - } - } catch (_) {} - try { - orphan.free(); - } catch (_) {} - // Give the native thread a moment to observe the port closure and exit. - await Future.delayed(const Duration(milliseconds: 100)); - } + // ── Hot-restart guard ───────────────────────────────────────────────── + // On hot restart Dart tears down the isolate without calling dispose(). + // The NativeCallable registered by flserial's setCallback() is + // isolate-local and gets freed when the isolate dies, but the native + // SerialThread is still alive and will call it → crash. + // + // flserial uses process-global native state. Calling fl_free() kills ALL + // SerialThreads for every open port across all Dart isolates (there is + // only one in a Flutter app). Then fl_init() re-initialises the slot + // table so subsequent fl_open() calls work normally. + // + // This must happen before we register any new NativeCallable, so it must + // be the very first thing we do in the desktop branch. + try { + bindings.fl_free(); + bindings.fl_init(16); + } catch (_) {} // On macOS, flserial lists both cu.* and tty.* device nodes. // When a cu.* open fails with FL_ERROR_PORT_NOT_EXIST, try the tty.* @@ -203,7 +193,6 @@ class UsbSerialService { serial.setRTS(false); serial.setDTR(true); _serial = serial; - _lastSerial = serial; // Update the normalized port name to whichever candidate succeeded. normalizedPortName = candidate; _debugLogService?.info( @@ -213,9 +202,8 @@ class UsbSerialService { opened = true; break; } on FlSerialException catch (error) { - // Do NOT call fl_free() here — it destroys global native library - // state and makes subsequent fl_init() calls unreliable. The native - // fl_open() already called fl_close() on failure internally. + // The native fl_open() already called fl_close() on failure + // internally, so no extra cleanup is needed here for this candidate. debugPrint( '[USB Serial] Failed to open $candidate: ${error.msg} (code ${error.error})', ); @@ -314,7 +302,6 @@ class UsbSerialService { // crashes with "Callback invoked after it has been deleted". final serial = _serial; _serial = null; - _lastSerial = null; try { if (serial?.isOpen() == FlOpenStatus.open) { serial?.closePort(); @@ -322,9 +309,9 @@ class UsbSerialService { } catch (_) { // Ignore errors while closing. } - try { - serial?.free(); - } catch (_) {} + // Note: we do NOT call free() here; that would globally reset native + // state for all ports. The global reset is done in connect() instead, + // before the next open, which is the safer place to do it. // Now it is safe to cancel the Dart subscription — the native thread has // already seen the port close and will not fire any more callbacks. @@ -350,6 +337,25 @@ class UsbSerialService { } void dispose() { + // Synchronously close the native port so the SerialThread exits before + // the Dart isolate is torn down (e.g. on hot restart). The async + // disconnect() path via unawaited() offers no ordering guarantee — the + // isolate may die before the Future resolves, leaving the thread alive + // with a dangling NativeCallable pointer. + if (_useDesktopFlSerial) { + final serial = _serial; + _serial = null; + _status = UsbSerialStatus.disconnected; + _connectedPortKey = null; + _connectedPortLabel = null; + try { + if (serial?.isOpen() == FlOpenStatus.open) { + serial?.closePort(); // synchronous C call — kills the SerialThread + } + } catch (_) {} + } + // Kick off the full async teardown for anything else (subscription cancel, + // stream controller close). These are best-effort at dispose time. unawaited(disconnect().whenComplete(_closeFrameController)); } From 32632669c3e32bd5126bac6ed5082e184aa9ecbb Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 2 Mar 2026 19:42:57 -0800 Subject: [PATCH 253/421] wip --- .gitmodules | 3 +++ vendor/flserial | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 vendor/flserial diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a47ab4d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/flserial"] + path = vendor/flserial + url = git@github.com:MeshEnvy/flserial.git diff --git a/vendor/flserial b/vendor/flserial new file mode 160000 index 0000000..58d83ec --- /dev/null +++ b/vendor/flserial @@ -0,0 +1 @@ +Subproject commit 58d83ecc72e6ecaf6fb7f77691cd7ebc3c39aeb4 From 63583dadda3e45931969613b30e83398c4a7a746 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 2 Mar 2026 19:43:04 -0800 Subject: [PATCH 254/421] wip --- pubspec.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 9e82770..9ce21c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -131,6 +131,10 @@ flutter_launcher_icons: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package +dependency_overrides: + flserial: + path: packages/flserial + build_pipe: workflows: default: From 74da9e82b57dc2ba106ad4a70d8ff8f97dc21ad2 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 2 Mar 2026 19:57:21 -0800 Subject: [PATCH 255/421] wip --- pubspec.yaml | 2 +- vendor/flserial | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 9ce21c3..d727919 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -133,7 +133,7 @@ flutter_launcher_icons: dependency_overrides: flserial: - path: packages/flserial + path: vendor/flserial build_pipe: workflows: diff --git a/vendor/flserial b/vendor/flserial index 58d83ec..4821631 160000 --- a/vendor/flserial +++ b/vendor/flserial @@ -1 +1 @@ -Subproject commit 58d83ecc72e6ecaf6fb7f77691cd7ebc3c39aeb4 +Subproject commit 48216310061efc8d5d217cc18014fc2cb501646e From 44c0670dae1845e67604a1658e2df094fa565e9c Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:48:19 -0500 Subject: [PATCH 256/421] Refine USB transport flow - replace Android USB dependency with app-owned USB host implementation\n- restore BLE-first scanner flow with USB secondary action\n- tighten Web Serial key handling and disconnect logging\n\nTODO (follow-up):\n- review non-English localization copy for tone and consistency\n- trim remaining unused/awkward localization strings introduced during USB UI changes --- android/app/build.gradle.kts | 3 +- .../meshcore/meshcore_open/MainActivity.kt | 396 +--- .../meshcore_open/MeshcoreUsbFunctions.kt | 574 +++++ android/build.gradle.kts | 1 - lib/connector/meshcore_connector.dart | 305 ++- lib/connector/meshcore_connector_usb.dart | 32 + lib/l10n/app_bg.arb | 1698 +++++++------- lib/l10n/app_de.arb | 556 +++-- lib/l10n/app_en.arb | 30 +- lib/l10n/app_es.arb | 684 +++--- lib/l10n/app_fr.arb | 1020 +++++---- lib/l10n/app_it.arb | 194 +- lib/l10n/app_localizations.dart | 38 +- lib/l10n/app_localizations_bg.dart | 1899 ++++++++-------- lib/l10n/app_localizations_de.dart | 562 +++-- lib/l10n/app_localizations_en.dart | 33 +- lib/l10n/app_localizations_es.dart | 694 +++--- lib/l10n/app_localizations_fr.dart | 1031 +++++---- lib/l10n/app_localizations_it.dart | 199 +- lib/l10n/app_localizations_nl.dart | 76 +- lib/l10n/app_localizations_pl.dart | 1227 ++++++----- lib/l10n/app_localizations_pt.dart | 790 +++---- lib/l10n/app_localizations_ru.dart | 1953 +++++++++-------- lib/l10n/app_localizations_sk.dart | 1492 ++++++------- lib/l10n/app_localizations_sl.dart | 801 +++---- lib/l10n/app_localizations_sv.dart | 914 ++++---- lib/l10n/app_localizations_uk.dart | 1945 ++++++++-------- lib/l10n/app_localizations_zh.dart | 1849 ++++++++-------- lib/l10n/app_nl.arb | 72 +- lib/l10n/app_pl.arb | 1190 +++++----- lib/l10n/app_pt.arb | 772 ++++--- lib/l10n/app_ru.arb | 1738 ++++++++------- lib/l10n/app_sk.arb | 1454 ++++++------ lib/l10n/app_sl.arb | 772 ++++--- lib/l10n/app_sv.arb | 898 ++++---- lib/l10n/app_uk.arb | 1724 ++++++++------- lib/l10n/app_zh.arb | 1732 ++++++++------- lib/main.dart | 4 +- lib/screens/connection_choice_screen.dart | 232 -- lib/screens/scanner_screen.dart | 84 +- lib/screens/usb_screen.dart | 54 +- lib/services/usb_serial_service_native.dart | 9 + lib/services/usb_serial_service_web.dart | 110 +- lib/utils/dialog_utils.dart | 2 + test/screens/usb_flow_test.dart | 14 +- 45 files changed, 16316 insertions(+), 15541 deletions(-) create mode 100644 android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt create mode 100644 lib/connector/meshcore_connector_usb.dart delete mode 100644 lib/screens/connection_choice_screen.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2e8f47f..e0a8029 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) { android { namespace = "com.meshcore.meshcore_open" compileSdk = flutter.compileSdkVersion - ndkVersion = "29.0.14206865" + ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -84,5 +84,4 @@ flutter { dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") - implementation("com.github.mik3y:usb-serial-for-android:3.9.0") } diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt index e0e0723..9022c8b 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MainActivity.kt @@ -1,408 +1,18 @@ package com.meshcore.meshcore_open -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.hardware.usb.UsbDevice -import android.hardware.usb.UsbDeviceConnection -import android.hardware.usb.UsbManager -import android.os.Build -import android.os.Handler -import android.os.Looper -import com.hoho.android.usbserial.driver.UsbSerialDriver -import com.hoho.android.usbserial.driver.UsbSerialPort -import com.hoho.android.usbserial.driver.UsbSerialProber -import com.hoho.android.usbserial.util.SerialInputOutputManager import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import java.util.Locale -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors class MainActivity : FlutterActivity() { - private val usbMethodChannelName = "meshcore_open/android_usb_serial" - private val usbEventChannelName = "meshcore_open/android_usb_serial_events" - private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION" - - private lateinit var usbManager: UsbManager - private val mainHandler = Handler(Looper.getMainLooper()) - private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor() - - private var eventSink: EventChannel.EventSink? = null - private var usbConnection: UsbDeviceConnection? = null - private var usbPort: UsbSerialPort? = null - private var ioManager: SerialInputOutputManager? = null - private var connectedDeviceName: String? = null - - private var pendingConnectResult: MethodChannel.Result? = null - private var pendingConnectPortName: String? = null - private var pendingConnectBaudRate: Int = 115200 - - private val permissionReceiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - UsbManager.ACTION_USB_DEVICE_DETACHED -> { - handleUsbDetached(intent) - return - } - usbPermissionAction -> { - } - else -> { - return - } - } - - if (intent.action != usbPermissionAction) { - return - } - - val result = pendingConnectResult - val portName = pendingConnectPortName - pendingConnectResult = null - pendingConnectPortName = null - - if (result == null || portName == null) { - return - } - - val granted = - intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) - if (!granted) { - result.error("usb_permission_denied", "USB permission denied", null) - return - } - - val device = findUsbDevice(portName) - if (device == null) { - result.error( - "usb_device_missing", - "USB device no longer available for $portName", - null, - ) - return - } - - openUsbDevice(device, pendingConnectBaudRate, result) - } - } + private val usbFunctions by lazy { MeshcoreUsbFunctions(this) } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - - usbManager = getSystemService(Context.USB_SERVICE) as UsbManager - registerUsbPermissionReceiver() - - MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName) - .setMethodCallHandler { call, result -> - when (call.method) { - "listPorts" -> result.success(listUsbPorts()) - "connect" -> handleUsbConnect(call, result) - "write" -> handleUsbWrite(call, result) - "disconnect" -> { - scheduleCloseUsbConnection { - result.success(null) - } - } - else -> result.notImplemented() - } - } - - EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName) - .setStreamHandler( - object : EventChannel.StreamHandler { - override fun onListen(arguments: Any?, events: EventChannel.EventSink) { - eventSink = events - } - - override fun onCancel(arguments: Any?) { - eventSink = null - } - }, - ) + usbFunctions.configureFlutterEngine(flutterEngine) } override fun onDestroy() { - closeUsbConnection() - usbIoExecutor.shutdownNow() - try { - unregisterReceiver(permissionReceiver) - } catch (_: IllegalArgumentException) { - } + usbFunctions.dispose() super.onDestroy() } - - private fun registerUsbPermissionReceiver() { - val filter = - IntentFilter().apply { - addAction(usbPermissionAction) - addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) - } else { - @Suppress("DEPRECATION") - registerReceiver(permissionReceiver, filter) - } - } - - private fun listUsbPorts(): List { - val drivers = UsbSerialProber.getDefaultProber().findAllDrivers(usbManager) - return drivers.map { driver -> - val device = driver.device - val productName = device.productName ?: "USB Serial Device" - val vendorProduct = - String.format( - Locale.US, - "VID:%04X PID:%04X", - device.vendorId, - device.productId, - ) - "${device.deviceName} - $productName - $vendorProduct" - } - } - - private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) { - val portName = call.argument("portName") - val baudRate = call.argument("baudRate") ?: 115200 - if (portName.isNullOrBlank()) { - result.error("usb_invalid_port", "Port name is required", null) - return - } - - val device = findUsbDevice(portName) - if (device == null) { - result.error("usb_device_missing", "USB device not found for $portName", null) - return - } - - if (usbManager.hasPermission(device)) { - openUsbDevice(device, baudRate, result) - return - } - - if (pendingConnectResult != null) { - result.error("usb_busy", "Another USB permission request is already pending", null) - return - } - - pendingConnectResult = result - pendingConnectPortName = portName - pendingConnectBaudRate = baudRate - - val permissionIntent = PendingIntent.getBroadcast( - this, - 0, - Intent(usbPermissionAction).setPackage(packageName), - pendingIntentFlags(), - ) - usbManager.requestPermission(device, permissionIntent) - } - - private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) { - val data = call.argument("data") - val port = usbPort - if (data == null) { - result.error("usb_invalid_data", "Data is required", null) - return - } - if (port == null) { - result.error("usb_not_connected", "USB serial port is not connected", null) - return - } - - usbIoExecutor.execute { - try { - port.write(data, 1000) - mainHandler.post { - result.success(null) - } - } catch (error: Exception) { - mainHandler.post { - result.error("usb_write_failed", error.message, null) - } - } - } - } - - private fun findUsbDevice(portName: String): UsbDevice? { - return usbManager.deviceList.values.firstOrNull { it.deviceName == portName } - } - - private fun openUsbDevice( - device: UsbDevice, - baudRate: Int, - result: MethodChannel.Result, - ) { - usbIoExecutor.execute { - try { - closeUsbConnection() - - val driver = UsbSerialProber.getDefaultProber().probeDevice(device) - if (driver == null) { - mainHandler.post { - result.error( - "usb_driver_missing", - "No USB serial driver for ${device.deviceName}", - null, - ) - } - return@execute - } - - val connection = usbManager.openDevice(device) - if (connection == null) { - mainHandler.post { - result.error( - "usb_open_failed", - "UsbManager could not open ${device.deviceName}", - null, - ) - } - return@execute - } - - val port = firstPort(driver) - if (port == null) { - connection.close() - mainHandler.post { - result.error( - "usb_port_missing", - "No USB serial port exposed by ${device.deviceName}", - null, - ) - } - return@execute - } - - port.open(connection) - port.setParameters( - baudRate, - 8, - UsbSerialPort.STOPBITS_1, - UsbSerialPort.PARITY_NONE, - ) - port.rts = false - port.dtr = true - - usbConnection = connection - usbPort = port - connectedDeviceName = device.deviceName - - ioManager = - SerialInputOutputManager( - port, - object : SerialInputOutputManager.Listener { - override fun onNewData(data: ByteArray) { - mainHandler.post { - eventSink?.success(data) - } - } - - override fun onRunError(e: Exception) { - mainHandler.post { - eventSink?.error( - "usb_io_error", - e.message ?: "USB serial I/O error", - null, - ) - } - scheduleCloseUsbConnection() - } - }, - ).also { manager -> - manager.start() - } - - mainHandler.post { - result.success(null) - } - } catch (error: Exception) { - closeUsbConnection() - mainHandler.post { - result.error("usb_connect_failed", error.message, null) - } - } - } - } - - private fun firstPort(driver: UsbSerialDriver): UsbSerialPort? { - return driver.ports.firstOrNull() - } - - private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) { - usbIoExecutor.execute { - closeUsbConnection() - if (onComplete != null) { - mainHandler.post(onComplete) - } - } - } - - @Synchronized - private fun closeUsbConnection() { - try { - ioManager?.stop() - } catch (_: Exception) { - } - ioManager = null - - try { - usbPort?.close() - } catch (_: Exception) { - } - usbPort = null - - try { - usbConnection?.close() - } catch (_: Exception) { - } - usbConnection = null - connectedDeviceName = null - } - - private fun handleUsbDetached(intent: Intent) { - val detachedDevice = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) - } - - val detachedName = detachedDevice?.deviceName ?: return - - if (pendingConnectPortName == detachedName) { - pendingConnectResult?.error( - "usb_device_detached", - "USB device was removed before the connection completed", - null, - ) - pendingConnectResult = null - pendingConnectPortName = null - } - - if (connectedDeviceName == detachedName) { - scheduleCloseUsbConnection { - eventSink?.error( - "usb_device_detached", - "USB device was disconnected", - null, - ) - } - } - } - - private fun pendingIntentFlags(): Int { - var flags = PendingIntent.FLAG_UPDATE_CURRENT - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - flags = flags or PendingIntent.FLAG_MUTABLE - } - return flags - } } diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt new file mode 100644 index 0000000..d6c09b7 --- /dev/null +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt @@ -0,0 +1,574 @@ +package com.meshcore.meshcore_open + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.hardware.usb.UsbConstants +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbDeviceConnection +import android.hardware.usb.UsbEndpoint +import android.hardware.usb.UsbInterface +import android.hardware.usb.UsbManager +import android.os.Build +import android.os.Handler +import android.os.Looper +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class MeshcoreUsbFunctions( + private val activity: FlutterActivity, +) { + private companion object { + const val usbRecipientInterface = 0x01 + } + + private val usbMethodChannelName = "meshcore_open/android_usb_serial" + private val usbEventChannelName = "meshcore_open/android_usb_serial_events" + private val usbPermissionAction = "com.meshcore.meshcore_open.USB_PERMISSION" + + private val usbManager by lazy { + activity.getSystemService(Context.USB_SERVICE) as UsbManager + } + private val mainHandler = Handler(Looper.getMainLooper()) + private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor() + + private var eventSink: EventChannel.EventSink? = null + private var usbConnection: UsbDeviceConnection? = null + private var usbInEndpoint: UsbEndpoint? = null + private var usbOutEndpoint: UsbEndpoint? = null + private var controlInterface: UsbInterface? = null + private var dataInterface: UsbInterface? = null + private var readThread: Thread? = null + @Volatile private var isReading = false + private var connectedDeviceName: String? = null + + private var pendingConnectResult: MethodChannel.Result? = null + private var pendingConnectPortName: String? = null + private var pendingConnectBaudRate: Int = 115200 + + private data class PortConfig( + val controlInterface: UsbInterface?, + val dataInterface: UsbInterface, + val inEndpoint: UsbEndpoint, + val outEndpoint: UsbEndpoint, + ) + + private val permissionReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + UsbManager.ACTION_USB_DEVICE_DETACHED -> { + handleUsbDetached(intent) + return + } + usbPermissionAction -> Unit + else -> return + } + + val result = pendingConnectResult + val portName = pendingConnectPortName + pendingConnectResult = null + pendingConnectPortName = null + + if (result == null || portName == null) { + return + } + + val granted = + intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) + if (!granted) { + result.error("usb_permission_denied", "USB permission denied", null) + return + } + + val device = findUsbDevice(portName) + if (device == null) { + result.error( + "usb_device_missing", + "USB device no longer available for $portName", + null, + ) + return + } + + openUsbDevice(device, pendingConnectBaudRate, result) + } + } + + fun configureFlutterEngine(flutterEngine: FlutterEngine) { + registerUsbPermissionReceiver() + + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, usbMethodChannelName) + .setMethodCallHandler { call, result -> + when (call.method) { + "listPorts" -> result.success(listUsbPorts()) + "connect" -> handleUsbConnect(call, result) + "write" -> handleUsbWrite(call, result) + "disconnect" -> { + scheduleCloseUsbConnection { + result.success(null) + } + } + else -> result.notImplemented() + } + } + + EventChannel(flutterEngine.dartExecutor.binaryMessenger, usbEventChannelName) + .setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + }, + ) + } + + fun dispose() { + closeUsbConnection() + usbIoExecutor.shutdownNow() + try { + activity.unregisterReceiver(permissionReceiver) + } catch (_: IllegalArgumentException) { + } + } + + private fun registerUsbPermissionReceiver() { + val filter = + IntentFilter().apply { + addAction(usbPermissionAction) + addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + activity.registerReceiver(permissionReceiver, filter, Context.RECEIVER_NOT_EXPORTED) + } else { + @Suppress("DEPRECATION") + activity.registerReceiver(permissionReceiver, filter) + } + } + + private fun listUsbPorts(): List { + return usbManager.deviceList.values.map { device -> + val productName = device.productName ?: "USB Serial Device" + val vendorProduct = + String.format( + Locale.US, + "VID:%04X PID:%04X", + device.vendorId, + device.productId, + ) + "${device.deviceName} - $productName - $vendorProduct" + } + } + + private fun handleUsbConnect(call: MethodCall, result: MethodChannel.Result) { + val portName = call.argument("portName") + val baudRate = call.argument("baudRate") ?: 115200 + if (portName.isNullOrBlank()) { + result.error("usb_invalid_port", "Port name is required", null) + return + } + + val device = findUsbDevice(portName) + if (device == null) { + result.error("usb_device_missing", "USB device not found for $portName", null) + return + } + + if (usbManager.hasPermission(device)) { + openUsbDevice(device, baudRate, result) + return + } + + if (pendingConnectResult != null) { + result.error("usb_busy", "Another USB permission request is already pending", null) + return + } + + pendingConnectResult = result + pendingConnectPortName = portName + pendingConnectBaudRate = baudRate + + val permissionIntent = PendingIntent.getBroadcast( + activity, + 0, + Intent(usbPermissionAction).setPackage(activity.packageName), + pendingIntentFlags(), + ) + usbManager.requestPermission(device, permissionIntent) + } + + private fun handleUsbWrite(call: MethodCall, result: MethodChannel.Result) { + val data = call.argument("data") + val connection = usbConnection + val endpoint = usbOutEndpoint + if (data == null) { + result.error("usb_invalid_data", "Data is required", null) + return + } + if (connection == null || endpoint == null) { + result.error("usb_not_connected", "USB serial port is not connected", null) + return + } + + usbIoExecutor.execute { + try { + writeToDevice(data) + mainHandler.post { result.success(null) } + } catch (error: Exception) { + mainHandler.post { + result.error("usb_write_failed", error.message, null) + } + } + } + } + + private fun findUsbDevice(portName: String): UsbDevice? { + return usbManager.deviceList.values.firstOrNull { it.deviceName == portName } + } + + private fun openUsbDevice( + device: UsbDevice, + baudRate: Int, + result: MethodChannel.Result, + ) { + usbIoExecutor.execute { + try { + closeUsbConnection() + + val config = resolvePortConfig(device) + if (config == null) { + mainHandler.post { + result.error( + "usb_driver_missing", + "No compatible USB serial interface for ${device.deviceName}", + null, + ) + } + return@execute + } + + val connection = usbManager.openDevice(device) + if (connection == null) { + mainHandler.post { + result.error( + "usb_open_failed", + "UsbManager could not open ${device.deviceName}", + null, + ) + } + return@execute + } + + if (!connection.claimInterface(config.dataInterface, true)) { + connection.close() + mainHandler.post { + result.error( + "usb_open_failed", + "Could not claim USB data interface for ${device.deviceName}", + null, + ) + } + return@execute + } + + if (config.controlInterface != null && + config.controlInterface.id != config.dataInterface.id && + !connection.claimInterface(config.controlInterface, true) + ) { + connection.releaseInterface(config.dataInterface) + connection.close() + mainHandler.post { + result.error( + "usb_open_failed", + "Could not claim USB control interface for ${device.deviceName}", + null, + ) + } + return@execute + } + + configureDevice(connection, config, baudRate) + + usbConnection = connection + usbInEndpoint = config.inEndpoint + usbOutEndpoint = config.outEndpoint + controlInterface = config.controlInterface + dataInterface = config.dataInterface + connectedDeviceName = device.deviceName + startReadLoop() + + mainHandler.post { + result.success(null) + } + } catch (error: Exception) { + closeUsbConnection() + mainHandler.post { + result.error("usb_connect_failed", error.message, null) + } + } + } + } + + private fun resolvePortConfig(device: UsbDevice): PortConfig? { + var preferredDataInterface: UsbInterface? = null + var preferredInEndpoint: UsbEndpoint? = null + var preferredOutEndpoint: UsbEndpoint? = null + var fallbackDataInterface: UsbInterface? = null + var fallbackInEndpoint: UsbEndpoint? = null + var fallbackOutEndpoint: UsbEndpoint? = null + var preferredControlInterface: UsbInterface? = null + + for (interfaceIndex in 0 until device.interfaceCount) { + val usbInterface = device.getInterface(interfaceIndex) + var inEndpoint: UsbEndpoint? = null + var outEndpoint: UsbEndpoint? = null + + for (endpointIndex in 0 until usbInterface.endpointCount) { + val endpoint = usbInterface.getEndpoint(endpointIndex) + if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) { + continue + } + when (endpoint.direction) { + UsbConstants.USB_DIR_IN -> if (inEndpoint == null) inEndpoint = endpoint + UsbConstants.USB_DIR_OUT -> if (outEndpoint == null) outEndpoint = endpoint + } + } + + val hasDataPair = inEndpoint != null && outEndpoint != null + when { + usbInterface.interfaceClass == UsbConstants.USB_CLASS_COMM && + preferredControlInterface == null -> { + preferredControlInterface = usbInterface + } + hasDataPair && + usbInterface.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA -> { + preferredDataInterface = usbInterface + preferredInEndpoint = inEndpoint + preferredOutEndpoint = outEndpoint + } + hasDataPair && fallbackDataInterface == null -> { + fallbackDataInterface = usbInterface + fallbackInEndpoint = inEndpoint + fallbackOutEndpoint = outEndpoint + } + } + } + + val dataInterface = preferredDataInterface ?: fallbackDataInterface ?: return null + val inEndpoint = preferredInEndpoint ?: fallbackInEndpoint ?: return null + val outEndpoint = preferredOutEndpoint ?: fallbackOutEndpoint ?: return null + return PortConfig(preferredControlInterface, dataInterface, inEndpoint, outEndpoint) + } + + private fun configureDevice( + connection: UsbDeviceConnection, + config: PortConfig, + baudRate: Int, + ) { + val control = config.controlInterface ?: return + val lineCoding = + byteArrayOf( + (baudRate and 0xFF).toByte(), + ((baudRate shr 8) and 0xFF).toByte(), + ((baudRate shr 16) and 0xFF).toByte(), + ((baudRate shr 24) and 0xFF).toByte(), + 0, // stop bits: 1 + 0, // parity: none + 8, // data bits + ) + + val lineCodingResult = + connection.controlTransfer( + UsbConstants.USB_DIR_OUT or + UsbConstants.USB_TYPE_CLASS or + usbRecipientInterface, + 0x20, + 0, + control.id, + lineCoding, + lineCoding.size, + 1000, + ) + if (lineCodingResult < 0) { + throw IllegalStateException("Failed to configure USB line coding") + } + + val controlLineResult = + connection.controlTransfer( + UsbConstants.USB_DIR_OUT or + UsbConstants.USB_TYPE_CLASS or + usbRecipientInterface, + 0x22, + 0x0001, // DTR on, RTS off + control.id, + null, + 0, + 1000, + ) + if (controlLineResult < 0) { + throw IllegalStateException("Failed to configure USB control line state") + } + } + + private fun startReadLoop() { + val connection = usbConnection ?: return + val endpoint = usbInEndpoint ?: return + + isReading = true + readThread = + Thread({ + val packetSize = endpoint.maxPacketSize.coerceAtLeast(64) + val buffer = ByteArray(packetSize * 4) + try { + while (isReading) { + val bytesRead = connection.bulkTransfer(endpoint, buffer, buffer.size, 250) + if (!isReading) { + break + } + if (bytesRead <= 0) { + continue + } + val packet = buffer.copyOf(bytesRead) + mainHandler.post { + eventSink?.success(packet) + } + } + } catch (error: Exception) { + if (isReading) { + mainHandler.post { + eventSink?.error( + "usb_io_error", + error.message ?: "USB serial I/O error", + null, + ) + } + scheduleCloseUsbConnection() + } + } + }, "MeshCoreUsbRead").also { thread -> + thread.isDaemon = true + thread.start() + } + } + + private fun writeToDevice(data: ByteArray) { + val connection = usbConnection ?: throw IllegalStateException("USB connection missing") + val endpoint = usbOutEndpoint ?: throw IllegalStateException("USB output endpoint missing") + var offset = 0 + val maxPacketSize = endpoint.maxPacketSize.coerceAtLeast(64) + while (offset < data.size) { + val chunkSize = minOf(maxPacketSize, data.size - offset) + val chunk = data.copyOfRange(offset, offset + chunkSize) + val bytesWritten = connection.bulkTransfer(endpoint, chunk, chunkSize, 1000) + if (bytesWritten != chunkSize) { + throw IllegalStateException("Short USB write: wrote $bytesWritten of $chunkSize bytes") + } + offset += chunkSize + } + } + + private fun scheduleCloseUsbConnection(onComplete: (() -> Unit)? = null) { + usbIoExecutor.execute { + closeUsbConnection() + if (onComplete != null) { + mainHandler.post(onComplete) + } + } + } + + @Synchronized + private fun closeUsbConnection() { + isReading = false + readThread?.interrupt() + if (readThread != null && readThread !== Thread.currentThread()) { + try { + readThread?.join(300) + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + } + } + readThread = null + + val connection = usbConnection + val claimedControl = controlInterface + val claimedData = dataInterface + + usbInEndpoint = null + usbOutEndpoint = null + controlInterface = null + dataInterface = null + usbConnection = null + + if (connection != null) { + if (claimedControl != null) { + try { + connection.releaseInterface(claimedControl) + } catch (_: Exception) { + } + } + if (claimedData != null && claimedData.id != claimedControl?.id) { + try { + connection.releaseInterface(claimedData) + } catch (_: Exception) { + } + } + try { + connection.close() + } catch (_: Exception) { + } + } + connectedDeviceName = null + } + + private fun handleUsbDetached(intent: Intent) { + val detachedDevice = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + } + + val detachedName = detachedDevice?.deviceName ?: return + + if (pendingConnectPortName == detachedName) { + pendingConnectResult?.error( + "usb_device_detached", + "USB device was removed before the connection completed", + null, + ) + pendingConnectResult = null + pendingConnectPortName = null + } + + if (connectedDeviceName == detachedName) { + scheduleCloseUsbConnection { + eventSink?.error( + "usb_device_detached", + "USB device was disconnected", + null, + ) + } + } + } + + private fun pendingIntentFlags(): Int { + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or PendingIntent.FLAG_MUTABLE + } + return flags + } +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts index eeea458..dbee657 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -2,7 +2,6 @@ allprojects { repositories { google() mavenCentral() - maven(url = "https://jitpack.io") } } diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index ecedb4e..2bdc033 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -166,6 +166,9 @@ class MeshCoreConnector extends ChangeNotifier { bool _hasReceivedDeviceInfo = false; bool _pendingInitialChannelSync = false; bool _pendingInitialContactsSync = false; + bool _bleInitialSyncStarted = false; + bool _pendingDeferredChannelSyncAfterContacts = false; + bool _webInitialHandshakeRequestSent = false; bool _preserveContactsOnRefresh = false; static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; @@ -364,6 +367,8 @@ class MeshCoreConnector extends ChangeNotifier { } } + // Re-sort after merging persisted and in-memory messages so the + // conversation window remains stable after optimistic inserts. mergedMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); final windowedMergedMessages = mergedMessages.length > _messageWindowSize ? mergedMessages.sublist(mergedMessages.length - _messageWindowSize) @@ -820,6 +825,76 @@ class MeshCoreConnector extends ChangeNotifier { _usbSerialService.setRequestPortLabel(label); } + Future connectUsb({ + required String portName, + int baudRate = 115200, + }) async { + if (_state == MeshCoreConnectionState.connecting || + _state == MeshCoreConnectionState.connected) { + return; + } + + _activeTransport = MeshCoreTransportType.bluetooth; + _activeUsbPortKey = null; + _activeUsbPortLabel = null; + + await stopScan(); + _cancelReconnectTimer(); + _manualDisconnect = false; + _resetConnectionHandshakeState(); + _activeTransport = MeshCoreTransportType.usb; + _activeUsbPortKey = portName; + _activeUsbPortLabel = portName; + _setState(MeshCoreConnectionState.connecting); + + try { + await _usbFrameSubscription?.cancel(); + _usbFrameSubscription = null; + await _usbSerialService.connect(portName: portName, baudRate: baudRate); + _activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey; + _activeUsbPortLabel = + _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; + notifyListeners(); + if (PlatformInfo.isWeb) { + await stopScan(); + } + await Future.delayed(const Duration(milliseconds: 200)); + _usbFrameSubscription = _usbSerialService.frameStream.listen( + _handleFrame, + onError: (error, stackTrace) { + _appDebugLogService?.error('USB transport error: $error', tag: 'USB'); + unawaited(disconnect(manual: false)); + }, + onDone: () { + unawaited(disconnect(manual: false)); + }, + ); + + _setState(MeshCoreConnectionState.connected); + _pendingInitialChannelSync = true; + await _requestDeviceInfo(); + _startBatteryPolling(); + var gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + } + if (!gotSelfInfo) { + throw StateError('Timed out waiting for SELF_INFO during connect'); + } + + await syncTime(); + } catch (error) { + _appDebugLogService?.error('USB connection error: $error', tag: 'USB'); + await disconnect(manual: false); + rethrow; + } + } + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { @@ -844,6 +919,7 @@ class MeshCoreConnector extends ChangeNotifier { _lastDeviceDisplayName = _deviceDisplayName; _manualDisconnect = false; _cancelReconnectTimer(); + _bleInitialSyncStarted = false; if (PlatformInfo.isWeb) { _resetConnectionHandshakeState(); } @@ -856,6 +932,10 @@ class MeshCoreConnector extends ChangeNotifier { 'Starting connect to $connectLabel', tag: 'BLE Connect', ); + await _connectionSubscription?.cancel(); + _connectionSubscription = null; + await _notifySubscription?.cancel(); + _notifySubscription = null; _connectionSubscription = device.connectionState.listen((state) { if (state == BluetoothConnectionState.disconnected && isConnected) { _handleDisconnection(); @@ -899,6 +979,8 @@ class MeshCoreConnector extends ChangeNotifier { ); if (PlatformInfo.isWeb && error.toString().contains('GATT Server is disconnected')) { + // Chrome Web Bluetooth intermittently disconnects between connect() + // and service discovery; retry once to recover that transient state. _appDebugLogService?.warn( 'retrying service discovery after transient web disconnect', tag: 'BLE Connect', @@ -995,42 +1077,7 @@ class MeshCoreConnector extends ChangeNotifier { _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = true; } - - await _requestDeviceInfo(); - _startBatteryPolling(); - if (PlatformInfo.isWeb && - _activeTransport == MeshCoreTransportType.bluetooth) { - // Chrome's Web Bluetooth stack commonly delays incoming notifications - // until the non-blocking notify setup settles. Avoid stacking extra - // startup writes while that is happening. Defer the clock sync until - // the connection has had time to settle. - unawaited( - Future(() async { - await Future.delayed(const Duration(seconds: 5)); - if (!isConnected || - !PlatformInfo.isWeb || - _activeTransport != MeshCoreTransportType.bluetooth) { - return; - } - await syncTime(); - }), - ); - } else { - final gotSelfInfo = await _waitForSelfInfo( - timeout: const Duration(seconds: 3), - ); - if (!gotSelfInfo) { - await refreshDeviceInfo(); - await _waitForSelfInfo(timeout: const Duration(seconds: 3)); - } - - unawaited(syncTime()); - } - - // Fetch channels so we can track unread counts for incoming messages - if (!_shouldGateInitialChannelSync) { - unawaited(getChannels()); - } + unawaited(Future.microtask(() => _startBleInitialSync())); } catch (e) { _appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect'); await disconnect(manual: false); @@ -1038,76 +1085,6 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future connectUsb({ - required String portName, - int baudRate = 115200, - }) async { - if (_state == MeshCoreConnectionState.connecting || - _state == MeshCoreConnectionState.connected) { - return; - } - - _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPortKey = null; - _activeUsbPortLabel = null; - - await stopScan(); - _cancelReconnectTimer(); - _manualDisconnect = false; - _resetConnectionHandshakeState(); - _activeTransport = MeshCoreTransportType.usb; - _activeUsbPortKey = portName; - _activeUsbPortLabel = portName; - _setState(MeshCoreConnectionState.connecting); - - try { - await _usbFrameSubscription?.cancel(); - _usbFrameSubscription = null; - await _usbSerialService.connect(portName: portName, baudRate: baudRate); - _activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey; - _activeUsbPortLabel = - _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; - notifyListeners(); - if (PlatformInfo.isWeb) { - await stopScan(); - } - await Future.delayed(const Duration(milliseconds: 200)); - _usbFrameSubscription = _usbSerialService.frameStream.listen( - _handleFrame, - onError: (error, stackTrace) { - _appDebugLogService?.error('USB transport error: $error', tag: 'USB'); - unawaited(disconnect(manual: false)); - }, - onDone: () { - unawaited(disconnect(manual: false)); - }, - ); - - _setState(MeshCoreConnectionState.connected); - _pendingInitialChannelSync = true; - await _requestDeviceInfo(); - _startBatteryPolling(); - var gotSelfInfo = await _waitForSelfInfo( - timeout: const Duration(seconds: 3), - ); - if (!gotSelfInfo) { - await refreshDeviceInfo(); - gotSelfInfo = await _waitForSelfInfo( - timeout: const Duration(seconds: 3), - ); - } - if (!gotSelfInfo) { - throw StateError('Timed out waiting for SELF_INFO during connect'); - } - - await syncTime(); - } catch (error) { - _appDebugLogService?.error('USB connection error: $error', tag: 'USB'); - await disconnect(manual: false); - rethrow; - } - } - Future _waitForSelfInfo({required Duration timeout}) async { if (_selfPublicKey != null) return true; if (!isConnected) return false; @@ -1139,17 +1116,60 @@ class MeshCoreConnector extends ChangeNotifier { return result; } + Future _startBleInitialSync() async { + if (_bleInitialSyncStarted || + !isConnected || + _activeTransport != MeshCoreTransportType.bluetooth) { + return; + } + _bleInitialSyncStarted = true; + + await _requestDeviceInfo(); + _startBatteryPolling(); + + if (PlatformInfo.isWeb) { + // Keep Web BLE startup writes light while notifications settle. + unawaited( + Future(() async { + await Future.delayed(const Duration(seconds: 5)); + if (!isConnected || + !PlatformInfo.isWeb || + _activeTransport != MeshCoreTransportType.bluetooth) { + return; + } + await syncTime(); + }), + ); + return; + } + + final gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + } + + unawaited(syncTime()); + _pendingDeferredChannelSyncAfterContacts = true; + } + void _resetConnectionHandshakeState() { _selfPublicKey = null; _selfName = null; _selfLatitude = null; _selfLongitude = null; _awaitingSelfInfo = false; + _webInitialHandshakeRequestSent = false; _selfInfoRetryTimer?.cancel(); _selfInfoRetryTimer = null; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; _pendingInitialContactsSync = false; + _bleInitialSyncStarted = false; + _pendingDeferredChannelSyncAfterContacts = false; + _webInitialHandshakeRequestSent = false; } bool get _shouldAutoReconnect => @@ -1205,6 +1225,14 @@ class MeshCoreConnector extends ChangeNotifier { Future disconnect({bool manual = true}) async { if (_state == MeshCoreConnectionState.disconnecting) return; final transportAtDisconnect = _activeTransport; + final transportLabel = transportAtDisconnect == MeshCoreTransportType.usb + ? 'USB' + : 'BLE'; + + _appDebugLogService?.info( + 'Starting disconnect transport=$transportLabel manual=$manual', + tag: 'Connection', + ); if (manual) { _manualDisconnect = true; @@ -1280,6 +1308,10 @@ class MeshCoreConnector extends ChangeNotifier { _activeUsbPortLabel = null; _setState(MeshCoreConnectionState.disconnected); + _appDebugLogService?.info( + 'Disconnect complete transport=$transportLabel manual=$manual', + tag: 'Connection', + ); if (!manual && transportAtDisconnect == MeshCoreTransportType.bluetooth) { _scheduleReconnect(); } @@ -1345,7 +1377,18 @@ class MeshCoreConnector extends ChangeNotifier { Future refreshDeviceInfo() async { if (!isConnected) return; + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + _webInitialHandshakeRequestSent && + _selfPublicKey == null) { + return; + } _awaitingSelfInfo = true; + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + _selfPublicKey == null) { + _webInitialHandshakeRequestSent = true; + } await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildAppStartFrame()); await requestBatteryStatus(force: true); @@ -1356,7 +1399,18 @@ class MeshCoreConnector extends ChangeNotifier { Future _requestDeviceInfo() async { if (!isConnected || _awaitingSelfInfo) return; + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + _webInitialHandshakeRequestSent && + _selfPublicKey == null) { + return; + } _awaitingSelfInfo = true; + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + _selfPublicKey == null) { + _webInitialHandshakeRequestSent = true; + } await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildAppStartFrame()); await sendFrame(buildGetCustomVarsFrame()); @@ -2183,6 +2237,12 @@ class MeshCoreConnector extends ChangeNotifier { _pendingQueueSync = false; unawaited(syncQueuedMessages(force: true)); } + if (_pendingDeferredChannelSyncAfterContacts && + (_activeTransport == MeshCoreTransportType.bluetooth || + _activeTransport == MeshCoreTransportType.usb)) { + _pendingDeferredChannelSyncAfterContacts = false; + unawaited(getChannels()); + } break; case respCodeContactMsgRecv: case respCodeContactMsgRecvV3: @@ -2294,6 +2354,8 @@ class MeshCoreConnector extends ChangeNotifier { // [58+] = node_name if (frame.length < 4 + pubKeySize) return; + final wasAwaitingSelfInfo = _awaitingSelfInfo; + _currentTxPower = frame[2]; _maxTxPower = frame[3]; _selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize)); @@ -2325,15 +2387,25 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; notifyListeners(); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + !wasAwaitingSelfInfo) { + return; + } + // Auto-fetch contacts after getting self info. On web BLE, defer this // until after channel 0 so startup writes stay serialized. if (PlatformInfo.isWeb && _activeTransport == MeshCoreTransportType.bluetooth) { _pendingInitialContactsSync = true; + } else if (_activeTransport == MeshCoreTransportType.usb) { + _pendingDeferredChannelSyncAfterContacts = true; + getContacts(); } else { getContacts(); } - if (_shouldGateInitialChannelSync) { + if (_shouldGateInitialChannelSync && + _activeTransport != MeshCoreTransportType.usb) { _maybeStartInitialChannelSync(); } } @@ -2367,6 +2439,7 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(loadChannelSettings(maxChannels: nextMaxChannels)); unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels)); if (isConnected && + _selfPublicKey != null && (!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) { unawaited(getChannels(maxChannels: nextMaxChannels)); } @@ -3524,17 +3597,13 @@ class MeshCoreConnector extends ChangeNotifier { // For 1:1 chats, sender is implicit (null) String? senderName; if (isRoomServer && !msg.isOutgoing) { - // Treat a missing room-contact key as unknown instead of matching every - // contact via an empty prefix. - if (msg.fourByteRoomContactKey.length == 4) { - final senderContact = _contacts.cast().firstWhere( - (c) => - c != null && - _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), - orElse: () => null, - ); - senderName = senderContact?.name; - } + final senderContact = _contacts.cast().firstWhere( + (c) => + c != null && + _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), + orElse: () => null, + ); + senderName = senderContact?.name; } else if (isRoomServer && msg.isOutgoing) { senderName = selfName; } diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart new file mode 100644 index 0000000..b1fbe15 --- /dev/null +++ b/lib/connector/meshcore_connector_usb.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart'; + +import 'meshcore_connector.dart'; + +class MeshCoreConnectorUsb { + const MeshCoreConnectorUsb(this.connector); + + final MeshCoreConnector connector; + + MeshCoreConnectionState get state => connector.state; + MeshCoreTransportType get activeTransport => connector.activeTransport; + String? get activeUsbPortDisplayLabel => connector.activeUsbPortDisplayLabel; + bool get isUsbTransportConnected => connector.isUsbTransportConnected; + + void addListener(VoidCallback listener) => connector.addListener(listener); + void removeListener(VoidCallback listener) => + connector.removeListener(listener); + + Future> listPorts() => connector.listUsbPorts(); + + void setRequestPortLabel(String label) { + connector.setUsbRequestPortLabel(label); + } + + Future connect({required String portName, int baudRate = 115200}) { + return connector.connectUsb(portName: portName, baudRate: baudRate); + } + + Future disconnect({bool manual = true}) { + return connector.disconnect(manual: manual); + } +} diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8c99338..1ac7acc 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", +{ + "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,33 +9,33 @@ }, "@@locale": "bg", "appTitle": "MeshCore Open", - "nav_contacts": "Контакти", - "nav_channels": "Канали", - "nav_map": "Карта", - "common_cancel": "Отказ", - "common_connect": "Свържи се", - "common_unknownDevice": "Неизвестно устройство", - "common_save": "Запази", - "common_delete": "Изтрий", - "common_close": "Затвори", - "common_edit": "Редактирай", - "common_add": "Добави", - "common_settings": "Настройки", - "common_disconnect": "Прекъсни", - "common_connected": "Свързано", - "common_disconnected": "Откъснато", - "common_create": "Създай", - "common_continue": "Продължи", - "common_share": "Сподели", - "common_copy": "Копирай", - "common_retry": "Опитай отново", - "common_hide": "Скриване", - "common_remove": "Изтрий", - "common_enable": "Активирай", - "common_disable": "Деактивирай", - "common_reboot": "Рестартирай", - "common_loading": "Зареждане...", - "common_notAvailable": "—", + "nav_contacts": "Контакти", + "nav_channels": "Канали", + "nav_map": "Карта", + "common_cancel": "Отказ", + "common_connect": "Свържи се", + "common_unknownDevice": "Неизвестно устройство", + "common_save": "Запази", + "common_delete": "Изтрий", + "common_close": "Затвори", + "common_edit": "Редактирай", + "common_add": "Добави", + "common_settings": "Настройки", + "common_disconnect": "Прекъсни", + "common_connected": "Свързано", + "common_disconnected": "Откъснато", + "common_create": "Създай", + "common_continue": "Продължи", + "common_share": "Сподели", + "common_copy": "Копирай", + "common_retry": "Опитай отново", + "common_hide": "Скриване", + "common_remove": "Изтрий", + "common_enable": "Активирай", + "common_disable": "Деактивирай", + "common_reboot": "Рестартирай", + "common_loading": "Зареждане...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Сканиране за устройства...", - "scanner_connecting": "Свързвам се...", - "scanner_disconnecting": "Изключване...", - "scanner_notConnected": "Не е свързан", - "scanner_connectedTo": "Свързано с {deviceName}", + "scanner_scanning": "Сканиране за устройства...", + "scanner_connecting": "Свързвам се...", + "scanner_disconnecting": "Изключване...", + "scanner_notConnected": "Не е свързан", + "scanner_connectedTo": "Свързано с {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,9 +65,9 @@ } } }, - "scanner_searchingDevices": "Търсене на устройства MeshCore...", - "scanner_tapToScan": "Натиснете Сканиране, за да намерите устройства MeshCore.", - "scanner_connectionFailed": "Връзката не успя: {error}", + "scanner_searchingDevices": "Търсене на устройства MeshCore...", + "scanner_tapToScan": "Натиснете Сканиране, за да намерите устройства MeshCore.", + "scanner_connectionFailed": "Връзката не успя: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,52 +75,52 @@ } } }, - "scanner_stop": "Спрете", - "scanner_scan": "Сканирай", - "device_quickSwitch": "Бързо превключване", + "scanner_stop": "Спрете", + "scanner_scan": "Сканирай", + "device_quickSwitch": "Бързо превключване", "device_meshcore": "MeshCore", - "settings_title": "Настройки", - "settings_deviceInfo": "Информация за устройството", - "settings_appSettings": "Настройки на приложението", - "settings_appSettingsSubtitle": "Уведомления, съобщения и предпочитания за карта", - "settings_nodeSettings": "Настройки на възела", - "settings_nodeName": "Име на възела", - "settings_nodeNameNotSet": "Не е зададено", - "settings_nodeNameHint": "Въведете име на възел", - "settings_nodeNameUpdated": "Името е актуализирано", - "settings_radioSettings": "Настройки на радиопредавателя", - "settings_radioSettingsSubtitle": "Честота, мощност, разпространяващ фактор", - "settings_radioSettingsUpdated": "Радио настройките са актуализирани", - "settings_location": "Местоположение", - "settings_locationSubtitle": "Координати на GPS", - "settings_locationUpdated": "Местоположението е актуализирано", - "settings_locationBothRequired": "Въведете както географска ширина, така и географска дължина.", - "settings_locationInvalid": "Невалидна ширина или дължина.", - "settings_latitude": "Широчина", - "settings_longitude": "Дължина", - "settings_privacyMode": "Режим на поверителност", - "settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", - "settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.", - "settings_privacyModeEnabled": "Режим на поверителност е активиран", - "settings_privacyModeDisabled": "Режим на поверителност е деактивиран", - "settings_actions": "Действия", - "settings_sendAdvertisement": "Изпрати Реклама", - "settings_sendAdvertisementSubtitle": "Сега присъствие в ефир", - "settings_advertisementSent": "Реклама изпратена", - "settings_syncTime": "Време за синхронизация", - "settings_syncTimeSubtitle": "Задайте часовника на устройството да отговаря на времето на телефона.", - "settings_timeSynchronized": "Синхронизирано във времето", - "settings_refreshContacts": "Презареди контакти", - "settings_refreshContactsSubtitle": "Презареди списъка с контакти от устройството", - "settings_rebootDevice": "Рестартирайте устройството", - "settings_rebootDeviceSubtitle": "Рестартирайте устройството MeshCore", - "settings_rebootDeviceConfirm": "Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.", - "settings_debug": "Отстрани", - "settings_bleDebugLog": "Лог за отстраняване на грешки на BLE", - "settings_bleDebugLogSubtitle": "Команди, отговори и сурови данни BLE", - "settings_appDebugLog": "Лог на отстраняване на грешки на приложението", - "settings_appDebugLogSubtitle": "Съобщения за отстраняване на грешки на приложението", - "settings_about": "За нас", + "settings_title": "Настройки", + "settings_deviceInfo": "Информация за устройството", + "settings_appSettings": "Настройки на приложението", + "settings_appSettingsSubtitle": "Уведомления, съобщения и предпочитания за карта", + "settings_nodeSettings": "Настройки на възела", + "settings_nodeName": "Име на възела", + "settings_nodeNameNotSet": "Не е зададено", + "settings_nodeNameHint": "Въведете име на възел", + "settings_nodeNameUpdated": "Името е актуализирано", + "settings_radioSettings": "Настройки на радиопредавателя", + "settings_radioSettingsSubtitle": "Честота, мощност, разпространяващ фактор", + "settings_radioSettingsUpdated": "Радио настройките са актуализирани", + "settings_location": "Местоположение", + "settings_locationSubtitle": "Координати на GPS", + "settings_locationUpdated": "Местоположението е актуализирано", + "settings_locationBothRequired": "Въведете както географска ширина, така и географска дължина.", + "settings_locationInvalid": "Невалидна ширина или дължина.", + "settings_latitude": "Широчина", + "settings_longitude": "Дължина", + "settings_privacyMode": "Режим на поверителност", + "settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", + "settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.", + "settings_privacyModeEnabled": "Режим на поверителност е активиран", + "settings_privacyModeDisabled": "Режим на поверителност е деактивиран", + "settings_actions": "Действия", + "settings_sendAdvertisement": "Изпрати Реклама", + "settings_sendAdvertisementSubtitle": "Сега присъствие в ефир", + "settings_advertisementSent": "Реклама изпратена", + "settings_syncTime": "Време за синхронизация", + "settings_syncTimeSubtitle": "Задайте часовника на устройството да отговаря на времето на телефона.", + "settings_timeSynchronized": "Синхронизирано във времето", + "settings_refreshContacts": "Презареди контакти", + "settings_refreshContactsSubtitle": "Презареди списъка с контакти от устройството", + "settings_rebootDevice": "Рестартирайте устройството", + "settings_rebootDeviceSubtitle": "Рестартирайте устройството MeshCore", + "settings_rebootDeviceConfirm": "Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.", + "settings_debug": "Отстрани", + "settings_bleDebugLog": "Лог за отстраняване на грешки на BLE", + "settings_bleDebugLogSubtitle": "Команди, отговори и сурови данни BLE", + "settings_appDebugLog": "Лог на отстраняване на грешки на приложението", + "settings_appDebugLogSubtitle": "Съобщения за отстраняване на грешки на приложението", + "settings_about": "За нас", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -129,26 +129,26 @@ } } }, - "settings_aboutLegalese": "Проект MeshCore с отворен код 2024 г.", - "settings_aboutDescription": "Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.", - "settings_infoName": "Име", - "settings_infoId": "ИД", - "settings_infoStatus": "Статус", - "settings_infoBattery": "Батерия", - "settings_infoPublicKey": "Общ публичен ключ", - "settings_infoContactsCount": "Брой контакти", - "settings_infoChannelCount": "Брой канали", - "settings_presets": "Предварителни настройки", - "settings_frequency": "Честота (MHz)", + "settings_aboutLegalese": "Проект MeshCore с отворен код 2024 г.", + "settings_aboutDescription": "Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.", + "settings_infoName": "Име", + "settings_infoId": "ИД", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батерия", + "settings_infoPublicKey": "Общ публичен ключ", + "settings_infoContactsCount": "Брой контакти", + "settings_infoChannelCount": "Брой канали", + "settings_presets": "Предварителни настройки", + "settings_frequency": "Честота (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)", - "settings_bandwidth": "Ширина на честотния спектър", - "settings_spreadingFactor": "Фактор на разпространение", - "settings_codingRate": "Такса за кодиране", - "settings_txPower": "TX Мощност (dBm)", + "settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)", + "settings_bandwidth": "Ширина на честотния спектър", + "settings_spreadingFactor": "Фактор на разпространение", + "settings_codingRate": "Такса за кодиране", + "settings_txPower": "TX Мощност (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)", - "settings_error": "Грешка: {message}", + "settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)", + "settings_error": "Грешка: {message}", "@settings_error": { "placeholders": { "message": { @@ -156,51 +156,51 @@ } } }, - "appSettings_title": "Настройки на приложението", - "appSettings_appearance": "Външен вид", - "appSettings_theme": "Тема", - "appSettings_themeSystem": "Система по подразбиране", - "appSettings_themeLight": "Ярка", - "appSettings_themeDark": "Тъмно", - "appSettings_language": "Език", - "appSettings_languageSystem": "Система по подразбиране", + "appSettings_title": "Настройки на приложението", + "appSettings_appearance": "Външен вид", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Система по подразбиране", + "appSettings_themeLight": "Ярка", + "appSettings_themeDark": "Тъмно", + "appSettings_language": "Език", + "appSettings_languageSystem": "Система по подразбиране", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_notifications": "Уведомления", - "appSettings_enableNotifications": "Активирай Известия", - "appSettings_enableNotificationsSubtitle": "Получете известия за съобщения и реклами", - "appSettings_notificationPermissionDenied": "Отказвано е разрешение за известия", - "appSettings_notificationsEnabled": "Уведомителни са активирани", - "appSettings_notificationsDisabled": "Известия са изключени", - "appSettings_messageNotifications": "Уведомления", - "appSettings_messageNotificationsSubtitle": "Покажи известие при получаване на нови съобщения", - "appSettings_channelMessageNotifications": "Уведомления за съобщения от канал", - "appSettings_channelMessageNotificationsSubtitle": "Покажи известие при получаване на съобщения от канали", - "appSettings_advertisementNotifications": "Уведомления за реклами", - "appSettings_advertisementNotificationsSubtitle": "Покажи известие, когато бъдат открити нови възли.", - "appSettings_messaging": "Съобщения", - "appSettings_clearPathOnMaxRetry": "Изчисти Път на Макс Опит", - "appSettings_clearPathOnMaxRetrySubtitle": "Възстанови контактния път след 5 неуспешни опита за изпращане", - "appSettings_pathsWillBeCleared": "Пътищата ще бъдат почистени след 5 неуспешни опита.", - "appSettings_pathsWillNotBeCleared": "Пътищата няма да бъдат автоматично изчистени.", - "appSettings_autoRouteRotation": "Автоматично маршрутизиране на завъртания", - "appSettings_autoRouteRotationSubtitle": "Превключете между най-добрите пътища и режим на наводняване", - "appSettings_autoRouteRotationEnabled": "Автоматично маршрутизиране вкл.", - "appSettings_autoRouteRotationDisabled": "Автоматично маршрутизирането е деактивирано", - "appSettings_battery": "Батерия", - "appSettings_batteryChemistry": "Химия на батерията", - "appSettings_batteryChemistryPerDevice": "Зададено за устройство ({deviceName})", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_notifications": "Уведомления", + "appSettings_enableNotifications": "Активирай Известия", + "appSettings_enableNotificationsSubtitle": "Получете известия за съобщения и реклами", + "appSettings_notificationPermissionDenied": "Отказвано е разрешение за известия", + "appSettings_notificationsEnabled": "Уведомителни са активирани", + "appSettings_notificationsDisabled": "Известия са изключени", + "appSettings_messageNotifications": "Уведомления", + "appSettings_messageNotificationsSubtitle": "Покажи известие при получаване на нови съобщения", + "appSettings_channelMessageNotifications": "Уведомления за съобщения от канал", + "appSettings_channelMessageNotificationsSubtitle": "Покажи известие при получаване на съобщения от канали", + "appSettings_advertisementNotifications": "Уведомления за реклами", + "appSettings_advertisementNotificationsSubtitle": "Покажи известие, когато бъдат открити нови възли.", + "appSettings_messaging": "Съобщения", + "appSettings_clearPathOnMaxRetry": "Изчисти Път на Макс Опит", + "appSettings_clearPathOnMaxRetrySubtitle": "Възстанови контактния път след 5 неуспешни опита за изпращане", + "appSettings_pathsWillBeCleared": "Пътищата ще бъдат почистени след 5 неуспешни опита.", + "appSettings_pathsWillNotBeCleared": "Пътищата няма да бъдат автоматично изчистени.", + "appSettings_autoRouteRotation": "Автоматично маршрутизиране на завъртания", + "appSettings_autoRouteRotationSubtitle": "Превключете между най-добрите пътища и режим на наводняване", + "appSettings_autoRouteRotationEnabled": "Автоматично маршрутизиране вкл.", + "appSettings_autoRouteRotationDisabled": "Автоматично маршрутизирането е деактивирано", + "appSettings_battery": "Батерия", + "appSettings_batteryChemistry": "Химия на батерията", + "appSettings_batteryChemistryPerDevice": "Зададено за устройство ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Свържете се с устройство, за да изберете.", + "appSettings_batteryChemistryConnectFirst": "Свържете се с устройство, за да изберете.", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", - "appSettings_batteryLifepo4": "Литиево желязо фосфат (2.6-3.65V)", - "appSettings_batteryLipo": "Литиев полимер (3.0-4.2V)", - "appSettings_mapDisplay": "Карта за показване", - "appSettings_showRepeaters": "Показване на повторители", - "appSettings_showRepeatersSubtitle": "Показване на възпроизвеждащи се възли на картата", - "appSettings_showChatNodes": "Покажи Възли на Чат", - "appSettings_showChatNodesSubtitle": "Показване на чат възли на картата", - "appSettings_showOtherNodes": "Покажи други възли", - "appSettings_showOtherNodesSubtitle": "Покажи други типове възли на картата", - "appSettings_timeFilter": "Филтриране по време", - "appSettings_timeFilterShowAll": "Покажи всички възли", - "appSettings_timeFilterShowLast": "Покажи възли от последните {hours} часа", + "appSettings_batteryLifepo4": "Литиево желязо фосфат (2.6-3.65V)", + "appSettings_batteryLipo": "Литиев полимер (3.0-4.2V)", + "appSettings_mapDisplay": "Карта за показване", + "appSettings_showRepeaters": "Показване на повторители", + "appSettings_showRepeatersSubtitle": "Показване на възпроизвеждащи се възли на картата", + "appSettings_showChatNodes": "Покажи Възли на Чат", + "appSettings_showChatNodesSubtitle": "Показване на чат възли на картата", + "appSettings_showOtherNodes": "Покажи други възли", + "appSettings_showOtherNodesSubtitle": "Покажи други типове възли на картата", + "appSettings_timeFilter": "Филтриране по време", + "appSettings_timeFilterShowAll": "Покажи всички възли", + "appSettings_timeFilterShowLast": "Покажи възли от последните {hours} часа", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -229,16 +229,16 @@ } } }, - "appSettings_mapTimeFilter": "Филтри за време на картата", - "appSettings_showNodesDiscoveredWithin": "Покажи възлите, открити в:", - "appSettings_allTime": "Всичко време", - "appSettings_lastHour": "Последната минута", - "appSettings_last6Hours": "Последни 6 часа", - "appSettings_last24Hours": "Последно 24 часа", - "appSettings_lastWeek": "Миналата седмица", - "appSettings_offlineMapCache": "Кеш на офлайн карти", - "appSettings_noAreaSelected": "Няма избрана област", - "appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})", + "appSettings_mapTimeFilter": "Филтри за време на картата", + "appSettings_showNodesDiscoveredWithin": "Покажи възлите, открити в:", + "appSettings_allTime": "Всичко време", + "appSettings_lastHour": "Последната минута", + "appSettings_last6Hours": "Последни 6 часа", + "appSettings_last24Hours": "Последно 24 часа", + "appSettings_lastWeek": "Миналата седмица", + "appSettings_offlineMapCache": "Кеш на офлайн карти", + "appSettings_noAreaSelected": "Няма избрана област", + "appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,19 +249,19 @@ } } }, - "appSettings_debugCard": "Отстрани", - "appSettings_appDebugLogging": "Логване за отстраняване на грешки на приложението", - "appSettings_appDebugLoggingSubtitle": "Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.", - "appSettings_appDebugLoggingEnabled": "Режимът за отстраняване на грешки в приложението е активиран.", - "appSettings_appDebugLoggingDisabled": "Логването за отстраняване на грешки в приложението е изключено.", - "contacts_title": "Контакти", - "contacts_noContacts": "Няма контакти към момента.", - "contacts_contactsWillAppear": "Контактите ще се появят, когато устройствата рекламират.", - "contacts_searchContacts": "Търсене на контакти...", - "contacts_noUnreadContacts": "Няма непрочетени контакти", - "contacts_noContactsFound": "Няма намерени контакти или групи.", - "contacts_deleteContact": "Изтрий Контакт", - "contacts_removeConfirm": "Изтрий {contactName} от контактите?", + "appSettings_debugCard": "Отстрани", + "appSettings_appDebugLogging": "Логване за отстраняване на грешки на приложението", + "appSettings_appDebugLoggingSubtitle": "Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.", + "appSettings_appDebugLoggingEnabled": "Режимът за отстраняване на грешки в приложението е активиран.", + "appSettings_appDebugLoggingDisabled": "Логването за отстраняване на грешки в приложението е изключено.", + "contacts_title": "Контакти", + "contacts_noContacts": "Няма контакти към момента.", + "contacts_contactsWillAppear": "Контактите ще се появят, когато устройствата рекламират.", + "contacts_searchContacts": "Търсене на контакти...", + "contacts_noUnreadContacts": "Няма непрочетени контакти", + "contacts_noContactsFound": "Няма намерени контакти или групи.", + "contacts_deleteContact": "Изтрий Контакт", + "contacts_removeConfirm": "Изтрий {contactName} от контактите?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -269,12 +269,12 @@ } } }, - "contacts_manageRepeater": "Управление на Повтарящ се Елемент", - "contacts_roomLogin": "Вход в стаята", - "contacts_openChat": "Отвори чат", - "contacts_editGroup": "Редактирай Група", - "contacts_deleteGroup": "Изтрий група", - "contacts_deleteGroupConfirm": "Премахнете \"{groupName}\"?", + "contacts_manageRepeater": "Управление на Повтарящ се Елемент", + "contacts_roomLogin": "Вход в стаята", + "contacts_openChat": "Отвори чат", + "contacts_editGroup": "Редактирай Група", + "contacts_deleteGroup": "Изтрий група", + "contacts_deleteGroupConfirm": "Премахнете \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -282,10 +282,10 @@ } } }, - "contacts_newGroup": "Нова група", - "contacts_groupName": "Група", - "contacts_groupNameRequired": "Името на групата е задължително.", - "contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.", + "contacts_newGroup": "Нова група", + "contacts_groupName": "Група", + "contacts_groupNameRequired": "Името на групата е задължително.", + "contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -293,11 +293,11 @@ } } }, - "contacts_filterContacts": "Филтрирайте контактите...", - "contacts_noContactsMatchFilter": "Няма съвпадения с вашия филтър.", - "contacts_noMembers": "Няма членове", - "contacts_lastSeenNow": "Последно видяно сега", - "contacts_lastSeenMinsAgo": "Последна активност {minutes} минути преди", + "contacts_filterContacts": "Филтрирайте контактите...", + "contacts_noContactsMatchFilter": "Няма съвпадения с вашия филтър.", + "contacts_noMembers": "Няма членове", + "contacts_lastSeenNow": "Последно видяно сега", + "contacts_lastSeenMinsAgo": "Последна активност {minutes} минути преди", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Последно видяно преди час", - "contacts_lastSeenHoursAgo": "Последно видян {hours} часа преди.", + "contacts_lastSeenHourAgo": "Последно видяно преди час", + "contacts_lastSeenHoursAgo": "Последно видян {hours} часа преди.", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Последно видяно преди 1 ден", - "contacts_lastSeenDaysAgo": "Последно видян {days} дни преди.", + "contacts_lastSeenDayAgo": "Последно видяно преди 1 ден", + "contacts_lastSeenDaysAgo": "Последно видян {days} дни преди.", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -323,12 +323,12 @@ } } }, - "channels_title": "Канали", - "channels_noChannelsConfigured": "Няма конфигурирани канали", - "channels_addPublicChannel": "Добави публичен канал", - "channels_searchChannels": "Търсене на канали...", - "channels_noChannelsFound": "Няма намерени канали", - "channels_channelIndex": "Канал {index}", + "channels_title": "Канали", + "channels_noChannelsConfigured": "Няма конфигурирани канали", + "channels_addPublicChannel": "Добави публичен канал", + "channels_searchChannels": "Търсене на канали...", + "channels_noChannelsFound": "Няма намерени канали", + "channels_channelIndex": "Канал {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -336,16 +336,16 @@ } } }, - "channels_hashtagChannel": "Канал с хаштаг", - "channels_public": "Публично", - "channels_private": "Личен", - "channels_publicChannel": "Публичен канал", - "channels_privateChannel": "Частен канал", - "channels_editChannel": "Редактирай канал", - "channels_muteChannel": "Заглуши канала", - "channels_unmuteChannel": "Включи известията на канала", - "channels_deleteChannel": "Изтрий канала", - "channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.", + "channels_hashtagChannel": "Канал с хаштаг", + "channels_public": "Публично", + "channels_private": "Личен", + "channels_publicChannel": "Публичен канал", + "channels_privateChannel": "Частен канал", + "channels_editChannel": "Редактирай канал", + "channels_muteChannel": "Заглуши канала", + "channels_unmuteChannel": "Включи известията на канала", + "channels_deleteChannel": "Изтрий канала", + "channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Каналът \"{name}\" е изтрит", + "channels_channelDeleted": "Каналът \"{name}\" е изтрит", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Добави Канал", - "channels_channelIndexLabel": "Индекс на канал", - "channels_channelName": "Име на канала", - "channels_usePublicChannel": "Използвайте публичен канал", - "channels_standardPublicPsk": "Стандартен публичен PSK", + "channels_addChannel": "Добави Канал", + "channels_channelIndexLabel": "Индекс на канал", + "channels_channelName": "Име на канала", + "channels_usePublicChannel": "Използвайте публичен канал", + "channels_standardPublicPsk": "Стандартен публичен PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Генерирай случайна PSK", - "channels_enterChannelName": "Моля, въведете име на канал.", - "channels_pskMustBe32Hex": "PSK трябва да бъде 32 шестнаредни знака.", - "channels_channelAdded": "Каналът \"{name}\" е добавен", + "channels_generateRandomPsk": "Генерирай случайна PSK", + "channels_enterChannelName": "Моля, въведете име на канал.", + "channels_pskMustBe32Hex": "PSK трябва да бъде 32 шестнаредни знака.", + "channels_channelAdded": "Каналът \"{name}\" е добавен", "@channels_channelAdded": { "placeholders": { "name": { @@ -378,7 +378,7 @@ } } }, - "channels_editChannelTitle": "Редактирай Канал {index}", + "channels_editChannelTitle": "Редактирай Канал {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -386,8 +386,8 @@ } } }, - "channels_smazCompression": "Компресия SMAZ", - "channels_channelUpdated": "Каналът \"{name}\" е актуализиран", + "channels_smazCompression": "Компресия SMAZ", + "channels_channelUpdated": "Каналът \"{name}\" е актуализиран", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,16 +395,16 @@ } } }, - "channels_publicChannelAdded": "Публичен канал добавен", - "channels_sortBy": "Сортирай по", - "channels_sortManual": "Ръчно", + "channels_publicChannelAdded": "Публичен канал добавен", + "channels_sortBy": "Сортирай по", + "channels_sortManual": "Ръчно", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Последни съобщения", - "channels_sortUnread": "Непрочетено", - "chat_noMessages": "Няма съобщения.", - "chat_sendMessageToStart": "Изпрати съобщение, за да започнеш.", - "chat_originalMessageNotFound": "Съобщението не е намерено", - "chat_replyingTo": "Отговарям на {name}", + "channels_sortLatestMessages": "Последни съобщения", + "channels_sortUnread": "Непрочетено", + "chat_noMessages": "Няма съобщения.", + "chat_sendMessageToStart": "Изпрати съобщение, за да започнеш.", + "chat_originalMessageNotFound": "Съобщението не е намерено", + "chat_replyingTo": "Отговарям на {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "Отговори на {name}", + "chat_replyTo": "Отговори на {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -420,8 +420,8 @@ } } }, - "chat_location": "Местоположение", - "chat_sendMessageTo": "Изпрати съобщение на {contactName}", + "chat_location": "Местоположение", + "chat_sendMessageTo": "Изпрати съобщение на {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "Въведете съобщение...", - "chat_messageTooLong": "Съобщението е твърде дълго (макс {maxBytes} байта).", + "chat_typeMessage": "Въведете съобщение...", + "chat_messageTooLong": "Съобщението е твърде дълго (макс {maxBytes} байта).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,10 +438,10 @@ } } }, - "chat_messageCopied": "Съобщението е копирано", - "chat_messageDeleted": "Съобщението е изтрито", - "chat_retryingMessage": "Опитваме се отново.", - "chat_retryCount": "Опитай отново {current}/{max}", + "chat_messageCopied": "Съобщението е копирано", + "chat_messageDeleted": "Съобщението е изтрито", + "chat_retryingMessage": "Опитваме се отново.", + "chat_retryCount": "Опитай отново {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -452,33 +452,33 @@ } } }, - "chat_sendGif": "Изпрати GIF", - "chat_reply": "Отговори", - "chat_addReaction": "Добави Реакция", - "chat_me": "Аз", - "emojiCategorySmileys": "Емотикони", - "emojiCategoryGestures": "Жестове", - "emojiCategoryHearts": "Сърца", - "emojiCategoryObjects": "Обекти", - "gifPicker_title": "Изберете GIF", - "gifPicker_searchHint": "Търсене на GIF-ове...", - "gifPicker_poweredBy": "Задвижвано от GIPHY", - "gifPicker_noGifsFound": "Няма намерени GIF файлове.", - "gifPicker_failedLoad": "Не можа да се заредят GIF файловете", - "gifPicker_failedSearch": "Неуспешно търсене на GIF-ове", - "gifPicker_noInternet": "Няма интернет връзка", - "debugLog_appTitle": "Лог на отстраняване на грешки на приложението", - "debugLog_bleTitle": "Лог за отстраняване на грешки на BLE", - "debugLog_copyLog": "Копирай лог", - "debugLog_clearLog": "Изчисти логовете", - "debugLog_copied": "Копирано лого за отстраняване на грешки", - "debugLog_bleCopied": "Копиран лог от BLE", - "debugLog_noEntries": "Все още няма дебъг логове.", - "debugLog_enableInSettings": "Активирайте отстраняване на грешки в настройките на приложението", - "debugLog_frames": "Рамки", + "chat_sendGif": "Изпрати GIF", + "chat_reply": "Отговори", + "chat_addReaction": "Добави Реакция", + "chat_me": "Аз", + "emojiCategorySmileys": "Емотикони", + "emojiCategoryGestures": "Жестове", + "emojiCategoryHearts": "Сърца", + "emojiCategoryObjects": "Обекти", + "gifPicker_title": "Изберете GIF", + "gifPicker_searchHint": "Търсене на GIF-ове...", + "gifPicker_poweredBy": "Задвижвано от GIPHY", + "gifPicker_noGifsFound": "Няма намерени GIF файлове.", + "gifPicker_failedLoad": "Не можа да се заредят GIF файловете", + "gifPicker_failedSearch": "Неуспешно търсене на GIF-ове", + "gifPicker_noInternet": "Няма интернет връзка", + "debugLog_appTitle": "Лог на отстраняване на грешки на приложението", + "debugLog_bleTitle": "Лог за отстраняване на грешки на BLE", + "debugLog_copyLog": "Копирай лог", + "debugLog_clearLog": "Изчисти логовете", + "debugLog_copied": "Копирано лого за отстраняване на грешки", + "debugLog_bleCopied": "Копиран лог от BLE", + "debugLog_noEntries": "Все още няма дебъг логове.", + "debugLog_enableInSettings": "Активирайте отстраняване на грешки в настройките на приложението", + "debugLog_frames": "Рамки", "debugLog_rawLogRx": "Raw Log-RX", - "debugLog_noBleActivity": "Няма BLE активност към момента.", - "debugFrame_length": "Дължина на кадъра: {count} байта", + "debugLog_noBleActivity": "Няма BLE активност към момента.", + "debugFrame_length": "Дължина на кадъра: {count} байта", "@debugFrame_length": { "placeholders": { "count": { @@ -486,7 +486,7 @@ } } }, - "debugFrame_command": "Команда: 0x{value}", + "debugFrame_command": "Команда: 0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -494,8 +494,8 @@ } } }, - "debugFrame_textMessageHeader": "Съобщение:", - "debugFrame_destinationPubKey": "- Дестинация Публичен Ключ: {pubKey}", + "debugFrame_textMessageHeader": "Съобщение:", + "debugFrame_destinationPubKey": "- Дестинация Публичен Ключ: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- Време: {timestamp}", + "debugFrame_timestamp": "- Време: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -511,7 +511,7 @@ } } }, - "debugFrame_flags": "- Флагове: 0x{value}", + "debugFrame_flags": "- Флагове: 0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -519,7 +519,7 @@ } } }, - "debugFrame_textType": "- Тип текст: {type} ({label})", + "debugFrame_textType": "- Тип текст: {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -531,8 +531,8 @@ } }, "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Просто", - "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_textTypePlain": "Просто", + "debugFrame_text": "- Текст: \"{text}\"", "@debugFrame_text": { "placeholders": { "text": { @@ -540,15 +540,15 @@ } } }, - "debugFrame_hexDump": "Хексадесетичен Dump:", - "chat_pathManagement": "Управление на пътища", - "chat_routingMode": "Режим на маршрутизиране", - "chat_autoUseSavedPath": "Автоматично (използвай запазения път)", - "chat_forceFloodMode": "Принуди режим на наводняване", - "chat_recentAckPaths": "Неотдавни ACK пътища (докоснете, за да използвате):", - "chat_pathHistoryFull": "Историята на пътя е пълна. Премахнете записи, за да добавите нови.", - "chat_hopSingular": "скочи", - "chat_hopPlural": "скоци", + "debugFrame_hexDump": "Хексадесетичен Dump:", + "chat_pathManagement": "Управление на пътища", + "chat_routingMode": "Режим на маршрутизиране", + "chat_autoUseSavedPath": "Автоматично (използвай запазения път)", + "chat_forceFloodMode": "Принуди режим на наводняване", + "chat_recentAckPaths": "Неотдавни ACK пътища (докоснете, за да използвате):", + "chat_pathHistoryFull": "Историята на пътя е пълна. Премахнете записи, за да добавите нови.", + "chat_hopSingular": "скочи", + "chat_hopPlural": "скоци", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { @@ -557,20 +557,20 @@ } } }, - "chat_successes": "Успехи", - "chat_removePath": "Премахни пътя", - "chat_noPathHistoryYet": "Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.", - "chat_pathActions": "Действия по пътя:", - "chat_setCustomPath": "Задайте персонализиран път", - "chat_setCustomPathSubtitle": "Ръчно укажете маршрутен път", - "chat_clearPath": "Почисти Път", - "chat_clearPathSubtitle": "Принуди преоткриване при следващо изпращане", - "chat_pathCleared": "Пътят е почистен. Следващото съобщение ще открие маршрута отново.", - "chat_floodModeSubtitle": "Използвайте превключвателя за маршрутизиране в лентата на приложението.", - "chat_floodModeEnabled": "Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.", - "chat_fullPath": "Пълен път", - "chat_pathDetailsNotAvailable": "Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.", - "chat_pathSetHops": "Пътят е зададен: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_successes": "Успехи", + "chat_removePath": "Премахни пътя", + "chat_noPathHistoryYet": "Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.", + "chat_pathActions": "Действия по пътя:", + "chat_setCustomPath": "Задайте персонализиран път", + "chat_setCustomPathSubtitle": "Ръчно укажете маршрутен път", + "chat_clearPath": "Почисти Път", + "chat_clearPathSubtitle": "Принуди преоткриване при следващо изпращане", + "chat_pathCleared": "Пътят е почистен. Следващото съобщение ще открие маршрута отново.", + "chat_floodModeSubtitle": "Използвайте превключвателя за маршрутизиране в лентата на приложението.", + "chat_floodModeEnabled": "Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.", + "chat_fullPath": "Пълен път", + "chat_pathDetailsNotAvailable": "Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.", + "chat_pathSetHops": "Пътят е зададен: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Запазено локално. Свържете се за синхронизиране.", - "chat_pathDeviceConfirmed": "Устройство потвърдено.", - "chat_pathDeviceNotConfirmed": "Устройството все още не е потвърдено.", - "chat_type": "Въведете", - "chat_path": "Пътекино", - "chat_publicKey": "Публичен ключ", - "chat_compressOutgoingMessages": "Компресиране на изходящи съобщения", - "chat_floodForced": "Потоп (принуден)", - "chat_directForced": "Директно (принудително)", - "chat_hopsForced": "{count} скока (принудително)", + "chat_pathSavedLocally": "Запазено локално. Свържете се за синхронизиране.", + "chat_pathDeviceConfirmed": "Устройство потвърдено.", + "chat_pathDeviceNotConfirmed": "Устройството все още не е потвърдено.", + "chat_type": "Въведете", + "chat_path": "Пътекино", + "chat_publicKey": "Публичен ключ", + "chat_compressOutgoingMessages": "Компресиране на изходящи съобщения", + "chat_floodForced": "Потоп (принуден)", + "chat_directForced": "Директно (принудително)", + "chat_hopsForced": "{count} скока (принудително)", "@chat_hopsForced": { "placeholders": { "count": { @@ -598,10 +598,10 @@ } } }, - "chat_floodAuto": "Потоп (автоматично)", - "chat_direct": "Директно", - "chat_poiShared": "Споделено място от интерес", - "chat_unread": "Непрочетени: {count}", + "chat_floodAuto": "Потоп (автоматично)", + "chat_direct": "Директно", + "chat_poiShared": "Споделено място от интерес", + "chat_unread": "Непрочетени: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Отваряне на връзката?", - "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?", - "chat_open": "Отвори", - "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}", + "chat_openLink": "Отваряне на връзката?", + "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?", + "chat_open": "Отвори", + "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,11 +620,11 @@ } } }, - "chat_invalidLink": "Невалиден формат на връзката", - "map_title": "Карта на възлите", - "map_noNodesWithLocation": "Няма възли с данни за местоположение.", - "map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.", - "map_nodesCount": "Нодове: {count}", + "chat_invalidLink": "Невалиден формат на връзката", + "map_title": "Карта на възлите", + "map_noNodesWithLocation": "Няма възли с данни за местоположение.", + "map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.", + "map_nodesCount": "Нодове: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -632,7 +632,7 @@ } } }, - "map_pinsCount": "Ключове: {count}", + "map_pinsCount": "Ключове: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -640,27 +640,27 @@ } } }, - "map_chat": "Чат", - "map_repeater": "Повтарящ се", - "map_room": "Стая", - "map_sensor": "Датчик", - "map_pinDm": "Задържане (DM)", - "map_pinPrivate": "Задържане (Приватно)", - "map_pinPublic": "Публичен ключ", - "map_lastSeen": "Последна видяна", - "map_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", - "map_from": "От", - "map_source": "Източник", - "map_flags": "Флаг", - "map_shareMarkerHere": "Споделете маркер тук", - "map_pinLabel": "Етикетиране на пин", - "map_label": "Етикет", - "map_pointOfInterest": "Точка на интерес", - "map_sendToContact": "Изпрати на контакт", - "map_sendToChannel": "Изпрати в канала", - "map_noChannelsAvailable": "Няма налични канали", - "map_publicLocationShare": "Споделяне на публично място", - "map_publicLocationShareConfirm": "Ще споделите местоположение в {channelLabel}. Този канал е публичен и всеки с PSK може да го види.", + "map_chat": "Чат", + "map_repeater": "Повтарящ се", + "map_room": "Стая", + "map_sensor": "Датчик", + "map_pinDm": "Задържане (DM)", + "map_pinPrivate": "Задържане (Приватно)", + "map_pinPublic": "Публичен ключ", + "map_lastSeen": "Последна видяна", + "map_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", + "map_from": "От", + "map_source": "Източник", + "map_flags": "Флаг", + "map_shareMarkerHere": "Споделете маркер тук", + "map_pinLabel": "Етикетиране на пин", + "map_label": "Етикет", + "map_pointOfInterest": "Точка на интерес", + "map_sendToContact": "Изпрати на контакт", + "map_sendToChannel": "Изпрати в канала", + "map_noChannelsAvailable": "Няма налични канали", + "map_publicLocationShare": "Споделяне на публично място", + "map_publicLocationShareConfirm": "Ще споделите местоположение в {channelLabel}. Този канал е публичен и всеки с PSK може да го види.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Свържете се с устройство, за да споделите маркери.", - "map_filterNodes": "Филтрирайте възли", - "map_nodeTypes": "Типове възли", - "map_chatNodes": "Възли на чата", - "map_repeaters": "Повторители", - "map_otherNodes": "Други възли", - "map_keyPrefix": "Префикс на ключа", - "map_filterByKeyPrefix": "Филтрирайте по префикс на ключ", - "map_publicKeyPrefix": "Префикс на публичен ключ", - "map_markers": "Маркери", - "map_showSharedMarkers": "Покажи споделени маркери", - "map_lastSeenTime": "Последна видяна дата", - "map_sharedPin": "Споделено копие", - "map_joinRoom": "Присъедини се към стаята", - "map_manageRepeater": "Управление на Повтарящ се Елемент", - "mapCache_title": "Кеш на офлайн карти", - "mapCache_selectAreaFirst": "Изберете област за кеширане първа", - "mapCache_noTilesToDownload": "Няма плочки за изтегляне за тази област.", - "mapCache_downloadTilesTitle": "Изтегли плочки", - "mapCache_downloadTilesPrompt": "Изтегли {count} плочки за офлайн употреба?", + "map_connectToShareMarkers": "Свържете се с устройство, за да споделите маркери.", + "map_filterNodes": "Филтрирайте възли", + "map_nodeTypes": "Типове възли", + "map_chatNodes": "Възли на чата", + "map_repeaters": "Повторители", + "map_otherNodes": "Други възли", + "map_keyPrefix": "Префикс на ключа", + "map_filterByKeyPrefix": "Филтрирайте по префикс на ключ", + "map_publicKeyPrefix": "Префикс на публичен ключ", + "map_markers": "Маркери", + "map_showSharedMarkers": "Покажи споделени маркери", + "map_lastSeenTime": "Последна видяна дата", + "map_sharedPin": "Споделено копие", + "map_joinRoom": "Присъедини се към стаята", + "map_manageRepeater": "Управление на Повтарящ се Елемент", + "mapCache_title": "Кеш на офлайн карти", + "mapCache_selectAreaFirst": "Изберете област за кеширане първа", + "mapCache_noTilesToDownload": "Няма плочки за изтегляне за тази област.", + "mapCache_downloadTilesTitle": "Изтегли плочки", + "mapCache_downloadTilesPrompt": "Изтегли {count} плочки за офлайн употреба?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,8 +695,8 @@ } } }, - "mapCache_downloadAction": "Изтегли", - "mapCache_cachedTiles": "Кеширани {count} плочки", + "mapCache_downloadAction": "Изтегли", + "mapCache_cachedTiles": "Кеширани {count} плочки", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Запазени {downloaded} плочки ({failed} неуспешни)", + "mapCache_cachedTilesWithFailed": "Запазени {downloaded} плочки ({failed} неуспешни)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Изчисти офлайн кеша", - "mapCache_clearOfflineCachePrompt": "Премахнете всички кеширани плочки на картата?", - "mapCache_offlineCacheCleared": "Кешът на устройството е изчистен.", - "mapCache_noAreaSelected": "Няма избрана област", - "mapCache_cacheArea": "Област с кеш", - "mapCache_useCurrentView": "Използвайте текущия изглед", - "mapCache_zoomRange": "Обхват на увеличението", - "mapCache_estimatedTiles": "Очаквани плочки: {count}", + "mapCache_clearOfflineCacheTitle": "Изчисти офлайн кеша", + "mapCache_clearOfflineCachePrompt": "Премахнете всички кеширани плочки на картата?", + "mapCache_offlineCacheCleared": "Кешът на устройството е изчистен.", + "mapCache_noAreaSelected": "Няма избрана област", + "mapCache_cacheArea": "Област с кеш", + "mapCache_useCurrentView": "Използвайте текущия изглед", + "mapCache_zoomRange": "Обхват на увеличението", + "mapCache_estimatedTiles": "Очаквани плочки: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Изтеглено {completed} / {total}", + "mapCache_downloadedTiles": "Изтеглено {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "Изтегли Плочки", - "mapCache_clearCacheButton": "Изчисти кеша", - "mapCache_failedDownloads": "Неуспешни изтегляния: {count}", + "mapCache_downloadTilesButton": "Изтегли Плочки", + "mapCache_clearCacheButton": "Изчисти кеша", + "mapCache_failedDownloads": "Неуспешни изтегляния: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -751,7 +751,7 @@ } } }, - "mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}", + "mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -768,8 +768,8 @@ } } }, - "time_justNow": "Сега", - "time_minutesAgo": "{minutes} минути преди", + "time_justNow": "Сега", + "time_minutesAgo": "{minutes} минути преди", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -777,7 +777,7 @@ } } }, - "time_hoursAgo": "{hours} часа преди", + "time_hoursAgo": "{hours} часа преди", "@time_hoursAgo": { "placeholders": { "hours": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} дни преди", + "time_daysAgo": "{days} дни преди", "@time_daysAgo": { "placeholders": { "days": { @@ -793,33 +793,33 @@ } } }, - "time_hour": "час", - "time_hours": "часове", - "time_day": "ден", - "time_days": "дни", - "time_week": "седмица", - "time_weeks": "секти", - "time_month": "месец", - "time_months": "месеци", - "time_minutes": "минути", - "time_allTime": "Всичко време", - "dialog_disconnect": "Прекъсни", - "dialog_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", - "login_repeaterLogin": "Повторител Вход", - "login_roomLogin": "Вход в стаята", - "login_password": "Парола", - "login_enterPassword": "Въведете парола", - "login_savePassword": "Запази парола", - "login_savePasswordSubtitle": "Паролата ще бъде съхранена сигурно на това устройство.", - "login_repeaterDescription": "Въведете паролата на репитера, за да получите достъп до настройките и статуса.", - "login_roomDescription": "Въведете паролата на стаята, за да получите достъп до настройките и статуса.", - "login_routing": "Маршрутизиране", - "login_routingMode": "Режим на маршрутизиране", - "login_autoUseSavedPath": "Автоматично (използвай запазения път)", - "login_forceFloodMode": "Принуди режим на наводняване", - "login_managePaths": "Управление на пътища", - "login_login": "Вход", - "login_attempt": "Опитвате {current}/{max}", + "time_hour": "час", + "time_hours": "часове", + "time_day": "ден", + "time_days": "дни", + "time_week": "седмица", + "time_weeks": "секти", + "time_month": "месец", + "time_months": "месеци", + "time_minutes": "минути", + "time_allTime": "Всичко време", + "dialog_disconnect": "Прекъсни", + "dialog_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", + "login_repeaterLogin": "Повторител Вход", + "login_roomLogin": "Вход в стаята", + "login_password": "Парола", + "login_enterPassword": "Въведете парола", + "login_savePassword": "Запази парола", + "login_savePasswordSubtitle": "Паролата ще бъде съхранена сигурно на това устройство.", + "login_repeaterDescription": "Въведете паролата на репитера, за да получите достъп до настройките и статуса.", + "login_roomDescription": "Въведете паролата на стаята, за да получите достъп до настройките и статуса.", + "login_routing": "Маршрутизиране", + "login_routingMode": "Режим на маршрутизиране", + "login_autoUseSavedPath": "Автоматично (използвай запазения път)", + "login_forceFloodMode": "Принуди режим на наводняване", + "login_managePaths": "Управление на пътища", + "login_login": "Вход", + "login_attempt": "Опитвате {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Входът не беше успешен: {error}", + "login_failed": "Входът не беше успешен: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.", - "common_reload": "Презареди", - "common_clear": "Изчисти", - "path_currentPath": "Текущ път: {path}", + "login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.", + "common_reload": "Презареди", + "common_clear": "Изчисти", + "path_currentPath": "Текущ път: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Използване на {count} {count, plural, =1{hop} other{hops}} път", + "path_usingHopsPath": "Използване на {count} {count, plural, =1{hop} other{hops}} път", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Въведете персонализиран път", - "path_currentPathLabel": "Текущ път", - "path_hexPrefixInstructions": "Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.", - "path_hexPrefixExample": "A1,F2,3C (всяка нода използва първия байт от публичния си ключ)", - "path_labelHexPrefixes": "Пътеки (шестнадесетични префикси)", - "path_helperMaxHops": "Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).", - "path_selectFromContacts": "Изберете от контакти:", - "path_noRepeatersFound": "Няма намерени репетитори или сървъри на стаи.", - "path_customPathsRequire": "Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.", - "path_invalidHexPrefixes": "Невалидни шестнадесетични префикси: {prefixes}", + "path_enterCustomPath": "Въведете персонализиран път", + "path_currentPathLabel": "Текущ път", + "path_hexPrefixInstructions": "Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.", + "path_hexPrefixExample": "A1,F2,3C (всяка нода използва първия байт от публичния си ключ)", + "path_labelHexPrefixes": "Пътеки (шестнадесетични префикси)", + "path_helperMaxHops": "Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).", + "path_selectFromContacts": "Изберете от контакти:", + "path_noRepeatersFound": "Няма намерени репетитори или сървъри на стаи.", + "path_customPathsRequire": "Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.", + "path_invalidHexPrefixes": "Невалидни шестнадесетични префикси: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Пътят е твърде дълъг. Максимум 64 скока са разрешени.", - "path_setPath": "Задайте път", - "repeater_management": "Управление на повторители", - "repeater_managementTools": "Инструменти за управление", - "repeater_status": "Статус", - "repeater_statusSubtitle": "Прегледайте статуса, статистиката и съседните устройства.", - "repeater_telemetry": "Телеметрия", - "repeater_telemetrySubtitle": "Прегледайте телеметрията на сензорите и системните статистики", + "path_tooLong": "Пътят е твърде дълъг. Максимум 64 скока са разрешени.", + "path_setPath": "Задайте път", + "repeater_management": "Управление на повторители", + "repeater_managementTools": "Инструменти за управление", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Прегледайте статуса, статистиката и съседните устройства.", + "repeater_telemetry": "Телеметрия", + "repeater_telemetrySubtitle": "Прегледайте телеметрията на сензорите и системните статистики", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Изпрати команди към ретранслатора", - "repeater_settings": "Настройки", - "repeater_settingsSubtitle": "Конфигурирайте параметрите на репитера", - "repeater_statusTitle": "Статус на повтарянето", - "repeater_routingMode": "Режим на маршрутизиране", - "repeater_autoUseSavedPath": "Автоматично (използвай запазения път)", - "repeater_forceFloodMode": "Принуди режим на наводняване", - "repeater_pathManagement": "Управление на пътища", - "repeater_refresh": "Презареди", - "repeater_statusRequestTimeout": "Заявката за статус премина прекалено дълго.", - "repeater_errorLoadingStatus": "Грешка при зареждане на статуса: {error}", + "repeater_cliSubtitle": "Изпрати команди към ретранслатора", + "repeater_settings": "Настройки", + "repeater_settingsSubtitle": "Конфигурирайте параметрите на репитера", + "repeater_statusTitle": "Статус на повтарянето", + "repeater_routingMode": "Режим на маршрутизиране", + "repeater_autoUseSavedPath": "Автоматично (използвай запазения път)", + "repeater_forceFloodMode": "Принуди режим на наводняване", + "repeater_pathManagement": "Управление на пътища", + "repeater_refresh": "Презареди", + "repeater_statusRequestTimeout": "Заявката за статус премина прекалено дълго.", + "repeater_errorLoadingStatus": "Грешка при зареждане на статуса: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -901,23 +901,23 @@ } } }, - "repeater_systemInformation": "Информация за системата", - "repeater_battery": "Батерия", - "repeater_clockAtLogin": "Часовник (при влизане)", - "repeater_uptime": "Наличност", - "repeater_queueLength": "Дължина на опашката", - "repeater_debugFlags": "Контролни точки за отстраняване на грешки", - "repeater_radioStatistics": "Статистика на радиостанциите", - "repeater_lastRssi": "Последна RSSI", - "repeater_lastSnr": "Последна SNR", - "repeater_noiseFloor": "Ниво на шум", + "repeater_systemInformation": "Информация за системата", + "repeater_battery": "Батерия", + "repeater_clockAtLogin": "Часовник (при влизане)", + "repeater_uptime": "Наличност", + "repeater_queueLength": "Дължина на опашката", + "repeater_debugFlags": "Контролни точки за отстраняване на грешки", + "repeater_radioStatistics": "Статистика на радиостанциите", + "repeater_lastRssi": "Последна RSSI", + "repeater_lastSnr": "Последна SNR", + "repeater_noiseFloor": "Ниво на шум", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Статистика на пакетите", - "repeater_sent": "Изпратено", - "repeater_received": "Получено", - "repeater_duplicates": "Дубликати", - "repeater_daysHoursMinsSecs": "{days} дни {hours}ч {minutes}м {seconds}с", + "repeater_packetStatistics": "Статистика на пакетите", + "repeater_sent": "Изпратено", + "repeater_received": "Получено", + "repeater_duplicates": "Дубликати", + "repeater_daysHoursMinsSecs": "{days} дни {hours}ч {minutes}м {seconds}с", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", + "repeater_packetTxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", + "repeater_packetRxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Поливане: {flood}, Директен: {direct}", + "repeater_duplicatesFloodDirect": "Поливане: {flood}, Директен: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -973,7 +973,7 @@ } } }, - "repeater_duplicatesTotal": "Общо: {total}", + "repeater_duplicatesTotal": "Общо: {total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -981,37 +981,37 @@ } } }, - "repeater_settingsTitle": "Настройки на повтарящия се елемент", - "repeater_basicSettings": "Основни настройки", - "repeater_repeaterName": "Име на повтарящ се елемент", - "repeater_repeaterNameHelper": "Показване на името на този репитер", - "repeater_adminPassword": "Парола на администратора", - "repeater_adminPasswordHelper": "Пълен достъпен парола", - "repeater_guestPassword": "Парола на гост", - "repeater_guestPasswordHelper": "Достъп с ограничен достъп", - "repeater_radioSettings": "Настройки на радиостанцията", - "repeater_frequencyMhz": "Честота (MHz)", + "repeater_settingsTitle": "Настройки на повтарящия се елемент", + "repeater_basicSettings": "Основни настройки", + "repeater_repeaterName": "Име на повтарящ се елемент", + "repeater_repeaterNameHelper": "Показване на името на този репитер", + "repeater_adminPassword": "Парола на администратора", + "repeater_adminPasswordHelper": "Пълен достъпен парола", + "repeater_guestPassword": "Парола на гост", + "repeater_guestPasswordHelper": "Достъп с ограничен достъп", + "repeater_radioSettings": "Настройки на радиостанцията", + "repeater_frequencyMhz": "Честота (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Ширина на честотния спектър", - "repeater_spreadingFactor": "Фактор на разпространение", - "repeater_codingRate": "Такса за кодиране", - "repeater_locationSettings": "Настройки на местоположението", - "repeater_latitude": "Широчина", - "repeater_latitudeHelper": "Десетични градуси (напр. 37.7749)", - "repeater_longitude": "Дължина", - "repeater_longitudeHelper": "Градуси с десетични знаци (напр. -122.4194)", - "repeater_features": "Характеристики", - "repeater_packetForwarding": "Пренасочване на пакети", - "repeater_packetForwardingSubtitle": "Активирайте репитера, за да препращате пакети.", - "repeater_guestAccess": "Достъп за Гост", - "repeater_guestAccessSubtitle": "Разрешете самочетене за гости", - "repeater_privacyMode": "Режим на поверителност", - "repeater_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", - "repeater_advertisementSettings": "Настройки на рекламите", - "repeater_localAdvertInterval": "Местен Рекламен Интервал", - "repeater_localAdvertIntervalMinutes": "{minutes} минути", + "repeater_bandwidth": "Ширина на честотния спектър", + "repeater_spreadingFactor": "Фактор на разпространение", + "repeater_codingRate": "Такса за кодиране", + "repeater_locationSettings": "Настройки на местоположението", + "repeater_latitude": "Широчина", + "repeater_latitudeHelper": "Десетични градуси (напр. 37.7749)", + "repeater_longitude": "Дължина", + "repeater_longitudeHelper": "Градуси с десетични знаци (напр. -122.4194)", + "repeater_features": "Характеристики", + "repeater_packetForwarding": "Пренасочване на пакети", + "repeater_packetForwardingSubtitle": "Активирайте репитера, за да препращате пакети.", + "repeater_guestAccess": "Достъп за Гост", + "repeater_guestAccessSubtitle": "Разрешете самочетене за гости", + "repeater_privacyMode": "Режим на поверителност", + "repeater_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", + "repeater_advertisementSettings": "Настройки на рекламите", + "repeater_localAdvertInterval": "Местен Рекламен Интервал", + "repeater_localAdvertIntervalMinutes": "{minutes} минути", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1019,8 +1019,8 @@ } } }, - "repeater_floodAdvertInterval": "Интервал на рекламата за наводнения", - "repeater_floodAdvertIntervalHours": "{hours} часа", + "repeater_floodAdvertInterval": "Интервал на рекламата за наводнения", + "repeater_floodAdvertIntervalHours": "{hours} часа", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Криптиран Рекламен Интервал", - "repeater_dangerZone": "Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно", - "repeater_rebootRepeater": "БеРестартирай Репитер", - "repeater_rebootRepeaterSubtitle": "Рестартирайте ретранслатора.", - "repeater_rebootRepeaterConfirm": "Сигурни ли сте, че искате да рестартирате този репитер?", - "repeater_regenerateIdentityKey": "Генериране на Ключ за Идентичност", - "repeater_regenerateIdentityKeySubtitle": "Генериране на нова двойка публичен/частен ключ", - "repeater_regenerateIdentityKeyConfirm": "БеТова ще генерира нова идентичност за репитера. Продължете?", - "repeater_eraseFileSystem": "Изтрий Файлова Система", - "repeater_eraseFileSystemSubtitle": "Форматирайте файла на репитера", - "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!", - "repeater_eraseSerialOnly": "Изтриването е достъпно само през серийния терминал.", - "repeater_commandSent": "Командата е изпратена: {command}", + "repeater_encryptedAdvertInterval": "Криптиран Рекламен Интервал", + "repeater_dangerZone": "Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно", + "repeater_rebootRepeater": "БеРестартирай Репитер", + "repeater_rebootRepeaterSubtitle": "Рестартирайте ретранслатора.", + "repeater_rebootRepeaterConfirm": "Сигурни ли сте, че искате да рестартирате този репитер?", + "repeater_regenerateIdentityKey": "Генериране на Ключ за Идентичност", + "repeater_regenerateIdentityKeySubtitle": "Генериране на нова двойка публичен/частен ключ", + "repeater_regenerateIdentityKeyConfirm": "БеТова ще генерира нова идентичност за репитера. Продължете?", + "repeater_eraseFileSystem": "Изтрий Файлова Система", + "repeater_eraseFileSystemSubtitle": "Форматирайте файла на репитера", + "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!", + "repeater_eraseSerialOnly": "Изтриването е достъпно само през серийния терминал.", + "repeater_commandSent": "Командата е изпратена: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Грешка при изпращане на командата: {error}", + "repeater_errorSendingCommand": "Грешка при изпращане на командата: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "БеПотвърди", - "repeater_settingsSaved": "Настройките са запазени успешно.", - "repeater_errorSavingSettings": "Грешка при запазване на настройките: {error}", + "repeater_confirm": "БеПотвърди", + "repeater_settingsSaved": "Настройките са запазени успешно.", + "repeater_errorSavingSettings": "Грешка при запазване на настройките: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "Обнови Основни Настройки", - "repeater_refreshRadioSettings": "Обнови настройките на радиопредавателите", - "repeater_refreshTxPower": "Обнови TX захранване", - "repeater_refreshLocationSettings": "Обнови настройките на местоположението", - "repeater_refreshPacketForwarding": "Обнови пакетно пренасочване", - "repeater_refreshGuestAccess": "Обнови достъп за гости", - "repeater_refreshPrivacyMode": "Обнови Режим на поверителност", - "repeater_refreshAdvertisementSettings": "Обнови Настройки на Рекламата", - "repeater_refreshed": "{label} е обновено", + "repeater_refreshBasicSettings": "Обнови Основни Настройки", + "repeater_refreshRadioSettings": "Обнови настройките на радиопредавателите", + "repeater_refreshTxPower": "Обнови TX захранване", + "repeater_refreshLocationSettings": "Обнови настройките на местоположението", + "repeater_refreshPacketForwarding": "Обнови пакетно пренасочване", + "repeater_refreshGuestAccess": "Обнови достъп за гости", + "repeater_refreshPrivacyMode": "Обнови Режим на поверителност", + "repeater_refreshAdvertisementSettings": "Обнови Настройки на Рекламата", + "repeater_refreshed": "{label} е обновено", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Грешка при обновяване на {label}", + "repeater_errorRefreshing": "Грешка при обновяване на {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1090,18 +1090,18 @@ } } }, - "repeater_cliTitle": "Повторител CLI", - "repeater_debugNextCommand": "Поправи Следваща Команда", - "repeater_commandHelp": "Помощ", - "repeater_clearHistory": "Изчисти История", - "repeater_noCommandsSent": "Няма изпратени команди засега.", - "repeater_typeCommandOrUseQuick": "Въведете команда по-долу или използвайте бързи команди", - "repeater_enterCommandHint": "Въведете команда...", - "repeater_previousCommand": "Предходна команда", - "repeater_nextCommand": "Следваща команда", - "repeater_enterCommandFirst": "Въведете първо команда.", - "repeater_cliCommandFrameTitle": "Рамка за команда CLI", - "repeater_cliCommandError": "Грешка: {error}", + "repeater_cliTitle": "Повторител CLI", + "repeater_debugNextCommand": "Поправи Следваща Команда", + "repeater_commandHelp": "Помощ", + "repeater_clearHistory": "Изчисти История", + "repeater_noCommandsSent": "Няма изпратени команди засега.", + "repeater_typeCommandOrUseQuick": "Въведете команда по-долу или използвайте бързи команди", + "repeater_enterCommandHint": "Въведете команда...", + "repeater_previousCommand": "Предходна команда", + "repeater_nextCommand": "Следваща команда", + "repeater_enterCommandFirst": "Въведете първо команда.", + "repeater_cliCommandFrameTitle": "Рамка за команда CLI", + "repeater_cliCommandError": "Грешка: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1109,81 +1109,81 @@ } } }, - "repeater_cliQuickGetName": "Получи име", - "repeater_cliQuickGetRadio": "Получи радио", - "repeater_cliQuickGetTx": "Получи TX", - "repeater_cliQuickNeighbors": "Съседи", - "repeater_cliQuickVersion": "Версия", - "repeater_cliQuickAdvertise": "Рекламирай", - "repeater_cliQuickClock": "Часовник", - "repeater_cliHelpAdvert": "Изпраща рекламен пакет", - "repeater_cliHelpReboot": "Рестартира устройството. (Забележка, може да получите 'Timeout', което е нормално)", - "repeater_cliHelpClock": "Показва текущото време според часовника на всяко устройство.", - "repeater_cliHelpPassword": "Задава се нова администраторска парола за устройството.", - "repeater_cliHelpVersion": "Показва версията на устройството и датата на компилация на фърмуера.", - "repeater_cliHelpClearStats": "Рестартира различни статистики броячи до нула.", - "repeater_cliHelpSetAf": "Задава времето на фактора.", - "repeater_cliHelpSetTx": "Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).", - "repeater_cliHelpSetRepeat": "Активира или деактивира ролята на репитера за този възел.", - "repeater_cliHelpSetAllowReadOnly": "(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).", - "repeater_cliHelpSetFloodMax": "Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).", - "repeater_cliHelpSetIntThresh": "Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.", - "repeater_cliHelpSetAgcResetInterval": "Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetMultiAcks": "Активира или деактивира функцията 'двойни ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetFloodAdvertInterval": "Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetGuestPassword": "Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")", - "repeater_cliHelpSetName": "Задава име на обявата.", - "repeater_cliHelpSetLat": "Задава географска ширина на картата с реклами (в десетими градуси).", - "repeater_cliHelpSetLon": "Задава обхвата на дължина на картата на рекламата. (десетими градуса)", - "repeater_cliHelpSetRadio": "Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.", - "repeater_cliHelpSetRxDelay": "Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetTxDelay": "Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).", - "repeater_cliHelpSetDirectTxDelay": "Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.", - "repeater_cliHelpSetBridgeEnabled": "Активиране/Деактивиране на мост.", - "repeater_cliHelpSetBridgeDelay": "Задайте забавяне преди преизпращане на пакети.", - "repeater_cliHelpSetBridgeSource": "Изберете дали мостът ще предава препратени пакети или получени пакети.", - "repeater_cliHelpSetBridgeBaud": "Задайте скоростта на предаване за RS232 мостовете.", - "repeater_cliHelpSetBridgeSecret": "Задайте тайна за мостовете на EspNow.", - "repeater_cliHelpSetAdcMultiplier": "Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).", - "repeater_cliHelpTempRadio": "Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).", - "repeater_cliHelpSetPerm": "Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).", - "repeater_cliHelpGetBridgeType": "Получава тип мост none, rs232, espnow", - "repeater_cliHelpLogStart": "Започва записване на пакети във файловата система.", - "repeater_cliHelpLogStop": "Спира записването на пакети във файловата система.", - "repeater_cliHelpLogErase": "Изтрива логовете от пакета от файловата система.", - "repeater_cliHelpNeighbors": "Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.", - "repeater_cliHelpRegion": "(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.", - "repeater_cliHelpRegionLoad": "Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.", - "repeater_cliHelpRegionGet": "Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) 'F'\"", - "repeater_cliHelpRegionPut": "Добавя или актуализира дефиниция на регион с дадено име.", - "repeater_cliHelpRegionRemove": "Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)", - "repeater_cliHelpRegionAllowf": "Задава 'Потоп' разрешение за посочената област. ('' за глобалния/стария обхват)", - "repeater_cliHelpRegionDenyf": "Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )", - "repeater_cliHelpRegionHome": "Отговаря с текущия 'home' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).", - "repeater_cliHelpRegionHomeSet": "Задава 'домашно' региона.", - "repeater_cliHelpRegionSave": "Запазва списъка/картата с региони в съхранение.", - "repeater_cliHelpGps": "Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.", - "repeater_cliHelpGpsOnOff": "Включва/Изключва GPS захранването.", - "repeater_cliHelpGpsSync": "Синхронизира времето на възела с GPS часовника.", - "repeater_cliHelpGpsSetLoc": "Задава координатите на нодата по GPS и запазва предпочитанията.", - "repeater_cliHelpGpsAdvert": "Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията", - "repeater_cliHelpGpsAdvertSet": "Задава конфигурация на обявите за местоположение.", - "repeater_commandsListTitle": "Списък с команди", - "repeater_commandsListNote": "ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".", - "repeater_general": "Общо", - "repeater_settingsCategory": "Настройки", - "repeater_bridge": "Мост", - "repeater_logging": "Логване", - "repeater_neighborsRepeaterOnly": "Съседи (Само за повтаряне)", - "repeater_regionManagementRepeaterOnly": "Управление на региони (Само за повтарящ се канал)", - "repeater_regionNote": "Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.", - "repeater_gpsManagement": "Управление на GPS", - "repeater_gpsNote": "GPS командата е въведена, за да управлява теми, свързани с местоположението.", - "telemetry_receivedData": "Получени телеметрични данни", - "telemetry_requestTimeout": "Заявката за телеметрия е прекъсната.", - "telemetry_errorLoading": "Грешка при зареждане на телеметрията: {error}", + "repeater_cliQuickGetName": "Получи име", + "repeater_cliQuickGetRadio": "Получи радио", + "repeater_cliQuickGetTx": "Получи TX", + "repeater_cliQuickNeighbors": "Съседи", + "repeater_cliQuickVersion": "Версия", + "repeater_cliQuickAdvertise": "Рекламирай", + "repeater_cliQuickClock": "Часовник", + "repeater_cliHelpAdvert": "Изпраща рекламен пакет", + "repeater_cliHelpReboot": "Рестартира устройството. (Забележка, може да получите 'Timeout', което е нормално)", + "repeater_cliHelpClock": "Показва текущото време според часовника на всяко устройство.", + "repeater_cliHelpPassword": "Задава се нова администраторска парола за устройството.", + "repeater_cliHelpVersion": "Показва версията на устройството и датата на компилация на фърмуера.", + "repeater_cliHelpClearStats": "Рестартира различни статистики броячи до нула.", + "repeater_cliHelpSetAf": "Задава времето на фактора.", + "repeater_cliHelpSetTx": "Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).", + "repeater_cliHelpSetRepeat": "Активира или деактивира ролята на репитера за този възел.", + "repeater_cliHelpSetAllowReadOnly": "(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).", + "repeater_cliHelpSetFloodMax": "Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).", + "repeater_cliHelpSetIntThresh": "Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.", + "repeater_cliHelpSetAgcResetInterval": "Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetMultiAcks": "Активира или деактивира функцията 'двойни ACKs'.", + "repeater_cliHelpSetAdvertInterval": "Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetFloodAdvertInterval": "Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetGuestPassword": "Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")", + "repeater_cliHelpSetName": "Задава име на обявата.", + "repeater_cliHelpSetLat": "Задава географска ширина на картата с реклами (в десетими градуси).", + "repeater_cliHelpSetLon": "Задава обхвата на дължина на картата на рекламата. (десетими градуса)", + "repeater_cliHelpSetRadio": "Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.", + "repeater_cliHelpSetRxDelay": "Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetTxDelay": "Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).", + "repeater_cliHelpSetDirectTxDelay": "Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.", + "repeater_cliHelpSetBridgeEnabled": "Активиране/Деактивиране на мост.", + "repeater_cliHelpSetBridgeDelay": "Задайте забавяне преди преизпращане на пакети.", + "repeater_cliHelpSetBridgeSource": "Изберете дали мостът ще предава препратени пакети или получени пакети.", + "repeater_cliHelpSetBridgeBaud": "Задайте скоростта на предаване за RS232 мостовете.", + "repeater_cliHelpSetBridgeSecret": "Задайте тайна за мостовете на EspNow.", + "repeater_cliHelpSetAdcMultiplier": "Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).", + "repeater_cliHelpTempRadio": "Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).", + "repeater_cliHelpSetPerm": "Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).", + "repeater_cliHelpGetBridgeType": "Получава тип мост none, rs232, espnow", + "repeater_cliHelpLogStart": "Започва записване на пакети във файловата система.", + "repeater_cliHelpLogStop": "Спира записването на пакети във файловата система.", + "repeater_cliHelpLogErase": "Изтрива логовете от пакета от файловата система.", + "repeater_cliHelpNeighbors": "Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.", + "repeater_cliHelpRegion": "(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.", + "repeater_cliHelpRegionLoad": "Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.", + "repeater_cliHelpRegionGet": "Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) 'F'\"", + "repeater_cliHelpRegionPut": "Добавя или актуализира дефиниция на регион с дадено име.", + "repeater_cliHelpRegionRemove": "Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)", + "repeater_cliHelpRegionAllowf": "Задава 'Потоп' разрешение за посочената област. ('' за глобалния/стария обхват)", + "repeater_cliHelpRegionDenyf": "Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )", + "repeater_cliHelpRegionHome": "Отговаря с текущия 'home' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).", + "repeater_cliHelpRegionHomeSet": "Задава 'домашно' региона.", + "repeater_cliHelpRegionSave": "Запазва списъка/картата с региони в съхранение.", + "repeater_cliHelpGps": "Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.", + "repeater_cliHelpGpsOnOff": "Включва/Изключва GPS захранването.", + "repeater_cliHelpGpsSync": "Синхронизира времето на възела с GPS часовника.", + "repeater_cliHelpGpsSetLoc": "Задава координатите на нодата по GPS и запазва предпочитанията.", + "repeater_cliHelpGpsAdvert": "Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията", + "repeater_cliHelpGpsAdvertSet": "Задава конфигурация на обявите за местоположение.", + "repeater_commandsListTitle": "Списък с команди", + "repeater_commandsListNote": "ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".", + "repeater_general": "Общо", + "repeater_settingsCategory": "Настройки", + "repeater_bridge": "Мост", + "repeater_logging": "Логване", + "repeater_neighborsRepeaterOnly": "Съседи (Само за повтаряне)", + "repeater_regionManagementRepeaterOnly": "Управление на региони (Само за повтарящ се канал)", + "repeater_regionNote": "Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.", + "repeater_gpsManagement": "Управление на GPS", + "repeater_gpsNote": "GPS командата е въведена, за да управлява теми, свързани с местоположението.", + "telemetry_receivedData": "Получени телеметрични данни", + "telemetry_requestTimeout": "Заявката за телеметрия е прекъсната.", + "telemetry_errorLoading": "Грешка при зареждане на телеметрията: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,8 +1191,8 @@ } } }, - "telemetry_noData": "Няма налични данни за телеметрията.", - "telemetry_channelTitle": "Канал {channel}", + "telemetry_noData": "Няма налични данни за телеметрията.", + "telemetry_channelTitle": "Канал {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1200,11 +1200,11 @@ } } }, - "telemetry_batteryLabel": "Батерия", - "telemetry_voltageLabel": "Напрежение", - "telemetry_mcuTemperatureLabel": "Температура на MCU", - "telemetry_temperatureLabel": "Температура", - "telemetry_currentLabel": "Текущо", + "telemetry_batteryLabel": "Батерия", + "telemetry_voltageLabel": "Напрежение", + "telemetry_mcuTemperatureLabel": "Температура на MCU", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Текущо", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Пътеки пъзел", - "channelPath_viewMap": "Преглед на картата", - "channelPath_otherObservedPaths": "Други Наблюдавани Пътища", - "channelPath_repeaterHops": "Повтарящи се скокове", - "channelPath_noHopDetails": "Детайлите за пакета не са предоставени.", - "channelPath_messageDetails": "Подробности на съобщението", - "channelPath_senderLabel": "Изпращач", - "channelPath_timeLabel": "Време", - "channelPath_repeatsLabel": "Повтаря", - "channelPath_pathLabel": "Път {index}", - "channelPath_observedLabel": "Наблюдавано", - "channelPath_observedPathTitle": "Наблюдаван път {index} • {hops}", + "channelPath_title": "Пътеки пъзел", + "channelPath_viewMap": "Преглед на картата", + "channelPath_otherObservedPaths": "Други Наблюдавани Пътища", + "channelPath_repeaterHops": "Повтарящи се скокове", + "channelPath_noHopDetails": "Детайлите за пакета не са предоставени.", + "channelPath_messageDetails": "Подробности на съобщението", + "channelPath_senderLabel": "Изпращач", + "channelPath_timeLabel": "Време", + "channelPath_repeatsLabel": "Повтаря", + "channelPath_pathLabel": "Път {index}", + "channelPath_observedLabel": "Наблюдавано", + "channelPath_observedPathTitle": "Наблюдаван път {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Няма данни за местоположение.", + "channelPath_noLocationData": "Няма данни за местоположение.", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1288,10 +1288,10 @@ } } }, - "channelPath_unknownPath": "Неизвестно", - "channelPath_floodPath": "Поливане", - "channelPath_directPath": "Директно", - "channelPath_observedZeroOf": "0 от {total} скокове", + "channelPath_unknownPath": "Неизвестно", + "channelPath_floodPath": "Поливане", + "channelPath_directPath": "Директно", + "channelPath_observedZeroOf": "0 от {total} скокове", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1299,7 +1299,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} от {total} скокове", + "channelPath_observedSomeOf": "{observed} от {total} скокове", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1310,9 +1310,9 @@ } } }, - "channelPath_mapTitle": "Карта на пътя", - "channelPath_noRepeaterLocations": "Няма налични местоположения на повторителите за този път.", - "channelPath_primaryPath": "Път {index} (Основен)", + "channelPath_mapTitle": "Карта на пътя", + "channelPath_noRepeaterLocations": "Няма налични местоположения на повторителите за този път.", + "channelPath_primaryPath": "Път {index} (Основен)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1327,9 +1327,9 @@ } } }, - "channelPath_pathLabelTitle": "Пътекино", - "channelPath_observedPathHeader": "Наблюдаван път", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Пътекино", + "channelPath_observedPathHeader": "Наблюдаван път", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,20 +1340,20 @@ } } }, - "channelPath_noHopDetailsAvailable": "Няма налични детайли за този пакет.", - "channelPath_unknownRepeater": "Неизвестен повторител", - "listFilter_tooltip": "Филтрирайте и сортирайте", - "listFilter_sortBy": "Сортирай по", - "listFilter_latestMessages": "Последни съобщения", - "listFilter_heardRecently": "Слушано е наскоро", + "channelPath_noHopDetailsAvailable": "Няма налични детайли за този пакет.", + "channelPath_unknownRepeater": "Неизвестен повторител", + "listFilter_tooltip": "Филтрирайте и сортирайте", + "listFilter_sortBy": "Сортирай по", + "listFilter_latestMessages": "Последни съобщения", + "listFilter_heardRecently": "Слушано е наскоро", "listFilter_az": "A-Z", - "listFilter_filters": "Филтри", - "listFilter_all": "Всички", - "listFilter_users": "Потребители", - "listFilter_repeaters": "Повторители", - "listFilter_roomServers": "Сървъри на стая", - "listFilter_unreadOnly": "Само непрочетените", - "listFilter_newGroup": "Нова група", + "listFilter_filters": "Филтри", + "listFilter_all": "Всички", + "listFilter_users": "Потребители", + "listFilter_repeaters": "Повторители", + "listFilter_roomServers": "Сървъри на стая", + "listFilter_unreadOnly": "Само непрочетените", + "listFilter_newGroup": "Нова група", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1361,25 +1361,25 @@ } } }, - "repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.", - "repeater_neighbors": "Съседи", - "neighbors_receivedData": "Получени данни за съседи", - "neighbors_requestTimedOut": "Съседите поискат изтичане на време.", - "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", - "neighbors_repeatersNeighbors": "Повторители Съседи", - "neighbors_noData": "Няма налични данни за съседи.", - "channels_createPrivateChannel": "Създай Частен Канал", - "channels_joinPrivateChannel": "Присъедини се към Частен Канал", - "channels_createPrivateChannelDesc": "Защитено с таен ключ.", - "channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.", - "channels_joinPublicChannel": "Присъединете се към Публичния канал", - "channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.", - "channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал", - "channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.", - "channels_scanQrCode": "Сканирайте QR код", - "channels_scanQrCodeComingSoon": "Ще излезе скоро", - "channels_enterHashtag": "Въведете хаштаг", - "channels_hashtagHint": "напр. #отбор", + "repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.", + "repeater_neighbors": "Съседи", + "neighbors_receivedData": "Получени данни за съседи", + "neighbors_requestTimedOut": "Съседите поискат изтичане на време.", + "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", + "neighbors_repeatersNeighbors": "Повторители Съседи", + "neighbors_noData": "Няма налични данни за съседи.", + "channels_createPrivateChannel": "Създай Частен Канал", + "channels_joinPrivateChannel": "Присъедини се към Частен Канал", + "channels_createPrivateChannelDesc": "Защитено с таен ключ.", + "channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.", + "channels_joinPublicChannel": "Присъединете се към Публичния канал", + "channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.", + "channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал", + "channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.", + "channels_scanQrCode": "Сканирайте QR код", + "channels_scanQrCodeComingSoon": "Ще излезе скоро", + "channels_enterHashtag": "Въведете хаштаг", + "channels_hashtagHint": "напр. #отбор", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_heardAgo": "Слушано преди {time}.", - "neighbors_unknownContact": "Неизвестна {pubkey}", - "settings_locationIntervalSec": "Интервал за GPS (Секунди)", - "settings_locationGPSEnable": "Активиране на GPS", - "settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.", - "settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.", - "room_management": "Управление на сървъра за стая", - "contacts_manageRoom": "Управление на сървър за стая", + "neighbors_heardAgo": "Слушано преди {time}.", + "neighbors_unknownContact": "Неизвестна {pubkey}", + "settings_locationIntervalSec": "Интервал за GPS (Секунди)", + "settings_locationGPSEnable": "Активиране на GPS", + "settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.", + "settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.", + "room_management": "Управление на сървъра за стая", + "contacts_manageRoom": "Управление на сървър за стая", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1458,36 +1458,36 @@ } } }, - "community_title": "Общност", - "common_ok": "Добре", - "community_createDesc": "Създайте нова общност и я споделете чрез QR код.", - "community_create": "Създай общност", - "community_joinTitle": "Присъедини се към общността", - "community_joinConfirmation": "Искате ли да се присъедините към общността \"{name}\"?", - "community_scanQr": "Сканирайте QR кода на общността", - "community_scanInstructions": "Насочете камерата към QR код на общността", - "community_showQr": "Покажи QR код", - "community_publicChannel": "Обществено общност", - "community_hashtagChannel": "Хаштаг на общността", - "community_name": "Име на общността", - "community_enterName": "Въведете име на общността", - "community_created": "Общността \"{name}\" е създадена", - "community_joined": "Присъединено общност \"{name}\"", - "community_qrTitle": "Споделяне в общността", - "community_join": "Присъедини се", - "community_qrInstructions": "Сканирайте този QR код, за да се присъедините към {name}.", - "community_hashtagPrivacyHint": "Хаштаг каналите на общността са достъпни само за членове на общността", - "community_invalidQrCode": "Невалиден QR код на общността", - "community_alreadyMember": "Вече съм член", - "community_alreadyMemberMessage": "Вие вече сте член на \"{name}\".", - "community_addPublicChannel": "Добави публичен общностен канал", - "community_addPublicChannelHint": "Автоматично добавете публичния канал за тази общност.", - "community_noCommunities": "Няма присъединени общности още.", - "community_scanOrCreate": "Сканирайте QR код или създайте общност, за да започнете.", - "community_manageCommunities": "Управление на общности", - "community_delete": "Напусни общността", - "community_deleteConfirm": "Напускате \"{name}\"?", - "community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.", + "community_title": "Общност", + "common_ok": "Добре", + "community_createDesc": "Създайте нова общност и я споделете чрез QR код.", + "community_create": "Създай общност", + "community_joinTitle": "Присъедини се към общността", + "community_joinConfirmation": "Искате ли да се присъедините към общността \"{name}\"?", + "community_scanQr": "Сканирайте QR кода на общността", + "community_scanInstructions": "Насочете камерата към QR код на общността", + "community_showQr": "Покажи QR код", + "community_publicChannel": "Обществено общност", + "community_hashtagChannel": "Хаштаг на общността", + "community_name": "Име на общността", + "community_enterName": "Въведете име на общността", + "community_created": "Общността \"{name}\" е създадена", + "community_joined": "Присъединено общност \"{name}\"", + "community_qrTitle": "Споделяне в общността", + "community_join": "Присъедини се", + "community_qrInstructions": "Сканирайте този QR код, за да се присъедините към {name}.", + "community_hashtagPrivacyHint": "Хаштаг каналите на общността са достъпни само за членове на общността", + "community_invalidQrCode": "Невалиден QR код на общността", + "community_alreadyMember": "Вече съм член", + "community_alreadyMemberMessage": "Вие вече сте член на \"{name}\".", + "community_addPublicChannel": "Добави публичен общностен канал", + "community_addPublicChannelHint": "Автоматично добавете публичния канал за тази общност.", + "community_noCommunities": "Няма присъединени общности още.", + "community_scanOrCreate": "Сканирайте QR код или създайте общност, за да започнете.", + "community_manageCommunities": "Управление на общности", + "community_delete": "Напусни общността", + "community_deleteConfirm": "Напускате \"{name}\"?", + "community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,15 +1495,15 @@ } } }, - "community_deleted": "Остави общността \"{name}\"", - "community_addHashtagChannel": "Добави общностен хаштаг", - "community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност", - "community_selectCommunity": "Изберете общност", - "community_regularHashtag": "Обикновен хаштаг", - "community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)", - "community_communityHashtag": "Общностен хаштаг", - "community_communityHashtagDesc": "Само за членове на общността", - "community_forCommunity": "За {name}", + "community_deleted": "Остави общността \"{name}\"", + "community_addHashtagChannel": "Добави общностен хаштаг", + "community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност", + "community_selectCommunity": "Изберете общност", + "community_regularHashtag": "Обикновен хаштаг", + "community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)", + "community_communityHashtag": "Общностен хаштаг", + "community_communityHashtagDesc": "Само за членове на общността", + "community_forCommunity": "За {name}", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1532,13 +1532,13 @@ } } }, - "community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.", - "community_secretRegenerated": "Секретно презареждане за \"{name}\"", - "community_regenerateSecret": "Регенерейрай секрет", - "community_regenerate": "Регенерация", - "community_updateSecret": "Актуализирай тайна", - "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"", - "community_secretUpdated": "Секретно обновено за \"{name}\"", + "community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.", + "community_secretRegenerated": "Секретно презареждане за \"{name}\"", + "community_regenerateSecret": "Регенерейрай секрет", + "community_regenerate": "Регенерация", + "community_updateSecret": "Актуализирай тайна", + "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"", + "community_secretUpdated": "Секретно обновено за \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1546,82 +1546,82 @@ } } }, - "pathTrace_you": "Вие", - "pathTrace_notAvailable": "Пътека за проследяване не е достъпна.", - "contacts_pathTrace": "Пътен проследяване", - "pathTrace_refreshTooltip": "Обнови Path Trace.", - "pathTrace_failed": "Пътят за проследяване не успя.", - "contacts_repeaterPing": "Пингване на повторителя", - "contacts_repeaterPathTrace": "Трасировка до повторител", - "contacts_ping": "Пинг", - "contacts_chatTraceRoute": "Трасиране на път", - "contacts_roomPathTrace": "Трасиране на път до съ", - "contacts_roomPing": "Ping на сървъра на стаята", - "contacts_pathTraceTo": "Проследи маршрут към {name}", - "appSettings_languageUk": "Украински", - "contacts_clipboardEmpty": "Клипборда е празна.", - "contacts_invalidAdvertFormat": "Невалидни данни за контакт", - "appSettings_languageRu": "Руски", - "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", - "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", - "contacts_contactImported": "Контактът е импортиран.", - "contacts_zeroHopAdvert": "Реклама без скок", - "contacts_contactImportFailed": "Контактът не е успешно импортиран.", - "contacts_floodAdvert": "Потопна реклама", - "contacts_addContactFromClipboard": "Добави контакт от клипборда", - "contacts_copyAdvertToClipboard": "Копирай обявата в клипборда", - "contacts_ShareContact": "Копирай контакт в клипборда", - "contacts_ShareContactZeroHop": "Сподели контакт чрез обява", - "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", - "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", - "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", - "notification_activityTitle": "Активност на MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}", - "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", - "notification_newTypeDiscovered": "Открит нов {contactType}", - "notification_receivedNewMessage": "Получено ново съобщение", - "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", - "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", - "settings_gpxExportAll": "Експортирай всички контакти в GPX", - "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", - "settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX", - "settings_gpxExportContacts": "Експортирай спътници към GPX", - "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", - "settings_gpxExportNoContacts": "Няма контакти за изlexport.", - "settings_gpxExportChat": "Местоположения на спътници", - "settings_gpxExportError": "Възникна грешка при изнасяне.", - "settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята", - "settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС", - "settings_gpxExportAllContacts": "Местоположения на всички контакти", - "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", - "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!", - "map_pathTraceCancelled": "Отменен е следването на пътя.", - "pathTrace_clearTooltip": "Изчисти пътя", - "map_removeLast": "Премахни Последно", - "map_runTrace": "Изпълни Път на Следване", - "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", - "scanner_bluetoothOff": "Bluetooth е изключен.", - "scanner_enableBluetooth": "Активирайте Bluetooth", - "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", - "scanner_chromeRequired": "Изисква се браузър Chrome", - "scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.", - "snrIndicator_lastSeen": "Последно видян", - "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", - "chat_ShowAllPaths": "Покажи всички пътища", - "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", - "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", - "settings_clientRepeat": "Без електричество – повторение", - "settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "единици", - "appSettings_unitsMetric": "Метрика (m / km)", - "appSettings_unitsImperial": "Имперска (ft / mi)", - "map_lineOfSight": "Линия на видимост", - "map_losScreenTitle": "Линия на видимост", - "losSelectStartEnd": "Изберете начални и крайни възли за LOS.", - "losRunFailed": "Проверката на пряката видимост е неуспешна: {error}", + "pathTrace_you": "Вие", + "pathTrace_notAvailable": "Пътека за проследяване не е достъпна.", + "contacts_pathTrace": "Пътен проследяване", + "pathTrace_refreshTooltip": "Обнови Path Trace.", + "pathTrace_failed": "Пътят за проследяване не успя.", + "contacts_repeaterPing": "Пингване на повторителя", + "contacts_repeaterPathTrace": "Трасировка до повторител", + "contacts_ping": "Пинг", + "contacts_chatTraceRoute": "Трасиране на път", + "contacts_roomPathTrace": "Трасиране на път до съ", + "contacts_roomPing": "Ping на сървъра на стаята", + "contacts_pathTraceTo": "Проследи маршрут към {name}", + "appSettings_languageUk": "Украински", + "contacts_clipboardEmpty": "Клипборда е празна.", + "contacts_invalidAdvertFormat": "Невалидни данни за контакт", + "appSettings_languageRu": "Руски", + "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", + "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", + "contacts_contactImported": "Контактът е импортиран.", + "contacts_zeroHopAdvert": "Реклама без скок", + "contacts_contactImportFailed": "Контактът не е успешно импортиран.", + "contacts_floodAdvert": "Потопна реклама", + "contacts_addContactFromClipboard": "Добави контакт от клипборда", + "contacts_copyAdvertToClipboard": "Копирай обявата в клипборда", + "contacts_ShareContact": "Копирай контакт в клипборда", + "contacts_ShareContactZeroHop": "Сподели контакт чрез обява", + "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", + "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", + "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "notification_activityTitle": "Активност на MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}", + "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", + "notification_newTypeDiscovered": "Открит нов {contactType}", + "notification_receivedNewMessage": "Получено ново съобщение", + "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", + "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", + "settings_gpxExportAll": "Експортирай всички контакти в GPX", + "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", + "settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX", + "settings_gpxExportContacts": "Експортирай спътници към GPX", + "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", + "settings_gpxExportNoContacts": "Няма контакти за изlexport.", + "settings_gpxExportChat": "Местоположения на спътници", + "settings_gpxExportError": "Възникна грешка при изнасяне.", + "settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята", + "settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС", + "settings_gpxExportAllContacts": "Местоположения на всички контакти", + "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", + "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!", + "map_pathTraceCancelled": "Отменен е следването на пътя.", + "pathTrace_clearTooltip": "Изчисти пътя", + "map_removeLast": "Премахни Последно", + "map_runTrace": "Изпълни Път на Следване", + "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", + "scanner_bluetoothOff": "Bluetooth е изключен.", + "scanner_enableBluetooth": "Активирайте Bluetooth", + "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "scanner_chromeRequired": "Изисква се браузър Chrome", + "scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.", + "snrIndicator_lastSeen": "Последно видян", + "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", + "chat_ShowAllPaths": "Покажи всички пътища", + "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", + "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", + "settings_clientRepeat": "Без електричество – повторение", + "settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "единици", + "appSettings_unitsMetric": "Метрика (m / km)", + "appSettings_unitsImperial": "Имперска (ft / mi)", + "map_lineOfSight": "Линия на видимост", + "map_losScreenTitle": "Линия на видимост", + "losSelectStartEnd": "Изберете начални и крайни възли за LOS.", + "losRunFailed": "Проверката на пряката видимост е неуспешна: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,13 +1629,13 @@ } } }, - "losClearAllPoints": "Изчистете всички точки", - "losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина", - "losMenuTitle": "LOS меню", - "losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки", - "losShowDisplayNodes": "Показване на възли на дисплея", - "losCustomPoints": "Персонализирани точки", - "losCustomPointLabel": "Персонализирано {index}", + "losClearAllPoints": "Изчистете всички точки", + "losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина", + "losMenuTitle": "LOS меню", + "losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки", + "losShowDisplayNodes": "Показване на възли на дисплея", + "losCustomPoints": "Персонализирани точки", + "losCustomPointLabel": "Персонализирано {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1643,9 +1643,9 @@ } } }, - "losPointA": "Точка А", - "losPointB": "Точка Б", - "losAntennaA": "Антена A: {value} {unit}", + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Антена B: {value} {unit}", + "losAntennaB": "Антена B: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1667,9 +1667,9 @@ } } }, - "losRun": "Стартирайте LOS", - "losNoElevationData": "Няма данни за надморска височина", - "losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}", + "losRun": "Стартирайте LOS", + "losNoElevationData": "Няма данни за надморска височина", + "losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1703,9 +1703,9 @@ } } }, - "losStatusChecking": "LOS: проверка...", - "losStatusNoData": "LOS: няма данни", - "losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно", + "losStatusChecking": "LOS: проверка...", + "losStatusNoData": "LOS: няма данни", + "losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.", - "losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.", - "losRenameCustomPoint": "Преименувайте персонализирана точка", - "losPointName": "Име на точката", - "losShowPanelTooltip": "Показване на LOS панел", - "losHidePanelTooltip": "Скриване на LOS панела", - "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Радиохоризонт", - "losLegendLosBeam": "Линия на видимост", - "losLegendTerrain": "Терен", - "losFrequencyLabel": "Честота", - "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", - "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", - "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", + "losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.", + "losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.", + "losRenameCustomPoint": "Преименувайте персонализирана точка", + "losPointName": "Име на точката", + "losShowPanelTooltip": "Показване на LOS панел", + "losHidePanelTooltip": "Скриване на LOS панела", + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиохоризонт", + "losLegendLosBeam": "Линия на видимост", + "losLegendTerrain": "Терен", + "losFrequencyLabel": "Честота", + "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", + "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_removeFromFavorites": "Премахване от списъка с любими", - "listFilter_addToFavorites": "Добави към любими", - "listFilter_favorites": "Любими", + "listFilter_removeFromFavorites": "Премахване от списъка с любими", + "listFilter_addToFavorites": "Добави към любими", + "listFilter_favorites": "Любими", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1796,19 +1796,17 @@ } } }, - "contacts_searchFavorites": "Търсене на {number}{str} любими...", - "contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...", - "contacts_unread": "Непрочетено", - "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", - "contacts_searchContactsNoNumber": "Търси контакти...", - "contacts_searchUsers": "Търсене на {number}{str} потребители...", + "contacts_searchFavorites": "Търсене на {number}{str} любими...", + "contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...", + "contacts_unread": "Непрочетено", + "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", + "contacts_searchContactsNoNumber": "Търси контакти...", + "contacts_searchUsers": "Търсене на {number}{str} потребители...", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceTitle": "Изберете метода на връзка.", - "connectionChoiceSubtitle": "Изберете как искате да получите вашия устройство MeshCore.", - "usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.", - "usbScreenStatus": "Изберете USB устройство", - "usbScreenTitle": "Свързване чрез USB", - "usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.", - "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново." + "usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.", + "usbScreenStatus": "Изберете USB устройство", + "usbScreenTitle": "Свързване чрез USB", + "usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.", + "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 44eca06..0b4bfff 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", +{ + "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -10,16 +10,16 @@ "@@locale": "de", "appTitle": "MeshCore Open", "nav_contacts": "Kontakte", - "nav_channels": "Kanäle", + "nav_channels": "Kanäle", "nav_map": "Karte", "common_cancel": "Abbrechen", "common_connect": "Verbinden", - "common_unknownDevice": "Unbekanntes Gerät", + "common_unknownDevice": "Unbekanntes Gerät", "common_save": "Speichern", - "common_delete": "Löschen", - "common_close": "Schließen", + "common_delete": "Löschen", + "common_close": "Schließen", "common_edit": "Bearbeiten", - "common_add": "Hinzufügen", + "common_add": "Hinzufügen", "common_settings": "Einstellungen", "common_disconnect": "Trennen", "common_connected": "Verbunden", @@ -30,12 +30,12 @@ "common_copy": "Kopieren", "common_retry": "Versuchen", "common_hide": "Ausblenden", - "common_remove": "Löschen", + "common_remove": "Löschen", "common_enable": "Aktivieren", "common_disable": "Deaktivieren", "common_reboot": "Neustart", "common_loading": "Laden...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,7 +53,7 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Scannen nach Geräten...", + "scanner_scanning": "Scannen nach Geräten...", "scanner_connecting": "Verbunden...", "scanner_disconnecting": "Trenne...", "scanner_notConnected": "Nicht verbunden", @@ -65,8 +65,8 @@ } } }, - "scanner_searchingDevices": "Suche nach MeshCore-Geräten...", - "scanner_tapToScan": "Tippen Sie auf Scan, um MeshCore-Geräte zu finden.", + "scanner_searchingDevices": "Suche nach MeshCore-Geräten...", + "scanner_tapToScan": "Tippen Sie auf Scan, um MeshCore-Geräte zu finden.", "scanner_connectionFailed": "Verbindungsfehler: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -80,7 +80,7 @@ "device_quickSwitch": "Schnelles Umschalten", "device_meshcore": "MeshCore", "settings_title": "Einstellungen", - "settings_deviceInfo": "Geräteinformationen", + "settings_deviceInfo": "Geräteinformationen", "settings_appSettings": "App-Einstellungen", "settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung", "settings_nodeSettings": "Knoten-Einstellungen", @@ -94,33 +94,33 @@ "settings_location": "Ort", "settings_locationSubtitle": "GPS-Koordinaten", "settings_locationUpdated": "Ort aktualisiert", - "settings_locationBothRequired": "Bitte geben Sie sowohl Breite als auch Längengrad ein.", - "settings_locationInvalid": "Ungültige Breiten- oder Längengrade.", + "settings_locationBothRequired": "Bitte geben Sie sowohl Breite als auch Längengrad ein.", + "settings_locationInvalid": "Ungültige Breiten- oder Längengrade.", "settings_latitude": "Breitengrad", - "settings_longitude": "Längengrad", - "settings_privacyMode": "Privatsphäreeinstellung", - "settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", - "settings_privacyModeToggle": "Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.", + "settings_longitude": "Längengrad", + "settings_privacyMode": "Privatsphäreeinstellung", + "settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", + "settings_privacyModeToggle": "Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.", "settings_privacyModeEnabled": "Datenschutzmodus aktiviert", "settings_privacyModeDisabled": "Datenschutzmodus deaktiviert", "settings_actions": "Aktionen", - "settings_sendAdvertisement": "Sende Ankündigung", - "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", - "settings_advertisementSent": "Ankündigung gesendet", + "settings_sendAdvertisement": "Sende Ankündigung", + "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", + "settings_advertisementSent": "Ankündigung gesendet", "settings_syncTime": "Zeitsynchronisierung", - "settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein", + "settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein", "settings_timeSynchronized": "Zeit synchronisiert", "settings_refreshContacts": "Kontakte aktualisieren", - "settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden", - "settings_rebootDevice": "Gerät neu starten", - "settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten", - "settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.", + "settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden", + "settings_rebootDevice": "Gerät neu starten", + "settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten", + "settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.", "settings_debug": "Fehlerbehebung", "settings_bleDebugLog": "BLE-Debug-Protokoll", "settings_bleDebugLogSubtitle": "BLE-Befehle, Antworten und Rohdaten", "settings_appDebugLog": "App-Debug-Protokoll", "settings_appDebugLogSubtitle": "Anwendung Debug-Nachrichten", - "settings_about": "Über", + "settings_about": "Über", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "MeshCore Open Source Projekt 2026", - "settings_aboutDescription": "Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.", + "settings_aboutDescription": "Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.", "settings_infoName": "Name", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Akku", - "settings_infoPublicKey": "Öffentlicher Schlüssel", + "settings_infoPublicKey": "Öffentlicher Schlüssel", "settings_infoContactsCount": "Anzahl Kontakte", - "settings_infoChannelCount": "Anzahl Kanäle", + "settings_infoChannelCount": "Anzahl Kanäle", "settings_presets": "Voreinstellungen", "settings_frequency": "Frequenz (MHz)", "settings_frequencyHelper": "300,00 - 2.500,00", - "settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)", + "settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)", "settings_bandwidth": "Bandbreite", "settings_spreadingFactor": "Verteilungsfaktor", "settings_codingRate": "Kodierungsrate", "settings_txPower": "TX-Leistung (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)", + "settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)", "settings_error": "Fehler: {message}", "@settings_error": { "placeholders": { @@ -165,21 +165,21 @@ "appSettings_language": "Sprache", "appSettings_languageSystem": "Systemstandard", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Benachrichtigungen", "appSettings_enableNotifications": "Benachrichtigungen aktivieren", - "appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Ankündigungen", + "appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Ankündigungen", "appSettings_notificationPermissionDenied": "Erlaubnis zur Benachrichtigung verweigert", "appSettings_notificationsEnabled": "Benachrichtigungen aktiviert", "appSettings_notificationsDisabled": "Benachrichtigungen deaktiviert", @@ -187,20 +187,20 @@ "appSettings_messageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfang neuer Direktnachrichten", "appSettings_channelMessageNotifications": "Kanalnachrichten Benachrichtigungen", "appSettings_channelMessageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfangen von Kanalnachrichten", - "appSettings_advertisementNotifications": "Ankündigungsbenachrichtigungen", + "appSettings_advertisementNotifications": "Ankündigungsbenachrichtigungen", "appSettings_advertisementNotificationsSubtitle": "Zeige Benachrichtigung, wenn neue Knoten entdeckt werden.", "appSettings_messaging": "Nachrichten", - "appSettings_clearPathOnMaxRetry": "Lösche Pfade bei Max Wiederholungsversuchen", - "appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen", - "appSettings_pathsWillBeCleared": "Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.", - "appSettings_pathsWillNotBeCleared": "Die Pfade werden nicht automatisch gelöscht.", + "appSettings_clearPathOnMaxRetry": "Lösche Pfade bei Max Wiederholungsversuchen", + "appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen", + "appSettings_pathsWillBeCleared": "Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.", + "appSettings_pathsWillNotBeCleared": "Die Pfade werden nicht automatisch gelöscht.", "appSettings_autoRouteRotation": "Automatische Routenrotation", "appSettings_autoRouteRotationSubtitle": "Wechseln zwischen den besten Pfaden und dem Fluten", "appSettings_autoRouteRotationEnabled": "Automatische Routenrotation aktiviert", "appSettings_autoRouteRotationDisabled": "Automatische Routenrotation deaktiviert", "appSettings_battery": "Akku", "appSettings_batteryChemistry": "Batteriechemie", - "appSettings_batteryChemistryPerDevice": "Konfiguriert pro Gerät ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Konfiguriert pro Gerät ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,10 +208,10 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Verbinde ein Gerät, um zu wählen", - "appSettings_batteryNmc": "18650 NMC (3,0–4,2 V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", - "appSettings_batteryLipo": "LiPo (3,0–4,2V)", + "appSettings_batteryChemistryConnectFirst": "Verbinde ein Gerät, um zu wählen", + "appSettings_batteryNmc": "18650 NMC (3,0–4,2 V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", + "appSettings_batteryLipo": "LiPo (3,0–4,2V)", "appSettings_mapDisplay": "Kartendarstellung", "appSettings_showRepeaters": "Zeige Repeater", "appSettings_showRepeatersSubtitle": "Zeige Repeater-Knoten auf der Karte an", @@ -237,8 +237,8 @@ "appSettings_last24Hours": "Letzte 24 Stunden", "appSettings_lastWeek": "Letzte Woche", "appSettings_offlineMapCache": "Offline-Karten-Cache", - "appSettings_noAreaSelected": "Kein Bereich ausgewählt", - "appSettings_areaSelectedZoom": "Ausgewählte Fläche (Zoom {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Kein Bereich ausgewählt", + "appSettings_areaSelectedZoom": "Ausgewählte Fläche (Zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -256,11 +256,11 @@ "appSettings_appDebugLoggingDisabled": "App-Debug-Protokollierung deaktiviert", "contacts_title": "Kontakte", "contacts_noContacts": "Noch keine Kontakte vorhanden.", - "contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.", + "contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.", "contacts_searchContacts": "Suche Kontakte...", "contacts_noUnreadContacts": "Keine ungesehene Kontakte", "contacts_noContactsFound": "Keine Kontakte oder Gruppen gefunden.", - "contacts_deleteContact": "Lösche den Kontakt", + "contacts_deleteContact": "Lösche den Kontakt", "contacts_removeConfirm": "{contactName} aus den Kontakten entfernen?", "@contacts_removeConfirm": { "placeholders": { @@ -271,10 +271,10 @@ }, "contacts_manageRepeater": "Repeater verwalten", "contacts_roomLogin": "Raum-Login", - "contacts_openChat": "Öffne Chat", + "contacts_openChat": "Öffne Chat", "contacts_editGroup": "Gruppe bearbeiten", - "contacts_deleteGroup": "Löschen Gruppe", - "contacts_deleteGroupConfirm": "Löschen von \"{groupName}\"?", + "contacts_deleteGroup": "Löschen Gruppe", + "contacts_deleteGroupConfirm": "Löschen von \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -323,11 +323,11 @@ } } }, - "channels_title": "Kanäle", - "channels_noChannelsConfigured": "Keine Kanäle konfiguriert", - "channels_addPublicChannel": "Öffentlichen Kanal hinzufügen", - "channels_searchChannels": "Suche Kanäle...", - "channels_noChannelsFound": "Keine Kanäle gefunden", + "channels_title": "Kanäle", + "channels_noChannelsConfigured": "Keine Kanäle konfiguriert", + "channels_addPublicChannel": "Öffentlichen Kanal hinzufügen", + "channels_searchChannels": "Suche Kanäle...", + "channels_noChannelsFound": "Keine Kanäle gefunden", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { "placeholders": { @@ -337,15 +337,15 @@ } }, "channels_hashtagChannel": "Hashtag-Kanal", - "channels_public": "Öffentlich", + "channels_public": "Öffentlich", "channels_private": "Privat", - "channels_publicChannel": "Öffentlicher Kanal", + "channels_publicChannel": "Öffentlicher Kanal", "channels_privateChannel": "Privater Kanal", "channels_editChannel": "Kanal bearbeiten", "channels_muteChannel": "Kanal stummschalten", "channels_unmuteChannel": "Kanal Stummschaltung aufheben", - "channels_deleteChannel": "Lösche den Kanal", - "channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.", + "channels_deleteChannel": "Lösche den Kanal", + "channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Kanal \"{name}\" gelöscht", + "channels_channelDeleted": "Kanal \"{name}\" gelöscht", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Kanal hinzufügen", + "channels_addChannel": "Kanal hinzufügen", "channels_channelIndexLabel": "Kanalindex", "channels_channelName": "Kanalname", - "channels_usePublicChannel": "Verwende öffentlichen Kanal", - "channels_standardPublicPsk": "Öffentliche Standard PSK", + "channels_usePublicChannel": "Verwende öffentlichen Kanal", + "channels_standardPublicPsk": "Öffentliche Standard PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Zufällige PSK generieren", + "channels_generateRandomPsk": "Zufällige PSK generieren", "channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.", "channels_pskMustBe32Hex": "Die PSK muss 32 hexadezimale Zeichen haben.", - "channels_channelAdded": "Kanal \"{name}\" hinzugefügt", + "channels_channelAdded": "Kanal \"{name}\" hinzugefügt", "@channels_channelAdded": { "placeholders": { "name": { @@ -395,7 +395,7 @@ } } }, - "channels_publicChannelAdded": "Öffentlicher Kanal hinzugefügt", + "channels_publicChannelAdded": "Öffentlicher Kanal hinzugefügt", "channels_sortBy": "Sortiere nach", "channels_sortManual": "Manuell", "channels_sortAZ": "A bis Z", @@ -439,7 +439,7 @@ } }, "chat_messageCopied": "Nachricht kopiert", - "chat_messageDeleted": "Nachricht gelöscht", + "chat_messageDeleted": "Nachricht gelöscht", "chat_retryingMessage": "Versuche es erneut.", "chat_retryCount": "Versuche {current}/{max}", "@chat_retryCount": { @@ -454,13 +454,13 @@ }, "chat_sendGif": "GIF senden", "chat_reply": "Beantworten", - "chat_addReaction": "Reaktion hinzufügen", + "chat_addReaction": "Reaktion hinzufügen", "chat_me": "Ich", "emojiCategorySmileys": "Emoticons", "emojiCategoryGestures": "Gesten", "emojiCategoryHearts": "Herz", "emojiCategoryObjects": "Objekte", - "gifPicker_title": "Wähle ein GIF", + "gifPicker_title": "Wähle ein GIF", "gifPicker_searchHint": "Suche nach GIFs...", "gifPicker_poweredBy": "Bereitgestellt von GIPHY", "gifPicker_noGifsFound": "Keine GIFs gefunden", @@ -470,15 +470,15 @@ "debugLog_appTitle": "App-Debug-Protokoll", "debugLog_bleTitle": "BLE-Debug-Protokoll", "debugLog_copyLog": "Kopieren des Protokolls", - "debugLog_clearLog": "Protokoll löschen", + "debugLog_clearLog": "Protokoll löschen", "debugLog_copied": "Debug-Protokoll kopiert", "debugLog_bleCopied": "BLE-Protokoll kopiert", - "debugLog_noEntries": "No Debug-Protokolle noch verfügbar", + "debugLog_noEntries": "No Debug-Protokolle noch verfügbar", "debugLog_enableInSettings": "Aktivieren Sie das App-Debug-Logging in den Einstellungen", "debugLog_frames": "Rahmen", "debugLog_rawLogRx": "Roh-Log-RX", - "debugLog_noBleActivity": "Bisher keine BLE-Aktivität", - "debugFrame_length": "Rahmenlänge: {count} Bytes", + "debugLog_noBleActivity": "Bisher keine BLE-Aktivität", + "debugFrame_length": "Rahmenlänge: {count} Bytes", "@debugFrame_length": { "placeholders": { "count": { @@ -495,7 +495,7 @@ } }, "debugFrame_textMessageHeader": "Textnachrichten Frame:", - "debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}", + "debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -546,10 +546,10 @@ "chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)", "chat_forceFloodMode": "Flut-Modus erzwingen", "chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):", - "chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.", + "chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.", "chat_hopSingular": "Sprung", - "chat_hopPlural": "Sprünge", - "chat_hopsCount": "{count} {count, plural, =1{Sprung} other{Sprünge}}", + "chat_hopPlural": "Sprünge", + "chat_hopsCount": "{count} {count, plural, =1{Sprung} other{Sprünge}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -563,13 +563,13 @@ "chat_pathActions": "Pfadaktionen:", "chat_setCustomPath": "Lege benutzerdefinierten Pfad fest", "chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen", - "chat_clearPath": "Pfad zurücksetzen", - "chat_clearPathSubtitle": "Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.", - "chat_pathCleared": "Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.", + "chat_clearPath": "Pfad zurücksetzen", + "chat_clearPathSubtitle": "Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.", + "chat_pathCleared": "Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.", "chat_floodModeSubtitle": "Verwende den Routingschalter in der App-Leiste", "chat_floodModeEnabled": "Flutmodus aktiviert.", - "chat_fullPath": "Vollständiger Pfad", - "chat_pathDetailsNotAvailable": "Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.", + "chat_fullPath": "Vollständiger Pfad", + "chat_pathDetailsNotAvailable": "Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.", "chat_pathSetHops": "Pfad gesetzt: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -582,15 +582,15 @@ } }, "chat_pathSavedLocally": "Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.", - "chat_pathDeviceConfirmed": "Gerät bestätigt.", - "chat_pathDeviceNotConfirmed": "Gerät noch nicht bestätigt.", + "chat_pathDeviceConfirmed": "Gerät bestätigt.", + "chat_pathDeviceNotConfirmed": "Gerät noch nicht bestätigt.", "chat_type": "Gebe ein", "chat_path": "Pfad", - "chat_publicKey": "Öffentlicher Schlüssel", + "chat_publicKey": "Öffentlicher Schlüssel", "chat_compressOutgoingMessages": "Komprimieren ausgehender Nachrichten", "chat_floodForced": "Geflutet (erzwungen)", "chat_directForced": "Direkt (erzwungen)", - "chat_hopsForced": "{count} Sprünge (erzwungen)", + "chat_hopsForced": "{count} Sprünge (erzwungen)", "@chat_hopsForced": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Link öffnen?", - "chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?", - "chat_open": "Öffnen", - "chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}", + "chat_openLink": "Link öffnen?", + "chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?", + "chat_open": "Öffnen", + "chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,10 +620,10 @@ } } }, - "chat_invalidLink": "Ungültiges Link-Format", + "chat_invalidLink": "Ungültiges Link-Format", "map_title": "Karte", "map_noNodesWithLocation": "Keine Knoten mit Standortdaten", - "map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.", + "map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.", "map_nodesCount": "Knoten: {count}", "@map_nodesCount": { "placeholders": { @@ -648,7 +648,7 @@ "map_pinPrivate": "Pin (Channel)", "map_pinPublic": "Pin (Public)", "map_lastSeen": "Letzte Sichtung", - "map_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", + "map_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", "map_from": "Von", "map_source": "Quelle", "map_flags": "Flags", @@ -658,9 +658,9 @@ "map_pointOfInterest": "Punkt von Interesse", "map_sendToContact": "Senden an Kontakt", "map_sendToChannel": "Senden an Kanal", - "map_noChannelsAvailable": "Keine Kanäle verfügbar", - "map_publicLocationShare": "Öffentliche Standortfreigabe", - "map_publicLocationShareConfirm": "Sie werden kurz darauf einen Ort in {channelLabel} teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.", + "map_noChannelsAvailable": "Keine Kanäle verfügbar", + "map_publicLocationShare": "Öffentliche Standortfreigabe", + "map_publicLocationShareConfirm": "Sie werden kurz darauf einen Ort in {channelLabel} teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,15 +668,15 @@ } } }, - "map_connectToShareMarkers": "Verbinde ein Gerät, um Marker zu teilen", + "map_connectToShareMarkers": "Verbinde ein Gerät, um Marker zu teilen", "map_filterNodes": "Knotenfilter", "map_nodeTypes": "Knotentypen", "map_chatNodes": "Chat-Knoten", "map_repeaters": "Repeater", "map_otherNodes": "Andere Knoten", - "map_keyPrefix": "Schlüsselpräfix", - "map_filterByKeyPrefix": "Filter nach Schlüsselpräfix", - "map_publicKeyPrefix": "Schlüsselpräfix", + "map_keyPrefix": "Schlüsselpräfix", + "map_filterByKeyPrefix": "Filter nach Schlüsselpräfix", + "map_publicKeyPrefix": "Schlüsselpräfix", "map_markers": "Marker", "map_showSharedMarkers": "Zeige gemeinsam genutzte Marker", "map_lastSeenTime": "Letzte Sichtung", @@ -684,10 +684,10 @@ "map_joinRoom": "Beitreten Sie dem Raum", "map_manageRepeater": "Repeater verwalten", "mapCache_title": "Offline-Karten-Cache", - "mapCache_selectAreaFirst": "Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.", - "mapCache_noTilesToDownload": "Keine Kacheln für diese Region zum Herunterladen verfügbar.", + "mapCache_selectAreaFirst": "Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.", + "mapCache_noTilesToDownload": "Keine Kacheln für diese Region zum Herunterladen verfügbar.", "mapCache_downloadTilesTitle": "Herunterladen von Kacheln", - "mapCache_downloadTilesPrompt": "Laden {count} Kacheln für den Offline-Bereich herunter?", + "mapCache_downloadTilesPrompt": "Laden {count} Kacheln für den Offline-Bereich herunter?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -717,12 +717,12 @@ }, "mapCache_clearOfflineCacheTitle": "Leere Offline-Cache", "mapCache_clearOfflineCachePrompt": "Alle zwischengespeicherten Kartenraster entfernen?", - "mapCache_offlineCacheCleared": "Offline-Cache gelöscht", - "mapCache_noAreaSelected": "Kein Bereich ausgewählt", + "mapCache_offlineCacheCleared": "Offline-Cache gelöscht", + "mapCache_noAreaSelected": "Kein Bereich ausgewählt", "mapCache_cacheArea": "Zwischenspeicherbereich", "mapCache_useCurrentView": "Aktuelle Ansicht verwenden", "mapCache_zoomRange": "Zoom Bereich", - "mapCache_estimatedTiles": "Geschätzte Kacheln: {count}", + "mapCache_estimatedTiles": "Geschätzte Kacheln: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -804,13 +804,13 @@ "time_minutes": "Minuten", "time_allTime": "Ganzer Zeitraum", "dialog_disconnect": "Trennen", - "dialog_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", + "dialog_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", "login_repeaterLogin": "Beim Repeater anmelden", "login_roomLogin": "Raum-Login", "login_password": "Passwort", "login_enterPassword": "Passwort eingeben", "login_savePassword": "Passwort speichern", - "login_savePasswordSubtitle": "Das Passwort wird auf diesem Gerät sicher gespeichert.", + "login_savePasswordSubtitle": "Das Passwort wird auf diesem Gerät sicher gespeichert.", "login_repeaterDescription": "Geben Sie das Repeater-Passwort ein, um auf Einstellungen und Status zuzugreifen.", "login_roomDescription": "Geben Sie das Raumkennwort ein, um auf die Einstellungen und den Status zuzugreifen.", "login_routing": "Routen", @@ -840,7 +840,7 @@ }, "login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.", "common_reload": "Neu laden", - "common_clear": "Löschen", + "common_clear": "Löschen", "path_currentPath": "Aktiver Pfad: {path}", "@path_currentPath": { "placeholders": { @@ -859,14 +859,14 @@ }, "path_enterCustomPath": "Gebe Pfad ein", "path_currentPathLabel": "Aktueller Pfad", - "path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.", - "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)", - "path_labelHexPrefixes": "Pfad (Hex-Präfixe)", - "path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)", - "path_selectFromContacts": "Oder wähle aus Kontakten aus:", + "path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.", + "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)", + "path_labelHexPrefixes": "Pfad (Hex-Präfixe)", + "path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)", + "path_selectFromContacts": "Oder wähle aus Kontakten aus:", "path_noRepeatersFound": "Keine Repeater oder Raumserver gefunden.", - "path_customPathsRequire": "Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.", - "path_invalidHexPrefixes": "Ungültige Hexadezimal-Präfixe: {prefixes}", + "path_customPathsRequire": "Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.", + "path_invalidHexPrefixes": "Ungültige Hexadezimal-Präfixe: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -904,8 +904,8 @@ "repeater_systemInformation": "Systeminformation", "repeater_battery": "Akku", "repeater_clockAtLogin": "Uhr (bei Anmeldung)", - "repeater_uptime": "Verfügbarkeit", - "repeater_queueLength": "Warteschlangenlänge", + "repeater_uptime": "Verfügbarkeit", + "repeater_queueLength": "Warteschlangenlänge", "repeater_debugFlags": "Fehlerbehebungsoptionen", "repeater_radioStatistics": "Funk-Statistik", "repeater_lastRssi": "Letzter RSSI", @@ -984,11 +984,11 @@ "repeater_settingsTitle": "Repeater Einstellungen", "repeater_basicSettings": "Grundlegende Einstellungen", "repeater_repeaterName": "Repeater Name", - "repeater_repeaterNameHelper": "Anzeigename für diesen Repeater", + "repeater_repeaterNameHelper": "Anzeigename für diesen Repeater", "repeater_adminPassword": "Admin-Passwort", "repeater_adminPasswordHelper": "Vollzugriffspasswort", "repeater_guestPassword": "Gast-Passwort", - "repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort", + "repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort", "repeater_radioSettings": "Funk Einstellungen", "repeater_frequencyMhz": "Frequenz (MHz)", "repeater_frequencyHelper": "300-2500 MHz", @@ -1000,17 +1000,17 @@ "repeater_locationSettings": "Standort Einstellungen", "repeater_latitude": "Breitengrad", "repeater_latitudeHelper": "Dezimalgrad (z.B. 37,7749)", - "repeater_longitude": "Längengrad", + "repeater_longitude": "Längengrad", "repeater_longitudeHelper": "Dezimalgrad (z.B. -122,4194)", "repeater_features": "Funktionen", "repeater_packetForwarding": "Paketweiterleitung", "repeater_packetForwardingSubtitle": "Aktivieren Sie den Repeater, um Pakete weiterzuleiten.", "repeater_guestAccess": "Gastzugriff", - "repeater_guestAccessSubtitle": "Gast-Zugriff mit beschränkten Rechten zulassen", - "repeater_privacyMode": "Privatsphäreeinstellung", - "repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", - "repeater_advertisementSettings": "Ankündigungseinstellungen", - "repeater_localAdvertInterval": "Intervall der lokalen Ankündigungen", + "repeater_guestAccessSubtitle": "Gast-Zugriff mit beschränkten Rechten zulassen", + "repeater_privacyMode": "Privatsphäreeinstellung", + "repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", + "repeater_advertisementSettings": "Ankündigungseinstellungen", + "repeater_localAdvertInterval": "Intervall der lokalen Ankündigungen", "repeater_localAdvertIntervalMinutes": "{minutes} Minuten", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervall der gefluteten Ankündigungen", + "repeater_floodAdvertInterval": "Intervall der gefluteten Ankündigungen", "repeater_floodAdvertIntervalHours": "{hours} Stunden", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,18 +1028,18 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung", + "repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung", "repeater_dangerZone": "Gefahrenzone", "repeater_rebootRepeater": "Neustart Repeater", - "repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.", - "repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?", - "repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung", - "repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren", - "repeater_regenerateIdentityKeyConfirm": "Dies generiert eine neue Identität für den Repeater. Fortfahren?", - "repeater_eraseFileSystem": "Dateisystem löschen", + "repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.", + "repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?", + "repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung", + "repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren", + "repeater_regenerateIdentityKeyConfirm": "Dies generiert eine neue Identität für den Repeater. Fortfahren?", + "repeater_eraseFileSystem": "Dateisystem löschen", "repeater_eraseFileSystemSubtitle": "Formatiere die Repeater-Dateisystemdatei", - "repeater_eraseFileSystemConfirm": "WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!", - "repeater_eraseSerialOnly": "Löschen ist nur über die serielle Konsole möglich.", + "repeater_eraseFileSystemConfirm": "WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!", + "repeater_eraseSerialOnly": "Löschen ist nur über die serielle Konsole möglich.", "repeater_commandSent": "Befehl gesendet: {command}", "@repeater_commandSent": { "placeholders": { @@ -1056,7 +1056,7 @@ } } }, - "repeater_confirm": "Bestätigen", + "repeater_confirm": "Bestätigen", "repeater_settingsSaved": "Einstellungen erfolgreich gespeichert", "repeater_errorSavingSettings": "Fehler beim Speichern der Einstellungen: {error}", "@repeater_errorSavingSettings": { @@ -1073,7 +1073,7 @@ "repeater_refreshPacketForwarding": "Aktualisieren Paketweiterleitung", "repeater_refreshGuestAccess": "Aktualisieren Sie den Gastzugriff", "repeater_refreshPrivacyMode": "Wiederherstellen des Datenschutzzustands", - "repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Ankündigungseinstellungen", + "repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Ankündigungseinstellungen", "repeater_refreshed": "{label} wurde aktualisiert", "@repeater_refreshed": { "placeholders": { @@ -1091,14 +1091,14 @@ } }, "repeater_cliTitle": "Repeater CLI", - "repeater_debugNextCommand": "Fehlersuche des nächsten Befehls", + "repeater_debugNextCommand": "Fehlersuche des nächsten Befehls", "repeater_commandHelp": "Hilfe", - "repeater_clearHistory": "Löschen der Historie", + "repeater_clearHistory": "Löschen der Historie", "repeater_noCommandsSent": "Noch keine Befehle gesendet.", "repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle", "repeater_enterCommandHint": "Geben Sie den Befehl ein...", "repeater_previousCommand": "Vorhergehende Aktion", - "repeater_nextCommand": "Nächste Aktion", + "repeater_nextCommand": "Nächste Aktion", "repeater_enterCommandFirst": "Geben Sie zuerst einen Befehl ein", "repeater_cliCommandFrameTitle": "CLI-Befehlsfenster", "repeater_cliCommandError": "Fehler: {error}", @@ -1114,73 +1114,73 @@ "repeater_cliQuickGetTx": "Erhalte TX", "repeater_cliQuickNeighbors": "Nachbarn", "repeater_cliQuickVersion": "Version", - "repeater_cliQuickAdvertise": "Ankündigungen", + "repeater_cliQuickAdvertise": "Ankündigungen", "repeater_cliQuickClock": "Uhr", - "repeater_cliHelpAdvert": "Sendet eine Ankündigung", - "repeater_cliHelpReboot": "Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer 'Timeout'-Situation kommt, was normal ist.)", - "repeater_cliHelpClock": "Zeigt die aktuelle Uhrzeit pro Gerät an.", - "repeater_cliHelpPassword": "Legt ein neues Administrator-Passwort für das Gerät fest.", - "repeater_cliHelpVersion": "Zeigt die Geräteversion und das Datum des Firmware-Builds an.", - "repeater_cliHelpClearStats": "Setzt verschiedene Statistikberechnungen auf Null zurück.", + "repeater_cliHelpAdvert": "Sendet eine Ankündigung", + "repeater_cliHelpReboot": "Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer 'Timeout'-Situation kommt, was normal ist.)", + "repeater_cliHelpClock": "Zeigt die aktuelle Uhrzeit pro Gerät an.", + "repeater_cliHelpPassword": "Legt ein neues Administrator-Passwort für das Gerät fest.", + "repeater_cliHelpVersion": "Zeigt die Geräteversion und das Datum des Firmware-Builds an.", + "repeater_cliHelpClearStats": "Setzt verschiedene Statistikberechnungen auf Null zurück.", "repeater_cliHelpSetAf": "Legt den Luftzeitfaktor fest.", - "repeater_cliHelpSetTx": "Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)", - "repeater_cliHelpSetRepeat": "Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.", - "repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).", - "repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)", + "repeater_cliHelpSetTx": "Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)", + "repeater_cliHelpSetRepeat": "Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.", + "repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).", + "repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)", "repeater_cliHelpSetIntThresh": "Legt den Interferenzeniveau (in dB) fest. Der Standardwert ist 14. Auf 0 setzen, um die Erkennung von Kanalinterferenzen zu deaktivieren.", - "repeater_cliHelpSetAgcResetInterval": "Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetAgcResetInterval": "Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.", "repeater_cliHelpSetMultiAcks": "Aktiviert oder deaktiviert die Funktion 'Doppel-ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.", - "repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.", - "repeater_cliHelpSetGuestPassword": "Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)", + "repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.", + "repeater_cliHelpSetGuestPassword": "Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)", "repeater_cliHelpSetName": "Legt den Anzeigenamen fest.", - "repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)", - "repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)", - "repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.", - "repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", - "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", - "repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.", - "repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.", - "repeater_cliHelpSetBridgeDelay": "Setze Verzögerung vor erneuter Übertragung von Paketen.", - "repeater_cliHelpSetBridgeSource": "Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.", - "repeater_cliHelpSetBridgeBaud": "Setze die serielle Link-Baudrate für RS232-Brücken.", - "repeater_cliHelpSetBridgeSecret": "Richte das Brückenpassword ein.", - "repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).", - "repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).", - "repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)", - "repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.", + "repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)", + "repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)", + "repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.", + "repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", + "repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.", + "repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.", + "repeater_cliHelpSetBridgeDelay": "Setze Verzögerung vor erneuter Übertragung von Paketen.", + "repeater_cliHelpSetBridgeSource": "Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.", + "repeater_cliHelpSetBridgeBaud": "Setze die serielle Link-Baudrate für RS232-Brücken.", + "repeater_cliHelpSetBridgeSecret": "Richte das Brückenpassword ein.", + "repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).", + "repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).", + "repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)", + "repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.", "repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.", "repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.", - "repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.", - "repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.", + "repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.", + "repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.", "repeater_cliHelpRegion": "Listet alle definierten Regionen auf.", - "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.", - "repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".", - "repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.", - "repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)", - "repeater_cliHelpRegionAllowf": "Legt die 'Flut'-Berechtigung für die angegebene Region fest. ('' für den globalen/legacy-Bereich)", - "repeater_cliHelpRegionDenyf": "Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)", - "repeater_cliHelpRegionHome": "Antwortet mit der aktuellen 'Home'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)", + "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.", + "repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".", + "repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.", + "repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)", + "repeater_cliHelpRegionAllowf": "Legt die 'Flut'-Berechtigung für die angegebene Region fest. ('' für den globalen/legacy-Bereich)", + "repeater_cliHelpRegionDenyf": "Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)", + "repeater_cliHelpRegionHome": "Antwortet mit der aktuellen 'Home'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)", "repeater_cliHelpRegionHomeSet": "Legt die 'Home'-Region fest.", "repeater_cliHelpRegionSave": "Speichert die Regionenliste/Karte in den Speicher.", "repeater_cliHelpGps": "Zeigt GPS-Status an. Wenn GPS deaktiviert ist, antwortet es nur mit \"aus\", wenn es eingeschaltet ist, antwortet es mit \"an\", \"Status\", \"Fix\" und Satellitenanzahl.", "repeater_cliHelpGpsOnOff": "Schaltet die GPS-Leistung ein/aus.", "repeater_cliHelpGpsSync": "Synchronisiert die Knotenzeit mit der GPS-Uhr.", - "repeater_cliHelpGpsSetLoc": "Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.", - "repeater_cliHelpGpsAdvert": "Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen", + "repeater_cliHelpGpsSetLoc": "Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.", + "repeater_cliHelpGpsAdvert": "Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen", "repeater_cliHelpGpsAdvertSet": "Legt die Standort-Anzeigekonfiguration fest.", "repeater_commandsListTitle": "Befehlsliste", - "repeater_commandsListNote": "ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.", + "repeater_commandsListNote": "ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.", "repeater_general": "Allgemein", "repeater_settingsCategory": "Einstellungen", - "repeater_bridge": "Brücke", + "repeater_bridge": "Brücke", "repeater_logging": "Protokollierung", "repeater_neighborsRepeaterOnly": "Nachbarn (nur Repeater)", "repeater_regionManagementRepeaterOnly": "Regionenverwaltung (nur Repeater)", - "repeater_regionNote": "Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.", + "repeater_regionNote": "Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.", "repeater_gpsManagement": "GPS-Verwaltung", - "repeater_gpsNote": "Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.", + "repeater_gpsNote": "Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.", "telemetry_receivedData": "Empfangene Telemetriedaten", "telemetry_requestTimeout": "Telemetry-Anfrage hat zu lange gedauert.", "telemetry_errorLoading": "Fehler beim Laden der Telemetrie: {error}", @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Keine Telemetriedaten verfügbar.", + "telemetry_noData": "Keine Telemetriedaten verfügbar.", "telemetry_channelTitle": "Kanal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1246,15 +1246,15 @@ "channelPath_title": "Paketpfad", "channelPath_viewMap": "Karte anzeigen", "channelPath_otherObservedPaths": "Sonstige beobachtete Pfade", - "channelPath_repeaterHops": "Repeater-Sprünge", - "channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.", + "channelPath_repeaterHops": "Repeater-Sprünge", + "channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.", "channelPath_messageDetails": "Nachrichtendetails", "channelPath_senderLabel": "Sender", "channelPath_timeLabel": "Zeit", "channelPath_repeatsLabel": "Wiederholungen", "channelPath_pathLabel": "Pfad {index}", "channelPath_observedLabel": "Beobachtet", - "channelPath_observedPathTitle": "Beobachteter Pfad {index} • {hops}", + "channelPath_observedPathTitle": "Beobachteter Pfad {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1291,7 +1291,7 @@ "channelPath_unknownPath": "Unbekannt", "channelPath_floodPath": "Geflutet", "channelPath_directPath": "Direkt", - "channelPath_observedZeroOf": "0 von {total} Sprüngen", + "channelPath_observedZeroOf": "0 von {total} Sprüngen", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1299,7 +1299,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} von {total} Sprüngen", + "channelPath_observedSomeOf": "{observed} von {total} Sprüngen", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1311,8 +1311,8 @@ } }, "channelPath_mapTitle": "Pfadkarte", - "channelPath_noRepeaterLocations": "Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.", - "channelPath_primaryPath": "Pfad {index} (Primär)", + "channelPath_noRepeaterLocations": "Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.", + "channelPath_primaryPath": "Pfad {index} (Primär)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Pfad", "channelPath_observedPathHeader": "Beobachteter Pfad", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,17 +1340,17 @@ } } }, - "channelPath_noHopDetailsAvailable": "Keine Informationen zu dieser Paketroute verfügbar.", + "channelPath_noHopDetailsAvailable": "Keine Informationen zu dieser Paketroute verfügbar.", "channelPath_unknownRepeater": "Unbekannter Repeater", "listFilter_tooltip": "Filteren und sortieren", "listFilter_sortBy": "Sortiere nach", "listFilter_latestMessages": "Letzte Nachrichten", - "listFilter_heardRecently": "Kürzlich gehört", + "listFilter_heardRecently": "Kürzlich gehört", "listFilter_az": "A-Z", "listFilter_filters": "Filtere", "listFilter_all": "Alle", "listFilter_favorites": "Favoriten", - "listFilter_addToFavorites": "Zu Favoriten hinzufügen", + "listFilter_addToFavorites": "Zu Favoriten hinzufügen", "listFilter_removeFromFavorites": "Aus Favoriten entfernen", "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", @@ -1370,17 +1370,17 @@ "neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", "neighbors_repeatersNeighbors": "Nachbarn", - "neighbors_noData": "Keine Nachbarsdaten verfügbar.", + "neighbors_noData": "Keine Nachbarsdaten verfügbar.", "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", - "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", + "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", "channels_createPrivateChannel": "Erstelle einen privaten Kanal", - "channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.", - "channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei", + "channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.", + "channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei", "channels_joinPublicChannelDesc": "Jeder kann diesem Kanal beitreten.", "channels_joinHashtagChannel": "Treten Sie einem Hashtag-Kanal bei", - "channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.", + "channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.", "channels_scanQrCode": "Scannen Sie einen QR-Code", - "channels_scanQrCodeComingSoon": "Bald verfügbar", + "channels_scanQrCodeComingSoon": "Bald verfügbar", "channels_enterHashtag": "Gib Hashtag ein", "channels_hashtagHint": "z.B. #team", "@neighbors_unknownContact": { @@ -1397,11 +1397,11 @@ } } }, - "neighbors_heardAgo": "Gehört vor: {time}", + "neighbors_heardAgo": "Gehört vor: {time}", "neighbors_unknownContact": "Unbekannt {pubkey}", "settings_locationGPSEnable": "GPS aktivieren", "settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.", - "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", + "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", "settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.", "contacts_manageRoom": "Raum-Server verwalten", "room_management": "Raum-Server-Verwaltung", @@ -1463,34 +1463,34 @@ }, "common_ok": "OK", "community_create": "Erstelle Community", - "community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.", + "community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.", "community_join": "Beitreten", "community_joinTitle": "Tritt der Community bei", - "community_joinConfirmation": "Möchten Sie sich der Community \"{name}\" anschließen?", + "community_joinConfirmation": "Möchten Sie sich der Community \"{name}\" anschließen?", "community_scanQr": "Scannen Sie die Community QR-Code", "community_scanInstructions": "Richten Sie die Kamera auf einen Community-QR-Code.", "community_showQr": "Zeige QR-Code", - "community_publicChannel": "Community Öffentlich", + "community_publicChannel": "Community Öffentlich", "community_enterName": "Bitte Community-Name eingeben", "community_title": "Community", "community_created": "Community \"{name}\" wurde erstellt", "community_joined": "Community \"{name}\" beigetreten", "community_qrTitle": "Teile Community", - "community_qrInstructions": "Scannen Sie diesen QR-Code, um sich \"{name}\" anzuschließen.", - "community_hashtagPrivacyHint": "Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden", + "community_qrInstructions": "Scannen Sie diesen QR-Code, um sich \"{name}\" anzuschließen.", + "community_hashtagPrivacyHint": "Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden", "community_hashtagChannel": "Community Hashtag", "community_name": "Community Name", - "community_invalidQrCode": "Ungültiger Community-QR-Code", + "community_invalidQrCode": "Ungültiger Community-QR-Code", "community_alreadyMember": "Bereits registriert", "community_alreadyMemberMessage": "Sie sind bereits Mitglied von \"{name}\".", - "community_addPublicChannel": "Füge einen öffentlichen Community-Kanal hinzu", - "community_addPublicChannelHint": "Automatisch den öffentlichen Kanal für diese Community hinzufügen", + "community_addPublicChannel": "Füge einen öffentlichen Community-Kanal hinzu", + "community_addPublicChannelHint": "Automatisch den öffentlichen Kanal für diese Community hinzufügen", "community_noCommunities": "Noch keiner Community beigetreten", "community_scanOrCreate": "Scannen Sie einen QR-Code oder eine Community erstellen, um loszulegen.", "community_manageCommunities": "Verwalten von Communities", "community_delete": "Verlasse Community", "community_deleteConfirm": "\"{name}\" verlassen?", - "community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.", + "community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1499,13 +1499,13 @@ } }, "community_deleted": "Community \"{name}\" verlassen", - "community_addHashtagChannel": "Füge einen Community-Hashtag hinzu", - "community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu", - "community_selectCommunity": "Wählen Sie eine Community", - "community_regularHashtag": "Regulärer Hashtag", - "community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)", - "community_communityHashtagDesc": "Nur für Mitglieder der Community", - "community_forCommunity": "Für {name}", + "community_addHashtagChannel": "Füge einen Community-Hashtag hinzu", + "community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu", + "community_selectCommunity": "Wählen Sie eine Community", + "community_regularHashtag": "Regulärer Hashtag", + "community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)", + "community_communityHashtagDesc": "Nur für Mitglieder der Community", + "community_forCommunity": "Für {name}", "community_communityHashtag": "Community Hashtag", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1536,12 +1536,12 @@ } }, "community_regenerate": "Neu generieren", - "community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich", - "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.", - "community_regenerateSecret": "Neugenerierung des Schlüssels", - "community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert", - "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", - "community_updateSecret": "Aktualisieren Sie den Schlüssel", + "community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich", + "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.", + "community_regenerateSecret": "Neugenerierung des Schlüssels", + "community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert", + "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", + "community_updateSecret": "Aktualisieren Sie den Schlüssel", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1552,7 +1552,7 @@ "pathTrace_refreshTooltip": "Path Trace aktualisieren.", "pathTrace_you": "Du", "pathTrace_failed": "Pfadverfolgung fehlgeschlagen.", - "pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.", + "pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.", "contacts_pathTrace": "Pfadverfolgung", "contacts_ping": "Pingen", "contacts_repeaterPathTrace": "Pfadverfolgung zum Repeater", @@ -1562,24 +1562,24 @@ "contacts_pathTraceTo": "Route nach {name} verfolgen", "contacts_chatTraceRoute": "Pfadverfolgungsroute", "appSettings_languageRu": "Russisch", - "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", + "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", "appSettings_languageUk": "Ukrainisch", "appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren", - "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", + "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", - "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", - "contacts_floodAdvert": "Flut-Ankündigung", - "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", - "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", - "contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren", + "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", + "contacts_floodAdvert": "Flut-Ankündigung", + "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", + "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", + "contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren", "contacts_ShareContact": "Kontakt in die Zwischenablage kopieren", "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", - "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", + "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", - "notification_activityTitle": "MeshCore Aktivität", + "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", + "notification_activityTitle": "MeshCore Aktivität", "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", "@notification_messagesCount": { "placeholders": { @@ -1623,36 +1623,36 @@ "settings_gpxExportChat": "Kontaktstandorte", "settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.", "settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.", - "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", + "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", "settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.", "settings_gpxExportAllContacts": "Alle Kontaktstandorte", "settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren", "settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert", "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!", "map_removeLast": "Letztes Entfernen", - "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", - "map_runTrace": "Pfadverlauf ausführen", - "pathTrace_clearTooltip": "Pfad löschen", + "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", + "map_runTrace": "Pfadverlauf ausführen", + "pathTrace_clearTooltip": "Pfad löschen", "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", - "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", + "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", "scanner_chromeRequired": "Chrome Browser erforderlich", - "scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.", + "scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", "scanner_enableBluetooth": "Bluetooth aktivieren", "snrIndicator_lastSeen": "Zuletzt gesehen", - "snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater", + "snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater", "chat_ShowAllPaths": "Alle Pfade anzeigen", "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", - "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.", - "settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)", + "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.", + "settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Einheiten", "appSettings_unitsMetric": "Metrisch (m/km)", "appSettings_unitsImperial": "Imperial (ft/mi)", "map_lineOfSight": "Sichtlinie", "map_losScreenTitle": "Sichtlinie", - "losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.", - "losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}", + "losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.", + "losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1660,10 +1660,10 @@ } } }, - "losClearAllPoints": "Löschen Sie alle Punkte", - "losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen", - "losMenuTitle": "LOS-Menü", - "losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen", + "losClearAllPoints": "Löschen Sie alle Punkte", + "losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen", + "losMenuTitle": "LOS-Menü", + "losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen", "losShowDisplayNodes": "Anzeigeknoten anzeigen", "losCustomPoints": "Benutzerdefinierte Punkte", "losCustomPointLabel": "Benutzerdefiniert {index}", @@ -1698,8 +1698,8 @@ } } }, - "losRun": "Führen Sie LOS aus", - "losNoElevationData": "Keine Höhendaten", + "losRun": "Führen Sie LOS aus", + "losNoElevationData": "Keine Höhendaten", "losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { @@ -1734,7 +1734,7 @@ } } }, - "losStatusChecking": "LOS: Überprüfen...", + "losStatusChecking": "LOS: Überprüfen...", "losStatusNoData": "LOS: keine Daten", "losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt", "@losStatusSummary": { @@ -1753,20 +1753,20 @@ } } }, - "losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.", - "losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.", + "losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.", + "losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.", "losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um", "losPointName": "Punktname", "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", - "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Funkhorizont", "losLegendLosBeam": "Sichtlinie", - "losLegendTerrain": "Gelände", + "losLegendTerrain": "Gelände", "losFrequencyLabel": "Frequenz", "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", - "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1830,13 +1830,11 @@ "contacts_searchFavorites": "Suche {number}{str} Favoriten...", "contacts_searchUsers": "Suche {number}{str} Benutzer...", "contacts_searchRoomServers": "Suche {number}{str} Raumserver...", - "connectionChoiceSubtitle": "Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceTitle": "Wählen Sie Ihre bevorzugte Verbindungsmethode.", - "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", - "usbScreenNote": "USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", - "usbScreenTitle": "Über USB verbinden", - "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", - "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie." + "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", + "usbScreenNote": "USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", + "usbScreenTitle": "Über USB verbinden", + "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", + "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9719c7c..58b04aa 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,4 @@ -{ +{ "@@locale": "en", "appTitle": "MeshCore Open", "nav_contacts": "Contacts", @@ -28,7 +28,7 @@ "common_disable": "Disable", "common_reboot": "Reboot", "common_loading": "Loading...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -46,8 +46,6 @@ } }, "scanner_title": "MeshCore Open", - "connectionChoiceTitle": "Choose your connection method", - "connectionChoiceSubtitle": "Select how you would like to reach your MeshCore device.", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", "usbScreenTitle": "Connect over USB", @@ -180,20 +178,20 @@ "appSettings_language": "Language", "appSettings_languageSystem": "System default", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_languageRu": "Русский", - "appSettings_languageUk": "Українська", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_languageRu": "Русский", + "appSettings_languageUk": "Українська", "appSettings_enableMessageTracing": "Enable Message Tracing", "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_notifications": "Notifications", @@ -1341,7 +1339,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1391,7 +1389,7 @@ "channelPath_repeatsLabel": "Repeats", "channelPath_pathLabel": "Path {index}", "channelPath_observedLabel": "Observed", - "channelPath_observedPathTitle": "Observed path {index} • {hops}", + "channelPath_observedPathTitle": "Observed path {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1466,7 +1464,7 @@ }, "channelPath_pathLabelTitle": "Path", "channelPath_observedPathHeader": "Observed Path", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index a69503a..8a11672 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -19,8 +19,8 @@ "common_delete": "Eliminar", "common_close": "Cerrar", "common_edit": "Editar", - "common_add": "Añadir", - "common_settings": "Configuración", + "common_add": "Añadir", + "common_settings": "Configuración", "common_disconnect": "Desconectar", "common_connected": "Conectado", "common_disconnected": "Desconectado", @@ -35,7 +35,7 @@ "common_disable": "Desactivar", "common_reboot": "Reiniciar", "common_loading": "Cargando...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -56,7 +56,7 @@ "scanner_scanning": "Escaneando dispositivos...", "scanner_connecting": "Conectando...", "scanner_disconnecting": "Desconectando...", - "scanner_notConnected": "No está conectado", + "scanner_notConnected": "No está conectado", "scanner_connectedTo": "Conectado a {deviceName}", "@scanner_connectedTo": { "placeholders": { @@ -67,7 +67,7 @@ }, "scanner_searchingDevices": "Buscando dispositivos MeshCore...", "scanner_tapToScan": "Toca Escanear para encontrar dispositivos MeshCore", - "scanner_connectionFailed": "Error de conexión: {error}", + "scanner_connectionFailed": "Error de conexión: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -77,49 +77,49 @@ }, "scanner_stop": "Detener", "scanner_scan": "Escanea", - "device_quickSwitch": "Cambiar rápidamente", + "device_quickSwitch": "Cambiar rápidamente", "device_meshcore": "MeshCore", - "settings_title": "Configuración", - "settings_deviceInfo": "Información del dispositivo", - "settings_appSettings": "Configuración de la App", + "settings_title": "Configuración", + "settings_deviceInfo": "Información del dispositivo", + "settings_appSettings": "Configuración de la App", "settings_appSettingsSubtitle": "Notificaciones, mensajes y preferencias de mapa", - "settings_nodeSettings": "Configuración del Nodo", + "settings_nodeSettings": "Configuración del Nodo", "settings_nodeName": "Nombre del nodo", - "settings_nodeNameNotSet": "No está configurado", + "settings_nodeNameNotSet": "No está configurado", "settings_nodeNameHint": "Introducir nombre de nodo", "settings_nodeNameUpdated": "Nombre actualizado", - "settings_radioSettings": "Configuración de Radio", - "settings_radioSettingsSubtitle": "Frecuencia, potencia, factor de dispersión", + "settings_radioSettings": "Configuración de Radio", + "settings_radioSettingsSubtitle": "Frecuencia, potencia, factor de dispersión", "settings_radioSettingsUpdated": "Ajustes de radio actualizados", - "settings_location": "Ubicación", + "settings_location": "Ubicación", "settings_locationSubtitle": "Coordenadas GPS", - "settings_locationUpdated": "Ubicación actualizada", + "settings_locationUpdated": "Ubicación actualizada", "settings_locationBothRequired": "Introduzca tanto la latitud como la longitud.", - "settings_locationInvalid": "Latitud o longitud inválidos.", + "settings_locationInvalid": "Latitud o longitud inválidos.", "settings_latitude": "Latitud", "settings_longitude": "Longitud", "settings_privacyMode": "Modo Privacidad", - "settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", - "settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.", + "settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", + "settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.", "settings_privacyModeEnabled": "Modo de privacidad activado", "settings_privacyModeDisabled": "Modo de privacidad desactivado", "settings_actions": "Acciones", "settings_sendAdvertisement": "Enviar Anuncio", - "settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora", + "settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora", "settings_advertisementSent": "Anuncio enviado", - "settings_syncTime": "Tiempo de Sincronización", - "settings_syncTimeSubtitle": "Establecer la hora del dispositivo al tiempo del teléfono", + "settings_syncTime": "Tiempo de Sincronización", + "settings_syncTimeSubtitle": "Establecer la hora del dispositivo al tiempo del teléfono", "settings_timeSynchronized": "Sincronizado en el tiempo", "settings_refreshContacts": "Actualizar Contactos", "settings_refreshContactsSubtitle": "Recargar lista de contactos del dispositivo", "settings_rebootDevice": "Reiniciar Dispositivo", "settings_rebootDeviceSubtitle": "Reiniciar el dispositivo MeshCore", - "settings_rebootDeviceConfirm": "¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.", + "settings_rebootDeviceConfirm": "¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.", "settings_debug": "Depurar", - "settings_bleDebugLog": "Registro de Depuración BLE", + "settings_bleDebugLog": "Registro de Depuración BLE", "settings_bleDebugLogSubtitle": "Comandos, respuestas y datos brutos de BLE", - "settings_appDebugLog": "Registro de Depuración de la App", - "settings_appDebugLogSubtitle": "Mensajes de depuración de la aplicación", + "settings_appDebugLog": "Registro de Depuración de la App", + "settings_appDebugLogSubtitle": "Mensajes de depuración de la aplicación", "settings_about": "Acerca de", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "2026 Proyecto Open Source MeshCore", - "settings_aboutDescription": "Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.", + "settings_aboutDescription": "Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.", "settings_infoName": "Nombre", "settings_infoId": "ID", "settings_infoStatus": "Estado", - "settings_infoBattery": "Batería", - "settings_infoPublicKey": "Clave Pública", - "settings_infoContactsCount": "Número de contactos", - "settings_infoChannelCount": "Número de canales", + "settings_infoBattery": "Batería", + "settings_infoPublicKey": "Clave Pública", + "settings_infoContactsCount": "Número de contactos", + "settings_infoChannelCount": "Número de canales", "settings_presets": "Preajustes", "settings_frequency": "Frecuencia (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", - "settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)", + "settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)", "settings_bandwidth": "Ancho de banda", - "settings_spreadingFactor": "Factor de propagación", - "settings_codingRate": "Tasa de Programación", + "settings_spreadingFactor": "Factor de propagación", + "settings_codingRate": "Tasa de Programación", "settings_txPower": "TX Potencia (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)", + "settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)", "settings_error": "Error: {message}", "@settings_error": { "placeholders": { @@ -156,7 +156,7 @@ } } }, - "appSettings_title": "Configuración de la App", + "appSettings_title": "Configuración de la App", "appSettings_appearance": "Apariencia", "appSettings_theme": "Tema", "appSettings_themeSystem": "Valor predeterminado del sistema", @@ -165,42 +165,42 @@ "appSettings_language": "Idioma", "appSettings_languageSystem": "Predeterminado del sistema", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notificaciones", "appSettings_enableNotifications": "Habilitar Notificaciones", "appSettings_enableNotificationsSubtitle": "Recibir notificaciones para mensajes y anuncios", - "appSettings_notificationPermissionDenied": "Permiso de notificación denegado", + "appSettings_notificationPermissionDenied": "Permiso de notificación denegado", "appSettings_notificationsEnabled": "Notificaciones activadas", "appSettings_notificationsDisabled": "Notificaciones desactivadas", "appSettings_messageNotifications": "Notificaciones de Mensaje", - "appSettings_messageNotificationsSubtitle": "Mostrar notificación al recibir nuevos mensajes", + "appSettings_messageNotificationsSubtitle": "Mostrar notificación al recibir nuevos mensajes", "appSettings_channelMessageNotifications": "Notificaciones de Mensajes del Canal", - "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificación al recibir mensajes del canal", + "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificación al recibir mensajes del canal", "appSettings_advertisementNotifications": "Notificaciones de Anuncios", - "appSettings_advertisementNotificationsSubtitle": "Mostrar notificación cuando se descubren nuevos nodos", - "appSettings_messaging": "Mensajería", + "appSettings_advertisementNotificationsSubtitle": "Mostrar notificación cuando se descubren nuevos nodos", + "appSettings_messaging": "Mensajería", "appSettings_clearPathOnMaxRetry": "Borrar Camino en Max Reintentos", - "appSettings_clearPathOnMaxRetrySubtitle": "Restablecer la ruta de contacto después de 5 intentos de envío fallidos", - "appSettings_pathsWillBeCleared": "Los caminos se limpiarán después de 5 intentos fallidos.", - "appSettings_pathsWillNotBeCleared": "Las rutas no se eliminarán automáticamente.", - "appSettings_autoRouteRotation": "Rotación de Ruta Automática", - "appSettings_autoRouteRotationSubtitle": "Alternar entre las mejores rutas y el modo inundación", - "appSettings_autoRouteRotationEnabled": "Rotación de ruta automática habilitada", - "appSettings_autoRouteRotationDisabled": "Rotación de ruta automática desactivada", - "appSettings_battery": "Batería", - "appSettings_batteryChemistry": "Química de la batería", - "appSettings_batteryChemistryPerDevice": "Configuración por dispositivo ({deviceName})", + "appSettings_clearPathOnMaxRetrySubtitle": "Restablecer la ruta de contacto después de 5 intentos de envío fallidos", + "appSettings_pathsWillBeCleared": "Los caminos se limpiarán después de 5 intentos fallidos.", + "appSettings_pathsWillNotBeCleared": "Las rutas no se eliminarán automáticamente.", + "appSettings_autoRouteRotation": "Rotación de Ruta Automática", + "appSettings_autoRouteRotationSubtitle": "Alternar entre las mejores rutas y el modo inundación", + "appSettings_autoRouteRotationEnabled": "Rotación de ruta automática habilitada", + "appSettings_autoRouteRotationDisabled": "Rotación de ruta automática desactivada", + "appSettings_battery": "Batería", + "appSettings_batteryChemistry": "Química de la batería", + "appSettings_batteryChemistryPerDevice": "Configuración por dispositivo ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,11 +208,11 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Conéctate a un dispositivo para elegir", + "appSettings_batteryChemistryConnectFirst": "Conéctate a un dispositivo para elegir", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)", "appSettings_batteryLipo": "LiPo (3.0-4.2V)", - "appSettings_mapDisplay": "Visualización del Mapa", + "appSettings_mapDisplay": "Visualización del Mapa", "appSettings_showRepeaters": "Mostrar Repetidores", "appSettings_showRepeatersSubtitle": "Mostrar nodos de repetidor en el mapa", "appSettings_showChatNodes": "Mostrar Nodos de Chat", @@ -221,7 +221,7 @@ "appSettings_showOtherNodesSubtitle": "Mostrar otros tipos de nodo en el mapa", "appSettings_timeFilter": "Filtro de Tiempo", "appSettings_timeFilterShowAll": "Mostrar todos los nodos", - "appSettings_timeFilterShowLast": "Mostrar nodos de las últimas {hours} horas", + "appSettings_timeFilterShowLast": "Mostrar nodos de las últimas {hours} horas", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -232,13 +232,13 @@ "appSettings_mapTimeFilter": "Filtro de Tiempo del Mapa", "appSettings_showNodesDiscoveredWithin": "Mostrar nodos descubiertos dentro de:", "appSettings_allTime": "Todo el tiempo", - "appSettings_lastHour": "Última hora", - "appSettings_last6Hours": "Últimas 6 horas", - "appSettings_last24Hours": "Últimas 24 horas", + "appSettings_lastHour": "Última hora", + "appSettings_last6Hours": "Últimas 6 horas", + "appSettings_last24Hours": "Últimas 24 horas", "appSettings_lastWeek": "La semana pasada", - "appSettings_offlineMapCache": "Caché de Mapa Offline", - "appSettings_noAreaSelected": "No se ha seleccionado ningún área", - "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", + "appSettings_offlineMapCache": "Caché de Mapa Offline", + "appSettings_noAreaSelected": "No se ha seleccionado ningún área", + "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,13 +250,13 @@ } }, "appSettings_debugCard": "Depurar", - "appSettings_appDebugLogging": "Registro de Depuración de la App", - "appSettings_appDebugLoggingSubtitle": "Registrar mensajes de depuración de la app de registro para solucionar problemas", - "appSettings_appDebugLoggingEnabled": "Registro de depuración de la aplicación habilitado", - "appSettings_appDebugLoggingDisabled": "El registro de depuración de la aplicación está desactivado", + "appSettings_appDebugLogging": "Registro de Depuración de la App", + "appSettings_appDebugLoggingSubtitle": "Registrar mensajes de depuración de la app de registro para solucionar problemas", + "appSettings_appDebugLoggingEnabled": "Registro de depuración de la aplicación habilitado", + "appSettings_appDebugLoggingDisabled": "El registro de depuración de la aplicación está desactivado", "contacts_title": "Contactos", - "contacts_noContacts": "Aún no hay contactos.", - "contacts_contactsWillAppear": "Los contactos aparecerán cuando los dispositivos anuncien.", + "contacts_noContacts": "Aún no hay contactos.", + "contacts_contactsWillAppear": "Los contactos aparecerán cuando los dispositivos anuncien.", "contacts_searchContacts": "Buscar contactos...", "contacts_noUnreadContacts": "No contactos sin leer", "contacts_noContactsFound": "No se encontraron contactos ni grupos.", @@ -296,8 +296,8 @@ "contacts_filterContacts": "Filtrar contactos...", "contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro", "contacts_noMembers": "No miembros", - "contacts_lastSeenNow": "Última vez que se vio ahora", - "contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.", + "contacts_lastSeenNow": "Última vez que se vio ahora", + "contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora", - "contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.", + "contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora", + "contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día", - "contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.", + "contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día", + "contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -325,7 +325,7 @@ }, "channels_title": "Canales", "channels_noChannelsConfigured": "No se han configurado canales", - "channels_addPublicChannel": "Añadir Canal Público", + "channels_addPublicChannel": "Añadir Canal Público", "channels_searchChannels": "Buscar canales...", "channels_noChannelsFound": "No se encontraron canales", "channels_channelIndex": "Canal {index}", @@ -337,9 +337,9 @@ } }, "channels_hashtagChannel": "Canal con hashtag", - "channels_public": "Público", + "channels_public": "Público", "channels_private": "Privado", - "channels_publicChannel": "Canal público", + "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", "channels_muteChannel": "Silenciar canal", @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Añadir Canal", - "channels_channelIndexLabel": "Índice de Canal", + "channels_addChannel": "Añadir Canal", + "channels_channelIndexLabel": "Índice de Canal", "channels_channelName": "Nombre del canal", - "channels_usePublicChannel": "Usar Canal Público", - "channels_standardPublicPsk": "PSK estándar público", + "channels_usePublicChannel": "Usar Canal Público", + "channels_standardPublicPsk": "PSK estándar público", "channels_pskHex": "PSK (Hex)", "channels_generateRandomPsk": "Generar PSK aleatorio", "channels_enterChannelName": "Por favor, introduce un nombre de canal", "channels_pskMustBe32Hex": "PSK debe ser de 32 caracteres hexadecimales.", - "channels_channelAdded": "Canal \"{name}\" añadido", + "channels_channelAdded": "Canal \"{name}\" añadido", "@channels_channelAdded": { "placeholders": { "name": { @@ -386,7 +386,7 @@ } } }, - "channels_smazCompression": "Compresión SMAZ", + "channels_smazCompression": "Compresión SMAZ", "channels_channelUpdated": "Canal \"{name}\" actualizado", "@channels_channelUpdated": { "placeholders": { @@ -395,13 +395,13 @@ } } }, - "channels_publicChannelAdded": "Canal público añadido", + "channels_publicChannelAdded": "Canal público añadido", "channels_sortBy": "Ordenar por", "channels_sortManual": "Manual", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Últimos mensajes", + "channels_sortLatestMessages": "Últimos mensajes", "channels_sortUnread": "Sin leer", - "chat_noMessages": "Aún no hay mensajes", + "chat_noMessages": "Aún no hay mensajes", "chat_sendMessageToStart": "Enviar un mensaje para comenzar", "chat_originalMessageNotFound": "Mensaje original no encontrado", "chat_replyingTo": "Responder a {name}", @@ -420,7 +420,7 @@ } } }, - "chat_location": "Ubicación", + "chat_location": "Ubicación", "chat_sendMessageTo": "Enviar un mensaje a {contactName}", "@chat_sendMessageTo": { "placeholders": { @@ -430,7 +430,7 @@ } }, "chat_typeMessage": "Escribe un mensaje...", - "chat_messageTooLong": "Mensaje demasiado largo (máximo {maxBytes} bytes).", + "chat_messageTooLong": "Mensaje demasiado largo (máximo {maxBytes} bytes).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -440,7 +440,7 @@ }, "chat_messageCopied": "Mensaje copiado", "chat_messageDeleted": "Mensaje borrado", - "chat_retryingMessage": "Reintentando…", + "chat_retryingMessage": "Reintentando…", "chat_retryCount": "Reintentar {current}/{max}", "@chat_retryCount": { "placeholders": { @@ -454,7 +454,7 @@ }, "chat_sendGif": "Enviar GIF", "chat_reply": "Responder", - "chat_addReaction": "Añadir Reacción", + "chat_addReaction": "Añadir Reacción", "chat_me": "Yo", "emojiCategorySmileys": "Emoticones", "emojiCategoryGestures": "Gestos", @@ -466,18 +466,18 @@ "gifPicker_noGifsFound": "No se encontraron GIFs", "gifPicker_failedLoad": "No se pudo cargar los GIFs", "gifPicker_failedSearch": "No se encontraron GIFs", - "gifPicker_noInternet": "No hay conexión a internet", - "debugLog_appTitle": "Registro de Depuración de la App", - "debugLog_bleTitle": "Registro de Depuración BLE", + "gifPicker_noInternet": "No hay conexión a internet", + "debugLog_appTitle": "Registro de Depuración de la App", + "debugLog_bleTitle": "Registro de Depuración BLE", "debugLog_copyLog": "Copiar registro", "debugLog_clearLog": "Borrar registro", - "debugLog_copied": "Registro de depuración copiado", + "debugLog_copied": "Registro de depuración copiado", "debugLog_bleCopied": "Registro BLE copiado", - "debugLog_noEntries": "Aún no hay registros de depuración.", - "debugLog_enableInSettings": "Habilitar el registro de depuración de la aplicación en la configuración", + "debugLog_noEntries": "Aún no hay registros de depuración.", + "debugLog_enableInSettings": "Habilitar el registro de depuración de la aplicación en la configuración", "debugLog_frames": "Marcos", "debugLog_rawLogRx": "Registro Crudo-RX", - "debugLog_noBleActivity": "Aún no hay actividad BLE", + "debugLog_noBleActivity": "Aún no hay actividad BLE", "debugFrame_length": "Longitud del Marco: {count} bytes", "@debugFrame_length": { "placeholders": { @@ -541,12 +541,12 @@ } }, "debugFrame_hexDump": "Mapeo Hexadecimal:", - "chat_pathManagement": "Gestión de Rutas", + "chat_pathManagement": "Gestión de Rutas", "chat_routingMode": "Modo de enrutamiento", "chat_autoUseSavedPath": "Auto (usar la ruta guardada)", - "chat_forceFloodMode": "Modo Inundación Forzado", + "chat_forceFloodMode": "Modo Inundación Forzado", "chat_recentAckPaths": "Rutas de ACK Recientes (tocar para usar):", - "chat_pathHistoryFull": "El historial de rutas está completo. Eliminar entradas para añadir nuevas.", + "chat_pathHistoryFull": "El historial de rutas está completo. Eliminar entradas para añadir nuevas.", "chat_hopSingular": "salta", "chat_hopPlural": "salta", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -557,19 +557,19 @@ } } }, - "chat_successes": "Éxitos", + "chat_successes": "Éxitos", "chat_removePath": "Eliminar ruta", - "chat_noPathHistoryYet": "Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.", + "chat_noPathHistoryYet": "Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.", "chat_pathActions": "Acciones de Ruta:", "chat_setCustomPath": "Establecer Ruta Personalizada", "chat_setCustomPathSubtitle": "Especificar manualmente la ruta de enrutamiento", "chat_clearPath": "Limpiar Ruta", - "chat_clearPathSubtitle": "Forzar redescubrimiento en el próximo envío", - "chat_pathCleared": "Ruta eliminada. El siguiente mensaje redescubrirá la ruta.", + "chat_clearPathSubtitle": "Forzar redescubrimiento en el próximo envío", + "chat_pathCleared": "Ruta eliminada. El siguiente mensaje redescubrirá la ruta.", "chat_floodModeSubtitle": "Utilizar el interruptor de enrutamiento en la barra de herramientas", - "chat_floodModeEnabled": "El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.", + "chat_floodModeEnabled": "El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.", "chat_fullPath": "Ruta completa", - "chat_pathDetailsNotAvailable": "Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.", + "chat_pathDetailsNotAvailable": "Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.", "chat_pathSetHops": "Ruta establecida: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -581,14 +581,14 @@ } } }, - "chat_pathSavedLocally": "Guardado localmente. Conéctate para sincronizar.", + "chat_pathSavedLocally": "Guardado localmente. Conéctate para sincronizar.", "chat_pathDeviceConfirmed": "Dispositivo confirmado.", - "chat_pathDeviceNotConfirmed": "Dispositivo aún no confirmado.", + "chat_pathDeviceNotConfirmed": "Dispositivo aún no confirmado.", "chat_type": "Escribe", "chat_path": "Ruta", - "chat_publicKey": "Clave Pública", + "chat_publicKey": "Clave Pública", "chat_compressOutgoingMessages": "Comprimir mensajes salientes", - "chat_floodForced": "Inundación (forzada)", + "chat_floodForced": "Inundación (forzada)", "chat_directForced": "Directo (forzado)", "chat_hopsForced": "{count} saltos (forzados)", "@chat_hopsForced": { @@ -598,9 +598,9 @@ } } }, - "chat_floodAuto": "Inundación (automática)", + "chat_floodAuto": "Inundación (automática)", "chat_direct": "Guardar", - "chat_poiShared": "Punto de Interés Compartido", + "chat_poiShared": "Punto de Interés Compartido", "chat_unread": "Sin leer: {count}", "@chat_unread": { "placeholders": { @@ -609,8 +609,8 @@ } } }, - "chat_openLink": "¿Abrir enlace?", - "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?", + "chat_openLink": "¿Abrir enlace?", + "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?", "chat_open": "Abrir", "chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}", "@chat_couldNotOpenLink": { @@ -620,9 +620,9 @@ } } }, - "chat_invalidLink": "Formato de enlace no válido", + "chat_invalidLink": "Formato de enlace no válido", "map_title": "Mapa de Nodos", - "map_noNodesWithLocation": "No hay nodos con datos de ubicación", + "map_noNodesWithLocation": "No hay nodos con datos de ubicación", "map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa", "map_nodesCount": "Nodos: {count}", "@map_nodesCount": { @@ -642,25 +642,25 @@ }, "map_chat": "Chat", "map_repeater": "Repetidor", - "map_room": "Habitación", + "map_room": "Habitación", "map_sensor": "Sensor", "map_pinDm": "Pin (DM)", "map_pinPrivate": "Bloqueo (Privado)", - "map_pinPublic": "Clave (Pública)", - "map_lastSeen": "Última vez que se vio", - "map_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", + "map_pinPublic": "Clave (Pública)", + "map_lastSeen": "Última vez que se vio", + "map_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", "map_from": "De", "map_source": "Fuente", "map_flags": "Banderas", - "map_shareMarkerHere": "Compartir marcador aquí", + "map_shareMarkerHere": "Compartir marcador aquí", "map_pinLabel": "Etiqueta de marcador", "map_label": "Etiqueta", - "map_pointOfInterest": "Punto de interés", + "map_pointOfInterest": "Punto de interés", "map_sendToContact": "Enviar a contacto", "map_sendToChannel": "Enviar a canal", "map_noChannelsAvailable": "No hay canales disponibles", - "map_publicLocationShare": "Compartir ubicación pública", - "map_publicLocationShareConfirm": "Estás a punto de compartir una ubicación en {channelLabel}. Este canal es público y cualquiera con la PSK puede verlo.", + "map_publicLocationShare": "Compartir ubicación pública", + "map_publicLocationShareConfirm": "Estás a punto de compartir una ubicación en {channelLabel}. Este canal es público y cualquiera con la PSK puede verlo.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,7 +668,7 @@ } } }, - "map_connectToShareMarkers": "Conéctate a un dispositivo para compartir marcadores", + "map_connectToShareMarkers": "Conéctate a un dispositivo para compartir marcadores", "map_filterNodes": "Filtrar Nodos", "map_nodeTypes": "Tipos de nodo", "map_chatNodes": "Nodos de Chat", @@ -676,18 +676,18 @@ "map_otherNodes": "Otros Nodos", "map_keyPrefix": "Prefijo de clave", "map_filterByKeyPrefix": "Filtrar por prefijo clave", - "map_publicKeyPrefix": "Prefijo de clave pública", + "map_publicKeyPrefix": "Prefijo de clave pública", "map_markers": "Marcadores", "map_showSharedMarkers": "Mostrar marcadores compartidos", - "map_lastSeenTime": "Última vez que se vio", + "map_lastSeenTime": "Última vez que se vio", "map_sharedPin": "Pin compartido", - "map_joinRoom": "Únete a la sala", + "map_joinRoom": "Únete a la sala", "map_manageRepeater": "Gestionar Repetidor", - "mapCache_title": "Caché de Mapa Offline", - "mapCache_selectAreaFirst": "Seleccionar un área para cachear primero", - "mapCache_noTilesToDownload": "No hay azulejos para descargar para este área.", + "mapCache_title": "Caché de Mapa Offline", + "mapCache_selectAreaFirst": "Seleccionar un área para cachear primero", + "mapCache_noTilesToDownload": "No hay azulejos para descargar para este área.", "mapCache_downloadTilesTitle": "Descargar ficheros", - "mapCache_downloadTilesPrompt": "Descargar {count} ficheros para usar sin conexión?", + "mapCache_downloadTilesPrompt": "Descargar {count} ficheros para usar sin conexión?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -715,11 +715,11 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Borrar caché offline", - "mapCache_clearOfflineCachePrompt": "Eliminar todas las baldosas en caché del mapa?", - "mapCache_offlineCacheCleared": "Almacén en caché sin conexión eliminado", - "mapCache_noAreaSelected": "No se ha seleccionado ningún área", - "mapCache_cacheArea": "Área de Caché", + "mapCache_clearOfflineCacheTitle": "Borrar caché offline", + "mapCache_clearOfflineCachePrompt": "Eliminar todas las baldosas en caché del mapa?", + "mapCache_offlineCacheCleared": "Almacén en caché sin conexión eliminado", + "mapCache_noAreaSelected": "No se ha seleccionado ningún área", + "mapCache_cacheArea": "Área de Caché", "mapCache_useCurrentView": "Usar Vista Actual", "mapCache_zoomRange": "Rango de Zoom", "mapCache_estimatedTiles": "Tiles estimados: {count}", @@ -742,7 +742,7 @@ } }, "mapCache_downloadTilesButton": "Descargar Mosaicos", - "mapCache_clearCacheButton": "Borrar Caché", + "mapCache_clearCacheButton": "Borrar Caché", "mapCache_failedDownloads": "Descargas fallidas: {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} días hace", + "time_daysAgo": "{days} días hace", "@time_daysAgo": { "placeholders": { "days": { @@ -795,8 +795,8 @@ }, "time_hour": "hora", "time_hours": "horas", - "time_day": "día", - "time_days": "días", + "time_day": "día", + "time_days": "días", "time_week": "semana", "time_weeks": "semanas", "time_month": "mes", @@ -804,21 +804,21 @@ "time_minutes": "minutos", "time_allTime": "Todas las veces", "dialog_disconnect": "Desconectar", - "dialog_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", - "login_repeaterLogin": "Iniciar sesión en el Repetidor", + "dialog_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", + "login_repeaterLogin": "Iniciar sesión en el Repetidor", "login_roomLogin": "Inicio de Sala", - "login_password": "Contraseña", - "login_enterPassword": "Introducir contraseña", - "login_savePassword": "Guardar contraseña", - "login_savePasswordSubtitle": "La contraseña se almacenará de forma segura en este dispositivo.", - "login_repeaterDescription": "Ingrese la contraseña del repetidor para acceder a la configuración y el estado.", - "login_roomDescription": "Ingrese la contraseña de la sala para acceder a la configuración y el estado.", + "login_password": "Contraseña", + "login_enterPassword": "Introducir contraseña", + "login_savePassword": "Guardar contraseña", + "login_savePasswordSubtitle": "La contraseña se almacenará de forma segura en este dispositivo.", + "login_repeaterDescription": "Ingrese la contraseña del repetidor para acceder a la configuración y el estado.", + "login_roomDescription": "Ingrese la contraseña de la sala para acceder a la configuración y el estado.", "login_routing": "Enrutamiento", "login_routingMode": "Modo de enrutamiento", "login_autoUseSavedPath": "Auto (usar la ruta guardada)", - "login_forceFloodMode": "Activar Modo Inundación Forzada", + "login_forceFloodMode": "Activar Modo Inundación Forzada", "login_managePaths": "Gestionar Rutas", - "login_login": "Iniciar sesión", + "login_login": "Iniciar sesión", "login_attempt": "Intentar {current}/{max}", "@login_attempt": { "placeholders": { @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", + "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", "common_reload": "Recargar", "common_clear": "Borrar", "path_currentPath": "Ruta actual: {path}", @@ -860,13 +860,13 @@ "path_enterCustomPath": "Introducir Ruta Personalizada", "path_currentPathLabel": "Ruta actual", "path_hexPrefixInstructions": "Introduzca los prefijos hexadecimales de 2 caracteres para cada salto, separados por comas.", - "path_hexPrefixExample": "Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).", + "path_hexPrefixExample": "Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).", "path_labelHexPrefixes": "Prefijos hexadecimales", - "path_helperMaxHops": "Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).", + "path_helperMaxHops": "Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).", "path_selectFromContacts": "O seleccionar de contactos:", "path_noRepeatersFound": "No se encontraron repetidores ni servidores de sala.", "path_customPathsRequire": "Las rutas personalizadas requieren saltos intermedios que pueden transmitir mensajes.", - "path_invalidHexPrefixes": "Prefijos hexadecimales inválidos: {prefixes}", + "path_invalidHexPrefixes": "Prefijos hexadecimales inválidos: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,25 +874,25 @@ } } }, - "path_tooLong": "La ruta es demasiado larga. Se permiten un máximo de 64 saltos.", + "path_tooLong": "La ruta es demasiado larga. Se permiten un máximo de 64 saltos.", "path_setPath": "Establecer Ruta", - "repeater_management": "Gestión de Repetidores", - "repeater_managementTools": "Herramientas de Gestión", + "repeater_management": "Gestión de Repetidores", + "repeater_managementTools": "Herramientas de Gestión", "repeater_status": "Estado", - "repeater_statusSubtitle": "Ver el estado, las estadísticas y los vecinos del repetidor", + "repeater_statusSubtitle": "Ver el estado, las estadísticas y los vecinos del repetidor", "repeater_telemetry": "Telemetry", - "repeater_telemetrySubtitle": "Ver la telemetría de los sensores y las estadísticas del sistema", + "repeater_telemetrySubtitle": "Ver la telemetría de los sensores y las estadísticas del sistema", "repeater_cli": "CLI", "repeater_cliSubtitle": "Enviar comandos al repetidor", - "repeater_settings": "Configuración", - "repeater_settingsSubtitle": "Configurar parámetros del repetidor", + "repeater_settings": "Configuración", + "repeater_settingsSubtitle": "Configurar parámetros del repetidor", "repeater_statusTitle": "Estado del Repetidor", "repeater_routingMode": "Modo de enrutamiento", "repeater_autoUseSavedPath": "Auto (usar la ruta guardada)", - "repeater_forceFloodMode": "Modo Inundación Forzado", - "repeater_pathManagement": "Gestión de rutas", + "repeater_forceFloodMode": "Modo Inundación Forzado", + "repeater_pathManagement": "Gestión de rutas", "repeater_refresh": "Actualizar", - "repeater_statusRequestTimeout": "Solicitud de estado caducó.", + "repeater_statusRequestTimeout": "Solicitud de estado caducó.", "repeater_errorLoadingStatus": "Error al cargar el estado: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -901,23 +901,23 @@ } } }, - "repeater_systemInformation": "Información del sistema", - "repeater_battery": "Batería", - "repeater_clockAtLogin": "Reloj (al inicio de sesión)", + "repeater_systemInformation": "Información del sistema", + "repeater_battery": "Batería", + "repeater_clockAtLogin": "Reloj (al inicio de sesión)", "repeater_uptime": "Tiempo de actividad", "repeater_queueLength": "Longitud de la cola", - "repeater_debugFlags": "Marcadores de Depuración", - "repeater_radioStatistics": "Estadísticas de Radio", - "repeater_lastRssi": "Último RSSI", - "repeater_lastSnr": "Último SNR", + "repeater_debugFlags": "Marcadores de Depuración", + "repeater_radioStatistics": "Estadísticas de Radio", + "repeater_lastRssi": "Último RSSI", + "repeater_lastSnr": "Último SNR", "repeater_noiseFloor": "Nivel de Ruido", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Estadísticas del Paquete", + "repeater_packetStatistics": "Estadísticas del Paquete", "repeater_sent": "Enviado", "repeater_received": "Recibido", "repeater_duplicates": "Duplicados", - "repeater_daysHoursMinsSecs": "{days} días {hours}h {minutes}m {seconds}s", + "repeater_daysHoursMinsSecs": "{days} días {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", + "repeater_packetTxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", + "repeater_packetRxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Inundación: {flood}, Directo: {direct}", + "repeater_duplicatesFloodDirect": "Inundación: {flood}, Directo: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,35 +981,35 @@ } } }, - "repeater_settingsTitle": "Configuración del Repetidor", - "repeater_basicSettings": "Configuración Básica", + "repeater_settingsTitle": "Configuración del Repetidor", + "repeater_basicSettings": "Configuración Básica", "repeater_repeaterName": "Nombre del Repetidor", "repeater_repeaterNameHelper": "Mostrar nombre para este repetidor", - "repeater_adminPassword": "Contraseña de Administrador", - "repeater_adminPasswordHelper": "Contraseña de acceso completo", - "repeater_guestPassword": "Contraseña de invitado", - "repeater_guestPasswordHelper": "Acceso de solo lectura con contraseña", - "repeater_radioSettings": "Configuración de Radio", + "repeater_adminPassword": "Contraseña de Administrador", + "repeater_adminPasswordHelper": "Contraseña de acceso completo", + "repeater_guestPassword": "Contraseña de invitado", + "repeater_guestPasswordHelper": "Acceso de solo lectura con contraseña", + "repeater_radioSettings": "Configuración de Radio", "repeater_frequencyMhz": "Frecuencia (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Potencia", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Ancho de banda", - "repeater_spreadingFactor": "Factor de propagación", - "repeater_codingRate": "Tasa de Programación", - "repeater_locationSettings": "Configuración de Ubicación", + "repeater_spreadingFactor": "Factor de propagación", + "repeater_codingRate": "Tasa de Programación", + "repeater_locationSettings": "Configuración de Ubicación", "repeater_latitude": "Latitud", "repeater_latitudeHelper": "Grados decimales (por ejemplo, 37.7749)", "repeater_longitude": "Longitud", "repeater_longitudeHelper": "Grados decimales (por ejemplo, -122.4194)", - "repeater_features": "Características", + "repeater_features": "Características", "repeater_packetForwarding": "Enrutamiento de Paquetes", "repeater_packetForwardingSubtitle": "Habilitar el repetidor para reenviar paquetes", "repeater_guestAccess": "Acceso de Invitados", "repeater_guestAccessSubtitle": "Permitir acceso de invitado en solo lectura", "repeater_privacyMode": "Modo Privacidad", - "repeater_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", - "repeater_advertisementSettings": "Configuración de Anuncios", + "repeater_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", + "repeater_advertisementSettings": "Configuración de Anuncios", "repeater_localAdvertInterval": "Intervalo de Anuncio Local", "repeater_localAdvertIntervalMinutes": "{minutes} minutos", "@repeater_localAdvertIntervalMinutes": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervalo de Anuncio de Inundación", + "repeater_floodAdvertInterval": "Intervalo de Anuncio de Inundación", "repeater_floodAdvertIntervalHours": "{hours} horas", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1032,14 +1032,14 @@ "repeater_dangerZone": "Zona de Peligro", "repeater_rebootRepeater": "Reiniciar Repetidor", "repeater_rebootRepeaterSubtitle": "Reiniciar el dispositivo repetidor", - "repeater_rebootRepeaterConfirm": "¿Está seguro de que desea reiniciar este repetidor?", + "repeater_rebootRepeaterConfirm": "¿Está seguro de que desea reiniciar este repetidor?", "repeater_regenerateIdentityKey": "Regenerar Clave de Identidad", - "repeater_regenerateIdentityKeySubtitle": "Generar nueva pareja de clave pública/privada", - "repeater_regenerateIdentityKeyConfirm": "Esto generará una nueva identidad para el repetidor. Continuar?", + "repeater_regenerateIdentityKeySubtitle": "Generar nueva pareja de clave pública/privada", + "repeater_regenerateIdentityKeyConfirm": "Esto generará una nueva identidad para el repetidor. Continuar?", "repeater_eraseFileSystem": "Borrar Sistema de Archivos", "repeater_eraseFileSystemSubtitle": "Formatear el sistema de archivos del repetidor", - "repeater_eraseFileSystemConfirm": "ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!", - "repeater_eraseSerialOnly": "Borrar solo está disponible a través de la consola serial.", + "repeater_eraseFileSystemConfirm": "ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!", + "repeater_eraseSerialOnly": "Borrar solo está disponible a través de la consola serial.", "repeater_commandSent": "Comando enviado: {command}", "@repeater_commandSent": { "placeholders": { @@ -1058,7 +1058,7 @@ }, "repeater_confirm": "Confirmar", "repeater_settingsSaved": "Guardado de ajustes exitoso", - "repeater_errorSavingSettings": "Error al guardar la configuración: {error}", + "repeater_errorSavingSettings": "Error al guardar la configuración: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,14 +1066,14 @@ } } }, - "repeater_refreshBasicSettings": "Actualizar Configuración Básica", + "repeater_refreshBasicSettings": "Actualizar Configuración Básica", "repeater_refreshRadioSettings": "Actualizar Ajustes de Radio", - "repeater_refreshTxPower": "Actualizar TX de energía", - "repeater_refreshLocationSettings": "Actualizar Configuración de Ubicación", + "repeater_refreshTxPower": "Actualizar TX de energía", + "repeater_refreshLocationSettings": "Actualizar Configuración de Ubicación", "repeater_refreshPacketForwarding": "Actualizar Enrutamiento de Paquetes", "repeater_refreshGuestAccess": "Actualizar Acceso Invitados", "repeater_refreshPrivacyMode": "Actualizar Modo Privacidad", - "repeater_refreshAdvertisementSettings": "Actualizar Configuración de Anuncios", + "repeater_refreshAdvertisementSettings": "Actualizar Configuración de Anuncios", "repeater_refreshed": "{label} actualizado", "@repeater_refreshed": { "placeholders": { @@ -1091,11 +1091,11 @@ } }, "repeater_cliTitle": "Repetidor CLI", - "repeater_debugNextCommand": "Siguiente Comando de Depuración", + "repeater_debugNextCommand": "Siguiente Comando de Depuración", "repeater_commandHelp": "Ayuda", "repeater_clearHistory": "Borrar historial", - "repeater_noCommandsSent": "Aún no se han enviado comandos.", - "repeater_typeCommandOrUseQuick": "Escriba un comando a continuación o use comandos rápidos", + "repeater_noCommandsSent": "Aún no se han enviado comandos.", + "repeater_typeCommandOrUseQuick": "Escriba un comando a continuación o use comandos rápidos", "repeater_enterCommandHint": "Escribir comando...", "repeater_previousCommand": "Comando anterior", "repeater_nextCommand": "Siguiente comando", @@ -1113,77 +1113,77 @@ "repeater_cliQuickGetRadio": "Obtener Radio", "repeater_cliQuickGetTx": "Obtener TX", "repeater_cliQuickNeighbors": "Vecinos", - "repeater_cliQuickVersion": "Versión", + "repeater_cliQuickVersion": "Versión", "repeater_cliQuickAdvertise": "Anunciar", "repeater_cliQuickClock": "Reloj", - "repeater_cliHelpAdvert": "Envía un paquete de publicidad", + "repeater_cliHelpAdvert": "Envía un paquete de publicidad", "repeater_cliHelpReboot": "Reinicia el dispositivo. (ten en cuenta, es normal que aparezca 'Timeout')", - "repeater_cliHelpClock": "Muestra la hora actual según el reloj del dispositivo.", - "repeater_cliHelpPassword": "Establece una nueva contraseña de administrador para el dispositivo.", - "repeater_cliHelpVersion": "Muestra la versión del dispositivo y la fecha de compilación del firmware.", - "repeater_cliHelpClearStats": "Reinicia varios contadores de estadísticas a cero.", + "repeater_cliHelpClock": "Muestra la hora actual según el reloj del dispositivo.", + "repeater_cliHelpPassword": "Establece una nueva contraseña de administrador para el dispositivo.", + "repeater_cliHelpVersion": "Muestra la versión del dispositivo y la fecha de compilación del firmware.", + "repeater_cliHelpClearStats": "Reinicia varios contadores de estadísticas a cero.", "repeater_cliHelpSetAf": "Establece el factor de tiempo de aire.", - "repeater_cliHelpSetTx": "Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).", + "repeater_cliHelpSetTx": "Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).", "repeater_cliHelpSetRepeat": "Habilita o deshabilita el rol del repetidor para este nodo.", - "repeater_cliHelpSetAllowReadOnly": "(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).", - "repeater_cliHelpSetFloodMax": "Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).", - "repeater_cliHelpSetIntThresh": "Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.", - "repeater_cliHelpSetAgcResetInterval": "Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.", - "repeater_cliHelpSetMultiAcks": "Habilita o deshabilita la función de 'ACKs dobles'.", + "repeater_cliHelpSetAllowReadOnly": "(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).", + "repeater_cliHelpSetFloodMax": "Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).", + "repeater_cliHelpSetIntThresh": "Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.", + "repeater_cliHelpSetAgcResetInterval": "Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.", + "repeater_cliHelpSetMultiAcks": "Habilita o deshabilita la función de 'ACKs dobles'.", "repeater_cliHelpSetAdvertInterval": "Establece el intervalo del temporizador en minutos para enviar un paquete de anuncio local (sin salto). Establecer en 0 para desactivarlo.", "repeater_cliHelpSetFloodAdvertInterval": "Establece el intervalo del temporizador en horas para enviar un paquete de anuncio masivo. Establecer en 0 para desactivarlo.", - "repeater_cliHelpSetGuestPassword": "Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")", + "repeater_cliHelpSetGuestPassword": "Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")", "repeater_cliHelpSetName": "Establece el nombre del anuncio.", "repeater_cliHelpSetLat": "Establece la latitud del mapa de publicidad. (grados decimales)", "repeater_cliHelpSetLon": "Establece la longitud del mapa de la publicidad. (grados decimales)", - "repeater_cliHelpSetRadio": "Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.", - "repeater_cliHelpSetRxDelay": "Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.", - "repeater_cliHelpSetTxDelay": "Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).", + "repeater_cliHelpSetRadio": "Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.", + "repeater_cliHelpSetRxDelay": "Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.", + "repeater_cliHelpSetTxDelay": "Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).", "repeater_cliHelpSetDirectTxDelay": "Igual que txdelay, pero para aplicar un retraso aleatorio a la transferencia de paquetes en modo directo.", "repeater_cliHelpSetBridgeEnabled": "Habilitar/Deshabilitar puente.", "repeater_cliHelpSetBridgeDelay": "Establecer retraso antes de retransmitir paquetes.", - "repeater_cliHelpSetBridgeSource": "Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.", + "repeater_cliHelpSetBridgeSource": "Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.", "repeater_cliHelpSetBridgeBaud": "Establecer la velocidad de baudios del enlace serial para los puentes rs232.", "repeater_cliHelpSetBridgeSecret": "Establecer secreto de puente para puentes espnow.", - "repeater_cliHelpSetAdcMultiplier": "Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).", - "repeater_cliHelpTempRadio": "Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).", - "repeater_cliHelpSetPerm": "Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).", + "repeater_cliHelpSetAdcMultiplier": "Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).", + "repeater_cliHelpTempRadio": "Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).", + "repeater_cliHelpSetPerm": "Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).", "repeater_cliHelpGetBridgeType": "Obtiene tipo de puente ninguno, rs232, espnow", "repeater_cliHelpLogStart": "Inicia el registro de paquetes en el sistema de archivos.", "repeater_cliHelpLogStop": "Detener el registro de paquetes al sistema de archivos.", "repeater_cliHelpLogErase": "Elimina los registros del paquete del sistema de archivos.", - "repeater_cliHelpNeighbors": "Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4", + "repeater_cliHelpNeighbors": "Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4", "repeater_cliHelpNeighborRemove": "Elimina la primera entrada coincidente (por prefijo de pubkey (hex)) de la lista de vecinos.", - "repeater_cliHelpRegion": "(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.", - "repeater_cliHelpRegionLoad": "NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.", - "repeater_cliHelpRegionGet": "Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) 'F'\"", - "repeater_cliHelpRegionPut": "Agrega o actualiza una definición de región con el nombre dado.", - "repeater_cliHelpRegionRemove": "Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)", - "repeater_cliHelpRegionAllowf": "Establece el permiso de 'F'lujo para la región dada. ('' para el ámbito global/legado)", - "repeater_cliHelpRegionDenyf": "Elimina el permiso de 'F'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)", - "repeater_cliHelpRegionHome": "Responde con la región 'home' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).", - "repeater_cliHelpRegionHomeSet": "Establece la región 'hogar'.", + "repeater_cliHelpRegion": "(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.", + "repeater_cliHelpRegionLoad": "NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.", + "repeater_cliHelpRegionGet": "Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) 'F'\"", + "repeater_cliHelpRegionPut": "Agrega o actualiza una definición de región con el nombre dado.", + "repeater_cliHelpRegionRemove": "Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)", + "repeater_cliHelpRegionAllowf": "Establece el permiso de 'F'lujo para la región dada. ('' para el ámbito global/legado)", + "repeater_cliHelpRegionDenyf": "Elimina el permiso de 'F'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)", + "repeater_cliHelpRegionHome": "Responde con la región 'home' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).", + "repeater_cliHelpRegionHomeSet": "Establece la región 'hogar'.", "repeater_cliHelpRegionSave": "Persiste la lista/mapa de regiones al almacenamiento.", - "repeater_cliHelpGps": "Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.", + "repeater_cliHelpGps": "Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.", "repeater_cliHelpGpsOnOff": "Activa o desactiva el modo GPS.", "repeater_cliHelpGpsSync": "Sincroniza la hora del nodo con el reloj GPS.", - "repeater_cliHelpGpsSetLoc": "Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.", - "repeater_cliHelpGpsAdvert": "Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias", - "repeater_cliHelpGpsAdvertSet": "Configura la configuración de la publicidad de la ubicación.", + "repeater_cliHelpGpsSetLoc": "Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.", + "repeater_cliHelpGpsAdvert": "Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias", + "repeater_cliHelpGpsAdvertSet": "Configura la configuración de la publicidad de la ubicación.", "repeater_commandsListTitle": "Lista de comandos", - "repeater_commandsListNote": "NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".", + "repeater_commandsListNote": "NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".", "repeater_general": "General", - "repeater_settingsCategory": "Configuración", + "repeater_settingsCategory": "Configuración", "repeater_bridge": "Puente", "repeater_logging": "Registrando", "repeater_neighborsRepeaterOnly": "Vecinos (solo repetidor)", - "repeater_regionManagementRepeaterOnly": "Gestión de Regiones (solo Repetidor)", - "repeater_regionNote": "Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.", - "repeater_gpsManagement": "Gestión de GPS", - "repeater_gpsNote": "Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.", - "telemetry_receivedData": "Datos de Telemetría Recibidos", - "telemetry_requestTimeout": "Solicitud de telemetría ha expirado.", - "telemetry_errorLoading": "Error al cargar la telemetría: {error}", + "repeater_regionManagementRepeaterOnly": "Gestión de Regiones (solo Repetidor)", + "repeater_regionNote": "Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.", + "repeater_gpsManagement": "Gestión de GPS", + "repeater_gpsNote": "Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.", + "telemetry_receivedData": "Datos de Telemetría Recibidos", + "telemetry_requestTimeout": "Solicitud de telemetría ha expirado.", + "telemetry_errorLoading": "Error al cargar la telemetría: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "No hay datos de telemetría disponibles.", + "telemetry_noData": "No hay datos de telemetría disponibles.", "telemetry_channelTitle": "Canal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1200,7 +1200,7 @@ } } }, - "telemetry_batteryLabel": "Batería", + "telemetry_batteryLabel": "Batería", "telemetry_voltageLabel": "Voltaje", "telemetry_mcuTemperatureLabel": "Temperatura del MCU", "telemetry_temperatureLabel": "Temperatura", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1247,14 +1247,14 @@ "channelPath_viewMap": "Ver mapa", "channelPath_otherObservedPaths": "Otros caminos observados", "channelPath_repeaterHops": "Saltos del Repetidor", - "channelPath_noHopDetails": "Los detalles del paquete no están disponibles.", + "channelPath_noHopDetails": "Los detalles del paquete no están disponibles.", "channelPath_messageDetails": "Detalles del mensaje", "channelPath_senderLabel": "Remitente", "channelPath_timeLabel": "Tiempo", "channelPath_repeatsLabel": "Repetir", "channelPath_pathLabel": "Ruta {index}", "channelPath_observedLabel": "Observado", - "channelPath_observedPathTitle": "Ruta observada {index} • {hops}", + "channelPath_observedPathTitle": "Ruta observada {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "No datos de ubicación", + "channelPath_noLocationData": "No datos de ubicación", "channelPath_timeWithDate": "{day}/{month} a las {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,7 +1289,7 @@ } }, "channelPath_unknownPath": "Desconocido", - "channelPath_floodPath": "Inundación", + "channelPath_floodPath": "Inundación", "channelPath_directPath": "Guardar", "channelPath_observedZeroOf": "0 de {total} saltos", "@channelPath_observedZeroOf": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Ruta", "channelPath_observedPathHeader": "Ruta Observada", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1344,7 +1344,7 @@ "channelPath_unknownRepeater": "Repetidor Desconocido", "listFilter_tooltip": "Filtrar y ordenar", "listFilter_sortBy": "Ordenar por", - "listFilter_latestMessages": "Últimos mensajes", + "listFilter_latestMessages": "Últimos mensajes", "listFilter_heardRecently": "Escuchado recientemente", "listFilter_az": "A-Z", "listFilter_filters": "Filtros", @@ -1368,16 +1368,16 @@ "neighbors_errorLoading": "Error al cargar vecinos: {error}", "neighbors_repeatersNeighbors": "Repetidores Vecinos", "neighbors_noData": "No hay datos de vecinos disponibles.", - "channels_joinPrivateChannel": "Únete a un Canal Privado", + "channels_joinPrivateChannel": "Únete a un Canal Privado", "channels_createPrivateChannel": "Crear un Canal Privado", "channels_createPrivateChannelDesc": "Cifrado con una clave secreta.", "channels_joinPrivateChannelDesc": "Introducir manualmente una clave secreta.", - "channels_joinPublicChannel": "Únete al Canal Público", + "channels_joinPublicChannel": "Únete al Canal Público", "channels_joinPublicChannelDesc": "Cualquiera puede unirse a este canal.", - "channels_joinHashtagChannel": "Únete a un Canal con Hashtag", + "channels_joinHashtagChannel": "Únete a un Canal con Hashtag", "channels_joinHashtagChannelDesc": "Cualquiera puede unirse a los canales de hashtag.", - "channels_scanQrCode": "Escanear un Código QR", - "channels_scanQrCodeComingSoon": "Próximamente", + "channels_scanQrCode": "Escanear un Código QR", + "channels_scanQrCodeComingSoon": "Próximamente", "channels_enterHashtag": "Introducir hashtag", "channels_hashtagHint": "ej. #equipo", "@neighbors_unknownContact": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_unknownContact": "Clave pública desconocida {pubkey}", - "neighbors_heardAgo": "Escuchado: {time} hace atrás", + "neighbors_unknownContact": "Clave pública desconocida {pubkey}", + "neighbors_heardAgo": "Escuchado: {time} hace atrás", "settings_locationGPSEnable": "Habilitar GPS", - "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", + "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.", - "contacts_manageRoom": "Gestionar Servidor de Habitación", - "room_management": "Administración del Servidor de Habitación", + "contacts_manageRoom": "Gestionar Servidor de Habitación", + "room_management": "Administración del Servidor de Habitación", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1459,35 +1459,35 @@ } }, "community_create": "Crear Comunidad", - "community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.", + "community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.", "community_title": "Comunidad", - "community_join": "Únete", - "community_joinTitle": "Únete a la comunidad", - "community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?", - "community_scanQr": "Escanear Código QR de la Comunidad", - "community_scanInstructions": "Apunte la cámara a un código QR de la comunidad", - "community_showQr": "Mostrar Código QR", - "community_publicChannel": "Comunidad Pública", + "community_join": "Únete", + "community_joinTitle": "Únete a la comunidad", + "community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?", + "community_scanQr": "Escanear Código QR de la Comunidad", + "community_scanInstructions": "Apunte la cámara a un código QR de la comunidad", + "community_showQr": "Mostrar Código QR", + "community_publicChannel": "Comunidad Pública", "community_hashtagChannel": "Hashtag de la Comunidad", "community_name": "Nombre de la comunidad", "common_ok": "De acuerdo", "community_enterName": "Introducir nombre de comunidad", "community_created": "Comunidad \"{name}\" creada", - "community_joined": "Se unió a la comunidad \"{name}\"", + "community_joined": "Se unió a la comunidad \"{name}\"", "community_qrTitle": "Compartir Comunidad", - "community_qrInstructions": "Escanear este código QR para unirte a {name}", + "community_qrInstructions": "Escanear este código QR para unirte a {name}", "community_hashtagPrivacyHint": "Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad", - "community_invalidQrCode": "Código QR de comunidad no válido", + "community_invalidQrCode": "Código QR de comunidad no válido", "community_alreadyMember": "Ya eres Miembro", "community_alreadyMemberMessage": "Ya eres miembro de \"{name}\".", - "community_addPublicChannel": "Añadir Canal Público de la Comunidad", - "community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.", - "community_noCommunities": "Aún no se han unido comunidades.", - "community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar", + "community_addPublicChannel": "Añadir Canal Público de la Comunidad", + "community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.", + "community_noCommunities": "Aún no se han unido comunidades.", + "community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar", "community_manageCommunities": "Gestionar Comunidades", "community_delete": "Salir de la Comunidad", - "community_deleteConfirm": "¿Salir de \"{name}\"?", - "community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.", + "community_deleteConfirm": "¿Salir de \"{name}\"?", + "community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1496,11 +1496,11 @@ } }, "community_deleted": "Has salido de la comunidad \"{name}\"", - "community_addHashtagChannel": "Añadir Hashtag de la Comunidad", - "community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad", + "community_addHashtagChannel": "Añadir Hashtag de la Comunidad", + "community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad", "community_selectCommunity": "Seleccionar Comunidad", "community_regularHashtag": "Etiqueta de Hashtag Regular", - "community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)", + "community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)", "community_communityHashtag": "Hashtag de la Comunidad", "community_communityHashtagDesc": "Exclusivo para miembros de la comunidad", "community_forCommunity": "Para {name}", @@ -1532,13 +1532,13 @@ } } }, - "community_regenerateSecret": "Regenerar Contraseña Secreta", - "community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.", - "community_secretRegenerated": "Código secreto regenerado para \"{name}\"", + "community_regenerateSecret": "Regenerar Contraseña Secreta", + "community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.", + "community_secretRegenerated": "Código secreto regenerado para \"{name}\"", "community_regenerate": "Regenerar", "community_secretUpdated": "Confidencialidad actualizada para \"{name}\"", - "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"", - "community_updateSecret": "Actualizar Contraseña", + "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"", + "community_updateSecret": "Actualizar Contraseña", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1546,34 +1546,34 @@ } } }, - "pathTrace_you": "Tú", - "pathTrace_failed": "El trazado de ruta falló.", + "pathTrace_you": "Tú", + "pathTrace_failed": "El trazado de ruta falló.", "pathTrace_refreshTooltip": "Actualizar Path Trace", "contacts_pathTrace": "Rastreo de caminos", "contacts_repeaterPathTrace": "Rastrear ruta al repetidor", "contacts_repeaterPing": "Pingar repetidor", "contacts_ping": "Ping", - "pathTrace_notAvailable": "El trazado de ruta no está disponible.", + "pathTrace_notAvailable": "El trazado de ruta no está disponible.", "contacts_roomPing": "Pingar servidor de sala", - "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", + "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", "contacts_pathTraceTo": "Rastrear ruta a {name}", "contacts_chatTraceRoute": "Ruta de trazado", "appSettings_languageUk": "Ucraniano", - "contacts_clipboardEmpty": "El portapapeles está vacío.", + "contacts_clipboardEmpty": "El portapapeles está vacío.", "appSettings_languageRu": "Ruso", "appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes", "appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes", - "contacts_invalidAdvertFormat": "Datos de contacto no válidos", - "contacts_floodAdvert": "Anuncio de inundación", + "contacts_invalidAdvertFormat": "Datos de contacto no válidos", + "contacts_floodAdvert": "Anuncio de inundación", "contacts_contactImported": "El contacto ha sido importado.", - "contacts_contactImportFailed": "Contacto no se importó correctamente.", + "contacts_contactImportFailed": "Contacto no se importó correctamente.", "contacts_zeroHopAdvert": "Anuncio de Zero Hop", "contacts_ShareContactZeroHop": "Compartir contacto por anuncio", "contacts_ShareContact": "Copiar contacto al Portapapeles", "contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles", "contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles", "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", - "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", + "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", "notification_activityTitle": "Actividad de MeshCore", @@ -1610,46 +1610,46 @@ } }, "notification_receivedNewMessage": "Nuevo mensaje recibido", - "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", + "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", - "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", + "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", "settings_gpxExportNoContacts": "No hay contactos para exportar.", "settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO", "settings_gpxExportError": "Hubo un error al exportar.", - "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.", - "settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.", + "settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.", "settings_gpxExportAll": "Exportar todos los contactos a GPX", - "settings_gpxExportContacts": "Exportar compañeros a GPX", - "settings_gpxExportChat": "Ubicaciones de compañero", + "settings_gpxExportContacts": "Exportar compañeros a GPX", + "settings_gpxExportChat": "Ubicaciones de compañero", "settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala", "settings_gpxExportAllContacts": "Todas las ubicaciones de contactos", "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", - "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación", + "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", + "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación", "pathTrace_clearTooltip": "Borrar ruta", "map_runTrace": "Ejecutar Rastreo de Ruta", "map_tapToAdd": "Pulse en los nodos para agregarlos al camino.", - "map_removeLast": "Eliminar último", + "map_removeLast": "Eliminar último", "map_pathTraceCancelled": "Rastreo de ruta cancelado.", "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", "scanner_chromeRequired": "Navegador Chrome requerido", - "scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.", - "scanner_bluetoothOff": "Bluetooth está desactivado.", + "scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.", + "scanner_bluetoothOff": "Bluetooth está desactivado.", "scanner_enableBluetooth": "Habilitar Bluetooth", "snrIndicator_nearByRepeaters": "Repetidores cercanos", - "snrIndicator_lastSeen": "Visto por última vez", + "snrIndicator_lastSeen": "Visto por última vez", "chat_ShowAllPaths": "Mostrar todos los caminos", - "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", - "settings_clientRepeat": "Repetir sin conexión", + "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", + "settings_clientRepeat": "Repetir sin conexión", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.", - "settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)", + "settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Unidades", - "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsMetric": "Métrico (m/km)", "appSettings_unitsImperial": "Imperial (pies/millas)", - "map_lineOfSight": "Línea de visión", - "map_losScreenTitle": "Línea de visión", + "map_lineOfSight": "Línea de visión", + "map_losScreenTitle": "Línea de visión", "losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.", - "losRunFailed": "Error en la comprobación de la línea de visión: {error}", + "losRunFailed": "Error en la comprobación de la línea de visión: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1658,10 +1658,10 @@ } }, "losClearAllPoints": "Borrar todos los puntos", - "losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación", - "losMenuTitle": "Menú LOS", + "losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación", + "losMenuTitle": "Menú LOS", "losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados", - "losShowDisplayNodes": "Mostrar nodos de visualización", + "losShowDisplayNodes": "Mostrar nodos de visualización", "losCustomPoints": "Puntos personalizados", "losCustomPointLabel": "Personalizado {index}", "@losCustomPointLabel": { @@ -1696,8 +1696,8 @@ } }, "losRun": "Ejecutar LOS", - "losNoElevationData": "Sin datos de elevación", - "losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}", + "losNoElevationData": "Sin datos de elevación", + "losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1750,20 +1750,20 @@ } } }, - "losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.", - "losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.", + "losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.", + "losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.", "losRenameCustomPoint": "Cambiar el nombre del punto personalizado", "losPointName": "Nombre del punto", "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", - "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Horizonte radioeléctrico", - "losLegendLosBeam": "Línea de visión", + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte radioeléctrico", + "losLegendLosBeam": "Línea de visión", "losLegendTerrain": "Terreno", "losFrequencyLabel": "Frecuencia", - "losFrequencyInfoTooltip": "Ver detalles del cálculo", - "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", - "losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", + "losFrequencyInfoTooltip": "Ver detalles del cálculo", + "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", + "losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1783,7 +1783,7 @@ }, "listFilter_favorites": "Favoritos", "listFilter_removeFromFavorites": "Eliminar de las favoritas", - "listFilter_addToFavorites": "Añadir a favoritos", + "listFilter_addToFavorites": "Añadir a favoritos", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1825,18 +1825,16 @@ } }, "contacts_searchContactsNoNumber": "Buscar contactos...", - "contacts_unread": "No leído", + "contacts_unread": "No leído", "contacts_searchFavorites": "Buscar {number}{str} Favoritos...", "contacts_searchUsers": "Buscar {number}{str} Usuarios...", "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...", - "connectionChoiceTitle": "Seleccione su método de conexión.", - "connectionChoiceSubtitle": "Seleccione la forma en que desea acceder a su dispositivo MeshCore.", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", "usbScreenStatus": "Seleccione un dispositivo USB", - "usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.", + "usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.", "usbScreenTitle": "Conecte mediante USB", - "usbScreenSubtitle": "Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.", + "usbScreenSubtitle": "Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.", "usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a cargar." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index aa33073..901f4e6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", +{ + "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -20,22 +20,22 @@ "common_close": "Fermer", "common_edit": "Modifier", "common_add": "Ajouter", - "common_settings": "Paramètres", - "common_disconnect": "Déconnecter", - "common_connected": "Connecté", - "common_disconnected": "Déconnecté", - "common_create": "Créer", + "common_settings": "Paramètres", + "common_disconnect": "Déconnecter", + "common_connected": "Connecté", + "common_disconnected": "Déconnecté", + "common_create": "Créer", "common_continue": "Continuer", "common_share": "Partager", "common_copy": "Copier", - "common_retry": "Réessayer", + "common_retry": "Réessayer", "common_hide": "Masquer", "common_remove": "Supprimer", "common_enable": "Activer", - "common_disable": "Désactiver", - "common_reboot": "Redémarrer", + "common_disable": "Désactiver", + "common_reboot": "Redémarrer", "common_loading": "Chargement...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Recherche de périphériques...", + "scanner_scanning": "Recherche de périphériques...", "scanner_connecting": "Connexion en cours...", - "scanner_disconnecting": "Déconnexion...", - "scanner_notConnected": "Non connecté", - "scanner_connectedTo": "Connecté à {deviceName}", + "scanner_disconnecting": "Déconnexion...", + "scanner_notConnected": "Non connecté", + "scanner_connectedTo": "Connecté à {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -67,7 +67,7 @@ }, "scanner_searchingDevices": "Recherche des appareils MeshCore...", "scanner_tapToScan": "Appuyez sur Scanner pour trouver les appareils MeshCore", - "scanner_connectionFailed": "Échec de la connexion : {error}", + "scanner_connectionFailed": "Échec de la connexion : {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,52 +75,52 @@ } } }, - "scanner_stop": "Arrêter", + "scanner_stop": "Arrêter", "scanner_scan": "Scanner", "device_quickSwitch": "Basculement rapide", "device_meshcore": "MeshCore", - "settings_title": "Paramètres", - "settings_deviceInfo": "Informations du périphérique", - "settings_appSettings": "Paramètres de l'application", - "settings_appSettingsSubtitle": "Notifications, messagerie et préférences de carte", - "settings_nodeSettings": "Paramètres du nœud", - "settings_nodeName": "Nom du nœud", - "settings_nodeNameNotSet": "Non défini", - "settings_nodeNameHint": "Entrer le nom du nœud", - "settings_nodeNameUpdated": "Nom mis à jour", - "settings_radioSettings": "Paramètres Radio", - "settings_radioSettingsSubtitle": "Fréquence, puissance, facteur d'espacement", - "settings_radioSettingsUpdated": "Paramètres radio mis à jour", + "settings_title": "Paramètres", + "settings_deviceInfo": "Informations du périphérique", + "settings_appSettings": "Paramètres de l'application", + "settings_appSettingsSubtitle": "Notifications, messagerie et préférences de carte", + "settings_nodeSettings": "Paramètres du nÅ“ud", + "settings_nodeName": "Nom du nÅ“ud", + "settings_nodeNameNotSet": "Non défini", + "settings_nodeNameHint": "Entrer le nom du nÅ“ud", + "settings_nodeNameUpdated": "Nom mis à jour", + "settings_radioSettings": "Paramètres Radio", + "settings_radioSettingsSubtitle": "Fréquence, puissance, facteur d'espacement", + "settings_radioSettingsUpdated": "Paramètres radio mis à jour", "settings_location": "Emplacement", - "settings_locationSubtitle": "Coordonnées GPS", - "settings_locationUpdated": "Emplacement mis à jour", + "settings_locationSubtitle": "Coordonnées GPS", + "settings_locationUpdated": "Emplacement mis à jour", "settings_locationBothRequired": "Entrez la latitude et la longitude.", "settings_locationInvalid": "Latitude ou longitude invalide.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", - "settings_privacyMode": "Mode de confidentialité", + "settings_privacyMode": "Mode de confidentialité", "settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces", - "settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", - "settings_privacyModeEnabled": "Mode de confidentialité activé", - "settings_privacyModeDisabled": "Mode de confidentialité désactivé", + "settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", + "settings_privacyModeEnabled": "Mode de confidentialité activé", + "settings_privacyModeDisabled": "Mode de confidentialité désactivé", "settings_actions": "Actions", "settings_sendAdvertisement": "S'annoncer", - "settings_sendAdvertisementSubtitle": "Présence diffusée maintenant", - "settings_advertisementSent": "Annonce envoyée", + "settings_sendAdvertisementSubtitle": "Présence diffusée maintenant", + "settings_advertisementSent": "Annonce envoyée", "settings_syncTime": "Temps de synchronisation", - "settings_syncTimeSubtitle": "Définir l'heure de l'appareil sur l'heure du téléphone.", + "settings_syncTimeSubtitle": "Définir l'heure de l'appareil sur l'heure du téléphone.", "settings_timeSynchronized": "Synchronisation temporelle", - "settings_refreshContacts": "Rafraîchir les Contacts", + "settings_refreshContacts": "Rafraîchir les Contacts", "settings_refreshContactsSubtitle": "Recharger la liste des contacts depuis l'appareil", - "settings_rebootDevice": "Redémarrer l'appareil", - "settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore", - "settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.", - "settings_debug": "Déboguer", - "settings_bleDebugLog": "Journal de débogage BLE", - "settings_bleDebugLogSubtitle": "Commandes BLE, réponses et données brutes", - "settings_appDebugLog": "Journal de débogage de l'application", - "settings_appDebugLogSubtitle": "Messages de débogage de l'application", - "settings_about": "À propos", + "settings_rebootDevice": "Redémarrer l'appareil", + "settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore", + "settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.", + "settings_debug": "Déboguer", + "settings_bleDebugLog": "Journal de débogage BLE", + "settings_bleDebugLogSubtitle": "Commandes BLE, réponses et données brutes", + "settings_appDebugLog": "Journal de débogage de l'application", + "settings_appDebugLogSubtitle": "Messages de débogage de l'application", + "settings_about": "À propos", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -130,20 +130,20 @@ } }, "settings_aboutLegalese": "Projet MeshCore Open Source 2026", - "settings_aboutDescription": "Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.", + "settings_aboutDescription": "Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.", "settings_infoName": "Nom", "settings_infoId": "ID", - "settings_infoStatus": "État", + "settings_infoStatus": "État", "settings_infoBattery": "Batterie", - "settings_infoPublicKey": "Clé Publique", + "settings_infoPublicKey": "Clé Publique", "settings_infoContactsCount": "Nombre de contacts", "settings_infoChannelCount": "Nombre de canaux", - "settings_presets": "Préréglages", - "settings_frequency": "Fréquence (MHz)", + "settings_presets": "Préréglages", + "settings_frequency": "Fréquence (MHz)", "settings_frequencyHelper": "300,0 - 2 500,0", - "settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)", + "settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)", "settings_bandwidth": "Bande passante", - "settings_spreadingFactor": "Facteur de répartition", + "settings_spreadingFactor": "Facteur de répartition", "settings_codingRate": "Taux de codage", "settings_txPower": "TX Puissance (dBm)", "settings_txPowerHelper": "0 - 22", @@ -156,51 +156,51 @@ } } }, - "appSettings_title": "Paramètres de l'application", + "appSettings_title": "Paramètres de l'application", "appSettings_appearance": "Apparence", - "appSettings_theme": "Thème", - "appSettings_themeSystem": "Défaut système", - "appSettings_themeLight": "Lumière", + "appSettings_theme": "Thème", + "appSettings_themeSystem": "Défaut système", + "appSettings_themeLight": "Lumière", "appSettings_themeDark": "Sombre", "appSettings_language": "Langue", - "appSettings_languageSystem": "Par défaut du système", + "appSettings_languageSystem": "Par défaut du système", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notifications", "appSettings_enableNotifications": "Activer les Notifications", "appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les annonces", - "appSettings_notificationPermissionDenied": "Permission de notification refusée", - "appSettings_notificationsEnabled": "Notifications activées", - "appSettings_notificationsDisabled": "Notifications désactivées", + "appSettings_notificationPermissionDenied": "Permission de notification refusée", + "appSettings_notificationsEnabled": "Notifications activées", + "appSettings_notificationsDisabled": "Notifications désactivées", "appSettings_messageNotifications": "Notifications de Messages", - "appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages", + "appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages", "appSettings_channelMessageNotifications": "Notifications des Messages de Canal", - "appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal", + "appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal", "appSettings_advertisementNotifications": "Notifications d'annonces", - "appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nœuds", + "appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nÅ“uds", "appSettings_messaging": "Messagerie", "appSettings_clearPathOnMaxRetry": "Effacer le chemin sur Max Retry", - "appSettings_clearPathOnMaxRetrySubtitle": "Réinitialiser le chemin de contact après 5 tentatives d'envoi infructueuses", - "appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.", - "appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.", - "appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique", - "appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)", - "appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée", - "appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée", + "appSettings_clearPathOnMaxRetrySubtitle": "Réinitialiser le chemin de contact après 5 tentatives d'envoi infructueuses", + "appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.", + "appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.", + "appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique", + "appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)", + "appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée", + "appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée", "appSettings_battery": "Batterie", "appSettings_batteryChemistry": "Chimie de la batterie", - "appSettings_batteryChemistryPerDevice": "Définir par appareil ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Définir par appareil ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -213,15 +213,15 @@ "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Affichage de la carte", - "appSettings_showRepeaters": "Afficher les répéteurs", - "appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte", - "appSettings_showChatNodes": "Afficher les nœuds de discussion", - "appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte", - "appSettings_showOtherNodes": "Afficher d'autres nœuds", - "appSettings_showOtherNodesSubtitle": "Afficher d'autres types de nœuds sur la carte", + "appSettings_showRepeaters": "Afficher les répéteurs", + "appSettings_showRepeatersSubtitle": "Afficher les nÅ“uds répéteurs sur la carte", + "appSettings_showChatNodes": "Afficher les nÅ“uds de discussion", + "appSettings_showChatNodesSubtitle": "Afficher les nÅ“uds de chat sur la carte", + "appSettings_showOtherNodes": "Afficher d'autres nÅ“uds", + "appSettings_showOtherNodesSubtitle": "Afficher d'autres types de nÅ“uds sur la carte", "appSettings_timeFilter": "Filtre du temps", - "appSettings_timeFilterShowAll": "Afficher tous les nœuds", - "appSettings_timeFilterShowLast": "Afficher les nœuds des {hours} dernières heures", + "appSettings_timeFilterShowAll": "Afficher tous les nÅ“uds", + "appSettings_timeFilterShowLast": "Afficher les nÅ“uds des {hours} dernières heures", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,15 +230,15 @@ } }, "appSettings_mapTimeFilter": "Filtre du Temps de la Carte", - "appSettings_showNodesDiscoveredWithin": "Afficher les nœuds découverts dans :", + "appSettings_showNodesDiscoveredWithin": "Afficher les nÅ“uds découverts dans :", "appSettings_allTime": "Tout le temps", - "appSettings_lastHour": "Dernière heure", - "appSettings_last6Hours": "Dernières 6 heures", - "appSettings_last24Hours": "Dernières 24 heures", - "appSettings_lastWeek": "La semaine dernière", + "appSettings_lastHour": "Dernière heure", + "appSettings_last6Hours": "Dernières 6 heures", + "appSettings_last24Hours": "Dernières 24 heures", + "appSettings_lastWeek": "La semaine dernière", "appSettings_offlineMapCache": "Cache de Carte Hors Ligne", - "appSettings_noAreaSelected": "Aucune zone sélectionnée", - "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Aucune zone sélectionnée", + "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,17 +249,17 @@ } } }, - "appSettings_debugCard": "Déboguer", - "appSettings_appDebugLogging": "Journalisation de débogage de l'application", - "appSettings_appDebugLoggingSubtitle": "Enregistrez les messages de débogage de l'application Log pour le dépannage.", - "appSettings_appDebugLoggingEnabled": "Journalisation de débogage de l'application activée", - "appSettings_appDebugLoggingDisabled": "Le débogage de l'application est désactivé.", + "appSettings_debugCard": "Déboguer", + "appSettings_appDebugLogging": "Journalisation de débogage de l'application", + "appSettings_appDebugLoggingSubtitle": "Enregistrez les messages de débogage de l'application Log pour le dépannage.", + "appSettings_appDebugLoggingEnabled": "Journalisation de débogage de l'application activée", + "appSettings_appDebugLoggingDisabled": "Le débogage de l'application est désactivé.", "contacts_title": "Contacts", - "contacts_noContacts": "Aucun contact trouvé.", - "contacts_contactsWillAppear": "Les contacts apparaîtront lorsque les appareils font leur annonce.", + "contacts_noContacts": "Aucun contact trouvé.", + "contacts_contactsWillAppear": "Les contacts apparaîtront lorsque les appareils font leur annonce.", "contacts_searchContacts": "Rechercher des contacts...", "contacts_noUnreadContacts": "Aucun contact non lu", - "contacts_noContactsFound": "Aucun contact ou groupe trouvé.", + "contacts_noContactsFound": "Aucun contact ou groupe trouvé.", "contacts_deleteContact": "Supprimer le contact", "contacts_removeConfirm": "Supprimer {contactName} des contacts ?", "@contacts_removeConfirm": { @@ -269,7 +269,7 @@ } } }, - "contacts_manageRepeater": "Gérer le répéteur", + "contacts_manageRepeater": "Gérer le répéteur", "contacts_roomLogin": "Connexion Room Server", "contacts_openChat": "Ouverture du Chat", "contacts_editGroup": "Modifier le groupe", @@ -285,7 +285,7 @@ "contacts_newGroup": "Nouveau Groupe", "contacts_groupName": "Nom du groupe", "contacts_groupNameRequired": "Le nom du groupe est obligatoire.", - "contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.", + "contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,7 +294,7 @@ } }, "contacts_filterContacts": "Filtrer les contacts...", - "contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.", + "contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.", "contacts_noMembers": "Aucun membre", "contacts_lastSeenNow": "Vu maintenant", "contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes", @@ -324,10 +324,10 @@ } }, "channels_title": "Canaux", - "channels_noChannelsConfigured": "Aucun canal configuré", + "channels_noChannelsConfigured": "Aucun canal configuré", "channels_addPublicChannel": "Ajouter un canal public", "channels_searchChannels": "Rechercher des canaux...", - "channels_noChannelsFound": "Aucun canal trouvé", + "channels_noChannelsFound": "Aucun canal trouvé", "channels_channelIndex": "Canal {index}", "@channels_channelIndex": { "placeholders": { @@ -338,14 +338,14 @@ }, "channels_hashtagChannel": "Canal avec hashtag", "channels_public": "Public", - "channels_private": "Privé", + "channels_private": "Privé", "channels_publicChannel": "Canal public", - "channels_privateChannel": "Canal privé", + "channels_privateChannel": "Canal privé", "channels_editChannel": "Modifier le canal", - "channels_muteChannel": "Désactiver les notifications du canal", - "channels_unmuteChannel": "Réactiver les notifications du canal", + "channels_muteChannel": "Désactiver les notifications du canal", + "channels_unmuteChannel": "Réactiver les notifications du canal", "channels_deleteChannel": "Supprimer le canal", - "channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.", + "channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Le canal \"{name}\" a été supprimé", + "channels_channelDeleted": "Le canal \"{name}\" a été supprimé", "@channels_channelDeleted": { "placeholders": { "name": { @@ -367,10 +367,10 @@ "channels_usePublicChannel": "Utiliser le canal public", "channels_standardPublicPsk": "PSK public standard", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Générer une clé de modulation PSK aléatoire", + "channels_generateRandomPsk": "Générer une clé de modulation PSK aléatoire", "channels_enterChannelName": "Veuillez entrer un nom de canal", - "channels_pskMustBe32Hex": "Le PKS doit être composé de 32 caractères hexadécimaux.", - "channels_channelAdded": "Le canal \"{name}\" a été ajouté", + "channels_pskMustBe32Hex": "Le PKS doit être composé de 32 caractères hexadécimaux.", + "channels_channelAdded": "Le canal \"{name}\" a été ajouté", "@channels_channelAdded": { "placeholders": { "name": { @@ -387,7 +387,7 @@ } }, "channels_smazCompression": "Compression SMAZ", - "channels_channelUpdated": "Le canal \"{name}\" a été mis à jour", + "channels_channelUpdated": "Le canal \"{name}\" a été mis à jour", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,16 +395,16 @@ } } }, - "channels_publicChannelAdded": "Le canal public a été ajouté", + "channels_publicChannelAdded": "Le canal public a été ajouté", "channels_sortBy": "Trier par", "channels_sortManual": "Manuel", - "channels_sortAZ": "A à Z", + "channels_sortAZ": "A à Z", "channels_sortLatestMessages": "Derniers messages", "channels_sortUnread": "Non lu", "chat_noMessages": "Aucun message pour le moment.", "chat_sendMessageToStart": "Envoyer un message pour commencer", - "chat_originalMessageNotFound": "Message d'origine non trouvé", - "chat_replyingTo": "Répondre à {name}", + "chat_originalMessageNotFound": "Message d'origine non trouvé", + "chat_replyingTo": "Répondre à {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "Répondre à {name}", + "chat_replyTo": "Répondre à {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,7 +421,7 @@ } }, "chat_location": "Emplacement", - "chat_sendMessageTo": "Envoyer un message à {contactName}", + "chat_sendMessageTo": "Envoyer un message à {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -438,9 +438,9 @@ } } }, - "chat_messageCopied": "Message copié", - "chat_messageDeleted": "Message supprimé", - "chat_retryingMessage": "Tentative de récupération.", + "chat_messageCopied": "Message copié", + "chat_messageDeleted": "Message supprimé", + "chat_retryingMessage": "Tentative de récupération.", "chat_retryCount": "Essai {current}/{max}", "@chat_retryCount": { "placeholders": { @@ -453,32 +453,32 @@ } }, "chat_sendGif": "Envoyer GIF", - "chat_reply": "Répondre", - "chat_addReaction": "Ajouter une Réaction", + "chat_reply": "Répondre", + "chat_addReaction": "Ajouter une Réaction", "chat_me": "Moi", - "emojiCategorySmileys": "Émojis", + "emojiCategorySmileys": "Émojis", "emojiCategoryGestures": "Gestes", - "emojiCategoryHearts": "Cœurs", + "emojiCategoryHearts": "CÅ“urs", "emojiCategoryObjects": "Objets", "gifPicker_title": "Choisir un GIF", "gifPicker_searchHint": "Rechercher des GIF...", - "gifPicker_poweredBy": "Propulsé par GIPHY", - "gifPicker_noGifsFound": "Aucun GIF trouvé", + "gifPicker_poweredBy": "Propulsé par GIPHY", + "gifPicker_noGifsFound": "Aucun GIF trouvé", "gifPicker_failedLoad": "Impossible de charger les GIFs", - "gifPicker_failedSearch": "Recherche de GIFs échouée", + "gifPicker_failedSearch": "Recherche de GIFs échouée", "gifPicker_noInternet": "Aucune connexion internet", - "debugLog_appTitle": "Journal de débogage de l'application", - "debugLog_bleTitle": "Journal de débogage BLE", + "debugLog_appTitle": "Journal de débogage de l'application", + "debugLog_bleTitle": "Journal de débogage BLE", "debugLog_copyLog": "Copier le journal", "debugLog_clearLog": "Effacer le journal", - "debugLog_copied": "Journal de débogage copié", - "debugLog_bleCopied": "Journal BLE copié", - "debugLog_noEntries": "Aucun journal de débogage pour le moment.", - "debugLog_enableInSettings": "Activer le débogage de l'application dans les paramètres", + "debugLog_copied": "Journal de débogage copié", + "debugLog_bleCopied": "Journal BLE copié", + "debugLog_noEntries": "Aucun journal de débogage pour le moment.", + "debugLog_enableInSettings": "Activer le débogage de l'application dans les paramètres", "debugLog_frames": "Cadres", "debugLog_rawLogRx": "Enregistrement brut - RX", - "debugLog_noBleActivity": "Pas d'activité BLE enregistrée pour le moment.", - "debugFrame_length": "Longueur du cadre : {count} octets", + "debugLog_noBleActivity": "Pas d'activité BLE enregistrée pour le moment.", + "debugFrame_length": "Longueur du cadre : {count} octets", "@debugFrame_length": { "placeholders": { "count": { @@ -519,7 +519,7 @@ } } }, - "debugFrame_textType": "- Type de texte : {type} ({label})", + "debugFrame_textType": "- Type de texte : {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -540,13 +540,13 @@ } } }, - "debugFrame_hexDump": "Vidéo de Dump Hexadécimal :", + "debugFrame_hexDump": "Vidéo de Dump Hexadécimal :", "chat_pathManagement": "Gestion des chemins", "chat_routingMode": "Mode de routage", - "chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", - "chat_forceFloodMode": "Mode tout le réseau forcé", - "chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :", - "chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.", + "chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", + "chat_forceFloodMode": "Mode tout le réseau forcé", + "chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :", + "chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.", "chat_hopSingular": "saut", "chat_hopPlural": "sauts", "chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}", @@ -557,20 +557,20 @@ } } }, - "chat_successes": "Succès", + "chat_successes": "Succès", "chat_removePath": "Supprimer le chemin", - "chat_noPathHistoryYet": "Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.", - "chat_pathActions": "Actions du chemin :", - "chat_setCustomPath": "Définir un chemin personnalisé", - "chat_setCustomPathSubtitle": "Spécifier manuellement le chemin de routage", + "chat_noPathHistoryYet": "Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.", + "chat_pathActions": "Actions du chemin :", + "chat_setCustomPath": "Définir un chemin personnalisé", + "chat_setCustomPathSubtitle": "Spécifier manuellement le chemin de routage", "chat_clearPath": "Effacer le chemin", - "chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi", - "chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.", + "chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi", + "chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.", "chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application", - "chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.", + "chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.", "chat_fullPath": "Chemin complet", - "chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.", - "chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.", + "chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Sauvegardé localement. Connectez-vous pour synchroniser.", - "chat_pathDeviceConfirmed": "Appareil confirmé.", - "chat_pathDeviceNotConfirmed": "L'appareil n'a pas encore été confirmé.", + "chat_pathSavedLocally": "Sauvegardé localement. Connectez-vous pour synchroniser.", + "chat_pathDeviceConfirmed": "Appareil confirmé.", + "chat_pathDeviceNotConfirmed": "L'appareil n'a pas encore été confirmé.", "chat_type": "Saisir", "chat_path": "Chemin", - "chat_publicKey": "Clé Publique", + "chat_publicKey": "Clé Publique", "chat_compressOutgoingMessages": "Compresser les messages sortants", - "chat_floodForced": "Tout le réseau (forcée)", - "chat_directForced": "Direct (forcé)", - "chat_hopsForced": "{count} sauts (forcés)", + "chat_floodForced": "Tout le réseau (forcée)", + "chat_directForced": "Direct (forcé)", + "chat_hopsForced": "{count} sauts (forcés)", "@chat_hopsForced": { "placeholders": { "count": { @@ -598,9 +598,9 @@ } } }, - "chat_floodAuto": "Tout le réseau (auto)", + "chat_floodAuto": "Tout le réseau (auto)", "chat_direct": "Afficher", - "chat_poiShared": "Point d'intérêt Partagé", + "chat_poiShared": "Point d'intérêt Partagé", "chat_unread": "Non lu : {count}", "@chat_unread": { "placeholders": { @@ -621,10 +621,10 @@ } }, "chat_invalidLink": "Format de lien invalide", - "map_title": "Carte des nœuds", - "map_noNodesWithLocation": "Aucun nœud avec des données de localisation", - "map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.", - "map_nodesCount": "Nœuds : {count}", + "map_title": "Carte des nÅ“uds", + "map_noNodesWithLocation": "Aucun nÅ“ud avec des données de localisation", + "map_nodesNeedGps": "Les nÅ“uds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.", + "map_nodesCount": "NÅ“uds : {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -641,26 +641,26 @@ } }, "map_chat": "Chat", - "map_repeater": "Répéteur", + "map_repeater": "Répéteur", "map_room": "Salle", "map_sensor": "Capteur", - "map_pinDm": "Clé (DM)", - "map_pinPrivate": "Verrouiller (Privé)", - "map_pinPublic": "Clé (Public)", - "map_lastSeen": "Dernière fois vu", - "map_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", - "map_from": "À partir de", + "map_pinDm": "Clé (DM)", + "map_pinPrivate": "Verrouiller (Privé)", + "map_pinPublic": "Clé (Public)", + "map_lastSeen": "Dernière fois vu", + "map_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", + "map_from": "À partir de", "map_source": "Source", "map_flags": "Drapeaux", "map_shareMarkerHere": "Partager le marqueur ici", - "map_pinLabel": "Étiquete de repin", - "map_label": "Étiquette", - "map_pointOfInterest": "Point d'intérêt", + "map_pinLabel": "Étiquete de repin", + "map_label": "Étiquette", + "map_pointOfInterest": "Point d'intérêt", "map_sendToContact": "Envoyer au contact", "map_sendToChannel": "Envoyer sur le canal", "map_noChannelsAvailable": "Aucun canal disponible", "map_publicLocationShare": "Partager dans un lieu public", - "map_publicLocationShareConfirm": "Vous êtes sur le point de partager un emplacement dans {channelLabel}. Ce canal est public et toute personne disposant de la clé PSK peut le voir.", + "map_publicLocationShareConfirm": "Vous êtes sur le point de partager un emplacement dans {channelLabel}. Ce canal est public et toute personne disposant de la clé PSK peut le voir.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Connectez-vous à un appareil pour partager des marqueurs", - "map_filterNodes": "Filtrer les nœuds", - "map_nodeTypes": "Types de nœuds", - "map_chatNodes": "Nœuds de Chat", - "map_repeaters": "Répéteurs", - "map_otherNodes": "Autres nœuds", - "map_keyPrefix": "Préfixe clé", - "map_filterByKeyPrefix": "Filtrer par préfixe de clé", - "map_publicKeyPrefix": "Préfixe de clé publique", + "map_connectToShareMarkers": "Connectez-vous à un appareil pour partager des marqueurs", + "map_filterNodes": "Filtrer les nÅ“uds", + "map_nodeTypes": "Types de nÅ“uds", + "map_chatNodes": "NÅ“uds de Chat", + "map_repeaters": "Répéteurs", + "map_otherNodes": "Autres nÅ“uds", + "map_keyPrefix": "Préfixe clé", + "map_filterByKeyPrefix": "Filtrer par préfixe de clé", + "map_publicKeyPrefix": "Préfixe de clé publique", "map_markers": "Marqueurs", - "map_showSharedMarkers": "Afficher les marqueurs partagés", - "map_lastSeenTime": "Dernière fois vu", - "map_sharedPin": "Clé partagée", + "map_showSharedMarkers": "Afficher les marqueurs partagés", + "map_lastSeenTime": "Dernière fois vu", + "map_sharedPin": "Clé partagée", "map_joinRoom": "Rejoindre la salle", - "map_manageRepeater": "Gérer le répéteur", + "map_manageRepeater": "Gérer le répéteur", "mapCache_title": "Cache de Carte Hors Ligne", - "mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier", - "mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.", - "mapCache_downloadTilesTitle": "Télécharger les tuiles", - "mapCache_downloadTilesPrompt": "Télécharger {count} tuiles pour un usage hors ligne ?", + "mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier", + "mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.", + "mapCache_downloadTilesTitle": "Télécharger les tuiles", + "mapCache_downloadTilesPrompt": "Télécharger {count} tuiles pour un usage hors ligne ?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,7 +695,7 @@ } } }, - "mapCache_downloadAction": "Télécharger", + "mapCache_downloadAction": "Télécharger", "mapCache_cachedTiles": "Cachez {count} tuiles", "@mapCache_cachedTiles": { "placeholders": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)", + "mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -716,9 +716,9 @@ } }, "mapCache_clearOfflineCacheTitle": "Vider le cache hors ligne", - "mapCache_clearOfflineCachePrompt": "Supprimer toutes les tuiles de carte mises en cache ?", - "mapCache_offlineCacheCleared": "Le cache hors ligne a été effacé.", - "mapCache_noAreaSelected": "Aucune zone sélectionnée", + "mapCache_clearOfflineCachePrompt": "Supprimer toutes les tuiles de carte mises en cache ?", + "mapCache_offlineCacheCleared": "Le cache hors ligne a été effacé.", + "mapCache_noAreaSelected": "Aucune zone sélectionnée", "mapCache_cacheArea": "Zone de cache", "mapCache_useCurrentView": "Utiliser la Vue Actuelle", "mapCache_zoomRange": "Plage de zoom", @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Téléchargé {completed} / {total}", + "mapCache_downloadedTiles": "Téléchargé {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "Télécharger les tuiles", + "mapCache_downloadTilesButton": "Télécharger les tuiles", "mapCache_clearCacheButton": "Vider le Cache", - "mapCache_failedDownloads": "Téléchargements échoués : {count}", + "mapCache_failedDownloads": "Téléchargements échoués : {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -803,21 +803,21 @@ "time_months": "mois", "time_minutes": "minutes", "time_allTime": "Tout le temps", - "dialog_disconnect": "Déconnecter", - "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", - "login_repeaterLogin": "Connexion au répéteur", + "dialog_disconnect": "Déconnecter", + "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", + "login_repeaterLogin": "Connexion au répéteur", "login_roomLogin": "Connexion Room Server", "login_password": "Mot de passe", "login_enterPassword": "Entrez votre mot de passe", "login_savePassword": "Sauvegarder le mot de passe", - "login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.", - "login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.", - "login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.", + "login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.", + "login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.", + "login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.", "login_routing": "Redirection", "login_routingMode": "Mode de routage", - "login_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", - "login_forceFloodMode": "Mode tout le réseau forcé", - "login_managePaths": "Gérer les chemins", + "login_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", + "login_forceFloodMode": "Mode tout le réseau forcé", + "login_managePaths": "Gérer les chemins", "login_login": "Connexion", "login_attempt": "Essayer {current}/{max}", "@login_attempt": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Connexion échouée : {error}", + "login_failed": "Connexion échouée : {error}", "@login_failed": { "placeholders": { "error": { @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.", + "login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.", "common_reload": "Recharger", "common_clear": "Effacer", "path_currentPath": "Chemin actuel : {path}", @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Entrer un chemin personnalisé", + "path_enterCustomPath": "Entrer un chemin personnalisé", "path_currentPathLabel": "Chemin actuel", - "path_hexPrefixInstructions": "Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.", - "path_hexPrefixExample": "Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).", - "path_labelHexPrefixes": "Préfixes hexadécimaux", - "path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)", - "path_selectFromContacts": "Sélectionner à partir des contacts :", - "path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.", - "path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.", - "path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}", + "path_hexPrefixInstructions": "Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.", + "path_hexPrefixExample": "Exemple : A1,F2,3C (chaque nÅ“ud utilise le premier octet de sa clé publique).", + "path_labelHexPrefixes": "Préfixes hexadécimaux", + "path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)", + "path_selectFromContacts": "Sélectionner à partir des contacts :", + "path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.", + "path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.", + "path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,25 +874,25 @@ } } }, - "path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.", - "path_setPath": "Définir le chemin", - "repeater_management": "Gestion des répéteurs", + "path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.", + "path_setPath": "Définir le chemin", + "repeater_management": "Gestion des répéteurs", "repeater_managementTools": "Outils de Gestion", - "repeater_status": "État", - "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur", - "repeater_telemetry": "Télémetrie", - "repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système", + "repeater_status": "État", + "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur", + "repeater_telemetry": "Télémetrie", + "repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Envoyer des commandes au répéteur", - "repeater_settings": "Paramètres", - "repeater_settingsSubtitle": "Configurer les paramètres du répéteur", - "repeater_statusTitle": "État du répéteur", + "repeater_cliSubtitle": "Envoyer des commandes au répéteur", + "repeater_settings": "Paramètres", + "repeater_settingsSubtitle": "Configurer les paramètres du répéteur", + "repeater_statusTitle": "État du répéteur", "repeater_routingMode": "Mode de routage", - "repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", - "repeater_forceFloodMode": "Mode tout le réseau forcé", + "repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", + "repeater_forceFloodMode": "Mode tout le réseau forcé", "repeater_pathManagement": "Gestion des chemins", - "repeater_refresh": "Rafraîchir", - "repeater_statusRequestTimeout": "Demande de statut délai dépassé.", + "repeater_refresh": "Rafraîchir", + "repeater_statusRequestTimeout": "Demande de statut délai dépassé.", "repeater_errorLoadingStatus": "Erreur lors du chargement du statut : {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -901,12 +901,12 @@ } } }, - "repeater_systemInformation": "Informations Système", + "repeater_systemInformation": "Informations Système", "repeater_battery": "Batterie", - "repeater_clockAtLogin": "Horloge (au démarrage)", - "repeater_uptime": "Disponibilité", + "repeater_clockAtLogin": "Horloge (au démarrage)", + "repeater_uptime": "Disponibilité", "repeater_queueLength": "Longueur de la file d'attente", - "repeater_debugFlags": "Marqueurs de débogage", + "repeater_debugFlags": "Marqueurs de débogage", "repeater_radioStatistics": "Statistiques Radio", "repeater_lastRssi": "Dernier RSSI", "repeater_lastSnr": "Dernier SNR", @@ -914,8 +914,8 @@ "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", "repeater_packetStatistics": "Statistiques des paquets", - "repeater_sent": "Envoyé", - "repeater_received": "Reçu", + "repeater_sent": "Envoyé", + "repeater_received": "Reçu", "repeater_duplicates": "Doublons", "repeater_daysHoursMinsSecs": "{days} jours {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", + "repeater_packetTxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", + "repeater_packetRxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Tout le réseau : {flood}, Direct : {direct}", + "repeater_duplicatesFloodDirect": "Tout le réseau : {flood}, Direct : {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,35 +981,35 @@ } } }, - "repeater_settingsTitle": "Paramètres du répéteur", - "repeater_basicSettings": "Paramètres de base", - "repeater_repeaterName": "Nom du répéteur", - "repeater_repeaterNameHelper": "Afficher le nom de ce répéteur", + "repeater_settingsTitle": "Paramètres du répéteur", + "repeater_basicSettings": "Paramètres de base", + "repeater_repeaterName": "Nom du répéteur", + "repeater_repeaterNameHelper": "Afficher le nom de ce répéteur", "repeater_adminPassword": "Mot de passe Administrateur", - "repeater_adminPasswordHelper": "Mot de passe d'accès complet", - "repeater_guestPassword": "Mot de passe invité", - "repeater_guestPasswordHelper": "Accès en lecture seule avec mot de passe", - "repeater_radioSettings": "Paramètres Radio", - "repeater_frequencyMhz": "Fréquence (MHz)", + "repeater_adminPasswordHelper": "Mot de passe d'accès complet", + "repeater_guestPassword": "Mot de passe invité", + "repeater_guestPasswordHelper": "Accès en lecture seule avec mot de passe", + "repeater_radioSettings": "Paramètres Radio", + "repeater_frequencyMhz": "Fréquence (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Puissance", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Bande passante", - "repeater_spreadingFactor": "Facteur de répartition", + "repeater_spreadingFactor": "Facteur de répartition", "repeater_codingRate": "Taux de codage", - "repeater_locationSettings": "Paramètres de localisation", + "repeater_locationSettings": "Paramètres de localisation", "repeater_latitude": "Latitude", - "repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)", + "repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)", "repeater_longitude": "Longitude", - "repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)", - "repeater_features": "Fonctionnalités", + "repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)", + "repeater_features": "Fonctionnalités", "repeater_packetForwarding": "Transfert de paquets", - "repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets", - "repeater_guestAccess": "Accès Invité", - "repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule", - "repeater_privacyMode": "Mode de confidentialité", + "repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets", + "repeater_guestAccess": "Accès Invité", + "repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule", + "repeater_privacyMode": "Mode de confidentialité", "repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces", - "repeater_advertisementSettings": "Paramètres d'annonces", + "repeater_advertisementSettings": "Paramètres d'annonces", "repeater_localAdvertInterval": "Intervalle des annonces Locale (0 saut)", "repeater_localAdvertIntervalMinutes": "{minutes} minutes", "@repeater_localAdvertIntervalMinutes": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervalle des annonces à tout le réseau (flood)", + "repeater_floodAdvertInterval": "Intervalle des annonces à tout le réseau (flood)", "repeater_floodAdvertIntervalHours": "{hours} heures", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées", + "repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées", "repeater_dangerZone": "Zone dangereuse", - "repeater_rebootRepeater": "Redémarrer Répéteur", - "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur", - "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?", - "repeater_regenerateIdentityKey": "Ré générer la clé d'identité", - "repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée", - "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?", - "repeater_eraseFileSystem": "Supprimer le système de fichiers", - "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur", - "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !", - "repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.", - "repeater_commandSent": "Commande envoyée : {command}", + "repeater_rebootRepeater": "Redémarrer Répéteur", + "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur", + "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?", + "repeater_regenerateIdentityKey": "Ré générer la clé d'identité", + "repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée", + "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?", + "repeater_eraseFileSystem": "Supprimer le système de fichiers", + "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur", + "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !", + "repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.", + "repeater_commandSent": "Commande envoyée : {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1057,8 +1057,8 @@ } }, "repeater_confirm": "Confirmer", - "repeater_settingsSaved": "Les paramètres ont été enregistrés avec succès.", - "repeater_errorSavingSettings": "Erreur lors de la sauvegarde des paramètres : {error}", + "repeater_settingsSaved": "Les paramètres ont été enregistrés avec succès.", + "repeater_errorSavingSettings": "Erreur lors de la sauvegarde des paramètres : {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "Rafraîchir les paramètres de base", - "repeater_refreshRadioSettings": "Rafraîchir les paramètres Radio", - "repeater_refreshTxPower": "Rafraîchir la tension TX", - "repeater_refreshLocationSettings": "Rafraîchir les paramètres de localisation", - "repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets", - "repeater_refreshGuestAccess": "Rafraîchir l'accès invité", - "repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité", - "repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres des annonces", - "repeater_refreshed": "{label} rafraîchi", + "repeater_refreshBasicSettings": "Rafraîchir les paramètres de base", + "repeater_refreshRadioSettings": "Rafraîchir les paramètres Radio", + "repeater_refreshTxPower": "Rafraîchir la tension TX", + "repeater_refreshLocationSettings": "Rafraîchir les paramètres de localisation", + "repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets", + "repeater_refreshGuestAccess": "Rafraîchir l'accès invité", + "repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité", + "repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres des annonces", + "repeater_refreshed": "{label} rafraîchi", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Erreur lors du rafraîchissement de {label}", + "repeater_errorRefreshing": "Erreur lors du rafraîchissement de {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1090,14 +1090,14 @@ } } }, - "repeater_cliTitle": "Répéteur CLI", - "repeater_debugNextCommand": "Déboguer Prochaine Commande", + "repeater_cliTitle": "Répéteur CLI", + "repeater_debugNextCommand": "Déboguer Prochaine Commande", "repeater_commandHelp": "Aide", "repeater_clearHistory": "Effacer l'historique", - "repeater_noCommandsSent": "Aucune commande n'a encore été envoyée.", + "repeater_noCommandsSent": "Aucune commande n'a encore été envoyée.", "repeater_typeCommandOrUseQuick": "Saisissez une commande ci-dessous ou utilisez les commandes rapides", "repeater_enterCommandHint": "Entrer la commande...", - "repeater_previousCommand": "Commande précédente", + "repeater_previousCommand": "Commande précédente", "repeater_nextCommand": "Prochaine commande", "repeater_enterCommandFirst": "Entrez d'abord une commande", "repeater_cliCommandFrameTitle": "Frame de commande CLI", @@ -1117,73 +1117,73 @@ "repeater_cliQuickAdvertise": "Publier", "repeater_cliQuickClock": "Horloge", "repeater_cliHelpAdvert": "Envoie un paquet d'annonce", - "repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)", + "repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)", "repeater_cliHelpClock": "Affiche l'heure actuelle par l'horloge de chaque appareil.", - "repeater_cliHelpPassword": "Définit un nouveau mot de passe administrateur pour l'appareil.", - "repeater_cliHelpVersion": "Affiche la version du périphérique et la date de construction du micrologiciel.", - "repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.", - "repeater_cliHelpSetAf": "Définit le facteur de temps d'air.", - "repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).", - "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.", - "repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", - "repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).", - "repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.", - "repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.", - "repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».", - "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.", - "repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.", - "repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")", - "repeater_cliHelpSetName": "Définit le nom de l'annonce.", - "repeater_cliHelpSetLat": "Définit la latitude de la carte des annonces. (degrés décimaux)", - "repeater_cliHelpSetLon": "Définit la longitude de la carte de l'annonce. (degrés décimaux)", - "repeater_cliHelpSetRadio": "Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.", - "repeater_cliHelpSetRxDelay": "Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.", - "repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).", - "repeater_cliHelpSetDirectTxDelay": "Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.", - "repeater_cliHelpSetBridgeEnabled": "Activer/Désactiver le pont.", - "repeater_cliHelpSetBridgeDelay": "Définir le délai avant de renvoyer les paquets.", - "repeater_cliHelpSetBridgeSource": "Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.", - "repeater_cliHelpSetBridgeBaud": "Définir la vitesse de communication série pour les ponts Rs232.", - "repeater_cliHelpSetBridgeSecret": "Définir le secret du pont pour les ponts espnow.", - "repeater_cliHelpSetAdcMultiplier": "Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).", - "repeater_cliHelpTempRadio": "Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d'origine. (ne sauvegarde pas dans les préférences).", - "repeater_cliHelpSetPerm": "Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).", + "repeater_cliHelpPassword": "Définit un nouveau mot de passe administrateur pour l'appareil.", + "repeater_cliHelpVersion": "Affiche la version du périphérique et la date de construction du micrologiciel.", + "repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.", + "repeater_cliHelpSetAf": "Définit le facteur de temps d'air.", + "repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).", + "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nÅ“ud.", + "repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", + "repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).", + "repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.", + "repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.", + "repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».", + "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.", + "repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.", + "repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")", + "repeater_cliHelpSetName": "Définit le nom de l'annonce.", + "repeater_cliHelpSetLat": "Définit la latitude de la carte des annonces. (degrés décimaux)", + "repeater_cliHelpSetLon": "Définit la longitude de la carte de l'annonce. (degrés décimaux)", + "repeater_cliHelpSetRadio": "Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.", + "repeater_cliHelpSetRxDelay": "Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.", + "repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).", + "repeater_cliHelpSetDirectTxDelay": "Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.", + "repeater_cliHelpSetBridgeEnabled": "Activer/Désactiver le pont.", + "repeater_cliHelpSetBridgeDelay": "Définir le délai avant de renvoyer les paquets.", + "repeater_cliHelpSetBridgeSource": "Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.", + "repeater_cliHelpSetBridgeBaud": "Définir la vitesse de communication série pour les ponts Rs232.", + "repeater_cliHelpSetBridgeSecret": "Définir le secret du pont pour les ponts espnow.", + "repeater_cliHelpSetAdcMultiplier": "Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).", + "repeater_cliHelpTempRadio": "Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d'origine. (ne sauvegarde pas dans les préférences).", + "repeater_cliHelpSetPerm": "Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).", "repeater_cliHelpGetBridgeType": "Obtenir le type de pont : aucun, rs232, espnow", - "repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.", - "repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.", - "repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.", - "repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", - "repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.", - "repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).", - "repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.", - "repeater_cliHelpRegionGet": "Recherche la région avec le préfixe de nom donné (ou \"\" pour l'étendue globale). Répond avec \"-> nom-de-région (nom-parent) 'F'\"", - "repeater_cliHelpRegionPut": "Ajoute ou met à jour une définition de région avec le nom donné.", - "repeater_cliHelpRegionRemove": "Supprime une définition de région avec le nom donné.", - "repeater_cliHelpRegionAllowf": "Définit les autorisations de \"Flot\" pour la région donnée. ('' pour la portée globale/héritée)", - "repeater_cliHelpRegionDenyf": "Supprime l'autorisation 'F'lood' pour la région donnée. (NOTE : à ce stade, il n'est pas conseillé de l'utiliser sur l'étendue globale/héritée !! )", - "repeater_cliHelpRegionHome": "Répond avec la région 'maison' actuelle. (Note appliquée nulle part pour l'instant, réservée à une utilisation future)", - "repeater_cliHelpRegionHomeSet": "Définit la région 'maison'.", - "repeater_cliHelpRegionSave": "Conserve la liste/la carte des régions dans le stockage.", - "repeater_cliHelpGps": "Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.", - "repeater_cliHelpGpsOnOff": "Activer/désactiver le GPS.", - "repeater_cliHelpGpsSync": "Synchronise l'heure du nœud avec l'horloge GPS.", - "repeater_cliHelpGpsSetLoc": "Définit la position du nœud aux coordonnées GPS et enregistre les préférences.", - "repeater_cliHelpGpsAdvert": "Donne la configuration de l'annonce de la localisation du nœud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences", - "repeater_cliHelpGpsAdvertSet": "Définit la configuration de l'annonce de localisation.", + "repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.", + "repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.", + "repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.", + "repeater_cliHelpNeighbors": "Affiche une liste d'autres nÅ“uds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", + "repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.", + "repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).", + "repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.", + "repeater_cliHelpRegionGet": "Recherche la région avec le préfixe de nom donné (ou \"\" pour l'étendue globale). Répond avec \"-> nom-de-région (nom-parent) 'F'\"", + "repeater_cliHelpRegionPut": "Ajoute ou met à jour une définition de région avec le nom donné.", + "repeater_cliHelpRegionRemove": "Supprime une définition de région avec le nom donné.", + "repeater_cliHelpRegionAllowf": "Définit les autorisations de \"Flot\" pour la région donnée. ('' pour la portée globale/héritée)", + "repeater_cliHelpRegionDenyf": "Supprime l'autorisation 'F'lood' pour la région donnée. (NOTE : à ce stade, il n'est pas conseillé de l'utiliser sur l'étendue globale/héritée !! )", + "repeater_cliHelpRegionHome": "Répond avec la région 'maison' actuelle. (Note appliquée nulle part pour l'instant, réservée à une utilisation future)", + "repeater_cliHelpRegionHomeSet": "Définit la région 'maison'.", + "repeater_cliHelpRegionSave": "Conserve la liste/la carte des régions dans le stockage.", + "repeater_cliHelpGps": "Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.", + "repeater_cliHelpGpsOnOff": "Activer/désactiver le GPS.", + "repeater_cliHelpGpsSync": "Synchronise l'heure du nÅ“ud avec l'horloge GPS.", + "repeater_cliHelpGpsSetLoc": "Définit la position du nÅ“ud aux coordonnées GPS et enregistre les préférences.", + "repeater_cliHelpGpsAdvert": "Donne la configuration de l'annonce de la localisation du nÅ“ud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences", + "repeater_cliHelpGpsAdvertSet": "Définit la configuration de l'annonce de localisation.", "repeater_commandsListTitle": "Liste des commandes", - "repeater_commandsListNote": "NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...", - "repeater_general": "Général", - "repeater_settingsCategory": "Paramètres", + "repeater_commandsListNote": "NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...", + "repeater_general": "Général", + "repeater_settingsCategory": "Paramètres", "repeater_bridge": "Pont", "repeater_logging": "Journalisation", - "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)", - "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)", - "repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.", + "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)", + "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)", + "repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.", "repeater_gpsManagement": "Gestion GPS", - "repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.", - "telemetry_receivedData": "Données de télémétrie reçues", - "telemetry_requestTimeout": "Demande de télémétrie expirée.", - "telemetry_errorLoading": "Erreur lors du chargement de la télémétrie : {error}", + "repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.", + "telemetry_receivedData": "Données de télémétrie reçues", + "telemetry_requestTimeout": "Demande de télémétrie expirée.", + "telemetry_errorLoading": "Erreur lors du chargement de la télémétrie : {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Aucune donnée de télémétrie disponible.", + "telemetry_noData": "Aucune donnée de télémétrie disponible.", "telemetry_channelTitle": "Canal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1202,8 +1202,8 @@ }, "telemetry_batteryLabel": "Batterie", "telemetry_voltageLabel": "Tension", - "telemetry_mcuTemperatureLabel": "Température du MCU", - "telemetry_temperatureLabel": "Température", + "telemetry_mcuTemperatureLabel": "Température du MCU", + "telemetry_temperatureLabel": "Température", "telemetry_currentLabel": "Actuellement", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1245,16 +1245,16 @@ }, "channelPath_title": "Chemin de paquet", "channelPath_viewMap": "Afficher la carte", - "channelPath_otherObservedPaths": "Autres chemins observés", - "channelPath_repeaterHops": "Sauts du répéteur", - "channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.", - "channelPath_messageDetails": "Détails du message", - "channelPath_senderLabel": "Expéditeur", + "channelPath_otherObservedPaths": "Autres chemins observés", + "channelPath_repeaterHops": "Sauts du répéteur", + "channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.", + "channelPath_messageDetails": "Détails du message", + "channelPath_senderLabel": "Expéditeur", "channelPath_timeLabel": "Temps", - "channelPath_repeatsLabel": "Répétitions", + "channelPath_repeatsLabel": "Répétitions", "channelPath_pathLabel": "Chemin {index}", - "channelPath_observedLabel": "Observé", - "channelPath_observedPathTitle": "Chemin observé {index} • {hops}", + "channelPath_observedLabel": "Observé", + "channelPath_observedPathTitle": "Chemin observé {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Aucune donnée de localisation", + "channelPath_noLocationData": "Aucune donnée de localisation", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,7 +1289,7 @@ } }, "channelPath_unknownPath": "Inconnu", - "channelPath_floodPath": "Tout le réseau", + "channelPath_floodPath": "Tout le réseau", "channelPath_directPath": "Afficher", "channelPath_observedZeroOf": "0 de {total} sauts", "@channelPath_observedZeroOf": { @@ -1311,7 +1311,7 @@ } }, "channelPath_mapTitle": "Carte du chemin", - "channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.", + "channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.", "channelPath_primaryPath": "Chemin {index} (Principal)", "@channelPath_primaryPath": { "placeholders": { @@ -1328,8 +1328,8 @@ } }, "channelPath_pathLabelTitle": "Chemin", - "channelPath_observedPathHeader": "Chemin observé", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_observedPathHeader": "Chemin observé", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,17 +1340,17 @@ } } }, - "channelPath_noHopDetailsAvailable": "Aucun détail de saut disponible pour ce paquet.", - "channelPath_unknownRepeater": "Répéteur Inconnu", + "channelPath_noHopDetailsAvailable": "Aucun détail de saut disponible pour ce paquet.", + "channelPath_unknownRepeater": "Répéteur Inconnu", "listFilter_tooltip": "Filtrer et trier", "listFilter_sortBy": "Trier par", "listFilter_latestMessages": "Derniers messages", - "listFilter_heardRecently": "Écoute récemment", - "listFilter_az": "A à Z", + "listFilter_heardRecently": "Écoute récemment", + "listFilter_az": "A à Z", "listFilter_filters": "Filtres", "listFilter_all": "Tout", "listFilter_users": "Utilisateurs", - "listFilter_repeaters": "Répéteurs", + "listFilter_repeaters": "Répéteurs", "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Messages non lus seulement", "listFilter_newGroup": "Nouveau groupe", @@ -1363,21 +1363,21 @@ }, "repeater_neighbors": "Voisins", "repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.", - "neighbors_receivedData": "Données des voisins reçues", - "neighbors_requestTimedOut": "Les voisins demandent un délai.", + "neighbors_receivedData": "Données des voisins reçues", + "neighbors_requestTimedOut": "Les voisins demandent un délai.", "neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}", - "neighbors_repeatersNeighbors": "Répéteurs Voisins", - "neighbors_noData": "Aucune donnée concernant les voisins disponible.", - "channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.", - "channels_joinPrivateChannel": "Rejoindre un Canal Privé", - "channels_createPrivateChannel": "Créer un Canal Privé", - "channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.", + "neighbors_repeatersNeighbors": "Répéteurs Voisins", + "neighbors_noData": "Aucune donnée concernant les voisins disponible.", + "channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.", + "channels_joinPrivateChannel": "Rejoindre un Canal Privé", + "channels_createPrivateChannel": "Créer un Canal Privé", + "channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.", "channels_joinPublicChannel": "Rejoindre le canal public", "channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.", "channels_joinHashtagChannel": "Rejoindre un Canal Hashtag", "channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.", "channels_scanQrCode": "Scanner un code QR", - "channels_scanQrCodeComingSoon": "Bientôt disponible", + "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", "channels_hashtagHint": "ex. #equipe", "@neighbors_unknownContact": { @@ -1394,13 +1394,13 @@ } } }, - "neighbors_unknownContact": "Clé publique inconnue {pubkey}", - "neighbors_heardAgo": "Écouté : {time} auparavant", + "neighbors_unknownContact": "Clé publique inconnue {pubkey}", + "neighbors_heardAgo": "Écouté : {time} auparavant", "settings_locationGPSEnable": "Activer le GPS", - "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS", - "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", - "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", - "contacts_manageRoom": "Gérer le Room Server", + "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS", + "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", + "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", + "contacts_manageRoom": "Gérer le Room Server", "room_management": "Administrattion Room Server", "@community_joinConfirmation": { "placeholders": { @@ -1459,35 +1459,35 @@ } }, "common_ok": "OK", - "community_title": "Communauté", - "community_create": "Créer une Communauté", - "community_createDesc": "Créer une nouvelle communauté et la partager via QR code.", + "community_title": "Communauté", + "community_create": "Créer une Communauté", + "community_createDesc": "Créer une nouvelle communauté et la partager via QR code.", "community_join": "Rejoindre", - "community_joinTitle": "Rejoindre la communauté", - "community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?", - "community_scanQr": "Scanner la communauté QR", + "community_joinTitle": "Rejoindre la communauté", + "community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?", + "community_scanQr": "Scanner la communauté QR", "community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.", "community_showQr": "Afficher le QR Code", - "community_publicChannel": "Communauté Publique", - "community_hashtagChannel": "Hashtag Communauté", - "community_name": "Nom de la communauté", - "community_enterName": "Entrez le nom de la communauté", - "community_created": "Communauté \"{name}\" créée", - "community_joined": "Rejoint la communauté \"{name}\"", - "community_qrTitle": "Partager Communauté", + "community_publicChannel": "Communauté Publique", + "community_hashtagChannel": "Hashtag Communauté", + "community_name": "Nom de la communauté", + "community_enterName": "Entrez le nom de la communauté", + "community_created": "Communauté \"{name}\" créée", + "community_joined": "Rejoint la communauté \"{name}\"", + "community_qrTitle": "Partager Communauté", "community_qrInstructions": "Scanner ce QR code pour rejoindre {name}", - "community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté", - "community_invalidQrCode": "Code QR de communauté non valide", - "community_alreadyMember": "Déjà membre", - "community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".", - "community_addPublicChannel": "Ajouter un Canal Public de la Communauté", - "community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté", - "community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.", - "community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer", - "community_manageCommunities": "Gérer les Communautés", - "community_delete": "Quitter la communauté", + "community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté", + "community_invalidQrCode": "Code QR de communauté non valide", + "community_alreadyMember": "Déjà membre", + "community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".", + "community_addPublicChannel": "Ajouter un Canal Public de la Communauté", + "community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté", + "community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.", + "community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer", + "community_manageCommunities": "Gérer les Communautés", + "community_delete": "Quitter la communauté", "community_deleteConfirm": "Quitter \"{name}\" ?", - "community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.", + "community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Communauté \"{name}\" quittée", - "community_addHashtagChannel": "Ajouter un Hashtag Communauté", - "community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté", - "community_selectCommunity": "Sélectionner Communauté", - "community_regularHashtag": "Hashtag régulier", + "community_deleted": "Communauté \"{name}\" quittée", + "community_addHashtagChannel": "Ajouter un Hashtag Communauté", + "community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté", + "community_selectCommunity": "Sélectionner Communauté", + "community_regularHashtag": "Hashtag régulier", "community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)", - "community_communityHashtag": "Hashtag de la communauté", - "community_communityHashtagDesc": "Exclusif aux membres de la communauté", + "community_communityHashtag": "Hashtag de la communauté", + "community_communityHashtagDesc": "Exclusif aux membres de la communauté", "community_forCommunity": "Pour {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1532,13 +1532,13 @@ } } }, - "community_regenerateSecret": "Régénérer le secret", - "community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.", - "community_regenerate": "Régénérer", - "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"", - "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"", - "community_updateSecret": "Mettre à jour le secret", - "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"", + "community_regenerateSecret": "Régénérer le secret", + "community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.", + "community_regenerate": "Régénérer", + "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"", + "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"", + "community_updateSecret": "Mettre à jour le secret", + "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1548,80 +1548,80 @@ }, "pathTrace_you": "Vous", "pathTrace_refreshTooltip": "Actualiser Path Trace", - "pathTrace_failed": "Traçage du chemin échoué.", - "pathTrace_notAvailable": "Tracé de chemin non disponible.", - "contacts_pathTrace": "Traçage de chemin", - "contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur", - "contacts_repeaterPing": "Pinguer le répéteur", - "contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle", + "pathTrace_failed": "Traçage du chemin échoué.", + "pathTrace_notAvailable": "Tracé de chemin non disponible.", + "contacts_pathTrace": "Traçage de chemin", + "contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur", + "contacts_repeaterPing": "Pinguer le répéteur", + "contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle", "contacts_chatTraceRoute": "Tracer le chemin", - "contacts_pathTraceTo": "Tracer l'itinéraire vers {name}", + "contacts_pathTraceTo": "Tracer l'itinéraire vers {name}", "contacts_ping": "Ping", "contacts_roomPing": "Pinguer le serveur de la salle", - "contacts_invalidAdvertFormat": "Données de contact non valides", + "contacts_invalidAdvertFormat": "Données de contact non valides", "appSettings_languageUk": "Ukrainien", "appSettings_languageRu": "Russe", - "appSettings_enableMessageTracing": "Activer le traçage des messages", - "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", + "appSettings_enableMessageTracing": "Activer le traçage des messages", + "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", "contacts_clipboardEmpty": "Le presse-papiers est vide.", - "contacts_contactImported": "Le contact a été importé.", - "contacts_floodAdvert": "Annonce à tout le réseau", - "contacts_contactImportFailed": "Échec de l'importation du contact.", + "contacts_contactImported": "Le contact a été importé.", + "contacts_floodAdvert": "Annonce à tout le réseau", + "contacts_contactImportFailed": "Échec de l'importation du contact.", "contacts_zeroHopAdvert": "Annonce Zero saut", "contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers", "contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers", "contacts_ShareContact": "Copier le contact dans le presse-papiers", "contacts_ShareContactZeroHop": "Partager un contact par annonce", - "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", - "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", + "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", + "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", - "notification_activityTitle": "Activité MeshCore", + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "notification_activityTitle": "Activité MeshCore", "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", "notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}", - "notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}", - "notification_newTypeDiscovered": "Nouveau {contactType} découvert", - "notification_receivedNewMessage": "Nouveau message reçu", - "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", - "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", - "settings_gpxExportNoContacts": "Aucun contact à exporter.", - "settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation", + "notification_newNodesCount": "{count} {count, plural, =1{nouveau nÅ“ud} other{nouveaux nÅ“uds}}", + "notification_newTypeDiscovered": "Nouveau {contactType} découvert", + "notification_receivedNewMessage": "Nouveau message reçu", + "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", + "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", + "settings_gpxExportNoContacts": "Aucun contact à exporter.", + "settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation", "settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.", - "settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle", + "settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle", "settings_gpxExportContacts": "Exporter les compagnons au format GPX", "settings_gpxExportAll": "Exporter tous les contacts au format GPX", "settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.", "settings_gpxExportContactsSubtitle": "Exporte les compagnons avec un emplacement vers un fichier GPX.", "settings_gpxExportChat": "Emplacements des compagnons", - "settings_gpxExportSuccess": "Fichier GPX exporté avec succès.", + "settings_gpxExportSuccess": "Fichier GPX exporté avec succès.", "settings_gpxExportAllContacts": "Tous les emplacements des contacts", - "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", + "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !", - "map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.", + "map_tapToAdd": "Appuyez sur les nÅ“uds pour les ajouter au chemin.", "pathTrace_clearTooltip": "Effacer le chemin", - "map_pathTraceCancelled": "Traçage de chemin annulé", + "map_pathTraceCancelled": "Traçage de chemin annulé", "map_removeLast": "Supprimer le dernier", - "map_runTrace": "Exécuter la traçage de chemin", + "map_runTrace": "Exécuter la traçage de chemin", "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", "scanner_chromeRequired": "Navigateur Chrome requis", - "scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.", - "scanner_bluetoothOff": "Le Bluetooth est désactivé.", + "scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.", + "scanner_bluetoothOff": "Le Bluetooth est désactivé.", "scanner_enableBluetooth": "Activer le Bluetooth", - "snrIndicator_lastSeen": "Dernière fois vu", - "snrIndicator_nearByRepeaters": "Répéteurs à proximité", + "snrIndicator_lastSeen": "Dernière fois vu", + "snrIndicator_nearByRepeaters": "Répéteurs à proximité", "chat_ShowAllPaths": "Afficher tous les chemins", - "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", - "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", - "settings_clientRepeat": "Répétition hors réseau", - "settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "Unités", - "appSettings_unitsMetric": "Métrique (m/km)", - "appSettings_unitsImperial": "Impérial (ft / mi)", + "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", + "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", + "settings_clientRepeat": "Répétition hors réseau", + "settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unités", + "appSettings_unitsMetric": "Métrique (m/km)", + "appSettings_unitsImperial": "Impérial (ft / mi)", "map_lineOfSight": "Ligne de vue", "map_losScreenTitle": "Ligne de vue", - "losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.", - "losRunFailed": "Échec de la vérification de la ligne de vue : {error}", + "losSelectStartEnd": "Sélectionnez les nÅ“uds de début et de fin pour LOS.", + "losRunFailed": "Échec de la vérification de la ligne de vue : {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1630,12 +1630,12 @@ } }, "losClearAllPoints": "Effacer tous les points", - "losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude", + "losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés", - "losShowDisplayNodes": "Afficher les nœuds d'affichage", - "losCustomPoints": "Points personnalisés", - "losCustomPointLabel": "Personnalisé {index}", + "losMenuSubtitle": "Appuyez sur les nÅ“uds ou appuyez longuement sur la carte pour des points personnalisés", + "losShowDisplayNodes": "Afficher les nÅ“uds d'affichage", + "losCustomPoints": "Points personnalisés", + "losCustomPointLabel": "Personnalisé {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1645,7 +1645,7 @@ }, "losPointA": "Point A", "losPointB": "Point B", - "losAntennaA": "Antenne A : {value} {unit}", + "losAntennaA": "Antenne A : {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Antenne B : {value} {unit}", + "losAntennaB": "Antenne B : {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1667,8 +1667,8 @@ } } }, - "losRun": "Exécuter la LOS", - "losNoElevationData": "Aucune donnée d'altitude", + "losRun": "Exécuter la LOS", + "losNoElevationData": "Aucune donnée d'altitude", "losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1703,9 +1703,9 @@ } } }, - "losStatusChecking": "LOS : vérification...", - "losStatusNoData": "LOS : aucune donnée", - "losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu", + "losStatusChecking": "LOS : vérification...", + "losStatusNoData": "LOS : aucune donnée", + "losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.", - "losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.", - "losRenameCustomPoint": "Renommer le point personnalisé", + "losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.", + "losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.", + "losRenameCustomPoint": "Renommer le point personnalisé", "losPointName": "Nom du point", "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", - "losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)", + "losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Horizon radio", - "losLegendLosBeam": "Ligne de visée", + "losLegendLosBeam": "Ligne de visée", "losLegendTerrain": "Terrain", - "losFrequencyLabel": "Fréquence", - "losFrequencyInfoTooltip": "Voir les détails du calcul", - "losFrequencyDialogTitle": "Calcul de l’horizon radio", - "losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", + "losFrequencyLabel": "Fréquence", + "losFrequencyInfoTooltip": "Voir les détails du calcul", + "losFrequencyDialogTitle": "Calcul de l’horizon radio", + "losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_addToFavorites": "Ajouter à mes favoris", + "listFilter_addToFavorites": "Ajouter à mes favoris", "listFilter_removeFromFavorites": "Supprimer des favoris", - "listFilter_favorites": "Préférences", + "listFilter_favorites": "Préférences", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1800,15 +1800,13 @@ "contacts_searchFavorites": "Rechercher {number}{str} Favoris...", "contacts_searchUsers": "Rechercher {number}{str} utilisateurs...", "contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...", - "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", + "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", "contacts_searchContactsNoNumber": "Rechercher des contacts...", - "connectionChoiceTitle": "Choisissez votre méthode de connexion.", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", - "connectionChoiceSubtitle": "Choisissez la méthode de connexion que vous préférez pour votre appareil MeshCore.", - "usbScreenStatus": "Sélectionnez un périphérique USB", - "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.", - "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.", + "usbScreenStatus": "Sélectionnez un périphérique USB", + "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nÅ“ud MeshCore.", + "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.", "usbScreenTitle": "Connectez via USB", - "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page." + "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 14c37fa..c77f1b9 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -35,7 +35,7 @@ "common_disable": "Disattivare", "common_reboot": "Riavvia", "common_loading": "Caricamento...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -98,11 +98,11 @@ "settings_locationInvalid": "Latitudine o longitudine non valida.", "settings_latitude": "Latitudine", "settings_longitude": "Longitudine", - "settings_privacyMode": "Modalità Privacy", + "settings_privacyMode": "Modalità Privacy", "settings_privacyModeSubtitle": "Nascondere nome/luogo negli annunci", - "settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.", - "settings_privacyModeEnabled": "Modalità privacy abilitata", - "settings_privacyModeDisabled": "Modalità privacy disabilitata", + "settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.", + "settings_privacyModeEnabled": "Modalità privacy abilitata", + "settings_privacyModeDisabled": "Modalità privacy disabilitata", "settings_actions": "Azioni", "settings_sendAdvertisement": "Invia Annuncio", "settings_sendAdvertisementSubtitle": "Presenza trasmessa ora", @@ -165,18 +165,18 @@ "appSettings_language": "Lingua", "appSettings_languageSystem": "Predefinito di sistema", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notifiche", "appSettings_enableNotifications": "Abilita Notifiche", "appSettings_enableNotificationsSubtitle": "Ricevi notifiche per messaggi e annunci", @@ -195,7 +195,7 @@ "appSettings_pathsWillBeCleared": "I percorsi verranno puliti dopo 5 tentativi falliti.", "appSettings_pathsWillNotBeCleared": "I percorsi non verranno eliminati automaticamente.", "appSettings_autoRouteRotation": "Rotazione Percorso Automatico", - "appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione", + "appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione", "appSettings_autoRouteRotationEnabled": "Rotazione percorso automatico abilitata", "appSettings_autoRouteRotationDisabled": "Rotazione del percorso automatico disabilitata", "appSettings_battery": "Batteria", @@ -284,8 +284,8 @@ }, "contacts_newGroup": "Nuovo Gruppo", "contacts_groupName": "Nome gruppo", - "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", - "contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.", + "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", + "contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -345,7 +345,7 @@ "channels_muteChannel": "Silenzia canale", "channels_unmuteChannel": "Attiva notifiche canale", "channels_deleteChannel": "Elimina canale", - "channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.", + "channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -477,7 +477,7 @@ "debugLog_enableInSettings": "Abilita il logging di debug dell'app nelle impostazioni", "debugLog_frames": "Frame", "debugLog_rawLogRx": "Log Raw-RX", - "debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.", + "debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.", "debugFrame_length": "Lunghezza del Frame: {count} byte", "@debugFrame_length": { "placeholders": { @@ -542,11 +542,11 @@ }, "debugFrame_hexDump": "Dumpa Esadecimale:", "chat_pathManagement": "Gestione Percorsi", - "chat_routingMode": "Modalità di routing", + "chat_routingMode": "Modalità di routing", "chat_autoUseSavedPath": "Utilizza il percorso salvato", - "chat_forceFloodMode": "Modalità Inondamento Forzato", + "chat_forceFloodMode": "Modalità Inondamento Forzato", "chat_recentAckPaths": "Percorsi ACK Recenti (tocca per usare):", - "chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.", + "chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.", "chat_hopSingular": "salta", "chat_hopPlural": "salta", "chat_hopsCount": "{count} {count, plural, =1{salto} other{salti}}", @@ -559,15 +559,15 @@ }, "chat_successes": "successi", "chat_removePath": "Rimuovi percorso", - "chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.", + "chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.", "chat_pathActions": "Azioni Percorso:", "chat_setCustomPath": "Imposta Percorso Personalizzato", "chat_setCustomPathSubtitle": "Specifica manualmente il percorso di routing", "chat_clearPath": "Cancella Percorso", "chat_clearPathSubtitle": "Riprova la scoperta alla prossima invio", - "chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.", + "chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.", "chat_floodModeSubtitle": "Utilizza l'interruttore di routing nella barra delle applicazioni", - "chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.", + "chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.", "chat_fullPath": "Percorso Completo", "chat_pathDetailsNotAvailable": "I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.", "chat_pathSetHops": "Percorso impostato: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", @@ -660,7 +660,7 @@ "map_sendToChannel": "Invia al canale", "map_noChannelsAvailable": "Nessun canale disponibile", "map_publicLocationShare": "Condividi in una posizione pubblica", - "map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.", + "map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -810,13 +810,13 @@ "login_password": "Password", "login_enterPassword": "Inserisci password", "login_savePassword": "Salva password", - "login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.", + "login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.", "login_repeaterDescription": "Inserisci la password del ripetitore per accedere alle impostazioni e allo stato.", "login_roomDescription": "Inserisci la password della stanza per accedere alle impostazioni e allo stato.", "login_routing": "Instradamento", - "login_routingMode": "Modalità di routing", + "login_routingMode": "Modalità di routing", "login_autoUseSavedPath": "Utilizza il percorso salvato", - "login_forceFloodMode": "Modalità Inondamento Forzato", + "login_forceFloodMode": "Modalità Inondamento Forzato", "login_managePaths": "Gestisci Percorsi", "login_login": "Accedi", "login_attempt": "Prova {current}/{max}", @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", + "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", "common_reload": "Ricaricare", "common_clear": "Cancella", "path_currentPath": "Percorso corrente: {path}", @@ -862,7 +862,7 @@ "path_hexPrefixInstructions": "Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.", "path_hexPrefixExample": "Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)", "path_labelHexPrefixes": "Prefisso esadecimale (percorso)", - "path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)", + "path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)", "path_selectFromContacts": "Seleziona da contatti:", "path_noRepeatersFound": "Non sono stati trovati ripetitori o server di stanza.", "path_customPathsRequire": "I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.", @@ -874,7 +874,7 @@ } } }, - "path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.", + "path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.", "path_setPath": "Imposta Percorso", "repeater_management": "Gestione Ripetitori", "repeater_managementTools": "Strumenti di Gestione", @@ -887,9 +887,9 @@ "repeater_settings": "Impostazioni", "repeater_settingsSubtitle": "Configura i parametri del ripetitore", "repeater_statusTitle": "Stato del Ripetitore", - "repeater_routingMode": "Modalità di routing", + "repeater_routingMode": "Modalità di routing", "repeater_autoUseSavedPath": "Percorso salvato automatico", - "repeater_forceFloodMode": "Modalità Inondamento Forzato", + "repeater_forceFloodMode": "Modalità Inondamento Forzato", "repeater_pathManagement": "Gestione dei percorsi", "repeater_refresh": "Aggiorna", "repeater_statusRequestTimeout": "Richiesta stato scaduta.", @@ -904,7 +904,7 @@ "repeater_systemInformation": "Informazioni di sistema", "repeater_battery": "Batteria", "repeater_clockAtLogin": "Orologio (all'accesso)", - "repeater_uptime": "Disponibilità", + "repeater_uptime": "Disponibilità", "repeater_queueLength": "Lunghezza della coda", "repeater_debugFlags": "Impostazioni Debug", "repeater_radioStatistics": "Statistiche Radio", @@ -1007,10 +1007,10 @@ "repeater_packetForwardingSubtitle": "Abilita il ripetitore per inoltrare i pacchetti", "repeater_guestAccess": "Accesso Ospite", "repeater_guestAccessSubtitle": "Consenti l'accesso ospite in sola lettura", - "repeater_privacyMode": "Modalità Privacy", + "repeater_privacyMode": "Modalità Privacy", "repeater_privacyModeSubtitle": "Nascondere nome/luogo negli annunci", "repeater_advertisementSettings": "Impostazioni Annuncio", - "repeater_localAdvertInterval": "Intervallo Pubblicità Locale", + "repeater_localAdvertInterval": "Intervallo Pubblicità Locale", "repeater_localAdvertIntervalMinutes": "{minutes} minuti", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione", + "repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione", "repeater_floodAdvertIntervalHours": "{hours} ore", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1033,13 +1033,13 @@ "repeater_rebootRepeater": "Riavvia Ripetitore", "repeater_rebootRepeaterSubtitle": "Riavvia il dispositivo ripetitore", "repeater_rebootRepeaterConfirm": "Sei sicuro di voler riavviare questo ripetitore?", - "repeater_regenerateIdentityKey": "Rigenera Chiave Identità", + "repeater_regenerateIdentityKey": "Rigenera Chiave Identità", "repeater_regenerateIdentityKeySubtitle": "Genera una nuova coppia di chiavi pubblica/privata", - "repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?", + "repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?", "repeater_eraseFileSystem": "Elimina File System", "repeater_eraseFileSystemSubtitle": "Formatta il file system del ripetitore", - "repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!", - "repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.", + "repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!", + "repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.", "repeater_commandSent": "Comando inviato: {command}", "@repeater_commandSent": { "placeholders": { @@ -1072,7 +1072,7 @@ "repeater_refreshLocationSettings": "Aggiorna le Impostazioni della Posizione", "repeater_refreshPacketForwarding": "Aggiorna il inoltro pacchetti", "repeater_refreshGuestAccess": "Aggiorna Accesso Ospite", - "repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy", + "repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy", "repeater_refreshAdvertisementSettings": "Aggiorna le Impostazioni dell'Annuncio", "repeater_refreshed": "{label} aggiornato", "@repeater_refreshed": { @@ -1117,7 +1117,7 @@ "repeater_cliQuickAdvertise": "Pubblicare", "repeater_cliQuickClock": "Orologio", "repeater_cliHelpAdvert": "Invia un pacchetto pubblicitario", - "repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)", + "repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)", "repeater_cliHelpClock": "Mostra l'ora corrente per l'orologio di ciascun dispositivo.", "repeater_cliHelpPassword": "Imposta una nuova password di amministratore per il dispositivo.", "repeater_cliHelpVersion": "Mostra la versione del dispositivo e la data di costruzione del firmware.", @@ -1125,12 +1125,12 @@ "repeater_cliHelpSetAf": "Imposta il fattore di tempo di trasmissione.", "repeater_cliHelpSetTx": "Imposta la potenza di trasmissione LoRa in dBm (riavvia per applicare).", "repeater_cliHelpSetRepeat": "Abilita o disabilita il ruolo del ripetitore per questo nodo.", - "repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).", + "repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).", "repeater_cliHelpSetFloodMax": "Imposta il numero massimo di salti per i pacchetti di inondazione in entrata (se >= max, il pacchetto non viene inoltrato)", - "repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.", + "repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.", "repeater_cliHelpSetAgcResetInterval": "Imposta l'intervallo per resettare il controllore Automatico del Guadagno. Imposta su 0 per disabilitare.", "repeater_cliHelpSetMultiAcks": "Abilita o disabilita la funzione 'double ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.", + "repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.", "repeater_cliHelpSetFloodAdvertInterval": "Imposta l'intervallo del timer in ore per inviare un pacchetto pubblicitario di massa. Imposta su 0 per disabilitare.", "repeater_cliHelpSetGuestPassword": "Imposta/aggiorna la password dell'ospite. (per ripetitori, gli accessi degli ospiti possono inviare la richiesta \"Get Stats\")", "repeater_cliHelpSetName": "Imposta il nome dell'annuncio.", @@ -1138,33 +1138,33 @@ "repeater_cliHelpSetLon": "Imposta la longitudine della mappa pubblicitaria. (gradi decimali)", "repeater_cliHelpSetRadio": "Imposta completamente nuovi parametri radio e li salva nelle preferenze. Richiede un comando \"reboot\" per l'applicazione.", "repeater_cliHelpSetRxDelay": "Impostazioni (experimental) base (deve essere > 1 per l'effetto) per applicare un leggero ritardo ai pacchetti ricevuti, in base alla forza del segnale/punteggio. Imposta a 0 per disabilitare.", - "repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).", - "repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.", + "repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).", + "repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.", "repeater_cliHelpSetBridgeEnabled": "Abilita/Disabilita ponte.", "repeater_cliHelpSetBridgeDelay": "Imposta il ritardo prima di ritrasmettere i pacchetti.", - "repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.", - "repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.", + "repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.", + "repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.", "repeater_cliHelpSetBridgeSecret": "Imposta il segreto per i ponti espnow.", "repeater_cliHelpSetAdcMultiplier": "Imposta un fattore personalizzato per regolare la tensione della batteria riportata (supportato solo su schede selezionate).", "repeater_cliHelpTempRadio": "Imposta parametri radio temporanei per il numero specificato di minuti, per poi tornare ai parametri radio originali. (non salva nelle preferenze).", - "repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)", + "repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)", "repeater_cliHelpGetBridgeType": "Ottiene tipo ponte nessuno, rs232, espnow", "repeater_cliHelpLogStart": "Avvia registrazione pacchetti nel file system.", "repeater_cliHelpLogStop": "Interrompi la registrazione dei pacchetti al file system.", "repeater_cliHelpLogErase": "Elimina i log del pacchetto dal file system.", - "repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4", + "repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4", "repeater_cliHelpNeighborRemove": "Rimuove la prima corrispondenza in base al prefisso (esadecimale) della pubkey, dalla lista dei vicini.", "repeater_cliHelpRegion": "(solo serie) Elenca tutte le regioni definite e le autorizzazioni di allagamento correnti.", - "repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.", + "repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.", "repeater_cliHelpRegionGet": "Cerca la regione con il prefisso del nome dato (o \"\" per l'ambito globale). Risponde con \"-> nome-regione (nome-genitore) 'F'\"", "repeater_cliHelpRegionPut": "Aggiunge o aggiorna una definizione di regione con il nome specificato.", "repeater_cliHelpRegionRemove": "Rimuove una definizione di regione con il dato nome. (deve corrispondere esattamente e non avere regioni figlio)", "repeater_cliHelpRegionAllowf": "Imposta il permesso di 'F'lood per la regione specificata. ('' per lo scope globale/legacy)", - "repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).", + "repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).", "repeater_cliHelpRegionHome": "Risposte con la regione 'home' corrente. (Nota applicata finora, riservata per il futuro)", "repeater_cliHelpRegionHomeSet": "Imposta la regione 'home'.", "repeater_cliHelpRegionSave": "Persiste l'elenco/mappa delle regioni all'archiviazione.", - "repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.", + "repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.", "repeater_cliHelpGpsOnOff": "Attiva/disattiva l'alimentazione del GPS.", "repeater_cliHelpGpsSync": "Sincronizza l'orario del nodo con l'orologio GPS.", "repeater_cliHelpGpsSetLoc": "Imposta la posizione del nodo alle coordinate GPS e salva le preferenze.", @@ -1180,7 +1180,7 @@ "repeater_regionManagementRepeaterOnly": "Gestione Regione (solo Ripetitore)", "repeater_regionNote": "Sono state introdotte le comandi di regione per gestire le definizioni e le autorizzazioni delle regioni.", "repeater_gpsManagement": "Gestione GPS", - "repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.", + "repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.", "telemetry_receivedData": "Dati Telemetria Ricevuti", "telemetry_requestTimeout": "Richiesta di telemetria scaduta.", "telemetry_errorLoading": "Errore nel caricamento della telemetria: {error}", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1254,7 +1254,7 @@ "channelPath_repeatsLabel": "Ripeti", "channelPath_pathLabel": "Percorso {index}", "channelPath_observedLabel": "Osservato", - "channelPath_observedPathTitle": "Percorso osservato {index} • {hops}", + "channelPath_observedPathTitle": "Percorso osservato {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Percorso", "channelPath_observedPathHeader": "Percorso Osservato", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1373,11 +1373,11 @@ "channels_joinPrivateChannel": "Unisciti a un Canale Privato", "channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.", "channels_joinPublicChannel": "Unisciti al Canale Pubblico", - "channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.", + "channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.", "channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag", - "channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.", + "channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.", "channels_scanQrCode": "Scansiona un codice QR", - "channels_scanQrCodeComingSoon": "Arriverà presto", + "channels_scanQrCodeComingSoon": "Arriverà presto", "channels_enterHashtag": "Inserisci hashtag", "channels_hashtagHint": "es. #team", "@neighbors_unknownContact": { @@ -1459,35 +1459,35 @@ } }, "common_ok": "OK", - "community_title": "Comunità", - "community_create": "Crea Comunità", - "community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.", + "community_title": "Comunità", + "community_create": "Crea Comunità", + "community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.", "community_join": "Unisciti", "community_joinTitle": "Unisciti alla Community", "community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?", "community_scanQr": "Scansiona il QR Code della Community", - "community_scanInstructions": "Punta la fotocamera su un codice QR della comunità", + "community_scanInstructions": "Punta la fotocamera su un codice QR della comunità", "community_showQr": "Mostra il codice QR", - "community_publicChannel": "Comunità Pubblica", - "community_hashtagChannel": "Hashtag della Comunità", - "community_name": "Nome della Comunità", - "community_enterName": "Inserisci il nome della comunità", - "community_created": "Comunità \"{name}\" creata", - "community_joined": "Unito alla comunità \"{name}\"", - "community_qrTitle": "Condividi Comunità", + "community_publicChannel": "Comunità Pubblica", + "community_hashtagChannel": "Hashtag della Comunità", + "community_name": "Nome della Comunità", + "community_enterName": "Inserisci il nome della comunità", + "community_created": "Comunità \"{name}\" creata", + "community_joined": "Unito alla comunità \"{name}\"", + "community_qrTitle": "Condividi Comunità", "community_qrInstructions": "Scansiona questo codice QR per unirti a {name}", "community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community", "community_invalidQrCode": "Codice QR della community non valido", - "community_alreadyMember": "Già membro", - "community_alreadyMemberMessage": "Sei già un membro di \"{name}\".", - "community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità", + "community_alreadyMember": "Già membro", + "community_alreadyMemberMessage": "Sei già un membro di \"{name}\".", + "community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità", "community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community", "community_noCommunities": "Nessun gruppo aggiunto finora", "community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.", - "community_manageCommunities": "Gestisci Comunità", - "community_delete": "Lascia la Comunità", + "community_manageCommunities": "Gestisci Comunità", + "community_delete": "Lascia la Comunità", "community_deleteConfirm": "Uscire da \"{name}\"?", - "community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.", + "community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Hai lasciato la comunità \"{name}\"", + "community_deleted": "Hai lasciato la comunità \"{name}\"", "community_addHashtagChannel": "Aggiungi Hashtag della Community", "community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community", - "community_selectCommunity": "Seleziona Comunità", + "community_selectCommunity": "Seleziona Comunità", "community_regularHashtag": "Hashtag regolare", - "community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)", - "community_communityHashtag": "Hashtag della Comunità", - "community_communityHashtagDesc": "Visibile solo ai membri della comunità", + "community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)", + "community_communityHashtag": "Hashtag della Comunità", + "community_communityHashtagDesc": "Visibile solo ai membri della comunità", "community_forCommunity": "Per {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1567,16 +1567,16 @@ "contacts_floodAdvert": "Annuncio alluvionale", "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", "contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard", - "contacts_clipboardEmpty": "La clipboard è vuota.", + "contacts_clipboardEmpty": "La clipboard è vuota.", "contacts_ShareContact": "Copia contatto negli Appunti", - "contacts_contactImported": "Il contatto è stato importato.", + "contacts_contactImported": "Il contatto è stato importato.", "contacts_contactImportFailed": "Contatto non importato con successo.", "contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.", "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", - "notification_activityTitle": "Attività MeshCore", + "notification_activityTitle": "Attività MeshCore", "notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}", "notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}", "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", @@ -1587,7 +1587,7 @@ "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", "settings_gpxExportNoContacts": "Nessun contatto da esportare.", "settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo", - "settings_gpxExportError": "Si è verificato un errore durante l'esportazione.", + "settings_gpxExportError": "Si è verificato un errore durante l'esportazione.", "settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.", "settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.", "settings_gpxExportAll": "Esporta tutti i contatti in GPX", @@ -1597,13 +1597,13 @@ "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", - "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", + "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", "map_removeLast": "Rimuovi ultimo", "map_pathTraceCancelled": "Tracciamento del percorso annullato.", "pathTrace_clearTooltip": "Pulisci percorso", "map_runTrace": "Esegui Path Trace", "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", - "scanner_bluetoothOff": "Il Bluetooth è disattivato.", + "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", "scanner_chromeRequired": "Browser Chrome richiesto", "scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.", @@ -1612,10 +1612,10 @@ "snrIndicator_lastSeen": "Ultimo accesso", "chat_ShowAllPaths": "Mostra tutti i percorsi", "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", - "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", + "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.", "settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "Unità", + "appSettings_unitsTitle": "Unità", "appSettings_unitsMetric": "Metrico (m/km)", "appSettings_unitsImperial": "Imperiale (ft / mi)", "map_lineOfSight": "Linea di vista", @@ -1631,7 +1631,7 @@ }, "losClearAllPoints": "Cancella tutti i punti", "losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico", - "losMenuTitle": "Menù LOS", + "losMenuTitle": "Menù LOS", "losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati", "losShowDisplayNodes": "Mostra i nodi di visualizzazione", "losCustomPoints": "Punti personalizzati", @@ -1722,7 +1722,7 @@ } } }, - "losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.", + "losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.", "losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.", "losRenameCustomPoint": "Rinomina punto personalizzato", "losPointName": "Nome del punto", @@ -1734,7 +1734,7 @@ "losLegendTerrain": "Terreno", "losFrequencyLabel": "Frequenza", "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", - "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", + "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", @@ -1802,11 +1802,9 @@ "contacts_unread": "Non letti", "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", "contacts_searchRoomServers": "Cerca {number}{str} server Room...", - "connectionChoiceTitle": "Scegli il metodo di connessione che preferisci.", - "connectionChoiceSubtitle": "Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", - "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", + "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", "usbScreenStatus": "Seleziona un dispositivo USB", "usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.", "usbScreenTitle": "Connessione tramite USB", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 162b760..2540c33 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -295,7 +295,7 @@ abstract class AppLocalizations { /// No description provided for @common_notAvailable. /// /// In en, this message translates to: - /// **'—'** + /// **'—'** String get common_notAvailable; /// No description provided for @common_voltageValue. @@ -316,18 +316,6 @@ abstract class AppLocalizations { /// **'MeshCore Open'** String get scanner_title; - /// No description provided for @connectionChoiceTitle. - /// - /// In en, this message translates to: - /// **'Choose your connection method'** - String get connectionChoiceTitle; - - /// No description provided for @connectionChoiceSubtitle. - /// - /// In en, this message translates to: - /// **'Select how you would like to reach your MeshCore device.'** - String get connectionChoiceSubtitle; - /// No description provided for @connectionChoiceUsbLabel. /// /// In en, this message translates to: @@ -955,13 +943,13 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageFr. /// /// In en, this message translates to: - /// **'Français'** + /// **'Français'** String get appSettings_languageFr; /// No description provided for @appSettings_languageEs. /// /// In en, this message translates to: - /// **'Español'** + /// **'Español'** String get appSettings_languageEs; /// No description provided for @appSettings_languageDe. @@ -979,13 +967,13 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageSl. /// /// In en, this message translates to: - /// **'Slovenščina'** + /// **'Slovenščina'** String get appSettings_languageSl; /// No description provided for @appSettings_languagePt. /// /// In en, this message translates to: - /// **'Português'** + /// **'Português'** String get appSettings_languagePt; /// No description provided for @appSettings_languageIt. @@ -997,7 +985,7 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageZh. /// /// In en, this message translates to: - /// **'中文'** + /// **'中文'** String get appSettings_languageZh; /// No description provided for @appSettings_languageSv. @@ -1015,25 +1003,25 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageSk. /// /// In en, this message translates to: - /// **'Slovenčina'** + /// **'Slovenčina'** String get appSettings_languageSk; /// No description provided for @appSettings_languageBg. /// /// In en, this message translates to: - /// **'Български'** + /// **'Български'** String get appSettings_languageBg; /// No description provided for @appSettings_languageRu. /// /// In en, this message translates to: - /// **'Русский'** + /// **'Русский'** String get appSettings_languageRu; /// No description provided for @appSettings_languageUk. /// /// In en, this message translates to: - /// **'Українська'** + /// **'Українська'** String get appSettings_languageUk; /// No description provided for @appSettings_enableMessageTracing. @@ -4349,7 +4337,7 @@ abstract class AppLocalizations { /// No description provided for @telemetry_temperatureValue. /// /// In en, this message translates to: - /// **'{celsius}°C / {fahrenheit}°F'** + /// **'{celsius}°C / {fahrenheit}°F'** String telemetry_temperatureValue(String celsius, String fahrenheit); /// No description provided for @neighbors_receivedData. @@ -4463,7 +4451,7 @@ abstract class AppLocalizations { /// No description provided for @channelPath_observedPathTitle. /// /// In en, this message translates to: - /// **'Observed path {index} • {hops}'** + /// **'Observed path {index} • {hops}'** String channelPath_observedPathTitle(int index, String hops); /// No description provided for @channelPath_noLocationData. @@ -4547,7 +4535,7 @@ abstract class AppLocalizations { /// No description provided for @channelPath_selectedPathLabel. /// /// In en, this message translates to: - /// **'{label} • {prefixes}'** + /// **'{label} • {prefixes}'** String channelPath_selectedPathLabel(String label, String prefixes); /// No description provided for @channelPath_noHopDetailsAvailable. diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 4eeedae..042ae8b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -12,88 +12,89 @@ class AppLocalizationsBg extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Контакти'; + String get nav_contacts => 'Контакти'; @override - String get nav_channels => 'Канали'; + String get nav_channels => 'Канали'; @override - String get nav_map => 'Карта'; + String get nav_map => 'Карта'; @override - String get common_cancel => 'Отказ'; + String get common_cancel => 'Отказ'; @override - String get common_ok => 'Добре'; + String get common_ok => 'Добре'; @override - String get common_connect => 'Свържи се'; + String get common_connect => 'Свържи се'; @override - String get common_unknownDevice => 'Неизвестно устройство'; + String get common_unknownDevice => + 'Неизвестно устройство'; @override - String get common_save => 'Запази'; + String get common_save => 'Запази'; @override - String get common_delete => 'Изтрий'; + String get common_delete => 'Изтрий'; @override - String get common_close => 'Затвори'; + String get common_close => 'Затвори'; @override - String get common_edit => 'Редактирай'; + String get common_edit => 'Редактирай'; @override - String get common_add => 'Добави'; + String get common_add => 'Добави'; @override - String get common_settings => 'Настройки'; + String get common_settings => 'Настройки'; @override - String get common_disconnect => 'Прекъсни'; + String get common_disconnect => 'Прекъсни'; @override - String get common_connected => 'Свързано'; + String get common_connected => 'Свързано'; @override - String get common_disconnected => 'Откъснато'; + String get common_disconnected => 'Откъснато'; @override - String get common_create => 'Създай'; + String get common_create => 'Създай'; @override - String get common_continue => 'Продължи'; + String get common_continue => 'Продължи'; @override - String get common_share => 'Сподели'; + String get common_share => 'Сподели'; @override - String get common_copy => 'Копирай'; + String get common_copy => 'Копирай'; @override - String get common_retry => 'Опитай отново'; + String get common_retry => 'Опитай отново'; @override - String get common_hide => 'Скриване'; + String get common_hide => 'Скриване'; @override - String get common_remove => 'Изтрий'; + String get common_remove => 'Изтрий'; @override - String get common_enable => 'Активирай'; + String get common_enable => 'Активирай'; @override - String get common_disable => 'Деактивирай'; + String get common_disable => 'Деактивирай'; @override - String get common_reboot => 'Рестартирай'; + String get common_reboot => 'Рестартирай'; @override - String get common_loading => 'Зареждане...'; + String get common_loading => 'Зареждане...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +109,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Изберете метода на връзка.'; - - @override - String get connectionChoiceSubtitle => - 'Изберете как искате да получите вашия устройство MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -122,235 +116,250 @@ class AppLocalizationsBg extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Свързване чрез USB'; + String get usbScreenTitle => 'Свързване чрез USB'; @override String get usbScreenSubtitle => - 'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.'; + 'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.'; @override - String get usbScreenStatus => 'Изберете USB устройство'; + String get usbScreenStatus => 'Изберете USB устройство'; @override String get usbScreenNote => - 'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.'; + 'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.'; @override String get usbScreenEmptyState => - 'Няма открити USB устройства. Включете едно и опитайте отново.'; + 'Няма открити USB устройства. Включете едно и опитайте отново.'; @override - String get scanner_scanning => 'Сканиране за устройства...'; + String get scanner_scanning => + 'Сканиране за устройства...'; @override - String get scanner_connecting => 'Свързвам се...'; + String get scanner_connecting => 'Свързвам се...'; @override - String get scanner_disconnecting => 'Изключване...'; + String get scanner_disconnecting => 'Изключване...'; @override - String get scanner_notConnected => 'Не е свързан'; + String get scanner_notConnected => 'Не е свързан'; @override String scanner_connectedTo(String deviceName) { - return 'Свързано с $deviceName'; + return 'Свързано с $deviceName'; } @override - String get scanner_searchingDevices => 'Търсене на устройства MeshCore...'; + String get scanner_searchingDevices => + 'Търсене на устройства MeshCore...'; @override String get scanner_tapToScan => - 'Натиснете Сканиране, за да намерите устройства MeshCore.'; + 'Натиснете Сканиране, за да намерите устройства MeshCore.'; @override String scanner_connectionFailed(String error) { - return 'Връзката не успя: $error'; + return 'Връзката не успя: $error'; } @override - String get scanner_stop => 'Спрете'; + String get scanner_stop => 'Спрете'; @override - String get scanner_scan => 'Сканирай'; + String get scanner_scan => 'Сканирай'; @override - String get scanner_bluetoothOff => 'Bluetooth е изключен.'; + String get scanner_bluetoothOff => 'Bluetooth е изключен.'; @override String get scanner_bluetoothOffMessage => - 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; + 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; @override - String get scanner_chromeRequired => 'Изисква се браузър Chrome'; + String get scanner_chromeRequired => + 'Изисква се браузър Chrome'; @override String get scanner_chromeRequiredMessage => - 'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.'; + 'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.'; @override - String get scanner_enableBluetooth => 'Активирайте Bluetooth'; + String get scanner_enableBluetooth => 'Активирайте Bluetooth'; @override - String get device_quickSwitch => 'Бързо превключване'; + String get device_quickSwitch => 'Бързо превключване'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Настройки'; + String get settings_title => 'Настройки'; @override - String get settings_deviceInfo => 'Информация за устройството'; + String get settings_deviceInfo => + 'Информация за устройството'; @override - String get settings_appSettings => 'Настройки на приложението'; + String get settings_appSettings => + 'Настройки на приложението'; @override String get settings_appSettingsSubtitle => - 'Уведомления, съобщения и предпочитания за карта'; + 'Уведомления, съобщения и предпочитания за карта'; @override - String get settings_nodeSettings => 'Настройки на възела'; + String get settings_nodeSettings => 'Настройки на възела'; @override - String get settings_nodeName => 'Име на възела'; + String get settings_nodeName => 'Име на възела'; @override - String get settings_nodeNameNotSet => 'Не е зададено'; + String get settings_nodeNameNotSet => 'Не е зададено'; @override - String get settings_nodeNameHint => 'Въведете име на възел'; + String get settings_nodeNameHint => 'Въведете име на възел'; @override - String get settings_nodeNameUpdated => 'Името е актуализирано'; + String get settings_nodeNameUpdated => + 'Името е актуализирано'; @override - String get settings_radioSettings => 'Настройки на радиопредавателя'; + String get settings_radioSettings => + 'Настройки на радиопредавателя'; @override String get settings_radioSettingsSubtitle => - 'Честота, мощност, разпространяващ фактор'; + 'Честота, мощност, разпространяващ фактор'; @override String get settings_radioSettingsUpdated => - 'Радио настройките са актуализирани'; + 'Радио настройките са актуализирани'; @override - String get settings_location => 'Местоположение'; + String get settings_location => 'Местоположение'; @override - String get settings_locationSubtitle => 'Координати на GPS'; + String get settings_locationSubtitle => 'Координати на GPS'; @override - String get settings_locationUpdated => 'Местоположението е актуализирано'; + String get settings_locationUpdated => + 'Местоположението е актуализирано'; @override String get settings_locationBothRequired => - 'Въведете както географска ширина, така и географска дължина.'; + 'Въведете както географска ширина, така и географска дължина.'; @override - String get settings_locationInvalid => 'Невалидна ширина или дължина.'; + String get settings_locationInvalid => + 'Невалидна ширина или дължина.'; @override - String get settings_locationGPSEnable => 'Активиране на GPS'; + String get settings_locationGPSEnable => 'Активиране на GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Активирайте автоматичното актуализиране на местоположението чрез GPS.'; + 'Активирайте автоматичното актуализиране на местоположението чрез GPS.'; @override - String get settings_locationIntervalSec => 'Интервал за GPS (Секунди)'; + String get settings_locationIntervalSec => + 'Интервал за GPS (Секунди)'; @override String get settings_locationIntervalInvalid => - 'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.'; + 'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.'; @override - String get settings_latitude => 'Широчина'; + String get settings_latitude => 'Широчина'; @override - String get settings_longitude => 'Дължина'; + String get settings_longitude => 'Дължина'; @override - String get settings_privacyMode => 'Режим на поверителност'; + String get settings_privacyMode => + 'Режим на поверителност'; @override String get settings_privacyModeSubtitle => - 'Скриване на име/местоположение в рекламите'; + 'Скриване на име/местоположение в рекламите'; @override String get settings_privacyModeToggle => - 'Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.'; + 'Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.'; @override String get settings_privacyModeEnabled => - 'Режим на поверителност е активиран'; + 'Режим на поверителност е активиран'; @override String get settings_privacyModeDisabled => - 'Режим на поверителност е деактивиран'; + 'Режим на поверителност е деактивиран'; @override - String get settings_actions => 'Действия'; + String get settings_actions => 'Действия'; @override - String get settings_sendAdvertisement => 'Изпрати Реклама'; + String get settings_sendAdvertisement => 'Изпрати Реклама'; @override - String get settings_sendAdvertisementSubtitle => 'Сега присъствие в ефир'; + String get settings_sendAdvertisementSubtitle => + 'Сега присъствие в ефир'; @override - String get settings_advertisementSent => 'Реклама изпратена'; + String get settings_advertisementSent => 'Реклама изпратена'; @override - String get settings_syncTime => 'Време за синхронизация'; + String get settings_syncTime => 'Време за синхронизация'; @override String get settings_syncTimeSubtitle => - 'Задайте часовника на устройството да отговаря на времето на телефона.'; + 'Задайте часовника на устройството да отговаря на времето на телефона.'; @override - String get settings_timeSynchronized => 'Синхронизирано във времето'; + String get settings_timeSynchronized => + 'Синхронизирано във времето'; @override - String get settings_refreshContacts => 'Презареди контакти'; + String get settings_refreshContacts => 'Презареди контакти'; @override String get settings_refreshContactsSubtitle => - 'Презареди списъка с контакти от устройството'; + 'Презареди списъка с контакти от устройството'; @override - String get settings_rebootDevice => 'Рестартирайте устройството'; + String get settings_rebootDevice => + 'Рестартирайте устройството'; @override String get settings_rebootDeviceSubtitle => - 'Рестартирайте устройството MeshCore'; + 'Рестартирайте устройството MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.'; + 'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.'; @override - String get settings_debug => 'Отстрани'; + String get settings_debug => 'Отстрани'; @override - String get settings_bleDebugLog => 'Лог за отстраняване на грешки на BLE'; + String get settings_bleDebugLog => + 'Лог за отстраняване на грешки на BLE'; @override String get settings_bleDebugLogSubtitle => - 'Команди, отговори и сурови данни BLE'; + 'Команди, отговори и сурови данни BLE'; @override String get settings_appDebugLog => - 'Лог на отстраняване на грешки на приложението'; + 'Лог на отстраняване на грешки на приложението'; @override String get settings_appDebugLogSubtitle => - 'Съобщения за отстраняване на грешки на приложението'; + 'Съобщения за отстраняване на грешки на приложението'; @override - String get settings_about => 'За нас'; + String get settings_about => 'За нас'; @override String settings_aboutVersion(String version) { @@ -358,115 +367,125 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get settings_aboutLegalese => 'Проект MeshCore с отворен код 2024 г.'; + String get settings_aboutLegalese => + 'Проект MeshCore с отворен код 2024 г.'; @override String get settings_aboutDescription => - 'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.'; + 'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.'; @override String get settings_aboutOpenMeteoAttribution => - 'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)'; + 'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'Име'; + String get settings_infoName => 'Име'; @override - String get settings_infoId => 'ИД'; + String get settings_infoId => 'ИД'; @override - String get settings_infoStatus => 'Статус'; + String get settings_infoStatus => 'Статус'; @override - String get settings_infoBattery => 'Батерия'; + String get settings_infoBattery => 'Батерия'; @override - String get settings_infoPublicKey => 'Общ публичен ключ'; + String get settings_infoPublicKey => 'Общ публичен ключ'; @override - String get settings_infoContactsCount => 'Брой контакти'; + String get settings_infoContactsCount => 'Брой контакти'; @override - String get settings_infoChannelCount => 'Брой канали'; + String get settings_infoChannelCount => 'Брой канали'; @override - String get settings_presets => 'Предварителни настройки'; + String get settings_presets => + 'Предварителни настройки'; @override - String get settings_frequency => 'Честота (MHz)'; + String get settings_frequency => 'Честота (MHz)'; @override String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => 'Невалидна честота (300-2500 MHz)'; + String get settings_frequencyInvalid => + 'Невалидна честота (300-2500 MHz)'; @override - String get settings_bandwidth => 'Ширина на честотния спектър'; + String get settings_bandwidth => + 'Ширина на честотния спектър'; @override - String get settings_spreadingFactor => 'Фактор на разпространение'; + String get settings_spreadingFactor => + 'Фактор на разпространение'; @override - String get settings_codingRate => 'Такса за кодиране'; + String get settings_codingRate => 'Такса за кодиране'; @override - String get settings_txPower => 'TX Мощност (dBm)'; + String get settings_txPower => 'TX Мощност (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)'; + String get settings_txPowerInvalid => + 'Невалидна мощност на TX (0-22 dBm)'; @override - String get settings_clientRepeat => 'Без електричество – повторение'; + String get settings_clientRepeat => + 'Без електричество – повторение'; @override String get settings_clientRepeatSubtitle => - 'Позволете на това устройство да предава пакети към мрежата за други устройства.'; + 'Позволете на това устройство да предава пакети към мрежата за други устройства.'; @override String get settings_clientRepeatFreqWarning => - 'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.'; + 'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.'; @override String settings_error(String message) { - return 'Грешка: $message'; + return 'Грешка: $message'; } @override - String get appSettings_title => 'Настройки на приложението'; + String get appSettings_title => + 'Настройки на приложението'; @override - String get appSettings_appearance => 'Външен вид'; + String get appSettings_appearance => 'Външен вид'; @override - String get appSettings_theme => 'Тема'; + String get appSettings_theme => 'Тема'; @override - String get appSettings_themeSystem => 'Система по подразбиране'; + String get appSettings_themeSystem => + 'Система по подразбиране'; @override - String get appSettings_themeLight => 'Ярка'; + String get appSettings_themeLight => 'Ярка'; @override - String get appSettings_themeDark => 'Тъмно'; + String get appSettings_themeDark => 'Тъмно'; @override - String get appSettings_language => 'Език'; + String get appSettings_language => 'Език'; @override - String get appSettings_languageSystem => 'Система по подразбиране'; + String get appSettings_languageSystem => + 'Система по подразбиране'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -475,16 +494,16 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -493,716 +512,765 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Руски'; + String get appSettings_languageRu => 'Руски'; @override - String get appSettings_languageUk => 'Украински'; + String get appSettings_languageUk => 'Украински'; @override String get appSettings_enableMessageTracing => - 'Разрешаване на проследяване на съобщения'; + 'Разрешаване на проследяване на съобщения'; @override String get appSettings_enableMessageTracingSubtitle => - 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; + 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; @override - String get appSettings_notifications => 'Уведомления'; + String get appSettings_notifications => 'Уведомления'; @override - String get appSettings_enableNotifications => 'Активирай Известия'; + String get appSettings_enableNotifications => + 'Активирай Известия'; @override String get appSettings_enableNotificationsSubtitle => - 'Получете известия за съобщения и реклами'; + 'Получете известия за съобщения и реклами'; @override String get appSettings_notificationPermissionDenied => - 'Отказвано е разрешение за известия'; + 'Отказвано е разрешение за известия'; @override - String get appSettings_notificationsEnabled => 'Уведомителни са активирани'; + String get appSettings_notificationsEnabled => + 'Уведомителни са активирани'; @override - String get appSettings_notificationsDisabled => 'Известия са изключени'; + String get appSettings_notificationsDisabled => + 'Известия са изключени'; @override - String get appSettings_messageNotifications => 'Уведомления'; + String get appSettings_messageNotifications => 'Уведомления'; @override String get appSettings_messageNotificationsSubtitle => - 'Покажи известие при получаване на нови съобщения'; + 'Покажи известие при получаване на нови съобщения'; @override String get appSettings_channelMessageNotifications => - 'Уведомления за съобщения от канал'; + 'Уведомления за съобщения от канал'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Покажи известие при получаване на съобщения от канали'; + 'Покажи известие при получаване на съобщения от канали'; @override - String get appSettings_advertisementNotifications => 'Уведомления за реклами'; + String get appSettings_advertisementNotifications => + 'Уведомления за реклами'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Покажи известие, когато бъдат открити нови възли.'; + 'Покажи известие, когато бъдат открити нови възли.'; @override - String get appSettings_messaging => 'Съобщения'; + String get appSettings_messaging => 'Съобщения'; @override - String get appSettings_clearPathOnMaxRetry => 'Изчисти Път на Макс Опит'; + String get appSettings_clearPathOnMaxRetry => + 'Изчисти Път на Макс Опит'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Възстанови контактния път след 5 неуспешни опита за изпращане'; + 'Възстанови контактния път след 5 неуспешни опита за изпращане'; @override String get appSettings_pathsWillBeCleared => - 'Пътищата ще бъдат почистени след 5 неуспешни опита.'; + 'Пътищата ще бъдат почистени след 5 неуспешни опита.'; @override String get appSettings_pathsWillNotBeCleared => - 'Пътищата няма да бъдат автоматично изчистени.'; + 'Пътищата няма да бъдат автоматично изчистени.'; @override String get appSettings_autoRouteRotation => - 'Автоматично маршрутизиране на завъртания'; + 'Автоматично маршрутизиране на завъртания'; @override String get appSettings_autoRouteRotationSubtitle => - 'Превключете между най-добрите пътища и режим на наводняване'; + 'Превключете между най-добрите пътища и режим на наводняване'; @override String get appSettings_autoRouteRotationEnabled => - 'Автоматично маршрутизиране вкл.'; + 'Автоматично маршрутизиране вкл.'; @override String get appSettings_autoRouteRotationDisabled => - 'Автоматично маршрутизирането е деактивирано'; + 'Автоматично маршрутизирането е деактивирано'; @override - String get appSettings_battery => 'Батерия'; + String get appSettings_battery => 'Батерия'; @override - String get appSettings_batteryChemistry => 'Химия на батерията'; + String get appSettings_batteryChemistry => + 'Химия на батерията'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Зададено за устройство ($deviceName)'; + return 'Зададено за устройство ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Свържете се с устройство, за да изберете.'; + 'Свържете се с устройство, за да изберете.'; @override String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; @override - String get appSettings_batteryLifepo4 => 'Литиево желязо фосфат (2.6-3.65V)'; + String get appSettings_batteryLifepo4 => + 'Литиево желязо фосфат (2.6-3.65V)'; @override - String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)'; + String get appSettings_batteryLipo => + 'Литиев полимер (3.0-4.2V)'; @override - String get appSettings_mapDisplay => 'Карта за показване'; + String get appSettings_mapDisplay => 'Карта за показване'; @override - String get appSettings_showRepeaters => 'Показване на повторители'; + String get appSettings_showRepeaters => + 'Показване на повторители'; @override String get appSettings_showRepeatersSubtitle => - 'Показване на възпроизвеждащи се възли на картата'; + 'Показване на възпроизвеждащи се възли на картата'; @override - String get appSettings_showChatNodes => 'Покажи Възли на Чат'; + String get appSettings_showChatNodes => 'Покажи Възли на Чат'; @override String get appSettings_showChatNodesSubtitle => - 'Показване на чат възли на картата'; + 'Показване на чат възли на картата'; @override - String get appSettings_showOtherNodes => 'Покажи други възли'; + String get appSettings_showOtherNodes => 'Покажи други възли'; @override String get appSettings_showOtherNodesSubtitle => - 'Покажи други типове възли на картата'; + 'Покажи други типове възли на картата'; @override - String get appSettings_timeFilter => 'Филтриране по време'; + String get appSettings_timeFilter => 'Филтриране по време'; @override - String get appSettings_timeFilterShowAll => 'Покажи всички възли'; + String get appSettings_timeFilterShowAll => + 'Покажи всички възли'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Покажи възли от последните $hours часа'; + return 'Покажи възли от последните $hours часа'; } @override - String get appSettings_mapTimeFilter => 'Филтри за време на картата'; + String get appSettings_mapTimeFilter => + 'Филтри за време на картата'; @override String get appSettings_showNodesDiscoveredWithin => - 'Покажи възлите, открити в:'; + 'Покажи възлите, открити в:'; @override - String get appSettings_allTime => 'Всичко време'; + String get appSettings_allTime => 'Всичко време'; @override - String get appSettings_lastHour => 'Последната минута'; + String get appSettings_lastHour => 'Последната минута'; @override - String get appSettings_last6Hours => 'Последни 6 часа'; + String get appSettings_last6Hours => 'Последни 6 часа'; @override - String get appSettings_last24Hours => 'Последно 24 часа'; + String get appSettings_last24Hours => 'Последно 24 часа'; @override - String get appSettings_lastWeek => 'Миналата седмица'; + String get appSettings_lastWeek => 'Миналата седмица'; @override - String get appSettings_offlineMapCache => 'Кеш на офлайн карти'; + String get appSettings_offlineMapCache => + 'Кеш на офлайн карти'; @override - String get appSettings_unitsTitle => 'единици'; + String get appSettings_unitsTitle => 'единици'; @override - String get appSettings_unitsMetric => 'Метрика (m / km)'; + String get appSettings_unitsMetric => 'Метрика (m / km)'; @override - String get appSettings_unitsImperial => 'Имперска (ft / mi)'; + String get appSettings_unitsImperial => 'Имперска (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Няма избрана област'; + String get appSettings_noAreaSelected => + 'Няма избрана област'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Избрана е област (мащаб $minZoom-$maxZoom)'; + return 'Избрана е област (мащаб $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Отстрани'; + String get appSettings_debugCard => 'Отстрани'; @override String get appSettings_appDebugLogging => - 'Логване за отстраняване на грешки на приложението'; + 'Логване за отстраняване на грешки на приложението'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.'; + 'Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.'; @override String get appSettings_appDebugLoggingEnabled => - 'Режимът за отстраняване на грешки в приложението е активиран.'; + 'Режимът за отстраняване на грешки в приложението е активиран.'; @override String get appSettings_appDebugLoggingDisabled => - 'Логването за отстраняване на грешки в приложението е изключено.'; + 'Логването за отстраняване на грешки в приложението е изключено.'; @override - String get contacts_title => 'Контакти'; + String get contacts_title => 'Контакти'; @override - String get contacts_noContacts => 'Няма контакти към момента.'; + String get contacts_noContacts => + 'Няма контакти към момента.'; @override String get contacts_contactsWillAppear => - 'Контактите ще се появят, когато устройствата рекламират.'; + 'Контактите ще се появят, когато устройствата рекламират.'; @override - String get contacts_unread => 'Непрочетено'; + String get contacts_unread => 'Непрочетено'; @override - String get contacts_searchContactsNoNumber => 'Търси контакти...'; + String get contacts_searchContactsNoNumber => + 'Търси контакти...'; @override String contacts_searchContacts(int number, String str) { - return 'Търсене на контакти...'; + return 'Търсене на контакти...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Търсене на $number$str любими...'; + return 'Търсене на $number$str любими...'; } @override String contacts_searchUsers(int number, String str) { - return 'Търсене на $number$str потребители...'; + return 'Търсене на $number$str потребители...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Търсене на $number$str повтарящи се...'; + return 'Търсене на $number$str повтарящи се...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Търсене на $number$str сървъри в стаята...'; + return 'Търсене на $number$str сървъри в стаята...'; } @override - String get contacts_noUnreadContacts => 'Няма непрочетени контакти'; + String get contacts_noUnreadContacts => + 'Няма непрочетени контакти'; @override - String get contacts_noContactsFound => 'Няма намерени контакти или групи.'; + String get contacts_noContactsFound => + 'Няма намерени контакти или групи.'; @override - String get contacts_deleteContact => 'Изтрий Контакт'; + String get contacts_deleteContact => 'Изтрий Контакт'; @override String contacts_removeConfirm(String contactName) { - return 'Изтрий $contactName от контактите?'; + return 'Изтрий $contactName от контактите?'; } @override - String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент'; + String get contacts_manageRepeater => + 'Управление на Повтарящ се Елемент'; @override - String get contacts_manageRoom => 'Управление на сървър за стая'; + String get contacts_manageRoom => + 'Управление на сървър за стая'; @override - String get contacts_roomLogin => 'Вход в стаята'; + String get contacts_roomLogin => 'Вход в стаята'; @override - String get contacts_openChat => 'Отвори чат'; + String get contacts_openChat => 'Отвори чат'; @override - String get contacts_editGroup => 'Редактирай Група'; + String get contacts_editGroup => 'Редактирай Група'; @override - String get contacts_deleteGroup => 'Изтрий група'; + String get contacts_deleteGroup => 'Изтрий група'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Премахнете \"$groupName\"?'; + return 'Премахнете \"$groupName\"?'; } @override - String get contacts_newGroup => 'Нова група'; + String get contacts_newGroup => 'Нова група'; @override - String get contacts_groupName => 'Група'; + String get contacts_groupName => 'Група'; @override - String get contacts_groupNameRequired => 'Името на групата е задължително.'; + String get contacts_groupNameRequired => + 'Името на групата е задължително.'; @override String contacts_groupAlreadyExists(String name) { - return 'Групата \"$name\" вече съществува.'; + return 'Групата \"$name\" вече съществува.'; } @override - String get contacts_filterContacts => 'Филтрирайте контактите...'; + String get contacts_filterContacts => + 'Филтрирайте контактите...'; @override String get contacts_noContactsMatchFilter => - 'Няма съвпадения с вашия филтър.'; + 'Няма съвпадения с вашия филтър.'; @override - String get contacts_noMembers => 'Няма членове'; + String get contacts_noMembers => 'Няма членове'; @override - String get contacts_lastSeenNow => 'Последно видяно сега'; + String get contacts_lastSeenNow => 'Последно видяно сега'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Последна активност $minutes минути преди'; + return 'Последна активност $minutes минути преди'; } @override - String get contacts_lastSeenHourAgo => 'Последно видяно преди час'; + String get contacts_lastSeenHourAgo => + 'Последно видяно преди час'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Последно видян $hours часа преди.'; + return 'Последно видян $hours часа преди.'; } @override - String get contacts_lastSeenDayAgo => 'Последно видяно преди 1 ден'; + String get contacts_lastSeenDayAgo => + 'Последно видяно преди 1 ден'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Последно видян $days дни преди.'; + return 'Последно видян $days дни преди.'; } @override - String get channels_title => 'Канали'; + String get channels_title => 'Канали'; @override - String get channels_noChannelsConfigured => 'Няма конфигурирани канали'; + String get channels_noChannelsConfigured => + 'Няма конфигурирани канали'; @override - String get channels_addPublicChannel => 'Добави публичен канал'; + String get channels_addPublicChannel => + 'Добави публичен канал'; @override - String get channels_searchChannels => 'Търсене на канали...'; + String get channels_searchChannels => 'Търсене на канали...'; @override - String get channels_noChannelsFound => 'Няма намерени канали'; + String get channels_noChannelsFound => + 'Няма намерени канали'; @override String channels_channelIndex(int index) { - return 'Канал $index'; + return 'Канал $index'; } @override - String get channels_hashtagChannel => 'Канал с хаштаг'; + String get channels_hashtagChannel => 'Канал с хаштаг'; @override - String get channels_public => 'Публично'; + String get channels_public => 'Публично'; @override - String get channels_private => 'Личен'; + String get channels_private => 'Личен'; @override - String get channels_publicChannel => 'Публичен канал'; + String get channels_publicChannel => 'Публичен канал'; @override - String get channels_privateChannel => 'Частен канал'; + String get channels_privateChannel => 'Частен канал'; @override - String get channels_editChannel => 'Редактирай канал'; + String get channels_editChannel => 'Редактирай канал'; @override - String get channels_muteChannel => 'Заглуши канала'; + String get channels_muteChannel => 'Заглуши канала'; @override - String get channels_unmuteChannel => 'Включи известията на канала'; + String get channels_unmuteChannel => + 'Включи известията на канала'; @override - String get channels_deleteChannel => 'Изтрий канала'; + String get channels_deleteChannel => 'Изтрий канала'; @override String channels_deleteChannelConfirm(String name) { - return 'Изтрий \"$name\"? Това не може да бъде отменено.'; + return 'Изтрий \"$name\"? Това не може да бъде отменено.'; } @override String channels_channelDeleteFailed(String name) { - return 'Неуспешно изтриване на канала \"$name\"'; + return 'Неуспешно изтриване на канала \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Каналът \"$name\" е изтрит'; + return 'Каналът \"$name\" е изтрит'; } @override - String get channels_addChannel => 'Добави Канал'; + String get channels_addChannel => 'Добави Канал'; @override - String get channels_channelIndexLabel => 'Индекс на канал'; + String get channels_channelIndexLabel => 'Индекс на канал'; @override - String get channels_channelName => 'Име на канала'; + String get channels_channelName => 'Име на канала'; @override - String get channels_usePublicChannel => 'Използвайте публичен канал'; + String get channels_usePublicChannel => + 'Използвайте публичен канал'; @override - String get channels_standardPublicPsk => 'Стандартен публичен PSK'; + String get channels_standardPublicPsk => + 'Стандартен публичен PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Генерирай случайна PSK'; + String get channels_generateRandomPsk => + 'Генерирай случайна PSK'; @override - String get channels_enterChannelName => 'Моля, въведете име на канал.'; + String get channels_enterChannelName => + 'Моля, въведете име на канал.'; @override String get channels_pskMustBe32Hex => - 'PSK трябва да бъде 32 шестнаредни знака.'; + 'PSK трябва да бъде 32 шестнаредни знака.'; @override String channels_channelAdded(String name) { - return 'Каналът \"$name\" е добавен'; + return 'Каналът \"$name\" е добавен'; } @override String channels_editChannelTitle(int index) { - return 'Редактирай Канал $index'; + return 'Редактирай Канал $index'; } @override - String get channels_smazCompression => 'Компресия SMAZ'; + String get channels_smazCompression => 'Компресия SMAZ'; @override String channels_channelUpdated(String name) { - return 'Каналът \"$name\" е актуализиран'; + return 'Каналът \"$name\" е актуализиран'; } @override - String get channels_publicChannelAdded => 'Публичен канал добавен'; + String get channels_publicChannelAdded => + 'Публичен канал добавен'; @override - String get channels_sortBy => 'Сортирай по'; + String get channels_sortBy => 'Сортирай по'; @override - String get channels_sortManual => 'Ръчно'; + String get channels_sortManual => 'Ръчно'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Последни съобщения'; + String get channels_sortLatestMessages => + 'Последни съобщения'; @override - String get channels_sortUnread => 'Непрочетено'; + String get channels_sortUnread => 'Непрочетено'; @override - String get channels_createPrivateChannel => 'Създай Частен Канал'; + String get channels_createPrivateChannel => + 'Създай Частен Канал'; @override - String get channels_createPrivateChannelDesc => 'Защитено с таен ключ.'; + String get channels_createPrivateChannelDesc => + 'Защитено с таен ключ.'; @override - String get channels_joinPrivateChannel => 'Присъедини се към Частен Канал'; + String get channels_joinPrivateChannel => + 'Присъедини се към Частен Канал'; @override - String get channels_joinPrivateChannelDesc => 'Ръчно въведете таен ключ.'; + String get channels_joinPrivateChannelDesc => + 'Ръчно въведете таен ключ.'; @override String get channels_joinPublicChannel => - 'Присъединете се към Публичния канал'; + 'Присъединете се към Публичния канал'; @override String get channels_joinPublicChannelDesc => - 'Всеки може да се присъедини към този канал.'; + 'Всеки може да се присъедини към този канал.'; @override - String get channels_joinHashtagChannel => 'Присъедини се към Хаштаг Канал'; + String get channels_joinHashtagChannel => + 'Присъедини се към Хаштаг Канал'; @override String get channels_joinHashtagChannelDesc => - 'Всеки може да се присъедини към хаштаговите канали.'; + 'Всеки може да се присъедини към хаштаговите канали.'; @override - String get channels_scanQrCode => 'Сканирайте QR код'; + String get channels_scanQrCode => 'Сканирайте QR код'; @override - String get channels_scanQrCodeComingSoon => 'Ще излезе скоро'; + String get channels_scanQrCodeComingSoon => 'Ще излезе скоро'; @override - String get channels_enterHashtag => 'Въведете хаштаг'; + String get channels_enterHashtag => 'Въведете хаштаг'; @override - String get channels_hashtagHint => 'напр. #отбор'; + String get channels_hashtagHint => 'напр. #отбор'; @override - String get chat_noMessages => 'Няма съобщения.'; + String get chat_noMessages => 'Няма съобщения.'; @override - String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.'; + String get chat_sendMessageToStart => + 'Изпрати съобщение, за да започнеш.'; @override - String get chat_originalMessageNotFound => 'Съобщението не е намерено'; + String get chat_originalMessageNotFound => + 'Съобщението не е намерено'; @override String chat_replyingTo(String name) { - return 'Отговарям на $name'; + return 'Отговарям на $name'; } @override String chat_replyTo(String name) { - return 'Отговори на $name'; + return 'Отговори на $name'; } @override - String get chat_location => 'Местоположение'; + String get chat_location => 'Местоположение'; @override String chat_sendMessageTo(String contactName) { - return 'Изпрати съобщение на $contactName'; + return 'Изпрати съобщение на $contactName'; } @override - String get chat_typeMessage => 'Въведете съобщение...'; + String get chat_typeMessage => 'Въведете съобщение...'; @override String chat_messageTooLong(int maxBytes) { - return 'Съобщението е твърде дълго (макс $maxBytes байта).'; + return 'Съобщението е твърде дълго (макс $maxBytes байта).'; } @override - String get chat_messageCopied => 'Съобщението е копирано'; + String get chat_messageCopied => 'Съобщението е копирано'; @override - String get chat_messageDeleted => 'Съобщението е изтрито'; + String get chat_messageDeleted => 'Съобщението е изтрито'; @override - String get chat_retryingMessage => 'Опитваме се отново.'; + String get chat_retryingMessage => 'Опитваме се отново.'; @override String chat_retryCount(int current, int max) { - return 'Опитай отново $current/$max'; + return 'Опитай отново $current/$max'; } @override - String get chat_sendGif => 'Изпрати GIF'; + String get chat_sendGif => 'Изпрати GIF'; @override - String get chat_reply => 'Отговори'; + String get chat_reply => 'Отговори'; @override - String get chat_addReaction => 'Добави Реакция'; + String get chat_addReaction => 'Добави Реакция'; @override - String get chat_me => 'Аз'; + String get chat_me => 'Аз'; @override - String get emojiCategorySmileys => 'Емотикони'; + String get emojiCategorySmileys => 'Емотикони'; @override - String get emojiCategoryGestures => 'Жестове'; + String get emojiCategoryGestures => 'Жестове'; @override - String get emojiCategoryHearts => 'Сърца'; + String get emojiCategoryHearts => 'Сърца'; @override - String get emojiCategoryObjects => 'Обекти'; + String get emojiCategoryObjects => 'Обекти'; @override - String get gifPicker_title => 'Изберете GIF'; + String get gifPicker_title => 'Изберете GIF'; @override - String get gifPicker_searchHint => 'Търсене на GIF-ове...'; + String get gifPicker_searchHint => 'Търсене на GIF-ове...'; @override - String get gifPicker_poweredBy => 'Задвижвано от GIPHY'; + String get gifPicker_poweredBy => 'Задвижвано от GIPHY'; @override - String get gifPicker_noGifsFound => 'Няма намерени GIF файлове.'; + String get gifPicker_noGifsFound => + 'Няма намерени GIF файлове.'; @override - String get gifPicker_failedLoad => 'Не можа да се заредят GIF файловете'; + String get gifPicker_failedLoad => + 'Не можа да се заредят GIF файловете'; @override - String get gifPicker_failedSearch => 'Неуспешно търсене на GIF-ове'; + String get gifPicker_failedSearch => + 'Неуспешно търсене на GIF-ове'; @override - String get gifPicker_noInternet => 'Няма интернет връзка'; + String get gifPicker_noInternet => 'Няма интернет връзка'; @override String get debugLog_appTitle => - 'Лог на отстраняване на грешки на приложението'; + 'Лог на отстраняване на грешки на приложението'; @override - String get debugLog_bleTitle => 'Лог за отстраняване на грешки на BLE'; + String get debugLog_bleTitle => + 'Лог за отстраняване на грешки на BLE'; @override - String get debugLog_copyLog => 'Копирай лог'; + String get debugLog_copyLog => 'Копирай лог'; @override - String get debugLog_clearLog => 'Изчисти логовете'; + String get debugLog_clearLog => 'Изчисти логовете'; @override - String get debugLog_copied => 'Копирано лого за отстраняване на грешки'; + String get debugLog_copied => + 'Копирано лого за отстраняване на грешки'; @override - String get debugLog_bleCopied => 'Копиран лог от BLE'; + String get debugLog_bleCopied => 'Копиран лог от BLE'; @override - String get debugLog_noEntries => 'Все още няма дебъг логове.'; + String get debugLog_noEntries => + 'Все още няма дебъг логове.'; @override String get debugLog_enableInSettings => - 'Активирайте отстраняване на грешки в настройките на приложението'; + 'Активирайте отстраняване на грешки в настройките на приложението'; @override - String get debugLog_frames => 'Рамки'; + String get debugLog_frames => 'Рамки'; @override String get debugLog_rawLogRx => 'Raw Log-RX'; @override - String get debugLog_noBleActivity => 'Няма BLE активност към момента.'; + String get debugLog_noBleActivity => + 'Няма BLE активност към момента.'; @override String debugFrame_length(int count) { - return 'Дължина на кадъра: $count байта'; + return 'Дължина на кадъра: $count байта'; } @override String debugFrame_command(String value) { - return 'Команда: 0x$value'; + return 'Команда: 0x$value'; } @override - String get debugFrame_textMessageHeader => 'Съобщение:'; + String get debugFrame_textMessageHeader => 'Съобщение:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Дестинация Публичен Ключ: $pubKey'; + return '- Дестинация Публичен Ключ: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Време: $timestamp'; + return '- Време: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Флагове: 0x$value'; + return '- Флагове: 0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- Тип текст: $type ($label)'; + return '- Тип текст: $type ($label)'; } @override String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Просто'; + String get debugFrame_textTypePlain => 'Просто'; @override String debugFrame_text(String text) { - return '- Текст: \"$text\"'; + return '- Текст: \"$text\"'; } @override - String get debugFrame_hexDump => 'Хексадесетичен Dump:'; + String get debugFrame_hexDump => 'Хексадесетичен Dump:'; @override - String get chat_pathManagement => 'Управление на пътища'; + String get chat_pathManagement => 'Управление на пътища'; @override - String get chat_ShowAllPaths => 'Покажи всички пътища'; + String get chat_ShowAllPaths => 'Покажи всички пътища'; @override - String get chat_routingMode => 'Режим на маршрутизиране'; + String get chat_routingMode => 'Режим на маршрутизиране'; @override - String get chat_autoUseSavedPath => 'Автоматично (използвай запазения път)'; + String get chat_autoUseSavedPath => + 'Автоматично (използвай запазения път)'; @override - String get chat_forceFloodMode => 'Принуди режим на наводняване'; + String get chat_forceFloodMode => + 'Принуди режим на наводняване'; @override String get chat_recentAckPaths => - 'Неотдавни ACK пътища (докоснете, за да използвате):'; + 'Неотдавни ACK пътища (докоснете, за да използвате):'; @override String get chat_pathHistoryFull => - 'Историята на пътя е пълна. Премахнете записи, за да добавите нови.'; + 'Историята на пътя е пълна. Премахнете записи, за да добавите нови.'; @override - String get chat_hopSingular => 'скочи'; + String get chat_hopSingular => 'скочи'; @override - String get chat_hopPlural => 'скоци'; + String get chat_hopPlural => 'скоци'; @override String chat_hopsCount(int count) { @@ -1216,49 +1284,51 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get chat_successes => 'Успехи'; + String get chat_successes => 'Успехи'; @override - String get chat_removePath => 'Премахни пътя'; + String get chat_removePath => 'Премахни пътя'; @override String get chat_noPathHistoryYet => - 'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.'; + 'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.'; @override - String get chat_pathActions => 'Действия по пътя:'; + String get chat_pathActions => 'Действия по пътя:'; @override - String get chat_setCustomPath => 'Задайте персонализиран път'; + String get chat_setCustomPath => + 'Задайте персонализиран път'; @override - String get chat_setCustomPathSubtitle => 'Ръчно укажете маршрутен път'; + String get chat_setCustomPathSubtitle => + 'Ръчно укажете маршрутен път'; @override - String get chat_clearPath => 'Почисти Път'; + String get chat_clearPath => 'Почисти Път'; @override String get chat_clearPathSubtitle => - 'Принуди преоткриване при следващо изпращане'; + 'Принуди преоткриване при следващо изпращане'; @override String get chat_pathCleared => - 'Пътят е почистен. Следващото съобщение ще открие маршрута отново.'; + 'Пътят е почистен. Следващото съобщение ще открие маршрута отново.'; @override String get chat_floodModeSubtitle => - 'Използвайте превключвателя за маршрутизиране в лентата на приложението.'; + 'Използвайте превключвателя за маршрутизиране в лентата на приложението.'; @override String get chat_floodModeEnabled => - 'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.'; + 'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.'; @override - String get chat_fullPath => 'Пълен път'; + String get chat_fullPath => 'Пълен път'; @override String get chat_pathDetailsNotAvailable => - 'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.'; + 'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1268,300 +1338,313 @@ class AppLocalizationsBg extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Пътят е зададен: $hopCount $_temp0 - $status'; + return 'Пътят е зададен: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Запазено локално. Свържете се за синхронизиране.'; + 'Запазено локално. Свържете се за синхронизиране.'; @override - String get chat_pathDeviceConfirmed => 'Устройство потвърдено.'; + String get chat_pathDeviceConfirmed => + 'Устройство потвърдено.'; @override String get chat_pathDeviceNotConfirmed => - 'Устройството все още не е потвърдено.'; + 'Устройството все още не е потвърдено.'; @override - String get chat_type => 'Въведете'; + String get chat_type => 'Въведете'; @override - String get chat_path => 'Пътекино'; + String get chat_path => 'Пътекино'; @override - String get chat_publicKey => 'Публичен ключ'; + String get chat_publicKey => 'Публичен ключ'; @override String get chat_compressOutgoingMessages => - 'Компресиране на изходящи съобщения'; + 'Компресиране на изходящи съобщения'; @override - String get chat_floodForced => 'Потоп (принуден)'; + String get chat_floodForced => 'Потоп (принуден)'; @override - String get chat_directForced => 'Директно (принудително)'; + String get chat_directForced => 'Директно (принудително)'; @override String chat_hopsForced(int count) { - return '$count скока (принудително)'; + return '$count скока (принудително)'; } @override - String get chat_floodAuto => 'Потоп (автоматично)'; + String get chat_floodAuto => 'Потоп (автоматично)'; @override - String get chat_direct => 'Директно'; + String get chat_direct => 'Директно'; @override - String get chat_poiShared => 'Споделено място от интерес'; + String get chat_poiShared => + 'Споделено място от интерес'; @override String chat_unread(int count) { - return 'Непрочетени: $count'; + return 'Непрочетени: $count'; } @override - String get chat_openLink => 'Отваряне на връзката?'; + String get chat_openLink => 'Отваряне на връзката?'; @override String get chat_openLinkConfirmation => - 'Искате ли да отворите тази връзка в браузъра си?'; + 'Искате ли да отворите тази връзка в браузъра си?'; @override - String get chat_open => 'Отвори'; + String get chat_open => 'Отвори'; @override String chat_couldNotOpenLink(String url) { - return 'Не можа да се отвори връзката: $url'; + return 'Не можа да се отвори връзката: $url'; } @override - String get chat_invalidLink => 'Невалиден формат на връзката'; + String get chat_invalidLink => + 'Невалиден формат на връзката'; @override - String get map_title => 'Карта на възлите'; + String get map_title => 'Карта на възлите'; @override - String get map_lineOfSight => 'Линия на видимост'; + String get map_lineOfSight => 'Линия на видимост'; @override - String get map_losScreenTitle => 'Линия на видимост'; + String get map_losScreenTitle => 'Линия на видимост'; @override - String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.'; + String get map_noNodesWithLocation => + 'Няма възли с данни за местоположение.'; @override String get map_nodesNeedGps => - 'Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.'; + 'Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.'; @override String map_nodesCount(int count) { - return 'Нодове: $count'; + return 'Нодове: $count'; } @override String map_pinsCount(int count) { - return 'Ключове: $count'; + return 'Ключове: $count'; } @override - String get map_chat => 'Чат'; + String get map_chat => 'Чат'; @override - String get map_repeater => 'Повтарящ се'; + String get map_repeater => 'Повтарящ се'; @override - String get map_room => 'Стая'; + String get map_room => 'Стая'; @override - String get map_sensor => 'Датчик'; + String get map_sensor => 'Датчик'; @override - String get map_pinDm => 'Задържане (DM)'; + String get map_pinDm => 'Задържане (DM)'; @override - String get map_pinPrivate => 'Задържане (Приватно)'; + String get map_pinPrivate => 'Задържане (Приватно)'; @override - String get map_pinPublic => 'Публичен ключ'; + String get map_pinPublic => 'Публичен ключ'; @override - String get map_lastSeen => 'Последна видяна'; + String get map_lastSeen => 'Последна видяна'; @override String get map_disconnectConfirm => - 'Сигурни ли сте, че искате да се откъснете от това устройство?'; + 'Сигурни ли сте, че искате да се откъснете от това устройство?'; @override - String get map_from => 'От'; + String get map_from => 'От'; @override - String get map_source => 'Източник'; + String get map_source => 'Източник'; @override - String get map_flags => 'Флаг'; + String get map_flags => 'Флаг'; @override - String get map_shareMarkerHere => 'Споделете маркер тук'; + String get map_shareMarkerHere => 'Споделете маркер тук'; @override - String get map_pinLabel => 'Етикетиране на пин'; + String get map_pinLabel => 'Етикетиране на пин'; @override - String get map_label => 'Етикет'; + String get map_label => 'Етикет'; @override - String get map_pointOfInterest => 'Точка на интерес'; + String get map_pointOfInterest => 'Точка на интерес'; @override - String get map_sendToContact => 'Изпрати на контакт'; + String get map_sendToContact => 'Изпрати на контакт'; @override - String get map_sendToChannel => 'Изпрати в канала'; + String get map_sendToChannel => 'Изпрати в канала'; @override - String get map_noChannelsAvailable => 'Няма налични канали'; + String get map_noChannelsAvailable => 'Няма налични канали'; @override - String get map_publicLocationShare => 'Споделяне на публично място'; + String get map_publicLocationShare => + 'Споделяне на публично място'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Ще споделите местоположение в $channelLabel. Този канал е публичен и всеки с PSK може да го види.'; + return 'Ще споделите местоположение в $channelLabel. Този канал е публичен и всеки с PSK може да го види.'; } @override String get map_connectToShareMarkers => - 'Свържете се с устройство, за да споделите маркери.'; + 'Свържете се с устройство, за да споделите маркери.'; @override - String get map_filterNodes => 'Филтрирайте възли'; + String get map_filterNodes => 'Филтрирайте възли'; @override - String get map_nodeTypes => 'Типове възли'; + String get map_nodeTypes => 'Типове възли'; @override - String get map_chatNodes => 'Възли на чата'; + String get map_chatNodes => 'Възли на чата'; @override - String get map_repeaters => 'Повторители'; + String get map_repeaters => 'Повторители'; @override - String get map_otherNodes => 'Други възли'; + String get map_otherNodes => 'Други възли'; @override - String get map_keyPrefix => 'Префикс на ключа'; + String get map_keyPrefix => 'Префикс на ключа'; @override - String get map_filterByKeyPrefix => 'Филтрирайте по префикс на ключ'; + String get map_filterByKeyPrefix => + 'Филтрирайте по префикс на ключ'; @override - String get map_publicKeyPrefix => 'Префикс на публичен ключ'; + String get map_publicKeyPrefix => + 'Префикс на публичен ключ'; @override - String get map_markers => 'Маркери'; + String get map_markers => 'Маркери'; @override - String get map_showSharedMarkers => 'Покажи споделени маркери'; + String get map_showSharedMarkers => + 'Покажи споделени маркери'; @override - String get map_lastSeenTime => 'Последна видяна дата'; + String get map_lastSeenTime => 'Последна видяна дата'; @override - String get map_sharedPin => 'Споделено копие'; + String get map_sharedPin => 'Споделено копие'; @override - String get map_joinRoom => 'Присъедини се към стаята'; + String get map_joinRoom => 'Присъедини се към стаята'; @override - String get map_manageRepeater => 'Управление на Повтарящ се Елемент'; + String get map_manageRepeater => + 'Управление на Повтарящ се Елемент'; @override String get map_tapToAdd => - 'Натиснете върху възлите, за да ги добавите към пътя.'; + 'Натиснете върху възлите, за да ги добавите към пътя.'; @override - String get map_runTrace => 'Изпълни Път на Следване'; + String get map_runTrace => 'Изпълни Път на Следване'; @override - String get map_removeLast => 'Премахни Последно'; + String get map_removeLast => 'Премахни Последно'; @override - String get map_pathTraceCancelled => 'Отменен е следването на пътя.'; + String get map_pathTraceCancelled => + 'Отменен е следването на пътя.'; @override - String get mapCache_title => 'Кеш на офлайн карти'; + String get mapCache_title => 'Кеш на офлайн карти'; @override - String get mapCache_selectAreaFirst => 'Изберете област за кеширане първа'; + String get mapCache_selectAreaFirst => + 'Изберете област за кеширане първа'; @override String get mapCache_noTilesToDownload => - 'Няма плочки за изтегляне за тази област.'; + 'Няма плочки за изтегляне за тази област.'; @override - String get mapCache_downloadTilesTitle => 'Изтегли плочки'; + String get mapCache_downloadTilesTitle => 'Изтегли плочки'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Изтегли $count плочки за офлайн употреба?'; + return 'Изтегли $count плочки за офлайн употреба?'; } @override - String get mapCache_downloadAction => 'Изтегли'; + String get mapCache_downloadAction => 'Изтегли'; @override String mapCache_cachedTiles(int count) { - return 'Кеширани $count плочки'; + return 'Кеширани $count плочки'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Запазени $downloaded плочки ($failed неуспешни)'; + return 'Запазени $downloaded плочки ($failed неуспешни)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Изчисти офлайн кеша'; + String get mapCache_clearOfflineCacheTitle => + 'Изчисти офлайн кеша'; @override String get mapCache_clearOfflineCachePrompt => - 'Премахнете всички кеширани плочки на картата?'; + 'Премахнете всички кеширани плочки на картата?'; @override String get mapCache_offlineCacheCleared => - 'Кешът на устройството е изчистен.'; + 'Кешът на устройството е изчистен.'; @override - String get mapCache_noAreaSelected => 'Няма избрана област'; + String get mapCache_noAreaSelected => 'Няма избрана област'; @override - String get mapCache_cacheArea => 'Област с кеш'; + String get mapCache_cacheArea => 'Област с кеш'; @override - String get mapCache_useCurrentView => 'Използвайте текущия изглед'; + String get mapCache_useCurrentView => + 'Използвайте текущия изглед'; @override - String get mapCache_zoomRange => 'Обхват на увеличението'; + String get mapCache_zoomRange => 'Обхват на увеличението'; @override String mapCache_estimatedTiles(int count) { - return 'Очаквани плочки: $count'; + return 'Очаквани плочки: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Изтеглено $completed / $total'; + return 'Изтеглено $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Изтегли Плочки'; + String get mapCache_downloadTilesButton => 'Изтегли Плочки'; @override - String get mapCache_clearCacheButton => 'Изчисти кеша'; + String get mapCache_clearCacheButton => 'Изчисти кеша'; @override String mapCache_failedDownloads(int count) { - return 'Неуспешни изтегляния: $count'; + return 'Неуспешни изтегляния: $count'; } @override @@ -1571,132 +1654,135 @@ class AppLocalizationsBg extends AppLocalizations { String east, String west, ) { - return 'Север $north, Юг $south, Изток $east, Запад $west'; + return 'Север $north, Юг $south, Изток $east, Запад $west'; } @override - String get time_justNow => 'Сега'; + String get time_justNow => 'Сега'; @override String time_minutesAgo(int minutes) { - return '$minutes минути преди'; + return '$minutes минути преди'; } @override String time_hoursAgo(int hours) { - return '$hours часа преди'; + return '$hours часа преди'; } @override String time_daysAgo(int days) { - return '$days дни преди'; + return '$days дни преди'; } @override - String get time_hour => 'час'; + String get time_hour => 'час'; @override - String get time_hours => 'часове'; + String get time_hours => 'часове'; @override - String get time_day => 'ден'; + String get time_day => 'ден'; @override - String get time_days => 'дни'; + String get time_days => 'дни'; @override - String get time_week => 'седмица'; + String get time_week => 'седмица'; @override - String get time_weeks => 'секти'; + String get time_weeks => 'секти'; @override - String get time_month => 'месец'; + String get time_month => 'месец'; @override - String get time_months => 'месеци'; + String get time_months => 'месеци'; @override - String get time_minutes => 'минути'; + String get time_minutes => 'минути'; @override - String get time_allTime => 'Всичко време'; + String get time_allTime => 'Всичко време'; @override - String get dialog_disconnect => 'Прекъсни'; + String get dialog_disconnect => 'Прекъсни'; @override String get dialog_disconnectConfirm => - 'Сигурни ли сте, че искате да се откъснете от това устройство?'; + 'Сигурни ли сте, че искате да се откъснете от това устройство?'; @override - String get login_repeaterLogin => 'Повторител Вход'; + String get login_repeaterLogin => 'Повторител Вход'; @override - String get login_roomLogin => 'Вход в стаята'; + String get login_roomLogin => 'Вход в стаята'; @override - String get login_password => 'Парола'; + String get login_password => 'Парола'; @override - String get login_enterPassword => 'Въведете парола'; + String get login_enterPassword => 'Въведете парола'; @override - String get login_savePassword => 'Запази парола'; + String get login_savePassword => 'Запази парола'; @override String get login_savePasswordSubtitle => - 'Паролата ще бъде съхранена сигурно на това устройство.'; + 'Паролата ще бъде съхранена сигурно на това устройство.'; @override String get login_repeaterDescription => - 'Въведете паролата на репитера, за да получите достъп до настройките и статуса.'; + 'Въведете паролата на репитера, за да получите достъп до настройките и статуса.'; @override String get login_roomDescription => - 'Въведете паролата на стаята, за да получите достъп до настройките и статуса.'; + 'Въведете паролата на стаята, за да получите достъп до настройките и статуса.'; @override - String get login_routing => 'Маршрутизиране'; + String get login_routing => 'Маршрутизиране'; @override - String get login_routingMode => 'Режим на маршрутизиране'; + String get login_routingMode => + 'Режим на маршрутизиране'; @override - String get login_autoUseSavedPath => 'Автоматично (използвай запазения път)'; + String get login_autoUseSavedPath => + 'Автоматично (използвай запазения път)'; @override - String get login_forceFloodMode => 'Принуди режим на наводняване'; + String get login_forceFloodMode => + 'Принуди режим на наводняване'; @override - String get login_managePaths => 'Управление на пътища'; + String get login_managePaths => 'Управление на пътища'; @override - String get login_login => 'Вход'; + String get login_login => 'Вход'; @override String login_attempt(int current, int max) { - return 'Опитвате $current/$max'; + return 'Опитвате $current/$max'; } @override String login_failed(String error) { - return 'Входът не беше успешен: $error'; + return 'Входът не беше успешен: $error'; } @override String get login_failedMessage => - 'Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.'; + 'Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.'; @override - String get common_reload => 'Презареди'; + String get common_reload => 'Презареди'; @override - String get common_clear => 'Изчисти'; + String get common_clear => 'Изчисти'; @override String path_currentPath(String path) { - return 'Текущ път: $path'; + return 'Текущ път: $path'; } @override @@ -1707,153 +1793,167 @@ class AppLocalizationsBg extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Използване на $count $_temp0 път'; + return 'Използване на $count $_temp0 път'; } @override - String get path_enterCustomPath => 'Въведете персонализиран път'; + String get path_enterCustomPath => + 'Въведете персонализиран път'; @override - String get path_currentPathLabel => 'Текущ път'; + String get path_currentPathLabel => 'Текущ път'; @override String get path_hexPrefixInstructions => - 'Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.'; + 'Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (всяка нода използва първия байт от публичния си ключ)'; + 'A1,F2,3C (всяка нода използва първия байт от публичния си ключ)'; @override - String get path_labelHexPrefixes => 'Пътеки (шестнадесетични префикси)'; + String get path_labelHexPrefixes => + 'Пътеки (шестнадесетични префикси)'; @override String get path_helperMaxHops => - 'Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).'; + 'Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).'; @override - String get path_selectFromContacts => 'Изберете от контакти:'; + String get path_selectFromContacts => + 'Изберете от контакти:'; @override String get path_noRepeatersFound => - 'Няма намерени репетитори или сървъри на стаи.'; + 'Няма намерени репетитори или сървъри на стаи.'; @override String get path_customPathsRequire => - 'Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.'; + 'Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Невалидни шестнадесетични префикси: $prefixes'; + return 'Невалидни шестнадесетични префикси: $prefixes'; } @override String get path_tooLong => - 'Пътят е твърде дълъг. Максимум 64 скока са разрешени.'; + 'Пътят е твърде дълъг. Максимум 64 скока са разрешени.'; @override - String get path_setPath => 'Задайте път'; + String get path_setPath => 'Задайте път'; @override - String get repeater_management => 'Управление на повторители'; + String get repeater_management => + 'Управление на повторители'; @override - String get room_management => 'Управление на сървъра за стая'; + String get room_management => + 'Управление на сървъра за стая'; @override - String get repeater_managementTools => 'Инструменти за управление'; + String get repeater_managementTools => + 'Инструменти за управление'; @override - String get repeater_status => 'Статус'; + String get repeater_status => 'Статус'; @override String get repeater_statusSubtitle => - 'Прегледайте статуса, статистиката и съседните устройства.'; + 'Прегледайте статуса, статистиката и съседните устройства.'; @override - String get repeater_telemetry => 'Телеметрия'; + String get repeater_telemetry => 'Телеметрия'; @override String get repeater_telemetrySubtitle => - 'Прегледайте телеметрията на сензорите и системните статистики'; + 'Прегледайте телеметрията на сензорите и системните статистики'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора'; + String get repeater_cliSubtitle => + 'Изпрати команди към ретранслатора'; @override - String get repeater_neighbors => 'Съседи'; + String get repeater_neighbors => 'Съседи'; @override String get repeater_neighborsSubtitle => - 'Преглед на съседни възли с нулев скок.'; + 'Преглед на съседни възли с нулев скок.'; @override - String get repeater_settings => 'Настройки'; + String get repeater_settings => 'Настройки'; @override String get repeater_settingsSubtitle => - 'Конфигурирайте параметрите на репитера'; + 'Конфигурирайте параметрите на репитера'; @override - String get repeater_statusTitle => 'Статус на повтарянето'; + String get repeater_statusTitle => 'Статус на повтарянето'; @override - String get repeater_routingMode => 'Режим на маршрутизиране'; + String get repeater_routingMode => + 'Режим на маршрутизиране'; @override String get repeater_autoUseSavedPath => - 'Автоматично (използвай запазения път)'; + 'Автоматично (използвай запазения път)'; @override - String get repeater_forceFloodMode => 'Принуди режим на наводняване'; + String get repeater_forceFloodMode => + 'Принуди режим на наводняване'; @override - String get repeater_pathManagement => 'Управление на пътища'; + String get repeater_pathManagement => + 'Управление на пътища'; @override - String get repeater_refresh => 'Презареди'; + String get repeater_refresh => 'Презареди'; @override String get repeater_statusRequestTimeout => - 'Заявката за статус премина прекалено дълго.'; + 'Заявката за статус премина прекалено дълго.'; @override String repeater_errorLoadingStatus(String error) { - return 'Грешка при зареждане на статуса: $error'; + return 'Грешка при зареждане на статуса: $error'; } @override - String get repeater_systemInformation => 'Информация за системата'; + String get repeater_systemInformation => + 'Информация за системата'; @override - String get repeater_battery => 'Батерия'; + String get repeater_battery => 'Батерия'; @override - String get repeater_clockAtLogin => 'Часовник (при влизане)'; + String get repeater_clockAtLogin => + 'Часовник (при влизане)'; @override - String get repeater_uptime => 'Наличност'; + String get repeater_uptime => 'Наличност'; @override - String get repeater_queueLength => 'Дължина на опашката'; + String get repeater_queueLength => 'Дължина на опашката'; @override - String get repeater_debugFlags => 'Контролни точки за отстраняване на грешки'; + String get repeater_debugFlags => + 'Контролни точки за отстраняване на грешки'; @override - String get repeater_radioStatistics => 'Статистика на радиостанциите'; + String get repeater_radioStatistics => + 'Статистика на радиостанциите'; @override - String get repeater_lastRssi => 'Последна RSSI'; + String get repeater_lastRssi => 'Последна RSSI'; @override - String get repeater_lastSnr => 'Последна SNR'; + String get repeater_lastSnr => 'Последна SNR'; @override - String get repeater_noiseFloor => 'Ниво на шум'; + String get repeater_noiseFloor => 'Ниво на шум'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1862,16 +1962,17 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Статистика на пакетите'; + String get repeater_packetStatistics => + 'Статистика на пакетите'; @override - String get repeater_sent => 'Изпратено'; + String get repeater_sent => 'Изпратено'; @override - String get repeater_received => 'Получено'; + String get repeater_received => 'Получено'; @override - String get repeater_duplicates => 'Дубликати'; + String get repeater_duplicates => 'Дубликати'; @override String repeater_daysHoursMinsSecs( @@ -1880,59 +1981,65 @@ class AppLocalizationsBg extends AppLocalizations { int minutes, int seconds, ) { - return '$days дни $hoursч $minutesм $secondsс'; + return '$days дни $hoursч $minutesм $secondsс'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Общо: $total, Наводнение: $flood, Директно: $direct'; + return 'Общо: $total, Наводнение: $flood, Директно: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Общо: $total, Наводнение: $flood, Директно: $direct'; + return 'Общо: $total, Наводнение: $flood, Директно: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Поливане: $flood, Директен: $direct'; + return 'Поливане: $flood, Директен: $direct'; } @override String repeater_duplicatesTotal(int total) { - return 'Общо: $total'; + return 'Общо: $total'; } @override - String get repeater_settingsTitle => 'Настройки на повтарящия се елемент'; + String get repeater_settingsTitle => + 'Настройки на повтарящия се елемент'; @override - String get repeater_basicSettings => 'Основни настройки'; + String get repeater_basicSettings => 'Основни настройки'; @override - String get repeater_repeaterName => 'Име на повтарящ се елемент'; + String get repeater_repeaterName => + 'Име на повтарящ се елемент'; @override String get repeater_repeaterNameHelper => - 'Показване на името на този репитер'; + 'Показване на името на този репитер'; @override - String get repeater_adminPassword => 'Парола на администратора'; + String get repeater_adminPassword => + 'Парола на администратора'; @override - String get repeater_adminPasswordHelper => 'Пълен достъпен парола'; + String get repeater_adminPasswordHelper => + 'Пълен достъпен парола'; @override - String get repeater_guestPassword => 'Парола на гост'; + String get repeater_guestPassword => 'Парола на гост'; @override - String get repeater_guestPasswordHelper => 'Достъп с ограничен достъп'; + String get repeater_guestPasswordHelper => + 'Достъп с ограничен достъп'; @override - String get repeater_radioSettings => 'Настройки на радиостанцията'; + String get repeater_radioSettings => + 'Настройки на радиостанцията'; @override - String get repeater_frequencyMhz => 'Честота (MHz)'; + String get repeater_frequencyMhz => 'Честота (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -1944,516 +2051,547 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Ширина на честотния спектър'; + String get repeater_bandwidth => + 'Ширина на честотния спектър'; @override - String get repeater_spreadingFactor => 'Фактор на разпространение'; + String get repeater_spreadingFactor => + 'Фактор на разпространение'; @override - String get repeater_codingRate => 'Такса за кодиране'; + String get repeater_codingRate => 'Такса за кодиране'; @override - String get repeater_locationSettings => 'Настройки на местоположението'; + String get repeater_locationSettings => + 'Настройки на местоположението'; @override - String get repeater_latitude => 'Широчина'; + String get repeater_latitude => 'Широчина'; @override - String get repeater_latitudeHelper => 'Десетични градуси (напр. 37.7749)'; + String get repeater_latitudeHelper => + 'Десетични градуси (напр. 37.7749)'; @override - String get repeater_longitude => 'Дължина'; + String get repeater_longitude => 'Дължина'; @override String get repeater_longitudeHelper => - 'Градуси с десетични знаци (напр. -122.4194)'; + 'Градуси с десетични знаци (напр. -122.4194)'; @override - String get repeater_features => 'Характеристики'; + String get repeater_features => 'Характеристики'; @override - String get repeater_packetForwarding => 'Пренасочване на пакети'; + String get repeater_packetForwarding => + 'Пренасочване на пакети'; @override String get repeater_packetForwardingSubtitle => - 'Активирайте репитера, за да препращате пакети.'; + 'Активирайте репитера, за да препращате пакети.'; @override - String get repeater_guestAccess => 'Достъп за Гост'; + String get repeater_guestAccess => 'Достъп за Гост'; @override - String get repeater_guestAccessSubtitle => 'Разрешете самочетене за гости'; + String get repeater_guestAccessSubtitle => + 'Разрешете самочетене за гости'; @override - String get repeater_privacyMode => 'Режим на поверителност'; + String get repeater_privacyMode => + 'Режим на поверителност'; @override String get repeater_privacyModeSubtitle => - 'Скриване на име/местоположение в рекламите'; + 'Скриване на име/местоположение в рекламите'; @override - String get repeater_advertisementSettings => 'Настройки на рекламите'; + String get repeater_advertisementSettings => + 'Настройки на рекламите'; @override - String get repeater_localAdvertInterval => 'Местен Рекламен Интервал'; + String get repeater_localAdvertInterval => + 'Местен Рекламен Интервал'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes минути'; + return '$minutes минути'; } @override String get repeater_floodAdvertInterval => - 'Интервал на рекламата за наводнения'; + 'Интервал на рекламата за наводнения'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours часа'; + return '$hours часа'; } @override - String get repeater_encryptedAdvertInterval => 'Криптиран Рекламен Интервал'; + String get repeater_encryptedAdvertInterval => + 'Криптиран Рекламен Интервал'; @override String get repeater_dangerZone => - 'Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно'; + 'Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно'; @override - String get repeater_rebootRepeater => 'БеРестартирай Репитер'; + String get repeater_rebootRepeater => + 'БеРестартирай Репитер'; @override - String get repeater_rebootRepeaterSubtitle => 'Рестартирайте ретранслатора.'; + String get repeater_rebootRepeaterSubtitle => + 'Рестартирайте ретранслатора.'; @override String get repeater_rebootRepeaterConfirm => - 'Сигурни ли сте, че искате да рестартирате този репитер?'; + 'Сигурни ли сте, че искате да рестартирате този репитер?'; @override String get repeater_regenerateIdentityKey => - 'Генериране на Ключ за Идентичност'; + 'Генериране на Ключ за Идентичност'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Генериране на нова двойка публичен/частен ключ'; + 'Генериране на нова двойка публичен/частен ключ'; @override String get repeater_regenerateIdentityKeyConfirm => - 'БеТова ще генерира нова идентичност за репитера. Продължете?'; + 'БеТова ще генерира нова идентичност за репитера. Продължете?'; @override - String get repeater_eraseFileSystem => 'Изтрий Файлова Система'; + String get repeater_eraseFileSystem => + 'Изтрий Файлова Система'; @override String get repeater_eraseFileSystemSubtitle => - 'Форматирайте файла на репитера'; + 'Форматирайте файла на репитера'; @override String get repeater_eraseFileSystemConfirm => - 'ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!'; + 'ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!'; @override String get repeater_eraseSerialOnly => - 'Изтриването е достъпно само през серийния терминал.'; + 'Изтриването е достъпно само през серийния терминал.'; @override String repeater_commandSent(String command) { - return 'Командата е изпратена: $command'; + return 'Командата е изпратена: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Грешка при изпращане на командата: $error'; + return 'Грешка при изпращане на командата: $error'; } @override - String get repeater_confirm => 'БеПотвърди'; + String get repeater_confirm => 'БеПотвърди'; @override - String get repeater_settingsSaved => 'Настройките са запазени успешно.'; + String get repeater_settingsSaved => + 'Настройките са запазени успешно.'; @override String repeater_errorSavingSettings(String error) { - return 'Грешка при запазване на настройките: $error'; + return 'Грешка при запазване на настройките: $error'; } @override - String get repeater_refreshBasicSettings => 'Обнови Основни Настройки'; + String get repeater_refreshBasicSettings => + 'Обнови Основни Настройки'; @override String get repeater_refreshRadioSettings => - 'Обнови настройките на радиопредавателите'; + 'Обнови настройките на радиопредавателите'; @override - String get repeater_refreshTxPower => 'Обнови TX захранване'; + String get repeater_refreshTxPower => 'Обнови TX захранване'; @override String get repeater_refreshLocationSettings => - 'Обнови настройките на местоположението'; + 'Обнови настройките на местоположението'; @override - String get repeater_refreshPacketForwarding => 'Обнови пакетно пренасочване'; + String get repeater_refreshPacketForwarding => + 'Обнови пакетно пренасочване'; @override - String get repeater_refreshGuestAccess => 'Обнови достъп за гости'; + String get repeater_refreshGuestAccess => + 'Обнови достъп за гости'; @override - String get repeater_refreshPrivacyMode => 'Обнови Режим на поверителност'; + String get repeater_refreshPrivacyMode => + 'Обнови Режим на поверителност'; @override String get repeater_refreshAdvertisementSettings => - 'Обнови Настройки на Рекламата'; + 'Обнови Настройки на Рекламата'; @override String repeater_refreshed(String label) { - return '$label е обновено'; + return '$label е обновено'; } @override String repeater_errorRefreshing(String label) { - return 'Грешка при обновяване на $label'; + return 'Грешка при обновяване на $label'; } @override - String get repeater_cliTitle => 'Повторител CLI'; + String get repeater_cliTitle => 'Повторител CLI'; @override - String get repeater_debugNextCommand => 'Поправи Следваща Команда'; + String get repeater_debugNextCommand => + 'Поправи Следваща Команда'; @override - String get repeater_commandHelp => 'Помощ'; + String get repeater_commandHelp => 'Помощ'; @override - String get repeater_clearHistory => 'Изчисти История'; + String get repeater_clearHistory => 'Изчисти История'; @override - String get repeater_noCommandsSent => 'Няма изпратени команди засега.'; + String get repeater_noCommandsSent => + 'Няма изпратени команди засега.'; @override String get repeater_typeCommandOrUseQuick => - 'Въведете команда по-долу или използвайте бързи команди'; + 'Въведете команда по-долу или използвайте бързи команди'; @override - String get repeater_enterCommandHint => 'Въведете команда...'; + String get repeater_enterCommandHint => 'Въведете команда...'; @override - String get repeater_previousCommand => 'Предходна команда'; + String get repeater_previousCommand => 'Предходна команда'; @override - String get repeater_nextCommand => 'Следваща команда'; + String get repeater_nextCommand => 'Следваща команда'; @override - String get repeater_enterCommandFirst => 'Въведете първо команда.'; + String get repeater_enterCommandFirst => + 'Въведете първо команда.'; @override - String get repeater_cliCommandFrameTitle => 'Рамка за команда CLI'; + String get repeater_cliCommandFrameTitle => + 'Рамка за команда CLI'; @override String repeater_cliCommandError(String error) { - return 'Грешка: $error'; + return 'Грешка: $error'; } @override - String get repeater_cliQuickGetName => 'Получи име'; + String get repeater_cliQuickGetName => 'Получи име'; @override - String get repeater_cliQuickGetRadio => 'Получи радио'; + String get repeater_cliQuickGetRadio => 'Получи радио'; @override - String get repeater_cliQuickGetTx => 'Получи TX'; + String get repeater_cliQuickGetTx => 'Получи TX'; @override - String get repeater_cliQuickNeighbors => 'Съседи'; + String get repeater_cliQuickNeighbors => 'Съседи'; @override - String get repeater_cliQuickVersion => 'Версия'; + String get repeater_cliQuickVersion => 'Версия'; @override - String get repeater_cliQuickAdvertise => 'Рекламирай'; + String get repeater_cliQuickAdvertise => 'Рекламирай'; @override - String get repeater_cliQuickClock => 'Часовник'; + String get repeater_cliQuickClock => 'Часовник'; @override - String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет'; + String get repeater_cliHelpAdvert => + 'Изпраща рекламен пакет'; @override String get repeater_cliHelpReboot => - 'Рестартира устройството. (Забележка, може да получите \'Timeout\', което е нормално)'; + 'Рестартира устройството. (Забележка, може да получите \'Timeout\', което е нормално)'; @override String get repeater_cliHelpClock => - 'Показва текущото време според часовника на всяко устройство.'; + 'Показва текущото време според часовника на всяко устройство.'; @override String get repeater_cliHelpPassword => - 'Задава се нова администраторска парола за устройството.'; + 'Задава се нова администраторска парола за устройството.'; @override String get repeater_cliHelpVersion => - 'Показва версията на устройството и датата на компилация на фърмуера.'; + 'Показва версията на устройството и датата на компилация на фърмуера.'; @override String get repeater_cliHelpClearStats => - 'Рестартира различни статистики броячи до нула.'; + 'Рестартира различни статистики броячи до нула.'; @override - String get repeater_cliHelpSetAf => 'Задава времето на фактора.'; + String get repeater_cliHelpSetAf => + 'Задава времето на фактора.'; @override String get repeater_cliHelpSetTx => - 'Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).'; + 'Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).'; @override String get repeater_cliHelpSetRepeat => - 'Активира или деактивира ролята на репитера за този възел.'; + 'Активира или деактивира ролята на репитера за този възел.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).'; + '(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).'; @override String get repeater_cliHelpSetFloodMax => - 'Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).'; + 'Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).'; @override String get repeater_cliHelpSetIntThresh => - 'Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.'; + 'Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.'; + 'Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetMultiAcks => - 'Активира или деактивира функцията \'двойни ACKs\'.'; + 'Активира или деактивира функцията \'двойни ACKs\'.'; @override String get repeater_cliHelpSetAdvertInterval => - 'Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.'; + 'Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.'; + 'Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetGuestPassword => - 'Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")'; + 'Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")'; @override - String get repeater_cliHelpSetName => 'Задава име на обявата.'; + String get repeater_cliHelpSetName => + 'Задава име на обявата.'; @override String get repeater_cliHelpSetLat => - 'Задава географска ширина на картата с реклами (в десетими градуси).'; + 'Задава географска ширина на картата с реклами (в десетими градуси).'; @override String get repeater_cliHelpSetLon => - 'Задава обхвата на дължина на картата на рекламата. (десетими градуса)'; + 'Задава обхвата на дължина на картата на рекламата. (десетими градуса)'; @override String get repeater_cliHelpSetRadio => - 'Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.'; + 'Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.'; @override String get repeater_cliHelpSetRxDelay => - 'Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.'; + 'Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetTxDelay => - 'Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).'; + 'Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.'; + 'Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.'; @override String get repeater_cliHelpSetBridgeEnabled => - 'Активиране/Деактивиране на мост.'; + 'Активиране/Деактивиране на мост.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Задайте забавяне преди преизпращане на пакети.'; + 'Задайте забавяне преди преизпращане на пакети.'; @override String get repeater_cliHelpSetBridgeSource => - 'Изберете дали мостът ще предава препратени пакети или получени пакети.'; + 'Изберете дали мостът ще предава препратени пакети или получени пакети.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Задайте скоростта на предаване за RS232 мостовете.'; + 'Задайте скоростта на предаване за RS232 мостовете.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Задайте тайна за мостовете на EspNow.'; + 'Задайте тайна за мостовете на EspNow.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).'; + 'Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).'; @override String get repeater_cliHelpTempRadio => - 'Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).'; + 'Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).'; @override String get repeater_cliHelpSetPerm => - 'Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).'; + 'Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).'; @override String get repeater_cliHelpGetBridgeType => - 'Получава тип мост none, rs232, espnow'; + 'Получава тип мост none, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Започва записване на пакети във файловата система.'; + 'Започва записване на пакети във файловата система.'; @override String get repeater_cliHelpLogStop => - 'Спира записването на пакети във файловата система.'; + 'Спира записването на пакети във файловата система.'; @override String get repeater_cliHelpLogErase => - 'Изтрива логовете от пакета от файловата система.'; + 'Изтрива логовете от пакета от файловата система.'; @override String get repeater_cliHelpNeighbors => - 'Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4'; + 'Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.'; + 'Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.'; @override String get repeater_cliHelpRegion => - '(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.'; + '(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.'; @override String get repeater_cliHelpRegionLoad => - 'Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.'; + 'Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.'; @override String get repeater_cliHelpRegionGet => - 'Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) \'F\'\"'; + 'Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Добавя или актуализира дефиниция на регион с дадено име.'; + 'Добавя или актуализира дефиниция на регион с дадено име.'; @override String get repeater_cliHelpRegionRemove => - 'Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)'; + 'Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)'; @override String get repeater_cliHelpRegionAllowf => - 'Задава \'Потоп\' разрешение за посочената област. (\'\' за глобалния/стария обхват)'; + 'Задава \'Потоп\' разрешение за посочената област. (\'\' за глобалния/стария обхват)'; @override String get repeater_cliHelpRegionDenyf => - 'Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )'; + 'Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )'; @override String get repeater_cliHelpRegionHome => - 'Отговаря с текущия \'home\' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).'; + 'Отговаря с текущия \'home\' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).'; @override - String get repeater_cliHelpRegionHomeSet => 'Задава \'домашно\' региона.'; + String get repeater_cliHelpRegionHomeSet => + 'Задава \'домашно\' региона.'; @override String get repeater_cliHelpRegionSave => - 'Запазва списъка/картата с региони в съхранение.'; + 'Запазва списъка/картата с региони в съхранение.'; @override String get repeater_cliHelpGps => - 'Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.'; + 'Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.'; @override - String get repeater_cliHelpGpsOnOff => 'Включва/Изключва GPS захранването.'; + String get repeater_cliHelpGpsOnOff => + 'Включва/Изключва GPS захранването.'; @override String get repeater_cliHelpGpsSync => - 'Синхронизира времето на възела с GPS часовника.'; + 'Синхронизира времето на възела с GPS часовника.'; @override String get repeater_cliHelpGpsSetLoc => - 'Задава координатите на нодата по GPS и запазва предпочитанията.'; + 'Задава координатите на нодата по GPS и запазва предпочитанията.'; @override String get repeater_cliHelpGpsAdvert => - 'Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията'; + 'Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията'; @override String get repeater_cliHelpGpsAdvertSet => - 'Задава конфигурация на обявите за местоположение.'; + 'Задава конфигурация на обявите за местоположение.'; @override - String get repeater_commandsListTitle => 'Списък с команди'; + String get repeater_commandsListTitle => 'Списък с команди'; @override String get repeater_commandsListNote => - 'ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".'; + 'ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".'; @override - String get repeater_general => 'Общо'; + String get repeater_general => 'Общо'; @override - String get repeater_settingsCategory => 'Настройки'; + String get repeater_settingsCategory => 'Настройки'; @override - String get repeater_bridge => 'Мост'; + String get repeater_bridge => 'Мост'; @override - String get repeater_logging => 'Логване'; + String get repeater_logging => 'Логване'; @override - String get repeater_neighborsRepeaterOnly => 'Съседи (Само за повтаряне)'; + String get repeater_neighborsRepeaterOnly => + 'Съседи (Само за повтаряне)'; @override String get repeater_regionManagementRepeaterOnly => - 'Управление на региони (Само за повтарящ се канал)'; + 'Управление на региони (Само за повтарящ се канал)'; @override String get repeater_regionNote => - 'Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.'; + 'Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.'; @override - String get repeater_gpsManagement => 'Управление на GPS'; + String get repeater_gpsManagement => 'Управление на GPS'; @override String get repeater_gpsNote => - 'GPS командата е въведена, за да управлява теми, свързани с местоположението.'; + 'GPS командата е въведена, за да управлява теми, свързани с местоположението.'; @override - String get telemetry_receivedData => 'Получени телеметрични данни'; + String get telemetry_receivedData => + 'Получени телеметрични данни'; @override - String get telemetry_requestTimeout => 'Заявката за телеметрия е прекъсната.'; + String get telemetry_requestTimeout => + 'Заявката за телеметрия е прекъсната.'; @override String telemetry_errorLoading(String error) { - return 'Грешка при зареждане на телеметрията: $error'; + return 'Грешка при зареждане на телеметрията: $error'; } @override - String get telemetry_noData => 'Няма налични данни за телеметрията.'; + String get telemetry_noData => + 'Няма налични данни за телеметрията.'; @override String telemetry_channelTitle(int channel) { - return 'Канал $channel'; + return 'Канал $channel'; } @override - String get telemetry_batteryLabel => 'Батерия'; + String get telemetry_batteryLabel => 'Батерия'; @override - String get telemetry_voltageLabel => 'Напрежение'; + String get telemetry_voltageLabel => 'Напрежение'; @override - String get telemetry_mcuTemperatureLabel => 'Температура на MCU'; + String get telemetry_mcuTemperatureLabel => 'Температура на MCU'; @override - String get telemetry_temperatureLabel => 'Температура'; + String get telemetry_temperatureLabel => 'Температура'; @override - String get telemetry_currentLabel => 'Текущо'; + String get telemetry_currentLabel => 'Текущо'; @override String telemetry_batteryValue(int percent, String volts) { @@ -2472,79 +2610,87 @@ class AppLocalizationsBg extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Получени данни за съседи'; + String get neighbors_receivedData => + 'Получени данни за съседи'; @override - String get neighbors_requestTimedOut => 'Съседите поискат изтичане на време.'; + String get neighbors_requestTimedOut => + 'Съседите поискат изтичане на време.'; @override String neighbors_errorLoading(String error) { - return 'Грешка при зареждане на съседи: $error'; + return 'Грешка при зареждане на съседи: $error'; } @override - String get neighbors_repeatersNeighbors => 'Повторители Съседи'; + String get neighbors_repeatersNeighbors => + 'Повторители Съседи'; @override - String get neighbors_noData => 'Няма налични данни за съседи.'; + String get neighbors_noData => + 'Няма налични данни за съседи.'; @override String neighbors_unknownContact(String pubkey) { - return 'Неизвестна $pubkey'; + return 'Неизвестна $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Слушано преди $time.'; + return 'Слушано преди $time.'; } @override - String get channelPath_title => 'Пътеки пъзел'; + String get channelPath_title => 'Пътеки пъзел'; @override - String get channelPath_viewMap => 'Преглед на картата'; + String get channelPath_viewMap => 'Преглед на картата'; @override - String get channelPath_otherObservedPaths => 'Други Наблюдавани Пътища'; + String get channelPath_otherObservedPaths => + 'Други Наблюдавани Пътища'; @override - String get channelPath_repeaterHops => 'Повтарящи се скокове'; + String get channelPath_repeaterHops => + 'Повтарящи се скокове'; @override String get channelPath_noHopDetails => - 'Детайлите за пакета не са предоставени.'; + 'Детайлите за пакета не са предоставени.'; @override - String get channelPath_messageDetails => 'Подробности на съобщението'; + String get channelPath_messageDetails => + 'Подробности на съобщението'; @override - String get channelPath_senderLabel => 'Изпращач'; + String get channelPath_senderLabel => 'Изпращач'; @override - String get channelPath_timeLabel => 'Време'; + String get channelPath_timeLabel => 'Време'; @override - String get channelPath_repeatsLabel => 'Повтаря'; + String get channelPath_repeatsLabel => 'Повтаря'; @override String channelPath_pathLabel(int index) { - return 'Път $index'; + return 'Път $index'; } @override - String get channelPath_observedLabel => 'Наблюдавано'; + String get channelPath_observedLabel => 'Наблюдавано'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Наблюдаван път $index • $hops'; + return 'Наблюдаван път $index • $hops'; } @override - String get channelPath_noLocationData => 'Няма данни за местоположение.'; + String get channelPath_noLocationData => + 'Няма данни за местоположение.'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2557,340 +2703,358 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Неизвестно'; + String get channelPath_unknownPath => 'Неизвестно'; @override - String get channelPath_floodPath => 'Поливане'; + String get channelPath_floodPath => 'Поливане'; @override - String get channelPath_directPath => 'Директно'; + String get channelPath_directPath => 'Директно'; @override String channelPath_observedZeroOf(int total) { - return '0 от $total скокове'; + return '0 от $total скокове'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed от $total скокове'; + return '$observed от $total скокове'; } @override - String get channelPath_mapTitle => 'Карта на пътя'; + String get channelPath_mapTitle => 'Карта на пътя'; @override String get channelPath_noRepeaterLocations => - 'Няма налични местоположения на повторителите за този път.'; + 'Няма налични местоположения на повторителите за този път.'; @override String channelPath_primaryPath(int index) { - return 'Път $index (Основен)'; + return 'Път $index (Основен)'; } @override - String get channelPath_pathLabelTitle => 'Пътекино'; + String get channelPath_pathLabelTitle => 'Пътекино'; @override - String get channelPath_observedPathHeader => 'Наблюдаван път'; + String get channelPath_observedPathHeader => 'Наблюдаван път'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Няма налични детайли за този пакет.'; + 'Няма налични детайли за този пакет.'; @override - String get channelPath_unknownRepeater => 'Неизвестен повторител'; + String get channelPath_unknownRepeater => + 'Неизвестен повторител'; @override - String get community_title => 'Общност'; + String get community_title => 'Общност'; @override - String get community_create => 'Създай общност'; + String get community_create => 'Създай общност'; @override String get community_createDesc => - 'Създайте нова общност и я споделете чрез QR код.'; + 'Създайте нова общност и я споделете чрез QR код.'; @override - String get community_join => 'Присъедини се'; + String get community_join => 'Присъедини се'; @override - String get community_joinTitle => 'Присъедини се към общността'; + String get community_joinTitle => + 'Присъедини се към общността'; @override String community_joinConfirmation(String name) { - return 'Искате ли да се присъедините към общността \"$name\"?'; + return 'Искате ли да се присъедините към общността \"$name\"?'; } @override - String get community_scanQr => 'Сканирайте QR кода на общността'; + String get community_scanQr => + 'Сканирайте QR кода на общността'; @override String get community_scanInstructions => - 'Насочете камерата към QR код на общността'; + 'Насочете камерата към QR код на общността'; @override - String get community_showQr => 'Покажи QR код'; + String get community_showQr => 'Покажи QR код'; @override - String get community_publicChannel => 'Обществено общност'; + String get community_publicChannel => 'Обществено общност'; @override - String get community_hashtagChannel => 'Хаштаг на общността'; + String get community_hashtagChannel => 'Хаштаг на общността'; @override - String get community_name => 'Име на общността'; + String get community_name => 'Име на общността'; @override - String get community_enterName => 'Въведете име на общността'; + String get community_enterName => + 'Въведете име на общността'; @override String community_created(String name) { - return 'Общността \"$name\" е създадена'; + return 'Общността \"$name\" е създадена'; } @override String community_joined(String name) { - return 'Присъединено общност \"$name\"'; + return 'Присъединено общност \"$name\"'; } @override - String get community_qrTitle => 'Споделяне в общността'; + String get community_qrTitle => 'Споделяне в общността'; @override String community_qrInstructions(String name) { - return 'Сканирайте този QR код, за да се присъедините към $name.'; + return 'Сканирайте този QR код, за да се присъедините към $name.'; } @override String get community_hashtagPrivacyHint => - 'Хаштаг каналите на общността са достъпни само за членове на общността'; + 'Хаштаг каналите на общността са достъпни само за членове на общността'; @override - String get community_invalidQrCode => 'Невалиден QR код на общността'; + String get community_invalidQrCode => + 'Невалиден QR код на общността'; @override - String get community_alreadyMember => 'Вече съм член'; + String get community_alreadyMember => 'Вече съм член'; @override String community_alreadyMemberMessage(String name) { - return 'Вие вече сте член на \"$name\".'; + return 'Вие вече сте член на \"$name\".'; } @override - String get community_addPublicChannel => 'Добави публичен общностен канал'; + String get community_addPublicChannel => + 'Добави публичен общностен канал'; @override String get community_addPublicChannelHint => - 'Автоматично добавете публичния канал за тази общност.'; + 'Автоматично добавете публичния канал за тази общност.'; @override - String get community_noCommunities => 'Няма присъединени общности още.'; + String get community_noCommunities => + 'Няма присъединени общности още.'; @override String get community_scanOrCreate => - 'Сканирайте QR код или създайте общност, за да започнете.'; + 'Сканирайте QR код или създайте общност, за да започнете.'; @override - String get community_manageCommunities => 'Управление на общности'; + String get community_manageCommunities => + 'Управление на общности'; @override - String get community_delete => 'Напусни общността'; + String get community_delete => 'Напусни общността'; @override String community_deleteConfirm(String name) { - return 'Напускате \"$name\"?'; + return 'Напускате \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Това ще изтрие също $count канал(а) и техните съобщения.'; + return 'Това ще изтрие също $count канал(а) и техните съобщения.'; } @override String community_deleted(String name) { - return 'Остави общността \"$name\"'; + return 'Остави общността \"$name\"'; } @override - String get community_regenerateSecret => 'Регенерейрай секрет'; + String get community_regenerateSecret => + 'Регенерейрай секрет'; @override String community_regenerateSecretConfirm(String name) { - return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.'; + return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.'; } @override - String get community_regenerate => 'Регенерация'; + String get community_regenerate => 'Регенерация'; @override String community_secretRegenerated(String name) { - return 'Секретно презареждане за \"$name\"'; + return 'Секретно презареждане за \"$name\"'; } @override - String get community_updateSecret => 'Актуализирай тайна'; + String get community_updateSecret => 'Актуализирай тайна'; @override String community_secretUpdated(String name) { - return 'Секретно обновено за \"$name\"'; + return 'Секретно обновено за \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"'; + return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"'; } @override - String get community_addHashtagChannel => 'Добави общностен хаштаг'; + String get community_addHashtagChannel => + 'Добави общностен хаштаг'; @override String get community_addHashtagChannelDesc => - 'Добавете хаштаг канал за тази общност'; + 'Добавете хаштаг канал за тази общност'; @override - String get community_selectCommunity => 'Изберете общност'; + String get community_selectCommunity => 'Изберете общност'; @override - String get community_regularHashtag => 'Обикновен хаштаг'; + String get community_regularHashtag => 'Обикновен хаштаг'; @override String get community_regularHashtagDesc => - 'Общ хаштаг (всеки може да се присъедини)'; + 'Общ хаштаг (всеки може да се присъедини)'; @override - String get community_communityHashtag => 'Общностен хаштаг'; + String get community_communityHashtag => 'Общностен хаштаг'; @override - String get community_communityHashtagDesc => 'Само за членове на общността'; + String get community_communityHashtagDesc => + 'Само за членове на общността'; @override String community_forCommunity(String name) { - return 'За $name'; + return 'За $name'; } @override - String get listFilter_tooltip => 'Филтрирайте и сортирайте'; + String get listFilter_tooltip => + 'Филтрирайте и сортирайте'; @override - String get listFilter_sortBy => 'Сортирай по'; + String get listFilter_sortBy => 'Сортирай по'; @override - String get listFilter_latestMessages => 'Последни съобщения'; + String get listFilter_latestMessages => 'Последни съобщения'; @override - String get listFilter_heardRecently => 'Слушано е наскоро'; + String get listFilter_heardRecently => 'Слушано е наскоро'; @override String get listFilter_az => 'A-Z'; @override - String get listFilter_filters => 'Филтри'; + String get listFilter_filters => 'Филтри'; @override - String get listFilter_all => 'Всички'; + String get listFilter_all => 'Всички'; @override - String get listFilter_favorites => 'Любими'; + String get listFilter_favorites => 'Любими'; @override - String get listFilter_addToFavorites => 'Добави към любими'; + String get listFilter_addToFavorites => 'Добави към любими'; @override - String get listFilter_removeFromFavorites => 'Премахване от списъка с любими'; + String get listFilter_removeFromFavorites => + 'Премахване от списъка с любими'; @override - String get listFilter_users => 'Потребители'; + String get listFilter_users => 'Потребители'; @override - String get listFilter_repeaters => 'Повторители'; + String get listFilter_repeaters => 'Повторители'; @override - String get listFilter_roomServers => 'Сървъри на стая'; + String get listFilter_roomServers => 'Сървъри на стая'; @override - String get listFilter_unreadOnly => 'Само непрочетените'; + String get listFilter_unreadOnly => 'Само непрочетените'; @override - String get listFilter_newGroup => 'Нова група'; + String get listFilter_newGroup => 'Нова група'; @override - String get pathTrace_you => 'Вие'; + String get pathTrace_you => 'Вие'; @override - String get pathTrace_failed => 'Пътят за проследяване не успя.'; + String get pathTrace_failed => + 'Пътят за проследяване не успя.'; @override - String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.'; + String get pathTrace_notAvailable => + 'Пътека за проследяване не е достъпна.'; @override - String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; + String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Един или повече от хмелите липсва местоположение!'; + 'Един или повече от хмелите липсва местоположение!'; @override - String get pathTrace_clearTooltip => 'Изчисти пътя'; + String get pathTrace_clearTooltip => 'Изчисти пътя'; @override - String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.'; + String get losSelectStartEnd => + 'Изберете начални и крайни възли за LOS.'; @override String losRunFailed(String error) { - return 'Проверката на пряката видимост е неуспешна: $error'; + return 'Проверката на пряката видимост е неуспешна: $error'; } @override - String get losClearAllPoints => 'Изчистете всички точки'; + String get losClearAllPoints => 'Изчистете всички точки'; @override String get losRunToViewElevationProfile => - 'Стартирайте LOS, за да видите профила на надморската височина'; + 'Стартирайте LOS, за да видите профила на надморската височина'; @override - String get losMenuTitle => 'LOS меню'; + String get losMenuTitle => 'LOS меню'; @override String get losMenuSubtitle => - 'Докоснете възли или натиснете продължително карта за персонализирани точки'; + 'Докоснете възли или натиснете продължително карта за персонализирани точки'; @override - String get losShowDisplayNodes => 'Показване на възли на дисплея'; + String get losShowDisplayNodes => + 'Показване на възли на дисплея'; @override - String get losCustomPoints => 'Персонализирани точки'; + String get losCustomPoints => 'Персонализирани точки'; @override String losCustomPointLabel(int index) { - return 'Персонализирано $index'; + return 'Персонализирано $index'; } @override - String get losPointA => 'Точка А'; + String get losPointA => 'Точка А'; @override - String get losPointB => 'Точка Б'; + String get losPointB => 'Точка Б'; @override String losAntennaA(String value, String unit) { - return 'Антена A: $value $unit'; + return 'Антена A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Антена B: $value $unit'; + return 'Антена B: $value $unit'; } @override - String get losRun => 'Стартирайте LOS'; + String get losRun => 'Стартирайте LOS'; @override - String get losNoElevationData => 'Няма данни за надморска височина'; + String get losNoElevationData => + 'Няма данни за надморска височина'; @override String losProfileClear( @@ -2899,7 +3063,7 @@ class AppLocalizationsBg extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit'; + return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit'; } @override @@ -2909,61 +3073,64 @@ class AppLocalizationsBg extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, блокиран от $obstruction $heightUnit'; + return '$distance $distanceUnit, блокиран от $obstruction $heightUnit'; } @override - String get losStatusChecking => 'LOS: проверка...'; + String get losStatusChecking => 'LOS: проверка...'; @override - String get losStatusNoData => 'LOS: няма данни'; + String get losStatusNoData => 'LOS: няма данни'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно'; + return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно'; } @override String get losErrorElevationUnavailable => - 'Няма налични данни за надморска височина за една или повече проби.'; + 'Няма налични данни за надморска височина за една или повече проби.'; @override String get losErrorInvalidInput => - 'Невалидни данни за точки/надморска височина за изчисляване на LOS.'; + 'Невалидни данни за точки/надморска височина за изчисляване на LOS.'; @override - String get losRenameCustomPoint => 'Преименувайте персонализирана точка'; + String get losRenameCustomPoint => + 'Преименувайте персонализирана точка'; @override - String get losPointName => 'Име на точката'; + String get losPointName => 'Име на точката'; @override - String get losShowPanelTooltip => 'Показване на LOS панел'; + String get losShowPanelTooltip => 'Показване на LOS панел'; @override - String get losHidePanelTooltip => 'Скриване на LOS панела'; + String get losHidePanelTooltip => 'Скриване на LOS панела'; @override String get losElevationAttribution => - 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; + 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Радиохоризонт'; + String get losLegendRadioHorizon => 'Радиохоризонт'; @override - String get losLegendLosBeam => 'Линия на видимост'; + String get losLegendLosBeam => 'Линия на видимост'; @override - String get losLegendTerrain => 'Терен'; + String get losLegendTerrain => 'Терен'; @override - String get losFrequencyLabel => 'Честота'; + String get losFrequencyLabel => 'Честота'; @override - String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението'; + String get losFrequencyInfoTooltip => + 'Преглед на детайли за изчислението'; @override - String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта'; + String get losFrequencyDialogTitle => + 'Изчисляване на радиохоризонта'; @override String losFrequencyDialogDescription( @@ -2972,91 +3139,101 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; } @override - String get contacts_pathTrace => 'Пътен проследяване'; + String get contacts_pathTrace => 'Пътен проследяване'; @override - String get contacts_ping => 'Пинг'; + String get contacts_ping => 'Пинг'; @override - String get contacts_repeaterPathTrace => 'Трасировка до повторител'; + String get contacts_repeaterPathTrace => + 'Трасировка до повторител'; @override - String get contacts_repeaterPing => 'Пингване на повторителя'; + String get contacts_repeaterPing => + 'Пингване на повторителя'; @override - String get contacts_roomPathTrace => 'Трасиране на път до съ'; + String get contacts_roomPathTrace => + 'Трасиране на път до съ'; @override - String get contacts_roomPing => 'Ping на сървъра на стаята'; + String get contacts_roomPing => 'Ping на сървъра на стаята'; @override - String get contacts_chatTraceRoute => 'Трасиране на път'; + String get contacts_chatTraceRoute => 'Трасиране на път'; @override String contacts_pathTraceTo(String name) { - return 'Проследи маршрут към $name'; + return 'Проследи маршрут към $name'; } @override - String get contacts_clipboardEmpty => 'Клипборда е празна.'; + String get contacts_clipboardEmpty => 'Клипборда е празна.'; @override - String get contacts_invalidAdvertFormat => 'Невалидни данни за контакт'; + String get contacts_invalidAdvertFormat => + 'Невалидни данни за контакт'; @override - String get contacts_contactImported => 'Контактът е импортиран.'; + String get contacts_contactImported => + 'Контактът е импортиран.'; @override String get contacts_contactImportFailed => - 'Контактът не е успешно импортиран.'; + 'Контактът не е успешно импортиран.'; @override - String get contacts_zeroHopAdvert => 'Реклама без скок'; + String get contacts_zeroHopAdvert => 'Реклама без скок'; @override - String get contacts_floodAdvert => 'Потопна реклама'; + String get contacts_floodAdvert => 'Потопна реклама'; @override - String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда'; + String get contacts_copyAdvertToClipboard => + 'Копирай обявата в клипборда'; @override - String get contacts_addContactFromClipboard => 'Добави контакт от клипборда'; + String get contacts_addContactFromClipboard => + 'Добави контакт от клипборда'; @override - String get contacts_ShareContact => 'Копирай контакт в клипборда'; + String get contacts_ShareContact => + 'Копирай контакт в клипборда'; @override - String get contacts_ShareContactZeroHop => 'Сподели контакт чрез обява'; + String get contacts_ShareContactZeroHop => + 'Сподели контакт чрез обява'; @override - String get contacts_zeroHopContactAdvertSent => 'Изпратен контакт по обява.'; + String get contacts_zeroHopContactAdvertSent => + 'Изпратен контакт по обява.'; @override String get contacts_zeroHopContactAdvertFailed => - 'Неуспешно изпращане на контакт.'; + 'Неуспешно изпращане на контакт.'; @override String get contacts_contactAdvertCopied => - 'Рекламата е копирана в клипборда.'; + 'Рекламата е копирана в клипборда.'; @override String get contacts_contactAdvertCopyFailed => - 'Копирането на обявата в клипборда не успя.'; + 'Копирането на обявата в клипборда не успя.'; @override - String get notification_activityTitle => 'Активност на MeshCore'; + String get notification_activityTitle => 'Активност на MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'съобщения', - one: 'съобщение', + other: 'съобщения', + one: 'съобщение', ); return '$count $_temp0'; } @@ -3066,8 +3243,8 @@ class AppLocalizationsBg extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'съобщения в канали', - one: 'съобщение в канал', + other: 'съобщения в канали', + one: 'съобщение в канал', ); return '$count $_temp0'; } @@ -3077,77 +3254,85 @@ class AppLocalizationsBg extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'нови възли', - one: 'нов възел', + other: 'нови възли', + one: 'нов възел', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Открит нов $contactType'; + return 'Открит нов $contactType'; } @override - String get notification_receivedNewMessage => 'Получено ново съобщение'; + String get notification_receivedNewMessage => + 'Получено ново съобщение'; @override String get settings_gpxExportRepeaters => - 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; + 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Изпраща повторители / roomserver с местоположение в GPX файл.'; + 'Изпраща повторители / roomserver с местоположение в GPX файл.'; @override - String get settings_gpxExportContacts => 'Експортирай спътници към GPX'; + String get settings_gpxExportContacts => + 'Експортирай спътници към GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Експортира спътници с местоположение в GPX файл.'; + 'Експортира спътници с местоположение в GPX файл.'; @override - String get settings_gpxExportAll => 'Експортирай всички контакти в GPX'; + String get settings_gpxExportAll => + 'Експортирай всички контакти в GPX'; @override String get settings_gpxExportAllSubtitle => - 'Експортира всички контакти с местоположение в файл GPX.'; + 'Експортира всички контакти с местоположение в файл GPX.'; @override - String get settings_gpxExportSuccess => 'Успешно изlexport на файл GPX.'; + String get settings_gpxExportSuccess => + 'Успешно изlexport на файл GPX.'; @override - String get settings_gpxExportNoContacts => 'Няма контакти за изlexport.'; + String get settings_gpxExportNoContacts => + 'Няма контакти за изlexport.'; @override String get settings_gpxExportNotAvailable => - 'Не е поддържан на вашето устройство/ОС'; + 'Не е поддържан на вашето устройство/ОС'; @override - String get settings_gpxExportError => 'Възникна грешка при изнасяне.'; + String get settings_gpxExportError => + 'Възникна грешка при изнасяне.'; @override String get settings_gpxExportRepeatersRoom => - 'Местоположения на повторител и сървър на стаята'; + 'Местоположения на повторител и сървър на стаята'; @override - String get settings_gpxExportChat => 'Местоположения на спътници'; + String get settings_gpxExportChat => + 'Местоположения на спътници'; @override String get settings_gpxExportAllContacts => - 'Местоположения на всички контакти'; + 'Местоположения на всички контакти'; @override String get settings_gpxExportShareText => - 'Картинни данни изнесени от meshcore-open'; + 'Картинни данни изнесени от meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open износ на данни за карта в формат GPX'; + 'meshcore-open износ на данни за карта в формат GPX'; @override - String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства'; + String get snrIndicator_nearByRepeaters => + 'Близки повтарящи се устройства'; @override - String get snrIndicator_lastSeen => 'Последно видян'; + String get snrIndicator_lastSeen => 'Последно видян'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a3ab54d..373467a 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -15,7 +15,7 @@ class AppLocalizationsDe extends AppLocalizations { String get nav_contacts => 'Kontakte'; @override - String get nav_channels => 'Kanäle'; + String get nav_channels => 'Kanäle'; @override String get nav_map => 'Karte'; @@ -30,22 +30,22 @@ class AppLocalizationsDe extends AppLocalizations { String get common_connect => 'Verbinden'; @override - String get common_unknownDevice => 'Unbekanntes Gerät'; + String get common_unknownDevice => 'Unbekanntes Gerät'; @override String get common_save => 'Speichern'; @override - String get common_delete => 'Löschen'; + String get common_delete => 'Löschen'; @override - String get common_close => 'Schließen'; + String get common_close => 'Schließen'; @override String get common_edit => 'Bearbeiten'; @override - String get common_add => 'Hinzufügen'; + String get common_add => 'Hinzufügen'; @override String get common_settings => 'Einstellungen'; @@ -78,7 +78,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_hide => 'Ausblenden'; @override - String get common_remove => 'Löschen'; + String get common_remove => 'Löschen'; @override String get common_enable => 'Aktivieren'; @@ -93,7 +93,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_loading => 'Laden...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,14 +108,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => - 'Wählen Sie Ihre bevorzugte Verbindungsmethode.'; - - @override - String get connectionChoiceSubtitle => - 'Wählen Sie, wie Sie Ihr MeshCore-Gerät erreichen möchten.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -123,25 +115,25 @@ class AppLocalizationsDe extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Über USB verbinden'; + String get usbScreenTitle => 'Über USB verbinden'; @override String get usbScreenSubtitle => - 'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.'; + 'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.'; @override - String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus'; + String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus'; @override String get usbScreenNote => - 'USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; + 'USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; @override String get usbScreenEmptyState => - 'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.'; + 'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.'; @override - String get scanner_scanning => 'Scannen nach Geräten...'; + String get scanner_scanning => 'Scannen nach Geräten...'; @override String get scanner_connecting => 'Verbunden...'; @@ -158,11 +150,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get scanner_searchingDevices => 'Suche nach MeshCore-Geräten...'; + String get scanner_searchingDevices => 'Suche nach MeshCore-Geräten...'; @override String get scanner_tapToScan => - 'Tippen Sie auf Scan, um MeshCore-Geräte zu finden.'; + 'Tippen Sie auf Scan, um MeshCore-Geräte zu finden.'; @override String scanner_connectionFailed(String error) { @@ -180,14 +172,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_bluetoothOffMessage => - 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; + 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; @override String get scanner_chromeRequired => 'Chrome Browser erforderlich'; @override String get scanner_chromeRequiredMessage => - 'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.'; + 'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.'; @override String get scanner_enableBluetooth => 'Bluetooth aktivieren'; @@ -202,7 +194,7 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_title => 'Einstellungen'; @override - String get settings_deviceInfo => 'Geräteinformationen'; + String get settings_deviceInfo => 'Geräteinformationen'; @override String get settings_appSettings => 'App-Einstellungen'; @@ -247,10 +239,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_locationBothRequired => - 'Bitte geben Sie sowohl Breite als auch Längengrad ein.'; + 'Bitte geben Sie sowohl Breite als auch Längengrad ein.'; @override - String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.'; + String get settings_locationInvalid => + 'Ungültige Breiten- oder Längengrade.'; @override String get settings_locationGPSEnable => 'GPS aktivieren'; @@ -260,7 +253,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Aktiviert GPS zur automatischen Aktualisierung des Standorts.'; @override - String get settings_locationIntervalSec => 'Intervall für GPS (Sekunden)'; + String get settings_locationIntervalSec => 'Intervall für GPS (Sekunden)'; @override String get settings_locationIntervalInvalid => @@ -270,18 +263,18 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_latitude => 'Breitengrad'; @override - String get settings_longitude => 'Längengrad'; + String get settings_longitude => 'Längengrad'; @override - String get settings_privacyMode => 'Privatsphäreeinstellung'; + String get settings_privacyMode => 'Privatsphäreeinstellung'; @override String get settings_privacyModeSubtitle => - 'Verstecken Sie Name/Ort in Ankündigungen'; + 'Verstecken Sie Name/Ort in Ankündigungen'; @override String get settings_privacyModeToggle => - 'Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.'; + 'Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.'; @override String get settings_privacyModeEnabled => 'Datenschutzmodus aktiviert'; @@ -293,20 +286,20 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_actions => 'Aktionen'; @override - String get settings_sendAdvertisement => 'Sende Ankündigung'; + String get settings_sendAdvertisement => 'Sende Ankündigung'; @override - String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung'; + String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung'; @override - String get settings_advertisementSent => 'Ankündigung gesendet'; + String get settings_advertisementSent => 'Ankündigung gesendet'; @override String get settings_syncTime => 'Zeitsynchronisierung'; @override String get settings_syncTimeSubtitle => - 'Stelle die Gerätezeit auf die Uhrzeit des Telefons ein'; + 'Stelle die Gerätezeit auf die Uhrzeit des Telefons ein'; @override String get settings_timeSynchronized => 'Zeit synchronisiert'; @@ -316,17 +309,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_refreshContactsSubtitle => - 'Kontakt-Liste vom Gerät neu laden'; + 'Kontakt-Liste vom Gerät neu laden'; @override - String get settings_rebootDevice => 'Gerät neu starten'; + String get settings_rebootDevice => 'Gerät neu starten'; @override - String get settings_rebootDeviceSubtitle => 'MeshCore-Gerät neu starten'; + String get settings_rebootDeviceSubtitle => 'MeshCore-Gerät neu starten'; @override String get settings_rebootDeviceConfirm => - 'Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.'; + 'Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.'; @override String get settings_debug => 'Fehlerbehebung'; @@ -345,7 +338,7 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_appDebugLogSubtitle => 'Anwendung Debug-Nachrichten'; @override - String get settings_about => 'Über'; + String get settings_about => 'Über'; @override String settings_aboutVersion(String version) { @@ -357,11 +350,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_aboutDescription => - 'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.'; + 'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.'; @override String get settings_aboutOpenMeteoAttribution => - 'LOS-Höhendaten: Open-Meteo (CC BY 4.0)'; + 'LOS-Höhendaten: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Name'; @@ -376,13 +369,13 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_infoBattery => 'Akku'; @override - String get settings_infoPublicKey => 'Öffentlicher Schlüssel'; + String get settings_infoPublicKey => 'Öffentlicher Schlüssel'; @override String get settings_infoContactsCount => 'Anzahl Kontakte'; @override - String get settings_infoChannelCount => 'Anzahl Kanäle'; + String get settings_infoChannelCount => 'Anzahl Kanäle'; @override String get settings_presets => 'Voreinstellungen'; @@ -394,7 +387,7 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_frequencyHelper => '300,00 - 2.500,00'; @override - String get settings_frequencyInvalid => 'Ungültige Frequenz (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Ungültige Frequenz (300-2500 MHz)'; @override String get settings_bandwidth => 'Bandbreite'; @@ -412,14 +405,14 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)'; + String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)'; @override String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss'; @override String get settings_clientRepeatSubtitle => - 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; + 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; @override String get settings_clientRepeatFreqWarning => @@ -458,10 +451,10 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -470,16 +463,16 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -488,10 +481,10 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russisch'; @@ -505,7 +498,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_enableMessageTracingSubtitle => - 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; + 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; @override String get appSettings_notifications => 'Benachrichtigungen'; @@ -515,7 +508,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_enableNotificationsSubtitle => - 'Erhalte Benachrichtigungen für Nachrichten und Ankündigungen'; + 'Erhalte Benachrichtigungen für Nachrichten und Ankündigungen'; @override String get appSettings_notificationPermissionDenied => @@ -546,7 +539,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_advertisementNotifications => - 'Ankündigungsbenachrichtigungen'; + 'Ankündigungsbenachrichtigungen'; @override String get appSettings_advertisementNotificationsSubtitle => @@ -557,19 +550,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetry => - 'Lösche Pfade bei Max Wiederholungsversuchen'; + 'Lösche Pfade bei Max Wiederholungsversuchen'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen'; + 'Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen'; @override String get appSettings_pathsWillBeCleared => - 'Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.'; + 'Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.'; @override String get appSettings_pathsWillNotBeCleared => - 'Die Pfade werden nicht automatisch gelöscht.'; + 'Die Pfade werden nicht automatisch gelöscht.'; @override String get appSettings_autoRouteRotation => 'Automatische Routenrotation'; @@ -594,21 +587,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Konfiguriert pro Gerät ($deviceName)'; + return 'Konfiguriert pro Gerät ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Verbinde ein Gerät, um zu wählen'; + 'Verbinde ein Gerät, um zu wählen'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0–4,2 V)'; + String get appSettings_batteryNmc => '18650 NMC (3,0–4,2 V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)'; + String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)'; @override String get appSettings_mapDisplay => 'Kartendarstellung'; @@ -680,11 +673,11 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_unitsImperial => 'Imperial (ft/mi)'; @override - String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt'; + String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Ausgewählte Fläche (Zoom $minZoom-$maxZoom)'; + return 'Ausgewählte Fläche (Zoom $minZoom-$maxZoom)'; } @override @@ -713,7 +706,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactsWillAppear => - 'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.'; + 'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.'; @override String get contacts_unread => 'Ungelesen'; @@ -754,7 +747,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Keine Kontakte oder Gruppen gefunden.'; @override - String get contacts_deleteContact => 'Lösche den Kontakt'; + String get contacts_deleteContact => 'Lösche den Kontakt'; @override String contacts_removeConfirm(String contactName) { @@ -771,17 +764,17 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_roomLogin => 'Raum-Login'; @override - String get contacts_openChat => 'Öffne Chat'; + String get contacts_openChat => 'Öffne Chat'; @override String get contacts_editGroup => 'Gruppe bearbeiten'; @override - String get contacts_deleteGroup => 'Löschen Gruppe'; + String get contacts_deleteGroup => 'Löschen Gruppe'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Löschen von \"$groupName\"?'; + return 'Löschen von \"$groupName\"?'; } @override @@ -833,19 +826,19 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get channels_title => 'Kanäle'; + String get channels_title => 'Kanäle'; @override - String get channels_noChannelsConfigured => 'Keine Kanäle konfiguriert'; + String get channels_noChannelsConfigured => 'Keine Kanäle konfiguriert'; @override - String get channels_addPublicChannel => 'Öffentlichen Kanal hinzufügen'; + String get channels_addPublicChannel => 'Öffentlichen Kanal hinzufügen'; @override - String get channels_searchChannels => 'Suche Kanäle...'; + String get channels_searchChannels => 'Suche Kanäle...'; @override - String get channels_noChannelsFound => 'Keine Kanäle gefunden'; + String get channels_noChannelsFound => 'Keine Kanäle gefunden'; @override String channels_channelIndex(int index) { @@ -856,13 +849,13 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_hashtagChannel => 'Hashtag-Kanal'; @override - String get channels_public => 'Öffentlich'; + String get channels_public => 'Öffentlich'; @override String get channels_private => 'Privat'; @override - String get channels_publicChannel => 'Öffentlicher Kanal'; + String get channels_publicChannel => 'Öffentlicher Kanal'; @override String get channels_privateChannel => 'Privater Kanal'; @@ -877,25 +870,25 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben'; @override - String get channels_deleteChannel => 'Lösche den Kanal'; + String get channels_deleteChannel => 'Lösche den Kanal'; @override String channels_deleteChannelConfirm(String name) { - return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; + return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; } @override String channels_channelDeleteFailed(String name) { - return 'Kanal $name konnte nicht gelöscht werden'; + return 'Kanal $name konnte nicht gelöscht werden'; } @override String channels_channelDeleted(String name) { - return 'Kanal \"$name\" gelöscht'; + return 'Kanal \"$name\" gelöscht'; } @override - String get channels_addChannel => 'Kanal hinzufügen'; + String get channels_addChannel => 'Kanal hinzufügen'; @override String get channels_channelIndexLabel => 'Kanalindex'; @@ -904,16 +897,16 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_channelName => 'Kanalname'; @override - String get channels_usePublicChannel => 'Verwende öffentlichen Kanal'; + String get channels_usePublicChannel => 'Verwende öffentlichen Kanal'; @override - String get channels_standardPublicPsk => 'Öffentliche Standard PSK'; + String get channels_standardPublicPsk => 'Öffentliche Standard PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Zufällige PSK generieren'; + String get channels_generateRandomPsk => 'Zufällige PSK generieren'; @override String get channels_enterChannelName => @@ -925,7 +918,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channels_channelAdded(String name) { - return 'Kanal \"$name\" hinzugefügt'; + return 'Kanal \"$name\" hinzugefügt'; } @override @@ -942,7 +935,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Öffentlicher Kanal hinzugefügt'; + String get channels_publicChannelAdded => 'Öffentlicher Kanal hinzugefügt'; @override String get channels_sortBy => 'Sortiere nach'; @@ -964,7 +957,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_createPrivateChannelDesc => - 'Verschlüsselt mit einem geheimen Schlüssel.'; + 'Verschlüsselt mit einem geheimen Schlüssel.'; @override String get channels_joinPrivateChannel => @@ -972,10 +965,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_joinPrivateChannelDesc => - 'Manuelle Eingabe eines geheimen Schlüssels.'; + 'Manuelle Eingabe eines geheimen Schlüssels.'; @override - String get channels_joinPublicChannel => 'Tritt dem öffentlichen Kanal bei'; + String get channels_joinPublicChannel => 'Tritt dem öffentlichen Kanal bei'; @override String get channels_joinPublicChannelDesc => @@ -987,13 +980,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_joinHashtagChannelDesc => - 'Jeder kann sich bei Hashtag-Kanälen beteiligen.'; + 'Jeder kann sich bei Hashtag-Kanälen beteiligen.'; @override String get channels_scanQrCode => 'Scannen Sie einen QR-Code'; @override - String get channels_scanQrCodeComingSoon => 'Bald verfügbar'; + String get channels_scanQrCodeComingSoon => 'Bald verfügbar'; @override String get channels_enterHashtag => 'Gib Hashtag ein'; @@ -1040,7 +1033,7 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_messageCopied => 'Nachricht kopiert'; @override - String get chat_messageDeleted => 'Nachricht gelöscht'; + String get chat_messageDeleted => 'Nachricht gelöscht'; @override String get chat_retryingMessage => 'Versuche es erneut.'; @@ -1057,7 +1050,7 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_reply => 'Beantworten'; @override - String get chat_addReaction => 'Reaktion hinzufügen'; + String get chat_addReaction => 'Reaktion hinzufügen'; @override String get chat_me => 'Ich'; @@ -1075,7 +1068,7 @@ class AppLocalizationsDe extends AppLocalizations { String get emojiCategoryObjects => 'Objekte'; @override - String get gifPicker_title => 'Wähle ein GIF'; + String get gifPicker_title => 'Wähle ein GIF'; @override String get gifPicker_searchHint => 'Suche nach GIFs...'; @@ -1105,7 +1098,7 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_copyLog => 'Kopieren des Protokolls'; @override - String get debugLog_clearLog => 'Protokoll löschen'; + String get debugLog_clearLog => 'Protokoll löschen'; @override String get debugLog_copied => 'Debug-Protokoll kopiert'; @@ -1114,7 +1107,7 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_bleCopied => 'BLE-Protokoll kopiert'; @override - String get debugLog_noEntries => 'No Debug-Protokolle noch verfügbar'; + String get debugLog_noEntries => 'No Debug-Protokolle noch verfügbar'; @override String get debugLog_enableInSettings => @@ -1127,11 +1120,11 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_rawLogRx => 'Roh-Log-RX'; @override - String get debugLog_noBleActivity => 'Bisher keine BLE-Aktivität'; + String get debugLog_noBleActivity => 'Bisher keine BLE-Aktivität'; @override String debugFrame_length(int count) { - return 'Rahmenlänge: $count Bytes'; + return 'Rahmenlänge: $count Bytes'; } @override @@ -1144,7 +1137,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String debugFrame_destinationPubKey(String pubKey) { - return '- Ziel-Public-Schlüssel: $pubKey'; + return '- Ziel-Public-Schlüssel: $pubKey'; } @override @@ -1198,20 +1191,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_pathHistoryFull => - 'Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.'; + 'Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.'; @override String get chat_hopSingular => 'Sprung'; @override - String get chat_hopPlural => 'Sprünge'; + String get chat_hopPlural => 'Sprünge'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Sprünge', + other: 'Sprünge', one: 'Sprung', ); return '$count $_temp0'; @@ -1237,15 +1230,15 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_setCustomPathSubtitle => 'Manuellen Routenpfad festlegen'; @override - String get chat_clearPath => 'Pfad zurücksetzen'; + String get chat_clearPath => 'Pfad zurücksetzen'; @override String get chat_clearPathSubtitle => - 'Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.'; + 'Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.'; @override String get chat_pathCleared => - 'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.'; + 'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.'; @override String get chat_floodModeSubtitle => @@ -1255,11 +1248,11 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_floodModeEnabled => 'Flutmodus aktiviert.'; @override - String get chat_fullPath => 'Vollständiger Pfad'; + String get chat_fullPath => 'Vollständiger Pfad'; @override String get chat_pathDetailsNotAvailable => - 'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.'; + 'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1277,10 +1270,10 @@ class AppLocalizationsDe extends AppLocalizations { 'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.'; @override - String get chat_pathDeviceConfirmed => 'Gerät bestätigt.'; + String get chat_pathDeviceConfirmed => 'Gerät bestätigt.'; @override - String get chat_pathDeviceNotConfirmed => 'Gerät noch nicht bestätigt.'; + String get chat_pathDeviceNotConfirmed => 'Gerät noch nicht bestätigt.'; @override String get chat_type => 'Gebe ein'; @@ -1289,7 +1282,7 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_path => 'Pfad'; @override - String get chat_publicKey => 'Öffentlicher Schlüssel'; + String get chat_publicKey => 'Öffentlicher Schlüssel'; @override String get chat_compressOutgoingMessages => @@ -1303,7 +1296,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String chat_hopsForced(int count) { - return '$count Sprünge (erzwungen)'; + return '$count Sprünge (erzwungen)'; } @override @@ -1321,22 +1314,22 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get chat_openLink => 'Link öffnen?'; + String get chat_openLink => 'Link öffnen?'; @override String get chat_openLinkConfirmation => - 'Möchten Sie diesen Link in Ihrem Browser öffnen?'; + 'Möchten Sie diesen Link in Ihrem Browser öffnen?'; @override - String get chat_open => 'Öffnen'; + String get chat_open => 'Öffnen'; @override String chat_couldNotOpenLink(String url) { - return 'Link konnte nicht geöffnet werden: $url'; + return 'Link konnte nicht geöffnet werden: $url'; } @override - String get chat_invalidLink => 'Ungültiges Link-Format'; + String get chat_invalidLink => 'Ungültiges Link-Format'; @override String get map_title => 'Karte'; @@ -1352,7 +1345,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_nodesNeedGps => - 'Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.'; + 'Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.'; @override String map_nodesCount(int count) { @@ -1390,7 +1383,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_disconnectConfirm => - 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; + 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; @override String get map_from => 'Von'; @@ -1420,19 +1413,19 @@ class AppLocalizationsDe extends AppLocalizations { String get map_sendToChannel => 'Senden an Kanal'; @override - String get map_noChannelsAvailable => 'Keine Kanäle verfügbar'; + String get map_noChannelsAvailable => 'Keine Kanäle verfügbar'; @override - String get map_publicLocationShare => 'Öffentliche Standortfreigabe'; + String get map_publicLocationShare => 'Öffentliche Standortfreigabe'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Sie werden kurz darauf einen Ort in $channelLabel teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.'; + return 'Sie werden kurz darauf einen Ort in $channelLabel teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.'; } @override String get map_connectToShareMarkers => - 'Verbinde ein Gerät, um Marker zu teilen'; + 'Verbinde ein Gerät, um Marker zu teilen'; @override String get map_filterNodes => 'Knotenfilter'; @@ -1450,13 +1443,13 @@ class AppLocalizationsDe extends AppLocalizations { String get map_otherNodes => 'Andere Knoten'; @override - String get map_keyPrefix => 'Schlüsselpräfix'; + String get map_keyPrefix => 'Schlüsselpräfix'; @override - String get map_filterByKeyPrefix => 'Filter nach Schlüsselpräfix'; + String get map_filterByKeyPrefix => 'Filter nach Schlüsselpräfix'; @override - String get map_publicKeyPrefix => 'Schlüsselpräfix'; + String get map_publicKeyPrefix => 'Schlüsselpräfix'; @override String get map_markers => 'Marker'; @@ -1478,10 +1471,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_tapToAdd => - 'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.'; + 'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.'; @override - String get map_runTrace => 'Pfadverlauf ausführen'; + String get map_runTrace => 'Pfadverlauf ausführen'; @override String get map_removeLast => 'Letztes Entfernen'; @@ -1494,18 +1487,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get mapCache_selectAreaFirst => - 'Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.'; + 'Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.'; @override String get mapCache_noTilesToDownload => - 'Keine Kacheln für diese Region zum Herunterladen verfügbar.'; + 'Keine Kacheln für diese Region zum Herunterladen verfügbar.'; @override String get mapCache_downloadTilesTitle => 'Herunterladen von Kacheln'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Laden $count Kacheln für den Offline-Bereich herunter?'; + return 'Laden $count Kacheln für den Offline-Bereich herunter?'; } @override @@ -1529,10 +1522,10 @@ class AppLocalizationsDe extends AppLocalizations { 'Alle zwischengespeicherten Kartenraster entfernen?'; @override - String get mapCache_offlineCacheCleared => 'Offline-Cache gelöscht'; + String get mapCache_offlineCacheCleared => 'Offline-Cache gelöscht'; @override - String get mapCache_noAreaSelected => 'Kein Bereich ausgewählt'; + String get mapCache_noAreaSelected => 'Kein Bereich ausgewählt'; @override String get mapCache_cacheArea => 'Zwischenspeicherbereich'; @@ -1545,7 +1538,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String mapCache_estimatedTiles(int count) { - return 'Geschätzte Kacheln: $count'; + return 'Geschätzte Kacheln: $count'; } @override @@ -1627,7 +1620,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dialog_disconnectConfirm => - 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; + 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; @override String get login_repeaterLogin => 'Beim Repeater anmelden'; @@ -1646,7 +1639,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'Das Passwort wird auf diesem Gerät sicher gespeichert.'; + 'Das Passwort wird auf diesem Gerät sicher gespeichert.'; @override String get login_repeaterDescription => @@ -1693,7 +1686,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_reload => 'Neu laden'; @override - String get common_clear => 'Löschen'; + String get common_clear => 'Löschen'; @override String path_currentPath(String path) { @@ -1719,21 +1712,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.'; + 'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.'; @override String get path_hexPrefixExample => - 'Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)'; + 'Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)'; @override - String get path_labelHexPrefixes => 'Pfad (Hex-Präfixe)'; + String get path_labelHexPrefixes => 'Pfad (Hex-Präfixe)'; @override String get path_helperMaxHops => - 'Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)'; + 'Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)'; @override - String get path_selectFromContacts => 'Oder wähle aus Kontakten aus:'; + String get path_selectFromContacts => 'Oder wähle aus Kontakten aus:'; @override String get path_noRepeatersFound => @@ -1741,11 +1734,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get path_customPathsRequire => - 'Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.'; + 'Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Ungültige Hexadezimal-Präfixe: $prefixes'; + return 'Ungültige Hexadezimal-Präfixe: $prefixes'; } @override @@ -1833,10 +1826,10 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_clockAtLogin => 'Uhr (bei Anmeldung)'; @override - String get repeater_uptime => 'Verfügbarkeit'; + String get repeater_uptime => 'Verfügbarkeit'; @override - String get repeater_queueLength => 'Warteschlangenlänge'; + String get repeater_queueLength => 'Warteschlangenlänge'; @override String get repeater_debugFlags => 'Fehlerbehebungsoptionen'; @@ -1911,7 +1904,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_repeaterName => 'Repeater Name'; @override - String get repeater_repeaterNameHelper => 'Anzeigename für diesen Repeater'; + String get repeater_repeaterNameHelper => 'Anzeigename für diesen Repeater'; @override String get repeater_adminPassword => 'Admin-Passwort'; @@ -1924,7 +1917,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_guestPasswordHelper => - 'Schreibgeschütztes Zugriffspasswort'; + 'Schreibgeschütztes Zugriffspasswort'; @override String get repeater_radioSettings => 'Funk Einstellungen'; @@ -1960,7 +1953,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_latitudeHelper => 'Dezimalgrad (z.B. 37,7749)'; @override - String get repeater_longitude => 'Längengrad'; + String get repeater_longitude => 'Längengrad'; @override String get repeater_longitudeHelper => 'Dezimalgrad (z.B. -122,4194)'; @@ -1980,21 +1973,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_guestAccessSubtitle => - 'Gast-Zugriff mit beschränkten Rechten zulassen'; + 'Gast-Zugriff mit beschränkten Rechten zulassen'; @override - String get repeater_privacyMode => 'Privatsphäreeinstellung'; + String get repeater_privacyMode => 'Privatsphäreeinstellung'; @override String get repeater_privacyModeSubtitle => - 'Verstecken Sie Name/Ort in Ankündigungen'; + 'Verstecken Sie Name/Ort in Ankündigungen'; @override - String get repeater_advertisementSettings => 'Ankündigungseinstellungen'; + String get repeater_advertisementSettings => 'Ankündigungseinstellungen'; @override String get repeater_localAdvertInterval => - 'Intervall der lokalen Ankündigungen'; + 'Intervall der lokalen Ankündigungen'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2003,7 +1996,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervall der gefluteten Ankündigungen'; + 'Intervall der gefluteten Ankündigungen'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2012,7 +2005,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervall der verschlüsselten Ankündigung'; + 'Intervall der verschlüsselten Ankündigung'; @override String get repeater_dangerZone => 'Gefahrenzone'; @@ -2021,26 +2014,26 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_rebootRepeater => 'Neustart Repeater'; @override - String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.'; + String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.'; @override String get repeater_rebootRepeaterConfirm => - 'Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?'; + 'Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?'; @override String get repeater_regenerateIdentityKey => - 'Schlüssel für die Identitätswiederherstellung'; + 'Schlüssel für die Identitätswiederherstellung'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Neuen öffentlichen/privaten Schlüsselpaar generieren'; + 'Neuen öffentlichen/privaten Schlüsselpaar generieren'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Dies generiert eine neue Identität für den Repeater. Fortfahren?'; + 'Dies generiert eine neue Identität für den Repeater. Fortfahren?'; @override - String get repeater_eraseFileSystem => 'Dateisystem löschen'; + String get repeater_eraseFileSystem => 'Dateisystem löschen'; @override String get repeater_eraseFileSystemSubtitle => @@ -2048,11 +2041,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!'; + 'WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!'; @override String get repeater_eraseSerialOnly => - 'Löschen ist nur über die serielle Konsole möglich.'; + 'Löschen ist nur über die serielle Konsole möglich.'; @override String repeater_commandSent(String command) { @@ -2065,7 +2058,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_confirm => 'Bestätigen'; + String get repeater_confirm => 'Bestätigen'; @override String get repeater_settingsSaved => 'Einstellungen erfolgreich gespeichert'; @@ -2103,7 +2096,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_refreshAdvertisementSettings => - 'Aktualisieren Sie die Ankündigungseinstellungen'; + 'Aktualisieren Sie die Ankündigungseinstellungen'; @override String repeater_refreshed(String label) { @@ -2119,13 +2112,13 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliTitle => 'Repeater CLI'; @override - String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls'; + String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls'; @override String get repeater_commandHelp => 'Hilfe'; @override - String get repeater_clearHistory => 'Löschen der Historie'; + String get repeater_clearHistory => 'Löschen der Historie'; @override String get repeater_noCommandsSent => 'Noch keine Befehle gesendet.'; @@ -2141,7 +2134,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_previousCommand => 'Vorhergehende Aktion'; @override - String get repeater_nextCommand => 'Nächste Aktion'; + String get repeater_nextCommand => 'Nächste Aktion'; @override String get repeater_enterCommandFirst => 'Geben Sie zuerst einen Befehl ein'; @@ -2170,52 +2163,52 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliQuickVersion => 'Version'; @override - String get repeater_cliQuickAdvertise => 'Ankündigungen'; + String get repeater_cliQuickAdvertise => 'Ankündigungen'; @override String get repeater_cliQuickClock => 'Uhr'; @override - String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; + String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; @override String get repeater_cliHelpReboot => - 'Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer \'Timeout\'-Situation kommt, was normal ist.)'; + 'Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer \'Timeout\'-Situation kommt, was normal ist.)'; @override String get repeater_cliHelpClock => - 'Zeigt die aktuelle Uhrzeit pro Gerät an.'; + 'Zeigt die aktuelle Uhrzeit pro Gerät an.'; @override String get repeater_cliHelpPassword => - 'Legt ein neues Administrator-Passwort für das Gerät fest.'; + 'Legt ein neues Administrator-Passwort für das Gerät fest.'; @override String get repeater_cliHelpVersion => - 'Zeigt die Geräteversion und das Datum des Firmware-Builds an.'; + 'Zeigt die Geräteversion und das Datum des Firmware-Builds an.'; @override String get repeater_cliHelpClearStats => - 'Setzt verschiedene Statistikberechnungen auf Null zurück.'; + 'Setzt verschiedene Statistikberechnungen auf Null zurück.'; @override String get repeater_cliHelpSetAf => 'Legt den Luftzeitfaktor fest.'; @override String get repeater_cliHelpSetTx => - 'Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)'; + 'Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)'; @override String get repeater_cliHelpSetRepeat => - 'Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.'; + 'Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Raumspeicher) Wenn \'an\', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).'; + '(Raumspeicher) Wenn \'an\', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).'; @override String get repeater_cliHelpSetFloodMax => - 'Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)'; + 'Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)'; @override String get repeater_cliHelpSetIntThresh => @@ -2223,7 +2216,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetAgcResetInterval => - 'Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetMultiAcks => @@ -2231,78 +2224,78 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.'; + 'Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.'; @override String get repeater_cliHelpSetGuestPassword => - 'Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)'; + 'Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)'; @override String get repeater_cliHelpSetName => 'Legt den Anzeigenamen fest.'; @override String get repeater_cliHelpSetLat => - 'Legt die Breitengrad der Ankündigung fest. (dezimale Grad)'; + 'Legt die Breitengrad der Ankündigung fest. (dezimale Grad)'; @override String get repeater_cliHelpSetLon => - 'Legt die Längengrade der Ankündigung fest. (dezimale Grad)'; + 'Legt die Längengrade der Ankündigung fest. (dezimale Grad)'; @override String get repeater_cliHelpSetRadio => - 'Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.'; + 'Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.'; @override String get repeater_cliHelpSetRxDelay => - 'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetTxDelay => - 'Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).'; + 'Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.'; + 'Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.'; @override String get repeater_cliHelpSetBridgeEnabled => - 'Brücke aktivieren/deaktivieren.'; + 'Brücke aktivieren/deaktivieren.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Setze Verzögerung vor erneuter Übertragung von Paketen.'; + 'Setze Verzögerung vor erneuter Übertragung von Paketen.'; @override String get repeater_cliHelpSetBridgeSource => - 'Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.'; + 'Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Setze die serielle Link-Baudrate für RS232-Brücken.'; + 'Setze die serielle Link-Baudrate für RS232-Brücken.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Richte das Brückenpassword ein.'; + 'Richte das Brückenpassword ein.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).'; + 'Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).'; @override String get repeater_cliHelpTempRadio => - 'Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).'; + 'Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).'; @override String get repeater_cliHelpSetPerm => - 'Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)'; + 'Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => - 'Ruft Brückentyp: none, rs232, espnow ab.'; + 'Ruft Brückentyp: none, rs232, espnow ab.'; @override String get repeater_cliHelpLogStart => @@ -2314,46 +2307,46 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpLogErase => - 'Löscht die Paketprotokolle aus dem Dateisystem.'; + 'Löscht die Paketprotokolle aus dem Dateisystem.'; @override String get repeater_cliHelpNeighbors => - 'Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4'; + 'Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.'; + 'Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.'; @override String get repeater_cliHelpRegion => 'Listet alle definierten Regionen auf.'; @override String get repeater_cliHelpRegionLoad => - 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.'; + 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.'; @override String get repeater_cliHelpRegionGet => - 'Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) \'F\'\".'; + 'Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) \'F\'\".'; @override String get repeater_cliHelpRegionPut => - 'Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.'; + 'Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.'; @override String get repeater_cliHelpRegionRemove => - 'Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)'; + 'Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)'; @override String get repeater_cliHelpRegionAllowf => - 'Legt die \'Flut\'-Berechtigung für die angegebene Region fest. (\'\' für den globalen/legacy-Bereich)'; + 'Legt die \'Flut\'-Berechtigung für die angegebene Region fest. (\'\' für den globalen/legacy-Bereich)'; @override String get repeater_cliHelpRegionDenyf => - 'Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)'; + 'Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)'; @override String get repeater_cliHelpRegionHome => - 'Antwortet mit der aktuellen \'Home\'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)'; + 'Antwortet mit der aktuellen \'Home\'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)'; @override String get repeater_cliHelpRegionHomeSet => 'Legt die \'Home\'-Region fest.'; @@ -2375,11 +2368,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpGpsSetLoc => - 'Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.'; + 'Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.'; @override String get repeater_cliHelpGpsAdvert => - 'Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen'; + 'Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen'; @override String get repeater_cliHelpGpsAdvertSet => @@ -2390,7 +2383,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_commandsListNote => - 'ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.'; + 'ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.'; @override String get repeater_general => 'Allgemein'; @@ -2399,7 +2392,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_settingsCategory => 'Einstellungen'; @override - String get repeater_bridge => 'Brücke'; + String get repeater_bridge => 'Brücke'; @override String get repeater_logging => 'Protokollierung'; @@ -2413,14 +2406,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_regionNote => - 'Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.'; + 'Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.'; @override String get repeater_gpsManagement => 'GPS-Verwaltung'; @override String get repeater_gpsNote => - 'Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.'; + 'Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.'; @override String get telemetry_receivedData => 'Empfangene Telemetriedaten'; @@ -2435,7 +2428,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get telemetry_noData => 'Keine Telemetriedaten verfügbar.'; + String get telemetry_noData => 'Keine Telemetriedaten verfügbar.'; @override String telemetry_channelTitle(int channel) { @@ -2474,7 +2467,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2493,7 +2486,7 @@ class AppLocalizationsDe extends AppLocalizations { String get neighbors_repeatersNeighbors => 'Nachbarn'; @override - String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; + String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; @override String neighbors_unknownContact(String pubkey) { @@ -2502,7 +2495,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Gehört vor: $time'; + return 'Gehört vor: $time'; } @override @@ -2515,11 +2508,11 @@ class AppLocalizationsDe extends AppLocalizations { String get channelPath_otherObservedPaths => 'Sonstige beobachtete Pfade'; @override - String get channelPath_repeaterHops => 'Repeater-Sprünge'; + String get channelPath_repeaterHops => 'Repeater-Sprünge'; @override String get channelPath_noHopDetails => - 'Die Detailangaben für dieses Paket sind nicht verfügbar.'; + 'Die Detailangaben für dieses Paket sind nicht verfügbar.'; @override String get channelPath_messageDetails => 'Nachrichtendetails'; @@ -2543,7 +2536,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Beobachteter Pfad $index • $hops'; + return 'Beobachteter Pfad $index • $hops'; } @override @@ -2570,12 +2563,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String channelPath_observedZeroOf(int total) { - return '0 von $total Sprüngen'; + return '0 von $total Sprüngen'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed von $total Sprüngen'; + return '$observed von $total Sprüngen'; } @override @@ -2583,11 +2576,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.'; + 'Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.'; @override String channelPath_primaryPath(int index) { - return 'Pfad $index (Primär)'; + return 'Pfad $index (Primär)'; } @override @@ -2598,12 +2591,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Keine Informationen zu dieser Paketroute verfügbar.'; + 'Keine Informationen zu dieser Paketroute verfügbar.'; @override String get channelPath_unknownRepeater => 'Unbekannter Repeater'; @@ -2616,7 +2609,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get community_createDesc => - 'Erstelle eine neue Community und teile sie über den QR-Code.'; + 'Erstelle eine neue Community und teile sie über den QR-Code.'; @override String get community_join => 'Beitreten'; @@ -2626,7 +2619,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_joinConfirmation(String name) { - return 'Möchten Sie sich der Community \"$name\" anschließen?'; + return 'Möchten Sie sich der Community \"$name\" anschließen?'; } @override @@ -2640,7 +2633,7 @@ class AppLocalizationsDe extends AppLocalizations { String get community_showQr => 'Zeige QR-Code'; @override - String get community_publicChannel => 'Community Öffentlich'; + String get community_publicChannel => 'Community Öffentlich'; @override String get community_hashtagChannel => 'Community Hashtag'; @@ -2666,15 +2659,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Scannen Sie diesen QR-Code, um sich \"$name\" anzuschließen.'; + return 'Scannen Sie diesen QR-Code, um sich \"$name\" anzuschließen.'; } @override String get community_hashtagPrivacyHint => - 'Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden'; + 'Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden'; @override - String get community_invalidQrCode => 'Ungültiger Community-QR-Code'; + String get community_invalidQrCode => 'Ungültiger Community-QR-Code'; @override String get community_alreadyMember => 'Bereits registriert'; @@ -2686,11 +2679,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get community_addPublicChannel => - 'Füge einen öffentlichen Community-Kanal hinzu'; + 'Füge einen öffentlichen Community-Kanal hinzu'; @override String get community_addPublicChannelHint => - 'Automatisch den öffentlichen Kanal für diese Community hinzufügen'; + 'Automatisch den öffentlichen Kanal für diese Community hinzufügen'; @override String get community_noCommunities => 'Noch keiner Community beigetreten'; @@ -2712,7 +2705,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Dies löscht auch $count Kanal/Kanäle und deren Nachrichten.'; + return 'Dies löscht auch $count Kanal/Kanäle und deren Nachrichten.'; } @override @@ -2721,11 +2714,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get community_regenerateSecret => 'Neugenerierung des Schlüssels'; + String get community_regenerateSecret => 'Neugenerierung des Schlüssels'; @override String community_regenerateSecretConfirm(String name) { - return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.'; + return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.'; } @override @@ -2733,50 +2726,50 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich'; + return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich'; } @override - String get community_updateSecret => 'Aktualisieren Sie den Schlüssel'; + String get community_updateSecret => 'Aktualisieren Sie den Schlüssel'; @override String community_secretUpdated(String name) { - return 'Schlüssel für \"$name\" aktualisiert'; + return 'Schlüssel für \"$name\" aktualisiert'; } @override String community_scanToUpdateSecret(String name) { - return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.'; + return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.'; } @override String get community_addHashtagChannel => - 'Füge einen Community-Hashtag hinzu'; + 'Füge einen Community-Hashtag hinzu'; @override String get community_addHashtagChannelDesc => - 'Füge einen Hashtag-Kanal für diese Community hinzu'; + 'Füge einen Hashtag-Kanal für diese Community hinzu'; @override - String get community_selectCommunity => 'Wählen Sie eine Community'; + String get community_selectCommunity => 'Wählen Sie eine Community'; @override - String get community_regularHashtag => 'Regulärer Hashtag'; + String get community_regularHashtag => 'Regulärer Hashtag'; @override String get community_regularHashtagDesc => - 'Öffentlicher Hashtag (jeder kann teilnehmen)'; + 'Öffentlicher Hashtag (jeder kann teilnehmen)'; @override String get community_communityHashtag => 'Community Hashtag'; @override String get community_communityHashtagDesc => - 'Nur für Mitglieder der Community'; + 'Nur für Mitglieder der Community'; @override String community_forCommunity(String name) { - return 'Für $name'; + return 'Für $name'; } @override @@ -2789,7 +2782,7 @@ class AppLocalizationsDe extends AppLocalizations { String get listFilter_latestMessages => 'Letzte Nachrichten'; @override - String get listFilter_heardRecently => 'Kürzlich gehört'; + String get listFilter_heardRecently => 'Kürzlich gehört'; @override String get listFilter_az => 'A-Z'; @@ -2804,7 +2797,7 @@ class AppLocalizationsDe extends AppLocalizations { String get listFilter_favorites => 'Favoriten'; @override - String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen'; + String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen'; @override String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen'; @@ -2831,7 +2824,7 @@ class AppLocalizationsDe extends AppLocalizations { String get pathTrace_failed => 'Pfadverfolgung fehlgeschlagen.'; @override - String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.'; + String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.'; @override String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.'; @@ -2841,30 +2834,30 @@ class AppLocalizationsDe extends AppLocalizations { 'Bei einer oder mehreren Knoten fehlt der Standort!'; @override - String get pathTrace_clearTooltip => 'Pfad löschen'; + String get pathTrace_clearTooltip => 'Pfad löschen'; @override String get losSelectStartEnd => - 'Wählen Sie Start- und Endknoten für LOS aus.'; + 'Wählen Sie Start- und Endknoten für LOS aus.'; @override String losRunFailed(String error) { - return 'Sichtlinienprüfung fehlgeschlagen: $error'; + return 'Sichtlinienprüfung fehlgeschlagen: $error'; } @override - String get losClearAllPoints => 'Löschen Sie alle Punkte'; + String get losClearAllPoints => 'Löschen Sie alle Punkte'; @override String get losRunToViewElevationProfile => - 'Führen Sie LOS aus, um das Höhenprofil anzuzeigen'; + 'Führen Sie LOS aus, um das Höhenprofil anzuzeigen'; @override - String get losMenuTitle => 'LOS-Menü'; + String get losMenuTitle => 'LOS-Menü'; @override String get losMenuSubtitle => - 'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen'; + 'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen'; @override String get losShowDisplayNodes => 'Anzeigeknoten anzeigen'; @@ -2894,10 +2887,10 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get losRun => 'Führen Sie LOS aus'; + String get losRun => 'Führen Sie LOS aus'; @override - String get losNoElevationData => 'Keine Höhendaten'; + String get losNoElevationData => 'Keine Höhendaten'; @override String losProfileClear( @@ -2920,7 +2913,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get losStatusChecking => 'LOS: Überprüfen...'; + String get losStatusChecking => 'LOS: Überprüfen...'; @override String get losStatusNoData => 'LOS: keine Daten'; @@ -2932,11 +2925,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.'; + 'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.'; @override String get losErrorInvalidInput => - 'Ungültige Punkte/Höhendaten für die LOS-Berechnung.'; + 'Ungültige Punkte/Höhendaten für die LOS-Berechnung.'; @override String get losRenameCustomPoint => @@ -2952,7 +2945,7 @@ class AppLocalizationsDe extends AppLocalizations { String get losHidePanelTooltip => 'LOS-Panel ausblenden'; @override - String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; + String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Funkhorizont'; @@ -2961,7 +2954,7 @@ class AppLocalizationsDe extends AppLocalizations { String get losLegendLosBeam => 'Sichtlinie'; @override - String get losLegendTerrain => 'Gelände'; + String get losLegendTerrain => 'Gelände'; @override String get losFrequencyLabel => 'Frequenz'; @@ -2979,7 +2972,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; } @override @@ -3012,7 +3005,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_clipboardEmpty => 'Die Zwischenablage ist leer.'; @override - String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten'; + String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten'; @override String get contacts_contactImported => 'Kontakt wurde importiert.'; @@ -3022,28 +3015,28 @@ class AppLocalizationsDe extends AppLocalizations { 'Kontakt konnte nicht importiert werden'; @override - String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung'; + String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung'; @override - String get contacts_floodAdvert => 'Flut-Ankündigung'; + String get contacts_floodAdvert => 'Flut-Ankündigung'; @override String get contacts_copyAdvertToClipboard => - 'Ankündigung in die Zwischenablage kopieren'; + 'Ankündigung in die Zwischenablage kopieren'; @override String get contacts_addContactFromClipboard => - 'Kontakt aus Zwischenablage hinzufügen'; + 'Kontakt aus Zwischenablage hinzufügen'; @override String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren'; @override - String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen'; + String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen'; @override String get contacts_zeroHopContactAdvertSent => - 'Kontakt über Anzeige gesendet'; + 'Kontakt über Anzeige gesendet'; @override String get contacts_zeroHopContactAdvertFailed => @@ -3055,10 +3048,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => - 'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.'; + 'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.'; @override - String get notification_activityTitle => 'MeshCore Aktivität'; + String get notification_activityTitle => 'MeshCore Aktivität'; @override String notification_messagesCount(int count) { @@ -3131,7 +3124,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_gpxExportNotAvailable => - 'Nicht auf Ihrem Gerät/Betriebssystem unterstützt'; + 'Nicht auf Ihrem Gerät/Betriebssystem unterstützt'; @override String get settings_gpxExportError => @@ -3156,7 +3149,8 @@ class AppLocalizationsDe extends AppLocalizations { 'GPX-Kartendaten aus meshcore-open exportieren'; @override - String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater'; + String get snrIndicator_nearByRepeaters => + 'In der Nähe befindliche Repeater'; @override String get snrIndicator_lastSeen => 'Zuletzt gesehen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2c287f7..a39d473 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -93,7 +93,7 @@ class AppLocalizationsEn extends AppLocalizations { String get common_loading => 'Loading...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Choose your connection method'; - - @override - String get connectionChoiceSubtitle => - 'Select how you would like to reach your MeshCore device.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -455,10 +448,10 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -467,16 +460,16 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -485,16 +478,16 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Русский'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Українська'; @override String get appSettings_enableMessageTracing => 'Enable Message Tracing'; @@ -2431,7 +2424,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2499,7 +2492,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Observed path $index • $hops'; + return 'Observed path $index • $hops'; } @override @@ -2554,7 +2547,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cc7261a..2604928 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -45,10 +45,10 @@ class AppLocalizationsEs extends AppLocalizations { String get common_edit => 'Editar'; @override - String get common_add => 'Añadir'; + String get common_add => 'Añadir'; @override - String get common_settings => 'Configuración'; + String get common_settings => 'Configuración'; @override String get common_disconnect => 'Desconectar'; @@ -93,7 +93,7 @@ class AppLocalizationsEs extends AppLocalizations { String get common_loading => 'Cargando...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Seleccione su método de conexión.'; - - @override - String get connectionChoiceSubtitle => - 'Seleccione la forma en que desea acceder a su dispositivo MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -126,14 +119,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbScreenSubtitle => - 'Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.'; + 'Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.'; @override String get usbScreenStatus => 'Seleccione un dispositivo USB'; @override String get usbScreenNote => - 'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.'; + 'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.'; @override String get usbScreenEmptyState => @@ -149,7 +142,7 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_disconnecting => 'Desconectando...'; @override - String get scanner_notConnected => 'No está conectado'; + String get scanner_notConnected => 'No está conectado'; @override String scanner_connectedTo(String deviceName) { @@ -165,7 +158,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Error de conexión: $error'; + return 'Error de conexión: $error'; } @override @@ -175,7 +168,7 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_scan => 'Escanea'; @override - String get scanner_bluetoothOff => 'Bluetooth está desactivado.'; + String get scanner_bluetoothOff => 'Bluetooth está desactivado.'; @override String get scanner_bluetoothOffMessage => @@ -186,38 +179,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_chromeRequiredMessage => - 'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.'; + 'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.'; @override String get scanner_enableBluetooth => 'Habilitar Bluetooth'; @override - String get device_quickSwitch => 'Cambiar rápidamente'; + String get device_quickSwitch => 'Cambiar rápidamente'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Configuración'; + String get settings_title => 'Configuración'; @override - String get settings_deviceInfo => 'Información del dispositivo'; + String get settings_deviceInfo => 'Información del dispositivo'; @override - String get settings_appSettings => 'Configuración de la App'; + String get settings_appSettings => 'Configuración de la App'; @override String get settings_appSettingsSubtitle => 'Notificaciones, mensajes y preferencias de mapa'; @override - String get settings_nodeSettings => 'Configuración del Nodo'; + String get settings_nodeSettings => 'Configuración del Nodo'; @override String get settings_nodeName => 'Nombre del nodo'; @override - String get settings_nodeNameNotSet => 'No está configurado'; + String get settings_nodeNameNotSet => 'No está configurado'; @override String get settings_nodeNameHint => 'Introducir nombre de nodo'; @@ -226,37 +219,37 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_nodeNameUpdated => 'Nombre actualizado'; @override - String get settings_radioSettings => 'Configuración de Radio'; + String get settings_radioSettings => 'Configuración de Radio'; @override String get settings_radioSettingsSubtitle => - 'Frecuencia, potencia, factor de dispersión'; + 'Frecuencia, potencia, factor de dispersión'; @override String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados'; @override - String get settings_location => 'Ubicación'; + String get settings_location => 'Ubicación'; @override String get settings_locationSubtitle => 'Coordenadas GPS'; @override - String get settings_locationUpdated => 'Ubicación actualizada'; + String get settings_locationUpdated => 'Ubicación actualizada'; @override String get settings_locationBothRequired => 'Introduzca tanto la latitud como la longitud.'; @override - String get settings_locationInvalid => 'Latitud o longitud inválidos.'; + String get settings_locationInvalid => 'Latitud o longitud inválidos.'; @override String get settings_locationGPSEnable => 'Habilitar GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Habilita la actualización automática de la ubicación mediante GPS.'; + 'Habilita la actualización automática de la ubicación mediante GPS.'; @override String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)'; @@ -276,11 +269,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_privacyModeSubtitle => - 'Ocultar nombre/ubicación en anuncios'; + 'Ocultar nombre/ubicación en anuncios'; @override String get settings_privacyModeToggle => - 'Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.'; + 'Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.'; @override String get settings_privacyModeEnabled => 'Modo de privacidad activado'; @@ -296,17 +289,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_sendAdvertisementSubtitle => - 'Presencia de transmisión ahora'; + 'Presencia de transmisión ahora'; @override String get settings_advertisementSent => 'Anuncio enviado'; @override - String get settings_syncTime => 'Tiempo de Sincronización'; + String get settings_syncTime => 'Tiempo de Sincronización'; @override String get settings_syncTimeSubtitle => - 'Establecer la hora del dispositivo al tiempo del teléfono'; + 'Establecer la hora del dispositivo al tiempo del teléfono'; @override String get settings_timeSynchronized => 'Sincronizado en el tiempo'; @@ -327,24 +320,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - '¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.'; + '¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.'; @override String get settings_debug => 'Depurar'; @override - String get settings_bleDebugLog => 'Registro de Depuración BLE'; + String get settings_bleDebugLog => 'Registro de Depuración BLE'; @override String get settings_bleDebugLogSubtitle => 'Comandos, respuestas y datos brutos de BLE'; @override - String get settings_appDebugLog => 'Registro de Depuración de la App'; + String get settings_appDebugLog => 'Registro de Depuración de la App'; @override String get settings_appDebugLogSubtitle => - 'Mensajes de depuración de la aplicación'; + 'Mensajes de depuración de la aplicación'; @override String get settings_about => 'Acerca de'; @@ -359,11 +352,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_aboutDescription => - 'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.'; + 'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Datos de elevación LOS: Open-Meteo (CC BY 4.0)'; + 'Datos de elevación LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Nombre'; @@ -375,16 +368,16 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_infoStatus => 'Estado'; @override - String get settings_infoBattery => 'Batería'; + String get settings_infoBattery => 'Batería'; @override - String get settings_infoPublicKey => 'Clave Pública'; + String get settings_infoPublicKey => 'Clave Pública'; @override - String get settings_infoContactsCount => 'Número de contactos'; + String get settings_infoContactsCount => 'Número de contactos'; @override - String get settings_infoChannelCount => 'Número de canales'; + String get settings_infoChannelCount => 'Número de canales'; @override String get settings_presets => 'Preajustes'; @@ -396,16 +389,16 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_frequencyHelper => '300,0 - 2500,0'; @override - String get settings_frequencyInvalid => 'Frecuencia inválida (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Frecuencia inválida (300-2500 MHz)'; @override String get settings_bandwidth => 'Ancho de banda'; @override - String get settings_spreadingFactor => 'Factor de propagación'; + String get settings_spreadingFactor => 'Factor de propagación'; @override - String get settings_codingRate => 'Tasa de Programación'; + String get settings_codingRate => 'Tasa de Programación'; @override String get settings_txPower => 'TX Potencia (dBm)'; @@ -414,10 +407,10 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)'; + String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)'; @override - String get settings_clientRepeat => 'Repetir sin conexión'; + String get settings_clientRepeat => 'Repetir sin conexión'; @override String get settings_clientRepeatSubtitle => @@ -425,7 +418,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_clientRepeatFreqWarning => - 'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.'; + 'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.'; @override String settings_error(String message) { @@ -433,7 +426,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get appSettings_title => 'Configuración de la App'; + String get appSettings_title => 'Configuración de la App'; @override String get appSettings_appearance => 'Apariencia'; @@ -460,10 +453,10 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -472,16 +465,16 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -490,10 +483,10 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Ruso'; @@ -521,7 +514,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_notificationPermissionDenied => - 'Permiso de notificación denegado'; + 'Permiso de notificación denegado'; @override String get appSettings_notificationsEnabled => 'Notificaciones activadas'; @@ -534,7 +527,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_messageNotificationsSubtitle => - 'Mostrar notificación al recibir nuevos mensajes'; + 'Mostrar notificación al recibir nuevos mensajes'; @override String get appSettings_channelMessageNotifications => @@ -542,7 +535,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_channelMessageNotificationsSubtitle => - 'Mostrar notificación al recibir mensajes del canal'; + 'Mostrar notificación al recibir mensajes del canal'; @override String get appSettings_advertisementNotifications => @@ -550,10 +543,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'Mostrar notificación cuando se descubren nuevos nodos'; + 'Mostrar notificación cuando se descubren nuevos nodos'; @override - String get appSettings_messaging => 'Mensajería'; + String get appSettings_messaging => 'Mensajería'; @override String get appSettings_clearPathOnMaxRetry => @@ -561,45 +554,45 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Restablecer la ruta de contacto después de 5 intentos de envío fallidos'; + 'Restablecer la ruta de contacto después de 5 intentos de envío fallidos'; @override String get appSettings_pathsWillBeCleared => - 'Los caminos se limpiarán después de 5 intentos fallidos.'; + 'Los caminos se limpiarán después de 5 intentos fallidos.'; @override String get appSettings_pathsWillNotBeCleared => - 'Las rutas no se eliminarán automáticamente.'; + 'Las rutas no se eliminarán automáticamente.'; @override - String get appSettings_autoRouteRotation => 'Rotación de Ruta Automática'; + String get appSettings_autoRouteRotation => 'Rotación de Ruta Automática'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alternar entre las mejores rutas y el modo inundación'; + 'Alternar entre las mejores rutas y el modo inundación'; @override String get appSettings_autoRouteRotationEnabled => - 'Rotación de ruta automática habilitada'; + 'Rotación de ruta automática habilitada'; @override String get appSettings_autoRouteRotationDisabled => - 'Rotación de ruta automática desactivada'; + 'Rotación de ruta automática desactivada'; @override - String get appSettings_battery => 'Batería'; + String get appSettings_battery => 'Batería'; @override - String get appSettings_batteryChemistry => 'Química de la batería'; + String get appSettings_batteryChemistry => 'Química de la batería'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Configuración por dispositivo ($deviceName)'; + return 'Configuración por dispositivo ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Conéctate a un dispositivo para elegir'; + 'Conéctate a un dispositivo para elegir'; @override String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; @@ -611,7 +604,7 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; @override - String get appSettings_mapDisplay => 'Visualización del Mapa'; + String get appSettings_mapDisplay => 'Visualización del Mapa'; @override String get appSettings_showRepeaters => 'Mostrar Repetidores'; @@ -642,7 +635,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String appSettings_timeFilterShowLast(int hours) { - return 'Mostrar nodos de las últimas $hours horas'; + return 'Mostrar nodos de las últimas $hours horas'; } @override @@ -656,67 +649,68 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_allTime => 'Todo el tiempo'; @override - String get appSettings_lastHour => 'Última hora'; + String get appSettings_lastHour => 'Última hora'; @override - String get appSettings_last6Hours => 'Últimas 6 horas'; + String get appSettings_last6Hours => 'Últimas 6 horas'; @override - String get appSettings_last24Hours => 'Últimas 24 horas'; + String get appSettings_last24Hours => 'Últimas 24 horas'; @override String get appSettings_lastWeek => 'La semana pasada'; @override - String get appSettings_offlineMapCache => 'Caché de Mapa Offline'; + String get appSettings_offlineMapCache => 'Caché de Mapa Offline'; @override String get appSettings_unitsTitle => 'Unidades'; @override - String get appSettings_unitsMetric => 'Métrico (m/km)'; + String get appSettings_unitsMetric => 'Métrico (m/km)'; @override String get appSettings_unitsImperial => 'Imperial (pies/millas)'; @override - String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área'; + String get appSettings_noAreaSelected => + 'No se ha seleccionado ningún área'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Área seleccionada (zoom $minZoom-$maxZoom)'; + return 'Área seleccionada (zoom $minZoom-$maxZoom)'; } @override String get appSettings_debugCard => 'Depurar'; @override - String get appSettings_appDebugLogging => 'Registro de Depuración de la App'; + String get appSettings_appDebugLogging => 'Registro de Depuración de la App'; @override String get appSettings_appDebugLoggingSubtitle => - 'Registrar mensajes de depuración de la app de registro para solucionar problemas'; + 'Registrar mensajes de depuración de la app de registro para solucionar problemas'; @override String get appSettings_appDebugLoggingEnabled => - 'Registro de depuración de la aplicación habilitado'; + 'Registro de depuración de la aplicación habilitado'; @override String get appSettings_appDebugLoggingDisabled => - 'El registro de depuración de la aplicación está desactivado'; + 'El registro de depuración de la aplicación está desactivado'; @override String get contacts_title => 'Contactos'; @override - String get contacts_noContacts => 'Aún no hay contactos.'; + String get contacts_noContacts => 'Aún no hay contactos.'; @override String get contacts_contactsWillAppear => - 'Los contactos aparecerán cuando los dispositivos anuncien.'; + 'Los contactos aparecerán cuando los dispositivos anuncien.'; @override - String get contacts_unread => 'No leído'; + String get contacts_unread => 'No leído'; @override String get contacts_searchContactsNoNumber => 'Buscar contactos...'; @@ -765,7 +759,7 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_manageRepeater => 'Gestionar Repetidor'; @override - String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; + String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; @override String get contacts_roomLogin => 'Inicio de Sala'; @@ -809,27 +803,27 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_noMembers => 'No miembros'; @override - String get contacts_lastSeenNow => 'Última vez que se vio ahora'; + String get contacts_lastSeenNow => 'Última vez que se vio ahora'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Última vez visto hace $minutes minutos.'; + return 'Última vez visto hace $minutes minutos.'; } @override - String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora'; + String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Última vez visto hace $hours horas.'; + return 'Última vez visto hace $hours horas.'; } @override - String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día'; + String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Última vez visto hace $days días.'; + return 'Última vez visto hace $days días.'; } @override @@ -839,7 +833,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_noChannelsConfigured => 'No se han configurado canales'; @override - String get channels_addPublicChannel => 'Añadir Canal Público'; + String get channels_addPublicChannel => 'Añadir Canal Público'; @override String get channels_searchChannels => 'Buscar canales...'; @@ -856,13 +850,13 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_hashtagChannel => 'Canal con hashtag'; @override - String get channels_public => 'Público'; + String get channels_public => 'Público'; @override String get channels_private => 'Privado'; @override - String get channels_publicChannel => 'Canal público'; + String get channels_publicChannel => 'Canal público'; @override String get channels_privateChannel => 'Canal privado'; @@ -895,19 +889,19 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get channels_addChannel => 'Añadir Canal'; + String get channels_addChannel => 'Añadir Canal'; @override - String get channels_channelIndexLabel => 'Índice de Canal'; + String get channels_channelIndexLabel => 'Índice de Canal'; @override String get channels_channelName => 'Nombre del canal'; @override - String get channels_usePublicChannel => 'Usar Canal Público'; + String get channels_usePublicChannel => 'Usar Canal Público'; @override - String get channels_standardPublicPsk => 'PSK estándar público'; + String get channels_standardPublicPsk => 'PSK estándar público'; @override String get channels_pskHex => 'PSK (Hex)'; @@ -925,7 +919,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String channels_channelAdded(String name) { - return 'Canal \"$name\" añadido'; + return 'Canal \"$name\" añadido'; } @override @@ -934,7 +928,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get channels_smazCompression => 'Compresión SMAZ'; + String get channels_smazCompression => 'Compresión SMAZ'; @override String channels_channelUpdated(String name) { @@ -942,7 +936,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Canal público añadido'; + String get channels_publicChannelAdded => 'Canal público añadido'; @override String get channels_sortBy => 'Ordenar por'; @@ -954,7 +948,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Últimos mensajes'; + String get channels_sortLatestMessages => 'Últimos mensajes'; @override String get channels_sortUnread => 'Sin leer'; @@ -967,31 +961,31 @@ class AppLocalizationsEs extends AppLocalizations { 'Cifrado con una clave secreta.'; @override - String get channels_joinPrivateChannel => 'Únete a un Canal Privado'; + String get channels_joinPrivateChannel => 'Únete a un Canal Privado'; @override String get channels_joinPrivateChannelDesc => 'Introducir manualmente una clave secreta.'; @override - String get channels_joinPublicChannel => 'Únete al Canal Público'; + String get channels_joinPublicChannel => 'Únete al Canal Público'; @override String get channels_joinPublicChannelDesc => 'Cualquiera puede unirse a este canal.'; @override - String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag'; + String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag'; @override String get channels_joinHashtagChannelDesc => 'Cualquiera puede unirse a los canales de hashtag.'; @override - String get channels_scanQrCode => 'Escanear un Código QR'; + String get channels_scanQrCode => 'Escanear un Código QR'; @override - String get channels_scanQrCodeComingSoon => 'Próximamente'; + String get channels_scanQrCodeComingSoon => 'Próximamente'; @override String get channels_enterHashtag => 'Introducir hashtag'; @@ -1000,7 +994,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_hashtagHint => 'ej. #equipo'; @override - String get chat_noMessages => 'Aún no hay mensajes'; + String get chat_noMessages => 'Aún no hay mensajes'; @override String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar'; @@ -1019,7 +1013,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_location => 'Ubicación'; + String get chat_location => 'Ubicación'; @override String chat_sendMessageTo(String contactName) { @@ -1031,7 +1025,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return 'Mensaje demasiado largo (máximo $maxBytes bytes).'; + return 'Mensaje demasiado largo (máximo $maxBytes bytes).'; } @override @@ -1041,7 +1035,7 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_messageDeleted => 'Mensaje borrado'; @override - String get chat_retryingMessage => 'Reintentando…'; + String get chat_retryingMessage => 'Reintentando…'; @override String chat_retryCount(int current, int max) { @@ -1055,7 +1049,7 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_reply => 'Responder'; @override - String get chat_addReaction => 'Añadir Reacción'; + String get chat_addReaction => 'Añadir Reacción'; @override String get chat_me => 'Yo'; @@ -1091,13 +1085,13 @@ class AppLocalizationsEs extends AppLocalizations { String get gifPicker_failedSearch => 'No se encontraron GIFs'; @override - String get gifPicker_noInternet => 'No hay conexión a internet'; + String get gifPicker_noInternet => 'No hay conexión a internet'; @override - String get debugLog_appTitle => 'Registro de Depuración de la App'; + String get debugLog_appTitle => 'Registro de Depuración de la App'; @override - String get debugLog_bleTitle => 'Registro de Depuración BLE'; + String get debugLog_bleTitle => 'Registro de Depuración BLE'; @override String get debugLog_copyLog => 'Copiar registro'; @@ -1106,17 +1100,17 @@ class AppLocalizationsEs extends AppLocalizations { String get debugLog_clearLog => 'Borrar registro'; @override - String get debugLog_copied => 'Registro de depuración copiado'; + String get debugLog_copied => 'Registro de depuración copiado'; @override String get debugLog_bleCopied => 'Registro BLE copiado'; @override - String get debugLog_noEntries => 'Aún no hay registros de depuración.'; + String get debugLog_noEntries => 'Aún no hay registros de depuración.'; @override String get debugLog_enableInSettings => - 'Habilitar el registro de depuración de la aplicación en la configuración'; + 'Habilitar el registro de depuración de la aplicación en la configuración'; @override String get debugLog_frames => 'Marcos'; @@ -1125,7 +1119,7 @@ class AppLocalizationsEs extends AppLocalizations { String get debugLog_rawLogRx => 'Registro Crudo-RX'; @override - String get debugLog_noBleActivity => 'Aún no hay actividad BLE'; + String get debugLog_noBleActivity => 'Aún no hay actividad BLE'; @override String debugFrame_length(int count) { @@ -1175,7 +1169,7 @@ class AppLocalizationsEs extends AppLocalizations { String get debugFrame_hexDump => 'Mapeo Hexadecimal:'; @override - String get chat_pathManagement => 'Gestión de Rutas'; + String get chat_pathManagement => 'Gestión de Rutas'; @override String get chat_ShowAllPaths => 'Mostrar todos los caminos'; @@ -1187,14 +1181,14 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_autoUseSavedPath => 'Auto (usar la ruta guardada)'; @override - String get chat_forceFloodMode => 'Modo Inundación Forzado'; + String get chat_forceFloodMode => 'Modo Inundación Forzado'; @override String get chat_recentAckPaths => 'Rutas de ACK Recientes (tocar para usar):'; @override String get chat_pathHistoryFull => - 'El historial de rutas está completo. Eliminar entradas para añadir nuevas.'; + 'El historial de rutas está completo. Eliminar entradas para añadir nuevas.'; @override String get chat_hopSingular => 'salta'; @@ -1214,14 +1208,14 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_successes => 'Éxitos'; + String get chat_successes => 'Éxitos'; @override String get chat_removePath => 'Eliminar ruta'; @override String get chat_noPathHistoryYet => - 'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.'; + 'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.'; @override String get chat_pathActions => 'Acciones de Ruta:'; @@ -1238,11 +1232,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_clearPathSubtitle => - 'Forzar redescubrimiento en el próximo envío'; + 'Forzar redescubrimiento en el próximo envío'; @override String get chat_pathCleared => - 'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.'; + 'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.'; @override String get chat_floodModeSubtitle => @@ -1250,14 +1244,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_floodModeEnabled => - 'El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.'; + 'El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.'; @override String get chat_fullPath => 'Ruta completa'; @override String get chat_pathDetailsNotAvailable => - 'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.'; + 'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1272,13 +1266,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_pathSavedLocally => - 'Guardado localmente. Conéctate para sincronizar.'; + 'Guardado localmente. Conéctate para sincronizar.'; @override String get chat_pathDeviceConfirmed => 'Dispositivo confirmado.'; @override - String get chat_pathDeviceNotConfirmed => 'Dispositivo aún no confirmado.'; + String get chat_pathDeviceNotConfirmed => 'Dispositivo aún no confirmado.'; @override String get chat_type => 'Escribe'; @@ -1287,13 +1281,13 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_path => 'Ruta'; @override - String get chat_publicKey => 'Clave Pública'; + String get chat_publicKey => 'Clave Pública'; @override String get chat_compressOutgoingMessages => 'Comprimir mensajes salientes'; @override - String get chat_floodForced => 'Inundación (forzada)'; + String get chat_floodForced => 'Inundación (forzada)'; @override String get chat_directForced => 'Directo (forzado)'; @@ -1304,13 +1298,13 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_floodAuto => 'Inundación (automática)'; + String get chat_floodAuto => 'Inundación (automática)'; @override String get chat_direct => 'Guardar'; @override - String get chat_poiShared => 'Punto de Interés Compartido'; + String get chat_poiShared => 'Punto de Interés Compartido'; @override String chat_unread(int count) { @@ -1318,11 +1312,11 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_openLink => '¿Abrir enlace?'; + String get chat_openLink => '¿Abrir enlace?'; @override String get chat_openLinkConfirmation => - '¿Quiere abrir este enlace en su navegador?'; + '¿Quiere abrir este enlace en su navegador?'; @override String get chat_open => 'Abrir'; @@ -1333,19 +1327,19 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_invalidLink => 'Formato de enlace no válido'; + String get chat_invalidLink => 'Formato de enlace no válido'; @override String get map_title => 'Mapa de Nodos'; @override - String get map_lineOfSight => 'Línea de visión'; + String get map_lineOfSight => 'Línea de visión'; @override - String get map_losScreenTitle => 'Línea de visión'; + String get map_losScreenTitle => 'Línea de visión'; @override - String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación'; + String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación'; @override String get map_nodesNeedGps => @@ -1368,7 +1362,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_repeater => 'Repetidor'; @override - String get map_room => 'Habitación'; + String get map_room => 'Habitación'; @override String get map_sensor => 'Sensor'; @@ -1380,14 +1374,14 @@ class AppLocalizationsEs extends AppLocalizations { String get map_pinPrivate => 'Bloqueo (Privado)'; @override - String get map_pinPublic => 'Clave (Pública)'; + String get map_pinPublic => 'Clave (Pública)'; @override - String get map_lastSeen => 'Última vez que se vio'; + String get map_lastSeen => 'Última vez que se vio'; @override String get map_disconnectConfirm => - '¿Está seguro de que desea desconectarse de este dispositivo?'; + '¿Está seguro de que desea desconectarse de este dispositivo?'; @override String get map_from => 'De'; @@ -1399,7 +1393,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_flags => 'Banderas'; @override - String get map_shareMarkerHere => 'Compartir marcador aquí'; + String get map_shareMarkerHere => 'Compartir marcador aquí'; @override String get map_pinLabel => 'Etiqueta de marcador'; @@ -1408,7 +1402,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_label => 'Etiqueta'; @override - String get map_pointOfInterest => 'Punto de interés'; + String get map_pointOfInterest => 'Punto de interés'; @override String get map_sendToContact => 'Enviar a contacto'; @@ -1420,16 +1414,16 @@ class AppLocalizationsEs extends AppLocalizations { String get map_noChannelsAvailable => 'No hay canales disponibles'; @override - String get map_publicLocationShare => 'Compartir ubicación pública'; + String get map_publicLocationShare => 'Compartir ubicación pública'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Estás a punto de compartir una ubicación en $channelLabel. Este canal es público y cualquiera con la PSK puede verlo.'; + return 'Estás a punto de compartir una ubicación en $channelLabel. Este canal es público y cualquiera con la PSK puede verlo.'; } @override String get map_connectToShareMarkers => - 'Conéctate a un dispositivo para compartir marcadores'; + 'Conéctate a un dispositivo para compartir marcadores'; @override String get map_filterNodes => 'Filtrar Nodos'; @@ -1453,7 +1447,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtrar por prefijo clave'; @override - String get map_publicKeyPrefix => 'Prefijo de clave pública'; + String get map_publicKeyPrefix => 'Prefijo de clave pública'; @override String get map_markers => 'Marcadores'; @@ -1462,13 +1456,13 @@ class AppLocalizationsEs extends AppLocalizations { String get map_showSharedMarkers => 'Mostrar marcadores compartidos'; @override - String get map_lastSeenTime => 'Última vez que se vio'; + String get map_lastSeenTime => 'Última vez que se vio'; @override String get map_sharedPin => 'Pin compartido'; @override - String get map_joinRoom => 'Únete a la sala'; + String get map_joinRoom => 'Únete a la sala'; @override String get map_manageRepeater => 'Gestionar Repetidor'; @@ -1480,28 +1474,28 @@ class AppLocalizationsEs extends AppLocalizations { String get map_runTrace => 'Ejecutar Rastreo de Ruta'; @override - String get map_removeLast => 'Eliminar último'; + String get map_removeLast => 'Eliminar último'; @override String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.'; @override - String get mapCache_title => 'Caché de Mapa Offline'; + String get mapCache_title => 'Caché de Mapa Offline'; @override String get mapCache_selectAreaFirst => - 'Seleccionar un área para cachear primero'; + 'Seleccionar un área para cachear primero'; @override String get mapCache_noTilesToDownload => - 'No hay azulejos para descargar para este área.'; + 'No hay azulejos para descargar para este área.'; @override String get mapCache_downloadTilesTitle => 'Descargar ficheros'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Descargar $count ficheros para usar sin conexión?'; + return 'Descargar $count ficheros para usar sin conexión?'; } @override @@ -1518,21 +1512,21 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get mapCache_clearOfflineCacheTitle => 'Borrar caché offline'; + String get mapCache_clearOfflineCacheTitle => 'Borrar caché offline'; @override String get mapCache_clearOfflineCachePrompt => - 'Eliminar todas las baldosas en caché del mapa?'; + 'Eliminar todas las baldosas en caché del mapa?'; @override String get mapCache_offlineCacheCleared => - 'Almacén en caché sin conexión eliminado'; + 'Almacén en caché sin conexión eliminado'; @override - String get mapCache_noAreaSelected => 'No se ha seleccionado ningún área'; + String get mapCache_noAreaSelected => 'No se ha seleccionado ningún área'; @override - String get mapCache_cacheArea => 'Área de Caché'; + String get mapCache_cacheArea => 'Área de Caché'; @override String get mapCache_useCurrentView => 'Usar Vista Actual'; @@ -1554,7 +1548,7 @@ class AppLocalizationsEs extends AppLocalizations { String get mapCache_downloadTilesButton => 'Descargar Mosaicos'; @override - String get mapCache_clearCacheButton => 'Borrar Caché'; + String get mapCache_clearCacheButton => 'Borrar Caché'; @override String mapCache_failedDownloads(int count) { @@ -1586,7 +1580,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String time_daysAgo(int days) { - return '$days días hace'; + return '$days días hace'; } @override @@ -1596,10 +1590,10 @@ class AppLocalizationsEs extends AppLocalizations { String get time_hours => 'horas'; @override - String get time_day => 'día'; + String get time_day => 'día'; @override - String get time_days => 'días'; + String get time_days => 'días'; @override String get time_week => 'semana'; @@ -1624,34 +1618,34 @@ class AppLocalizationsEs extends AppLocalizations { @override String get dialog_disconnectConfirm => - '¿Está seguro de que desea desconectarse de este dispositivo?'; + '¿Está seguro de que desea desconectarse de este dispositivo?'; @override - String get login_repeaterLogin => 'Iniciar sesión en el Repetidor'; + String get login_repeaterLogin => 'Iniciar sesión en el Repetidor'; @override String get login_roomLogin => 'Inicio de Sala'; @override - String get login_password => 'Contraseña'; + String get login_password => 'Contraseña'; @override - String get login_enterPassword => 'Introducir contraseña'; + String get login_enterPassword => 'Introducir contraseña'; @override - String get login_savePassword => 'Guardar contraseña'; + String get login_savePassword => 'Guardar contraseña'; @override String get login_savePasswordSubtitle => - 'La contraseña se almacenará de forma segura en este dispositivo.'; + 'La contraseña se almacenará de forma segura en este dispositivo.'; @override String get login_repeaterDescription => - 'Ingrese la contraseña del repetidor para acceder a la configuración y el estado.'; + 'Ingrese la contraseña del repetidor para acceder a la configuración y el estado.'; @override String get login_roomDescription => - 'Ingrese la contraseña de la sala para acceder a la configuración y el estado.'; + 'Ingrese la contraseña de la sala para acceder a la configuración y el estado.'; @override String get login_routing => 'Enrutamiento'; @@ -1663,13 +1657,13 @@ class AppLocalizationsEs extends AppLocalizations { String get login_autoUseSavedPath => 'Auto (usar la ruta guardada)'; @override - String get login_forceFloodMode => 'Activar Modo Inundación Forzada'; + String get login_forceFloodMode => 'Activar Modo Inundación Forzada'; @override String get login_managePaths => 'Gestionar Rutas'; @override - String get login_login => 'Iniciar sesión'; + String get login_login => 'Iniciar sesión'; @override String login_attempt(int current, int max) { @@ -1683,7 +1677,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get login_failedMessage => - 'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.'; + 'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.'; @override String get common_reload => 'Recargar'; @@ -1719,14 +1713,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get path_hexPrefixExample => - 'Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).'; + 'Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).'; @override String get path_labelHexPrefixes => 'Prefijos hexadecimales'; @override String get path_helperMaxHops => - 'Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).'; + 'Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).'; @override String get path_selectFromContacts => 'O seleccionar de contactos:'; @@ -1741,38 +1735,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String path_invalidHexPrefixes(String prefixes) { - return 'Prefijos hexadecimales inválidos: $prefixes'; + return 'Prefijos hexadecimales inválidos: $prefixes'; } @override String get path_tooLong => - 'La ruta es demasiado larga. Se permiten un máximo de 64 saltos.'; + 'La ruta es demasiado larga. Se permiten un máximo de 64 saltos.'; @override String get path_setPath => 'Establecer Ruta'; @override - String get repeater_management => 'Gestión de Repetidores'; + String get repeater_management => 'Gestión de Repetidores'; @override - String get room_management => 'Administración del Servidor de Habitación'; + String get room_management => 'Administración del Servidor de Habitación'; @override - String get repeater_managementTools => 'Herramientas de Gestión'; + String get repeater_managementTools => 'Herramientas de Gestión'; @override String get repeater_status => 'Estado'; @override String get repeater_statusSubtitle => - 'Ver el estado, las estadísticas y los vecinos del repetidor'; + 'Ver el estado, las estadísticas y los vecinos del repetidor'; @override String get repeater_telemetry => 'Telemetry'; @override String get repeater_telemetrySubtitle => - 'Ver la telemetría de los sensores y las estadísticas del sistema'; + 'Ver la telemetría de los sensores y las estadísticas del sistema'; @override String get repeater_cli => 'CLI'; @@ -1787,10 +1781,11 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.'; @override - String get repeater_settings => 'Configuración'; + String get repeater_settings => 'Configuración'; @override - String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor'; + String get repeater_settingsSubtitle => + 'Configurar parámetros del repetidor'; @override String get repeater_statusTitle => 'Estado del Repetidor'; @@ -1802,16 +1797,16 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_autoUseSavedPath => 'Auto (usar la ruta guardada)'; @override - String get repeater_forceFloodMode => 'Modo Inundación Forzado'; + String get repeater_forceFloodMode => 'Modo Inundación Forzado'; @override - String get repeater_pathManagement => 'Gestión de rutas'; + String get repeater_pathManagement => 'Gestión de rutas'; @override String get repeater_refresh => 'Actualizar'; @override - String get repeater_statusRequestTimeout => 'Solicitud de estado caducó.'; + String get repeater_statusRequestTimeout => 'Solicitud de estado caducó.'; @override String repeater_errorLoadingStatus(String error) { @@ -1819,13 +1814,13 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get repeater_systemInformation => 'Información del sistema'; + String get repeater_systemInformation => 'Información del sistema'; @override - String get repeater_battery => 'Batería'; + String get repeater_battery => 'Batería'; @override - String get repeater_clockAtLogin => 'Reloj (al inicio de sesión)'; + String get repeater_clockAtLogin => 'Reloj (al inicio de sesión)'; @override String get repeater_uptime => 'Tiempo de actividad'; @@ -1834,16 +1829,16 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_queueLength => 'Longitud de la cola'; @override - String get repeater_debugFlags => 'Marcadores de Depuración'; + String get repeater_debugFlags => 'Marcadores de Depuración'; @override - String get repeater_radioStatistics => 'Estadísticas de Radio'; + String get repeater_radioStatistics => 'Estadísticas de Radio'; @override - String get repeater_lastRssi => 'Último RSSI'; + String get repeater_lastRssi => 'Último RSSI'; @override - String get repeater_lastSnr => 'Último SNR'; + String get repeater_lastSnr => 'Último SNR'; @override String get repeater_noiseFloor => 'Nivel de Ruido'; @@ -1855,7 +1850,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Estadísticas del Paquete'; + String get repeater_packetStatistics => 'Estadísticas del Paquete'; @override String get repeater_sent => 'Enviado'; @@ -1873,22 +1868,22 @@ class AppLocalizationsEs extends AppLocalizations { int minutes, int seconds, ) { - return '$days días ${hours}h ${minutes}m ${seconds}s'; + return '$days días ${hours}h ${minutes}m ${seconds}s'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundación: $flood, Directo: $direct'; + return 'Total: $total, Inundación: $flood, Directo: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundación: $flood, Directo: $direct'; + return 'Total: $total, Inundación: $flood, Directo: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Inundación: $flood, Directo: $direct'; + return 'Inundación: $flood, Directo: $direct'; } @override @@ -1897,10 +1892,10 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Configuración del Repetidor'; + String get repeater_settingsTitle => 'Configuración del Repetidor'; @override - String get repeater_basicSettings => 'Configuración Básica'; + String get repeater_basicSettings => 'Configuración Básica'; @override String get repeater_repeaterName => 'Nombre del Repetidor'; @@ -1910,20 +1905,20 @@ class AppLocalizationsEs extends AppLocalizations { 'Mostrar nombre para este repetidor'; @override - String get repeater_adminPassword => 'Contraseña de Administrador'; + String get repeater_adminPassword => 'Contraseña de Administrador'; @override - String get repeater_adminPasswordHelper => 'Contraseña de acceso completo'; + String get repeater_adminPasswordHelper => 'Contraseña de acceso completo'; @override - String get repeater_guestPassword => 'Contraseña de invitado'; + String get repeater_guestPassword => 'Contraseña de invitado'; @override String get repeater_guestPasswordHelper => - 'Acceso de solo lectura con contraseña'; + 'Acceso de solo lectura con contraseña'; @override - String get repeater_radioSettings => 'Configuración de Radio'; + String get repeater_radioSettings => 'Configuración de Radio'; @override String get repeater_frequencyMhz => 'Frecuencia (MHz)'; @@ -1941,13 +1936,13 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_bandwidth => 'Ancho de banda'; @override - String get repeater_spreadingFactor => 'Factor de propagación'; + String get repeater_spreadingFactor => 'Factor de propagación'; @override - String get repeater_codingRate => 'Tasa de Programación'; + String get repeater_codingRate => 'Tasa de Programación'; @override - String get repeater_locationSettings => 'Configuración de Ubicación'; + String get repeater_locationSettings => 'Configuración de Ubicación'; @override String get repeater_latitude => 'Latitud'; @@ -1964,7 +1959,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Grados decimales (por ejemplo, -122.4194)'; @override - String get repeater_features => 'Características'; + String get repeater_features => 'Características'; @override String get repeater_packetForwarding => 'Enrutamiento de Paquetes'; @@ -1985,10 +1980,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_privacyModeSubtitle => - 'Ocultar nombre/ubicación en anuncios'; + 'Ocultar nombre/ubicación en anuncios'; @override - String get repeater_advertisementSettings => 'Configuración de Anuncios'; + String get repeater_advertisementSettings => 'Configuración de Anuncios'; @override String get repeater_localAdvertInterval => 'Intervalo de Anuncio Local'; @@ -2000,7 +1995,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervalo de Anuncio de Inundación'; + 'Intervalo de Anuncio de Inundación'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2022,18 +2017,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_rebootRepeaterConfirm => - '¿Está seguro de que desea reiniciar este repetidor?'; + '¿Está seguro de que desea reiniciar este repetidor?'; @override String get repeater_regenerateIdentityKey => 'Regenerar Clave de Identidad'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Generar nueva pareja de clave pública/privada'; + 'Generar nueva pareja de clave pública/privada'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Esto generará una nueva identidad para el repetidor. Continuar?'; + 'Esto generará una nueva identidad para el repetidor. Continuar?'; @override String get repeater_eraseFileSystem => 'Borrar Sistema de Archivos'; @@ -2044,11 +2039,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!'; + 'ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!'; @override String get repeater_eraseSerialOnly => - 'Borrar solo está disponible a través de la consola serial.'; + 'Borrar solo está disponible a través de la consola serial.'; @override String repeater_commandSent(String command) { @@ -2068,21 +2063,22 @@ class AppLocalizationsEs extends AppLocalizations { @override String repeater_errorSavingSettings(String error) { - return 'Error al guardar la configuración: $error'; + return 'Error al guardar la configuración: $error'; } @override - String get repeater_refreshBasicSettings => 'Actualizar Configuración Básica'; + String get repeater_refreshBasicSettings => + 'Actualizar Configuración Básica'; @override String get repeater_refreshRadioSettings => 'Actualizar Ajustes de Radio'; @override - String get repeater_refreshTxPower => 'Actualizar TX de energía'; + String get repeater_refreshTxPower => 'Actualizar TX de energía'; @override String get repeater_refreshLocationSettings => - 'Actualizar Configuración de Ubicación'; + 'Actualizar Configuración de Ubicación'; @override String get repeater_refreshPacketForwarding => @@ -2096,7 +2092,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_refreshAdvertisementSettings => - 'Actualizar Configuración de Anuncios'; + 'Actualizar Configuración de Anuncios'; @override String repeater_refreshed(String label) { @@ -2112,7 +2108,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliTitle => 'Repetidor CLI'; @override - String get repeater_debugNextCommand => 'Siguiente Comando de Depuración'; + String get repeater_debugNextCommand => 'Siguiente Comando de Depuración'; @override String get repeater_commandHelp => 'Ayuda'; @@ -2121,11 +2117,11 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_clearHistory => 'Borrar historial'; @override - String get repeater_noCommandsSent => 'Aún no se han enviado comandos.'; + String get repeater_noCommandsSent => 'Aún no se han enviado comandos.'; @override String get repeater_typeCommandOrUseQuick => - 'Escriba un comando a continuación o use comandos rápidos'; + 'Escriba un comando a continuación o use comandos rápidos'; @override String get repeater_enterCommandHint => 'Escribir comando...'; @@ -2160,7 +2156,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliQuickNeighbors => 'Vecinos'; @override - String get repeater_cliQuickVersion => 'Versión'; + String get repeater_cliQuickVersion => 'Versión'; @override String get repeater_cliQuickAdvertise => 'Anunciar'; @@ -2169,7 +2165,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliQuickClock => 'Reloj'; @override - String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; + String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; @override String get repeater_cliHelpReboot => @@ -2177,26 +2173,26 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpClock => - 'Muestra la hora actual según el reloj del dispositivo.'; + 'Muestra la hora actual según el reloj del dispositivo.'; @override String get repeater_cliHelpPassword => - 'Establece una nueva contraseña de administrador para el dispositivo.'; + 'Establece una nueva contraseña de administrador para el dispositivo.'; @override String get repeater_cliHelpVersion => - 'Muestra la versión del dispositivo y la fecha de compilación del firmware.'; + 'Muestra la versión del dispositivo y la fecha de compilación del firmware.'; @override String get repeater_cliHelpClearStats => - 'Reinicia varios contadores de estadísticas a cero.'; + 'Reinicia varios contadores de estadísticas a cero.'; @override String get repeater_cliHelpSetAf => 'Establece el factor de tiempo de aire.'; @override String get repeater_cliHelpSetTx => - 'Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).'; + 'Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).'; @override String get repeater_cliHelpSetRepeat => @@ -2204,23 +2200,23 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetAllowReadOnly => - '(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).'; + '(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).'; @override String get repeater_cliHelpSetFloodMax => - 'Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).'; + 'Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).'; @override String get repeater_cliHelpSetIntThresh => - 'Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.'; + 'Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.'; + 'Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.'; @override String get repeater_cliHelpSetMultiAcks => - 'Habilita o deshabilita la función de \'ACKs dobles\'.'; + 'Habilita o deshabilita la función de \'ACKs dobles\'.'; @override String get repeater_cliHelpSetAdvertInterval => @@ -2232,7 +2228,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetGuestPassword => - 'Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")'; + 'Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")'; @override String get repeater_cliHelpSetName => 'Establece el nombre del anuncio.'; @@ -2247,15 +2243,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetRadio => - 'Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.'; + 'Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.'; @override String get repeater_cliHelpSetRxDelay => - 'Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.'; + 'Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.'; @override String get repeater_cliHelpSetTxDelay => - 'Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).'; + 'Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).'; @override String get repeater_cliHelpSetDirectTxDelay => @@ -2271,7 +2267,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetBridgeSource => - 'Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.'; + 'Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.'; @override String get repeater_cliHelpSetBridgeBaud => @@ -2283,15 +2279,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetAdcMultiplier => - 'Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).'; + 'Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).'; @override String get repeater_cliHelpTempRadio => - 'Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).'; + 'Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).'; @override String get repeater_cliHelpSetPerm => - 'Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).'; + 'Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).'; @override String get repeater_cliHelpGetBridgeType => @@ -2311,7 +2307,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4'; + 'Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4'; @override String get repeater_cliHelpNeighborRemove => @@ -2319,38 +2315,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpRegion => - '(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.'; + '(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.'; @override String get repeater_cliHelpRegionLoad => - 'NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.'; + 'NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.'; @override String get repeater_cliHelpRegionGet => - 'Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) \'F\'\"'; + 'Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Agrega o actualiza una definición de región con el nombre dado.'; + 'Agrega o actualiza una definición de región con el nombre dado.'; @override String get repeater_cliHelpRegionRemove => - 'Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)'; + 'Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)'; @override String get repeater_cliHelpRegionAllowf => - 'Establece el permiso de \'F\'lujo para la región dada. (\'\' para el ámbito global/legado)'; + 'Establece el permiso de \'F\'lujo para la región dada. (\'\' para el ámbito global/legado)'; @override String get repeater_cliHelpRegionDenyf => - 'Elimina el permiso de \'F\'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)'; + 'Elimina el permiso de \'F\'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)'; @override String get repeater_cliHelpRegionHome => - 'Responde con la región \'home\' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).'; + 'Responde con la región \'home\' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).'; @override - String get repeater_cliHelpRegionHomeSet => 'Establece la región \'hogar\'.'; + String get repeater_cliHelpRegionHomeSet => 'Establece la región \'hogar\'.'; @override String get repeater_cliHelpRegionSave => @@ -2358,7 +2354,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.'; + 'Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.'; @override String get repeater_cliHelpGpsOnOff => 'Activa o desactiva el modo GPS.'; @@ -2369,28 +2365,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpGpsSetLoc => - 'Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.'; + 'Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.'; @override String get repeater_cliHelpGpsAdvert => - 'Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias'; + 'Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias'; @override String get repeater_cliHelpGpsAdvertSet => - 'Configura la configuración de la publicidad de la ubicación.'; + 'Configura la configuración de la publicidad de la ubicación.'; @override String get repeater_commandsListTitle => 'Lista de comandos'; @override String get repeater_commandsListNote => - 'NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".'; + 'NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".'; @override String get repeater_general => 'General'; @override - String get repeater_settingsCategory => 'Configuración'; + String get repeater_settingsCategory => 'Configuración'; @override String get repeater_bridge => 'Puente'; @@ -2403,32 +2399,33 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_regionManagementRepeaterOnly => - 'Gestión de Regiones (solo Repetidor)'; + 'Gestión de Regiones (solo Repetidor)'; @override String get repeater_regionNote => - 'Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.'; + 'Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.'; @override - String get repeater_gpsManagement => 'Gestión de GPS'; + String get repeater_gpsManagement => 'Gestión de GPS'; @override String get repeater_gpsNote => - 'Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.'; + 'Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.'; @override - String get telemetry_receivedData => 'Datos de Telemetría Recibidos'; + String get telemetry_receivedData => 'Datos de Telemetría Recibidos'; @override - String get telemetry_requestTimeout => 'Solicitud de telemetría ha expirado.'; + String get telemetry_requestTimeout => + 'Solicitud de telemetría ha expirado.'; @override String telemetry_errorLoading(String error) { - return 'Error al cargar la telemetría: $error'; + return 'Error al cargar la telemetría: $error'; } @override - String get telemetry_noData => 'No hay datos de telemetría disponibles.'; + String get telemetry_noData => 'No hay datos de telemetría disponibles.'; @override String telemetry_channelTitle(int channel) { @@ -2436,7 +2433,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get telemetry_batteryLabel => 'Batería'; + String get telemetry_batteryLabel => 'Batería'; @override String get telemetry_voltageLabel => 'Voltaje'; @@ -2467,7 +2464,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2490,12 +2487,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String neighbors_unknownContact(String pubkey) { - return 'Clave pública desconocida $pubkey'; + return 'Clave pública desconocida $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Escuchado: $time hace atrás'; + return 'Escuchado: $time hace atrás'; } @override @@ -2512,7 +2509,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channelPath_noHopDetails => - 'Los detalles del paquete no están disponibles.'; + 'Los detalles del paquete no están disponibles.'; @override String get channelPath_messageDetails => 'Detalles del mensaje'; @@ -2536,11 +2533,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Ruta observada $index • $hops'; + return 'Ruta observada $index • $hops'; } @override - String get channelPath_noLocationData => 'No datos de ubicación'; + String get channelPath_noLocationData => 'No datos de ubicación'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2556,7 +2553,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channelPath_unknownPath => 'Desconocido'; @override - String get channelPath_floodPath => 'Inundación'; + String get channelPath_floodPath => 'Inundación'; @override String get channelPath_directPath => 'Guardar'; @@ -2591,7 +2588,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2609,31 +2606,31 @@ class AppLocalizationsEs extends AppLocalizations { @override String get community_createDesc => - 'Crear una nueva comunidad y compartir a través de código QR.'; + 'Crear una nueva comunidad y compartir a través de código QR.'; @override - String get community_join => 'Únete'; + String get community_join => 'Únete'; @override - String get community_joinTitle => 'Únete a la comunidad'; + String get community_joinTitle => 'Únete a la comunidad'; @override String community_joinConfirmation(String name) { - return '¿Quieres unirte a la comunidad \"$name\"?'; + return '¿Quieres unirte a la comunidad \"$name\"?'; } @override - String get community_scanQr => 'Escanear Código QR de la Comunidad'; + String get community_scanQr => 'Escanear Código QR de la Comunidad'; @override String get community_scanInstructions => - 'Apunte la cámara a un código QR de la comunidad'; + 'Apunte la cámara a un código QR de la comunidad'; @override - String get community_showQr => 'Mostrar Código QR'; + String get community_showQr => 'Mostrar Código QR'; @override - String get community_publicChannel => 'Comunidad Pública'; + String get community_publicChannel => 'Comunidad Pública'; @override String get community_hashtagChannel => 'Hashtag de la Comunidad'; @@ -2651,7 +2648,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_joined(String name) { - return 'Se unió a la comunidad \"$name\"'; + return 'Se unió a la comunidad \"$name\"'; } @override @@ -2659,7 +2656,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Escanear este código QR para unirte a $name'; + return 'Escanear este código QR para unirte a $name'; } @override @@ -2667,7 +2664,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad'; @override - String get community_invalidQrCode => 'Código QR de comunidad no válido'; + String get community_invalidQrCode => 'Código QR de comunidad no válido'; @override String get community_alreadyMember => 'Ya eres Miembro'; @@ -2679,18 +2676,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get community_addPublicChannel => - 'Añadir Canal Público de la Comunidad'; + 'Añadir Canal Público de la Comunidad'; @override String get community_addPublicChannelHint => - 'Añade automáticamente el canal público para esta comunidad.'; + 'Añade automáticamente el canal público para esta comunidad.'; @override - String get community_noCommunities => 'Aún no se han unido comunidades.'; + String get community_noCommunities => 'Aún no se han unido comunidades.'; @override String get community_scanOrCreate => - 'Escanear un código QR o crear una comunidad para comenzar'; + 'Escanear un código QR o crear una comunidad para comenzar'; @override String get community_manageCommunities => 'Gestionar Comunidades'; @@ -2700,12 +2697,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_deleteConfirm(String name) { - return '¿Salir de \"$name\"?'; + return '¿Salir de \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Esto también eliminará $count canal(es) y sus mensajes.'; + return 'Esto también eliminará $count canal(es) y sus mensajes.'; } @override @@ -2714,11 +2711,11 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerar Contraseña Secreta'; + String get community_regenerateSecret => 'Regenerar Contraseña Secreta'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.'; + return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.'; } @override @@ -2726,11 +2723,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Código secreto regenerado para \"$name\"'; + return 'Código secreto regenerado para \"$name\"'; } @override - String get community_updateSecret => 'Actualizar Contraseña'; + String get community_updateSecret => 'Actualizar Contraseña'; @override String community_secretUpdated(String name) { @@ -2739,15 +2736,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"'; + return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"'; } @override - String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad'; + String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad'; @override String get community_addHashtagChannelDesc => - 'Añadir un canal con hashtag para esta comunidad'; + 'Añadir un canal con hashtag para esta comunidad'; @override String get community_selectCommunity => 'Seleccionar Comunidad'; @@ -2757,7 +2754,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get community_regularHashtagDesc => - 'Hashtag público (cualquiera puede unirse)'; + 'Hashtag público (cualquiera puede unirse)'; @override String get community_communityHashtag => 'Hashtag de la Comunidad'; @@ -2778,7 +2775,7 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_sortBy => 'Ordenar por'; @override - String get listFilter_latestMessages => 'Últimos mensajes'; + String get listFilter_latestMessages => 'Últimos mensajes'; @override String get listFilter_heardRecently => 'Escuchado recientemente'; @@ -2796,7 +2793,7 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_favorites => 'Favoritos'; @override - String get listFilter_addToFavorites => 'Añadir a favoritos'; + String get listFilter_addToFavorites => 'Añadir a favoritos'; @override String get listFilter_removeFromFavorites => 'Eliminar de las favoritas'; @@ -2817,20 +2814,21 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_newGroup => 'Nuevo grupo'; @override - String get pathTrace_you => 'Tú'; + String get pathTrace_you => 'Tú'; @override - String get pathTrace_failed => 'El trazado de ruta falló.'; + String get pathTrace_failed => 'El trazado de ruta falló.'; @override - String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.'; + String get pathTrace_notAvailable => + 'El trazado de ruta no está disponible.'; @override String get pathTrace_refreshTooltip => 'Actualizar Path Trace'; @override String get pathTrace_someHopsNoLocation => - 'Uno o más de los lúpulos carecen de una ubicación'; + 'Uno o más de los lúpulos carecen de una ubicación'; @override String get pathTrace_clearTooltip => 'Borrar ruta'; @@ -2841,7 +2839,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String losRunFailed(String error) { - return 'Error en la comprobación de la línea de visión: $error'; + return 'Error en la comprobación de la línea de visión: $error'; } @override @@ -2849,17 +2847,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get losRunToViewElevationProfile => - 'Ejecute LOS para ver el perfil de elevación'; + 'Ejecute LOS para ver el perfil de elevación'; @override - String get losMenuTitle => 'Menú LOS'; + String get losMenuTitle => 'Menú LOS'; @override String get losMenuSubtitle => 'Toque nodos o mantenga presionado el mapa para puntos personalizados'; @override - String get losShowDisplayNodes => 'Mostrar nodos de visualización'; + String get losShowDisplayNodes => 'Mostrar nodos de visualización'; @override String get losCustomPoints => 'Puntos personalizados'; @@ -2889,7 +2887,7 @@ class AppLocalizationsEs extends AppLocalizations { String get losRun => 'Ejecutar LOS'; @override - String get losNoElevationData => 'Sin datos de elevación'; + String get losNoElevationData => 'Sin datos de elevación'; @override String losProfileClear( @@ -2898,7 +2896,7 @@ class AppLocalizationsEs extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit'; + return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit'; } @override @@ -2924,11 +2922,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Datos de elevación no disponibles para una o más muestras.'; + 'Datos de elevación no disponibles para una o más muestras.'; @override String get losErrorInvalidInput => - 'Datos de puntos/elevación no válidos para el cálculo de LOS.'; + 'Datos de puntos/elevación no válidos para el cálculo de LOS.'; @override String get losRenameCustomPoint => @@ -2945,13 +2943,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get losElevationAttribution => - 'Datos de elevación: Open-Meteo (CC BY 4.0)'; + 'Datos de elevación: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; + String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; @override - String get losLegendLosBeam => 'Línea de visión'; + String get losLegendLosBeam => 'Línea de visión'; @override String get losLegendTerrain => 'Terreno'; @@ -2960,10 +2958,11 @@ class AppLocalizationsEs extends AppLocalizations { String get losFrequencyLabel => 'Frecuencia'; @override - String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; + String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; @override - String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico'; + String get losFrequencyDialogTitle => + 'Cálculo del horizonte radioeléctrico'; @override String losFrequencyDialogDescription( @@ -2972,7 +2971,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; + return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; } @override @@ -2989,7 +2988,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_roomPathTrace => - 'Rastreo de ruta al servidor de la habitación'; + 'Rastreo de ruta al servidor de la habitación'; @override String get contacts_roomPing => 'Pingar servidor de sala'; @@ -3003,23 +3002,23 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'El portapapeles está vacío.'; + String get contacts_clipboardEmpty => 'El portapapeles está vacío.'; @override - String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos'; + String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos'; @override String get contacts_contactImported => 'El contacto ha sido importado.'; @override String get contacts_contactImportFailed => - 'Contacto no se importó correctamente.'; + 'Contacto no se importó correctamente.'; @override String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop'; @override - String get contacts_floodAdvert => 'Anuncio de inundación'; + String get contacts_floodAdvert => 'Anuncio de inundación'; @override String get contacts_copyAdvertToClipboard => 'Copiar anuncio al portapapeles'; @@ -3035,7 +3034,8 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio'; @override - String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.'; + String get contacts_zeroHopContactAdvertSent => + 'Envió contacto por anuncio.'; @override String get contacts_zeroHopContactAdvertFailed => @@ -3098,24 +3098,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportRepeatersSubtitle => - 'Exporta repetidores o roomserver con una ubicación a un archivo GPX.'; + 'Exporta repetidores o roomserver con una ubicación a un archivo GPX.'; @override - String get settings_gpxExportContacts => 'Exportar compañeros a GPX'; + String get settings_gpxExportContacts => 'Exportar compañeros a GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exporta compañeros con una ubicación a archivo GPX.'; + 'Exporta compañeros con una ubicación a archivo GPX.'; @override String get settings_gpxExportAll => 'Exportar todos los contactos a GPX'; @override String get settings_gpxExportAllSubtitle => - 'Exporta todos los contactos con una ubicación a un archivo GPX.'; + 'Exporta todos los contactos con una ubicación a un archivo GPX.'; @override - String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.'; + String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.'; @override String get settings_gpxExportNoContacts => 'No hay contactos para exportar.'; @@ -3132,7 +3132,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Ubicaciones del servidor de repetidor y sala'; @override - String get settings_gpxExportChat => 'Ubicaciones de compañero'; + String get settings_gpxExportChat => 'Ubicaciones de compañero'; @override String get settings_gpxExportAllContacts => @@ -3144,11 +3144,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportShareSubject => - 'meshcore-open exportación de datos de mapa GPX'; + 'meshcore-open exportación de datos de mapa GPX'; @override String get snrIndicator_nearByRepeaters => 'Repetidores cercanos'; @override - String get snrIndicator_lastSeen => 'Visto por última vez'; + String get snrIndicator_lastSeen => 'Visto por última vez'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index aaca233..945a26b 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -48,19 +48,19 @@ class AppLocalizationsFr extends AppLocalizations { String get common_add => 'Ajouter'; @override - String get common_settings => 'Paramètres'; + String get common_settings => 'Paramètres'; @override - String get common_disconnect => 'Déconnecter'; + String get common_disconnect => 'Déconnecter'; @override - String get common_connected => 'Connecté'; + String get common_connected => 'Connecté'; @override - String get common_disconnected => 'Déconnecté'; + String get common_disconnected => 'Déconnecté'; @override - String get common_create => 'Créer'; + String get common_create => 'Créer'; @override String get common_continue => 'Continuer'; @@ -72,7 +72,7 @@ class AppLocalizationsFr extends AppLocalizations { String get common_copy => 'Copier'; @override - String get common_retry => 'Réessayer'; + String get common_retry => 'Réessayer'; @override String get common_hide => 'Masquer'; @@ -84,16 +84,16 @@ class AppLocalizationsFr extends AppLocalizations { String get common_enable => 'Activer'; @override - String get common_disable => 'Désactiver'; + String get common_disable => 'Désactiver'; @override - String get common_reboot => 'Redémarrer'; + String get common_reboot => 'Redémarrer'; @override String get common_loading => 'Chargement...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Choisissez votre méthode de connexion.'; - - @override - String get connectionChoiceSubtitle => - 'Choisissez la méthode de connexion que vous préférez pour votre appareil MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -126,34 +119,34 @@ class AppLocalizationsFr extends AppLocalizations { @override String get usbScreenSubtitle => - 'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.'; + 'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nÅ“ud MeshCore.'; @override - String get usbScreenStatus => 'Sélectionnez un périphérique USB'; + String get usbScreenStatus => 'Sélectionnez un périphérique USB'; @override String get usbScreenNote => - 'La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.'; + 'La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.'; @override String get usbScreenEmptyState => - 'Aucun périphérique USB n\'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.'; + 'Aucun périphérique USB n\'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.'; @override - String get scanner_scanning => 'Recherche de périphériques...'; + String get scanner_scanning => 'Recherche de périphériques...'; @override String get scanner_connecting => 'Connexion en cours...'; @override - String get scanner_disconnecting => 'Déconnexion...'; + String get scanner_disconnecting => 'Déconnexion...'; @override - String get scanner_notConnected => 'Non connecté'; + String get scanner_notConnected => 'Non connecté'; @override String scanner_connectedTo(String deviceName) { - return 'Connecté à $deviceName'; + return 'Connecté à $deviceName'; } @override @@ -165,17 +158,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Échec de la connexion : $error'; + return 'Échec de la connexion : $error'; } @override - String get scanner_stop => 'Arrêter'; + String get scanner_stop => 'Arrêter'; @override String get scanner_scan => 'Scanner'; @override - String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.'; + String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.'; @override String get scanner_bluetoothOffMessage => @@ -186,7 +179,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_chromeRequiredMessage => - 'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.'; + 'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.'; @override String get scanner_enableBluetooth => 'Activer le Bluetooth'; @@ -198,51 +191,51 @@ class AppLocalizationsFr extends AppLocalizations { String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Paramètres'; + String get settings_title => 'Paramètres'; @override - String get settings_deviceInfo => 'Informations du périphérique'; + String get settings_deviceInfo => 'Informations du périphérique'; @override - String get settings_appSettings => 'Paramètres de l\'application'; + String get settings_appSettings => 'Paramètres de l\'application'; @override String get settings_appSettingsSubtitle => - 'Notifications, messagerie et préférences de carte'; + 'Notifications, messagerie et préférences de carte'; @override - String get settings_nodeSettings => 'Paramètres du nœud'; + String get settings_nodeSettings => 'Paramètres du nÅ“ud'; @override - String get settings_nodeName => 'Nom du nœud'; + String get settings_nodeName => 'Nom du nÅ“ud'; @override - String get settings_nodeNameNotSet => 'Non défini'; + String get settings_nodeNameNotSet => 'Non défini'; @override - String get settings_nodeNameHint => 'Entrer le nom du nœud'; + String get settings_nodeNameHint => 'Entrer le nom du nÅ“ud'; @override - String get settings_nodeNameUpdated => 'Nom mis à jour'; + String get settings_nodeNameUpdated => 'Nom mis à jour'; @override - String get settings_radioSettings => 'Paramètres Radio'; + String get settings_radioSettings => 'Paramètres Radio'; @override String get settings_radioSettingsSubtitle => - 'Fréquence, puissance, facteur d\'espacement'; + 'Fréquence, puissance, facteur d\'espacement'; @override - String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour'; + String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour'; @override String get settings_location => 'Emplacement'; @override - String get settings_locationSubtitle => 'Coordonnées GPS'; + String get settings_locationSubtitle => 'Coordonnées GPS'; @override - String get settings_locationUpdated => 'Emplacement mis à jour'; + String get settings_locationUpdated => 'Emplacement mis à jour'; @override String get settings_locationBothRequired => @@ -256,15 +249,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Activer la mise à jour automatique de la position via GPS'; + 'Activer la mise à jour automatique de la position via GPS'; @override String get settings_locationIntervalSec => - 'Intervalle de mise-à-jour du GPS (Secondes)'; + 'Intervalle de mise-à-jour du GPS (Secondes)'; @override String get settings_locationIntervalInvalid => - 'L\'intervalle doit être compris entre 60 et 86400 secondes.'; + 'L\'intervalle doit être compris entre 60 et 86400 secondes.'; @override String get settings_latitude => 'Latitude'; @@ -273,7 +266,7 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_longitude => 'Longitude'; @override - String get settings_privacyMode => 'Mode de confidentialité'; + String get settings_privacyMode => 'Mode de confidentialité'; @override String get settings_privacyModeSubtitle => @@ -281,14 +274,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_privacyModeToggle => - 'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.'; + 'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.'; @override - String get settings_privacyModeEnabled => 'Mode de confidentialité activé'; + String get settings_privacyModeEnabled => 'Mode de confidentialité activé'; @override String get settings_privacyModeDisabled => - 'Mode de confidentialité désactivé'; + 'Mode de confidentialité désactivé'; @override String get settings_actions => 'Actions'; @@ -298,57 +291,58 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_sendAdvertisementSubtitle => - 'Présence diffusée maintenant'; + 'Présence diffusée maintenant'; @override - String get settings_advertisementSent => 'Annonce envoyée'; + String get settings_advertisementSent => 'Annonce envoyée'; @override String get settings_syncTime => 'Temps de synchronisation'; @override String get settings_syncTimeSubtitle => - 'Définir l\'heure de l\'appareil sur l\'heure du téléphone.'; + 'Définir l\'heure de l\'appareil sur l\'heure du téléphone.'; @override String get settings_timeSynchronized => 'Synchronisation temporelle'; @override - String get settings_refreshContacts => 'Rafraîchir les Contacts'; + String get settings_refreshContacts => 'Rafraîchir les Contacts'; @override String get settings_refreshContactsSubtitle => 'Recharger la liste des contacts depuis l\'appareil'; @override - String get settings_rebootDevice => 'Redémarrer l\'appareil'; + String get settings_rebootDevice => 'Redémarrer l\'appareil'; @override - String get settings_rebootDeviceSubtitle => 'Redémarrer l\'appareil MeshCore'; + String get settings_rebootDeviceSubtitle => + 'Redémarrer l\'appareil MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Êtes-vous sûr de vouloir redémarrer l\'appareil ? Vous serez déconnecté.'; + 'Êtes-vous sûr de vouloir redémarrer l\'appareil ? Vous serez déconnecté.'; @override - String get settings_debug => 'Déboguer'; + String get settings_debug => 'Déboguer'; @override - String get settings_bleDebugLog => 'Journal de débogage BLE'; + String get settings_bleDebugLog => 'Journal de débogage BLE'; @override String get settings_bleDebugLogSubtitle => - 'Commandes BLE, réponses et données brutes'; + 'Commandes BLE, réponses et données brutes'; @override - String get settings_appDebugLog => 'Journal de débogage de l\'application'; + String get settings_appDebugLog => 'Journal de débogage de l\'application'; @override String get settings_appDebugLogSubtitle => - 'Messages de débogage de l\'application'; + 'Messages de débogage de l\'application'; @override - String get settings_about => 'À propos'; + String get settings_about => 'À propos'; @override String settings_aboutVersion(String version) { @@ -360,11 +354,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_aboutDescription => - 'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.'; + 'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.'; @override String get settings_aboutOpenMeteoAttribution => - 'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)'; + 'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Nom'; @@ -373,13 +367,13 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_infoId => 'ID'; @override - String get settings_infoStatus => 'État'; + String get settings_infoStatus => 'État'; @override String get settings_infoBattery => 'Batterie'; @override - String get settings_infoPublicKey => 'Clé Publique'; + String get settings_infoPublicKey => 'Clé Publique'; @override String get settings_infoContactsCount => 'Nombre de contacts'; @@ -388,22 +382,22 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_infoChannelCount => 'Nombre de canaux'; @override - String get settings_presets => 'Préréglages'; + String get settings_presets => 'Préréglages'; @override - String get settings_frequency => 'Fréquence (MHz)'; + String get settings_frequency => 'Fréquence (MHz)'; @override String get settings_frequencyHelper => '300,0 - 2 500,0'; @override - String get settings_frequencyInvalid => 'Fréquence invalide (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Fréquence invalide (300-2500 MHz)'; @override String get settings_bandwidth => 'Bande passante'; @override - String get settings_spreadingFactor => 'Facteur de répartition'; + String get settings_spreadingFactor => 'Facteur de répartition'; @override String get settings_codingRate => 'Taux de codage'; @@ -418,15 +412,15 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)'; @override - String get settings_clientRepeat => 'Répétition hors réseau'; + String get settings_clientRepeat => 'Répétition hors réseau'; @override String get settings_clientRepeatSubtitle => - 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; + 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; @override String get settings_clientRepeatFreqWarning => - 'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.'; + 'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { @@ -434,19 +428,19 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get appSettings_title => 'Paramètres de l\'application'; + String get appSettings_title => 'Paramètres de l\'application'; @override String get appSettings_appearance => 'Apparence'; @override - String get appSettings_theme => 'Thème'; + String get appSettings_theme => 'Thème'; @override - String get appSettings_themeSystem => 'Défaut système'; + String get appSettings_themeSystem => 'Défaut système'; @override - String get appSettings_themeLight => 'Lumière'; + String get appSettings_themeLight => 'Lumière'; @override String get appSettings_themeDark => 'Sombre'; @@ -455,16 +449,16 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_language => 'Langue'; @override - String get appSettings_languageSystem => 'Par défaut du système'; + String get appSettings_languageSystem => 'Par défaut du système'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -473,16 +467,16 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -491,10 +485,10 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russe'; @@ -504,11 +498,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_enableMessageTracing => - 'Activer le traçage des messages'; + 'Activer le traçage des messages'; @override String get appSettings_enableMessageTracingSubtitle => - 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; + 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; @override String get appSettings_notifications => 'Notifications'; @@ -522,20 +516,20 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_notificationPermissionDenied => - 'Permission de notification refusée'; + 'Permission de notification refusée'; @override - String get appSettings_notificationsEnabled => 'Notifications activées'; + String get appSettings_notificationsEnabled => 'Notifications activées'; @override - String get appSettings_notificationsDisabled => 'Notifications désactivées'; + String get appSettings_notificationsDisabled => 'Notifications désactivées'; @override String get appSettings_messageNotifications => 'Notifications de Messages'; @override String get appSettings_messageNotificationsSubtitle => - 'Afficher une notification lors de la réception de nouveaux messages'; + 'Afficher une notification lors de la réception de nouveaux messages'; @override String get appSettings_channelMessageNotifications => @@ -543,7 +537,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_channelMessageNotificationsSubtitle => - 'Afficher une notification lors de la réception des messages de canal'; + 'Afficher une notification lors de la réception des messages de canal'; @override String get appSettings_advertisementNotifications => @@ -551,7 +545,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'Afficher une notification lors de la découverte de nouveaux nœuds'; + 'Afficher une notification lors de la découverte de nouveaux nÅ“uds'; @override String get appSettings_messaging => 'Messagerie'; @@ -562,31 +556,31 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Réinitialiser le chemin de contact après 5 tentatives d\'envoi infructueuses'; + 'Réinitialiser le chemin de contact après 5 tentatives d\'envoi infructueuses'; @override String get appSettings_pathsWillBeCleared => - 'Les chemins seront effacés après 5 tentatives infructueuses.'; + 'Les chemins seront effacés après 5 tentatives infructueuses.'; @override String get appSettings_pathsWillNotBeCleared => - 'Les chemins ne seront pas effacés automatiquement.'; + 'Les chemins ne seront pas effacés automatiquement.'; @override String get appSettings_autoRouteRotation => - 'Rotation de l\'itinéraire automatique'; + 'Rotation de l\'itinéraire automatique'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alterner entre les meilleurs chemins et le mode d\'envoi sur tout le réseau (flood)'; + 'Alterner entre les meilleurs chemins et le mode d\'envoi sur tout le réseau (flood)'; @override String get appSettings_autoRouteRotationEnabled => - 'Rotation du routage automatique activée'; + 'Rotation du routage automatique activée'; @override String get appSettings_autoRouteRotationDisabled => - 'Rotation de l\'itinéraire automatique désactivée'; + 'Rotation de l\'itinéraire automatique désactivée'; @override String get appSettings_battery => 'Batterie'; @@ -596,7 +590,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Définir par appareil ($deviceName)'; + return 'Définir par appareil ($deviceName)'; } @override @@ -616,35 +610,35 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_mapDisplay => 'Affichage de la carte'; @override - String get appSettings_showRepeaters => 'Afficher les répéteurs'; + String get appSettings_showRepeaters => 'Afficher les répéteurs'; @override String get appSettings_showRepeatersSubtitle => - 'Afficher les nœuds répéteurs sur la carte'; + 'Afficher les nÅ“uds répéteurs sur la carte'; @override - String get appSettings_showChatNodes => 'Afficher les nœuds de discussion'; + String get appSettings_showChatNodes => 'Afficher les nÅ“uds de discussion'; @override String get appSettings_showChatNodesSubtitle => - 'Afficher les nœuds de chat sur la carte'; + 'Afficher les nÅ“uds de chat sur la carte'; @override - String get appSettings_showOtherNodes => 'Afficher d\'autres nœuds'; + String get appSettings_showOtherNodes => 'Afficher d\'autres nÅ“uds'; @override String get appSettings_showOtherNodesSubtitle => - 'Afficher d\'autres types de nœuds sur la carte'; + 'Afficher d\'autres types de nÅ“uds sur la carte'; @override String get appSettings_timeFilter => 'Filtre du temps'; @override - String get appSettings_timeFilterShowAll => 'Afficher tous les nœuds'; + String get appSettings_timeFilterShowAll => 'Afficher tous les nÅ“uds'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Afficher les nœuds des $hours dernières heures'; + return 'Afficher les nÅ“uds des $hours dernières heures'; } @override @@ -652,71 +646,71 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_showNodesDiscoveredWithin => - 'Afficher les nœuds découverts dans :'; + 'Afficher les nÅ“uds découverts dans :'; @override String get appSettings_allTime => 'Tout le temps'; @override - String get appSettings_lastHour => 'Dernière heure'; + String get appSettings_lastHour => 'Dernière heure'; @override - String get appSettings_last6Hours => 'Dernières 6 heures'; + String get appSettings_last6Hours => 'Dernières 6 heures'; @override - String get appSettings_last24Hours => 'Dernières 24 heures'; + String get appSettings_last24Hours => 'Dernières 24 heures'; @override - String get appSettings_lastWeek => 'La semaine dernière'; + String get appSettings_lastWeek => 'La semaine dernière'; @override String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne'; @override - String get appSettings_unitsTitle => 'Unités'; + String get appSettings_unitsTitle => 'Unités'; @override - String get appSettings_unitsMetric => 'Métrique (m/km)'; + String get appSettings_unitsMetric => 'Métrique (m/km)'; @override - String get appSettings_unitsImperial => 'Impérial (ft / mi)'; + String get appSettings_unitsImperial => 'Impérial (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Aucune zone sélectionnée'; + String get appSettings_noAreaSelected => 'Aucune zone sélectionnée'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Zone sélectionnée (zoom $minZoom-$maxZoom)'; + return 'Zone sélectionnée (zoom $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Déboguer'; + String get appSettings_debugCard => 'Déboguer'; @override String get appSettings_appDebugLogging => - 'Journalisation de débogage de l\'application'; + 'Journalisation de débogage de l\'application'; @override String get appSettings_appDebugLoggingSubtitle => - 'Enregistrez les messages de débogage de l\'application Log pour le dépannage.'; + 'Enregistrez les messages de débogage de l\'application Log pour le dépannage.'; @override String get appSettings_appDebugLoggingEnabled => - 'Journalisation de débogage de l\'application activée'; + 'Journalisation de débogage de l\'application activée'; @override String get appSettings_appDebugLoggingDisabled => - 'Le débogage de l\'application est désactivé.'; + 'Le débogage de l\'application est désactivé.'; @override String get contacts_title => 'Contacts'; @override - String get contacts_noContacts => 'Aucun contact trouvé.'; + String get contacts_noContacts => 'Aucun contact trouvé.'; @override String get contacts_contactsWillAppear => - 'Les contacts apparaîtront lorsque les appareils font leur annonce.'; + 'Les contacts apparaîtront lorsque les appareils font leur annonce.'; @override String get contacts_unread => 'Non lu'; @@ -741,7 +735,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_searchRepeaters(int number, String str) { - return 'Rechercher $number$str Répéteurs...'; + return 'Rechercher $number$str Répéteurs...'; } @override @@ -753,7 +747,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_noUnreadContacts => 'Aucun contact non lu'; @override - String get contacts_noContactsFound => 'Aucun contact ou groupe trouvé.'; + String get contacts_noContactsFound => 'Aucun contact ou groupe trouvé.'; @override String get contacts_deleteContact => 'Supprimer le contact'; @@ -764,10 +758,10 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Gérer le répéteur'; + String get contacts_manageRepeater => 'Gérer le répéteur'; @override - String get contacts_manageRoom => 'Gérer le Room Server'; + String get contacts_manageRoom => 'Gérer le Room Server'; @override String get contacts_roomLogin => 'Connexion Room Server'; @@ -797,7 +791,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_groupAlreadyExists(String name) { - return 'Le groupe \"$name\" existe déjà.'; + return 'Le groupe \"$name\" existe déjà.'; } @override @@ -805,7 +799,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Aucun contact ne correspond à votre filtre.'; + 'Aucun contact ne correspond à votre filtre.'; @override String get contacts_noMembers => 'Aucun membre'; @@ -838,7 +832,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_title => 'Canaux'; @override - String get channels_noChannelsConfigured => 'Aucun canal configuré'; + String get channels_noChannelsConfigured => 'Aucun canal configuré'; @override String get channels_addPublicChannel => 'Ajouter un canal public'; @@ -847,7 +841,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_searchChannels => 'Rechercher des canaux...'; @override - String get channels_noChannelsFound => 'Aucun canal trouvé'; + String get channels_noChannelsFound => 'Aucun canal trouvé'; @override String channels_channelIndex(int index) { @@ -861,39 +855,39 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_public => 'Public'; @override - String get channels_private => 'Privé'; + String get channels_private => 'Privé'; @override String get channels_publicChannel => 'Canal public'; @override - String get channels_privateChannel => 'Canal privé'; + String get channels_privateChannel => 'Canal privé'; @override String get channels_editChannel => 'Modifier le canal'; @override - String get channels_muteChannel => 'Désactiver les notifications du canal'; + String get channels_muteChannel => 'Désactiver les notifications du canal'; @override - String get channels_unmuteChannel => 'Réactiver les notifications du canal'; + String get channels_unmuteChannel => 'Réactiver les notifications du canal'; @override String get channels_deleteChannel => 'Supprimer le canal'; @override String channels_deleteChannelConfirm(String name) { - return 'Supprimer $name? Cela ne peut pas être annulé.'; + return 'Supprimer $name? Cela ne peut pas être annulé.'; } @override String channels_channelDeleteFailed(String name) { - return 'Échec de la suppression de la chaîne \"$name\"'; + return 'Échec de la suppression de la chaîne \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Le canal \"$name\" a été supprimé'; + return 'Le canal \"$name\" a été supprimé'; } @override @@ -916,18 +910,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channels_generateRandomPsk => - 'Générer une clé de modulation PSK aléatoire'; + 'Générer une clé de modulation PSK aléatoire'; @override String get channels_enterChannelName => 'Veuillez entrer un nom de canal'; @override String get channels_pskMustBe32Hex => - 'Le PKS doit être composé de 32 caractères hexadécimaux.'; + 'Le PKS doit être composé de 32 caractères hexadécimaux.'; @override String channels_channelAdded(String name) { - return 'Le canal \"$name\" a été ajouté'; + return 'Le canal \"$name\" a été ajouté'; } @override @@ -940,11 +934,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String channels_channelUpdated(String name) { - return 'Le canal \"$name\" a été mis à jour'; + return 'Le canal \"$name\" a été mis à jour'; } @override - String get channels_publicChannelAdded => 'Le canal public a été ajouté'; + String get channels_publicChannelAdded => 'Le canal public a été ajouté'; @override String get channels_sortBy => 'Trier par'; @@ -953,7 +947,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_sortManual => 'Manuel'; @override - String get channels_sortAZ => 'A à Z'; + String get channels_sortAZ => 'A à Z'; @override String get channels_sortLatestMessages => 'Derniers messages'; @@ -962,18 +956,18 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_sortUnread => 'Non lu'; @override - String get channels_createPrivateChannel => 'Créer un Canal Privé'; + String get channels_createPrivateChannel => 'Créer un Canal Privé'; @override String get channels_createPrivateChannelDesc => - 'Sécurisé avec une clé secrète.'; + 'Sécurisé avec une clé secrète.'; @override - String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé'; + String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé'; @override String get channels_joinPrivateChannelDesc => - 'Entrer manuellement une clé secrète.'; + 'Entrer manuellement une clé secrète.'; @override String get channels_joinPublicChannel => 'Rejoindre le canal public'; @@ -993,7 +987,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_scanQrCode => 'Scanner un code QR'; @override - String get channels_scanQrCodeComingSoon => 'Bientôt disponible'; + String get channels_scanQrCodeComingSoon => 'Bientôt disponible'; @override String get channels_enterHashtag => 'Entrez le hashtag'; @@ -1008,16 +1002,16 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_sendMessageToStart => 'Envoyer un message pour commencer'; @override - String get chat_originalMessageNotFound => 'Message d\'origine non trouvé'; + String get chat_originalMessageNotFound => 'Message d\'origine non trouvé'; @override String chat_replyingTo(String name) { - return 'Répondre à $name'; + return 'Répondre à $name'; } @override String chat_replyTo(String name) { - return 'Répondre à $name'; + return 'Répondre à $name'; } @override @@ -1025,7 +1019,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'Envoyer un message à $contactName'; + return 'Envoyer un message à $contactName'; } @override @@ -1037,13 +1031,13 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get chat_messageCopied => 'Message copié'; + String get chat_messageCopied => 'Message copié'; @override - String get chat_messageDeleted => 'Message supprimé'; + String get chat_messageDeleted => 'Message supprimé'; @override - String get chat_retryingMessage => 'Tentative de récupération.'; + String get chat_retryingMessage => 'Tentative de récupération.'; @override String chat_retryCount(int current, int max) { @@ -1054,22 +1048,22 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_sendGif => 'Envoyer GIF'; @override - String get chat_reply => 'Répondre'; + String get chat_reply => 'Répondre'; @override - String get chat_addReaction => 'Ajouter une Réaction'; + String get chat_addReaction => 'Ajouter une Réaction'; @override String get chat_me => 'Moi'; @override - String get emojiCategorySmileys => 'Émojis'; + String get emojiCategorySmileys => 'Émojis'; @override String get emojiCategoryGestures => 'Gestes'; @override - String get emojiCategoryHearts => 'Cœurs'; + String get emojiCategoryHearts => 'CÅ“urs'; @override String get emojiCategoryObjects => 'Objets'; @@ -1081,25 +1075,25 @@ class AppLocalizationsFr extends AppLocalizations { String get gifPicker_searchHint => 'Rechercher des GIF...'; @override - String get gifPicker_poweredBy => 'Propulsé par GIPHY'; + String get gifPicker_poweredBy => 'Propulsé par GIPHY'; @override - String get gifPicker_noGifsFound => 'Aucun GIF trouvé'; + String get gifPicker_noGifsFound => 'Aucun GIF trouvé'; @override String get gifPicker_failedLoad => 'Impossible de charger les GIFs'; @override - String get gifPicker_failedSearch => 'Recherche de GIFs échouée'; + String get gifPicker_failedSearch => 'Recherche de GIFs échouée'; @override String get gifPicker_noInternet => 'Aucune connexion internet'; @override - String get debugLog_appTitle => 'Journal de débogage de l\'application'; + String get debugLog_appTitle => 'Journal de débogage de l\'application'; @override - String get debugLog_bleTitle => 'Journal de débogage BLE'; + String get debugLog_bleTitle => 'Journal de débogage BLE'; @override String get debugLog_copyLog => 'Copier le journal'; @@ -1108,17 +1102,17 @@ class AppLocalizationsFr extends AppLocalizations { String get debugLog_clearLog => 'Effacer le journal'; @override - String get debugLog_copied => 'Journal de débogage copié'; + String get debugLog_copied => 'Journal de débogage copié'; @override - String get debugLog_bleCopied => 'Journal BLE copié'; + String get debugLog_bleCopied => 'Journal BLE copié'; @override - String get debugLog_noEntries => 'Aucun journal de débogage pour le moment.'; + String get debugLog_noEntries => 'Aucun journal de débogage pour le moment.'; @override String get debugLog_enableInSettings => - 'Activer le débogage de l\'application dans les paramètres'; + 'Activer le débogage de l\'application dans les paramètres'; @override String get debugLog_frames => 'Cadres'; @@ -1128,11 +1122,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get debugLog_noBleActivity => - 'Pas d\'activité BLE enregistrée pour le moment.'; + 'Pas d\'activité BLE enregistrée pour le moment.'; @override String debugFrame_length(int count) { - return 'Longueur du cadre : $count octets'; + return 'Longueur du cadre : $count octets'; } @override @@ -1160,7 +1154,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String debugFrame_textType(int type, String label) { - return '- Type de texte : $type ($label)'; + return '- Type de texte : $type ($label)'; } @override @@ -1175,7 +1169,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :'; + String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :'; @override String get chat_pathManagement => 'Gestion des chemins'; @@ -1187,18 +1181,18 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_routingMode => 'Mode de routage'; @override - String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; + String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; @override - String get chat_forceFloodMode => 'Mode tout le réseau forcé'; + String get chat_forceFloodMode => 'Mode tout le réseau forcé'; @override String get chat_recentAckPaths => - 'Chemins ACK récents (touchez pour utiliser) :'; + 'Chemins ACK récents (touchez pour utiliser) :'; @override String get chat_pathHistoryFull => - 'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.'; + 'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.'; @override String get chat_hopSingular => 'saut'; @@ -1218,35 +1212,35 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get chat_successes => 'Succès'; + String get chat_successes => 'Succès'; @override String get chat_removePath => 'Supprimer le chemin'; @override String get chat_noPathHistoryYet => - 'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; + 'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; @override - String get chat_pathActions => 'Actions du chemin :'; + String get chat_pathActions => 'Actions du chemin :'; @override - String get chat_setCustomPath => 'Définir un chemin personnalisé'; + String get chat_setCustomPath => 'Définir un chemin personnalisé'; @override String get chat_setCustomPathSubtitle => - 'Spécifier manuellement le chemin de routage'; + 'Spécifier manuellement le chemin de routage'; @override String get chat_clearPath => 'Effacer le chemin'; @override String get chat_clearPathSubtitle => - 'Forcer la redécouverte lors de la prochaine envoi'; + 'Forcer la redécouverte lors de la prochaine envoi'; @override String get chat_pathCleared => - 'Le chemin est dégagé. Le prochain message redécouvrira le tracé.'; + 'Le chemin est dégagé. Le prochain message redécouvrira le tracé.'; @override String get chat_floodModeSubtitle => @@ -1254,14 +1248,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.'; + 'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.'; @override String get chat_fullPath => 'Chemin complet'; @override String get chat_pathDetailsNotAvailable => - 'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.'; + 'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1271,19 +1265,19 @@ class AppLocalizationsFr extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Chemin défini : $hopCount $_temp0 - $status'; + return 'Chemin défini : $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Sauvegardé localement. Connectez-vous pour synchroniser.'; + 'Sauvegardé localement. Connectez-vous pour synchroniser.'; @override - String get chat_pathDeviceConfirmed => 'Appareil confirmé.'; + String get chat_pathDeviceConfirmed => 'Appareil confirmé.'; @override String get chat_pathDeviceNotConfirmed => - 'L\'appareil n\'a pas encore été confirmé.'; + 'L\'appareil n\'a pas encore été confirmé.'; @override String get chat_type => 'Saisir'; @@ -1292,31 +1286,31 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_path => 'Chemin'; @override - String get chat_publicKey => 'Clé Publique'; + String get chat_publicKey => 'Clé Publique'; @override String get chat_compressOutgoingMessages => 'Compresser les messages sortants'; @override - String get chat_floodForced => 'Tout le réseau (forcée)'; + String get chat_floodForced => 'Tout le réseau (forcée)'; @override - String get chat_directForced => 'Direct (forcé)'; + String get chat_directForced => 'Direct (forcé)'; @override String chat_hopsForced(int count) { - return '$count sauts (forcés)'; + return '$count sauts (forcés)'; } @override - String get chat_floodAuto => 'Tout le réseau (auto)'; + String get chat_floodAuto => 'Tout le réseau (auto)'; @override String get chat_direct => 'Afficher'; @override - String get chat_poiShared => 'Point d\'intérêt Partagé'; + String get chat_poiShared => 'Point d\'intérêt Partagé'; @override String chat_unread(int count) { @@ -1342,7 +1336,7 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_invalidLink => 'Format de lien invalide'; @override - String get map_title => 'Carte des nœuds'; + String get map_title => 'Carte des nÅ“uds'; @override String get map_lineOfSight => 'Ligne de vue'; @@ -1352,15 +1346,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_noNodesWithLocation => - 'Aucun nœud avec des données de localisation'; + 'Aucun nÅ“ud avec des données de localisation'; @override String get map_nodesNeedGps => - 'Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.'; + 'Les nÅ“uds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.'; @override String map_nodesCount(int count) { - return 'Nœuds : $count'; + return 'NÅ“uds : $count'; } @override @@ -1372,7 +1366,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_chat => 'Chat'; @override - String get map_repeater => 'Répéteur'; + String get map_repeater => 'Répéteur'; @override String get map_room => 'Salle'; @@ -1381,23 +1375,23 @@ class AppLocalizationsFr extends AppLocalizations { String get map_sensor => 'Capteur'; @override - String get map_pinDm => 'Clé (DM)'; + String get map_pinDm => 'Clé (DM)'; @override - String get map_pinPrivate => 'Verrouiller (Privé)'; + String get map_pinPrivate => 'Verrouiller (Privé)'; @override - String get map_pinPublic => 'Clé (Public)'; + String get map_pinPublic => 'Clé (Public)'; @override - String get map_lastSeen => 'Dernière fois vu'; + String get map_lastSeen => 'Dernière fois vu'; @override String get map_disconnectConfirm => - 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; + 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; @override - String get map_from => 'À partir de'; + String get map_from => 'À partir de'; @override String get map_source => 'Source'; @@ -1409,13 +1403,13 @@ class AppLocalizationsFr extends AppLocalizations { String get map_shareMarkerHere => 'Partager le marqueur ici'; @override - String get map_pinLabel => 'Étiquete de repin'; + String get map_pinLabel => 'Étiquete de repin'; @override - String get map_label => 'Étiquette'; + String get map_label => 'Étiquette'; @override - String get map_pointOfInterest => 'Point d\'intérêt'; + String get map_pointOfInterest => 'Point d\'intérêt'; @override String get map_sendToContact => 'Envoyer au contact'; @@ -1431,89 +1425,89 @@ class AppLocalizationsFr extends AppLocalizations { @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Vous êtes sur le point de partager un emplacement dans $channelLabel. Ce canal est public et toute personne disposant de la clé PSK peut le voir.'; + return 'Vous êtes sur le point de partager un emplacement dans $channelLabel. Ce canal est public et toute personne disposant de la clé PSK peut le voir.'; } @override String get map_connectToShareMarkers => - 'Connectez-vous à un appareil pour partager des marqueurs'; + 'Connectez-vous à un appareil pour partager des marqueurs'; @override - String get map_filterNodes => 'Filtrer les nœuds'; + String get map_filterNodes => 'Filtrer les nÅ“uds'; @override - String get map_nodeTypes => 'Types de nœuds'; + String get map_nodeTypes => 'Types de nÅ“uds'; @override - String get map_chatNodes => 'Nœuds de Chat'; + String get map_chatNodes => 'NÅ“uds de Chat'; @override - String get map_repeaters => 'Répéteurs'; + String get map_repeaters => 'Répéteurs'; @override - String get map_otherNodes => 'Autres nœuds'; + String get map_otherNodes => 'Autres nÅ“uds'; @override - String get map_keyPrefix => 'Préfixe clé'; + String get map_keyPrefix => 'Préfixe clé'; @override - String get map_filterByKeyPrefix => 'Filtrer par préfixe de clé'; + String get map_filterByKeyPrefix => 'Filtrer par préfixe de clé'; @override - String get map_publicKeyPrefix => 'Préfixe de clé publique'; + String get map_publicKeyPrefix => 'Préfixe de clé publique'; @override String get map_markers => 'Marqueurs'; @override - String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; + String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; @override - String get map_lastSeenTime => 'Dernière fois vu'; + String get map_lastSeenTime => 'Dernière fois vu'; @override - String get map_sharedPin => 'Clé partagée'; + String get map_sharedPin => 'Clé partagée'; @override String get map_joinRoom => 'Rejoindre la salle'; @override - String get map_manageRepeater => 'Gérer le répéteur'; + String get map_manageRepeater => 'Gérer le répéteur'; @override String get map_tapToAdd => - 'Appuyez sur les nœuds pour les ajouter au chemin.'; + 'Appuyez sur les nÅ“uds pour les ajouter au chemin.'; @override - String get map_runTrace => 'Exécuter la traçage de chemin'; + String get map_runTrace => 'Exécuter la traçage de chemin'; @override String get map_removeLast => 'Supprimer le dernier'; @override - String get map_pathTraceCancelled => 'Traçage de chemin annulé'; + String get map_pathTraceCancelled => 'Traçage de chemin annulé'; @override String get mapCache_title => 'Cache de Carte Hors Ligne'; @override String get mapCache_selectAreaFirst => - 'Sélectionner une zone pour la mise en cache en premier'; + 'Sélectionner une zone pour la mise en cache en premier'; @override String get mapCache_noTilesToDownload => - 'Aucun tuilage à télécharger pour cette zone.'; + 'Aucun tuilage à télécharger pour cette zone.'; @override - String get mapCache_downloadTilesTitle => 'Télécharger les tuiles'; + String get mapCache_downloadTilesTitle => 'Télécharger les tuiles'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Télécharger $count tuiles pour un usage hors ligne ?'; + return 'Télécharger $count tuiles pour un usage hors ligne ?'; } @override - String get mapCache_downloadAction => 'Télécharger'; + String get mapCache_downloadAction => 'Télécharger'; @override String mapCache_cachedTiles(int count) { @@ -1522,7 +1516,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Tuiles mis en cache ($downloaded) ($failed ratés)'; + return 'Tuiles mis en cache ($downloaded) ($failed ratés)'; } @override @@ -1530,14 +1524,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get mapCache_clearOfflineCachePrompt => - 'Supprimer toutes les tuiles de carte mises en cache ?'; + 'Supprimer toutes les tuiles de carte mises en cache ?'; @override String get mapCache_offlineCacheCleared => - 'Le cache hors ligne a été effacé.'; + 'Le cache hors ligne a été effacé.'; @override - String get mapCache_noAreaSelected => 'Aucune zone sélectionnée'; + String get mapCache_noAreaSelected => 'Aucune zone sélectionnée'; @override String get mapCache_cacheArea => 'Zone de cache'; @@ -1555,18 +1549,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String mapCache_downloadedTiles(int completed, int total) { - return 'Téléchargé $completed / $total'; + return 'Téléchargé $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Télécharger les tuiles'; + String get mapCache_downloadTilesButton => 'Télécharger les tuiles'; @override String get mapCache_clearCacheButton => 'Vider le Cache'; @override String mapCache_failedDownloads(int count) { - return 'Téléchargements échoués : $count'; + return 'Téléchargements échoués : $count'; } @override @@ -1628,14 +1622,14 @@ class AppLocalizationsFr extends AppLocalizations { String get time_allTime => 'Tout le temps'; @override - String get dialog_disconnect => 'Déconnecter'; + String get dialog_disconnect => 'Déconnecter'; @override String get dialog_disconnectConfirm => - 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; + 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; @override - String get login_repeaterLogin => 'Connexion au répéteur'; + String get login_repeaterLogin => 'Connexion au répéteur'; @override String get login_roomLogin => 'Connexion Room Server'; @@ -1651,15 +1645,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'Le mot de passe sera stocké en toute sécurité sur cet appareil.'; + 'Le mot de passe sera stocké en toute sécurité sur cet appareil.'; @override String get login_repeaterDescription => - 'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.'; + 'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.'; @override String get login_roomDescription => - 'Entrez le mot de passe de la pièce pour accéder aux paramètres et à l\'état.'; + 'Entrez le mot de passe de la pièce pour accéder aux paramètres et à l\'état.'; @override String get login_routing => 'Redirection'; @@ -1668,13 +1662,13 @@ class AppLocalizationsFr extends AppLocalizations { String get login_routingMode => 'Mode de routage'; @override - String get login_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; + String get login_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; @override - String get login_forceFloodMode => 'Mode tout le réseau forcé'; + String get login_forceFloodMode => 'Mode tout le réseau forcé'; @override - String get login_managePaths => 'Gérer les chemins'; + String get login_managePaths => 'Gérer les chemins'; @override String get login_login => 'Connexion'; @@ -1686,12 +1680,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String login_failed(String error) { - return 'Connexion échouée : $error'; + return 'Connexion échouée : $error'; } @override String get login_failedMessage => - 'Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.'; + 'Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.'; @override String get common_reload => 'Recharger'; @@ -1716,51 +1710,52 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get path_enterCustomPath => 'Entrer un chemin personnalisé'; + String get path_enterCustomPath => 'Entrer un chemin personnalisé'; @override String get path_currentPathLabel => 'Chemin actuel'; @override String get path_hexPrefixInstructions => - 'Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.'; + 'Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.'; @override String get path_hexPrefixExample => - 'Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).'; + 'Exemple : A1,F2,3C (chaque nÅ“ud utilise le premier octet de sa clé publique).'; @override - String get path_labelHexPrefixes => 'Préfixes hexadécimaux'; + String get path_labelHexPrefixes => 'Préfixes hexadécimaux'; @override String get path_helperMaxHops => - 'Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)'; + 'Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)'; @override - String get path_selectFromContacts => 'Sélectionner à partir des contacts :'; + String get path_selectFromContacts => + 'Sélectionner à partir des contacts :'; @override String get path_noRepeatersFound => - 'Aucun répéteur ou serveur de salle n\'a été trouvé.'; + 'Aucun répéteur ou serveur de salle n\'a été trouvé.'; @override String get path_customPathsRequire => - 'Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.'; + 'Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Préfixes hexadécimaux invalides : $prefixes'; + return 'Préfixes hexadécimaux invalides : $prefixes'; } @override String get path_tooLong => - 'Le chemin est trop long. Maximum 64 sauts autorisés.'; + 'Le chemin est trop long. Maximum 64 sauts autorisés.'; @override - String get path_setPath => 'Définir le chemin'; + String get path_setPath => 'Définir le chemin'; @override - String get repeater_management => 'Gestion des répéteurs'; + String get repeater_management => 'Gestion des répéteurs'; @override String get room_management => 'Administrattion Room Server'; @@ -1769,24 +1764,24 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_managementTools => 'Outils de Gestion'; @override - String get repeater_status => 'État'; + String get repeater_status => 'État'; @override String get repeater_statusSubtitle => - 'Afficher l\'état, les statistiques et les voisins du répéteur'; + 'Afficher l\'état, les statistiques et les voisins du répéteur'; @override - String get repeater_telemetry => 'Télémetrie'; + String get repeater_telemetry => 'Télémetrie'; @override String get repeater_telemetrySubtitle => - 'Afficher la télémétrie des capteurs et les statistiques du système'; + 'Afficher la télémétrie des capteurs et les statistiques du système'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; + String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; @override String get repeater_neighbors => 'Voisins'; @@ -1795,34 +1790,34 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.'; @override - String get repeater_settings => 'Paramètres'; + String get repeater_settings => 'Paramètres'; @override String get repeater_settingsSubtitle => - 'Configurer les paramètres du répéteur'; + 'Configurer les paramètres du répéteur'; @override - String get repeater_statusTitle => 'État du répéteur'; + String get repeater_statusTitle => 'État du répéteur'; @override String get repeater_routingMode => 'Mode de routage'; @override String get repeater_autoUseSavedPath => - 'Auto (utiliser le chemin sauvegardé)'; + 'Auto (utiliser le chemin sauvegardé)'; @override - String get repeater_forceFloodMode => 'Mode tout le réseau forcé'; + String get repeater_forceFloodMode => 'Mode tout le réseau forcé'; @override String get repeater_pathManagement => 'Gestion des chemins'; @override - String get repeater_refresh => 'Rafraîchir'; + String get repeater_refresh => 'Rafraîchir'; @override String get repeater_statusRequestTimeout => - 'Demande de statut délai dépassé.'; + 'Demande de statut délai dépassé.'; @override String repeater_errorLoadingStatus(String error) { @@ -1830,22 +1825,22 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_systemInformation => 'Informations Système'; + String get repeater_systemInformation => 'Informations Système'; @override String get repeater_battery => 'Batterie'; @override - String get repeater_clockAtLogin => 'Horloge (au démarrage)'; + String get repeater_clockAtLogin => 'Horloge (au démarrage)'; @override - String get repeater_uptime => 'Disponibilité'; + String get repeater_uptime => 'Disponibilité'; @override String get repeater_queueLength => 'Longueur de la file d\'attente'; @override - String get repeater_debugFlags => 'Marqueurs de débogage'; + String get repeater_debugFlags => 'Marqueurs de débogage'; @override String get repeater_radioStatistics => 'Statistiques Radio'; @@ -1869,10 +1864,10 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_packetStatistics => 'Statistiques des paquets'; @override - String get repeater_sent => 'Envoyé'; + String get repeater_sent => 'Envoyé'; @override - String get repeater_received => 'Reçu'; + String get repeater_received => 'Reçu'; @override String get repeater_duplicates => 'Doublons'; @@ -1889,17 +1884,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; + return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; + return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Tout le réseau : $flood, Direct : $direct'; + return 'Tout le réseau : $flood, Direct : $direct'; } @override @@ -1908,35 +1903,35 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Paramètres du répéteur'; + String get repeater_settingsTitle => 'Paramètres du répéteur'; @override - String get repeater_basicSettings => 'Paramètres de base'; + String get repeater_basicSettings => 'Paramètres de base'; @override - String get repeater_repeaterName => 'Nom du répéteur'; + String get repeater_repeaterName => 'Nom du répéteur'; @override - String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur'; + String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur'; @override String get repeater_adminPassword => 'Mot de passe Administrateur'; @override - String get repeater_adminPasswordHelper => 'Mot de passe d\'accès complet'; + String get repeater_adminPasswordHelper => 'Mot de passe d\'accès complet'; @override - String get repeater_guestPassword => 'Mot de passe invité'; + String get repeater_guestPassword => 'Mot de passe invité'; @override String get repeater_guestPasswordHelper => - 'Accès en lecture seule avec mot de passe'; + 'Accès en lecture seule avec mot de passe'; @override - String get repeater_radioSettings => 'Paramètres Radio'; + String get repeater_radioSettings => 'Paramètres Radio'; @override - String get repeater_frequencyMhz => 'Fréquence (MHz)'; + String get repeater_frequencyMhz => 'Fréquence (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -1951,54 +1946,54 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_bandwidth => 'Bande passante'; @override - String get repeater_spreadingFactor => 'Facteur de répartition'; + String get repeater_spreadingFactor => 'Facteur de répartition'; @override String get repeater_codingRate => 'Taux de codage'; @override - String get repeater_locationSettings => 'Paramètres de localisation'; + String get repeater_locationSettings => 'Paramètres de localisation'; @override String get repeater_latitude => 'Latitude'; @override String get repeater_latitudeHelper => - 'Degrés décimaux (par exemple, 37.7749)'; + 'Degrés décimaux (par exemple, 37.7749)'; @override String get repeater_longitude => 'Longitude'; @override String get repeater_longitudeHelper => - 'Degrés décimaux (par exemple, -122,4194)'; + 'Degrés décimaux (par exemple, -122,4194)'; @override - String get repeater_features => 'Fonctionnalités'; + String get repeater_features => 'Fonctionnalités'; @override String get repeater_packetForwarding => 'Transfert de paquets'; @override String get repeater_packetForwardingSubtitle => - 'Activer le répéteur pour transmettre des paquets'; + 'Activer le répéteur pour transmettre des paquets'; @override - String get repeater_guestAccess => 'Accès Invité'; + String get repeater_guestAccess => 'Accès Invité'; @override String get repeater_guestAccessSubtitle => - 'Autoriser l\'accès invité en lecture seule'; + 'Autoriser l\'accès invité en lecture seule'; @override - String get repeater_privacyMode => 'Mode de confidentialité'; + String get repeater_privacyMode => 'Mode de confidentialité'; @override String get repeater_privacyModeSubtitle => 'Cacher le nom/l\'emplacement dans les annonces'; @override - String get repeater_advertisementSettings => 'Paramètres d\'annonces'; + String get repeater_advertisementSettings => 'Paramètres d\'annonces'; @override String get repeater_localAdvertInterval => @@ -2011,7 +2006,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervalle des annonces à tout le réseau (flood)'; + 'Intervalle des annonces à tout le réseau (flood)'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2020,51 +2015,52 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervalle d\'annonces cryptées'; + 'Intervalle d\'annonces cryptées'; @override String get repeater_dangerZone => 'Zone dangereuse'; @override - String get repeater_rebootRepeater => 'Redémarrer Répéteur'; + String get repeater_rebootRepeater => 'Redémarrer Répéteur'; @override String get repeater_rebootRepeaterSubtitle => - 'Réinitialiser l\'appareil répéteur'; + 'Réinitialiser l\'appareil répéteur'; @override String get repeater_rebootRepeaterConfirm => - 'Êtes-vous sûr de vouloir redémarrer ce répéteur ?'; + 'Êtes-vous sûr de vouloir redémarrer ce répéteur ?'; @override - String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité'; + String get repeater_regenerateIdentityKey => + 'Ré générer la clé d\'identité'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Générer une nouvelle paire de clés publique/privée'; + 'Générer une nouvelle paire de clés publique/privée'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Cela générera une nouvelle identité pour le répéteur. Continuer ?'; + 'Cela générera une nouvelle identité pour le répéteur. Continuer ?'; @override - String get repeater_eraseFileSystem => 'Supprimer le système de fichiers'; + String get repeater_eraseFileSystem => 'Supprimer le système de fichiers'; @override String get repeater_eraseFileSystemSubtitle => - 'Formater le système de fichiers du répéteur'; + 'Formater le système de fichiers du répéteur'; @override String get repeater_eraseFileSystemConfirm => - 'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !'; + 'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !'; @override String get repeater_eraseSerialOnly => - 'Erase n\'est disponible que via la console série.'; + 'Erase n\'est disponible que via la console série.'; @override String repeater_commandSent(String command) { - return 'Commande envoyée : $command'; + return 'Commande envoyée : $command'; } @override @@ -2077,57 +2073,58 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_settingsSaved => - 'Les paramètres ont été enregistrés avec succès.'; + 'Les paramètres ont été enregistrés avec succès.'; @override String repeater_errorSavingSettings(String error) { - return 'Erreur lors de la sauvegarde des paramètres : $error'; + return 'Erreur lors de la sauvegarde des paramètres : $error'; } @override String get repeater_refreshBasicSettings => - 'Rafraîchir les paramètres de base'; + 'Rafraîchir les paramètres de base'; @override - String get repeater_refreshRadioSettings => 'Rafraîchir les paramètres Radio'; + String get repeater_refreshRadioSettings => + 'Rafraîchir les paramètres Radio'; @override - String get repeater_refreshTxPower => 'Rafraîchir la tension TX'; + String get repeater_refreshTxPower => 'Rafraîchir la tension TX'; @override String get repeater_refreshLocationSettings => - 'Rafraîchir les paramètres de localisation'; + 'Rafraîchir les paramètres de localisation'; @override String get repeater_refreshPacketForwarding => - 'Rafraîchir le routage des paquets'; + 'Rafraîchir le routage des paquets'; @override - String get repeater_refreshGuestAccess => 'Rafraîchir l\'accès invité'; + String get repeater_refreshGuestAccess => 'Rafraîchir l\'accès invité'; @override String get repeater_refreshPrivacyMode => - 'Rafraîchir le Mode Confidentialité'; + 'Rafraîchir le Mode Confidentialité'; @override String get repeater_refreshAdvertisementSettings => - 'Rafraîchir les Paramètres des annonces'; + 'Rafraîchir les Paramètres des annonces'; @override String repeater_refreshed(String label) { - return '$label rafraîchi'; + return '$label rafraîchi'; } @override String repeater_errorRefreshing(String label) { - return 'Erreur lors du rafraîchissement de $label'; + return 'Erreur lors du rafraîchissement de $label'; } @override - String get repeater_cliTitle => 'Répéteur CLI'; + String get repeater_cliTitle => 'Répéteur CLI'; @override - String get repeater_debugNextCommand => 'Déboguer Prochaine Commande'; + String get repeater_debugNextCommand => 'Déboguer Prochaine Commande'; @override String get repeater_commandHelp => 'Aide'; @@ -2137,7 +2134,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_noCommandsSent => - 'Aucune commande n\'a encore été envoyée.'; + 'Aucune commande n\'a encore été envoyée.'; @override String get repeater_typeCommandOrUseQuick => @@ -2147,7 +2144,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_enterCommandHint => 'Entrer la commande...'; @override - String get repeater_previousCommand => 'Commande précédente'; + String get repeater_previousCommand => 'Commande précédente'; @override String get repeater_nextCommand => 'Prochaine commande'; @@ -2189,7 +2186,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpReboot => - 'Redémarre l\'appareil. (Note, vous risquez d\'obtenir \'Timeout\' ce qui est normal)'; + 'Redémarre l\'appareil. (Note, vous risquez d\'obtenir \'Timeout\' ce qui est normal)'; @override String get repeater_cliHelpClock => @@ -2197,116 +2194,116 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpPassword => - 'Définit un nouveau mot de passe administrateur pour l\'appareil.'; + 'Définit un nouveau mot de passe administrateur pour l\'appareil.'; @override String get repeater_cliHelpVersion => - 'Affiche la version du périphérique et la date de construction du micrologiciel.'; + 'Affiche la version du périphérique et la date de construction du micrologiciel.'; @override String get repeater_cliHelpClearStats => - 'Réinitialise divers compteurs de statistiques à zéro.'; + 'Réinitialise divers compteurs de statistiques à zéro.'; @override - String get repeater_cliHelpSetAf => 'Définit le facteur de temps d\'air.'; + String get repeater_cliHelpSetAf => 'Définit le facteur de temps d\'air.'; @override String get repeater_cliHelpSetTx => - 'Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).'; + 'Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).'; @override String get repeater_cliHelpSetRepeat => - 'Active ou désactive le rôle du répéteur pour ce nœud.'; + 'Active ou désactive le rôle du répéteur pour ce nÅ“ud.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)'; + '(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)'; @override String get repeater_cliHelpSetFloodMax => - 'Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n\'est pas acheminé).'; + 'Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n\'est pas acheminé).'; @override String get repeater_cliHelpSetIntThresh => - 'Définit le seuil d\'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.'; + 'Définit le seuil d\'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Définit l\'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.'; + 'Définit l\'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.'; @override String get repeater_cliHelpSetMultiAcks => - 'Active ou désactive la fonctionnalité « double ACKs ».'; + 'Active ou désactive la fonctionnalité « double ACKs ».'; @override String get repeater_cliHelpSetAdvertInterval => - 'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.'; + 'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Définit l\'intervalle du minuteur en heures pour envoyer un paquet d\'annonce massive. Définir sur 0 pour désactiver.'; + 'Définit l\'intervalle du minuteur en heures pour envoyer un paquet d\'annonce massive. Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetGuestPassword => - 'Définit/met à jour le mot de passe de l\'invité. (pour les répéteurs, les connexions d\'invités peuvent envoyer la requête \"Get Stats\")'; + 'Définit/met à jour le mot de passe de l\'invité. (pour les répéteurs, les connexions d\'invités peuvent envoyer la requête \"Get Stats\")'; @override - String get repeater_cliHelpSetName => 'Définit le nom de l\'annonce.'; + String get repeater_cliHelpSetName => 'Définit le nom de l\'annonce.'; @override String get repeater_cliHelpSetLat => - 'Définit la latitude de la carte des annonces. (degrés décimaux)'; + 'Définit la latitude de la carte des annonces. (degrés décimaux)'; @override String get repeater_cliHelpSetLon => - 'Définit la longitude de la carte de l\'annonce. (degrés décimaux)'; + 'Définit la longitude de la carte de l\'annonce. (degrés décimaux)'; @override String get repeater_cliHelpSetRadio => - 'Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.'; + 'Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.'; @override String get repeater_cliHelpSetRxDelay => - 'Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.'; + 'Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetTxDelay => - 'Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).'; + 'Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.'; + 'Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Activer/Désactiver le pont.'; + String get repeater_cliHelpSetBridgeEnabled => 'Activer/Désactiver le pont.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Définir le délai avant de renvoyer les paquets.'; + 'Définir le délai avant de renvoyer les paquets.'; @override String get repeater_cliHelpSetBridgeSource => - 'Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.'; + 'Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Définir la vitesse de communication série pour les ponts Rs232.'; + 'Définir la vitesse de communication série pour les ponts Rs232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Définir le secret du pont pour les ponts espnow.'; + 'Définir le secret du pont pour les ponts espnow.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).'; + 'Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).'; @override String get repeater_cliHelpTempRadio => - 'Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d\'origine. (ne sauvegarde pas dans les préférences).'; + 'Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d\'origine. (ne sauvegarde pas dans les préférences).'; @override String get repeater_cliHelpSetPerm => - 'Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).'; + 'Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).'; @override String get repeater_cliHelpGetBridgeType => @@ -2314,98 +2311,98 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpLogStart => - 'Démarre l\'enregistrement des paquets dans le système de fichiers.'; + 'Démarre l\'enregistrement des paquets dans le système de fichiers.'; @override String get repeater_cliHelpLogStop => - 'Arrêter de journaliser les paquets vers le système de fichiers.'; + 'Arrêter de journaliser les paquets vers le système de fichiers.'; @override String get repeater_cliHelpLogErase => - 'Supprime les journaux de paquets du système de fichiers.'; + 'Supprime les journaux de paquets du système de fichiers.'; @override String get repeater_cliHelpNeighbors => - 'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; + 'Affiche une liste d\'autres nÅ“uds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; @override String get repeater_cliHelpNeighborRemove => - 'Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.'; + 'Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.'; @override String get repeater_cliHelpRegion => - '(série uniquement) Liste toutes les régions définies et les autorisations actuelles d\'annonces sur tout le réseau (flood).'; + '(série uniquement) Liste toutes les régions définies et les autorisations actuelles d\'annonces sur tout le réseau (flood).'; @override String get repeater_cliHelpRegionLoad => - 'REMARQUE : il s\'agit d\'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d\'un espace). Terminé par l\'envoi d\'une ligne vide/commande.'; + 'REMARQUE : il s\'agit d\'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d\'un espace). Terminé par l\'envoi d\'une ligne vide/commande.'; @override String get repeater_cliHelpRegionGet => - 'Recherche la région avec le préfixe de nom donné (ou \"\" pour l\'étendue globale). Répond avec \"-> nom-de-région (nom-parent) \'F\'\"'; + 'Recherche la région avec le préfixe de nom donné (ou \"\" pour l\'étendue globale). Répond avec \"-> nom-de-région (nom-parent) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Ajoute ou met à jour une définition de région avec le nom donné.'; + 'Ajoute ou met à jour une définition de région avec le nom donné.'; @override String get repeater_cliHelpRegionRemove => - 'Supprime une définition de région avec le nom donné.'; + 'Supprime une définition de région avec le nom donné.'; @override String get repeater_cliHelpRegionAllowf => - 'Définit les autorisations de \"Flot\" pour la région donnée. (\'\' pour la portée globale/héritée)'; + 'Définit les autorisations de \"Flot\" pour la région donnée. (\'\' pour la portée globale/héritée)'; @override String get repeater_cliHelpRegionDenyf => - 'Supprime l\'autorisation \'F\'lood\' pour la région donnée. (NOTE : à ce stade, il n\'est pas conseillé de l\'utiliser sur l\'étendue globale/héritée !! )'; + 'Supprime l\'autorisation \'F\'lood\' pour la région donnée. (NOTE : à ce stade, il n\'est pas conseillé de l\'utiliser sur l\'étendue globale/héritée !! )'; @override String get repeater_cliHelpRegionHome => - 'Répond avec la région \'maison\' actuelle. (Note appliquée nulle part pour l\'instant, réservée à une utilisation future)'; + 'Répond avec la région \'maison\' actuelle. (Note appliquée nulle part pour l\'instant, réservée à une utilisation future)'; @override - String get repeater_cliHelpRegionHomeSet => 'Définit la région \'maison\'.'; + String get repeater_cliHelpRegionHomeSet => 'Définit la région \'maison\'.'; @override String get repeater_cliHelpRegionSave => - 'Conserve la liste/la carte des régions dans le stockage.'; + 'Conserve la liste/la carte des régions dans le stockage.'; @override String get repeater_cliHelpGps => - 'Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.'; + 'Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.'; @override - String get repeater_cliHelpGpsOnOff => 'Activer/désactiver le GPS.'; + String get repeater_cliHelpGpsOnOff => 'Activer/désactiver le GPS.'; @override String get repeater_cliHelpGpsSync => - 'Synchronise l\'heure du nœud avec l\'horloge GPS.'; + 'Synchronise l\'heure du nÅ“ud avec l\'horloge GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Définit la position du nœud aux coordonnées GPS et enregistre les préférences.'; + 'Définit la position du nÅ“ud aux coordonnées GPS et enregistre les préférences.'; @override String get repeater_cliHelpGpsAdvert => - 'Donne la configuration de l\'annonce de la localisation du nœud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences'; + 'Donne la configuration de l\'annonce de la localisation du nÅ“ud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences'; @override String get repeater_cliHelpGpsAdvertSet => - 'Définit la configuration de l\'annonce de localisation.'; + 'Définit la configuration de l\'annonce de localisation.'; @override String get repeater_commandsListTitle => 'Liste des commandes'; @override String get repeater_commandsListNote => - 'NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...'; + 'NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...'; @override - String get repeater_general => 'Général'; + String get repeater_general => 'Général'; @override - String get repeater_settingsCategory => 'Paramètres'; + String get repeater_settingsCategory => 'Paramètres'; @override String get repeater_bridge => 'Pont'; @@ -2414,36 +2411,37 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_logging => 'Journalisation'; @override - String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)'; + String get repeater_neighborsRepeaterOnly => + 'Voisins (Uniquement répéteur)'; @override String get repeater_regionManagementRepeaterOnly => - 'Gestion des régions (uniquement pour le répéteur)'; + 'Gestion des régions (uniquement pour le répéteur)'; @override String get repeater_regionNote => - 'Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.'; + 'Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.'; @override String get repeater_gpsManagement => 'Gestion GPS'; @override String get repeater_gpsNote => - 'La commande GPS a été introduite pour gérer les sujets liés à la localisation.'; + 'La commande GPS a été introduite pour gérer les sujets liés à la localisation.'; @override - String get telemetry_receivedData => 'Données de télémétrie reçues'; + String get telemetry_receivedData => 'Données de télémétrie reçues'; @override - String get telemetry_requestTimeout => 'Demande de télémétrie expirée.'; + String get telemetry_requestTimeout => 'Demande de télémétrie expirée.'; @override String telemetry_errorLoading(String error) { - return 'Erreur lors du chargement de la télémétrie : $error'; + return 'Erreur lors du chargement de la télémétrie : $error'; } @override - String get telemetry_noData => 'Aucune donnée de télémétrie disponible.'; + String get telemetry_noData => 'Aucune donnée de télémétrie disponible.'; @override String telemetry_channelTitle(int channel) { @@ -2457,10 +2455,10 @@ class AppLocalizationsFr extends AppLocalizations { String get telemetry_voltageLabel => 'Tension'; @override - String get telemetry_mcuTemperatureLabel => 'Température du MCU'; + String get telemetry_mcuTemperatureLabel => 'Température du MCU'; @override - String get telemetry_temperatureLabel => 'Température'; + String get telemetry_temperatureLabel => 'Température'; @override String get telemetry_currentLabel => 'Actuellement'; @@ -2482,14 +2480,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Données des voisins reçues'; + String get neighbors_receivedData => 'Données des voisins reçues'; @override - String get neighbors_requestTimedOut => 'Les voisins demandent un délai.'; + String get neighbors_requestTimedOut => 'Les voisins demandent un délai.'; @override String neighbors_errorLoading(String error) { @@ -2497,20 +2495,20 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get neighbors_repeatersNeighbors => 'Répéteurs Voisins'; + String get neighbors_repeatersNeighbors => 'Répéteurs Voisins'; @override String get neighbors_noData => - 'Aucune donnée concernant les voisins disponible.'; + 'Aucune donnée concernant les voisins disponible.'; @override String neighbors_unknownContact(String pubkey) { - return 'Clé publique inconnue $pubkey'; + return 'Clé publique inconnue $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Écouté : $time auparavant'; + return 'Écouté : $time auparavant'; } @override @@ -2520,26 +2518,26 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_viewMap => 'Afficher la carte'; @override - String get channelPath_otherObservedPaths => 'Autres chemins observés'; + String get channelPath_otherObservedPaths => 'Autres chemins observés'; @override - String get channelPath_repeaterHops => 'Sauts du répéteur'; + String get channelPath_repeaterHops => 'Sauts du répéteur'; @override String get channelPath_noHopDetails => - 'Les détails de l\'envoi ne sont pas fournis pour ce paquet.'; + 'Les détails de l\'envoi ne sont pas fournis pour ce paquet.'; @override - String get channelPath_messageDetails => 'Détails du message'; + String get channelPath_messageDetails => 'Détails du message'; @override - String get channelPath_senderLabel => 'Expéditeur'; + String get channelPath_senderLabel => 'Expéditeur'; @override String get channelPath_timeLabel => 'Temps'; @override - String get channelPath_repeatsLabel => 'Répétitions'; + String get channelPath_repeatsLabel => 'Répétitions'; @override String channelPath_pathLabel(int index) { @@ -2547,15 +2545,15 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get channelPath_observedLabel => 'Observé'; + String get channelPath_observedLabel => 'Observé'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Chemin observé $index • $hops'; + return 'Chemin observé $index • $hops'; } @override - String get channelPath_noLocationData => 'Aucune donnée de localisation'; + String get channelPath_noLocationData => 'Aucune donnée de localisation'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2571,7 +2569,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_unknownPath => 'Inconnu'; @override - String get channelPath_floodPath => 'Tout le réseau'; + String get channelPath_floodPath => 'Tout le réseau'; @override String get channelPath_directPath => 'Afficher'; @@ -2591,7 +2589,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Aucune position de répéteur disponible pour ce chemin.'; + 'Aucune position de répéteur disponible pour ce chemin.'; @override String channelPath_primaryPath(int index) { @@ -2602,43 +2600,43 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_pathLabelTitle => 'Chemin'; @override - String get channelPath_observedPathHeader => 'Chemin observé'; + String get channelPath_observedPathHeader => 'Chemin observé'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Aucun détail de saut disponible pour ce paquet.'; + 'Aucun détail de saut disponible pour ce paquet.'; @override - String get channelPath_unknownRepeater => 'Répéteur Inconnu'; + String get channelPath_unknownRepeater => 'Répéteur Inconnu'; @override - String get community_title => 'Communauté'; + String get community_title => 'Communauté'; @override - String get community_create => 'Créer une Communauté'; + String get community_create => 'Créer une Communauté'; @override String get community_createDesc => - 'Créer une nouvelle communauté et la partager via QR code.'; + 'Créer une nouvelle communauté et la partager via QR code.'; @override String get community_join => 'Rejoindre'; @override - String get community_joinTitle => 'Rejoindre la communauté'; + String get community_joinTitle => 'Rejoindre la communauté'; @override String community_joinConfirmation(String name) { - return 'Souhaitez-vous rejoindre la communauté \"$name\" ?'; + return 'Souhaitez-vous rejoindre la communauté \"$name\" ?'; } @override - String get community_scanQr => 'Scanner la communauté QR'; + String get community_scanQr => 'Scanner la communauté QR'; @override String get community_scanInstructions => @@ -2648,29 +2646,29 @@ class AppLocalizationsFr extends AppLocalizations { String get community_showQr => 'Afficher le QR Code'; @override - String get community_publicChannel => 'Communauté Publique'; + String get community_publicChannel => 'Communauté Publique'; @override - String get community_hashtagChannel => 'Hashtag Communauté'; + String get community_hashtagChannel => 'Hashtag Communauté'; @override - String get community_name => 'Nom de la communauté'; + String get community_name => 'Nom de la communauté'; @override - String get community_enterName => 'Entrez le nom de la communauté'; + String get community_enterName => 'Entrez le nom de la communauté'; @override String community_created(String name) { - return 'Communauté \"$name\" créée'; + return 'Communauté \"$name\" créée'; } @override String community_joined(String name) { - return 'Rejoint la communauté \"$name\"'; + return 'Rejoint la communauté \"$name\"'; } @override - String get community_qrTitle => 'Partager Communauté'; + String get community_qrTitle => 'Partager Communauté'; @override String community_qrInstructions(String name) { @@ -2679,40 +2677,40 @@ class AppLocalizationsFr extends AppLocalizations { @override String get community_hashtagPrivacyHint => - 'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté'; + 'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté'; @override - String get community_invalidQrCode => 'Code QR de communauté non valide'; + String get community_invalidQrCode => 'Code QR de communauté non valide'; @override - String get community_alreadyMember => 'Déjà membre'; + String get community_alreadyMember => 'Déjà membre'; @override String community_alreadyMemberMessage(String name) { - return 'Vous êtes déjà membre de \"$name\".'; + return 'Vous êtes déjà membre de \"$name\".'; } @override String get community_addPublicChannel => - 'Ajouter un Canal Public de la Communauté'; + 'Ajouter un Canal Public de la Communauté'; @override String get community_addPublicChannelHint => - 'Ajouter automatiquement le canal public pour cette communauté'; + 'Ajouter automatiquement le canal public pour cette communauté'; @override String get community_noCommunities => - 'Aucun groupe n\'a été rejoint pour le moment.'; + 'Aucun groupe n\'a été rejoint pour le moment.'; @override String get community_scanOrCreate => - 'Scanner un code QR ou créer une communauté pour commencer'; + 'Scanner un code QR ou créer une communauté pour commencer'; @override - String get community_manageCommunities => 'Gérer les Communautés'; + String get community_manageCommunities => 'Gérer les Communautés'; @override - String get community_delete => 'Quitter la communauté'; + String get community_delete => 'Quitter la communauté'; @override String community_deleteConfirm(String name) { @@ -2721,66 +2719,66 @@ class AppLocalizationsFr extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Cela supprimera également $count canal/canaux et leurs messages.'; + return 'Cela supprimera également $count canal/canaux et leurs messages.'; } @override String community_deleted(String name) { - return 'Communauté \"$name\" quittée'; + return 'Communauté \"$name\" quittée'; } @override - String get community_regenerateSecret => 'Régénérer le secret'; + String get community_regenerateSecret => 'Régénérer le secret'; @override String community_regenerateSecretConfirm(String name) { - return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.'; + return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.'; } @override - String get community_regenerate => 'Régénérer'; + String get community_regenerate => 'Régénérer'; @override String community_secretRegenerated(String name) { - return 'Mot de passe secret régénéré pour \"$name\"'; + return 'Mot de passe secret régénéré pour \"$name\"'; } @override - String get community_updateSecret => 'Mettre à jour le secret'; + String get community_updateSecret => 'Mettre à jour le secret'; @override String community_secretUpdated(String name) { - return 'Modification secrète mise à jour pour \"$name\"'; + return 'Modification secrète mise à jour pour \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"'; + return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"'; } @override - String get community_addHashtagChannel => 'Ajouter un Hashtag Communauté'; + String get community_addHashtagChannel => 'Ajouter un Hashtag Communauté'; @override String get community_addHashtagChannelDesc => - 'Ajouter un canal hashtag pour cette communauté'; + 'Ajouter un canal hashtag pour cette communauté'; @override - String get community_selectCommunity => 'Sélectionner Communauté'; + String get community_selectCommunity => 'Sélectionner Communauté'; @override - String get community_regularHashtag => 'Hashtag régulier'; + String get community_regularHashtag => 'Hashtag régulier'; @override String get community_regularHashtagDesc => 'Hashtag public (tout le monde peut rejoindre)'; @override - String get community_communityHashtag => 'Hashtag de la communauté'; + String get community_communityHashtag => 'Hashtag de la communauté'; @override String get community_communityHashtagDesc => - 'Exclusif aux membres de la communauté'; + 'Exclusif aux membres de la communauté'; @override String community_forCommunity(String name) { @@ -2797,10 +2795,10 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_latestMessages => 'Derniers messages'; @override - String get listFilter_heardRecently => 'Écoute récemment'; + String get listFilter_heardRecently => 'Écoute récemment'; @override - String get listFilter_az => 'A à Z'; + String get listFilter_az => 'A à Z'; @override String get listFilter_filters => 'Filtres'; @@ -2809,10 +2807,10 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_all => 'Tout'; @override - String get listFilter_favorites => 'Préférences'; + String get listFilter_favorites => 'Préférences'; @override - String get listFilter_addToFavorites => 'Ajouter à mes favoris'; + String get listFilter_addToFavorites => 'Ajouter à mes favoris'; @override String get listFilter_removeFromFavorites => 'Supprimer des favoris'; @@ -2821,7 +2819,7 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_users => 'Utilisateurs'; @override - String get listFilter_repeaters => 'Répéteurs'; + String get listFilter_repeaters => 'Répéteurs'; @override String get listFilter_roomServers => 'Room servers'; @@ -2836,10 +2834,10 @@ class AppLocalizationsFr extends AppLocalizations { String get pathTrace_you => 'Vous'; @override - String get pathTrace_failed => 'Traçage du chemin échoué.'; + String get pathTrace_failed => 'Traçage du chemin échoué.'; @override - String get pathTrace_notAvailable => 'Tracé de chemin non disponible.'; + String get pathTrace_notAvailable => 'Tracé de chemin non disponible.'; @override String get pathTrace_refreshTooltip => 'Actualiser Path Trace'; @@ -2853,11 +2851,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losSelectStartEnd => - 'Sélectionnez les nœuds de début et de fin pour LOS.'; + 'Sélectionnez les nÅ“uds de début et de fin pour LOS.'; @override String losRunFailed(String error) { - return 'Échec de la vérification de la ligne de vue : $error'; + return 'Échec de la vérification de la ligne de vue : $error'; } @override @@ -2865,24 +2863,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losRunToViewElevationProfile => - 'Exécutez LOS pour afficher le profil d\'altitude'; + 'Exécutez LOS pour afficher le profil d\'altitude'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés'; + 'Appuyez sur les nÅ“uds ou appuyez longuement sur la carte pour des points personnalisés'; @override - String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage'; + String get losShowDisplayNodes => 'Afficher les nÅ“uds d\'affichage'; @override - String get losCustomPoints => 'Points personnalisés'; + String get losCustomPoints => 'Points personnalisés'; @override String losCustomPointLabel(int index) { - return 'Personnalisé $index'; + return 'Personnalisé $index'; } @override @@ -2893,19 +2891,19 @@ class AppLocalizationsFr extends AppLocalizations { @override String losAntennaA(String value, String unit) { - return 'Antenne A : $value $unit'; + return 'Antenne A : $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Antenne B : $value $unit'; + return 'Antenne B : $value $unit'; } @override - String get losRun => 'Exécuter la LOS'; + String get losRun => 'Exécuter la LOS'; @override - String get losNoElevationData => 'Aucune donnée d\'altitude'; + String get losNoElevationData => 'Aucune donnée d\'altitude'; @override String losProfileClear( @@ -2924,30 +2922,30 @@ class AppLocalizationsFr extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, bloqué par $obstruction $heightUnit'; + return '$distance $distanceUnit, bloqué par $obstruction $heightUnit'; } @override - String get losStatusChecking => 'LOS : vérification...'; + String get losStatusChecking => 'LOS : vérification...'; @override - String get losStatusNoData => 'LOS : aucune donnée'; + String get losStatusNoData => 'LOS : aucune donnée'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu'; + return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu'; } @override String get losErrorElevationUnavailable => - 'Données d\'altitude indisponibles pour un ou plusieurs échantillons.'; + 'Données d\'altitude indisponibles pour un ou plusieurs échantillons.'; @override String get losErrorInvalidInput => - 'Données de points/d\'altitude non valides pour le calcul de la LOS.'; + 'Données de points/d\'altitude non valides pour le calcul de la LOS.'; @override - String get losRenameCustomPoint => 'Renommer le point personnalisé'; + String get losRenameCustomPoint => 'Renommer le point personnalisé'; @override String get losPointName => 'Nom du point'; @@ -2960,25 +2958,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losElevationAttribution => - 'Données d’altitude : Open-Meteo (CC BY 4.0)'; + 'Données d’altitude : Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Horizon radio'; @override - String get losLegendLosBeam => 'Ligne de visée'; + String get losLegendLosBeam => 'Ligne de visée'; @override String get losLegendTerrain => 'Terrain'; @override - String get losFrequencyLabel => 'Fréquence'; + String get losFrequencyLabel => 'Fréquence'; @override - String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; + String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; @override - String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; + String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; @override String losFrequencyDialogDescription( @@ -2987,24 +2985,25 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; + return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; } @override - String get contacts_pathTrace => 'Traçage de chemin'; + String get contacts_pathTrace => 'Traçage de chemin'; @override String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => 'Tracer le chemin vers le répéteur'; + String get contacts_repeaterPathTrace => + 'Tracer le chemin vers le répéteur'; @override - String get contacts_repeaterPing => 'Pinguer le répéteur'; + String get contacts_repeaterPing => 'Pinguer le répéteur'; @override String get contacts_roomPathTrace => - 'Traçage du chemin vers le serveur de la salle'; + 'Traçage du chemin vers le serveur de la salle'; @override String get contacts_roomPing => 'Pinguer le serveur de la salle'; @@ -3014,27 +3013,27 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_pathTraceTo(String name) { - return 'Tracer l\'itinéraire vers $name'; + return 'Tracer l\'itinéraire vers $name'; } @override String get contacts_clipboardEmpty => 'Le presse-papiers est vide.'; @override - String get contacts_invalidAdvertFormat => 'Données de contact non valides'; + String get contacts_invalidAdvertFormat => 'Données de contact non valides'; @override - String get contacts_contactImported => 'Le contact a été importé.'; + String get contacts_contactImported => 'Le contact a été importé.'; @override String get contacts_contactImportFailed => - 'Échec de l\'importation du contact.'; + 'Échec de l\'importation du contact.'; @override String get contacts_zeroHopAdvert => 'Annonce Zero saut'; @override - String get contacts_floodAdvert => 'Annonce à tout le réseau'; + String get contacts_floodAdvert => 'Annonce à tout le réseau'; @override String get contacts_copyAdvertToClipboard => @@ -3057,18 +3056,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_zeroHopContactAdvertFailed => - 'Échec de l\'envoi du contact.'; + 'Échec de l\'envoi du contact.'; @override String get contacts_contactAdvertCopied => - 'Annonce copiée dans le presse-papiers.'; + 'Annonce copiée dans le presse-papiers.'; @override String get contacts_contactAdvertCopyFailed => - 'La copie de l\'annonce vers le presse-papiers a échoué.'; + 'La copie de l\'annonce vers le presse-papiers a échoué.'; @override - String get notification_activityTitle => 'Activité MeshCore'; + String get notification_activityTitle => 'Activité MeshCore'; @override String notification_messagesCount(int count) { @@ -3097,27 +3096,27 @@ class AppLocalizationsFr extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'nouveaux nœuds', - one: 'nouveau nœud', + other: 'nouveaux nÅ“uds', + one: 'nouveau nÅ“ud', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Nouveau $contactType découvert'; + return 'Nouveau $contactType découvert'; } @override - String get notification_receivedNewMessage => 'Nouveau message reçu'; + String get notification_receivedNewMessage => 'Nouveau message reçu'; @override String get settings_gpxExportRepeaters => - 'Exporter les répéteurs / serveur de salle au format GPX'; + 'Exporter les répéteurs / serveur de salle au format GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.'; + 'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.'; @override String get settings_gpxExportContacts => @@ -3136,14 +3135,14 @@ class AppLocalizationsFr extends AppLocalizations { 'Exporte tous les contacts avec une localisation vers un fichier GPX.'; @override - String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.'; + String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.'; @override - String get settings_gpxExportNoContacts => 'Aucun contact à exporter.'; + String get settings_gpxExportNoContacts => 'Aucun contact à exporter.'; @override String get settings_gpxExportNotAvailable => - 'Non pris en charge sur votre appareil/Système d\'exploitation'; + 'Non pris en charge sur votre appareil/Système d\'exploitation'; @override String get settings_gpxExportError => @@ -3151,7 +3150,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_gpxExportRepeatersRoom => - 'Emplacements des serveurs de répéteur et de salle'; + 'Emplacements des serveurs de répéteur et de salle'; @override String get settings_gpxExportChat => 'Emplacements des compagnons'; @@ -3162,15 +3161,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_gpxExportShareText => - 'Données de carte exportées à partir de meshcore-open'; + 'Données de carte exportées à partir de meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open exporter les données de carte GPX'; + 'meshcore-open exporter les données de carte GPX'; @override - String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité'; + String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité'; @override - String get snrIndicator_lastSeen => 'Dernière fois vu'; + String get snrIndicator_lastSeen => 'Dernière fois vu'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index aef53e1..128f6e4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -93,7 +93,7 @@ class AppLocalizationsIt extends AppLocalizations { String get common_loading => 'Caricamento...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,14 +108,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => - 'Scegli il metodo di connessione che preferisci.'; - - @override - String get connectionChoiceSubtitle => - 'Seleziona il metodo che preferisci per accedere al tuo dispositivo MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -134,7 +126,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get usbScreenNote => - 'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.'; + 'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.'; @override String get usbScreenEmptyState => @@ -176,7 +168,7 @@ class AppLocalizationsIt extends AppLocalizations { String get scanner_scan => 'Scansiona'; @override - String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.'; + String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.'; @override String get scanner_bluetoothOffMessage => @@ -273,7 +265,7 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_longitude => 'Longitudine'; @override - String get settings_privacyMode => 'Modalità Privacy'; + String get settings_privacyMode => 'Modalità Privacy'; @override String get settings_privacyModeSubtitle => @@ -281,13 +273,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_privacyModeToggle => - 'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.'; + 'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.'; @override - String get settings_privacyModeEnabled => 'Modalità privacy abilitata'; + String get settings_privacyModeEnabled => 'Modalità privacy abilitata'; @override - String get settings_privacyModeDisabled => 'Modalità privacy disabilitata'; + String get settings_privacyModeDisabled => 'Modalità privacy disabilitata'; @override String get settings_actions => 'Azioni'; @@ -425,7 +417,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_clientRepeatFreqWarning => - 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; + 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; @override String settings_error(String message) { @@ -460,10 +452,10 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -472,16 +464,16 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -490,10 +482,10 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russo'; @@ -576,7 +568,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_autoRouteRotationSubtitle => - 'Alterna tra i percorsi migliori e la modalità alluvione'; + 'Alterna tra i percorsi migliori e la modalità alluvione'; @override String get appSettings_autoRouteRotationEnabled => @@ -671,7 +663,7 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_offlineMapCache => 'Cache Mappa Offline'; @override - String get appSettings_unitsTitle => 'Unità'; + String get appSettings_unitsTitle => 'Unità'; @override String get appSettings_unitsMetric => 'Metrico (m/km)'; @@ -790,11 +782,12 @@ class AppLocalizationsIt extends AppLocalizations { String get contacts_groupName => 'Nome gruppo'; @override - String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.'; + String get contacts_groupNameRequired => + 'Il nome del gruppo è obbligatorio.'; @override String contacts_groupAlreadyExists(String name) { - return 'Il gruppo \"$name\" esiste già.'; + return 'Il gruppo \"$name\" esiste già.'; } @override @@ -880,7 +873,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return 'Eliminare \"$name\"? Non può essere annullato.'; + return 'Eliminare \"$name\"? Non può essere annullato.'; } @override @@ -977,20 +970,20 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channels_joinPublicChannelDesc => - 'Chiunque può unirsi a questo canale.'; + 'Chiunque può unirsi a questo canale.'; @override String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag'; @override String get channels_joinHashtagChannelDesc => - 'Chiunque può unirsi ai canali hashtag.'; + 'Chiunque può unirsi ai canali hashtag.'; @override String get channels_scanQrCode => 'Scansiona un codice QR'; @override - String get channels_scanQrCodeComingSoon => 'Arriverà presto'; + String get channels_scanQrCodeComingSoon => 'Arriverà presto'; @override String get channels_enterHashtag => 'Inserisci hashtag'; @@ -1124,7 +1117,7 @@ class AppLocalizationsIt extends AppLocalizations { String get debugLog_rawLogRx => 'Log Raw-RX'; @override - String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.'; + String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.'; @override String debugFrame_length(int count) { @@ -1180,20 +1173,20 @@ class AppLocalizationsIt extends AppLocalizations { String get chat_ShowAllPaths => 'Mostra tutti i percorsi'; @override - String get chat_routingMode => 'Modalità di routing'; + String get chat_routingMode => 'Modalità di routing'; @override String get chat_autoUseSavedPath => 'Utilizza il percorso salvato'; @override - String get chat_forceFloodMode => 'Modalità Inondamento Forzato'; + String get chat_forceFloodMode => 'Modalità Inondamento Forzato'; @override String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):'; @override String get chat_pathHistoryFull => - 'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.'; + 'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.'; @override String get chat_hopSingular => 'salta'; @@ -1220,7 +1213,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.'; + 'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.'; @override String get chat_pathActions => 'Azioni Percorso:'; @@ -1241,7 +1234,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_pathCleared => - 'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; + 'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; @override String get chat_floodModeSubtitle => @@ -1249,7 +1242,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.'; + 'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.'; @override String get chat_fullPath => 'Percorso Completo'; @@ -1424,7 +1417,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.'; + return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.'; } @override @@ -1642,7 +1635,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'La password verrà memorizzata in modo sicuro su questo dispositivo.'; + 'La password verrà memorizzata in modo sicuro su questo dispositivo.'; @override String get login_repeaterDescription => @@ -1656,13 +1649,13 @@ class AppLocalizationsIt extends AppLocalizations { String get login_routing => 'Instradamento'; @override - String get login_routingMode => 'Modalità di routing'; + String get login_routingMode => 'Modalità di routing'; @override String get login_autoUseSavedPath => 'Utilizza il percorso salvato'; @override - String get login_forceFloodMode => 'Modalità Inondamento Forzato'; + String get login_forceFloodMode => 'Modalità Inondamento Forzato'; @override String get login_managePaths => 'Gestisci Percorsi'; @@ -1682,7 +1675,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get login_failedMessage => - 'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.'; + 'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.'; @override String get common_reload => 'Ricaricare'; @@ -1725,7 +1718,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get path_helperMaxHops => - 'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)'; + 'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)'; @override String get path_selectFromContacts => 'Seleziona da contatti:'; @@ -1745,7 +1738,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get path_tooLong => - 'Il percorso è troppo lungo. Massimo 64 salti consentiti.'; + 'Il percorso è troppo lungo. Massimo 64 salti consentiti.'; @override String get path_setPath => 'Imposta Percorso'; @@ -1797,13 +1790,13 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_statusTitle => 'Stato del Ripetitore'; @override - String get repeater_routingMode => 'Modalità di routing'; + String get repeater_routingMode => 'Modalità di routing'; @override String get repeater_autoUseSavedPath => 'Percorso salvato automatico'; @override - String get repeater_forceFloodMode => 'Modalità Inondamento Forzato'; + String get repeater_forceFloodMode => 'Modalità Inondamento Forzato'; @override String get repeater_pathManagement => 'Gestione dei percorsi'; @@ -1829,7 +1822,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_clockAtLogin => 'Orologio (all\'accesso)'; @override - String get repeater_uptime => 'Disponibilità'; + String get repeater_uptime => 'Disponibilità'; @override String get repeater_queueLength => 'Lunghezza della coda'; @@ -1981,7 +1974,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Consenti l\'accesso ospite in sola lettura'; @override - String get repeater_privacyMode => 'Modalità Privacy'; + String get repeater_privacyMode => 'Modalità Privacy'; @override String get repeater_privacyModeSubtitle => @@ -1991,7 +1984,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_advertisementSettings => 'Impostazioni Annuncio'; @override - String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale'; + String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2000,7 +1993,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervallo Pubblicità Inondazione'; + 'Intervallo Pubblicità Inondazione'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2026,7 +2019,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Sei sicuro di voler riavviare questo ripetitore?'; @override - String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità'; + String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità'; @override String get repeater_regenerateIdentityKeySubtitle => @@ -2034,7 +2027,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'Questo genererà una nuova identità per il ripetitore. Procedere?'; + 'Questo genererà una nuova identità per il ripetitore. Procedere?'; @override String get repeater_eraseFileSystem => 'Elimina File System'; @@ -2045,11 +2038,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!'; + 'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!'; @override String get repeater_eraseSerialOnly => - 'Elimina è disponibile solo tramite console seriale.'; + 'Elimina è disponibile solo tramite console seriale.'; @override String repeater_commandSent(String command) { @@ -2093,7 +2086,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_refreshGuestAccess => 'Aggiorna Accesso Ospite'; @override - String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy'; + String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy'; @override String get repeater_refreshAdvertisementSettings => @@ -2174,7 +2167,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpReboot => - 'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)'; + 'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)'; @override String get repeater_cliHelpClock => @@ -2206,7 +2199,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetAllowReadOnly => - '(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).'; + '(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).'; @override String get repeater_cliHelpSetFloodMax => @@ -2214,7 +2207,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetIntThresh => - 'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.'; + 'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.'; @override String get repeater_cliHelpSetAgcResetInterval => @@ -2226,7 +2219,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.'; + 'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.'; @override String get repeater_cliHelpSetFloodAdvertInterval => @@ -2257,11 +2250,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetTxDelay => - 'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).'; + 'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.'; + 'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.'; @override String get repeater_cliHelpSetBridgeEnabled => 'Abilita/Disabilita ponte.'; @@ -2272,11 +2265,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetBridgeSource => - 'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.'; + 'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Imposta la velocità di trasmissione per i ponti rs232.'; + 'Imposta la velocità di trasmissione per i ponti rs232.'; @override String get repeater_cliHelpSetBridgeSecret => @@ -2292,7 +2285,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetPerm => - 'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)'; + 'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => @@ -2312,7 +2305,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4'; + 'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4'; @override String get repeater_cliHelpNeighborRemove => @@ -2324,7 +2317,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.'; + 'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.'; @override String get repeater_cliHelpRegionGet => @@ -2344,7 +2337,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpRegionDenyf => - 'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).'; + 'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).'; @override String get repeater_cliHelpRegionHome => @@ -2359,7 +2352,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.'; + 'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.'; @override String get repeater_cliHelpGpsOnOff => @@ -2416,7 +2409,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_gpsNote => - 'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.'; + 'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.'; @override String get telemetry_receivedData => 'Dati Telemetria Ricevuti'; @@ -2469,7 +2462,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2537,7 +2530,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Percorso osservato $index • $hops'; + return 'Percorso osservato $index • $hops'; } @override @@ -2592,7 +2585,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2603,14 +2596,14 @@ class AppLocalizationsIt extends AppLocalizations { String get channelPath_unknownRepeater => 'Ripetitore sconosciuto'; @override - String get community_title => 'Comunità'; + String get community_title => 'Comunità'; @override - String get community_create => 'Crea Comunità'; + String get community_create => 'Crea Comunità'; @override String get community_createDesc => - 'Crea una nuova comunità e condividila tramite codice QR.'; + 'Crea una nuova comunità e condividila tramite codice QR.'; @override String get community_join => 'Unisciti'; @@ -2628,35 +2621,35 @@ class AppLocalizationsIt extends AppLocalizations { @override String get community_scanInstructions => - 'Punta la fotocamera su un codice QR della comunità'; + 'Punta la fotocamera su un codice QR della comunità'; @override String get community_showQr => 'Mostra il codice QR'; @override - String get community_publicChannel => 'Comunità Pubblica'; + String get community_publicChannel => 'Comunità Pubblica'; @override - String get community_hashtagChannel => 'Hashtag della Comunità'; + String get community_hashtagChannel => 'Hashtag della Comunità'; @override - String get community_name => 'Nome della Comunità'; + String get community_name => 'Nome della Comunità'; @override - String get community_enterName => 'Inserisci il nome della comunità'; + String get community_enterName => 'Inserisci il nome della comunità'; @override String community_created(String name) { - return 'Comunità \"$name\" creata'; + return 'Comunità \"$name\" creata'; } @override String community_joined(String name) { - return 'Unito alla comunità \"$name\"'; + return 'Unito alla comunità \"$name\"'; } @override - String get community_qrTitle => 'Condividi Comunità'; + String get community_qrTitle => 'Condividi Comunità'; @override String community_qrInstructions(String name) { @@ -2671,16 +2664,16 @@ class AppLocalizationsIt extends AppLocalizations { String get community_invalidQrCode => 'Codice QR della community non valido'; @override - String get community_alreadyMember => 'Già membro'; + String get community_alreadyMember => 'Già membro'; @override String community_alreadyMemberMessage(String name) { - return 'Sei già un membro di \"$name\".'; + return 'Sei già un membro di \"$name\".'; } @override String get community_addPublicChannel => - 'Aggiungi Canale Pubblico della Comunità'; + 'Aggiungi Canale Pubblico della Comunità'; @override String get community_addPublicChannelHint => @@ -2694,10 +2687,10 @@ class AppLocalizationsIt extends AppLocalizations { 'Scansiona un codice QR o crea una community per iniziare.'; @override - String get community_manageCommunities => 'Gestisci Comunità'; + String get community_manageCommunities => 'Gestisci Comunità'; @override - String get community_delete => 'Lascia la Comunità'; + String get community_delete => 'Lascia la Comunità'; @override String community_deleteConfirm(String name) { @@ -2706,12 +2699,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Questo eliminerà anche $count canale/i e i loro messaggi.'; + return 'Questo eliminerà anche $count canale/i e i loro messaggi.'; } @override String community_deleted(String name) { - return 'Hai lasciato la comunità \"$name\"'; + return 'Hai lasciato la comunità \"$name\"'; } @override @@ -2751,21 +2744,21 @@ class AppLocalizationsIt extends AppLocalizations { 'Aggiungi un canale con hashtag per questa community'; @override - String get community_selectCommunity => 'Seleziona Comunità'; + String get community_selectCommunity => 'Seleziona Comunità'; @override String get community_regularHashtag => 'Hashtag regolare'; @override String get community_regularHashtagDesc => - 'Hashtag pubblico (chiunque può unirsi)'; + 'Hashtag pubblico (chiunque può unirsi)'; @override - String get community_communityHashtag => 'Hashtag della Comunità'; + String get community_communityHashtag => 'Hashtag della Comunità'; @override String get community_communityHashtagDesc => - 'Visibile solo ai membri della comunità'; + 'Visibile solo ai membri della comunità'; @override String community_forCommunity(String name) { @@ -2832,7 +2825,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => - 'Uno o più dei luppoli mancano di una posizione!'; + 'Uno o più dei luppoli mancano di una posizione!'; @override String get pathTrace_clearTooltip => 'Pulisci percorso'; @@ -2854,7 +2847,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Eseguire LOS per visualizzare il profilo altimetrico'; @override - String get losMenuTitle => 'Menù LOS'; + String get losMenuTitle => 'Menù LOS'; @override String get losMenuSubtitle => @@ -2926,7 +2919,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Dati di elevazione non disponibili per uno o più campioni.'; + 'Dati di elevazione non disponibili per uno o più campioni.'; @override String get losErrorInvalidInput => @@ -2964,7 +2957,7 @@ class AppLocalizationsIt extends AppLocalizations { String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; @override - String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; + String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; @override String losFrequencyDialogDescription( @@ -3004,13 +2997,13 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'La clipboard è vuota.'; + String get contacts_clipboardEmpty => 'La clipboard è vuota.'; @override String get contacts_invalidAdvertFormat => 'Dati di contatto non validi'; @override - String get contacts_contactImported => 'Il contatto è stato importato.'; + String get contacts_contactImported => 'Il contatto è stato importato.'; @override String get contacts_contactImportFailed => @@ -3052,7 +3045,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Copia dell\'annuncio nella Clipboard non riuscita.'; @override - String get notification_activityTitle => 'Attività MeshCore'; + String get notification_activityTitle => 'Attività MeshCore'; @override String notification_messagesCount(int count) { @@ -3130,7 +3123,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_gpxExportError => - 'Si è verificato un errore durante l\'esportazione.'; + 'Si è verificato un errore durante l\'esportazione.'; @override String get settings_gpxExportRepeatersRoom => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index b1c3452..21d2fd4 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -69,7 +69,7 @@ class AppLocalizationsNl extends AppLocalizations { String get common_share => 'Delen'; @override - String get common_copy => 'Kopiëren'; + String get common_copy => 'Kopiëren'; @override String get common_retry => 'Nogmaals proberen'; @@ -93,7 +93,7 @@ class AppLocalizationsNl extends AppLocalizations { String get common_loading => 'Laden...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Kies uw verbindingsmethode'; - - @override - String get connectionChoiceSubtitle => - 'Kies hoe u uw MeshCore-apparaat wilt bereiken.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -126,7 +119,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get usbScreenSubtitle => - 'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; + 'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; @override String get usbScreenStatus => 'Selecteer een USB-apparaat'; @@ -238,7 +231,7 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_location => 'Locatie'; @override - String get settings_locationSubtitle => 'GPS coördinaten'; + String get settings_locationSubtitle => 'GPS coördinaten'; @override String get settings_locationUpdated => 'Locatie bijgewerkt'; @@ -457,10 +450,10 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -469,16 +462,16 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -487,16 +480,16 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russisch'; @override - String get appSettings_languageUk => 'Oekraïens'; + String get appSettings_languageUk => 'Oekraïens'; @override String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; @@ -854,7 +847,7 @@ class AppLocalizationsNl extends AppLocalizations { String get channels_public => 'Openbaar'; @override - String get channels_private => 'Privé'; + String get channels_private => 'Privé'; @override String get channels_publicChannel => 'Open kanaal'; @@ -954,14 +947,14 @@ class AppLocalizationsNl extends AppLocalizations { String get channels_sortUnread => 'Ongelezen'; @override - String get channels_createPrivateChannel => 'Maak een Privé Kanaal'; + String get channels_createPrivateChannel => 'Maak een Privé Kanaal'; @override String get channels_createPrivateChannelDesc => 'Beveiligd met een geheime sleutel.'; @override - String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan'; + String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan'; @override String get channels_joinPrivateChannelDesc => @@ -1343,7 +1336,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_nodesNeedGps => - 'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen'; + 'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen'; @override String map_nodesCount(int count) { @@ -1371,7 +1364,7 @@ class AppLocalizationsNl extends AppLocalizations { String get map_pinDm => 'Verzenden als bericht (DM)'; @override - String get map_pinPrivate => 'Beveiligd (Privé)'; + String get map_pinPrivate => 'Beveiligd (Privé)'; @override String get map_pinPublic => 'Openbaar spikken'; @@ -2038,7 +2031,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_eraseSerialOnly => - 'Verwijderen is alleen beschikbaar via de seriële console.'; + 'Verwijderen is alleen beschikbaar via de seriële console.'; @override String repeater_commandSent(String command) { @@ -2266,7 +2259,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpSetBridgeBaud => - 'Stel de seriële link baudrate in voor rs232 bruggen.'; + 'Stel de seriële link baudrate in voor rs232 bruggen.'; @override String get repeater_cliHelpSetBridgeSecret => @@ -2282,7 +2275,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpSetPerm => - 'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)'; + 'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => @@ -2314,7 +2307,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.'; + 'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.'; @override String get repeater_cliHelpRegionGet => @@ -2359,7 +2352,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpGpsSetLoc => - 'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.'; + 'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.'; @override String get repeater_cliHelpGpsAdvert => @@ -2397,14 +2390,14 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_regionNote => - 'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.'; + 'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.'; @override String get repeater_gpsManagement => 'Beheer GPS'; @override String get repeater_gpsNote => - 'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.'; + 'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.'; @override String get telemetry_receivedData => 'Ontvangen Telemetriedata'; @@ -2457,7 +2450,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2526,7 +2519,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Waargenomen pad $index • $hops'; + return 'Waargenomen pad $index • $hops'; } @override @@ -2581,7 +2574,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2998,11 +2991,11 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens'; @override - String get contacts_contactImported => 'Contact is geïmporteerd.'; + String get contacts_contactImported => 'Contact is geïmporteerd.'; @override String get contacts_contactImportFailed => - 'Contact kon niet geïmporteerd worden.'; + 'Contact kon niet geïmporteerd worden.'; @override String get contacts_zeroHopAdvert => 'Zero Hop Reclame'; @@ -3011,14 +3004,14 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_floodAdvert => 'Overstromingsadvertentie'; @override - String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren'; + String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren'; @override String get contacts_addContactFromClipboard => 'Contact uit klembord toevoegen'; @override - String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren'; + String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren'; @override String get contacts_ShareContactZeroHop => 'Contact delen via advertentie'; @@ -3037,7 +3030,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => - 'Kopiëren van advertentie naar Clipboard is mislukt.'; + 'Kopiëren van advertentie naar Clipboard is mislukt.'; @override String get notification_activityTitle => 'MeshCore Activiteit'; @@ -3106,7 +3099,8 @@ class AppLocalizationsNl extends AppLocalizations { 'Exporteert alle contacten met een locatie naar een GPX-bestand.'; @override - String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.'; + String get settings_gpxExportSuccess => + 'Succesvol GPX-bestand geëxporteerd.'; @override String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.'; @@ -3130,7 +3124,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_gpxExportShareText => - 'Kaartgegevens geëxporteerd uit meshcore-open'; + 'Kaartgegevens geëxporteerd uit meshcore-open'; @override String get settings_gpxExportShareSubject => diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 5b1712f..8f26f1c 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -15,7 +15,7 @@ class AppLocalizationsPl extends AppLocalizations { String get nav_contacts => 'Kontakty'; @override - String get nav_channels => 'Kanały'; + String get nav_channels => 'KanaÅ‚y'; @override String get nav_map => 'Mapa'; @@ -27,19 +27,19 @@ class AppLocalizationsPl extends AppLocalizations { String get common_ok => 'OK'; @override - String get common_connect => 'Połącz'; + String get common_connect => 'Połącz'; @override - String get common_unknownDevice => 'Nieznane urządzenie'; + String get common_unknownDevice => 'Nieznane urzÄ…dzenie'; @override String get common_save => 'Zapisz'; @override - String get common_delete => 'Usuń'; + String get common_delete => 'UsuÅ„'; @override - String get common_close => 'Zamknąć'; + String get common_close => 'Zamknąć'; @override String get common_edit => 'Edytuj'; @@ -51,49 +51,49 @@ class AppLocalizationsPl extends AppLocalizations { String get common_settings => 'Ustawienia'; @override - String get common_disconnect => 'Odłącz'; + String get common_disconnect => 'Odłącz'; @override - String get common_connected => 'Połączono'; + String get common_connected => 'Połączono'; @override - String get common_disconnected => 'Odłączony'; + String get common_disconnected => 'Odłączony'; @override - String get common_create => 'Utwórz'; + String get common_create => 'Utwórz'; @override String get common_continue => 'Kontynuuj'; @override - String get common_share => 'Udostępnij'; + String get common_share => 'UdostÄ™pnij'; @override String get common_copy => 'Kopiuj'; @override - String get common_retry => 'Spróbować'; + String get common_retry => 'Spróbować'; @override String get common_hide => 'Ukryj'; @override - String get common_remove => 'Usuń'; + String get common_remove => 'UsuÅ„'; @override - String get common_enable => 'Włącz'; + String get common_enable => 'Włącz'; @override - String get common_disable => 'Wyłączyć'; + String get common_disable => 'Wyłączyć'; @override - String get common_reboot => 'Zrestartować'; + String get common_reboot => 'Zrestartować'; @override - String get common_loading => 'Ładowanie...'; + String get common_loading => 'Ładowanie...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Wybierz metodę połączenia.'; - - @override - String get connectionChoiceSubtitle => - 'Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -122,50 +115,50 @@ class AppLocalizationsPl extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Połącz przez USB'; + String get usbScreenTitle => 'Połącz przez USB'; @override String get usbScreenSubtitle => - 'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.'; + 'Wybierz wykryty urzÄ…dzenie szeregowe i podłącz je bezpoÅ›rednio do swojego wÄ™zÅ‚a MeshCore.'; @override - String get usbScreenStatus => 'Wybierz urządzenie USB'; + String get usbScreenStatus => 'Wybierz urzÄ…dzenie USB'; @override String get usbScreenNote => - 'Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.'; + 'Port szeregowy USB jest aktywny na urzÄ…dzeniach z Androidem i platformach stacjonarnych, które obsÅ‚ugujÄ… tÄ™ funkcjÄ™.'; @override String get usbScreenEmptyState => - 'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.'; + 'Nie znaleziono żadnych urzÄ…dzeÅ„ USB. Podłącz jedno i zaktualizuj.'; @override - String get scanner_scanning => 'Skanowanie urządzeń...'; + String get scanner_scanning => 'Skanowanie urzÄ…dzeÅ„...'; @override - String get scanner_connecting => 'Łączenie...'; + String get scanner_connecting => 'Łączenie...'; @override - String get scanner_disconnecting => 'Odłączanie...'; + String get scanner_disconnecting => 'Odłączanie...'; @override - String get scanner_notConnected => 'Niepołączony'; + String get scanner_notConnected => 'Niepołączony'; @override String scanner_connectedTo(String deviceName) { - return 'Połączono z $deviceName'; + return 'Połączono z $deviceName'; } @override - String get scanner_searchingDevices => 'Wyszukiwanie urządzeń MeshCore...'; + String get scanner_searchingDevices => 'Wyszukiwanie urzÄ…dzeÅ„ MeshCore...'; @override String get scanner_tapToScan => - 'Naciśnij Skan, aby znaleźć urządzenia MeshCore'; + 'NaciÅ›nij Skan, aby znaleźć urzÄ…dzenia MeshCore'; @override String scanner_connectionFailed(String error) { - return 'Połączenie nieudane: $error'; + return 'Połączenie nieudane: $error'; } @override @@ -175,21 +168,21 @@ class AppLocalizationsPl extends AppLocalizations { String get scanner_scan => 'Przeskanuj'; @override - String get scanner_bluetoothOff => 'Bluetooth jest wyłączony'; + String get scanner_bluetoothOff => 'Bluetooth jest wyłączony'; @override String get scanner_bluetoothOffMessage => - 'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.'; + 'Prosimy włączyć Bluetooth, aby przeskanować urzÄ…dzenia.'; @override - String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome'; + String get scanner_chromeRequired => 'Wymagana przeglÄ…darka Chrome'; @override String get scanner_chromeRequiredMessage => - 'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.'; + 'Ta aplikacja internetowa wymaga przeglÄ…darki Google Chrome lub opartej na Chromium do obsÅ‚ugi Bluetooth.'; @override - String get scanner_enableBluetooth => 'Włącz Bluetooth'; + String get scanner_enableBluetooth => 'Włącz Bluetooth'; @override String get device_quickSwitch => 'Szybka zmiana'; @@ -201,40 +194,40 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_title => 'Ustawienia'; @override - String get settings_deviceInfo => 'Informacje o urządzeniu'; + String get settings_deviceInfo => 'Informacje o urzÄ…dzeniu'; @override String get settings_appSettings => 'Ustawienia aplikacji'; @override String get settings_appSettingsSubtitle => - 'Powiadomienia, wiadomości i preferencje mapy'; + 'Powiadomienia, wiadomoÅ›ci i preferencje mapy'; @override - String get settings_nodeSettings => 'Ustawienia węzła'; + String get settings_nodeSettings => 'Ustawienia wÄ™zÅ‚a'; @override - String get settings_nodeName => 'Nazwa węzła'; + String get settings_nodeName => 'Nazwa wÄ™zÅ‚a'; @override String get settings_nodeNameNotSet => 'Nie ustawione'; @override - String get settings_nodeNameHint => 'Wprowadź nazwę węzła'; + String get settings_nodeNameHint => 'Wprowadź nazwÄ™ wÄ™zÅ‚a'; @override - String get settings_nodeNameUpdated => 'Imię zaktualizowane'; + String get settings_nodeNameUpdated => 'ImiÄ™ zaktualizowane'; @override String get settings_radioSettings => 'Ustawienia radia'; @override String get settings_radioSettingsSubtitle => - 'Częstotliwość, moc, współczynnik rozpraszania'; + 'CzÄ™stotliwość, moc, współczynnik rozpraszania'; @override String get settings_radioSettingsUpdated => - 'Ustawienia radia zostały zaktualizowane'; + 'Ustawienia radia zostaÅ‚y zaktualizowane'; @override String get settings_location => 'Lokalizacja'; @@ -247,94 +240,94 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_locationBothRequired => - 'Wprowadź zarówno szerokość, jak i długość geograficzną.'; + 'Wprowadź zarówno szerokość, jak i dÅ‚ugość geograficznÄ….'; @override String get settings_locationInvalid => - 'Nieprawidłowa szerokość geograficzna lub długość geograficzna.'; + 'NieprawidÅ‚owa szerokość geograficzna lub dÅ‚ugość geograficzna.'; @override - String get settings_locationGPSEnable => 'Włącz GPS'; + String get settings_locationGPSEnable => 'Włącz GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.'; + 'Włącza automatyczne aktualizowanie pozycji za pomocÄ… GPS.'; @override - String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)'; + String get settings_locationIntervalSec => 'InterwaÅ‚ dla GPS (Sekundy)'; @override String get settings_locationIntervalInvalid => - 'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.'; + 'InterwaÅ‚ musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.'; @override - String get settings_latitude => 'Szerokość'; + String get settings_latitude => 'Szerokość'; @override - String get settings_longitude => 'Długość'; + String get settings_longitude => 'DÅ‚ugość'; @override String get settings_privacyMode => 'Tryb Prywatny'; @override String get settings_privacyModeSubtitle => - 'Ukryj imię/lokalizację w reklamach'; + 'Ukryj imiÄ™/lokalizacjÄ™ w reklamach'; @override String get settings_privacyModeToggle => - 'Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w reklamach.'; + 'Włącz tryb prywatnoÅ›ci, aby ukryć swoje imiÄ™ i lokalizacjÄ™ w reklamach.'; @override - String get settings_privacyModeEnabled => 'Tryb prywatności włączony'; + String get settings_privacyModeEnabled => 'Tryb prywatnoÅ›ci włączony'; @override - String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony'; + String get settings_privacyModeDisabled => 'Tryb prywatnoÅ›ci wyłączony'; @override - String get settings_actions => 'Działania'; + String get settings_actions => 'DziaÅ‚ania'; @override - String get settings_sendAdvertisement => 'Wyślij Reklamę'; + String get settings_sendAdvertisement => 'WyÅ›lij ReklamÄ™'; @override String get settings_sendAdvertisementSubtitle => - 'Obecność transmisji jest teraz'; + 'Obecność transmisji jest teraz'; @override - String get settings_advertisementSent => 'Reklama wysłana'; + String get settings_advertisementSent => 'Reklama wysÅ‚ana'; @override String get settings_syncTime => 'Czas synchronizacji'; @override String get settings_syncTimeSubtitle => - 'Ustaw zegar urządzenia na czas telefonu.'; + 'Ustaw zegar urzÄ…dzenia na czas telefonu.'; @override String get settings_timeSynchronized => 'Synchronizacja czasu'; @override - String get settings_refreshContacts => 'Odśwież Kontakty'; + String get settings_refreshContacts => 'OdÅ›wież Kontakty'; @override String get settings_refreshContactsSubtitle => - 'Odśwież listę kontaktów z urządzenia'; + 'OdÅ›wież listÄ™ kontaktów z urzÄ…dzenia'; @override - String get settings_rebootDevice => 'Zrestartuj Urządzenie'; + String get settings_rebootDevice => 'Zrestartuj UrzÄ…dzenie'; @override - String get settings_rebootDeviceSubtitle => 'Zrestartuj urządzenie MeshCore'; + String get settings_rebootDeviceSubtitle => 'Zrestartuj urzÄ…dzenie MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Czy na pewno chcesz zrestartować urządzenie? Będziesz odłączony.'; + 'Czy na pewno chcesz zrestartować urzÄ…dzenie? BÄ™dziesz odłączony.'; @override String get settings_debug => 'Debug'; @override - String get settings_bleDebugLog => 'Log błędów BLE'; + String get settings_bleDebugLog => 'Log błędów BLE'; @override String get settings_bleDebugLogSubtitle => @@ -359,14 +352,14 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_aboutDescription => - 'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.'; + 'Otwarty kod źródÅ‚owy klient Flutter dla urzÄ…dzeÅ„ do sieci mesh LoRa MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)'; + 'Dane wysokoÅ›ciowe LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'Imię'; + String get settings_infoName => 'ImiÄ™'; @override String get settings_infoId => 'ID'; @@ -381,29 +374,29 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_infoPublicKey => 'Klucz Publiczny'; @override - String get settings_infoContactsCount => 'Liczba kontaktów'; + String get settings_infoContactsCount => 'Liczba kontaktów'; @override - String get settings_infoChannelCount => 'Liczba kanałów'; + String get settings_infoChannelCount => 'Liczba kanałów'; @override String get settings_presets => 'Preset'; @override - String get settings_frequency => 'Częstotliwość (MHz)'; + String get settings_frequency => 'CzÄ™stotliwość (MHz)'; @override String get settings_frequencyHelper => '300,0 - 2500,0'; @override String get settings_frequencyInvalid => - 'Nieprawidłowa częstotliwość (300-2500 MHz)'; + 'NieprawidÅ‚owa czÄ™stotliwość (300-2500 MHz)'; @override - String get settings_bandwidth => 'Przepustowość'; + String get settings_bandwidth => 'Przepustowość'; @override - String get settings_spreadingFactor => 'Rozkład Czynnika'; + String get settings_spreadingFactor => 'RozkÅ‚ad Czynnika'; @override String get settings_codingRate => 'Stawka Kodowania'; @@ -415,35 +408,35 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)'; + String get settings_txPowerInvalid => 'NieprawidÅ‚owa moc TX (0-22 dBm)'; @override - String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci'; + String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci'; @override String get settings_clientRepeatSubtitle => - 'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.'; + 'Pozwól temu urzÄ…dzeniu powtarzać pakiety danych dla innych urzÄ…dzeÅ„.'; @override String get settings_clientRepeatFreqWarning => - 'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.'; + 'Powtórka poza sieciÄ… wymaga czÄ™stotliwoÅ›ci 433, 869 lub 918 MHz.'; @override String settings_error(String message) { - return 'Błąd: $message'; + return 'Błąd: $message'; } @override String get appSettings_title => 'Ustawienia aplikacji'; @override - String get appSettings_appearance => 'Wygląd'; + String get appSettings_appearance => 'WyglÄ…d'; @override String get appSettings_theme => 'Motyw'; @override - String get appSettings_themeSystem => 'Domyślne ustawienia systemu'; + String get appSettings_themeSystem => 'DomyÅ›lne ustawienia systemu'; @override String get appSettings_themeLight => 'Jasne'; @@ -452,19 +445,19 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_themeDark => 'Ciemny'; @override - String get appSettings_language => 'Język'; + String get appSettings_language => 'JÄ™zyk'; @override - String get appSettings_languageSystem => 'Domyślny systemowy'; + String get appSettings_languageSystem => 'DomyÅ›lny systemowy'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -473,16 +466,16 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -491,59 +484,60 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Rosyjski'; @override - String get appSettings_languageUk => 'Ukraińska'; + String get appSettings_languageUk => 'UkraiÅ„ska'; @override - String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości'; + String get appSettings_enableMessageTracing => + 'Włącz Å›ledzenie wiadomoÅ›ci'; @override String get appSettings_enableMessageTracingSubtitle => - 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości'; + 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomoÅ›ci'; @override String get appSettings_notifications => 'Powiadomienia'; @override - String get appSettings_enableNotifications => 'Włącz Powiadomienia'; + String get appSettings_enableNotifications => 'Włącz Powiadomienia'; @override String get appSettings_enableNotificationsSubtitle => - 'Otrzymuj powiadomienia o wiadomościach i reklamach.'; + 'Otrzymuj powiadomienia o wiadomoÅ›ciach i reklamach.'; @override String get appSettings_notificationPermissionDenied => 'Odmowa zezwolenia na powiadomienia'; @override - String get appSettings_notificationsEnabled => 'Powiadomienia włączone'; + String get appSettings_notificationsEnabled => 'Powiadomienia włączone'; @override - String get appSettings_notificationsDisabled => 'Powiadomienia wyłączone'; + String get appSettings_notificationsDisabled => 'Powiadomienia wyłączone'; @override String get appSettings_messageNotifications => - 'Powiadomienia o wiadomościach'; + 'Powiadomienia o wiadomoÅ›ciach'; @override String get appSettings_messageNotificationsSubtitle => - 'Pokaż powiadomienie przy otrzymywaniu nowych wiadomości'; + 'Pokaż powiadomienie przy otrzymywaniu nowych wiadomoÅ›ci'; @override String get appSettings_channelMessageNotifications => - 'Powiadomienia o Wiadomościach na Kanałach'; + 'Powiadomienia o WiadomoÅ›ciach na KanaÅ‚ach'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Pokaż powiadomienie przy odbieraniu wiadomości z kanału'; + 'Pokaż powiadomienie przy odbieraniu wiadomoÅ›ci z kanaÅ‚u'; @override String get appSettings_advertisementNotifications => @@ -551,22 +545,22 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'Wyświetl powiadomienie, gdy zostaną odkryte nowe węzły.'; + 'WyÅ›wietl powiadomienie, gdy zostanÄ… odkryte nowe wÄ™zÅ‚y.'; @override - String get appSettings_messaging => 'Wiadomości'; + String get appSettings_messaging => 'WiadomoÅ›ci'; @override String get appSettings_clearPathOnMaxRetry => - 'Wyczyść Ścieżkę na Maksymalnej Próbie'; + 'Wyczyść ÅšcieżkÄ™ na Maksymalnej Próbie'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Resetuj ścieżkę kontaktu po 5 nieudanych próbach wysłania'; + 'Resetuj Å›cieżkÄ™ kontaktu po 5 nieudanych próbach wysÅ‚ania'; @override String get appSettings_pathsWillBeCleared => - 'Droga będzie wyczyszczona po 5 nieudanych próbach.'; + 'Droga bÄ™dzie wyczyszczona po 5 nieudanych próbach.'; @override String get appSettings_pathsWillNotBeCleared => @@ -577,15 +571,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_autoRouteRotationSubtitle => - 'Przełączaj się między najlepszymi ścieżkami a trybem zalewowym.'; + 'Przełączaj siÄ™ miÄ™dzy najlepszymi Å›cieżkami a trybem zalewowym.'; @override String get appSettings_autoRouteRotationEnabled => - 'Automatyczne obracanie tras włączone'; + 'Automatyczne obracanie tras włączone'; @override String get appSettings_autoRouteRotationDisabled => - 'Automatyczne obracanie tras wyłączone'; + 'Automatyczne obracanie tras wyłączone'; @override String get appSettings_battery => 'Bateria'; @@ -595,12 +589,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Ustawione na urządzenie ($deviceName)'; + return 'Ustawione na urzÄ…dzenie ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Połącz się z urządzeniem, aby wybrać'; + 'Połącz siÄ™ z urzÄ…dzeniem, aby wybrać'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; @@ -612,45 +606,46 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override - String get appSettings_mapDisplay => 'Wyświetlanie mapy'; + String get appSettings_mapDisplay => 'WyÅ›wietlanie mapy'; @override - String get appSettings_showRepeaters => 'Pokaż Powtórniki'; + String get appSettings_showRepeaters => 'Pokaż Powtórniki'; @override String get appSettings_showRepeatersSubtitle => - 'Wyświetl węzły powtarzające się na mapie'; + 'WyÅ›wietl wÄ™zÅ‚y powtarzajÄ…ce siÄ™ na mapie'; @override - String get appSettings_showChatNodes => 'Pokaż Węzły Rozmowy'; + String get appSettings_showChatNodes => 'Pokaż WÄ™zÅ‚y Rozmowy'; @override String get appSettings_showChatNodesSubtitle => - 'Wyświetl węzły czatu na mapie'; + 'WyÅ›wietl wÄ™zÅ‚y czatu na mapie'; @override - String get appSettings_showOtherNodes => 'Pokaż inne węzły'; + String get appSettings_showOtherNodes => 'Pokaż inne wÄ™zÅ‚y'; @override String get appSettings_showOtherNodesSubtitle => - 'Wyświetl inne typy węzłów na mapie'; + 'WyÅ›wietl inne typy wÄ™złów na mapie'; @override String get appSettings_timeFilter => 'Filtrowanie Czasu'; @override - String get appSettings_timeFilterShowAll => 'Pokaż wszystkie węzły'; + String get appSettings_timeFilterShowAll => 'Pokaż wszystkie wÄ™zÅ‚y'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Pokaż węzły z ostatnich $hours godzin'; + return 'Pokaż wÄ™zÅ‚y z ostatnich $hours godzin'; } @override String get appSettings_mapTimeFilter => 'Filtrowanie Czasu Mapy'; @override - String get appSettings_showNodesDiscoveredWithin => 'Pokaż węzły odkryte w:'; + String get appSettings_showNodesDiscoveredWithin => + 'Pokaż wÄ™zÅ‚y odkryte w:'; @override String get appSettings_allTime => 'Wszystko czasowo'; @@ -665,7 +660,7 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_last24Hours => 'Ostatnie 24 godziny'; @override - String get appSettings_lastWeek => 'Tydzień temu'; + String get appSettings_lastWeek => 'TydzieÅ„ temu'; @override String get appSettings_offlineMapCache => 'Bufor Map Offline'; @@ -680,7 +675,8 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_unitsImperial => 'Imperialne (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; + String get appSettings_noAreaSelected => + 'Nie zaznaczono żadnej powierzchni.'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { @@ -695,25 +691,25 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_appDebugLoggingSubtitle => - 'Loguj wiadomości debugowania aplikacji w celu rozwiązywania problemów.'; + 'Loguj wiadomoÅ›ci debugowania aplikacji w celu rozwiÄ…zywania problemów.'; @override String get appSettings_appDebugLoggingEnabled => - 'Zdebugowanie aplikacji włączone'; + 'Zdebugowanie aplikacji włączone'; @override String get appSettings_appDebugLoggingDisabled => - 'Zasubskrybowane logi debugowania aplikacji wyłączone.'; + 'Zasubskrybowane logi debugowania aplikacji wyłączone.'; @override String get contacts_title => 'Kontakty'; @override - String get contacts_noContacts => 'Brak jeszcze kontaktów.'; + String get contacts_noContacts => 'Brak jeszcze kontaktów.'; @override String get contacts_contactsWillAppear => - 'Kontakty będą wyświetlane, gdy urządzenia reklamują się.'; + 'Kontakty bÄ™dÄ… wyÅ›wietlane, gdy urzÄ…dzenia reklamujÄ… siÄ™.'; @override String get contacts_unread => 'Nieprzeczytane'; @@ -733,55 +729,55 @@ class AppLocalizationsPl extends AppLocalizations { @override String contacts_searchUsers(int number, String str) { - return 'Wyszukaj $number$str Użytkowników...'; + return 'Wyszukaj $number$str Użytkowników...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Wyszukaj $number$str powtórników...'; + return 'Wyszukaj $number$str powtórników...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Wyszukaj $number$str serwerów Room...'; + return 'Wyszukaj $number$str serwerów Room...'; } @override - String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów'; + String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów'; @override String get contacts_noContactsFound => - 'Brak znalezionych kontaktów ani grup.'; + 'Brak znalezionych kontaktów ani grup.'; @override - String get contacts_deleteContact => 'Usuń Kontakt'; + String get contacts_deleteContact => 'UsuÅ„ Kontakt'; @override String contacts_removeConfirm(String contactName) { - return 'Usuń $contactName z kontaktów?'; + return 'UsuÅ„ $contactName z kontaktów?'; } @override - String get contacts_manageRepeater => 'Zarządzaj Powtórzami'; + String get contacts_manageRepeater => 'ZarzÄ…dzaj Powtórzami'; @override - String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju'; + String get contacts_manageRoom => 'ZarzÄ…dzaj Serwerem Pokoju'; @override String get contacts_roomLogin => 'Logowanie do pokoju'; @override - String get contacts_openChat => 'Otwórz czat'; + String get contacts_openChat => 'Otwórz czat'; @override - String get contacts_editGroup => 'Edytuj Grupę'; + String get contacts_editGroup => 'Edytuj GrupÄ™'; @override - String get contacts_deleteGroup => 'Usuń Grupę'; + String get contacts_deleteGroup => 'UsuÅ„ GrupÄ™'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Usuń \"$groupName\"?'; + return 'UsuÅ„ \"$groupName\"?'; } @override @@ -795,7 +791,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String contacts_groupAlreadyExists(String name) { - return 'Grupa \"$name\" już istnieje'; + return 'Grupa \"$name\" już istnieje'; } @override @@ -803,57 +799,57 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Brak pasujących kontaktów do Twojego filtra'; + 'Brak pasujÄ…cych kontaktów do Twojego filtra'; @override - String get contacts_noMembers => 'Brak członków'; + String get contacts_noMembers => 'Brak czÅ‚onków'; @override - String get contacts_lastSeenNow => 'Ostatnie połączenie'; + String get contacts_lastSeenNow => 'Ostatnie połączenie'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Ostatnie połączenie $minutes min temu'; + return 'Ostatnie połączenie $minutes min temu'; } @override - String get contacts_lastSeenHourAgo => 'Ostatni raz widziany 1 godzinę temu'; + String get contacts_lastSeenHourAgo => 'Ostatni raz widziany 1 godzinÄ™ temu'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Ostatnie połączenie $hours godzin temu'; + return 'Ostatnie połączenie $hours godzin temu'; } @override - String get contacts_lastSeenDayAgo => 'Ostatni raz widziany 1 dzień temu'; + String get contacts_lastSeenDayAgo => 'Ostatni raz widziany 1 dzieÅ„ temu'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Ostatnie połączenie $days dni temu'; + return 'Ostatnie połączenie $days dni temu'; } @override - String get channels_title => 'Kanały'; + String get channels_title => 'KanaÅ‚y'; @override - String get channels_noChannelsConfigured => 'Brak skonfigurowanych kanałów'; + String get channels_noChannelsConfigured => 'Brak skonfigurowanych kanałów'; @override - String get channels_addPublicChannel => 'Dodaj kanał publiczny'; + String get channels_addPublicChannel => 'Dodaj kanaÅ‚ publiczny'; @override - String get channels_searchChannels => 'Wyszukaj kanały...'; + String get channels_searchChannels => 'Wyszukaj kanaÅ‚y...'; @override - String get channels_noChannelsFound => 'Brak znalezionych kanałów'; + String get channels_noChannelsFound => 'Brak znalezionych kanałów'; @override String channels_channelIndex(int index) { - return 'Kanał $index'; + return 'KanaÅ‚ $index'; } @override - String get channels_hashtagChannel => 'Kanał z hashtagami'; + String get channels_hashtagChannel => 'KanaÅ‚ z hashtagami'; @override String get channels_public => 'Publiczny'; @@ -862,49 +858,49 @@ class AppLocalizationsPl extends AppLocalizations { String get channels_private => 'Prywatne'; @override - String get channels_publicChannel => 'Kanał publiczny'; + String get channels_publicChannel => 'KanaÅ‚ publiczny'; @override - String get channels_privateChannel => 'Prywatny kanał'; + String get channels_privateChannel => 'Prywatny kanaÅ‚'; @override - String get channels_editChannel => 'Edytuj kanał'; + String get channels_editChannel => 'Edytuj kanaÅ‚'; @override - String get channels_muteChannel => 'Wycisz kanał'; + String get channels_muteChannel => 'Wycisz kanaÅ‚'; @override - String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału'; + String get channels_unmuteChannel => 'Wyłącz wyciszenie kanaÅ‚u'; @override - String get channels_deleteChannel => 'Usuń kanał'; + String get channels_deleteChannel => 'UsuÅ„ kanaÅ‚'; @override String channels_deleteChannelConfirm(String name) { - return 'Usuń \"$name\"? Nie można tego cofnąć.'; + return 'UsuÅ„ \"$name\"? Nie można tego cofnąć.'; } @override String channels_channelDeleteFailed(String name) { - return 'Nie udało się usunąć kanału \"$name\"'; + return 'Nie udaÅ‚o siÄ™ usunąć kanaÅ‚u \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Kanał \"$name\" usunięto'; + return 'KanaÅ‚ \"$name\" usuniÄ™to'; } @override - String get channels_addChannel => 'Dodaj Kanał'; + String get channels_addChannel => 'Dodaj KanaÅ‚'; @override - String get channels_channelIndexLabel => 'Indeks kanału'; + String get channels_channelIndexLabel => 'Indeks kanaÅ‚u'; @override - String get channels_channelName => 'Nazwa kanału'; + String get channels_channelName => 'Nazwa kanaÅ‚u'; @override - String get channels_usePublicChannel => 'Użyj kanału publicznego'; + String get channels_usePublicChannel => 'Użyj kanaÅ‚u publicznego'; @override String get channels_standardPublicPsk => 'Standard public PSK'; @@ -916,19 +912,19 @@ class AppLocalizationsPl extends AppLocalizations { String get channels_generateRandomPsk => 'Wygeneruj losowy klucz PSK'; @override - String get channels_enterChannelName => 'Proszę podać nazwę kanału.'; + String get channels_enterChannelName => 'ProszÄ™ podać nazwÄ™ kanaÅ‚u.'; @override - String get channels_pskMustBe32Hex => 'PSK musi mieć 32 znaki szesnastkowe.'; + String get channels_pskMustBe32Hex => 'PSK musi mieć 32 znaki szesnastkowe.'; @override String channels_channelAdded(String name) { - return 'Kanał \"$name\" dodany'; + return 'KanaÅ‚ \"$name\" dodany'; } @override String channels_editChannelTitle(int index) { - return 'Edytuj Kanał $index'; + return 'Edytuj KanaÅ‚ $index'; } @override @@ -936,76 +932,77 @@ class AppLocalizationsPl extends AppLocalizations { @override String channels_channelUpdated(String name) { - return 'Kanał \"$name\" został zaktualizowany'; + return 'KanaÅ‚ \"$name\" zostaÅ‚ zaktualizowany'; } @override - String get channels_publicChannelAdded => 'Kanał publiczny dodany'; + String get channels_publicChannelAdded => 'KanaÅ‚ publiczny dodany'; @override String get channels_sortBy => 'Sortuj po'; @override - String get channels_sortManual => 'Ręczna'; + String get channels_sortManual => 'RÄ™czna'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Najnowsze wiadomości'; + String get channels_sortLatestMessages => 'Najnowsze wiadomoÅ›ci'; @override - String get channels_sortUnread => 'Niezgłoszone'; + String get channels_sortUnread => 'NiezgÅ‚oszone'; @override - String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał'; + String get channels_createPrivateChannel => 'Utwórz Prywatny KanaÅ‚'; @override String get channels_createPrivateChannelDesc => 'Zabezpieczone kluczem szyfrowym.'; @override - String get channels_joinPrivateChannel => 'Dołącz do Prywatnego Kanału'; + String get channels_joinPrivateChannel => 'Dołącz do Prywatnego KanaÅ‚u'; @override - String get channels_joinPrivateChannelDesc => 'Ręcznie wprowadź klucz tajny.'; + String get channels_joinPrivateChannelDesc => + 'RÄ™cznie wprowadź klucz tajny.'; @override - String get channels_joinPublicChannel => 'Dołącz do kanału publicznego.'; + String get channels_joinPublicChannel => 'Dołącz do kanaÅ‚u publicznego.'; @override String get channels_joinPublicChannelDesc => - 'Każdy może dołączyć do tego kanału.'; + 'Każdy może dołączyć do tego kanaÅ‚u.'; @override String get channels_joinHashtagChannel => - 'Dołącz do kanału oznaczanego hashtagiem'; + 'Dołącz do kanaÅ‚u oznaczanego hashtagiem'; @override String get channels_joinHashtagChannelDesc => - 'Każdy może dołączyć do kanałów z hashtagami.'; + 'Każdy może dołączyć do kanałów z hashtagami.'; @override String get channels_scanQrCode => 'Skanuj kod QR'; @override - String get channels_scanQrCodeComingSoon => 'Wkrótce'; + String get channels_scanQrCodeComingSoon => 'Wkrótce'; @override - String get channels_enterHashtag => 'Wprowadź hashtag'; + String get channels_enterHashtag => 'Wprowadź hashtag'; @override - String get channels_hashtagHint => 'np. #zespół'; + String get channels_hashtagHint => 'np. #zespół'; @override - String get chat_noMessages => 'Brak jeszcze wiadomości'; + String get chat_noMessages => 'Brak jeszcze wiadomoÅ›ci'; @override - String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.'; + String get chat_sendMessageToStart => 'WyÅ›lij wiadomość, aby rozpocząć.'; @override String get chat_originalMessageNotFound => - 'Błąd: Nie znaleziono oryginalnego komunikatu'; + 'Błąd: Nie znaleziono oryginalnego komunikatu'; @override String chat_replyingTo(String name) { @@ -1022,39 +1019,39 @@ class AppLocalizationsPl extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'Wyślij wiadomość do $contactName'; + return 'WyÅ›lij wiadomość do $contactName'; } @override - String get chat_typeMessage => 'Wpisz wiadomość...'; + String get chat_typeMessage => 'Wpisz wiadomość...'; @override String chat_messageTooLong(int maxBytes) { - return 'Wiadomość jest za długa (maksymalnie $maxBytes bajtów).'; + return 'Wiadomość jest za dÅ‚uga (maksymalnie $maxBytes bajtów).'; } @override - String get chat_messageCopied => 'Wiadomość skopiowana'; + String get chat_messageCopied => 'Wiadomość skopiowana'; @override - String get chat_messageDeleted => 'Wiadomość usunięta'; + String get chat_messageDeleted => 'Wiadomość usuniÄ™ta'; @override - String get chat_retryingMessage => 'Próba ponowienia'; + String get chat_retryingMessage => 'Próba ponowienia'; @override String chat_retryCount(int current, int max) { - return 'Spróbuj $current/$max'; + return 'Spróbuj $current/$max'; } @override - String get chat_sendGif => 'Wyślij GIF'; + String get chat_sendGif => 'WyÅ›lij GIF'; @override String get chat_reply => 'Odpowiedz'; @override - String get chat_addReaction => 'Dodaj Reakcję'; + String get chat_addReaction => 'Dodaj ReakcjÄ™'; @override String get chat_me => 'Ja'; @@ -1081,28 +1078,28 @@ class AppLocalizationsPl extends AppLocalizations { String get gifPicker_poweredBy => 'Zasilane przez GIPHY'; @override - String get gifPicker_noGifsFound => 'Nie znaleziono GIF-ów'; + String get gifPicker_noGifsFound => 'Nie znaleziono GIF-ów'; @override - String get gifPicker_failedLoad => 'Nie udało się załadować GIF-ów'; + String get gifPicker_failedLoad => 'Nie udaÅ‚o siÄ™ zaÅ‚adować GIF-ów'; @override - String get gifPicker_failedSearch => 'Nie udało się znaleźć GIF-ów'; + String get gifPicker_failedSearch => 'Nie udaÅ‚o siÄ™ znaleźć GIF-ów'; @override - String get gifPicker_noInternet => 'Brak połączenia internetowego'; + String get gifPicker_noInternet => 'Brak połączenia internetowego'; @override String get debugLog_appTitle => 'Log Wykonywania Aplikacji'; @override - String get debugLog_bleTitle => 'Log błędów BLE'; + String get debugLog_bleTitle => 'Log błędów BLE'; @override String get debugLog_copyLog => 'Kopiuj log'; @override - String get debugLog_clearLog => 'Wyczyść dziennik'; + String get debugLog_clearLog => 'Wyczyść dziennik'; @override String get debugLog_copied => 'Skopiowano dziennik debugowania'; @@ -1111,11 +1108,12 @@ class AppLocalizationsPl extends AppLocalizations { String get debugLog_bleCopied => 'Skopiowany log BLE'; @override - String get debugLog_noEntries => 'Nie ma jeszcze żadnych logów debugowania.'; + String get debugLog_noEntries => + 'Nie ma jeszcze żadnych logów debugowania.'; @override String get debugLog_enableInSettings => - 'Włącz logowanie debugowania aplikacji w ustawieniach'; + 'Włącz logowanie debugowania aplikacji w ustawieniach'; @override String get debugLog_frames => 'Ramy'; @@ -1124,11 +1122,11 @@ class AppLocalizationsPl extends AppLocalizations { String get debugLog_rawLogRx => 'Surowe Log-RX'; @override - String get debugLog_noBleActivity => 'Brak aktywności BLE jeszcze.'; + String get debugLog_noBleActivity => 'Brak aktywnoÅ›ci BLE jeszcze.'; @override String debugFrame_length(int count) { - return 'Długość ramy: $count bajtów'; + return 'DÅ‚ugość ramy: $count bajtów'; } @override @@ -1137,7 +1135,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => 'Wiadomość tekstowa:'; + String get debugFrame_textMessageHeader => 'Wiadomość tekstowa:'; @override String debugFrame_destinationPubKey(String pubKey) { @@ -1171,30 +1169,31 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get debugFrame_hexDump => 'Wyjście SzESZCZNULNE:'; + String get debugFrame_hexDump => 'WyjÅ›cie SzESZCZNULNE:'; @override - String get chat_pathManagement => 'Zarządzanie ścieżkami'; + String get chat_pathManagement => 'ZarzÄ…dzanie Å›cieżkami'; @override - String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki'; + String get chat_ShowAllPaths => 'Pokaż wszystkie Å›cieżki'; @override String get chat_routingMode => 'Tryb routingu'; @override - String get chat_autoUseSavedPath => 'Automatyczne (użyj zapisanej ścieżki)'; + String get chat_autoUseSavedPath => + 'Automatyczne (użyj zapisanej Å›cieżki)'; @override String get chat_forceFloodMode => 'Wymusz Tryb Powodowany'; @override String get chat_recentAckPaths => - 'Ostatnie ścieżki ACK (naciśnij, aby użyć):'; + 'Ostatnie Å›cieżki ACK (naciÅ›nij, aby użyć):'; @override String get chat_pathHistoryFull => - 'Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.'; + 'Historia Å›cieżek jest peÅ‚na. UsuÅ„ wpisy, aby dodać nowe.'; @override String get chat_hopSingular => 'Skacz'; @@ -1217,46 +1216,46 @@ class AppLocalizationsPl extends AppLocalizations { String get chat_successes => 'Sukcesy'; @override - String get chat_removePath => 'Usuń ścieżkę'; + String get chat_removePath => 'UsuÅ„ Å›cieżkÄ™'; @override String get chat_noPathHistoryYet => - 'Brak jeszcze historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.'; + 'Brak jeszcze historii Å›cieżek.\nWyÅ›lij wiadomość, aby odkryć Å›cieżki.'; @override - String get chat_pathActions => 'Działania ścieżki:'; + String get chat_pathActions => 'DziaÅ‚ania Å›cieżki:'; @override - String get chat_setCustomPath => 'Ustaw Ścieżkę Dostosowaną'; + String get chat_setCustomPath => 'Ustaw ÅšcieżkÄ™ DostosowanÄ…'; @override - String get chat_setCustomPathSubtitle => 'Ręcznie określ trasę.'; + String get chat_setCustomPathSubtitle => 'RÄ™cznie okreÅ›l trasÄ™.'; @override - String get chat_clearPath => 'Wyczyść Ścieżkę'; + String get chat_clearPath => 'Wyczyść ÅšcieżkÄ™'; @override String get chat_clearPathSubtitle => - 'Zmusz do ponownej identyfikacji przy następnym wysłaniu'; + 'Zmusz do ponownej identyfikacji przy nastÄ™pnym wysÅ‚aniu'; @override String get chat_pathCleared => - 'Ścieżka oczyszczona. Kolejne powiadomienie odnajdzie trasę.'; + 'Åšcieżka oczyszczona. Kolejne powiadomienie odnajdzie trasÄ™.'; @override String get chat_floodModeSubtitle => - 'Użyj przełącznika routingu w pasku narzędzi.'; + 'Użyj przełącznika routingu w pasku narzÄ™dzi.'; @override String get chat_floodModeEnabled => - 'Tryb powodziowy włączony. Włącz ponownie za pomocą ikony routingu w pasku narzędzi.'; + 'Tryb powodziowy włączony. Włącz ponownie za pomocÄ… ikony routingu w pasku narzÄ™dzi.'; @override - String get chat_fullPath => 'Pełna ścieżka'; + String get chat_fullPath => 'PeÅ‚na Å›cieżka'; @override String get chat_pathDetailsNotAvailable => - 'Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.'; + 'Szczegóły Å›cieżki jeszcze niedostÄ™pne. Spróbuj wysÅ‚ać wiadomość, aby odÅ›wieżyć.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1266,77 +1265,78 @@ class AppLocalizationsPl extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Ścieżka ustawiona: $hopCount $_temp0 - $status'; + return 'Åšcieżka ustawiona: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Zapisano lokalnie. Połącz się, aby zsynchronizować.'; + 'Zapisano lokalnie. Połącz siÄ™, aby zsynchronizować.'; @override - String get chat_pathDeviceConfirmed => 'Urządzenie potwierdzone.'; + String get chat_pathDeviceConfirmed => 'UrzÄ…dzenie potwierdzone.'; @override String get chat_pathDeviceNotConfirmed => - 'Urządzenie nie zostało jeszcze potwierdzone.'; + 'UrzÄ…dzenie nie zostaÅ‚o jeszcze potwierdzone.'; @override - String get chat_type => 'Wprowadź'; + String get chat_type => 'Wprowadź'; @override - String get chat_path => 'Ścieżka'; + String get chat_path => 'Åšcieżka'; @override String get chat_publicKey => 'Klucz Publiczny'; @override - String get chat_compressOutgoingMessages => 'Kompresuj wychodzące wiadomości'; + String get chat_compressOutgoingMessages => + 'Kompresuj wychodzÄ…ce wiadomoÅ›ci'; @override - String get chat_floodForced => 'Powodowana Powódź'; + String get chat_floodForced => 'Powodowana Powódź'; @override - String get chat_directForced => 'Bezpośrednio (wymuszono)'; + String get chat_directForced => 'BezpoÅ›rednio (wymuszono)'; @override String chat_hopsForced(int count) { - return '$count skoków (wymuszonych)'; + return '$count skoków (wymuszonych)'; } @override String get chat_floodAuto => 'Powodzie (automatyczne)'; @override - String get chat_direct => 'Bezpośrednio'; + String get chat_direct => 'BezpoÅ›rednio'; @override - String get chat_poiShared => 'Wspólny POI'; + String get chat_poiShared => 'Wspólny POI'; @override String chat_unread(int count) { - return 'Niezgłoszone: $count'; + return 'NiezgÅ‚oszone: $count'; } @override - String get chat_openLink => 'Otworzyć link?'; + String get chat_openLink => 'Otworzyć link?'; @override String get chat_openLinkConfirmation => - 'Czy chcesz otworzyć ten link w przeglądarce?'; + 'Czy chcesz otworzyć ten link w przeglÄ…darce?'; @override - String get chat_open => 'Otwórz'; + String get chat_open => 'Otwórz'; @override String chat_couldNotOpenLink(String url) { - return 'Nie można otworzyć linku: $url'; + return 'Nie można otworzyć linku: $url'; } @override - String get chat_invalidLink => 'Nieprawidłowy format linku'; + String get chat_invalidLink => 'NieprawidÅ‚owy format linku'; @override - String get map_title => 'Mapa węzłów'; + String get map_title => 'Mapa wÄ™złów'; @override String get map_lineOfSight => 'Linia wzroku'; @@ -1345,15 +1345,16 @@ class AppLocalizationsPl extends AppLocalizations { String get map_losScreenTitle => 'Linia wzroku'; @override - String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi'; + String get map_noNodesWithLocation => + 'Brak wÄ™złów z danymi lokalizacyjnymi'; @override String get map_nodesNeedGps => - 'Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.'; + 'WÄ™zÅ‚y muszÄ… udostÄ™pniać swoje współrzÄ™dne GPS,\naby pojawić siÄ™ na mapie.'; @override String map_nodesCount(int count) { - return 'Węzły: $count'; + return 'WÄ™zÅ‚y: $count'; } @override @@ -1365,10 +1366,10 @@ class AppLocalizationsPl extends AppLocalizations { String get map_chat => 'Rozmowa'; @override - String get map_repeater => 'Powtórzacz'; + String get map_repeater => 'Powtórzacz'; @override - String get map_room => 'Pokój'; + String get map_room => 'Pokój'; @override String get map_sensor => 'Czujnik'; @@ -1387,64 +1388,64 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_disconnectConfirm => - 'Czy na pewno chcesz się odłączyć od tego urządzenia?'; + 'Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?'; @override String get map_from => 'Od'; @override - String get map_source => 'Źródło'; + String get map_source => 'ŹródÅ‚o'; @override String get map_flags => 'Flagi'; @override - String get map_shareMarkerHere => 'Udostępnij znacznik tutaj'; + String get map_shareMarkerHere => 'UdostÄ™pnij znacznik tutaj'; @override - String get map_pinLabel => 'Oznacz etykietę'; + String get map_pinLabel => 'Oznacz etykietÄ™'; @override String get map_label => 'Etykieta'; @override - String get map_pointOfInterest => 'Punkt zainteresowań'; + String get map_pointOfInterest => 'Punkt zainteresowaÅ„'; @override - String get map_sendToContact => 'Wyślij do kontaktu'; + String get map_sendToContact => 'WyÅ›lij do kontaktu'; @override - String get map_sendToChannel => 'Wyślij do kanału'; + String get map_sendToChannel => 'WyÅ›lij do kanaÅ‚u'; @override - String get map_noChannelsAvailable => 'Brak dostępnych kanałów'; + String get map_noChannelsAvailable => 'Brak dostÄ™pnych kanałów'; @override - String get map_publicLocationShare => 'Udostępnij lokalizację publicznie'; + String get map_publicLocationShare => 'UdostÄ™pnij lokalizacjÄ™ publicznie'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Wkrótce udostępnisz lokalizację w $channelLabel. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.'; + return 'Wkrótce udostÄ™pnisz lokalizacjÄ™ w $channelLabel. Ten kanaÅ‚ jest publiczny i każdy z PSK może go zobaczyć.'; } @override String get map_connectToShareMarkers => - 'Połącz się z urządzeniem, aby udostępniać znacznik.'; + 'Połącz siÄ™ z urzÄ…dzeniem, aby udostÄ™pniać znacznik.'; @override - String get map_filterNodes => 'Filtruj Węzły'; + String get map_filterNodes => 'Filtruj WÄ™zÅ‚y'; @override - String get map_nodeTypes => 'Typy węzłów'; + String get map_nodeTypes => 'Typy wÄ™złów'; @override - String get map_chatNodes => 'Węzły czatu'; + String get map_chatNodes => 'WÄ™zÅ‚y czatu'; @override String get map_repeaters => 'Powtarzacze'; @override - String get map_otherNodes => 'Inne węzły'; + String get map_otherNodes => 'Inne wÄ™zÅ‚y'; @override String get map_keyPrefix => 'Prefiks klucza'; @@ -1453,13 +1454,13 @@ class AppLocalizationsPl extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtruj po prefiksie klucza'; @override - String get map_publicKeyPrefix => 'Przewód klucza publicznego'; + String get map_publicKeyPrefix => 'Przewód klucza publicznego'; @override String get map_markers => 'Oznaczarki'; @override - String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; + String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; @override String get map_lastSeenTime => 'Ostatni raz widiany'; @@ -1468,40 +1469,40 @@ class AppLocalizationsPl extends AppLocalizations { String get map_sharedPin => 'Podzielony PIN'; @override - String get map_joinRoom => 'Dołącz do pokoju'; + String get map_joinRoom => 'Dołącz do pokoju'; @override - String get map_manageRepeater => 'Zarządzaj Powtórzami'; + String get map_manageRepeater => 'ZarzÄ…dzaj Powtórzami'; @override - String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.'; + String get map_tapToAdd => 'Kliknij na wÄ™zÅ‚y, aby dodać je do Å›cieżki.'; @override - String get map_runTrace => 'Uruchom ślad ścieżki'; + String get map_runTrace => 'Uruchom Å›lad Å›cieżki'; @override - String get map_removeLast => 'Usuń ostatni'; + String get map_removeLast => 'UsuÅ„ ostatni'; @override - String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.'; + String get map_pathTraceCancelled => 'Åšledzenie Å›cieżki anulowano.'; @override String get mapCache_title => 'Bufor Map Offline'; @override String get mapCache_selectAreaFirst => - 'Wybierz obszar do wstępnego pobrania.'; + 'Wybierz obszar do wstÄ™pnego pobrania.'; @override String get mapCache_noTilesToDownload => - 'Brak dostępnych płytek do pobrania dla tego obszaru.'; + 'Brak dostÄ™pnych pÅ‚ytek do pobrania dla tego obszaru.'; @override - String get mapCache_downloadTilesTitle => 'Pobierz płytki'; + String get mapCache_downloadTilesTitle => 'Pobierz pÅ‚ytki'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Pobierz $count płytek do użytku offline?'; + return 'Pobierz $count pÅ‚ytek do użytku offline?'; } @override @@ -1509,41 +1510,41 @@ class AppLocalizationsPl extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return 'Pamiętanych $count płytek'; + return 'PamiÄ™tanych $count pÅ‚ytek'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Pamiętane $downloaded płytki ($failed nieudane)'; + return 'PamiÄ™tane $downloaded pÅ‚ytki ($failed nieudane)'; } @override String get mapCache_clearOfflineCacheTitle => - 'Wyczyść pamięć podręczną offline'; + 'Wyczyść pamięć podrÄ™cznÄ… offline'; @override String get mapCache_clearOfflineCachePrompt => - 'Usuń wszystkie tymczasowe kafelki mapy?'; + 'UsuÅ„ wszystkie tymczasowe kafelki mapy?'; @override String get mapCache_offlineCacheCleared => - 'Pamięć podręczna offline została wyczyszczona'; + 'Pamięć podrÄ™czna offline zostaÅ‚a wyczyszczona'; @override - String get mapCache_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; + String get mapCache_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; @override - String get mapCache_cacheArea => 'Obszar pamięci podręcznej'; + String get mapCache_cacheArea => 'Obszar pamiÄ™ci podrÄ™cznej'; @override - String get mapCache_useCurrentView => 'Użyj aktualnego widoku'; + String get mapCache_useCurrentView => 'Użyj aktualnego widoku'; @override - String get mapCache_zoomRange => 'Zakres powiększenia'; + String get mapCache_zoomRange => 'Zakres powiÄ™kszenia'; @override String mapCache_estimatedTiles(int count) { - return 'Szacunkowa liczba płytek: $count'; + return 'Szacunkowa liczba pÅ‚ytek: $count'; } @override @@ -1555,7 +1556,7 @@ class AppLocalizationsPl extends AppLocalizations { String get mapCache_downloadTilesButton => 'Pobierz Paski'; @override - String get mapCache_clearCacheButton => 'Wyczyść pamięć podręczną'; + String get mapCache_clearCacheButton => 'Wyczyść pamięć podrÄ™cznÄ…'; @override String mapCache_failedDownloads(int count) { @@ -1573,7 +1574,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get time_justNow => 'Właśnie teraz'; + String get time_justNow => 'WÅ‚aÅ›nie teraz'; @override String time_minutesAgo(int minutes) { @@ -1597,19 +1598,19 @@ class AppLocalizationsPl extends AppLocalizations { String get time_hours => 'godziny'; @override - String get time_day => 'dzień'; + String get time_day => 'dzieÅ„'; @override String get time_days => 'dni'; @override - String get time_week => 'tydzień'; + String get time_week => 'tydzieÅ„'; @override String get time_weeks => 'tygodnie'; @override - String get time_month => 'miesiąc'; + String get time_month => 'miesiÄ…c'; @override String get time_months => 'miesiace'; @@ -1621,38 +1622,38 @@ class AppLocalizationsPl extends AppLocalizations { String get time_allTime => 'Wszystko czasowo'; @override - String get dialog_disconnect => 'Odłącz'; + String get dialog_disconnect => 'Odłącz'; @override String get dialog_disconnectConfirm => - 'Czy na pewno chcesz się odłączyć od tego urządzenia?'; + 'Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?'; @override - String get login_repeaterLogin => 'Powtórz Logowanie'; + String get login_repeaterLogin => 'Powtórz Logowanie'; @override String get login_roomLogin => 'Logowanie do pokoju'; @override - String get login_password => 'Hasło'; + String get login_password => 'HasÅ‚o'; @override - String get login_enterPassword => 'Wprowadź hasło'; + String get login_enterPassword => 'Wprowadź hasÅ‚o'; @override - String get login_savePassword => 'Zapisz hasło'; + String get login_savePassword => 'Zapisz hasÅ‚o'; @override String get login_savePasswordSubtitle => - 'Hasło będzie bezpiecznie przechowywane na tym urządzeniu.'; + 'HasÅ‚o bÄ™dzie bezpiecznie przechowywane na tym urzÄ…dzeniu.'; @override String get login_repeaterDescription => - 'Wprowadź hasło do powtarzacza, aby uzyskać dostęp do ustawień i statusu.'; + 'Wprowadź hasÅ‚o do powtarzacza, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.'; @override String get login_roomDescription => - 'Wprowadź hasło do pokoju, aby uzyskać dostęp do ustawień i statusu.'; + 'Wprowadź hasÅ‚o do pokoju, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.'; @override String get login_routing => 'Przekierowanie'; @@ -1661,40 +1662,41 @@ class AppLocalizationsPl extends AppLocalizations { String get login_routingMode => 'Tryb routingu'; @override - String get login_autoUseSavedPath => 'Automatycznie (użyj zapisanej ścieżki)'; + String get login_autoUseSavedPath => + 'Automatycznie (użyj zapisanej Å›cieżki)'; @override String get login_forceFloodMode => 'Wymusz Tryb Powodowany'; @override - String get login_managePaths => 'Zarządzaj Ścieżkami'; + String get login_managePaths => 'ZarzÄ…dzaj Åšcieżkami'; @override - String get login_login => 'Zaloguj się'; + String get login_login => 'Zaloguj siÄ™'; @override String login_attempt(int current, int max) { - return 'Próba $current/$max'; + return 'Próba $current/$max'; } @override String login_failed(String error) { - return 'Zalogowanie się nie powiodło: $error'; + return 'Zalogowanie siÄ™ nie powiodÅ‚o: $error'; } @override String get login_failedMessage => - 'Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.'; + 'Logowanie nie powiodÅ‚o siÄ™. HasÅ‚o jest nieprawidÅ‚owe albo repeater jest nieosiÄ…galny.'; @override - String get common_reload => 'Ponownie załadować'; + String get common_reload => 'Ponownie zaÅ‚adować'; @override - String get common_clear => 'Wyczyść'; + String get common_clear => 'Wyczyść'; @override String path_currentPath(String path) { - return 'Aktualny ścieżka: $path'; + return 'Aktualny Å›cieżka: $path'; } @override @@ -1705,88 +1707,88 @@ class AppLocalizationsPl extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Użyj ścieżki $count $_temp0.'; + return 'Użyj Å›cieżki $count $_temp0.'; } @override - String get path_enterCustomPath => 'Wprowadź własną ścieżkę'; + String get path_enterCustomPath => 'Wprowadź wÅ‚asnÄ… Å›cieżkÄ™'; @override - String get path_currentPathLabel => 'Aktualny ścieżka'; + String get path_currentPathLabel => 'Aktualny Å›cieżka'; @override String get path_hexPrefixInstructions => - 'Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.'; + 'Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (każedy węzeł używa pierwszego bajtu swojego klucza publicznego)'; + 'A1,F2,3C (każedy wÄ™zeÅ‚ używa pierwszego bajtu swojego klucza publicznego)'; @override - String get path_labelHexPrefixes => 'Ścieżka (przesunięcia bitowe)'; + String get path_labelHexPrefixes => 'Åšcieżka (przesuniÄ™cia bitowe)'; @override String get path_helperMaxHops => - 'Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).'; + 'Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).'; @override - String get path_selectFromContacts => 'Albo wybierz z kontaktów:'; + String get path_selectFromContacts => 'Albo wybierz z kontaktów:'; @override String get path_noRepeatersFound => - 'Nie znaleziono repeaterów ani serwerów pokoi.'; + 'Nie znaleziono repeaterów ani serwerów pokoi.'; @override String get path_customPathsRequire => - 'Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.'; + 'Dostosowane Å›cieżki wymagajÄ… poÅ›rednich skoków, które mogÄ… przekazywać wiadomoÅ›ci.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Nieprawidłowe prefiksy szesnastkowe: $prefixes'; + return 'NieprawidÅ‚owe prefiksy szesnastkowe: $prefixes'; } @override String get path_tooLong => - 'Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.'; + 'Åšcieżka jest zbyt dÅ‚uga. Dozwolonych skoków wynosi 64.'; @override - String get path_setPath => 'Ustaw Ścieżkę'; + String get path_setPath => 'Ustaw ÅšcieżkÄ™'; @override - String get repeater_management => 'Zarządzanie Powtórzami'; + String get repeater_management => 'ZarzÄ…dzanie Powtórzami'; @override - String get room_management => 'Zarządzanie Serwerem Pokoju'; + String get room_management => 'ZarzÄ…dzanie Serwerem Pokoju'; @override - String get repeater_managementTools => 'Narzędzia Zarządzania'; + String get repeater_managementTools => 'NarzÄ™dzia ZarzÄ…dzania'; @override String get repeater_status => 'Status'; @override String get repeater_statusSubtitle => - 'Wyświetl status powtarzacza, statystyki i sąsiadów.'; + 'WyÅ›wietl status powtarzacza, statystyki i sÄ…siadów.'; @override String get repeater_telemetry => 'Telemetry'; @override String get repeater_telemetrySubtitle => - 'Wyświetl dane telemetryczne z czujników i statystyki systemu'; + 'WyÅ›wietl dane telemetryczne z czujników i statystyki systemu'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza'; + String get repeater_cliSubtitle => 'WyÅ›lij polecenia do powielacza'; @override - String get repeater_neighbors => 'Sąsiedzi'; + String get repeater_neighbors => 'SÄ…siedzi'; @override String get repeater_neighborsSubtitle => - 'Wyświetl sąsiedztwo zerowych hopów.'; + 'WyÅ›wietl sÄ…siedztwo zerowych hopów.'; @override String get repeater_settings => 'Ustawienia'; @@ -1802,23 +1804,23 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_autoUseSavedPath => - 'Automatycznie (użyj zapisanej ścieżki)'; + 'Automatycznie (użyj zapisanej Å›cieżki)'; @override String get repeater_forceFloodMode => 'Wymusz Tryb Powodowany'; @override - String get repeater_pathManagement => 'Zarządzanie ścieżkami'; + String get repeater_pathManagement => 'ZarzÄ…dzanie Å›cieżkami'; @override - String get repeater_refresh => 'Odśwież'; + String get repeater_refresh => 'OdÅ›wież'; @override - String get repeater_statusRequestTimeout => 'Życzenie statusu timed out.'; + String get repeater_statusRequestTimeout => 'Å»yczenie statusu timed out.'; @override String repeater_errorLoadingStatus(String error) { - return 'Błąd podczas ładowania statusu: $error'; + return 'Błąd podczas Å‚adowania statusu: $error'; } @override @@ -1831,10 +1833,10 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_clockAtLogin => 'Godzina (przy logowaniu)'; @override - String get repeater_uptime => 'Dostępność'; + String get repeater_uptime => 'DostÄ™pność'; @override - String get repeater_queueLength => 'Długość kolejki'; + String get repeater_queueLength => 'DÅ‚ugość kolejki'; @override String get repeater_debugFlags => 'Opcje debugowania'; @@ -1849,7 +1851,7 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_lastSnr => 'Ostatnie SNR'; @override - String get repeater_noiseFloor => 'Poziom Szumów'; + String get repeater_noiseFloor => 'Poziom Szumów'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1858,16 +1860,16 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Statystyki pakietów'; + String get repeater_packetStatistics => 'Statystyki pakietów'; @override - String get repeater_sent => 'Wysłane'; + String get repeater_sent => 'WysÅ‚ane'; @override String get repeater_received => 'Otrzymano'; @override - String get repeater_duplicates => 'Powtórzenia'; + String get repeater_duplicates => 'Powtórzenia'; @override String repeater_daysHoursMinsSecs( @@ -1881,17 +1883,17 @@ class AppLocalizationsPl extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Razem: $total, Powodzenie: $flood, Bezpośrednio: $direct'; + return 'Razem: $total, Powodzenie: $flood, BezpoÅ›rednio: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Razem: $total, Powodzenie: $flood, Bezpośrednio: $direct'; + return 'Razem: $total, Powodzenie: $flood, BezpoÅ›rednio: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Powodzie: $flood, Bezpośrednie: $direct'; + return 'Powodzie: $flood, BezpoÅ›rednie: $direct'; } @override @@ -1900,34 +1902,34 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Ustawienia Powtórki'; + String get repeater_settingsTitle => 'Ustawienia Powtórki'; @override String get repeater_basicSettings => 'Podstawowe Ustawienia'; @override - String get repeater_repeaterName => 'Nazwa Powtórnika'; + String get repeater_repeaterName => 'Nazwa Powtórnika'; @override - String get repeater_repeaterNameHelper => 'Wyświetl nazwę tego powtarzacza'; + String get repeater_repeaterNameHelper => 'WyÅ›wietl nazwÄ™ tego powtarzacza'; @override - String get repeater_adminPassword => 'Hasło Administracyjne'; + String get repeater_adminPassword => 'HasÅ‚o Administracyjne'; @override - String get repeater_adminPasswordHelper => 'Pełny dostęp hasło'; + String get repeater_adminPasswordHelper => 'PeÅ‚ny dostÄ™p hasÅ‚o'; @override - String get repeater_guestPassword => 'Hasło gościa'; + String get repeater_guestPassword => 'HasÅ‚o goÅ›cia'; @override - String get repeater_guestPasswordHelper => 'Dostęp tylko do odczytu hasło'; + String get repeater_guestPasswordHelper => 'DostÄ™p tylko do odczytu hasÅ‚o'; @override String get repeater_radioSettings => 'Ustawienia radia'; @override - String get repeater_frequencyMhz => 'Częstotliwość (MHz)'; + String get repeater_frequencyMhz => 'CzÄ™stotliwość (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -1939,10 +1941,10 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Przepustowość'; + String get repeater_bandwidth => 'Przepustowość'; @override - String get repeater_spreadingFactor => 'Rozkład Czynnika'; + String get repeater_spreadingFactor => 'RozkÅ‚ad Czynnika'; @override String get repeater_codingRate => 'Stawka kodowania'; @@ -1951,46 +1953,46 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_locationSettings => 'Ustawienia Lokalizacji'; @override - String get repeater_latitude => 'Szerokość'; + String get repeater_latitude => 'Szerokość'; @override - String get repeater_latitudeHelper => 'Stopnie dziesiętne (np. 37.7749)'; + String get repeater_latitudeHelper => 'Stopnie dziesiÄ™tne (np. 37.7749)'; @override - String get repeater_longitude => 'Długość'; + String get repeater_longitude => 'DÅ‚ugość'; @override - String get repeater_longitudeHelper => 'Stopnie dziesiętne (np. -122,4194)'; + String get repeater_longitudeHelper => 'Stopnie dziesiÄ™tne (np. -122,4194)'; @override String get repeater_features => 'Funkcje'; @override - String get repeater_packetForwarding => 'Przekierowanie pakietów'; + String get repeater_packetForwarding => 'Przekierowanie pakietów'; @override String get repeater_packetForwardingSubtitle => - 'Włącz repeater, aby przekazywać pakiety.'; + 'Włącz repeater, aby przekazywać pakiety.'; @override - String get repeater_guestAccess => 'Dostęp dla gości'; + String get repeater_guestAccess => 'DostÄ™p dla goÅ›ci'; @override String get repeater_guestAccessSubtitle => - 'Umożliw dostęp tylko do odczytu dla gości.'; + 'Umożliw dostÄ™p tylko do odczytu dla goÅ›ci.'; @override - String get repeater_privacyMode => 'Tryb Prywatności'; + String get repeater_privacyMode => 'Tryb PrywatnoÅ›ci'; @override String get repeater_privacyModeSubtitle => - 'Ukryj imię/lokalizację w reklamach'; + 'Ukryj imiÄ™/lokalizacjÄ™ w reklamach'; @override String get repeater_advertisementSettings => 'Ustawienia Reklam'; @override - String get repeater_localAdvertInterval => 'Interwał Reklamy Lokalnej'; + String get repeater_localAdvertInterval => 'InterwaÅ‚ Reklamy Lokalnej'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -1998,7 +2000,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => 'Interwał Reklamy Powodziowej'; + String get repeater_floodAdvertInterval => 'InterwaÅ‚ Reklamy Powodziowej'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2007,146 +2009,149 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Zaszyfrowany Interwał Reklamowy'; + 'Zaszyfrowany InterwaÅ‚ Reklamowy'; @override - String get repeater_dangerZone => 'Strefa Zagrożeń'; + String get repeater_dangerZone => 'Strefa ZagrożeÅ„'; @override String get repeater_rebootRepeater => 'Zrestartuj Powtarzacz'; @override String get repeater_rebootRepeaterSubtitle => - 'Zrestartuj urządzenie powtarzające.'; + 'Zrestartuj urzÄ…dzenie powtarzajÄ…ce.'; @override String get repeater_rebootRepeaterConfirm => - 'Czy na pewno chcesz zrestartować ten repeater?'; + 'Czy na pewno chcesz zrestartować ten repeater?'; @override - String get repeater_regenerateIdentityKey => 'Wygeneruj klucz tożsamości'; + String get repeater_regenerateIdentityKey => 'Wygeneruj klucz tożsamoÅ›ci'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Wygeneruj nową parę kluczy publicznych/prywatnych'; + 'Wygeneruj nowÄ… parÄ™ kluczy publicznych/prywatnych'; @override String get repeater_regenerateIdentityKeyConfirm => - 'To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?'; + 'To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?'; @override - String get repeater_eraseFileSystem => 'Wyczyść System Plików'; + String get repeater_eraseFileSystem => 'Wyczyść System Plików'; @override String get repeater_eraseFileSystemSubtitle => - 'Sformatuj system plików powielacza'; + 'Sformatuj system plików powielacza'; @override String get repeater_eraseFileSystemConfirm => - 'OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z powtarzacza. Nie da się tego cofnąć!'; + 'OSTRZEÅ»ENIE: To spowoduje usuniÄ™cie wszystkich danych z powtarzacza. Nie da siÄ™ tego cofnąć!'; @override String get repeater_eraseSerialOnly => - 'Usunięcie jest dostępne tylko przez konsolę szeregową.'; + 'UsuniÄ™cie jest dostÄ™pne tylko przez konsolÄ™ szeregowÄ….'; @override String repeater_commandSent(String command) { - return 'Polecenie wysłane: $command'; + return 'Polecenie wysÅ‚ane: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Błąd podczas wysyłania polecenia: $error'; + return 'Błąd podczas wysyÅ‚ania polecenia: $error'; } @override - String get repeater_confirm => 'Potwierdź'; + String get repeater_confirm => 'Potwierdź'; @override - String get repeater_settingsSaved => 'Ustawienia zostały pomyślnie zapisane.'; + String get repeater_settingsSaved => + 'Ustawienia zostaÅ‚y pomyÅ›lnie zapisane.'; @override String repeater_errorSavingSettings(String error) { - return 'Błąd zapisu ustawień: $error'; + return 'Błąd zapisu ustawieÅ„: $error'; } @override - String get repeater_refreshBasicSettings => 'Odśwież Podstawowe Ustawienia'; + String get repeater_refreshBasicSettings => 'OdÅ›wież Podstawowe Ustawienia'; @override - String get repeater_refreshRadioSettings => 'Odśwież Ustawienia Radio'; + String get repeater_refreshRadioSettings => 'OdÅ›wież Ustawienia Radio'; @override - String get repeater_refreshTxPower => 'Odśwież TX power'; + String get repeater_refreshTxPower => 'OdÅ›wież TX power'; @override String get repeater_refreshLocationSettings => - 'Odśwież Ustawienia Lokalizacji'; + 'OdÅ›wież Ustawienia Lokalizacji'; @override - String get repeater_refreshPacketForwarding => 'Odśwież trasowanie pakietów'; + String get repeater_refreshPacketForwarding => + 'OdÅ›wież trasowanie pakietów'; @override - String get repeater_refreshGuestAccess => 'Odśwież dostęp gościa'; + String get repeater_refreshGuestAccess => 'OdÅ›wież dostÄ™p goÅ›cia'; @override - String get repeater_refreshPrivacyMode => 'Odśwież Tryb Prywatności'; + String get repeater_refreshPrivacyMode => 'OdÅ›wież Tryb PrywatnoÅ›ci'; @override String get repeater_refreshAdvertisementSettings => - 'Odśwież Ustawienia Reklamy'; + 'OdÅ›wież Ustawienia Reklamy'; @override String repeater_refreshed(String label) { - return '$label odświeżone'; + return '$label odÅ›wieżone'; } @override String repeater_errorRefreshing(String label) { - return 'Błąd podczas odświeżania $label'; + return 'Błąd podczas odÅ›wieżania $label'; } @override String get repeater_cliTitle => 'Powtarzacz CLI'; @override - String get repeater_debugNextCommand => 'Debug Następną Komendę'; + String get repeater_debugNextCommand => 'Debug NastÄ™pnÄ… KomendÄ™'; @override String get repeater_commandHelp => 'Pomoc'; @override - String get repeater_clearHistory => 'Wyczyść historię'; + String get repeater_clearHistory => 'Wyczyść historiÄ™'; @override - String get repeater_noCommandsSent => 'Nie wysłano jeszcze żadnych poleceń'; + String get repeater_noCommandsSent => + 'Nie wysÅ‚ano jeszcze żadnych poleceÅ„'; @override String get repeater_typeCommandOrUseQuick => - 'Wprowadź polecenie poniżej lub użyj szybkich poleceń'; + 'Wprowadź polecenie poniżej lub użyj szybkich poleceÅ„'; @override - String get repeater_enterCommandHint => 'Wprowadź polecenie...'; + String get repeater_enterCommandHint => 'Wprowadź polecenie...'; @override String get repeater_previousCommand => 'Poprzednia komenda'; @override - String get repeater_nextCommand => 'Następna komenda'; + String get repeater_nextCommand => 'NastÄ™pna komenda'; @override - String get repeater_enterCommandFirst => 'Wprowadź najpierw polecenie'; + String get repeater_enterCommandFirst => 'Wprowadź najpierw polecenie'; @override - String get repeater_cliCommandFrameTitle => 'Określony Wyraz Polecenia CLI'; + String get repeater_cliCommandFrameTitle => 'OkreÅ›lony Wyraz Polecenia CLI'; @override String repeater_cliCommandError(String error) { - return 'Błąd: $error'; + return 'Błąd: $error'; } @override - String get repeater_cliQuickGetName => 'Pobierz imię'; + String get repeater_cliQuickGetName => 'Pobierz imiÄ™'; @override String get repeater_cliQuickGetRadio => 'Uzyskaj Radio'; @@ -2155,7 +2160,7 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_cliQuickGetTx => 'Pobierz TX'; @override - String get repeater_cliQuickNeighbors => 'Sąsiedzi'; + String get repeater_cliQuickNeighbors => 'SÄ…siedzi'; @override String get repeater_cliQuickVersion => 'Wersja'; @@ -2167,81 +2172,81 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_cliQuickClock => 'Godzina'; @override - String get repeater_cliHelpAdvert => 'Wysyła pakiet reklamowy'; + String get repeater_cliHelpAdvert => 'WysyÅ‚a pakiet reklamowy'; @override String get repeater_cliHelpReboot => - 'Zresetuj urządzenie. (Uwaga, może pojawić się \'Timeout\', co jest normalne)'; + 'Zresetuj urzÄ…dzenie. (Uwaga, może pojawić siÄ™ \'Timeout\', co jest normalne)'; @override String get repeater_cliHelpClock => - 'Wyświetla aktualny czas zgodnie z zegarem urządzenia.'; + 'WyÅ›wietla aktualny czas zgodnie z zegarem urzÄ…dzenia.'; @override String get repeater_cliHelpPassword => - 'Ustawia nowe hasło administratora dla urządzenia.'; + 'Ustawia nowe hasÅ‚o administratora dla urzÄ…dzenia.'; @override String get repeater_cliHelpVersion => - 'Wyświetla wersję urządzenia i datę budowy oprogramowania.'; + 'WyÅ›wietla wersjÄ™ urzÄ…dzenia i datÄ™ budowy oprogramowania.'; @override String get repeater_cliHelpClearStats => - 'Resetuje różne wskaźniki statystyk do zera.'; + 'Resetuje różne wskaźniki statystyk do zera.'; @override String get repeater_cliHelpSetAf => 'Ustawia czynnik czasu powietrznego.'; @override String get repeater_cliHelpSetTx => - 'Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)'; + 'Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)'; @override String get repeater_cliHelpSetRepeat => - 'Włącza lub wyłącza rolę powtarzacza dla tego węzła.'; + 'Włącza lub wyłącza rolÄ™ powtarzacza dla tego wÄ™zÅ‚a.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Serwer pokoju) Jeśli \'włączone\', to logowanie z pustym hasłem będzie dozwolone, ale nie można publikować w pokoju (tylko czytać).'; + '(Serwer pokoju) JeÅ›li \'włączone\', to logowanie z pustym hasÅ‚em bÄ™dzie dozwolone, ale nie można publikować w pokoju (tylko czytać).'; @override String get repeater_cliHelpSetFloodMax => - 'Ustawia maksymalną liczbę skoków pakietu powrotnego (jeśli >= max, pakiet nie jest przekierowywany)'; + 'Ustawia maksymalnÄ… liczbÄ™ skoków pakietu powrotnego (jeÅ›li >= max, pakiet nie jest przekierowywany)'; @override String get repeater_cliHelpSetIntThresh => - 'Ustawia Próg Interferencji (w dB). Domyślnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceń kanału.'; + 'Ustawia Próg Interferencji (w dB). DomyÅ›lnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceÅ„ kanaÅ‚u.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Ustawia interwał do zresetowania Automatycznego Sterownika Głośności. Ustaw na 0, aby wyłączyć.'; + 'Ustawia interwaÅ‚ do zresetowania Automatycznego Sterownika GÅ‚oÅ›noÅ›ci. Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetMultiAcks => - 'Włącza lub wyłącza funkcję \'podwójnych potwierdzeń\'.'; + 'Włącza lub wyłącza funkcjÄ™ \'podwójnych potwierdzeÅ„\'.'; @override String get repeater_cliHelpSetAdvertInterval => - 'Ustawia interwał timera w minutach do wysyłania pakietu reklamy lokalnej (bezpośredniej). Ustaw na 0, aby wyłączyć.'; + 'Ustawia interwaÅ‚ timera w minutach do wysyÅ‚ania pakietu reklamy lokalnej (bezpoÅ›redniej). Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Ustawia interwał timera w godzinach do wysłania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.'; + 'Ustawia interwaÅ‚ timera w godzinach do wysÅ‚ania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetGuestPassword => - 'Ustawia/aktualizuje hasło gościa. (dla repeaterów, loginy gości mogą wysyłać żądanie \"Get Stats\")'; + 'Ustawia/aktualizuje hasÅ‚o goÅ›cia. (dla repeaterów, loginy goÅ›ci mogÄ… wysyÅ‚ać żądanie \"Get Stats\")'; @override - String get repeater_cliHelpSetName => 'Ustawia nazwę reklamy.'; + String get repeater_cliHelpSetName => 'Ustawia nazwÄ™ reklamy.'; @override String get repeater_cliHelpSetLat => - 'Ustawia współrzędną geograficzne (w stopniach dziesiętnych) mapy reklam.'; + 'Ustawia współrzÄ™dnÄ… geograficzne (w stopniach dziesiÄ™tnych) mapy reklam.'; @override String get repeater_cliHelpSetLon => - 'Ustawia współrzędną długościową mapy reklamy. (stopnie dziesiętne)'; + 'Ustawia współrzÄ™dnÄ… dÅ‚ugoÅ›ciowÄ… mapy reklamy. (stopnie dziesiÄ™tne)'; @override String get repeater_cliHelpSetRadio => @@ -2249,46 +2254,46 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliHelpSetRxDelay => - 'Ustawienia (eksperymentalne) bazowe (muszą być > 1, aby działać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnału/wynik. Ustaw na 0, aby wyłączyć.'; + 'Ustawienia (eksperymentalne) bazowe (muszÄ… być > 1, aby dziaÅ‚ać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnaÅ‚u/wynik. Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetTxDelay => - 'Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyłanie (zmniejszając prawdopodobieństwo kolizji).'; + 'Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyÅ‚anie (zmniejszajÄ…c prawdopodobieÅ„stwo kolizji).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpośrednim.'; + 'Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpoÅ›rednim.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Włącz/Wyłącz mostek.'; + String get repeater_cliHelpSetBridgeEnabled => 'Włącz/Wyłącz mostek.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Ustaw czas opóźnienia przed ponownym wysyłaniem pakietów.'; + 'Ustaw czas opóźnienia przed ponownym wysyÅ‚aniem pakietów.'; @override String get repeater_cliHelpSetBridgeSource => - 'Wybierz, czy most będzie ponownie transmitował otrzymywane pakiety, czy też wysyłane.'; + 'Wybierz, czy most bÄ™dzie ponownie transmitowaÅ‚ otrzymywane pakiety, czy też wysyÅ‚ane.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Ustaw prędkość transmisji magistrali szeregowej dla mostów rs232.'; + 'Ustaw prÄ™dkość transmisji magistrali szeregowej dla mostów rs232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Ustaw sekret dla mostów ESPNOW.'; + 'Ustaw sekret dla mostów ESPNOW.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Ustawia niestandardowy współczynnik do korekty zgłaszanego napięcia baterii (obsługa tylko na wybranych płytach).'; + 'Ustawia niestandardowy współczynnik do korekty zgÅ‚aszanego napiÄ™cia baterii (obsÅ‚uga tylko na wybranych pÅ‚ytach).'; @override String get repeater_cliHelpTempRadio => - 'Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a następnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).'; + 'Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a nastÄ™pnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).'; @override String get repeater_cliHelpSetPerm => - 'Modyfikuje ACL. Usuwa dopasowaną wpis (z prefiksem pubkey), jeśli \"permissions\" wynosi zero. Dodaje nowy wpis, jeśli pubkey-hex ma pełną długość i nie znajduje się obecnie w ACL. Aktualizuje wpis, dopasowując prefiks pubkey. Bit uprawnień zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).'; + 'Modyfikuje ACL. Usuwa dopasowanÄ… wpis (z prefiksem pubkey), jeÅ›li \"permissions\" wynosi zero. Dodaje nowy wpis, jeÅ›li pubkey-hex ma peÅ‚nÄ… dÅ‚ugość i nie znajduje siÄ™ obecnie w ACL. Aktualizuje wpis, dopasowujÄ…c prefiks pubkey. Bit uprawnieÅ„ zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).'; @override String get repeater_cliHelpGetBridgeType => @@ -2296,31 +2301,31 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliHelpLogStart => - 'Rozpoczyna się logowanie pakietów do systemu plików.'; + 'Rozpoczyna siÄ™ logowanie pakietów do systemu plików.'; @override String get repeater_cliHelpLogStop => - 'Zatrzymuje logowanie pakietów do systemu plików.'; + 'Zatrzymuje logowanie pakietów do systemu plików.'; @override String get repeater_cliHelpLogErase => - 'Usuwa logi pakietów z systemu plików.'; + 'Usuwa logi pakietów z systemu plików.'; @override String get repeater_cliHelpNeighbors => - 'Wyświetla listę innych węzłów powtarzających się, które usłyszano dzięki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4'; + 'WyÅ›wietla listÄ™ innych wÄ™złów powtarzajÄ…cych siÄ™, które usÅ‚yszano dziÄ™ki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Usuwa pierwszy pasujący wpis (z prefiksem pubkey (hex)) z listy sąsiadów.'; + 'Usuwa pierwszy pasujÄ…cy wpis (z prefiksem pubkey (hex)) z listy sÄ…siadów.'; @override String get repeater_cliHelpRegion => - '(tylko seria) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.'; + '(tylko seria) WyÅ›wietla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.'; @override String get repeater_cliHelpRegionLoad => - 'ZAPOMNIJ: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.'; + 'ZAPOMNIJ: to jest specjalne wywoÅ‚anie wielokomendowe. Każda nastÄ™pna komenda jest nazwÄ… regionu (wciÄ™ta spacjami, aby wskazywać hierarchiÄ™ nadrzÄ™dnÄ…, z minimum jednÄ… spacjÄ…). ZakoÅ„czona wysÅ‚aniem pustej linii/komendy.'; @override String get repeater_cliHelpRegionGet => @@ -2328,63 +2333,63 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliHelpRegionPut => - 'Dodaje lub aktualizuje definicję regionu z podaną nazwą.'; + 'Dodaje lub aktualizuje definicjÄ™ regionu z podanÄ… nazwÄ….'; @override String get repeater_cliHelpRegionRemove => - 'Usuwa definicję regionu o podanej nazwie. (musi się dokładnie zgadzać i nie może mieć podregionów).'; + 'Usuwa definicjÄ™ regionu o podanej nazwie. (musi siÄ™ dokÅ‚adnie zgadzać i nie może mieć podregionów).'; @override String get repeater_cliHelpRegionAllowf => - 'Ustawia uprawnienia \'P\'łytkowe dla podanego regionu. (\'\' dla zakresu globalnego/starszego)'; + 'Ustawia uprawnienia \'P\'Å‚ytkowe dla podanego regionu. (\'\' dla zakresu globalnego/starszego)'; @override String get repeater_cliHelpRegionDenyf => - 'Usuwa uprawnienie \'Pływające\' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).'; + 'Usuwa uprawnienie \'PÅ‚ywajÄ…ce\' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca siÄ™ używania tego na globalnym/starszym zakresie!!).'; @override String get repeater_cliHelpRegionHome => - 'Odpowiada z aktualnej \'home\' region. (Uwaga: nie zostało jeszcze zastosowane, zarezerwowane na przyszłość).'; + 'Odpowiada z aktualnej \'home\' region. (Uwaga: nie zostaÅ‚o jeszcze zastosowane, zarezerwowane na przyszÅ‚ość).'; @override String get repeater_cliHelpRegionHomeSet => 'Ustawia region \'domowe\'.'; @override String get repeater_cliHelpRegionSave => - 'Zapisuje listę/mapę regionów do pamięci.'; + 'Zapisuje listÄ™/mapÄ™ regionów do pamiÄ™ci.'; @override String get repeater_cliHelpGps => - 'Wyświetla status GPS. Jeśli GPS jest wyłączony, odpowiada tylko \"off\", jeśli jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbą satelitów.'; + 'WyÅ›wietla status GPS. JeÅ›li GPS jest wyłączony, odpowiada tylko \"off\", jeÅ›li jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbÄ… satelitów.'; @override - String get repeater_cliHelpGpsOnOff => 'Włącza/wyłącza nawigację GPS.'; + String get repeater_cliHelpGpsOnOff => 'Włącza/wyłącza nawigacjÄ™ GPS.'; @override String get repeater_cliHelpGpsSync => - 'Synchronizuje czas węzła z zegarem GPS.'; + 'Synchronizuje czas wÄ™zÅ‚a z zegarem GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Ustawia pozycję węzła na współrzędne GPS i zapisuje preferencje.'; + 'Ustawia pozycjÄ™ wÄ™zÅ‚a na współrzÄ™dne GPS i zapisuje preferencje.'; @override String get repeater_cliHelpGpsAdvert => - 'Udostępnia konfigurację reklamy lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w reklamach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: reklamuj lokalizację przechowywaną w ustawieniach'; + 'UdostÄ™pnia konfiguracjÄ™ reklamy lokalizacji wÄ™zÅ‚a:\n- brak: nie uwzglÄ™dniaj lokalizacji w reklamach\n- udostÄ™pnia: udostÄ™pnia lokalizacjÄ™ GPS (z SensorManager)\n- ustawienia: reklamuj lokalizacjÄ™ przechowywanÄ… w ustawieniach'; @override String get repeater_cliHelpGpsAdvertSet => - 'Ustawia konfigurację reklamy w lokalizacji.'; + 'Ustawia konfiguracjÄ™ reklamy w lokalizacji.'; @override - String get repeater_commandsListTitle => 'Lista poleceń'; + String get repeater_commandsListTitle => 'Lista poleceÅ„'; @override String get repeater_commandsListNote => - 'ZAPAMIĘTAJ: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".'; + 'ZAPAMIĘTAJ: dla różnych poleceÅ„ \"set ...\" istnieje również polecenie \"get ...\".'; @override - String get repeater_general => 'Ogólne'; + String get repeater_general => 'Ogólne'; @override String get repeater_settingsCategory => 'Ustawienia'; @@ -2396,48 +2401,48 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_logging => 'Rejestrowanie'; @override - String get repeater_neighborsRepeaterOnly => 'Sąsiedzi (tylko powtarzacz)'; + String get repeater_neighborsRepeaterOnly => 'SÄ…siedzi (tylko powtarzacz)'; @override String get repeater_regionManagementRepeaterOnly => - 'Zarządzanie Regionem (tylko Powtarzacz)'; + 'ZarzÄ…dzanie Regionem (tylko Powtarzacz)'; @override String get repeater_regionNote => - 'Wprowadzono komendy regionalne w celu zarządzania definicjami i uprawnieniami regionów.'; + 'Wprowadzono komendy regionalne w celu zarzÄ…dzania definicjami i uprawnieniami regionów.'; @override - String get repeater_gpsManagement => 'Zarządzanie GPS'; + String get repeater_gpsManagement => 'ZarzÄ…dzanie GPS'; @override String get repeater_gpsNote => - 'Polecenie GPS zostało wprowadzone w celu zarządzania tematami związanymi z lokalizacją.'; + 'Polecenie GPS zostaÅ‚o wprowadzone w celu zarzÄ…dzania tematami zwiÄ…zanymi z lokalizacjÄ….'; @override String get telemetry_receivedData => 'Otrzymano Dane Telemetrii'; @override String get telemetry_requestTimeout => - 'Życzenie o danych telemetrycznych nie udało się.'; + 'Å»yczenie o danych telemetrycznych nie udaÅ‚o siÄ™.'; @override String telemetry_errorLoading(String error) { - return 'Błąd podczas ładowania telemetry: $error'; + return 'Błąd podczas Å‚adowania telemetry: $error'; } @override - String get telemetry_noData => 'Brak dostępnych danych telemetrycznych.'; + String get telemetry_noData => 'Brak dostÄ™pnych danych telemetrycznych.'; @override String telemetry_channelTitle(int channel) { - return 'Kanał $channel'; + return 'KanaÅ‚ $channel'; } @override String get telemetry_batteryLabel => 'Bateria'; @override - String get telemetry_voltageLabel => 'Napięcie'; + String get telemetry_voltageLabel => 'NapiÄ™cie'; @override String get telemetry_mcuTemperatureLabel => 'Temperatura MCU'; @@ -2465,26 +2470,26 @@ class AppLocalizationsPl extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa'; + String get neighbors_receivedData => 'Otrzymano dane sÄ…siedztwa'; @override String get neighbors_requestTimedOut => - 'Sąsiedzi proszą o wyłączenie timingu.'; + 'SÄ…siedzi proszÄ… o wyłączenie timingu.'; @override String neighbors_errorLoading(String error) { - return 'Błąd podczas ładowania sąsiadów: $error'; + return 'Błąd podczas Å‚adowania sÄ…siadów: $error'; } @override - String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi'; + String get neighbors_repeatersNeighbors => 'Powtarzacze SÄ…siedzi'; @override - String get neighbors_noData => 'Brak danych dotyczących sąsiadów.'; + String get neighbors_noData => 'Brak danych dotyczÄ…cych sÄ…siadów.'; @override String neighbors_unknownContact(String pubkey) { @@ -2493,27 +2498,27 @@ class AppLocalizationsPl extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Usłyszano: $time temu'; + return 'UsÅ‚yszano: $time temu'; } @override - String get channelPath_title => 'Ścieżka pakietu'; + String get channelPath_title => 'Åšcieżka pakietu'; @override - String get channelPath_viewMap => 'Wyświetl mapę'; + String get channelPath_viewMap => 'WyÅ›wietl mapÄ™'; @override - String get channelPath_otherObservedPaths => 'Inne Zauważone Ścieżki'; + String get channelPath_otherObservedPaths => 'Inne Zauważone Åšcieżki'; @override - String get channelPath_repeaterHops => 'Skoki Powtórki'; + String get channelPath_repeaterHops => 'Skoki Powtórki'; @override String get channelPath_noHopDetails => - 'Szczegóły dotyczące tego pakietu nie zostały podane.'; + 'Szczegóły dotyczÄ…ce tego pakietu nie zostaÅ‚y podane.'; @override - String get channelPath_messageDetails => 'Szczegóły wiadomości'; + String get channelPath_messageDetails => 'Szczegóły wiadomoÅ›ci'; @override String get channelPath_senderLabel => 'Nadawca'; @@ -2522,11 +2527,11 @@ class AppLocalizationsPl extends AppLocalizations { String get channelPath_timeLabel => 'Czas'; @override - String get channelPath_repeatsLabel => 'Powtórzenia'; + String get channelPath_repeatsLabel => 'Powtórzenia'; @override String channelPath_pathLabel(int index) { - return 'Ścieżka $index'; + return 'Åšcieżka $index'; } @override @@ -2534,7 +2539,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Obserwowany ścieżka $index • $hops'; + return 'Obserwowany Å›cieżka $index • $hops'; } @override @@ -2557,158 +2562,159 @@ class AppLocalizationsPl extends AppLocalizations { String get channelPath_floodPath => 'Powodzenie'; @override - String get channelPath_directPath => 'Bezpośrednio'; + String get channelPath_directPath => 'BezpoÅ›rednio'; @override String channelPath_observedZeroOf(int total) { - return '0 z $total skoków'; + return '0 z $total skoków'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed z $total skoków'; + return '$observed z $total skoków'; } @override - String get channelPath_mapTitle => 'Mapa ścieżek'; + String get channelPath_mapTitle => 'Mapa Å›cieżek'; @override String get channelPath_noRepeaterLocations => - 'Brak dostępnych lokalizacji powtarzaczy dla tego ścieżki.'; + 'Brak dostÄ™pnych lokalizacji powtarzaczy dla tego Å›cieżki.'; @override String channelPath_primaryPath(int index) { - return 'Ścieżka $index (Główna)'; + return 'Åšcieżka $index (Główna)'; } @override - String get channelPath_pathLabelTitle => 'Ścieżka'; + String get channelPath_pathLabelTitle => 'Åšcieżka'; @override - String get channelPath_observedPathHeader => 'Obserwowana ścieżka'; + String get channelPath_observedPathHeader => 'Obserwowana Å›cieżka'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Brak dostępnych szczegółów hopa dla tego pakietu.'; + 'Brak dostÄ™pnych szczegółów hopa dla tego pakietu.'; @override String get channelPath_unknownRepeater => 'Nieznany Powtarzacz'; @override - String get community_title => 'Społeczność'; + String get community_title => 'SpoÅ‚eczność'; @override - String get community_create => 'Utwórz Społeczność'; + String get community_create => 'Utwórz SpoÅ‚eczność'; @override String get community_createDesc => - 'Utwórz nową społeczność i udostępnij za pomocą kodu QR.'; + 'Utwórz nowÄ… spoÅ‚eczność i udostÄ™pnij za pomocÄ… kodu QR.'; @override - String get community_join => 'Dołącz'; + String get community_join => 'Dołącz'; @override - String get community_joinTitle => 'Dołącz do społeczności'; + String get community_joinTitle => 'Dołącz do spoÅ‚ecznoÅ›ci'; @override String community_joinConfirmation(String name) { - return 'Czy chcesz dołączyć do społeczności \"$name\"?'; + return 'Czy chcesz dołączyć do spoÅ‚ecznoÅ›ci \"$name\"?'; } @override - String get community_scanQr => 'Skanuj QR kod społeczności'; + String get community_scanQr => 'Skanuj QR kod spoÅ‚ecznoÅ›ci'; @override String get community_scanInstructions => - 'Skieruj kamerę w kierunku kodu QR społeczności.'; + 'Skieruj kamerÄ™ w kierunku kodu QR spoÅ‚ecznoÅ›ci.'; @override - String get community_showQr => 'Pokaż kod QR'; + String get community_showQr => 'Pokaż kod QR'; @override - String get community_publicChannel => 'Społeczność Publiczna'; + String get community_publicChannel => 'SpoÅ‚eczność Publiczna'; @override - String get community_hashtagChannel => 'Hashtag Społeczności'; + String get community_hashtagChannel => 'Hashtag SpoÅ‚ecznoÅ›ci'; @override - String get community_name => 'Nazwa Społeczności'; + String get community_name => 'Nazwa SpoÅ‚ecznoÅ›ci'; @override - String get community_enterName => 'Wprowadź nazwę społeczności'; + String get community_enterName => 'Wprowadź nazwÄ™ spoÅ‚ecznoÅ›ci'; @override String community_created(String name) { - return 'Społeczność \"$name\" została utworzona'; + return 'SpoÅ‚eczność \"$name\" zostaÅ‚a utworzona'; } @override String community_joined(String name) { - return 'Dołączył do społeczności \"$name\"'; + return 'DołączyÅ‚ do spoÅ‚ecznoÅ›ci \"$name\"'; } @override - String get community_qrTitle => 'Dziel się Społecznością'; + String get community_qrTitle => 'Dziel siÄ™ SpoÅ‚ecznoÅ›ciÄ…'; @override String community_qrInstructions(String name) { - return 'Skanuj ten kod QR, aby dołączyć $name'; + return 'Skanuj ten kod QR, aby dołączyć $name'; } @override String get community_hashtagPrivacyHint => - 'Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności'; + 'KanaÅ‚y hashtagowe spoÅ‚ecznoÅ›ci sÄ… dostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci'; @override - String get community_invalidQrCode => 'Nieprawidłowy kod QR społeczności.'; + String get community_invalidQrCode => 'NieprawidÅ‚owy kod QR spoÅ‚ecznoÅ›ci.'; @override - String get community_alreadyMember => 'Już jesteś członkiem.'; + String get community_alreadyMember => 'Już jesteÅ› czÅ‚onkiem.'; @override String community_alreadyMemberMessage(String name) { - return 'Jesteś już członkiem \"$name\".'; + return 'JesteÅ› już czÅ‚onkiem \"$name\".'; } @override - String get community_addPublicChannel => 'Dodaj Kanał Publiczny Społeczności'; + String get community_addPublicChannel => + 'Dodaj KanaÅ‚ Publiczny SpoÅ‚ecznoÅ›ci'; @override String get community_addPublicChannelHint => - 'Automatycznie dodaj kanał publiczny dla tej społeczności.'; + 'Automatycznie dodaj kanaÅ‚ publiczny dla tej spoÅ‚ecznoÅ›ci.'; @override String get community_noCommunities => - 'Nie dołączono jeszcze żadnych społeczności.'; + 'Nie dołączono jeszcze żadnych spoÅ‚ecznoÅ›ci.'; @override String get community_scanOrCreate => - 'Skanuj kod QR lub utwórz społeczność, aby zacząć.'; + 'Skanuj kod QR lub utwórz spoÅ‚eczność, aby zacząć.'; @override - String get community_manageCommunities => 'Zarządzaj Grupami'; + String get community_manageCommunities => 'ZarzÄ…dzaj Grupami'; @override - String get community_delete => 'Opuszczenie Społeczności'; + String get community_delete => 'Opuszczenie SpoÅ‚ecznoÅ›ci'; @override String community_deleteConfirm(String name) { - return 'Opuścić \"$name\"?'; + return 'OpuÅ›cić \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Spowoduje to również usunięcie $count kanału/kanałów i ich wiadomości.'; + return 'Spowoduje to również usuniÄ™cie $count kanaÅ‚u/kanałów i ich wiadomoÅ›ci.'; } @override String community_deleted(String name) { - return 'Opuszczono społeczność \"$name\"'; + return 'Opuszczono spoÅ‚eczność \"$name\"'; } @override @@ -2716,7 +2722,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.'; + return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy czÅ‚onkowie bÄ™dÄ… musieli zeskanować nowy kod QR, aby kontynuować komunikacjÄ™.'; } @override @@ -2724,7 +2730,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Hasło ponownie wygenerowane dla \"$name\"'; + return 'HasÅ‚o ponownie wygenerowane dla \"$name\"'; } @override @@ -2732,37 +2738,37 @@ class AppLocalizationsPl extends AppLocalizations { @override String community_secretUpdated(String name) { - return 'Hasło zaktualizowane dla \"$name\"'; + return 'HasÅ‚o zaktualizowane dla \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"'; + return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"'; } @override - String get community_addHashtagChannel => 'Dodaj hashtag społeczności'; + String get community_addHashtagChannel => 'Dodaj hashtag spoÅ‚ecznoÅ›ci'; @override String get community_addHashtagChannelDesc => - 'Dodaj kanał z hashtagiem dla tej społeczności'; + 'Dodaj kanaÅ‚ z hashtagiem dla tej spoÅ‚ecznoÅ›ci'; @override - String get community_selectCommunity => 'Wybierz społeczność'; + String get community_selectCommunity => 'Wybierz spoÅ‚eczność'; @override String get community_regularHashtag => 'Hashtag regular'; @override String get community_regularHashtagDesc => - 'Publiczny hashtag (każdy może dołączyć)'; + 'Publiczny hashtag (każdy może dołączyć)'; @override - String get community_communityHashtag => 'Hashtag Społeczności'; + String get community_communityHashtag => 'Hashtag SpoÅ‚ecznoÅ›ci'; @override String get community_communityHashtagDesc => - 'Dostępne tylko dla członków społeczności'; + 'DostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci'; @override String community_forCommunity(String name) { @@ -2776,10 +2782,10 @@ class AppLocalizationsPl extends AppLocalizations { String get listFilter_sortBy => 'Sortuj po'; @override - String get listFilter_latestMessages => 'Najnowsze wiadomości'; + String get listFilter_latestMessages => 'Najnowsze wiadomoÅ›ci'; @override - String get listFilter_heardRecently => 'Słyszano niedawno'; + String get listFilter_heardRecently => 'SÅ‚yszano niedawno'; @override String get listFilter_az => 'A-Z'; @@ -2797,10 +2803,10 @@ class AppLocalizationsPl extends AppLocalizations { String get listFilter_addToFavorites => 'Dodaj do ulubionych'; @override - String get listFilter_removeFromFavorites => 'Usuń z ulubionych'; + String get listFilter_removeFromFavorites => 'UsuÅ„ z ulubionych'; @override - String get listFilter_users => 'Użytkownicy'; + String get listFilter_users => 'Użytkownicy'; @override String get listFilter_repeaters => 'Powtarzacze'; @@ -2818,45 +2824,46 @@ class AppLocalizationsPl extends AppLocalizations { String get pathTrace_you => 'Ty'; @override - String get pathTrace_failed => 'Śledzenie ścieżki nie powiodło się.'; + String get pathTrace_failed => 'Åšledzenie Å›cieżki nie powiodÅ‚o siÄ™.'; @override - String get pathTrace_notAvailable => 'Ścieżka śledzenia niedostępna.'; + String get pathTrace_notAvailable => 'Åšcieżka Å›ledzenia niedostÄ™pna.'; @override - String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.'; + String get pathTrace_refreshTooltip => 'OdÅ›wież Å›cieżkÄ™.'; @override String get pathTrace_someHopsNoLocation => - 'Jeden lub więcej z chmieli nie ma określonej lokalizacji!'; + 'Jeden lub wiÄ™cej z chmieli nie ma okreÅ›lonej lokalizacji!'; @override - String get pathTrace_clearTooltip => 'Wyczyść ścieżkę'; + String get pathTrace_clearTooltip => 'Wyczyść Å›cieżkÄ™'; @override - String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.'; + String get losSelectStartEnd => + 'Wybierz wÄ™zÅ‚y poczÄ…tkowe i koÅ„cowe dla LOS.'; @override String losRunFailed(String error) { - return 'Sprawdzenie pola widzenia nie powiodło się: $error'; + return 'Sprawdzenie pola widzenia nie powiodÅ‚o siÄ™: $error'; } @override - String get losClearAllPoints => 'Wyczyść wszystkie punkty'; + String get losClearAllPoints => 'Wyczyść wszystkie punkty'; @override String get losRunToViewElevationProfile => - 'Uruchom LOS, aby wyświetlić profil wysokości'; + 'Uruchom LOS, aby wyÅ›wietlić profil wysokoÅ›ci'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty'; + 'Stuknij wÄ™zÅ‚y lub naciÅ›nij i przytrzymaj mapÄ™, aby uzyskać niestandardowe punkty'; @override - String get losShowDisplayNodes => 'Pokaż węzły wyświetlające'; + String get losShowDisplayNodes => 'Pokaż wÄ™zÅ‚y wyÅ›wietlajÄ…ce'; @override String get losCustomPoints => 'Punkty niestandardowe'; @@ -2886,7 +2893,7 @@ class AppLocalizationsPl extends AppLocalizations { String get losRun => 'Uruchom LOS-a'; @override - String get losNoElevationData => 'Brak danych o wysokości'; + String get losNoElevationData => 'Brak danych o wysokoÅ›ci'; @override String losProfileClear( @@ -2895,7 +2902,7 @@ class AppLocalizationsPl extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit'; + return '$distance $distanceUnit, czysty LOS, minimalny przeÅ›wit $clearance $heightUnit'; } @override @@ -2921,42 +2928,42 @@ class AppLocalizationsPl extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.'; + 'Dane dotyczÄ…ce wysokoÅ›ci sÄ… niedostÄ™pne dla jednej lub wiÄ™kszej liczby próbek.'; @override String get losErrorInvalidInput => - 'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.'; + 'NieprawidÅ‚owe dane punktów/wysokoÅ›ci do obliczenia LOS.'; @override - String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego'; + String get losRenameCustomPoint => 'ZmieÅ„ nazwÄ™ punktu niestandardowego'; @override String get losPointName => 'Nazwa punktu'; @override - String get losShowPanelTooltip => 'Pokaż panel LOS'; + String get losShowPanelTooltip => 'Pokaż panel LOS'; @override String get losHidePanelTooltip => 'Ukryj panel LOS'; @override String get losElevationAttribution => - 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; + 'Dane dotyczÄ…ce wysokoÅ›ci: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Horyzont radiowy'; @override - String get losLegendLosBeam => 'Linia widoczności'; + String get losLegendLosBeam => 'Linia widocznoÅ›ci'; @override String get losLegendTerrain => 'Teren'; @override - String get losFrequencyLabel => 'Częstotliwość'; + String get losFrequencyLabel => 'CzÄ™stotliwość'; @override - String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia'; + String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia'; @override String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego'; @@ -2968,48 +2975,48 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; + return 'ZaczynajÄ…c od k=$baselineK przy $baselineFreq MHz, obliczenia korygujÄ… współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; } @override - String get contacts_pathTrace => 'Śledzenie Ścieżek'; + String get contacts_pathTrace => 'Åšledzenie Åšcieżek'; @override - String get contacts_ping => 'Pingować'; + String get contacts_ping => 'Pingować'; @override - String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera'; + String get contacts_repeaterPathTrace => 'Åšledzenie Å›cieżki do repeatera'; @override String get contacts_repeaterPing => 'Repeater pingowy'; @override String get contacts_roomPathTrace => - 'Śledzenie ścieżki do serwera pokojowego'; + 'Åšledzenie Å›cieżki do serwera pokojowego'; @override String get contacts_roomPing => 'Pinguj serwer pokoju'; @override - String get contacts_chatTraceRoute => 'Śledź trasę promienia'; + String get contacts_chatTraceRoute => 'Åšledź trasÄ™ promienia'; @override String contacts_pathTraceTo(String name) { - return 'Śledź trasę do $name'; + return 'Åšledź trasÄ™ do $name'; } @override String get contacts_clipboardEmpty => 'Schowek jest pusty.'; @override - String get contacts_invalidAdvertFormat => 'Nieprawidłowe dane kontaktowe'; + String get contacts_invalidAdvertFormat => 'NieprawidÅ‚owe dane kontaktowe'; @override - String get contacts_contactImported => 'Kontakt został zaimportowany.'; + String get contacts_contactImported => 'Kontakt zostaÅ‚ zaimportowany.'; @override String get contacts_contactImportFailed => - 'Kontakt nie został zaimportowany.'; + 'Kontakt nie zostaÅ‚ zaimportowany.'; @override String get contacts_zeroHopAdvert => 'Reklama Zero Hop'; @@ -3018,7 +3025,7 @@ class AppLocalizationsPl extends AppLocalizations { String get contacts_floodAdvert => 'Reklama powodziowa'; @override - String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka'; + String get contacts_copyAdvertToClipboard => 'Kopiuj ogÅ‚oszenie do schowka'; @override String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka'; @@ -3028,35 +3035,35 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_ShareContactZeroHop => - 'Udostępnij kontakt przez ogłoszenie'; + 'UdostÄ™pnij kontakt przez ogÅ‚oszenie'; @override String get contacts_zeroHopContactAdvertSent => - 'Wysłano kontakt przez ogłoszenie.'; + 'WysÅ‚ano kontakt przez ogÅ‚oszenie.'; @override String get contacts_zeroHopContactAdvertFailed => - 'Nie udało się wysłać kontaktu.'; + 'Nie udaÅ‚o siÄ™ wysÅ‚ać kontaktu.'; @override String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.'; @override String get contacts_contactAdvertCopyFailed => - 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; + 'Kopiowanie ogÅ‚oszenia do schowka nie powiodÅ‚o siÄ™.'; @override - String get notification_activityTitle => 'Aktywność MeshCore'; + String get notification_activityTitle => 'Aktywność MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'wiadomości', - many: 'wiadomości', - few: 'wiadomości', - one: 'wiadomość', + other: 'wiadomoÅ›ci', + many: 'wiadomoÅ›ci', + few: 'wiadomoÅ›ci', + one: 'wiadomość', ); return '$count $_temp0'; } @@ -3066,10 +3073,10 @@ class AppLocalizationsPl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'wiadomości kanału', - many: 'wiadomości kanału', - few: 'wiadomości kanału', - one: 'wiadomość kanału', + other: 'wiadomoÅ›ci kanaÅ‚u', + many: 'wiadomoÅ›ci kanaÅ‚u', + few: 'wiadomoÅ›ci kanaÅ‚u', + one: 'wiadomość kanaÅ‚u', ); return '$count $_temp0'; } @@ -3079,10 +3086,10 @@ class AppLocalizationsPl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'nowych węzłów', - many: 'nowych węzłów', - few: 'nowe węzły', - one: 'nowy węzeł', + other: 'nowych wÄ™złów', + many: 'nowych wÄ™złów', + few: 'nowe wÄ™zÅ‚y', + one: 'nowy wÄ™zeÅ‚', ); return '$count $_temp0'; } @@ -3093,53 +3100,55 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get notification_receivedNewMessage => 'Otrzymano nową wiadomość'; + String get notification_receivedNewMessage => 'Otrzymano nowÄ… wiadomość'; @override String get settings_gpxExportRepeaters => - 'Eksportuj powtórki / serwer pokojowy do GPX'; + 'Eksportuj powtórki / serwer pokojowy do GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.'; + 'Eksportuje powtarzacze / roomserver z lokalizacjÄ… do pliku GPX.'; @override String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Eksportuje towarzyszy z lokalizacją do pliku GPX.'; + 'Eksportuje towarzyszy z lokalizacjÄ… do pliku GPX.'; @override String get settings_gpxExportAll => 'Eksportuj wszystkie kontakty do GPX'; @override String get settings_gpxExportAllSubtitle => - 'Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.'; + 'Eksportuje wszystkie kontakty z lokalizacjÄ… do pliku GPX.'; @override - String get settings_gpxExportSuccess => 'Pomyślnie wyeksportowano plik GPX.'; + String get settings_gpxExportSuccess => 'PomyÅ›lnie wyeksportowano plik GPX.'; @override String get settings_gpxExportNoContacts => - 'Brak kontaktów do wyeksportowania.'; + 'Brak kontaktów do wyeksportowania.'; @override String get settings_gpxExportNotAvailable => - 'Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym'; + 'Nie obsÅ‚ugiwane na Twoim urzÄ…dzeniu/systemie operacyjnym'; @override - String get settings_gpxExportError => 'Wystąpił błąd podczas eksportowania.'; + String get settings_gpxExportError => + 'WystÄ…piÅ‚ błąd podczas eksportowania.'; @override String get settings_gpxExportRepeatersRoom => - 'Lokalizacje serwerów powtarzających i pomieszczeń'; + 'Lokalizacje serwerów powtarzajÄ…cych i pomieszczeÅ„'; @override String get settings_gpxExportChat => 'Lokalizacje towarzyszy'; @override - String get settings_gpxExportAllContacts => 'Wszystkie lokalizacje kontaktów'; + String get settings_gpxExportAllContacts => + 'Wszystkie lokalizacje kontaktów'; @override String get settings_gpxExportShareText => @@ -3150,7 +3159,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Eksport danych mapy GPX meshcore-open'; @override - String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu'; + String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu'; @override String get snrIndicator_lastSeen => 'Ostatnio widziany'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 1bf5647..2a2f9c2 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -48,7 +48,7 @@ class AppLocalizationsPt extends AppLocalizations { String get common_add => 'Adicionar'; @override - String get common_settings => 'Configurações'; + String get common_settings => 'Configurações'; @override String get common_disconnect => 'Desconectar'; @@ -93,7 +93,7 @@ class AppLocalizationsPt extends AppLocalizations { String get common_loading => 'Carregando...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Escolha o método de conexão desejado.'; - - @override - String get connectionChoiceSubtitle => - 'Selecione a forma como você deseja acessar seu dispositivo MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -126,14 +119,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get usbScreenSubtitle => - 'Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.'; + 'Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.'; @override String get usbScreenStatus => 'Selecione um dispositivo USB'; @override String get usbScreenNote => - 'A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.'; + 'A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.'; @override String get usbScreenEmptyState => @@ -149,7 +142,7 @@ class AppLocalizationsPt extends AppLocalizations { String get scanner_disconnecting => 'Desconectando...'; @override - String get scanner_notConnected => 'Não está conectado'; + String get scanner_notConnected => 'Não está conectado'; @override String scanner_connectedTo(String deviceName) { @@ -165,7 +158,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Falha na conexão: $error'; + return 'Falha na conexão: $error'; } @override @@ -175,18 +168,18 @@ class AppLocalizationsPt extends AppLocalizations { String get scanner_scan => 'Digitalizar'; @override - String get scanner_bluetoothOff => 'Bluetooth está desativado'; + String get scanner_bluetoothOff => 'Bluetooth está desativado'; @override String get scanner_bluetoothOffMessage => 'Por favor, ative o Bluetooth para escanear por dispositivos.'; @override - String get scanner_chromeRequired => 'Navegador Chrome necessário'; + String get scanner_chromeRequired => 'Navegador Chrome necessário'; @override String get scanner_chromeRequiredMessage => - 'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.'; + 'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.'; @override String get scanner_enableBluetooth => 'Ative o Bluetooth'; @@ -198,66 +191,66 @@ class AppLocalizationsPt extends AppLocalizations { String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Configurações'; + String get settings_title => 'Configurações'; @override - String get settings_deviceInfo => 'Informações do Dispositivo'; + String get settings_deviceInfo => 'Informações do Dispositivo'; @override - String get settings_appSettings => 'Configurações do App'; + String get settings_appSettings => 'Configurações do App'; @override String get settings_appSettingsSubtitle => - 'Notificações, mensagens e preferências de mapa'; + 'Notificações, mensagens e preferências de mapa'; @override - String get settings_nodeSettings => 'Configurações do Nó'; + String get settings_nodeSettings => 'Configurações do Nó'; @override - String get settings_nodeName => 'Nome do Nó'; + String get settings_nodeName => 'Nome do Nó'; @override - String get settings_nodeNameNotSet => 'Não definido'; + String get settings_nodeNameNotSet => 'Não definido'; @override - String get settings_nodeNameHint => 'Insira o nome do nó'; + String get settings_nodeNameHint => 'Insira o nome do nó'; @override String get settings_nodeNameUpdated => 'Nome atualizado'; @override - String get settings_radioSettings => 'Configurações de Rádio'; + String get settings_radioSettings => 'Configurações de Rádio'; @override String get settings_radioSettingsSubtitle => - 'Frequência, potência, fator de espalhamento'; + 'Frequência, potência, fator de espalhamento'; @override String get settings_radioSettingsUpdated => - 'Configurações de rádio atualizadas'; + 'Configurações de rádio atualizadas'; @override - String get settings_location => 'Localização'; + String get settings_location => 'Localização'; @override String get settings_locationSubtitle => 'Coordenadas GPS'; @override - String get settings_locationUpdated => 'Localização atualizada'; + String get settings_locationUpdated => 'Localização atualizada'; @override String get settings_locationBothRequired => 'Insira a latitude e a longitude.'; @override - String get settings_locationInvalid => 'Latitude ou longitude inválidos.'; + String get settings_locationInvalid => 'Latitude ou longitude inválidos.'; @override String get settings_locationGPSEnable => 'Ativar GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Habilita a atualização automática da localização via GPS.'; + 'Habilita a atualização automática da localização via GPS.'; @override String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)'; @@ -277,11 +270,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_privacyModeSubtitle => - 'Esconder nome/localização em anúncios'; + 'Esconder nome/localização em anúncios'; @override String get settings_privacyModeToggle => - 'Ative o modo de privacidade para ocultar seu nome e localização em anúncios.'; + 'Ative o modo de privacidade para ocultar seu nome e localização em anúncios.'; @override String get settings_privacyModeEnabled => 'Modo de privacidade ativado'; @@ -290,24 +283,24 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_privacyModeDisabled => 'Modo de privacidade desativado'; @override - String get settings_actions => 'Ações'; + String get settings_actions => 'Ações'; @override String get settings_sendAdvertisement => 'Enviar Publicidade'; @override String get settings_sendAdvertisementSubtitle => - 'Presença de transmissão agora'; + 'Presença de transmissão agora'; @override - String get settings_advertisementSent => 'Anúncio enviado'; + String get settings_advertisementSent => 'Anúncio enviado'; @override - String get settings_syncTime => 'Tempo de Sincronização'; + String get settings_syncTime => 'Tempo de Sincronização'; @override String get settings_syncTimeSubtitle => - 'Definir o relógio do dispositivo para o horário do telefone'; + 'Definir o relógio do dispositivo para o horário do telefone'; @override String get settings_timeSynchronized => 'Sincronizado com o tempo'; @@ -328,24 +321,24 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - 'Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.'; + 'Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.'; @override String get settings_debug => 'Depurar'; @override - String get settings_bleDebugLog => 'Log de Depuração BLE'; + String get settings_bleDebugLog => 'Log de Depuração BLE'; @override String get settings_bleDebugLogSubtitle => 'Comandos, respostas e dados brutos do BLE'; @override - String get settings_appDebugLog => 'Log de Depuração do Aplicativo'; + String get settings_appDebugLog => 'Log de Depuração do Aplicativo'; @override String get settings_appDebugLogSubtitle => - 'Mensagens de depuração do aplicativo'; + 'Mensagens de depuração do aplicativo'; @override String get settings_about => 'Sobre'; @@ -356,15 +349,16 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get settings_aboutLegalese => 'Projeto MeshCore de Código Aberto 2024'; + String get settings_aboutLegalese => + 'Projeto MeshCore de Código Aberto 2024'; @override String get settings_aboutDescription => - 'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.'; + 'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Dados de elevação LOS: Open-Meteo (CC BY 4.0)'; + 'Dados de elevação LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Nome'; @@ -379,46 +373,47 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_infoBattery => 'Bateria'; @override - String get settings_infoPublicKey => 'Chave Pública'; + String get settings_infoPublicKey => 'Chave Pública'; @override - String get settings_infoContactsCount => 'Número de Contatos'; + String get settings_infoContactsCount => 'Número de Contatos'; @override - String get settings_infoChannelCount => 'Número do Canal'; + String get settings_infoChannelCount => 'Número do Canal'; @override String get settings_presets => 'Presets'; @override - String get settings_frequency => 'Frequência (MHz)'; + String get settings_frequency => 'Frequência (MHz)'; @override String get settings_frequencyHelper => '300,0 - 2500,0'; @override - String get settings_frequencyInvalid => 'Frequência inválida (300-2500 MHz)'; + String get settings_frequencyInvalid => + 'Frequência inválida (300-2500 MHz)'; @override String get settings_bandwidth => 'Largura de banda'; @override - String get settings_spreadingFactor => 'Fator de Dispersão'; + String get settings_spreadingFactor => 'Fator de Dispersão'; @override - String get settings_codingRate => 'Taxa de Codificação'; + String get settings_codingRate => 'Taxa de Codificação'; @override - String get settings_txPower => 'TX Potência (dBm)'; + String get settings_txPower => 'TX Potência (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)'; + String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)'; @override - String get settings_clientRepeat => 'Repetição sem rede'; + String get settings_clientRepeat => 'Repetição sem rede'; @override String get settings_clientRepeatSubtitle => @@ -426,7 +421,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_clientRepeatFreqWarning => - 'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.'; + 'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { @@ -434,16 +429,16 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get appSettings_title => 'Configurações do App'; + String get appSettings_title => 'Configurações do App'; @override - String get appSettings_appearance => 'Aparência'; + String get appSettings_appearance => 'Aparência'; @override String get appSettings_theme => 'Tema'; @override - String get appSettings_themeSystem => 'Padrão do sistema'; + String get appSettings_themeSystem => 'Padrão do sistema'; @override String get appSettings_themeLight => 'Luz'; @@ -455,16 +450,16 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_language => 'Idioma'; @override - String get appSettings_languageSystem => 'Padrão do sistema'; + String get appSettings_languageSystem => 'Padrão do sistema'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -473,16 +468,16 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -491,10 +486,10 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russo'; @@ -511,87 +506,87 @@ class AppLocalizationsPt extends AppLocalizations { 'Mostrar metadados detalhados de roteamento e tempo para as mensagens'; @override - String get appSettings_notifications => 'Notificações'; + String get appSettings_notifications => 'Notificações'; @override - String get appSettings_enableNotifications => 'Ativar Notificações'; + String get appSettings_enableNotifications => 'Ativar Notificações'; @override String get appSettings_enableNotificationsSubtitle => - 'Receber notificações para mensagens e anúncios'; + 'Receber notificações para mensagens e anúncios'; @override String get appSettings_notificationPermissionDenied => - 'Permissão de notificação negada'; + 'Permissão de notificação negada'; @override - String get appSettings_notificationsEnabled => 'Notificações ativadas'; + String get appSettings_notificationsEnabled => 'Notificações ativadas'; @override - String get appSettings_notificationsDisabled => 'Notificações desativadas'; + String get appSettings_notificationsDisabled => 'Notificações desativadas'; @override - String get appSettings_messageNotifications => 'Notificações de Mensagem'; + String get appSettings_messageNotifications => 'Notificações de Mensagem'; @override String get appSettings_messageNotificationsSubtitle => - 'Mostrar notificação ao receber novas mensagens'; + 'Mostrar notificação ao receber novas mensagens'; @override String get appSettings_channelMessageNotifications => - 'Notificações de Mensagens do Canal'; + 'Notificações de Mensagens do Canal'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Mostrar notificação ao receber mensagens do canal'; + 'Mostrar notificação ao receber mensagens do canal'; @override String get appSettings_advertisementNotifications => - 'Notificações de Anúncios'; + 'Notificações de Anúncios'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Mostrar notificação quando novos nós forem descobertos'; + 'Mostrar notificação quando novos nós forem descobertos'; @override String get appSettings_messaging => 'Mensagens'; @override String get appSettings_clearPathOnMaxRetry => - 'Limpar Caminho em Tentativas Máximas'; + 'Limpar Caminho em Tentativas Máximas'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Redefinir o caminho de contato após 5 tentativas de envio falhas'; + 'Redefinir o caminho de contato após 5 tentativas de envio falhas'; @override String get appSettings_pathsWillBeCleared => - 'Os caminhos serão limpos após 5 tentativas falhas.'; + 'Os caminhos serão limpos após 5 tentativas falhas.'; @override String get appSettings_pathsWillNotBeCleared => - 'Os caminhos não serão limpos automaticamente.'; + 'Os caminhos não serão limpos automaticamente.'; @override - String get appSettings_autoRouteRotation => 'Rotação de Rota Automática'; + String get appSettings_autoRouteRotation => 'Rotação de Rota Automática'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alternar entre os melhores caminhos e o modo inundação'; + 'Alternar entre os melhores caminhos e o modo inundação'; @override String get appSettings_autoRouteRotationEnabled => - 'Rotação de roteamento automático habilitada'; + 'Rotação de roteamento automático habilitada'; @override String get appSettings_autoRouteRotationDisabled => - 'Rotação de roteamento automático desativada'; + 'Rotação de roteamento automático desativada'; @override String get appSettings_battery => 'Bateria'; @override - String get appSettings_batteryChemistry => 'Química da Bateria'; + String get appSettings_batteryChemistry => 'Química da Bateria'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { @@ -612,37 +607,37 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override - String get appSettings_mapDisplay => 'Exibição do Mapa'; + String get appSettings_mapDisplay => 'Exibição do Mapa'; @override String get appSettings_showRepeaters => 'Mostrar Repetidores'; @override String get appSettings_showRepeatersSubtitle => - 'Exibir nós de repetidor no mapa'; + 'Exibir nós de repetidor no mapa'; @override - String get appSettings_showChatNodes => 'Mostrar Nós de Chat'; + String get appSettings_showChatNodes => 'Mostrar Nós de Chat'; @override - String get appSettings_showChatNodesSubtitle => 'Exibir nós de chat no mapa'; + String get appSettings_showChatNodesSubtitle => 'Exibir nós de chat no mapa'; @override - String get appSettings_showOtherNodes => 'Mostrar Outros Nós'; + String get appSettings_showOtherNodes => 'Mostrar Outros Nós'; @override String get appSettings_showOtherNodesSubtitle => - 'Exibir outros tipos de nó no mapa'; + 'Exibir outros tipos de nó no mapa'; @override String get appSettings_timeFilter => 'Filtro de Tempo'; @override - String get appSettings_timeFilterShowAll => 'Mostrar todos os nós'; + String get appSettings_timeFilterShowAll => 'Mostrar todos os nós'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Mostrar nós das últimas $hours horas'; + return 'Mostrar nós das últimas $hours horas'; } @override @@ -650,22 +645,22 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_showNodesDiscoveredWithin => - 'Mostrar nós descobertos dentro de:'; + 'Mostrar nós descobertos dentro de:'; @override String get appSettings_allTime => 'Todos os tempos'; @override - String get appSettings_lastHour => 'Última hora'; + String get appSettings_lastHour => 'Última hora'; @override - String get appSettings_last6Hours => 'Últimos 6 horas'; + String get appSettings_last6Hours => 'Últimos 6 horas'; @override - String get appSettings_last24Hours => 'Últimas 24 horas'; + String get appSettings_last24Hours => 'Últimas 24 horas'; @override - String get appSettings_lastWeek => 'Da última semana'; + String get appSettings_lastWeek => 'Da última semana'; @override String get appSettings_offlineMapCache => 'Cache de Mapa Offline'; @@ -674,17 +669,17 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_unitsTitle => 'Unidades'; @override - String get appSettings_unitsMetric => 'Métrico (m/km)'; + String get appSettings_unitsMetric => 'Métrico (m/km)'; @override String get appSettings_unitsImperial => 'Imperial (ft/mi)'; @override - String get appSettings_noAreaSelected => 'Nenhuma área selecionada'; + String get appSettings_noAreaSelected => 'Nenhuma área selecionada'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Área selecionada (zoom $minZoom-$maxZoom)'; + return 'Área selecionada (zoom $minZoom-$maxZoom)'; } @override @@ -692,32 +687,32 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_appDebugLogging => - 'Rastreamento de Depuração do Aplicativo'; + 'Rastreamento de Depuração do Aplicativo'; @override String get appSettings_appDebugLoggingSubtitle => - 'Registrar mensagens de depuração do aplicativo Log para solucionar problemas'; + 'Registrar mensagens de depuração do aplicativo Log para solucionar problemas'; @override String get appSettings_appDebugLoggingEnabled => - 'Log de depuração do aplicativo habilitado'; + 'Log de depuração do aplicativo habilitado'; @override String get appSettings_appDebugLoggingDisabled => - 'O registro de depuração do aplicativo está desativado.'; + 'O registro de depuração do aplicativo está desativado.'; @override String get contacts_title => 'Contactos'; @override - String get contacts_noContacts => 'Ainda não existem contatos.'; + String get contacts_noContacts => 'Ainda não existem contatos.'; @override String get contacts_contactsWillAppear => - 'Os contatos serão exibidos quando os dispositivos anunciarem.'; + 'Os contatos serão exibidos quando os dispositivos anunciarem.'; @override - String get contacts_unread => 'Não lido'; + String get contacts_unread => 'Não lido'; @override String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...'; @@ -734,7 +729,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String contacts_searchUsers(int number, String str) { - return 'Pesquisar $number$str Usuários...'; + return 'Pesquisar $number$str Usuários...'; } @override @@ -748,11 +743,11 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get contacts_noUnreadContacts => 'Sem contatos não lidos.'; + String get contacts_noUnreadContacts => 'Sem contatos não lidos.'; @override String get contacts_noContactsFound => - 'Não foram encontrados contatos ou grupos.'; + 'Não foram encontrados contatos ou grupos.'; @override String get contacts_deleteContact => 'Excluir Contato'; @@ -792,11 +787,11 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_groupName => 'Nome do grupo'; @override - String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.'; + String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.'; @override String contacts_groupAlreadyExists(String name) { - return 'O grupo \"$name\" já existe'; + return 'O grupo \"$name\" já existe'; } @override @@ -804,43 +799,46 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Não existem contatos que correspondam ao seu filtro'; + 'Não existem contatos que correspondam ao seu filtro'; @override String get contacts_noMembers => 'Nenhum membro'; @override - String get contacts_lastSeenNow => 'Última vez que foi visto agora'; + String get contacts_lastSeenNow => 'Última vez que foi visto agora'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Última vez que foi visto $minutes minutos atrás'; + return 'Última vez que foi visto $minutes minutos atrás'; } @override - String get contacts_lastSeenHourAgo => 'Última vez que foi visto há 1 hora.'; + String get contacts_lastSeenHourAgo => + 'Última vez que foi visto há 1 hora.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Última vez visto $hours horas atrás'; + return 'Última vez visto $hours horas atrás'; } @override - String get contacts_lastSeenDayAgo => 'Última vez que foi visto 1 dia atrás'; + String get contacts_lastSeenDayAgo => + 'Última vez que foi visto 1 dia atrás'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Última vez visto $days dias atrás'; + return 'Última vez visto $days dias atrás'; } @override String get channels_title => 'Canais'; @override - String get channels_noChannelsConfigured => 'Nenhuma canalização configurada'; + String get channels_noChannelsConfigured => + 'Nenhuma canalização configurada'; @override - String get channels_addPublicChannel => 'Adicionar Canal Público'; + String get channels_addPublicChannel => 'Adicionar Canal Público'; @override String get channels_searchChannels => 'Pesquisar canais...'; @@ -857,13 +855,13 @@ class AppLocalizationsPt extends AppLocalizations { String get channels_hashtagChannel => 'Canal com hashtag'; @override - String get channels_public => 'Público'; + String get channels_public => 'Público'; @override String get channels_private => 'Privado'; @override - String get channels_publicChannel => 'Canal público'; + String get channels_publicChannel => 'Canal público'; @override String get channels_privateChannel => 'Canal privado'; @@ -882,7 +880,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return 'Excluir \"$name\"? Não pode ser desfeito.'; + return 'Excluir \"$name\"? Não pode ser desfeito.'; } @override @@ -892,29 +890,29 @@ class AppLocalizationsPt extends AppLocalizations { @override String channels_channelDeleted(String name) { - return 'Canal \"$name\" excluído'; + return 'Canal \"$name\" excluído'; } @override String get channels_addChannel => 'Adicionar Canal'; @override - String get channels_channelIndexLabel => 'Índice do Canal'; + String get channels_channelIndexLabel => 'Índice do Canal'; @override String get channels_channelName => 'Nome do Canal'; @override - String get channels_usePublicChannel => 'Usar Canal Público'; + String get channels_usePublicChannel => 'Usar Canal Público'; @override - String get channels_standardPublicPsk => 'PSK público padrão'; + String get channels_standardPublicPsk => 'PSK público padrão'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Gerar PSK aleatório'; + String get channels_generateRandomPsk => 'Gerar PSK aleatório'; @override String get channels_enterChannelName => 'Por favor, insira um nome de canal'; @@ -934,7 +932,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get channels_smazCompression => 'Compressão SMAZ'; + String get channels_smazCompression => 'Compressão SMAZ'; @override String channels_channelUpdated(String name) { @@ -942,7 +940,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Canal público adicionado'; + String get channels_publicChannelAdded => 'Canal público adicionado'; @override String get channels_sortBy => 'Ordenar por'; @@ -954,10 +952,10 @@ class AppLocalizationsPt extends AppLocalizations { String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Últimas mensagens'; + String get channels_sortLatestMessages => 'Últimas mensagens'; @override - String get channels_sortUnread => 'Não lido'; + String get channels_sortUnread => 'Não lido'; @override String get channels_createPrivateChannel => 'Criar um Canal Privado'; @@ -974,7 +972,7 @@ class AppLocalizationsPt extends AppLocalizations { 'Inserir uma chave secreta manualmente.'; @override - String get channels_joinPublicChannel => 'Junte-se ao Canal Público'; + String get channels_joinPublicChannel => 'Junte-se ao Canal Público'; @override String get channels_joinPublicChannelDesc => @@ -988,7 +986,7 @@ class AppLocalizationsPt extends AppLocalizations { 'Qualquer pessoa pode participar de canais com hashtag.'; @override - String get channels_scanQrCode => 'Digitalizar um Código QR'; + String get channels_scanQrCode => 'Digitalizar um Código QR'; @override String get channels_scanQrCodeComingSoon => 'Em breve'; @@ -1000,13 +998,14 @@ class AppLocalizationsPt extends AppLocalizations { String get channels_hashtagHint => 'ex. #equipe'; @override - String get chat_noMessages => 'Ainda não existem mensagens.'; + String get chat_noMessages => 'Ainda não existem mensagens.'; @override - String get chat_sendMessageToStart => 'Enviar uma mensagem para começar'; + String get chat_sendMessageToStart => 'Enviar uma mensagem para começar'; @override - String get chat_originalMessageNotFound => 'Mensagem original não encontrada'; + String get chat_originalMessageNotFound => + 'Mensagem original não encontrada'; @override String chat_replyingTo(String name) { @@ -1019,7 +1018,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get chat_location => 'Localização'; + String get chat_location => 'Localização'; @override String chat_sendMessageTo(String contactName) { @@ -1031,14 +1030,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return 'Mensagem muito longa (máximo $maxBytes bytes).'; + return 'Mensagem muito longa (máximo $maxBytes bytes).'; } @override String get chat_messageCopied => 'Mensagem copiada'; @override - String get chat_messageDeleted => 'Mensagem excluída'; + String get chat_messageDeleted => 'Mensagem excluída'; @override String get chat_retryingMessage => 'Tentando novamente'; @@ -1055,7 +1054,7 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_reply => 'Responder'; @override - String get chat_addReaction => 'Adicionar Reação'; + String get chat_addReaction => 'Adicionar Reação'; @override String get chat_me => 'Eu'; @@ -1067,7 +1066,7 @@ class AppLocalizationsPt extends AppLocalizations { String get emojiCategoryGestures => 'Gestos'; @override - String get emojiCategoryHearts => 'Corações'; + String get emojiCategoryHearts => 'Corações'; @override String get emojiCategoryObjects => 'Objetos'; @@ -1085,19 +1084,19 @@ class AppLocalizationsPt extends AppLocalizations { String get gifPicker_noGifsFound => 'Nenhum GIF encontrado'; @override - String get gifPicker_failedLoad => 'Não foi possível carregar os GIFs'; + String get gifPicker_failedLoad => 'Não foi possível carregar os GIFs'; @override String get gifPicker_failedSearch => 'Falha na pesquisa de GIFs'; @override - String get gifPicker_noInternet => 'Sem conexão com a internet'; + String get gifPicker_noInternet => 'Sem conexão com a internet'; @override - String get debugLog_appTitle => 'Log de Depuração do Aplicativo'; + String get debugLog_appTitle => 'Log de Depuração do Aplicativo'; @override - String get debugLog_bleTitle => 'Log de Depuração BLE'; + String get debugLog_bleTitle => 'Log de Depuração BLE'; @override String get debugLog_copyLog => 'Copiar log'; @@ -1106,17 +1105,17 @@ class AppLocalizationsPt extends AppLocalizations { String get debugLog_clearLog => 'Limpar log'; @override - String get debugLog_copied => 'Log de depuração copiado'; + String get debugLog_copied => 'Log de depuração copiado'; @override String get debugLog_bleCopied => 'Log BLE copiado'; @override - String get debugLog_noEntries => 'Ainda não existem logs de depuração.'; + String get debugLog_noEntries => 'Ainda não existem logs de depuração.'; @override String get debugLog_enableInSettings => - 'Ativar o log de depuração do aplicativo nas configurações'; + 'Ativar o log de depuração do aplicativo nas configurações'; @override String get debugLog_frames => 'Estruturas'; @@ -1125,7 +1124,7 @@ class AppLocalizationsPt extends AppLocalizations { String get debugLog_rawLogRx => 'Log Raw-RX'; @override - String get debugLog_noBleActivity => 'Ainda não há atividade BLE.'; + String get debugLog_noBleActivity => 'Ainda não há atividade BLE.'; @override String debugFrame_length(int count) { @@ -1172,7 +1171,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get debugFrame_hexDump => 'Espaço Hexadecimal:'; + String get debugFrame_hexDump => 'Espaço Hexadecimal:'; @override String get chat_pathManagement => 'Gerenciamento de Caminhos'; @@ -1187,14 +1186,14 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_autoUseSavedPath => 'Auto (usar caminho salvo)'; @override - String get chat_forceFloodMode => 'Modo de Inundação Forçado'; + String get chat_forceFloodMode => 'Modo de Inundação Forçado'; @override String get chat_recentAckPaths => 'Rotas de ACK Recentes (toque para usar):'; @override String get chat_pathHistoryFull => - 'O histórico está cheio. Remova entradas para adicionar novas.'; + 'O histórico está cheio. Remova entradas para adicionar novas.'; @override String get chat_hopSingular => 'pule'; @@ -1221,10 +1220,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.'; + 'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.'; @override - String get chat_pathActions => 'Ações do Caminho:'; + String get chat_pathActions => 'Ações do Caminho:'; @override String get chat_setCustomPath => 'Definir Caminho Personalizado'; @@ -1238,11 +1237,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_clearPathSubtitle => - 'Forçar a descoberta na próxima transmissão'; + 'Forçar a descoberta na próxima transmissão'; @override String get chat_pathCleared => - 'Caminho limpo. A próxima mensagem redescobrirá a rota.'; + 'Caminho limpo. A próxima mensagem redescobrirá a rota.'; @override String get chat_floodModeSubtitle => @@ -1250,14 +1249,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.'; + 'Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.'; @override String get chat_fullPath => 'Caminho Completo'; @override String get chat_pathDetailsNotAvailable => - 'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.'; + 'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1278,7 +1277,8 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_pathDeviceConfirmed => 'Dispositivo confirmado.'; @override - String get chat_pathDeviceNotConfirmed => 'Dispositivo ainda não confirmado.'; + String get chat_pathDeviceNotConfirmed => + 'Dispositivo ainda não confirmado.'; @override String get chat_type => 'Digite'; @@ -1287,24 +1287,24 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_path => 'Caminho'; @override - String get chat_publicKey => 'Chave Pública'; + String get chat_publicKey => 'Chave Pública'; @override String get chat_compressOutgoingMessages => 'Comprimir mensagens enviadas'; @override - String get chat_floodForced => 'Inundação (forçada)'; + String get chat_floodForced => 'Inundação (forçada)'; @override - String get chat_directForced => 'Direto (forçado)'; + String get chat_directForced => 'Direto (forçado)'; @override String chat_hopsForced(int count) { - return '$count saltos (forçado)'; + return '$count saltos (forçado)'; } @override - String get chat_floodAuto => 'Inundação (automática)'; + String get chat_floodAuto => 'Inundação (automática)'; @override String get chat_direct => 'Salvar'; @@ -1314,7 +1314,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String chat_unread(int count) { - return 'Não lido: $count'; + return 'Não lido: $count'; } @override @@ -1329,32 +1329,32 @@ class AppLocalizationsPt extends AppLocalizations { @override String chat_couldNotOpenLink(String url) { - return 'Não foi possível abrir o link: $url'; + return 'Não foi possível abrir o link: $url'; } @override - String get chat_invalidLink => 'Formato de link inválido'; + String get chat_invalidLink => 'Formato de link inválido'; @override - String get map_title => 'Mapa de Nós'; + String get map_title => 'Mapa de Nós'; @override - String get map_lineOfSight => 'Linha de visão'; + String get map_lineOfSight => 'Linha de visão'; @override - String get map_losScreenTitle => 'Linha de visão'; + String get map_losScreenTitle => 'Linha de visão'; @override String get map_noNodesWithLocation => - 'Não existem nós com dados de localização.'; + 'Não existem nós com dados de localização.'; @override String get map_nodesNeedGps => - 'Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa'; + 'Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa'; @override String map_nodesCount(int count) { - return 'Nós: $count'; + return 'Nós: $count'; } @override @@ -1381,10 +1381,10 @@ class AppLocalizationsPt extends AppLocalizations { String get map_pinPrivate => 'Bloquear (Privado)'; @override - String get map_pinPublic => 'Pin (Público)'; + String get map_pinPublic => 'Pin (Público)'; @override - String get map_lastSeen => 'Última Visão'; + String get map_lastSeen => 'Última Visão'; @override String get map_disconnectConfirm => @@ -1403,10 +1403,10 @@ class AppLocalizationsPt extends AppLocalizations { String get map_shareMarkerHere => 'Compartilhar marcador aqui'; @override - String get map_pinLabel => 'Rótulo de marcador'; + String get map_pinLabel => 'Rótulo de marcador'; @override - String get map_label => 'Rótulo'; + String get map_label => 'Rótulo'; @override String get map_pointOfInterest => 'Ponto de interesse'; @@ -1418,14 +1418,14 @@ class AppLocalizationsPt extends AppLocalizations { String get map_sendToChannel => 'Enviar para o canal'; @override - String get map_noChannelsAvailable => 'Não existem canais disponíveis.'; + String get map_noChannelsAvailable => 'Não existem canais disponíveis.'; @override - String get map_publicLocationShare => 'Compartilhar local público'; + String get map_publicLocationShare => 'Compartilhar local público'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Você está prestes a compartilhar uma localização em $channelLabel. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.'; + return 'Você está prestes a compartilhar uma localização em $channelLabel. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.'; } @override @@ -1433,19 +1433,19 @@ class AppLocalizationsPt extends AppLocalizations { 'Conecte-se a um dispositivo para compartilhar marcadores'; @override - String get map_filterNodes => 'Filtrar Nós'; + String get map_filterNodes => 'Filtrar Nós'; @override - String get map_nodeTypes => 'Tipos de Nó'; + String get map_nodeTypes => 'Tipos de Nó'; @override - String get map_chatNodes => 'Nós de Chat'; + String get map_chatNodes => 'Nós de Chat'; @override String get map_repeaters => 'Repetidores'; @override - String get map_otherNodes => 'Outros Nós'; + String get map_otherNodes => 'Outros Nós'; @override String get map_keyPrefix => 'Prefixo Chave'; @@ -1454,7 +1454,7 @@ class AppLocalizationsPt extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtrar por prefixo-chave'; @override - String get map_publicKeyPrefix => 'Prefixo de chave pública'; + String get map_publicKeyPrefix => 'Prefixo de chave pública'; @override String get map_markers => 'Marcadores'; @@ -1463,25 +1463,25 @@ class AppLocalizationsPt extends AppLocalizations { String get map_showSharedMarkers => 'Mostrar marcadores compartilhados'; @override - String get map_lastSeenTime => 'Último Tempo de Visualização'; + String get map_lastSeenTime => 'Último Tempo de Visualização'; @override String get map_sharedPin => 'Pin compartilhado'; @override - String get map_joinRoom => 'Junte-se à Sala'; + String get map_joinRoom => 'Junte-se à Sala'; @override String get map_manageRepeater => 'Gerenciar Repetidor'; @override - String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.'; + String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.'; @override - String get map_runTrace => 'Executar Traçado de Caminho'; + String get map_runTrace => 'Executar Traçado de Caminho'; @override - String get map_removeLast => 'Remover Último'; + String get map_removeLast => 'Remover Último'; @override String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.'; @@ -1491,11 +1491,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get mapCache_selectAreaFirst => - 'Selecione uma área para armazenar em cache primeiro'; + 'Selecione uma área para armazenar em cache primeiro'; @override String get mapCache_noTilesToDownload => - 'Não há tiles para baixar para esta área.'; + 'Não há tiles para baixar para esta área.'; @override String get mapCache_downloadTilesTitle => 'Baixar tiles'; @@ -1529,13 +1529,13 @@ class AppLocalizationsPt extends AppLocalizations { String get mapCache_offlineCacheCleared => 'Cache offline limpa'; @override - String get mapCache_noAreaSelected => 'Nenhuma área selecionada'; + String get mapCache_noAreaSelected => 'Nenhuma área selecionada'; @override - String get mapCache_cacheArea => 'Área de Cache'; + String get mapCache_cacheArea => 'Área de Cache'; @override - String get mapCache_useCurrentView => 'Usar a Visualização Atual'; + String get mapCache_useCurrentView => 'Usar a Visualização Atual'; @override String get mapCache_zoomRange => 'Intervalo de Zoom'; @@ -1576,17 +1576,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String time_minutesAgo(int minutes) { - return '$minutes minutos atrás'; + return '$minutes minutos atrás'; } @override String time_hoursAgo(int hours) { - return '${hours}h atrás'; + return '${hours}h atrás'; } @override String time_daysAgo(int days) { - return '$days dias atrás'; + return '$days dias atrás'; } @override @@ -1608,7 +1608,7 @@ class AppLocalizationsPt extends AppLocalizations { String get time_weeks => 'semanas'; @override - String get time_month => 'mês'; + String get time_month => 'mês'; @override String get time_months => 'meses'; @@ -1643,15 +1643,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'A senha será armazenada com segurança neste dispositivo.'; + 'A senha será armazenada com segurança neste dispositivo.'; @override String get login_repeaterDescription => - 'Insira a senha do repetidor para acessar as configurações e o status.'; + 'Insira a senha do repetidor para acessar as configurações e o status.'; @override String get login_roomDescription => - 'Insira a senha da sala para acessar as configurações e o status.'; + 'Insira a senha da sala para acessar as configurações e o status.'; @override String get login_routing => 'Rotas'; @@ -1663,7 +1663,7 @@ class AppLocalizationsPt extends AppLocalizations { String get login_autoUseSavedPath => 'Auto (usar caminho salvo)'; @override - String get login_forceFloodMode => 'Modo de Inundação Forçado'; + String get login_forceFloodMode => 'Modo de Inundação Forçado'; @override String get login_managePaths => 'Gerenciar Caminhos'; @@ -1683,7 +1683,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get login_failedMessage => - 'Falha no login. A senha está incorreta ou o repetidor está inacessível.'; + 'Falha no login. A senha está incorreta ou o repetidor está inacessível.'; @override String get common_reload => 'Recarregar'; @@ -1715,38 +1715,38 @@ class AppLocalizationsPt extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.'; + 'Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)'; + 'A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)'; @override String get path_labelHexPrefixes => 'Prefixo Hexadecimal'; @override String get path_helperMaxHops => - 'Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)'; + 'Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)'; @override String get path_selectFromContacts => 'Ou selecione de contatos:'; @override String get path_noRepeatersFound => - 'Não foram encontrados repetidores ou servidores de sala.'; + 'Não foram encontrados repetidores ou servidores de sala.'; @override String get path_customPathsRequire => - 'Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.'; + 'Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Prefixos hexadecimais inválidos: $prefixes'; + return 'Prefixos hexadecimais inválidos: $prefixes'; } @override String get path_tooLong => - 'Caminho muito longo. Máximo de 64 saltos permitidos.'; + 'Caminho muito longo. Máximo de 64 saltos permitidos.'; @override String get path_setPath => 'Definir Caminho'; @@ -1765,14 +1765,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_statusSubtitle => - 'Visualizar status do repetidor, estatísticas e vizinhos.'; + 'Visualizar status do repetidor, estatísticas e vizinhos.'; @override String get repeater_telemetry => 'Telemetria'; @override String get repeater_telemetrySubtitle => - 'Visualizar telemetria de sensores e estatísticas do sistema'; + 'Visualizar telemetria de sensores e estatísticas do sistema'; @override String get repeater_cli => 'CLI'; @@ -1787,10 +1787,10 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.'; @override - String get repeater_settings => 'Configurações'; + String get repeater_settings => 'Configurações'; @override - String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; + String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; @override String get repeater_statusTitle => 'Status do Repetidor'; @@ -1802,7 +1802,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_autoUseSavedPath => 'Auto (usar caminho salvo)'; @override - String get repeater_forceFloodMode => 'Modo de Inundação Forçado'; + String get repeater_forceFloodMode => 'Modo de Inundação Forçado'; @override String get repeater_pathManagement => 'Gerenciamento de caminhos'; @@ -1811,7 +1811,8 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_refresh => 'Atualizar'; @override - String get repeater_statusRequestTimeout => 'Solicitação de status expirou.'; + String get repeater_statusRequestTimeout => + 'Solicitação de status expirou.'; @override String repeater_errorLoadingStatus(String error) { @@ -1819,13 +1820,13 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get repeater_systemInformation => 'Informações do Sistema'; + String get repeater_systemInformation => 'Informações do Sistema'; @override String get repeater_battery => 'Bateria'; @override - String get repeater_clockAtLogin => 'Relógio (no login)'; + String get repeater_clockAtLogin => 'Relógio (no login)'; @override String get repeater_uptime => 'Disponibilidade'; @@ -1834,19 +1835,19 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_queueLength => 'Comprimento da Fila'; @override - String get repeater_debugFlags => 'Marcar Flags de Depuração'; + String get repeater_debugFlags => 'Marcar Flags de Depuração'; @override - String get repeater_radioStatistics => 'Estatísticas de Rádio'; + String get repeater_radioStatistics => 'Estatísticas de Rádio'; @override - String get repeater_lastRssi => 'Último RSSI'; + String get repeater_lastRssi => 'Último RSSI'; @override - String get repeater_lastSnr => 'Último SNR'; + String get repeater_lastSnr => 'Último SNR'; @override - String get repeater_noiseFloor => 'Nível de Ruído'; + String get repeater_noiseFloor => 'Nível de Ruído'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1855,7 +1856,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Estatísticas de Pacote'; + String get repeater_packetStatistics => 'Estatísticas de Pacote'; @override String get repeater_sent => 'Enviado'; @@ -1878,17 +1879,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundação: $flood, Direto: $direct'; + return 'Total: $total, Inundação: $flood, Direto: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundação: $flood, Direto: $direct'; + return 'Total: $total, Inundação: $flood, Direto: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Inundação: $flood, Direto: $direct'; + return 'Inundação: $flood, Direto: $direct'; } @override @@ -1897,10 +1898,10 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Configurações do Repetidor'; + String get repeater_settingsTitle => 'Configurações do Repetidor'; @override - String get repeater_basicSettings => 'Configurações Básicas'; + String get repeater_basicSettings => 'Configurações Básicas'; @override String get repeater_repeaterName => 'Nome do Repetidor'; @@ -1922,10 +1923,10 @@ class AppLocalizationsPt extends AppLocalizations { 'Acesso com senha de leitura somente'; @override - String get repeater_radioSettings => 'Configurações de Rádio'; + String get repeater_radioSettings => 'Configurações de Rádio'; @override - String get repeater_frequencyMhz => 'Frequência (MHz)'; + String get repeater_frequencyMhz => 'Frequência (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -1940,13 +1941,13 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_bandwidth => 'Largura de banda'; @override - String get repeater_spreadingFactor => 'Fator de Dispersão'; + String get repeater_spreadingFactor => 'Fator de Dispersão'; @override - String get repeater_codingRate => 'Taxa de Codificação'; + String get repeater_codingRate => 'Taxa de Codificação'; @override - String get repeater_locationSettings => 'Configurações de Localização'; + String get repeater_locationSettings => 'Configurações de Localização'; @override String get repeater_latitude => 'Latitude'; @@ -1983,13 +1984,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_privacyModeSubtitle => - 'Esconder nome/localização em anúncios'; + 'Esconder nome/localização em anúncios'; @override - String get repeater_advertisementSettings => 'Configurações de Anúncios'; + String get repeater_advertisementSettings => 'Configurações de Anúncios'; @override - String get repeater_localAdvertInterval => 'Intervalo de Anúncio Local'; + String get repeater_localAdvertInterval => 'Intervalo de Anúncio Local'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -1998,7 +1999,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervalo de Anúncio de Inundação'; + 'Intervalo de Anúncio de Inundação'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2007,7 +2008,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervalo de Anúncio Criptografado'; + 'Intervalo de Anúncio Criptografado'; @override String get repeater_dangerZone => 'Zona de Perigo'; @@ -2028,11 +2029,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_regenerateIdentityKeySubtitle => - 'Gerar nova chave pública/privada'; + 'Gerar nova chave pública/privada'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Isso gerará uma nova identidade para o repetidor. Continuar?'; + 'Isso gerará uma nova identidade para o repetidor. Continuar?'; @override String get repeater_eraseFileSystem => 'Excluir Sistema de Arquivos'; @@ -2043,11 +2044,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!'; + 'AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!'; @override String get repeater_eraseSerialOnly => - 'Apagar está disponível apenas via console serial.'; + 'Apagar está disponível apenas via console serial.'; @override String repeater_commandSent(String command) { @@ -2063,26 +2064,27 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_confirm => 'Confirmar'; @override - String get repeater_settingsSaved => 'Configurações salvas com sucesso'; + String get repeater_settingsSaved => 'Configurações salvas com sucesso'; @override String repeater_errorSavingSettings(String error) { - return 'Erro ao salvar as configurações: $error'; + return 'Erro ao salvar as configurações: $error'; } @override - String get repeater_refreshBasicSettings => 'Atualizar Configurações Básicas'; + String get repeater_refreshBasicSettings => + 'Atualizar Configurações Básicas'; @override String get repeater_refreshRadioSettings => - 'Atualizar Configurações de Rádio'; + 'Atualizar Configurações de Rádio'; @override String get repeater_refreshTxPower => 'Atualizar TX de energia'; @override String get repeater_refreshLocationSettings => - 'Atualizar Configurações de Localização'; + 'Atualizar Configurações de Localização'; @override String get repeater_refreshPacketForwarding => @@ -2096,7 +2098,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_refreshAdvertisementSettings => - 'Atualizar Configurações do Anúncio'; + 'Atualizar Configurações do Anúncio'; @override String repeater_refreshed(String label) { @@ -2112,20 +2114,20 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliTitle => 'Repetidor CLI'; @override - String get repeater_debugNextCommand => 'Depurar Próximo Comando'; + String get repeater_debugNextCommand => 'Depurar Próximo Comando'; @override String get repeater_commandHelp => 'Ajuda'; @override - String get repeater_clearHistory => 'Limpar Histórico'; + String get repeater_clearHistory => 'Limpar Histórico'; @override - String get repeater_noCommandsSent => 'Ainda não foram enviadas comandos.'; + String get repeater_noCommandsSent => 'Ainda não foram enviadas comandos.'; @override String get repeater_typeCommandOrUseQuick => - 'Digite um comando abaixo ou use comandos rápidos'; + 'Digite um comando abaixo ou use comandos rápidos'; @override String get repeater_enterCommandHint => 'Insira o comando...'; @@ -2134,7 +2136,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_previousCommand => 'Comando anterior'; @override - String get repeater_nextCommand => 'Próxima ação'; + String get repeater_nextCommand => 'Próxima ação'; @override String get repeater_enterCommandFirst => 'Insira um comando primeiro'; @@ -2151,7 +2153,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliQuickGetName => 'Obter Nome'; @override - String get repeater_cliQuickGetRadio => 'Obter Rádio'; + String get repeater_cliQuickGetRadio => 'Obter Rádio'; @override String get repeater_cliQuickGetTx => 'Obter TX'; @@ -2160,24 +2162,24 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliQuickNeighbors => 'Vizinhos'; @override - String get repeater_cliQuickVersion => 'Versão'; + String get repeater_cliQuickVersion => 'Versão'; @override String get repeater_cliQuickAdvertise => 'Anunciar'; @override - String get repeater_cliQuickClock => 'Relógio'; + String get repeater_cliQuickClock => 'Relógio'; @override - String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; + String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; @override String get repeater_cliHelpReboot => - 'Reinicia o dispositivo. (note, você pode obter \'Timeout\' que é normal)'; + 'Reinicia o dispositivo. (note, você pode obter \'Timeout\' que é normal)'; @override String get repeater_cliHelpClock => - 'Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.'; + 'Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.'; @override String get repeater_cliHelpPassword => @@ -2185,38 +2187,38 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpVersion => - 'Mostra a versão do dispositivo e a data de construção do firmware.'; + 'Mostra a versão do dispositivo e a data de construção do firmware.'; @override String get repeater_cliHelpClearStats => - 'Reseta vários contadores de estatísticas para zero.'; + 'Reseta vários contadores de estatísticas para zero.'; @override String get repeater_cliHelpSetAf => 'Define o fator de tempo de ar.'; @override String get repeater_cliHelpSetTx => - 'Define a potência de transmissão LoRa em dBm (redefinir para aplicar).'; + 'Define a potência de transmissão LoRa em dBm (redefinir para aplicar).'; @override String get repeater_cliHelpSetRepeat => - 'Habilita ou desabilita o papel do repetidor para este nó.'; + 'Habilita ou desabilita o papel do repetidor para este nó.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Servidor de sala) Se \'ligado\', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).'; + '(Servidor de sala) Se \'ligado\', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).'; @override String get repeater_cliHelpSetFloodMax => - 'Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)'; + 'Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)'; @override String get repeater_cliHelpSetIntThresh => - 'Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.'; + 'Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.'; + 'Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.'; @override String get repeater_cliHelpSetMultiAcks => @@ -2224,42 +2226,42 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.'; + 'Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.'; + 'Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.'; @override String get repeater_cliHelpSetGuestPassword => - 'Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")'; + 'Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")'; @override - String get repeater_cliHelpSetName => 'Define o nome do anúncio.'; + String get repeater_cliHelpSetName => 'Define o nome do anúncio.'; @override String get repeater_cliHelpSetLat => - 'Define a latitude do mapa de anúncios. (graus decimais)'; + 'Define a latitude do mapa de anúncios. (graus decimais)'; @override String get repeater_cliHelpSetLon => - 'Define a longitude do mapa de anúncios. (graus decimais)'; + 'Define a longitude do mapa de anúncios. (graus decimais)'; @override String get repeater_cliHelpSetRadio => - 'Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.'; + 'Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.'; @override String get repeater_cliHelpSetRxDelay => - 'Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.'; + 'Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.'; @override String get repeater_cliHelpSetTxDelay => - 'Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)'; + 'Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.'; + 'Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.'; @override String get repeater_cliHelpSetBridgeEnabled => 'Ativar/Desativar ponte.'; @@ -2270,7 +2272,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpSetBridgeSource => - 'Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.'; + 'Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.'; @override String get repeater_cliHelpSetBridgeBaud => @@ -2286,15 +2288,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpTempRadio => - 'Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).'; + 'Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).'; @override String get repeater_cliHelpSetPerm => - 'Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)'; + 'Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => - 'Obtém tipo de ponte nenhum, rs232, espnow'; + 'Obtém tipo de ponte nenhum, rs232, espnow'; @override String get repeater_cliHelpLogStart => @@ -2310,86 +2312,86 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4'; + 'Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4'; @override String get repeater_cliHelpNeighborRemove => - 'Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.'; + 'Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.'; @override String get repeater_cliHelpRegion => - '(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.'; + '(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.'; @override String get repeater_cliHelpRegionLoad => - 'NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.'; + 'NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.'; @override String get repeater_cliHelpRegionGet => - 'Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) \'F\'\"'; + 'Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Adiciona ou atualiza uma definição de região com o nome fornecido.'; + 'Adiciona ou atualiza uma definição de região com o nome fornecido.'; @override String get repeater_cliHelpRegionRemove => - 'Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)'; + 'Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)'; @override String get repeater_cliHelpRegionAllowf => - 'Define a permissão de \'F\'luido para a região especificada. (\'\' para o escopo global/legado)'; + 'Define a permissão de \'F\'luido para a região especificada. (\'\' para o escopo global/legado)'; @override String get repeater_cliHelpRegionDenyf => - 'Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)'; + 'Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)'; @override String get repeater_cliHelpRegionHome => - 'Responde com a região \'home\' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)'; + 'Responde com a região \'home\' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)'; @override - String get repeater_cliHelpRegionHomeSet => 'Define a região \'casa\'.'; + String get repeater_cliHelpRegionHomeSet => 'Define a região \'casa\'.'; @override String get repeater_cliHelpRegionSave => - 'Persiste a lista/mapa de regiões para o armazenamento.'; + 'Persiste a lista/mapa de regiões para o armazenamento.'; @override String get repeater_cliHelpGps => - 'Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.'; + 'Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.'; @override String get repeater_cliHelpGpsOnOff => 'Alterna o estado de energia do GPS.'; @override String get repeater_cliHelpGpsSync => - 'Sincroniza o tempo do nó com o relógio GPS.'; + 'Sincroniza o tempo do nó com o relógio GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Define a posição do nó para coordenadas GPS e salvar preferências.'; + 'Define a posição do nó para coordenadas GPS e salvar preferências.'; @override String get repeater_cliHelpGpsAdvert => - 'Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências'; + 'Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências'; @override String get repeater_cliHelpGpsAdvertSet => - 'Define a configuração do anúncio de localização.'; + 'Define a configuração do anúncio de localização.'; @override String get repeater_commandsListTitle => 'Lista de Comandos'; @override String get repeater_commandsListNote => - 'NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".'; + 'NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".'; @override String get repeater_general => 'Geral'; @override - String get repeater_settingsCategory => 'Configurações'; + String get repeater_settingsCategory => 'Configurações'; @override String get repeater_bridge => 'Ponte'; @@ -2402,25 +2404,25 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_regionManagementRepeaterOnly => - 'Gerenciamento de Região (Apenas Repetidor)'; + 'Gerenciamento de Região (Apenas Repetidor)'; @override String get repeater_regionNote => - 'Os comandos de região foram introduzidos para gerenciar definições e permissões de região.'; + 'Os comandos de região foram introduzidos para gerenciar definições e permissões de região.'; @override String get repeater_gpsManagement => 'Gerenciamento GPS'; @override String get repeater_gpsNote => - 'O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.'; + 'O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.'; @override String get telemetry_receivedData => 'Dados de Telemetria Recebidos'; @override String get telemetry_requestTimeout => - 'Solicitação de telemetria expirou o tempo.'; + 'Solicitação de telemetria expirou o tempo.'; @override String telemetry_errorLoading(String error) { @@ -2428,7 +2430,8 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get telemetry_noData => 'Não estão disponíveis dados de telemetria.'; + String get telemetry_noData => + 'Não estão disponíveis dados de telemetria.'; @override String telemetry_channelTitle(int channel) { @@ -2439,7 +2442,7 @@ class AppLocalizationsPt extends AppLocalizations { String get telemetry_batteryLabel => 'Bateria'; @override - String get telemetry_voltageLabel => 'Tensão'; + String get telemetry_voltageLabel => 'Tensão'; @override String get telemetry_mcuTemperatureLabel => 'Temperatura do MCU'; @@ -2467,7 +2470,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2486,7 +2489,7 @@ class AppLocalizationsPt extends AppLocalizations { String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos'; @override - String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.'; + String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.'; @override String neighbors_unknownContact(String pubkey) { @@ -2495,11 +2498,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Ouvido: $time atrás'; + return 'Ouvido: $time atrás'; } @override - String get channelPath_title => 'Rótulo de Caminho de Pacote'; + String get channelPath_title => 'Rótulo de Caminho de Pacote'; @override String get channelPath_viewMap => 'Ver mapa'; @@ -2512,7 +2515,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channelPath_noHopDetails => - 'Os detalhes do pacote não estão disponíveis.'; + 'Os detalhes do pacote não estão disponíveis.'; @override String get channelPath_messageDetails => 'Detalhes da Mensagem'; @@ -2536,11 +2539,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Rastreamento observado $index • $hops'; + return 'Rastreamento observado $index • $hops'; } @override - String get channelPath_noLocationData => 'Não há dados de localização.'; + String get channelPath_noLocationData => 'Não há dados de localização.'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2556,7 +2559,7 @@ class AppLocalizationsPt extends AppLocalizations { String get channelPath_unknownPath => 'Desconhecido'; @override - String get channelPath_floodPath => 'Inundação'; + String get channelPath_floodPath => 'Inundação'; @override String get channelPath_directPath => 'Salvar'; @@ -2576,11 +2579,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Não estão disponíveis localizações de repetidores para este caminho.'; + 'Não estão disponíveis localizações de repetidores para este caminho.'; @override String channelPath_primaryPath(int index) { - return 'Caminho $index (Primário)'; + return 'Caminho $index (Primário)'; } @override @@ -2591,12 +2594,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Não estão disponíveis detalhes de voo para este pacote.'; + 'Não estão disponíveis detalhes de voo para este pacote.'; @override String get channelPath_unknownRepeater => 'Repetidor Desconhecido'; @@ -2609,17 +2612,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get community_createDesc => - 'Crie uma nova comunidade e compartilhe via código QR.'; + 'Crie uma nova comunidade e compartilhe via código QR.'; @override String get community_join => 'Junte-se'; @override - String get community_joinTitle => 'Junte-se à Comunidade'; + String get community_joinTitle => 'Junte-se à Comunidade'; @override String community_joinConfirmation(String name) { - return 'Você gostaria de se juntar à comunidade \"$name\"?'; + return 'Você gostaria de se juntar à comunidade \"$name\"?'; } @override @@ -2627,13 +2630,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get community_scanInstructions => - 'Aponte a câmera para um código QR da comunidade'; + 'Aponte a câmera para um código QR da comunidade'; @override - String get community_showQr => 'Mostrar Código QR'; + String get community_showQr => 'Mostrar Código QR'; @override - String get community_publicChannel => 'Comunidade Pública'; + String get community_publicChannel => 'Comunidade Pública'; @override String get community_hashtagChannel => 'Hashtag da Comunidade'; @@ -2651,7 +2654,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_joined(String name) { - return 'Juntou-se à comunidade \"$name\"'; + return 'Juntou-se à comunidade \"$name\"'; } @override @@ -2659,39 +2662,39 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Escanear este código QR para juntar-se a $name'; + return 'Escanear este código QR para juntar-se a $name'; } @override String get community_hashtagPrivacyHint => - 'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade'; + 'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade'; @override - String get community_invalidQrCode => 'Código QR da comunidade inválido'; + String get community_invalidQrCode => 'Código QR da comunidade inválido'; @override - String get community_alreadyMember => 'Já é Membro'; + String get community_alreadyMember => 'Já é Membro'; @override String community_alreadyMemberMessage(String name) { - return 'Você já é membro de \"$name\".'; + return 'Você já é membro de \"$name\".'; } @override String get community_addPublicChannel => - 'Adicionar Canal Público da Comunidade'; + 'Adicionar Canal Público da Comunidade'; @override String get community_addPublicChannelHint => - 'Adicionar automaticamente o canal público para esta comunidade'; + 'Adicionar automaticamente o canal público para esta comunidade'; @override String get community_noCommunities => - 'Ainda não foram adicionadas comunidades.'; + 'Ainda não foram adicionadas comunidades.'; @override String get community_scanOrCreate => - 'Escaneie um código QR ou crie uma comunidade para começar.'; + 'Escaneie um código QR ou crie uma comunidade para começar.'; @override String get community_manageCommunities => 'Gerenciar Comunidades'; @@ -2706,7 +2709,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Isso também excluirá $count canal/canais e suas mensagens.'; + return 'Isso também excluirá $count canal/canais e suas mensagens.'; } @override @@ -2719,7 +2722,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.'; + return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.'; } @override @@ -2740,7 +2743,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++'; + return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++'; } @override @@ -2758,7 +2761,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get community_regularHashtagDesc => - 'Hashtag público (qualquer pessoa pode participar)'; + 'Hashtag público (qualquer pessoa pode participar)'; @override String get community_communityHashtag => 'Hashtag da Comunidade'; @@ -2779,7 +2782,7 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_sortBy => 'Ordenar por'; @override - String get listFilter_latestMessages => 'Últimas mensagens'; + String get listFilter_latestMessages => 'Últimas mensagens'; @override String get listFilter_heardRecently => 'Ouvido recentemente'; @@ -2803,7 +2806,7 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_removeFromFavorites => 'Remover da lista de favoritos'; @override - String get listFilter_users => 'Usuários'; + String get listFilter_users => 'Usuários'; @override String get listFilter_repeaters => 'Repetidores'; @@ -2812,36 +2815,36 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_roomServers => 'Servidores de sala'; @override - String get listFilter_unreadOnly => 'Apenas não lido'; + String get listFilter_unreadOnly => 'Apenas não lido'; @override String get listFilter_newGroup => 'Novo grupo'; @override - String get pathTrace_you => 'Você'; + String get pathTrace_you => 'Você'; @override String get pathTrace_failed => 'Falha no rastreamento de caminho.'; @override - String get pathTrace_notAvailable => 'Traçado de caminho não disponível.'; + String get pathTrace_notAvailable => 'Traçado de caminho não disponível.'; @override String get pathTrace_refreshTooltip => 'Atualizar Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Um ou mais dos lúpulos estão sem localização!'; + 'Um ou mais dos lúpulos estão sem localização!'; @override String get pathTrace_clearTooltip => 'Limpar caminho'; @override - String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.'; + String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.'; @override String losRunFailed(String error) { - return 'Falha na verificação da linha de visão: $error'; + return 'Falha na verificação da linha de visão: $error'; } @override @@ -2849,17 +2852,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get losRunToViewElevationProfile => - 'Execute o LOS para visualizar o perfil de elevação'; + 'Execute o LOS para visualizar o perfil de elevação'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados'; + 'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados'; @override - String get losShowDisplayNodes => 'Mostrar nós de exibição'; + String get losShowDisplayNodes => 'Mostrar nós de exibição'; @override String get losCustomPoints => 'Pontos personalizados'; @@ -2889,7 +2892,7 @@ class AppLocalizationsPt extends AppLocalizations { String get losRun => 'Executar LOS'; @override - String get losNoElevationData => 'Sem dados de elevação'; + String get losNoElevationData => 'Sem dados de elevação'; @override String losProfileClear( @@ -2898,7 +2901,7 @@ class AppLocalizationsPt extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit'; + return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit'; } @override @@ -2924,11 +2927,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Dados de elevação indisponíveis para uma ou mais amostras.'; + 'Dados de elevação indisponíveis para uma ou mais amostras.'; @override String get losErrorInvalidInput => - 'Dados de pontos/elevação inválidos para cálculo de LOS.'; + 'Dados de pontos/elevação inválidos para cálculo de LOS.'; @override String get losRenameCustomPoint => 'Renomear ponto personalizado'; @@ -2944,10 +2947,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get losElevationAttribution => - 'Dados de elevação: Open-Meteo (CC BY 4.0)'; + 'Dados de elevação: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Horizonte de rádio'; + String get losLegendRadioHorizon => 'Horizonte de rádio'; @override String get losLegendLosBeam => 'Linha de visada'; @@ -2956,13 +2959,13 @@ class AppLocalizationsPt extends AppLocalizations { String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequência'; + String get losFrequencyLabel => 'Frequência'; @override - String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; + String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; @override - String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; + String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; @override String losFrequencyDialogDescription( @@ -2971,23 +2974,24 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; + return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; } @override - String get contacts_pathTrace => 'Traçado de Caminho'; + String get contacts_pathTrace => 'Traçado de Caminho'; @override String get contacts_ping => 'Pingar'; @override - String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor'; + String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor'; @override String get contacts_repeaterPing => 'Pingar repetidor'; @override - String get contacts_roomPathTrace => 'Traçar caminho para o servidor da sala'; + String get contacts_roomPathTrace => + 'Traçar caminho para o servidor da sala'; @override String get contacts_roomPing => 'Pingar servidor da sala'; @@ -3001,10 +3005,10 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.'; + String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.'; @override - String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos'; + String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos'; @override String get contacts_contactImported => 'Contato foi importado.'; @@ -3013,39 +3017,41 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_contactImportFailed => 'Contato falhou ao ser importado.'; @override - String get contacts_zeroHopAdvert => 'Anúncio Zero Hop'; + String get contacts_zeroHopAdvert => 'Anúncio Zero Hop'; @override - String get contacts_floodAdvert => 'Anúncio de Inundação'; + String get contacts_floodAdvert => 'Anúncio de Inundação'; @override String get contacts_copyAdvertToClipboard => - 'Copiar Anúncio para Área de Transferência'; + 'Copiar Anúncio para Área de Transferência'; @override String get contacts_addContactFromClipboard => - 'Adicionar Contato da Área de Transferência'; + 'Adicionar Contato da Área de Transferência'; @override String get contacts_ShareContact => - 'Copiar contato para Área de Transferência'; + 'Copiar contato para Área de Transferência'; @override - String get contacts_ShareContactZeroHop => 'Compartilhar contato por anúncio'; + String get contacts_ShareContactZeroHop => + 'Compartilhar contato por anúncio'; @override - String get contacts_zeroHopContactAdvertSent => 'Enviou contato por anúncio.'; + String get contacts_zeroHopContactAdvertSent => + 'Enviou contato por anúncio.'; @override String get contacts_zeroHopContactAdvertFailed => 'Falha ao enviar contato.'; @override String get contacts_contactAdvertCopied => - 'Anúncio copiado para a Área de Transferência.'; + 'Anúncio copiado para a Área de Transferência.'; @override String get contacts_contactAdvertCopyFailed => - 'Cópia do anúncio para a Área de Transferência falhou.'; + 'Cópia do anúncio para a Área de Transferência falhou.'; @override String get notification_activityTitle => 'Atividade MeshCore'; @@ -3077,8 +3083,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'novos nós', - one: 'novo nó', + other: 'novos nós', + one: 'novo nó', ); return '$count $_temp0'; } @@ -3097,21 +3103,21 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportRepeatersSubtitle => - 'Exporta repetidores / roomserver com localização para arquivo GPX.'; + 'Exporta repetidores / roomserver com localização para arquivo GPX.'; @override String get settings_gpxExportContacts => 'Exportar companheiros para GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exporta companheiros com uma localização para um arquivo GPX.'; + 'Exporta companheiros com uma localização para um arquivo GPX.'; @override String get settings_gpxExportAll => 'Exportar todos os contatos para GPX'; @override String get settings_gpxExportAllSubtitle => - 'Exporta todos os contatos com uma localização para um arquivo GPX.'; + 'Exporta todos os contatos com uma localização para um arquivo GPX.'; @override String get settings_gpxExportSuccess => 'Arquivo GPX exportado com sucesso.'; @@ -3121,17 +3127,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportNotAvailable => - 'Não suportado no seu dispositivo/SO'; + 'Não suportado no seu dispositivo/SO'; @override String get settings_gpxExportError => 'Ocorreu um erro ao exportar.'; @override String get settings_gpxExportRepeatersRoom => - 'Localizações do servidor de repetidor e sala'; + 'Localizações do servidor de repetidor e sala'; @override - String get settings_gpxExportChat => 'Localizações de companheiros'; + String get settings_gpxExportChat => 'Localizações de companheiros'; @override String get settings_gpxExportAllContacts => 'Todos os locais de contatos'; @@ -3142,11 +3148,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportShareSubject => - 'meshcore-open exportação de dados de mapa GPX'; + 'meshcore-open exportação de dados de mapa GPX'; @override - String get snrIndicator_nearByRepeaters => 'Repetidores Próximos'; + String get snrIndicator_nearByRepeaters => 'Repetidores Próximos'; @override - String get snrIndicator_lastSeen => 'Visto pela última vez'; + String get snrIndicator_lastSeen => 'Visto pela última vez'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 6a25aa2..456d497 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -12,92 +12,93 @@ class AppLocalizationsRu extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Контакты'; + String get nav_contacts => 'Контакты'; @override - String get nav_channels => 'Каналы'; + String get nav_channels => 'Каналы'; @override - String get nav_map => 'Карта'; + String get nav_map => 'Карта'; @override - String get common_cancel => 'Отмена'; + String get common_cancel => 'Отмена'; @override String get common_ok => 'OK'; @override - String get common_connect => 'Коннект'; + String get common_connect => 'Коннект'; @override - String get common_unknownDevice => 'Неизвестное устройство'; + String get common_unknownDevice => + 'Неизвестное устройство'; @override - String get common_save => 'Сохранить'; + String get common_save => 'Сохранить'; @override - String get common_delete => 'Удалить'; + String get common_delete => 'Удалить'; @override - String get common_close => 'Закрыть'; + String get common_close => 'Закрыть'; @override - String get common_edit => 'Изменить'; + String get common_edit => 'Изменить'; @override - String get common_add => 'Добавить'; + String get common_add => 'Добавить'; @override - String get common_settings => 'Настройки'; + String get common_settings => 'Настройки'; @override - String get common_disconnect => 'Отключить'; + String get common_disconnect => 'Отключить'; @override - String get common_connected => 'Подключено'; + String get common_connected => 'Подключено'; @override - String get common_disconnected => 'Отключено'; + String get common_disconnected => 'Отключено'; @override - String get common_create => 'Создать'; + String get common_create => 'Создать'; @override - String get common_continue => 'Продолжить'; + String get common_continue => 'Продолжить'; @override - String get common_share => 'Поделиться'; + String get common_share => 'Поделиться'; @override - String get common_copy => 'Копировать'; + String get common_copy => 'Копировать'; @override - String get common_retry => 'Повторить'; + String get common_retry => 'Повторить'; @override - String get common_hide => 'Скрыть'; + String get common_hide => 'Скрыть'; @override - String get common_remove => 'Убрать'; + String get common_remove => 'Убрать'; @override - String get common_enable => 'Включить'; + String get common_enable => 'Включить'; @override - String get common_disable => 'Выключить'; + String get common_disable => 'Выключить'; @override - String get common_reboot => 'Перезагрузить'; + String get common_reboot => 'Перезагрузить'; @override - String get common_loading => 'Загрузка...'; + String get common_loading => 'Загрузка...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { - return '$volts В'; + return '$volts Ð’'; } @override @@ -108,13 +109,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Выберите способ подключения'; - - @override - String get connectionChoiceSubtitle => - 'Выберите, каким способом вы хотите получить свой устройство MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -122,230 +116,247 @@ class AppLocalizationsRu extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Подключение через USB'; + String get usbScreenTitle => 'Подключение через USB'; @override String get usbScreenSubtitle => - 'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.'; + 'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.'; @override - String get usbScreenStatus => 'Выберите USB-устройство'; + String get usbScreenStatus => 'Выберите USB-устройство'; @override String get usbScreenNote => - 'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.'; + 'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.'; @override String get usbScreenEmptyState => - 'Не обнаружено устройств USB. Подключите одно из них и обновите список.'; + 'Не обнаружено устройств USB. Подключите одно из них и обновите список.'; @override - String get scanner_scanning => 'Поиск устройств...'; + String get scanner_scanning => 'Поиск устройств...'; @override - String get scanner_connecting => 'Подключение...'; + String get scanner_connecting => 'Подключение...'; @override - String get scanner_disconnecting => 'Отключение...'; + String get scanner_disconnecting => 'Отключение...'; @override - String get scanner_notConnected => 'Не подключено'; + String get scanner_notConnected => 'Не подключено'; @override String scanner_connectedTo(String deviceName) { - return 'Подключено к $deviceName'; + return 'Подключено к $deviceName'; } @override - String get scanner_searchingDevices => 'Поиск устройств MeshCore...'; + String get scanner_searchingDevices => + 'Поиск устройств MeshCore...'; @override - String get scanner_tapToScan => 'Нажмите для поиска MeshCore устройств'; + String get scanner_tapToScan => + 'Нажмите для поиска MeshCore устройств'; @override String scanner_connectionFailed(String error) { - return 'Подключение не удалось: $error'; + return 'Подключение не удалось: $error'; } @override - String get scanner_stop => 'Стоп'; + String get scanner_stop => 'Стоп'; @override - String get scanner_scan => 'Сканирование'; + String get scanner_scan => 'Сканирование'; @override - String get scanner_bluetoothOff => 'Bluetooth выключен'; + String get scanner_bluetoothOff => 'Bluetooth выключен'; @override String get scanner_bluetoothOffMessage => - 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; + 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; @override - String get scanner_chromeRequired => 'Требуется браузер Chrome'; + String get scanner_chromeRequired => + 'Требуется браузер Chrome'; @override String get scanner_chromeRequiredMessage => - 'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.'; + 'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.'; @override - String get scanner_enableBluetooth => 'Включите Bluetooth'; + String get scanner_enableBluetooth => 'Включите Bluetooth'; @override - String get device_quickSwitch => 'Быстрое переключение'; + String get device_quickSwitch => 'Быстрое переключение'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Настройки'; + String get settings_title => 'Настройки'; @override - String get settings_deviceInfo => 'Информация об устройстве'; + String get settings_deviceInfo => + 'Информация об устройстве'; @override - String get settings_appSettings => 'Настройки приложения'; + String get settings_appSettings => 'Настройки приложения'; @override String get settings_appSettingsSubtitle => - 'Уведомления, сообщения и настройки карты'; + 'Уведомления, сообщения и настройки карты'; @override - String get settings_nodeSettings => 'Настройки ноды'; + String get settings_nodeSettings => 'Настройки ноды'; @override - String get settings_nodeName => 'Имя ноды'; + String get settings_nodeName => 'Имя ноды'; @override - String get settings_nodeNameNotSet => 'Не установлено'; + String get settings_nodeNameNotSet => 'Не установлено'; @override - String get settings_nodeNameHint => 'Введите имя ноды'; + String get settings_nodeNameHint => 'Введите имя ноды'; @override - String get settings_nodeNameUpdated => 'Имя обновлено'; + String get settings_nodeNameUpdated => 'Имя обновлено'; @override - String get settings_radioSettings => 'Настройки радио'; + String get settings_radioSettings => 'Настройки радио'; @override String get settings_radioSettingsSubtitle => - 'Частота, мощность и коэффициент распространения'; + 'Частота, мощность и коэффициент распространения'; @override - String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; + String get settings_radioSettingsUpdated => + 'Настройки радио обновлены'; @override - String get settings_location => 'Позиция'; + String get settings_location => 'Позиция'; @override - String get settings_locationSubtitle => 'Координаты GPS'; + String get settings_locationSubtitle => 'Координаты GPS'; @override - String get settings_locationUpdated => 'Позиция и настройки GPS обновлены'; + String get settings_locationUpdated => + 'Позиция и настройки GPS обновлены'; @override - String get settings_locationBothRequired => 'Введите широту и долготу.'; + String get settings_locationBothRequired => + 'Введите широту и долготу.'; @override - String get settings_locationInvalid => 'Неверная широта или долгота.'; + String get settings_locationInvalid => + 'Неверная широта или долгота.'; @override - String get settings_locationGPSEnable => 'Включить GPS'; + String get settings_locationGPSEnable => 'Включить GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Включение GPS для автоматического обновления позиции.'; + 'Включение GPS для автоматического обновления позиции.'; @override String get settings_locationIntervalSec => - 'Интервал для позиционирования GPS (секунды)'; + 'Интервал для позиционирования GPS (секунды)'; @override String get settings_locationIntervalInvalid => - 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; + 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; @override - String get settings_latitude => 'Широта'; + String get settings_latitude => 'Широта'; @override - String get settings_longitude => 'Долгота'; + String get settings_longitude => 'Долгота'; @override - String get settings_privacyMode => 'Режим конфиденциальности'; + String get settings_privacyMode => + 'Режим конфиденциальности'; @override String get settings_privacyModeSubtitle => - 'Скрыть имя/позицию в анонсировании'; + 'Скрыть имя/позицию в анонсировании'; @override String get settings_privacyModeToggle => - 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; + 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; @override - String get settings_privacyModeEnabled => 'Режим конфиденциальности включен'; + String get settings_privacyModeEnabled => + 'Режим конфиденциальности включен'; @override String get settings_privacyModeDisabled => - 'Режим конфиденциальности выключен'; + 'Режим конфиденциальности выключен'; @override - String get settings_actions => 'Действия'; + String get settings_actions => 'Действия'; @override - String get settings_sendAdvertisement => 'Отправить анонсирование'; + String get settings_sendAdvertisement => + 'Отправить анонсирование'; @override String get settings_sendAdvertisementSubtitle => - 'Отправить анонсирование о присутствии сейчас'; + 'Отправить анонсирование о присутствии сейчас'; @override - String get settings_advertisementSent => 'Анонсирование отправлено'; + String get settings_advertisementSent => + 'Анонсирование отправлено'; @override - String get settings_syncTime => 'Синхронизация времени'; + String get settings_syncTime => 'Синхронизация времени'; @override - String get settings_syncTimeSubtitle => 'Синхронизировать время с телефоном'; + String get settings_syncTimeSubtitle => + 'Синхронизировать время с телефоном'; @override - String get settings_timeSynchronized => 'Время синхронизировано'; + String get settings_timeSynchronized => + 'Время синхронизировано'; @override - String get settings_refreshContacts => 'Обновить контакты'; + String get settings_refreshContacts => 'Обновить контакты'; @override String get settings_refreshContactsSubtitle => - 'Перезагрузить список контактов с устройства'; + 'Перезагрузить список контактов с устройства'; @override - String get settings_rebootDevice => 'Перезагрузить устройство'; + String get settings_rebootDevice => + 'Перезагрузить устройство'; @override String get settings_rebootDeviceSubtitle => - 'Перезапустить устройство MeshCore'; + 'Перезапустить устройство MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.'; + 'Ð’Ñ‹ уверены, что хотите перезагрузить устройство? Ð’Ñ‹ будете отключены.'; @override - String get settings_debug => 'Отладка'; + String get settings_debug => 'Отладка'; @override - String get settings_bleDebugLog => 'Журнал отладки BLE'; + String get settings_bleDebugLog => 'Журнал отладки BLE'; @override String get settings_bleDebugLogSubtitle => - 'Команды BLE, ответы и сырые данные'; + 'Команды BLE, ответы и сырые данные'; @override - String get settings_appDebugLog => 'Журнал отладки приложения'; + String get settings_appDebugLog => + 'Журнал отладки приложения'; @override - String get settings_appDebugLogSubtitle => 'Сообщения отладки приложения'; + String get settings_appDebugLogSubtitle => + 'Сообщения отладки приложения'; @override - String get settings_about => 'О программе'; + String get settings_about => 'О программе'; @override String settings_aboutVersion(String version) { @@ -357,1209 +368,1278 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_aboutDescription => - 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; + 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; @override String get settings_aboutOpenMeteoAttribution => - 'Данные о высоте LOS: Open-Meteo (CC BY 4.0)'; + 'Данные о высоте LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'Имя'; + String get settings_infoName => 'Имя'; @override String get settings_infoId => 'ID'; @override - String get settings_infoStatus => 'Статус'; + String get settings_infoStatus => 'Статус'; @override - String get settings_infoBattery => 'Батарея'; + String get settings_infoBattery => 'Батарея'; @override - String get settings_infoPublicKey => 'Публичный ключ'; + String get settings_infoPublicKey => 'Публичный ключ'; @override - String get settings_infoContactsCount => 'Количество контактов'; + String get settings_infoContactsCount => + 'Количество контактов'; @override - String get settings_infoChannelCount => 'Количество каналов'; + String get settings_infoChannelCount => 'Количество каналов'; @override - String get settings_presets => 'Пресеты'; + String get settings_presets => 'Пресеты'; @override - String get settings_frequency => 'Частота (МГц)'; + String get settings_frequency => 'Частота (МГц)'; @override - String get settings_frequencyHelper => '300.0 – 2500.0'; + String get settings_frequencyHelper => '300.0 – 2500.0'; @override - String get settings_frequencyInvalid => 'Недопустимая частота (300–2500 МГц)'; + String get settings_frequencyInvalid => + 'Недопустимая частота (300–2500 МГц)'; @override - String get settings_bandwidth => 'Полоса пропускания'; + String get settings_bandwidth => 'Полоса пропускания'; @override - String get settings_spreadingFactor => 'Коэффициент расширения'; + String get settings_spreadingFactor => + 'Коэффициент расширения'; @override - String get settings_codingRate => 'Коэффициент кодирования'; + String get settings_codingRate => + 'Коэффициент кодирования'; @override - String get settings_txPower => 'Мощность передачи (дБм)'; + String get settings_txPower => 'Мощность передачи (дБм)'; @override - String get settings_txPowerHelper => '0 – 22'; + String get settings_txPowerHelper => '0 – 22'; @override String get settings_txPowerInvalid => - 'Недопустимая мощность передачи (0–22 дБм)'; + 'Недопустимая мощность передачи (0–22 дБм)'; @override - String get settings_clientRepeat => 'Повторение \"вне сети\"'; + String get settings_clientRepeat => + 'Повторение \"вне сети\"'; @override String get settings_clientRepeatSubtitle => - 'Позвольте этому устройству повторять пакеты данных для других устройств.'; + 'Позвольте этому устройству повторять пакеты данных для других устройств.'; @override String get settings_clientRepeatFreqWarning => - 'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.'; + 'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.'; @override String settings_error(String message) { - return 'Ошибка: $message'; + return 'Ошибка: $message'; } @override - String get appSettings_title => 'Настройки приложения'; + String get appSettings_title => 'Настройки приложения'; @override - String get appSettings_appearance => 'Внешний вид'; + String get appSettings_appearance => 'Внешний вид'; @override - String get appSettings_theme => 'Тема'; + String get appSettings_theme => 'Тема'; @override - String get appSettings_themeSystem => 'Как в системе'; + String get appSettings_themeSystem => 'Как в системе'; @override - String get appSettings_themeLight => 'Светлая'; + String get appSettings_themeLight => 'Светлая'; @override - String get appSettings_themeDark => 'Тёмная'; + String get appSettings_themeDark => 'Тёмная'; @override - String get appSettings_language => 'Язык'; + String get appSettings_language => 'Язык'; @override - String get appSettings_languageSystem => 'Как в системе'; + String get appSettings_languageSystem => 'Как в системе'; @override - String get appSettings_languageEn => 'Английский'; + String get appSettings_languageEn => 'Английский'; @override - String get appSettings_languageFr => 'Французский'; + String get appSettings_languageFr => 'Французский'; @override - String get appSettings_languageEs => 'Испанский'; + String get appSettings_languageEs => 'Испанский'; @override - String get appSettings_languageDe => 'Немецкий'; + String get appSettings_languageDe => 'Немецкий'; @override - String get appSettings_languagePl => 'Польский'; + String get appSettings_languagePl => 'Польский'; @override - String get appSettings_languageSl => 'Словенский'; + String get appSettings_languageSl => 'Словенский'; @override - String get appSettings_languagePt => 'Португальский'; + String get appSettings_languagePt => 'Португальский'; @override - String get appSettings_languageIt => 'Итальянский'; + String get appSettings_languageIt => 'Итальянский'; @override - String get appSettings_languageZh => 'Китайский'; + String get appSettings_languageZh => 'Китайский'; @override - String get appSettings_languageSv => 'Шведский'; + String get appSettings_languageSv => 'Шведский'; @override - String get appSettings_languageNl => 'Нидерландский'; + String get appSettings_languageNl => 'Нидерландский'; @override - String get appSettings_languageSk => 'Словацкий'; + String get appSettings_languageSk => 'Словацкий'; @override - String get appSettings_languageBg => 'Болгарский'; + String get appSettings_languageBg => 'Болгарский'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Русский'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Українська'; @override String get appSettings_enableMessageTracing => - 'Включить трассировку сообщений'; + 'Включить трассировку сообщений'; @override String get appSettings_enableMessageTracingSubtitle => - 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; + 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; @override - String get appSettings_notifications => 'Уведомления'; + String get appSettings_notifications => 'Уведомления'; @override - String get appSettings_enableNotifications => 'Включить уведомления'; + String get appSettings_enableNotifications => + 'Включить уведомления'; @override String get appSettings_enableNotificationsSubtitle => - 'Получать уведомления о сообщениях и оповещениях'; + 'Получать уведомления о сообщениях и оповещениях'; @override String get appSettings_notificationPermissionDenied => - 'Разрешение на уведомления отклонено'; + 'Разрешение на уведомления отклонено'; @override - String get appSettings_notificationsEnabled => 'Уведомления включены'; + String get appSettings_notificationsEnabled => + 'Уведомления включены'; @override - String get appSettings_notificationsDisabled => 'Уведомления отключены'; + String get appSettings_notificationsDisabled => + 'Уведомления отключены'; @override - String get appSettings_messageNotifications => 'Уведомления о сообщениях'; + String get appSettings_messageNotifications => + 'Уведомления о сообщениях'; @override String get appSettings_messageNotificationsSubtitle => - 'Показывать уведомление при получении новых сообщений'; + 'Показывать уведомление при получении новых сообщений'; @override String get appSettings_channelMessageNotifications => - 'Уведомления о сообщениях в каналах'; + 'Уведомления о сообщениях в каналах'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Показывать уведомление при получении сообщений в каналах'; + 'Показывать уведомление при получении сообщений в каналах'; @override String get appSettings_advertisementNotifications => - 'Уведомления об анонсированиях'; + 'Уведомления об анонсированиях'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Показывать уведомление при обнаружении новых нод'; + 'Показывать уведомление при обнаружении новых нод'; @override - String get appSettings_messaging => 'Обмен сообщениями'; + String get appSettings_messaging => 'Обмен сообщениями'; @override String get appSettings_clearPathOnMaxRetry => - 'Сбросить маршрут после максимального числа попыток'; + 'Сбросить маршрут после максимального числа попыток'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; + 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; @override String get appSettings_pathsWillBeCleared => - 'Маршруты будут сброшены после 5 неудачных попыток'; + 'Маршруты будут сброшены после 5 неудачных попыток'; @override String get appSettings_pathsWillNotBeCleared => - 'Маршруты не будут автоматически сбрасываться'; + 'Маршруты не будут автоматически сбрасываться'; @override String get appSettings_autoRouteRotation => - 'Автоматическое переключение маршрутов'; + 'Автоматическое переключение маршрутов'; @override String get appSettings_autoRouteRotationSubtitle => - 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; + 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; @override String get appSettings_autoRouteRotationEnabled => - 'Автоматическое переключение маршрутов включено'; + 'Автоматическое переключение маршрутов включено'; @override String get appSettings_autoRouteRotationDisabled => - 'Автоматическое переключение маршрутов отключено'; + 'Автоматическое переключение маршрутов отключено'; @override - String get appSettings_battery => 'Батарея'; + String get appSettings_battery => 'Батарея'; @override - String get appSettings_batteryChemistry => 'Химия батареи'; + String get appSettings_batteryChemistry => 'Химия батареи'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Установить для устройства ($deviceName)'; + return 'Установить для устройства ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Подключитесь к устройству, чтобы выбрать'; + 'Подключитесь к устройству, чтобы выбрать'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 В)'; + String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 Ð’)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 В)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 Ð’)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; + String get appSettings_batteryLipo => 'LiPo (3.0–4.2 Ð’)'; @override - String get appSettings_mapDisplay => 'Отображение карты'; + String get appSettings_mapDisplay => 'Отображение карты'; @override - String get appSettings_showRepeaters => 'Показывать репитеры'; + String get appSettings_showRepeaters => + 'Показывать репитеры'; @override String get appSettings_showRepeatersSubtitle => - 'Отображать репитеры на карте'; + 'Отображать репитеры на карте'; @override - String get appSettings_showChatNodes => 'Показывать чат-ноды'; + String get appSettings_showChatNodes => + 'Показывать чат-ноды'; @override String get appSettings_showChatNodesSubtitle => - 'Отображать чат-ноды на карте'; + 'Отображать чат-ноды на карте'; @override - String get appSettings_showOtherNodes => 'Показывать другие ноды'; + String get appSettings_showOtherNodes => + 'Показывать другие ноды'; @override String get appSettings_showOtherNodesSubtitle => - 'Отображать другие типы нод на карте'; + 'Отображать другие типы нод на карте'; @override - String get appSettings_timeFilter => 'Фильтр по времени'; + String get appSettings_timeFilter => 'Фильтр по времени'; @override - String get appSettings_timeFilterShowAll => 'Показывать все ноды'; + String get appSettings_timeFilterShowAll => + 'Показывать все ноды'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Показывать ноды за последние $hours ч'; + return 'Показывать ноды за последние $hours ч'; } @override - String get appSettings_mapTimeFilter => 'Временной фильтр карты'; + String get appSettings_mapTimeFilter => + 'Временной фильтр карты'; @override String get appSettings_showNodesDiscoveredWithin => - 'Показывать ноды, обнаруженные за:'; + 'Показывать ноды, обнаруженные за:'; @override - String get appSettings_allTime => 'Всё время'; + String get appSettings_allTime => 'Всё время'; @override - String get appSettings_lastHour => 'Последний час'; + String get appSettings_lastHour => 'Последний час'; @override - String get appSettings_last6Hours => 'Последние 6 часов'; + String get appSettings_last6Hours => 'Последние 6 часов'; @override - String get appSettings_last24Hours => 'Последние 24 часа'; + String get appSettings_last24Hours => 'Последние 24 часа'; @override - String get appSettings_lastWeek => 'Последнюю неделю'; + String get appSettings_lastWeek => 'Последнюю неделю'; @override - String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; + String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; @override - String get appSettings_unitsTitle => 'Единицы'; + String get appSettings_unitsTitle => 'Единицы'; @override - String get appSettings_unitsMetric => 'Метрическая (м/км)'; + String get appSettings_unitsMetric => 'Метрическая (м/км)'; @override - String get appSettings_unitsImperial => 'Имперская (ft / mi)'; + String get appSettings_unitsImperial => 'Имперская (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Область не выбрана'; + String get appSettings_noAreaSelected => 'Область не выбрана'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Область выбрана (масштаб $minZoom–$maxZoom)'; + return 'Область выбрана (масштаб $minZoom–$maxZoom)'; } @override - String get appSettings_debugCard => 'Отладка'; + String get appSettings_debugCard => 'Отладка'; @override - String get appSettings_appDebugLogging => 'Журнал отладки приложения'; + String get appSettings_appDebugLogging => + 'Журнал отладки приложения'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записывать отладочные сообщения приложения для диагностики'; + 'Записывать отладочные сообщения приложения для диагностики'; @override String get appSettings_appDebugLoggingEnabled => - 'Журнал отладки приложения включён'; + 'Журнал отладки приложения включён'; @override String get appSettings_appDebugLoggingDisabled => - 'Журнал отладки приложения отключён'; + 'Журнал отладки приложения отключён'; @override - String get contacts_title => 'Контакты'; + String get contacts_title => 'Контакты'; @override - String get contacts_noContacts => 'Контактов пока нет'; + String get contacts_noContacts => 'Контактов пока нет'; @override String get contacts_contactsWillAppear => - 'Контакты появятся, когда устройства начнут рассылать оповещения'; + 'Контакты появятся, когда устройства начнут рассылать оповещения'; @override - String get contacts_unread => 'Непрочитанное'; + String get contacts_unread => 'Непрочитанное'; @override - String get contacts_searchContactsNoNumber => 'Поиск контактов...'; + String get contacts_searchContactsNoNumber => + 'Поиск контактов...'; @override String contacts_searchContacts(int number, String str) { - return 'Поиск контактов...'; + return 'Поиск контактов...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Поиск $number$str избранного...'; + return 'Поиск $number$str избранного...'; } @override String contacts_searchUsers(int number, String str) { - return 'Поиск $number$str пользователей...'; + return 'Поиск $number$str пользователей...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Поиск $number$str ретрансляторов...'; + return 'Поиск $number$str ретрансляторов...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Поиск $number$str серверов комнат...'; + return 'Поиск $number$str серверов комнат...'; } @override - String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; + String get contacts_noUnreadContacts => + 'Нет непрочитанных контактов'; @override - String get contacts_noContactsFound => 'Контакты или группы не найдены'; + String get contacts_noContactsFound => + 'Контакты или группы не найдены'; @override - String get contacts_deleteContact => 'Удалить контакт'; + String get contacts_deleteContact => 'Удалить контакт'; @override String contacts_removeConfirm(String contactName) { - return 'Удалить $contactName из контактов?'; + return 'Удалить $contactName из контактов?'; } @override - String get contacts_manageRepeater => 'Управление репитером'; + String get contacts_manageRepeater => + 'Управление репитером'; @override - String get contacts_manageRoom => 'Управление сервером комнат'; + String get contacts_manageRoom => + 'Управление сервером комнат'; @override - String get contacts_roomLogin => 'Вход на сервер комнат'; + String get contacts_roomLogin => 'Вход на сервер комнат'; @override - String get contacts_openChat => 'Открыть чат'; + String get contacts_openChat => 'Открыть чат'; @override - String get contacts_editGroup => 'Изменить группу'; + String get contacts_editGroup => 'Изменить группу'; @override - String get contacts_deleteGroup => 'Удалить группу'; + String get contacts_deleteGroup => 'Удалить группу'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Удалить \"$groupName\"?'; + return 'Удалить \"$groupName\"?'; } @override - String get contacts_newGroup => 'Новая группа'; + String get contacts_newGroup => 'Новая группа'; @override - String get contacts_groupName => 'Имя группы'; + String get contacts_groupName => 'Имя группы'; @override - String get contacts_groupNameRequired => 'Имя группы обязательно'; + String get contacts_groupNameRequired => + 'Имя группы обязательно'; @override String contacts_groupAlreadyExists(String name) { - return 'Группа \"$name\" уже существует'; + return 'Группа \"$name\" уже существует'; } @override - String get contacts_filterContacts => 'Фильтр контактов...'; + String get contacts_filterContacts => 'Фильтр контактов...'; @override String get contacts_noContactsMatchFilter => - 'Нет контактов, соответствующих фильтру'; + 'Нет контактов, соответствующих фильтру'; @override - String get contacts_noMembers => 'Нет участников'; + String get contacts_noMembers => 'Нет участников'; @override - String get contacts_lastSeenNow => 'Видели только что'; + String get contacts_lastSeenNow => 'Видели только что'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Видели $minutes мин назад'; + return 'Видели $minutes мин назад'; } @override - String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; + String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Видели $hours ч назад'; + return 'Видели $hours ч назад'; } @override - String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; + String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Видели $days дн. назад'; + return 'Видели $days дн. назад'; } @override - String get channels_title => 'Каналы'; + String get channels_title => 'Каналы'; @override - String get channels_noChannelsConfigured => 'Каналы не настроены'; + String get channels_noChannelsConfigured => + 'Каналы не настроены'; @override - String get channels_addPublicChannel => 'Добавить публичный канал'; + String get channels_addPublicChannel => + 'Добавить публичный канал'; @override - String get channels_searchChannels => 'Поиск каналов...'; + String get channels_searchChannels => 'Поиск каналов...'; @override - String get channels_noChannelsFound => 'Каналы не найдены'; + String get channels_noChannelsFound => 'Каналы не найдены'; @override String channels_channelIndex(int index) { - return 'Канал $index'; + return 'Канал $index'; } @override - String get channels_hashtagChannel => 'Хэштег-канал'; + String get channels_hashtagChannel => 'Хэштег-канал'; @override - String get channels_public => 'Публичный'; + String get channels_public => 'Публичный'; @override - String get channels_private => 'Приватный'; + String get channels_private => 'Приватный'; @override - String get channels_publicChannel => 'Публичный канал'; + String get channels_publicChannel => 'Публичный канал'; @override - String get channels_privateChannel => 'Приватный канал'; + String get channels_privateChannel => 'Приватный канал'; @override - String get channels_editChannel => 'Изменить канал'; + String get channels_editChannel => 'Изменить канал'; @override - String get channels_muteChannel => 'Отключить уведомления канала'; + String get channels_muteChannel => + 'Отключить уведомления канала'; @override - String get channels_unmuteChannel => 'Включить уведомления канала'; + String get channels_unmuteChannel => + 'Включить уведомления канала'; @override - String get channels_deleteChannel => 'Удалить канал'; + String get channels_deleteChannel => 'Удалить канал'; @override String channels_deleteChannelConfirm(String name) { - return 'Удалить \"$name\"? Это действие нельзя отменить.'; + return 'Удалить \"$name\"? Это действие нельзя отменить.'; } @override String channels_channelDeleteFailed(String name) { - return 'Не удалось удалить канал $name.'; + return 'Не удалось удалить канал $name.'; } @override String channels_channelDeleted(String name) { - return 'Канал \"$name\" удалён'; + return 'Канал \"$name\" удалён'; } @override - String get channels_addChannel => 'Добавить канал'; + String get channels_addChannel => 'Добавить канал'; @override - String get channels_channelIndexLabel => 'Индекс канала'; + String get channels_channelIndexLabel => 'Индекс канала'; @override - String get channels_channelName => 'Имя канала'; + String get channels_channelName => 'Имя канала'; @override - String get channels_usePublicChannel => 'Использовать публичный канал'; + String get channels_usePublicChannel => + 'Использовать публичный канал'; @override - String get channels_standardPublicPsk => 'Стандартный публичный PSK'; + String get channels_standardPublicPsk => + 'Стандартный публичный PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Сгенерировать случайный PSK'; + String get channels_generateRandomPsk => + 'Сгенерировать случайный PSK'; @override - String get channels_enterChannelName => 'Введите имя канала'; + String get channels_enterChannelName => 'Введите имя канала'; @override String get channels_pskMustBe32Hex => - 'PSK должен содержать 32 шестнадцатеричных символа'; + 'PSK должен содержать 32 шестнадцатеричных символа'; @override String channels_channelAdded(String name) { - return 'Канал \"$name\" добавлен'; + return 'Канал \"$name\" добавлен'; } @override String channels_editChannelTitle(int index) { - return 'Изменить канал $index'; + return 'Изменить канал $index'; } @override - String get channels_smazCompression => 'Сжатие SMAZ'; + String get channels_smazCompression => 'Сжатие SMAZ'; @override String channels_channelUpdated(String name) { - return 'Канал \"$name\" обновлён'; + return 'Канал \"$name\" обновлён'; } @override - String get channels_publicChannelAdded => 'Публичный канал добавлен'; + String get channels_publicChannelAdded => + 'Публичный канал добавлен'; @override - String get channels_sortBy => 'Сортировка'; + String get channels_sortBy => 'Сортировка'; @override - String get channels_sortManual => 'Вручную'; + String get channels_sortManual => 'Вручную'; @override - String get channels_sortAZ => 'По алфавиту'; + String get channels_sortAZ => 'По алфавиту'; @override - String get channels_sortLatestMessages => 'По последним сообщениям'; + String get channels_sortLatestMessages => + 'По последним сообщениям'; @override - String get channels_sortUnread => 'По непрочитанным'; + String get channels_sortUnread => 'По непрочитанным'; @override - String get channels_createPrivateChannel => 'Создать приватный канал'; + String get channels_createPrivateChannel => + 'Создать приватный канал'; @override - String get channels_createPrivateChannelDesc => 'Защищён секретным ключом.'; + String get channels_createPrivateChannelDesc => + 'Защищён секретным ключом.'; @override String get channels_joinPrivateChannel => - 'Присоединиться к приватному каналу'; + 'Присоединиться к приватному каналу'; @override String get channels_joinPrivateChannelDesc => - 'Введите секретный ключ вручную.'; + 'Введите секретный ключ вручную.'; @override - String get channels_joinPublicChannel => 'Присоединиться к публичному каналу'; + String get channels_joinPublicChannel => + 'Присоединиться к публичному каналу'; @override String get channels_joinPublicChannelDesc => - 'К этому каналу может присоединиться любой.'; + 'К этому каналу может присоединиться любой.'; @override - String get channels_joinHashtagChannel => 'Присоединиться к хэштег-каналу'; + String get channels_joinHashtagChannel => + 'Присоединиться к хэштег-каналу'; @override String get channels_joinHashtagChannelDesc => - 'К хэштег-каналам может присоединиться любой.'; + 'К хэштег-каналам может присоединиться любой.'; @override - String get channels_scanQrCode => 'Сканировать QR-код'; + String get channels_scanQrCode => 'Сканировать QR-код'; @override - String get channels_scanQrCodeComingSoon => 'Скоро будет'; + String get channels_scanQrCodeComingSoon => 'Скоро будет'; @override - String get channels_enterHashtag => 'Введите хэштег'; + String get channels_enterHashtag => 'Введите хэштег'; @override - String get channels_hashtagHint => 'например, #команда'; + String get channels_hashtagHint => 'например, #команда'; @override - String get chat_noMessages => 'Сообщений пока нет'; + String get chat_noMessages => 'Сообщений пока нет'; @override - String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; + String get chat_sendMessageToStart => + 'Отправьте сообщение, чтобы начать'; @override - String get chat_originalMessageNotFound => 'Исходное сообщение не найдено'; + String get chat_originalMessageNotFound => + 'Исходное сообщение не найдено'; @override String chat_replyingTo(String name) { - return 'Ответ для $name'; + return 'Ответ для $name'; } @override String chat_replyTo(String name) { - return 'Ответить $name'; + return 'Ответить $name'; } @override - String get chat_location => 'Местоположение'; + String get chat_location => 'Местоположение'; @override String chat_sendMessageTo(String contactName) { - return 'Отправить сообщение $contactName'; + return 'Отправить сообщение $contactName'; } @override - String get chat_typeMessage => 'Напишите сообщение...'; + String get chat_typeMessage => 'Напишите сообщение...'; @override String chat_messageTooLong(int maxBytes) { - return 'Сообщение слишком длинное (макс. $maxBytes байт).'; + return 'Сообщение слишком длинное (макс. $maxBytes байт).'; } @override - String get chat_messageCopied => 'Сообщение скопировано'; + String get chat_messageCopied => 'Сообщение скопировано'; @override - String get chat_messageDeleted => 'Сообщение удалено'; + String get chat_messageDeleted => 'Сообщение удалено'; @override - String get chat_retryingMessage => 'Повтор отправки сообщения'; + String get chat_retryingMessage => + 'Повтор отправки сообщения'; @override String chat_retryCount(int current, int max) { - return 'Попытка $current/$max'; + return 'Попытка $current/$max'; } @override - String get chat_sendGif => 'Отправить GIF'; + String get chat_sendGif => 'Отправить GIF'; @override - String get chat_reply => 'Ответить'; + String get chat_reply => 'Ответить'; @override - String get chat_addReaction => 'Добавить реакцию'; + String get chat_addReaction => 'Добавить реакцию'; @override - String get chat_me => 'Я'; + String get chat_me => 'Я'; @override - String get emojiCategorySmileys => 'Смайлы'; + String get emojiCategorySmileys => 'Смайлы'; @override - String get emojiCategoryGestures => 'Жесты'; + String get emojiCategoryGestures => 'Жесты'; @override - String get emojiCategoryHearts => 'Сердечки'; + String get emojiCategoryHearts => 'Сердечки'; @override - String get emojiCategoryObjects => 'Предметы'; + String get emojiCategoryObjects => 'Предметы'; @override - String get gifPicker_title => 'Выберите GIF'; + String get gifPicker_title => 'Выберите GIF'; @override - String get gifPicker_searchHint => 'Поиск GIF...'; + String get gifPicker_searchHint => 'Поиск GIF...'; @override - String get gifPicker_poweredBy => 'Работает на GIPHY'; + String get gifPicker_poweredBy => 'Работает на GIPHY'; @override - String get gifPicker_noGifsFound => 'GIF не найдены'; + String get gifPicker_noGifsFound => 'GIF не найдены'; @override - String get gifPicker_failedLoad => 'Не удалось загрузить GIF'; + String get gifPicker_failedLoad => + 'Не удалось загрузить GIF'; @override - String get gifPicker_failedSearch => 'Не удалось выполнить поиск GIF'; + String get gifPicker_failedSearch => + 'Не удалось выполнить поиск GIF'; @override - String get gifPicker_noInternet => 'Нет подключения к интернету'; + String get gifPicker_noInternet => + 'Нет подключения к интернету'; @override - String get debugLog_appTitle => 'Журнал отладки приложения'; + String get debugLog_appTitle => + 'Журнал отладки приложения'; @override - String get debugLog_bleTitle => 'Журнал отладки BLE'; + String get debugLog_bleTitle => 'Журнал отладки BLE'; @override - String get debugLog_copyLog => 'Копировать журнал'; + String get debugLog_copyLog => 'Копировать журнал'; @override - String get debugLog_clearLog => 'Очистить журнал'; + String get debugLog_clearLog => 'Очистить журнал'; @override - String get debugLog_copied => 'Журнал отладки скопирован'; + String get debugLog_copied => + 'Журнал отладки скопирован'; @override - String get debugLog_bleCopied => 'Журнал BLE скопирован'; + String get debugLog_bleCopied => 'Журнал BLE скопирован'; @override - String get debugLog_noEntries => 'Журнал отладки пока пуст'; + String get debugLog_noEntries => + 'Журнал отладки пока пуст'; @override String get debugLog_enableInSettings => - 'Включите запись журнала отладки в настройках'; + 'Включите запись журнала отладки в настройках'; @override - String get debugLog_frames => 'Фреймы'; + String get debugLog_frames => 'Фреймы'; @override - String get debugLog_rawLogRx => 'Сырой журнал приёма'; + String get debugLog_rawLogRx => 'Сырой журнал приёма'; @override - String get debugLog_noBleActivity => 'Активность BLE пока отсутствует'; + String get debugLog_noBleActivity => + 'Активность BLE пока отсутствует'; @override String debugFrame_length(int count) { - return 'Длина фрейма: $count байт'; + return 'Длина фрейма: $count байт'; } @override String debugFrame_command(String value) { - return 'Команда: 0x$value'; + return 'Команда: 0x$value'; } @override - String get debugFrame_textMessageHeader => 'Фрейм текстового сообщения:'; + String get debugFrame_textMessageHeader => + 'Фрейм текстового сообщения:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Публичный ключ получателя: $pubKey'; + return '- Публичный ключ получателя: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Временная метка: $timestamp'; + return '- Временная метка: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Флаги: 0x$value'; + return '- Флаги: 0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- Тип текста: $type ($label)'; + return '- Тип текста: $type ($label)'; } @override String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Обычный'; + String get debugFrame_textTypePlain => 'Обычный'; @override String debugFrame_text(String text) { - return '- Текст: \"$text\"'; + return '- Текст: \"$text\"'; } @override - String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; + String get debugFrame_hexDump => + 'Шестнадцатеричный дамп:'; @override - String get chat_pathManagement => 'Управление маршрутами'; + String get chat_pathManagement => 'Управление маршрутами'; @override - String get chat_ShowAllPaths => 'Показать все пути'; + String get chat_ShowAllPaths => 'Показать все пути'; @override - String get chat_routingMode => 'Режим маршрутизации'; + String get chat_routingMode => 'Режим маршрутизации'; @override - String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; + String get chat_autoUseSavedPath => + 'Авто (использовать сохранённый маршрут)'; @override - String get chat_forceFloodMode => 'Принудительный режим рассылки'; + String get chat_forceFloodMode => + 'Принудительный режим рассылки'; @override String get chat_recentAckPaths => - 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; + 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; @override String get chat_pathHistoryFull => - 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; + 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; @override - String get chat_hopSingular => 'хоп'; + String get chat_hopSingular => 'хоп'; @override - String get chat_hopPlural => 'хопов'; + String get chat_hopPlural => 'хопов'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', ); return '$count $_temp0'; } @override - String get chat_successes => 'успешно'; + String get chat_successes => 'успешно'; @override - String get chat_removePath => 'Удалить маршрут'; + String get chat_removePath => 'Удалить маршрут'; @override String get chat_noPathHistoryYet => - 'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; + 'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; @override - String get chat_pathActions => 'Действия с маршрутом:'; + String get chat_pathActions => 'Действия с маршрутом:'; @override - String get chat_setCustomPath => 'Указать маршрут вручную'; + String get chat_setCustomPath => + 'Указать маршрут вручную'; @override - String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи'; + String get chat_setCustomPathSubtitle => + 'Вручную задать маршрут передачи'; @override - String get chat_clearPath => 'Очистить маршрут'; + String get chat_clearPath => 'Очистить маршрут'; @override String get chat_clearPathSubtitle => - 'Принудительно обновить маршрут при следующей отправке'; + 'Принудительно обновить маршрут при следующей отправке'; @override String get chat_pathCleared => - 'Маршрут очищен. Следующее сообщение обновит маршрут.'; + 'Маршрут очищен. Следующее сообщение обновит маршрут.'; @override String get chat_floodModeSubtitle => - 'Используйте переключатель маршрутизации в панели приложения'; + 'Используйте переключатель маршрутизации в панели приложения'; @override String get chat_floodModeEnabled => - 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; + 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; @override - String get chat_fullPath => 'Полный маршрут'; + String get chat_fullPath => 'Полный маршрут'; @override String get chat_pathDetailsNotAvailable => - 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; + 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; @override String chat_pathSetHops(int hopCount, String status) { String _temp0 = intl.Intl.pluralLogic( hopCount, locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', ); - return 'Маршрут установлен: $hopCount $_temp0 — $status'; + return 'Маршрут установлен: $hopCount $_temp0 — $status'; } @override String get chat_pathSavedLocally => - 'Сохранено локально. Подключитесь для синхронизации.'; + 'Сохранено локально. Подключитесь для синхронизации.'; @override - String get chat_pathDeviceConfirmed => 'Подтверждено устройством.'; + String get chat_pathDeviceConfirmed => + 'Подтверждено устройством.'; @override - String get chat_pathDeviceNotConfirmed => 'Ещё не подтверждено устройством.'; + String get chat_pathDeviceNotConfirmed => + 'Ещё не подтверждено устройством.'; @override - String get chat_type => 'Тип'; + String get chat_type => 'Тип'; @override - String get chat_path => 'Маршрут'; + String get chat_path => 'Маршрут'; @override - String get chat_publicKey => 'Публичный ключ'; + String get chat_publicKey => 'Публичный ключ'; @override - String get chat_compressOutgoingMessages => 'Сжимать исходящие сообщения'; + String get chat_compressOutgoingMessages => + 'Сжимать исходящие сообщения'; @override - String get chat_floodForced => 'Рассылка (принудительно)'; + String get chat_floodForced => + 'Рассылка (принудительно)'; @override - String get chat_directForced => 'Прямой (принудительно)'; + String get chat_directForced => 'Прямой (принудительно)'; @override String chat_hopsForced(int count) { - return '$count хоп(ов) (принудительно)'; + return '$count хоп(ов) (принудительно)'; } @override - String get chat_floodAuto => 'Рассылка (авто)'; + String get chat_floodAuto => 'Рассылка (авто)'; @override - String get chat_direct => 'Прямой'; + String get chat_direct => 'Прямой'; @override - String get chat_poiShared => 'Точка интереса отправлена'; + String get chat_poiShared => + 'Точка интереса отправлена'; @override String chat_unread(int count) { - return 'Непрочитанных: $count'; + return 'Непрочитанных: $count'; } @override - String get chat_openLink => 'Открыть ссылку?'; + String get chat_openLink => 'Открыть ссылку?'; @override String get chat_openLinkConfirmation => - 'Хотите открыть эту ссылку в вашем браузере?'; + 'Хотите открыть эту ссылку в вашем браузере?'; @override - String get chat_open => 'Открыть'; + String get chat_open => 'Открыть'; @override String chat_couldNotOpenLink(String url) { - return 'Не удалось открыть ссылку: $url'; + return 'Не удалось открыть ссылку: $url'; } @override - String get chat_invalidLink => 'Неправильный формат ссылки'; + String get chat_invalidLink => + 'Неправильный формат ссылки'; @override - String get map_title => 'Карта нод'; + String get map_title => 'Карта нод'; @override - String get map_lineOfSight => 'Линия видимости'; + String get map_lineOfSight => 'Линия видимости'; @override - String get map_losScreenTitle => 'Линия видимости'; + String get map_losScreenTitle => 'Линия видимости'; @override - String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; + String get map_noNodesWithLocation => + 'Нет нод с данными о местоположении'; @override String get map_nodesNeedGps => - 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; + 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; @override String map_nodesCount(int count) { - return 'Нод: $count'; + return 'Нод: $count'; } @override String map_pinsCount(int count) { - return 'Меток: $count'; + return 'Меток: $count'; } @override - String get map_chat => 'Чат'; + String get map_chat => 'Чат'; @override - String get map_repeater => 'Репитер'; + String get map_repeater => 'Репитер'; @override - String get map_room => 'Комната'; + String get map_room => 'Комната'; @override - String get map_sensor => 'Сенсор'; + String get map_sensor => 'Сенсор'; @override - String get map_pinDm => 'Метка (ЛС)'; + String get map_pinDm => 'Метка (ЛС)'; @override - String get map_pinPrivate => 'Метка (Приватная)'; + String get map_pinPrivate => 'Метка (Приватная)'; @override - String get map_pinPublic => 'Метка (Публичная)'; + String get map_pinPublic => 'Метка (Публичная)'; @override - String get map_lastSeen => 'Последнее появление'; + String get map_lastSeen => 'Последнее появление'; @override String get map_disconnectConfirm => - 'Вы уверены, что хотите отключиться от этого устройства?'; + 'Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?'; @override - String get map_from => 'От'; + String get map_from => 'От'; @override - String get map_source => 'Источник'; + String get map_source => 'Источник'; @override - String get map_flags => 'Флаги'; + String get map_flags => 'Флаги'; @override - String get map_shareMarkerHere => 'Поделиться меткой здесь'; + String get map_shareMarkerHere => + 'Поделиться меткой здесь'; @override - String get map_pinLabel => 'Метка'; + String get map_pinLabel => 'Метка'; @override - String get map_label => 'Подпись'; + String get map_label => 'Подпись'; @override - String get map_pointOfInterest => 'Точка интереса'; + String get map_pointOfInterest => 'Точка интереса'; @override - String get map_sendToContact => 'Отправить контакту'; + String get map_sendToContact => 'Отправить контакту'; @override - String get map_sendToChannel => 'Отправить в канал'; + String get map_sendToChannel => 'Отправить в канал'; @override - String get map_noChannelsAvailable => 'Нет доступных каналов'; + String get map_noChannelsAvailable => + 'Нет доступных каналов'; @override - String get map_publicLocationShare => 'Публичная передача местоположения'; + String get map_publicLocationShare => + 'Публичная передача местоположения'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Вы собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; + return 'Ð’Ñ‹ собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; } @override String get map_connectToShareMarkers => - 'Подключитесь к устройству, чтобы делиться метками'; + 'Подключитесь к устройству, чтобы делиться метками'; @override - String get map_filterNodes => 'Фильтр нод'; + String get map_filterNodes => 'Фильтр нод'; @override - String get map_nodeTypes => 'Типы нод'; + String get map_nodeTypes => 'Типы нод'; @override - String get map_chatNodes => 'Чат-ноды'; + String get map_chatNodes => 'Чат-ноды'; @override - String get map_repeaters => 'Репитеры'; + String get map_repeaters => 'Репитеры'; @override - String get map_otherNodes => 'Другие ноды'; + String get map_otherNodes => 'Другие ноды'; @override - String get map_keyPrefix => 'Префикс ключа'; + String get map_keyPrefix => 'Префикс ключа'; @override - String get map_filterByKeyPrefix => 'Фильтр по префиксу ключа'; + String get map_filterByKeyPrefix => + 'Фильтр по префиксу ключа'; @override - String get map_publicKeyPrefix => 'Префикс публичного ключа'; + String get map_publicKeyPrefix => + 'Префикс публичного ключа'; @override - String get map_markers => 'Метки'; + String get map_markers => 'Метки'; @override - String get map_showSharedMarkers => 'Показывать общие метки'; + String get map_showSharedMarkers => + 'Показывать общие метки'; @override - String get map_lastSeenTime => 'Время последнего появления'; + String get map_lastSeenTime => + 'Время последнего появления'; @override - String get map_sharedPin => 'Общая метка'; + String get map_sharedPin => 'Общая метка'; @override - String get map_joinRoom => 'Присоединиться к комнате'; + String get map_joinRoom => 'Присоединиться к комнате'; @override - String get map_manageRepeater => 'Управление репитером'; + String get map_manageRepeater => 'Управление репитером'; @override - String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.'; + String get map_tapToAdd => + 'Нажимайте на узлы, чтобы добавить их в путь.'; @override - String get map_runTrace => 'Запустить трассировку пути'; + String get map_runTrace => + 'Запустить трассировку пути'; @override - String get map_removeLast => 'Удалить последний'; + String get map_removeLast => 'Удалить последний'; @override - String get map_pathTraceCancelled => 'Отмена трассировки пути'; + String get map_pathTraceCancelled => + 'Отмена трассировки пути'; @override - String get mapCache_title => 'Кэш офлайн-карты'; + String get mapCache_title => 'Кэш офлайн-карты'; @override String get mapCache_selectAreaFirst => - 'Сначала выберите область для кэширования'; + 'Сначала выберите область для кэширования'; @override String get mapCache_noTilesToDownload => - 'Нет плиток для загрузки в этой области'; + 'Нет плиток для загрузки в этой области'; @override - String get mapCache_downloadTilesTitle => 'Загрузить плитки'; + String get mapCache_downloadTilesTitle => 'Загрузить плитки'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Загрузить $count плиток для офлайн-использования?'; + return 'Загрузить $count плиток для офлайн-использования?'; } @override - String get mapCache_downloadAction => 'Загрузить'; + String get mapCache_downloadAction => 'Загрузить'; @override String mapCache_cachedTiles(int count) { - return 'Закэшировано $count плиток'; + return 'Закэшировано $count плиток'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Закэшировано $downloaded плиток ($failed не загружено)'; + return 'Закэшировано $downloaded плиток ($failed не загружено)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Очистить офлайн-кэш'; + String get mapCache_clearOfflineCacheTitle => + 'Очистить офлайн-кэш'; @override String get mapCache_clearOfflineCachePrompt => - 'Удалить все закэшированные плитки карты?'; + 'Удалить все закэшированные плитки карты?'; @override - String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; + String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; @override - String get mapCache_noAreaSelected => 'Область не выбрана'; + String get mapCache_noAreaSelected => 'Область не выбрана'; @override - String get mapCache_cacheArea => 'Область кэширования'; + String get mapCache_cacheArea => 'Область кэширования'; @override - String get mapCache_useCurrentView => 'Использовать текущий вид'; + String get mapCache_useCurrentView => + 'Использовать текущий вид'; @override - String get mapCache_zoomRange => 'Диапазон масштаба'; + String get mapCache_zoomRange => 'Диапазон масштаба'; @override String mapCache_estimatedTiles(int count) { - return 'Оценочное количество плиток: $count'; + return 'Оценочное количество плиток: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Загружено $completed из $total'; + return 'Загружено $completed из $total'; } @override - String get mapCache_downloadTilesButton => 'Загрузить плитки'; + String get mapCache_downloadTilesButton => 'Загрузить плитки'; @override - String get mapCache_clearCacheButton => 'Очистить кэш'; + String get mapCache_clearCacheButton => 'Очистить кэш'; @override String mapCache_failedDownloads(int count) { - return 'Неудачных загрузок: $count'; + return 'Неудачных загрузок: $count'; } @override @@ -1569,133 +1649,134 @@ class AppLocalizationsRu extends AppLocalizations { String east, String west, ) { - return 'С $north, Ю $south, В $east, З $west'; + return 'С $north, Ю $south, Ð’ $east, З $west'; } @override - String get time_justNow => 'Только что'; + String get time_justNow => 'Только что'; @override String time_minutesAgo(int minutes) { - return '$minutes мин назад'; + return '$minutes мин назад'; } @override String time_hoursAgo(int hours) { - return '$hours ч назад'; + return '$hours ч назад'; } @override String time_daysAgo(int days) { - return '$days дн. назад'; + return '$days дн. назад'; } @override - String get time_hour => 'час'; + String get time_hour => 'час'; @override - String get time_hours => 'часов'; + String get time_hours => 'часов'; @override - String get time_day => 'день'; + String get time_day => 'день'; @override - String get time_days => 'дней'; + String get time_days => 'дней'; @override - String get time_week => 'неделя'; + String get time_week => 'неделя'; @override - String get time_weeks => 'недель'; + String get time_weeks => 'недель'; @override - String get time_month => 'месяц'; + String get time_month => 'месяц'; @override - String get time_months => 'месяцев'; + String get time_months => 'месяцев'; @override - String get time_minutes => 'минут'; + String get time_minutes => 'минут'; @override - String get time_allTime => 'Всё время'; + String get time_allTime => 'Всё время'; @override - String get dialog_disconnect => 'Отключиться'; + String get dialog_disconnect => 'Отключиться'; @override String get dialog_disconnectConfirm => - 'Вы уверены, что хотите отключиться от этого устройства?'; + 'Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?'; @override - String get login_repeaterLogin => 'Вход в репитер'; + String get login_repeaterLogin => 'Вход в репитер'; @override - String get login_roomLogin => 'Вход на сервер комнат'; + String get login_roomLogin => 'Вход на сервер комнат'; @override - String get login_password => 'Пароль'; + String get login_password => 'Пароль'; @override - String get login_enterPassword => 'Введите пароль'; + String get login_enterPassword => 'Введите пароль'; @override - String get login_savePassword => 'Сохранить пароль'; + String get login_savePassword => 'Сохранить пароль'; @override String get login_savePasswordSubtitle => - 'Пароль будет надёжно сохранён на этом устройстве'; + 'Пароль будет надёжно сохранён на этом устройстве'; @override String get login_repeaterDescription => - 'Введите пароль репитера для доступа к настройкам и статусу.'; + 'Введите пароль репитера для доступа к настройкам и статусу.'; @override String get login_roomDescription => - 'Введите пароль комнаты для доступа к настройкам и статусу.'; + 'Введите пароль комнаты для доступа к настройкам и статусу.'; @override - String get login_routing => 'Маршрутизация'; + String get login_routing => 'Маршрутизация'; @override - String get login_routingMode => 'Режим маршрутизации'; + String get login_routingMode => 'Режим маршрутизации'; @override String get login_autoUseSavedPath => - 'Авто (использовать сохранённый маршрут)'; + 'Авто (использовать сохранённый маршрут)'; @override - String get login_forceFloodMode => 'Принудительный режим рассылки'; + String get login_forceFloodMode => + 'Принудительный режим рассылки'; @override - String get login_managePaths => 'Управление маршрутами'; + String get login_managePaths => 'Управление маршрутами'; @override - String get login_login => 'Войти'; + String get login_login => 'Войти'; @override String login_attempt(int current, int max) { - return 'Попытка $current/$max'; + return 'Попытка $current/$max'; } @override String login_failed(String error) { - return 'Ошибка входа: $error'; + return 'Ошибка входа: $error'; } @override String get login_failedMessage => - 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; + 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; @override - String get common_reload => 'Обновить'; + String get common_reload => 'Обновить'; @override - String get common_clear => 'Очистить'; + String get common_clear => 'Очистить'; @override String path_currentPath(String path) { - return 'Текущий маршрут: $path'; + return 'Текущий маршрут: $path'; } @override @@ -1703,171 +1784,185 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', ); - return 'Используется маршрут из $count $_temp0'; + return 'Используется маршрут из $count $_temp0'; } @override - String get path_enterCustomPath => 'Введите маршрут вручную'; + String get path_enterCustomPath => + 'Введите маршрут вручную'; @override - String get path_currentPathLabel => 'Текущий маршрут'; + String get path_currentPathLabel => 'Текущий маршрут'; @override String get path_hexPrefixInstructions => - 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; + 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; @override String get path_hexPrefixExample => - 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; + 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; @override - String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)'; + String get path_labelHexPrefixes => + 'Маршрут (шестнадцатеричные префиксы)'; @override String get path_helperMaxHops => - 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; + 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; @override - String get path_selectFromContacts => 'Или выберите из контактов:'; + String get path_selectFromContacts => + 'Или выберите из контактов:'; @override - String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; + String get path_noRepeatersFound => + 'Репитеры или серверы комнат не найдены.'; @override String get path_customPathsRequire => - 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; + 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; + return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; } @override - String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.'; + String get path_tooLong => + 'Маршрут слишком длинный. Максимум 64 хопа.'; @override - String get path_setPath => 'Установить маршрут'; + String get path_setPath => 'Установить маршрут'; @override - String get repeater_management => 'Управление репитером'; + String get repeater_management => 'Управление репитером'; @override - String get room_management => 'Управление сервером комнат'; + String get room_management => + 'Управление сервером комнат'; @override - String get repeater_managementTools => 'Инструменты управления'; + String get repeater_managementTools => + 'Инструменты управления'; @override - String get repeater_status => 'Статус'; + String get repeater_status => 'Статус'; @override String get repeater_statusSubtitle => - 'Просмотр статуса, статистики и соседей репитера'; + 'Просмотр статуса, статистики и соседей репитера'; @override - String get repeater_telemetry => 'Телеметрия'; + String get repeater_telemetry => 'Телеметрия'; @override String get repeater_telemetrySubtitle => - 'Просмотр телеметрии датчиков и системной статистики'; + 'Просмотр телеметрии датчиков и системной статистики'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Отправка команд репитеру'; + String get repeater_cliSubtitle => + 'Отправка команд репитеру'; @override - String get repeater_neighbors => 'Соседи'; + String get repeater_neighbors => 'Соседи'; @override - String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.'; + String get repeater_neighborsSubtitle => + 'Просмотр соседей на нулевом хопе.'; @override - String get repeater_settings => 'Настройки'; + String get repeater_settings => 'Настройки'; @override - String get repeater_settingsSubtitle => 'Настройка параметров репитера'; + String get repeater_settingsSubtitle => + 'Настройка параметров репитера'; @override - String get repeater_statusTitle => 'Статус репитера'; + String get repeater_statusTitle => 'Статус репитера'; @override - String get repeater_routingMode => 'Режим маршрутизации'; + String get repeater_routingMode => 'Режим маршрутизации'; @override String get repeater_autoUseSavedPath => - 'Авто (использовать сохранённый маршрут)'; + 'Авто (использовать сохранённый маршрут)'; @override - String get repeater_forceFloodMode => 'Принудительный режим рассылки'; + String get repeater_forceFloodMode => + 'Принудительный режим рассылки'; @override - String get repeater_pathManagement => 'Управление маршрутами'; + String get repeater_pathManagement => + 'Управление маршрутами'; @override - String get repeater_refresh => 'Обновить'; + String get repeater_refresh => 'Обновить'; @override - String get repeater_statusRequestTimeout => 'Время ожидания статуса истекло.'; + String get repeater_statusRequestTimeout => + 'Время ожидания статуса истекло.'; @override String repeater_errorLoadingStatus(String error) { - return 'Ошибка загрузки статуса: $error'; + return 'Ошибка загрузки статуса: $error'; } @override - String get repeater_systemInformation => 'Системная информация'; + String get repeater_systemInformation => + 'Системная информация'; @override - String get repeater_battery => 'Батарея'; + String get repeater_battery => 'Батарея'; @override - String get repeater_clockAtLogin => 'Время (при входе)'; + String get repeater_clockAtLogin => 'Время (при входе)'; @override - String get repeater_uptime => 'Время работы'; + String get repeater_uptime => 'Время работы'; @override - String get repeater_queueLength => 'Длина очереди'; + String get repeater_queueLength => 'Длина очереди'; @override - String get repeater_debugFlags => 'Флаги отладки'; + String get repeater_debugFlags => 'Флаги отладки'; @override - String get repeater_radioStatistics => 'Радиостатистика'; + String get repeater_radioStatistics => 'Радиостатистика'; @override - String get repeater_lastRssi => 'Последний RSSI'; + String get repeater_lastRssi => 'Последний RSSI'; @override - String get repeater_lastSnr => 'Последний SNR'; + String get repeater_lastSnr => 'Последний SNR'; @override - String get repeater_noiseFloor => 'Уровень шума'; + String get repeater_noiseFloor => 'Уровень шума'; @override - String get repeater_txAirtime => 'Время эфира (передача)'; + String get repeater_txAirtime => 'Время эфира (передача)'; @override - String get repeater_rxAirtime => 'Время эфира (приём)'; + String get repeater_rxAirtime => 'Время эфира (приём)'; @override - String get repeater_packetStatistics => 'Статистика пакетов'; + String get repeater_packetStatistics => 'Статистика пакетов'; @override - String get repeater_sent => 'Отправлено'; + String get repeater_sent => 'Отправлено'; @override - String get repeater_received => 'Получено'; + String get repeater_received => 'Получено'; @override - String get repeater_duplicates => 'Дубликаты'; + String get repeater_duplicates => 'Дубликаты'; @override String repeater_daysHoursMinsSecs( @@ -1876,674 +1971,708 @@ class AppLocalizationsRu extends AppLocalizations { int minutes, int seconds, ) { - return '$days дн. $hoursч $minutesм $secondsс'; + return '$days дн. $hoursч $minutesм $secondsс'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Рассылка: $flood, Прямые: $direct'; + return 'Рассылка: $flood, Прямые: $direct'; } @override String repeater_duplicatesTotal(int total) { - return 'Всего: $total'; + return 'Всего: $total'; } @override - String get repeater_settingsTitle => 'Настройки репитера'; + String get repeater_settingsTitle => 'Настройки репитера'; @override - String get repeater_basicSettings => 'Основные настройки'; + String get repeater_basicSettings => 'Основные настройки'; @override - String get repeater_repeaterName => 'Имя репитера'; + String get repeater_repeaterName => 'Имя репитера'; @override - String get repeater_repeaterNameHelper => 'Отображаемое имя этого репитера'; + String get repeater_repeaterNameHelper => + 'Отображаемое имя этого репитера'; @override - String get repeater_adminPassword => 'Пароль администратора'; + String get repeater_adminPassword => + 'Пароль администратора'; @override - String get repeater_adminPasswordHelper => 'Пароль с полным доступом'; + String get repeater_adminPasswordHelper => + 'Пароль с полным доступом'; @override - String get repeater_guestPassword => 'Гостевой пароль'; + String get repeater_guestPassword => 'Гостевой пароль'; @override String get repeater_guestPasswordHelper => - 'Пароль для доступа только для чтения'; + 'Пароль для доступа только для чтения'; @override - String get repeater_radioSettings => 'Настройки радио'; + String get repeater_radioSettings => 'Настройки радио'; @override - String get repeater_frequencyMhz => 'Частота (МГц)'; + String get repeater_frequencyMhz => 'Частота (МГц)'; @override - String get repeater_frequencyHelper => '300–2500 МГц'; + String get repeater_frequencyHelper => '300–2500 МГц'; @override - String get repeater_txPower => 'Мощность передачи'; + String get repeater_txPower => 'Мощность передачи'; @override - String get repeater_txPowerHelper => '1–30 дБм'; + String get repeater_txPowerHelper => '1–30 дБм'; @override - String get repeater_bandwidth => 'Полоса пропускания'; + String get repeater_bandwidth => 'Полоса пропускания'; @override - String get repeater_spreadingFactor => 'Коэффициент расширения'; + String get repeater_spreadingFactor => + 'Коэффициент расширения'; @override - String get repeater_codingRate => 'Коэффициент кодирования'; + String get repeater_codingRate => + 'Коэффициент кодирования'; @override - String get repeater_locationSettings => 'Настройки местоположения'; + String get repeater_locationSettings => + 'Настройки местоположения'; @override - String get repeater_latitude => 'Широта'; + String get repeater_latitude => 'Широта'; @override String get repeater_latitudeHelper => - 'В десятичных градусах (напр., 37.7749)'; + 'Ð’ десятичных градусах (напр., 37.7749)'; @override - String get repeater_longitude => 'Долгота'; + String get repeater_longitude => 'Долгота'; @override String get repeater_longitudeHelper => - 'В десятичных градусах (напр., -122.4194)'; + 'Ð’ десятичных градусах (напр., -122.4194)'; @override - String get repeater_features => 'Функции'; + String get repeater_features => 'Функции'; @override - String get repeater_packetForwarding => 'Пересылка пакетов'; + String get repeater_packetForwarding => 'Пересылка пакетов'; @override String get repeater_packetForwardingSubtitle => - 'Разрешить репитеру пересылать пакеты'; + 'Разрешить репитеру пересылать пакеты'; @override - String get repeater_guestAccess => 'Гостевой доступ'; + String get repeater_guestAccess => 'Гостевой доступ'; @override String get repeater_guestAccessSubtitle => - 'Разрешить гостевой доступ только для чтения'; + 'Разрешить гостевой доступ только для чтения'; @override - String get repeater_privacyMode => 'Режим конфиденциальности'; + String get repeater_privacyMode => + 'Режим конфиденциальности'; @override String get repeater_privacyModeSubtitle => - 'Скрывать имя/местоположение в оповещениях'; + 'Скрывать имя/местоположение в оповещениях'; @override - String get repeater_advertisementSettings => 'Настройки анонсирования'; + String get repeater_advertisementSettings => + 'Настройки анонсирования'; @override - String get repeater_localAdvertInterval => 'Интервал локальных анонсирований'; + String get repeater_localAdvertInterval => + 'Интервал локальных анонсирований'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes минут'; + return '$minutes минут'; } @override String get repeater_floodAdvertInterval => - 'Интервал анонсирований рассылкой (flood)'; + 'Интервал анонсирований рассылкой (flood)'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours часов'; + return '$hours часов'; } @override String get repeater_encryptedAdvertInterval => - 'Интервал зашифрованных анонсирований'; + 'Интервал зашифрованных анонсирований'; @override - String get repeater_dangerZone => 'Опасная зона'; + String get repeater_dangerZone => 'Опасная зона'; @override - String get repeater_rebootRepeater => 'Перезагрузить репитер'; + String get repeater_rebootRepeater => + 'Перезагрузить репитер'; @override String get repeater_rebootRepeaterSubtitle => - 'Перезапустить устройство репитера'; + 'Перезапустить устройство репитера'; @override String get repeater_rebootRepeaterConfirm => - 'Вы уверены, что хотите перезагрузить этот репитер?'; + 'Ð’Ñ‹ уверены, что хотите перезагрузить этот репитер?'; @override - String get repeater_regenerateIdentityKey => 'Пересоздать ключ идентификации'; + String get repeater_regenerateIdentityKey => + 'Пересоздать ключ идентификации'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Сгенерировать новую пару публичного/приватного ключей'; + 'Сгенерировать новую пару публичного/приватного ключей'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Это создаст новую идентичность для репитера. Продолжить?'; + 'Это создаст новую идентичность для репитера. Продолжить?'; @override - String get repeater_eraseFileSystem => 'Стереть файловую систему'; + String get repeater_eraseFileSystem => + 'Стереть файловую систему'; @override String get repeater_eraseFileSystemSubtitle => - 'Отформатировать файловую систему репитера'; + 'Отформатировать файловую систему репитера'; @override String get repeater_eraseFileSystemConfirm => - 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; + 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; @override String get repeater_eraseSerialOnly => - 'Очистка доступна только через последовательную консоль.'; + 'Очистка доступна только через последовательную консоль.'; @override String repeater_commandSent(String command) { - return 'Команда отправлена: $command'; + return 'Команда отправлена: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Ошибка отправки команды: $error'; + return 'Ошибка отправки команды: $error'; } @override - String get repeater_confirm => 'Подтвердить'; + String get repeater_confirm => 'Подтвердить'; @override - String get repeater_settingsSaved => 'Настройки успешно сохранены'; + String get repeater_settingsSaved => + 'Настройки успешно сохранены'; @override String repeater_errorSavingSettings(String error) { - return 'Ошибка сохранения настроек: $error'; + return 'Ошибка сохранения настроек: $error'; } @override - String get repeater_refreshBasicSettings => 'Обновить основные настройки'; + String get repeater_refreshBasicSettings => + 'Обновить основные настройки'; @override - String get repeater_refreshRadioSettings => 'Обновить настройки радио'; + String get repeater_refreshRadioSettings => + 'Обновить настройки радио'; @override - String get repeater_refreshTxPower => 'Обновить мощность передачи'; + String get repeater_refreshTxPower => + 'Обновить мощность передачи'; @override String get repeater_refreshLocationSettings => - 'Обновить настройки местоположения'; + 'Обновить настройки местоположения'; @override - String get repeater_refreshPacketForwarding => 'Обновить пересылку пакетов'; + String get repeater_refreshPacketForwarding => + 'Обновить пересылку пакетов'; @override - String get repeater_refreshGuestAccess => 'Обновить гостевой доступ'; + String get repeater_refreshGuestAccess => + 'Обновить гостевой доступ'; @override - String get repeater_refreshPrivacyMode => 'Обновить режим конфиденциальности'; + String get repeater_refreshPrivacyMode => + 'Обновить режим конфиденциальности'; @override String get repeater_refreshAdvertisementSettings => - 'Обновить настройки анонсирований'; + 'Обновить настройки анонсирований'; @override String repeater_refreshed(String label) { - return '$label обновлён'; + return '$label обновлён'; } @override String repeater_errorRefreshing(String label) { - return 'Ошибка обновления $label'; + return 'Ошибка обновления $label'; } @override - String get repeater_cliTitle => 'CLI репитера'; + String get repeater_cliTitle => 'CLI репитера'; @override - String get repeater_debugNextCommand => 'Отладка следующей команды'; + String get repeater_debugNextCommand => + 'Отладка следующей команды'; @override - String get repeater_commandHelp => 'Справка по командам'; + String get repeater_commandHelp => 'Справка по командам'; @override - String get repeater_clearHistory => 'Очистить историю'; + String get repeater_clearHistory => 'Очистить историю'; @override - String get repeater_noCommandsSent => 'Команды ещё не отправлялись'; + String get repeater_noCommandsSent => + 'Команды ещё не отправлялись'; @override String get repeater_typeCommandOrUseQuick => - 'Введите команду ниже или используйте быстрые команды'; + 'Введите команду ниже или используйте быстрые команды'; @override - String get repeater_enterCommandHint => 'Введите команду...'; + String get repeater_enterCommandHint => 'Введите команду...'; @override - String get repeater_previousCommand => 'Предыдущая команда'; + String get repeater_previousCommand => 'Предыдущая команда'; @override - String get repeater_nextCommand => 'Следующая команда'; + String get repeater_nextCommand => 'Следующая команда'; @override - String get repeater_enterCommandFirst => 'Сначала введите команду'; + String get repeater_enterCommandFirst => + 'Сначала введите команду'; @override - String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; + String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; @override String repeater_cliCommandError(String error) { - return 'Ошибка: $error'; + return 'Ошибка: $error'; } @override - String get repeater_cliQuickGetName => 'Получить имя'; + String get repeater_cliQuickGetName => 'Получить имя'; @override - String get repeater_cliQuickGetRadio => 'Получить радио'; + String get repeater_cliQuickGetRadio => 'Получить радио'; @override - String get repeater_cliQuickGetTx => 'Получить TX'; + String get repeater_cliQuickGetTx => 'Получить TX'; @override - String get repeater_cliQuickNeighbors => 'Соседи'; + String get repeater_cliQuickNeighbors => 'Соседи'; @override - String get repeater_cliQuickVersion => 'Версия'; + String get repeater_cliQuickVersion => 'Версия'; @override - String get repeater_cliQuickAdvertise => 'Анонсировать'; + String get repeater_cliQuickAdvertise => 'Анонсировать'; @override - String get repeater_cliQuickClock => 'Время'; + String get repeater_cliQuickClock => 'Время'; @override - String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; + String get repeater_cliHelpAdvert => + 'Отправляет пакет анонсирования'; @override String get repeater_cliHelpReboot => - 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; + 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; @override String get repeater_cliHelpClock => - 'Показывает текущее время по часам устройства.'; + 'Показывает текущее время по часам устройства.'; @override String get repeater_cliHelpPassword => - 'Устанавливает новый пароль администратора для устройства.'; + 'Устанавливает новый пароль администратора для устройства.'; @override String get repeater_cliHelpVersion => - 'Показывает версию устройства и дату сборки прошивки.'; + 'Показывает версию устройства и дату сборки прошивки.'; @override String get repeater_cliHelpClearStats => - 'Сбрасывает различные счётчики статистики в ноль.'; + 'Сбрасывает различные счётчики статистики в ноль.'; @override String get repeater_cliHelpSetAf => - 'Устанавливает коэффициент времени в эфире.'; + 'Устанавливает коэффициент времени в эфире.'; @override String get repeater_cliHelpSetTx => - 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; + 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; @override String get repeater_cliHelpSetRepeat => - 'Включает или отключает роль репитера для этой ноды.'; + 'Включает или отключает роль репитера для этой ноды.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; + '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; @override String get repeater_cliHelpSetFloodMax => - 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; + 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; @override String get repeater_cliHelpSetIntThresh => - 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; + 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; + 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetMultiAcks => - 'Включает или отключает функцию «двойных ACK».'; + 'Включает или отключает функцию «двойных ACK».'; @override String get repeater_cliHelpSetAdvertInterval => - 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; + 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; + 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetGuestPassword => - 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; + 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; @override - String get repeater_cliHelpSetName => 'Устанавливает имя в оповещениях.'; + String get repeater_cliHelpSetName => + 'Устанавливает имя в оповещениях.'; @override String get repeater_cliHelpSetLat => - 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; + 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; @override String get repeater_cliHelpSetLon => - 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; + 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; @override String get repeater_cliHelpSetRadio => - 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; + 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; @override String get repeater_cliHelpSetRxDelay => - 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; + 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetTxDelay => - 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; + 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; + 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Включить/выключить мост.'; + String get repeater_cliHelpSetBridgeEnabled => + 'Включить/выключить мост.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Установить задержку перед ретрансляцией пакетов.'; + 'Установить задержку перед ретрансляцией пакетов.'; @override String get repeater_cliHelpSetBridgeSource => - 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; + 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Установить скорость последовательного соединения для мостов RS232.'; + 'Установить скорость последовательного соединения для мостов RS232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Установить секрет моста для мостов ESP-NOW.'; + 'Установить секрет моста для мостов ESP-NOW.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; + 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; @override String get repeater_cliHelpTempRadio => - 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; + 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; @override String get repeater_cliHelpSetPerm => - 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; + 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; @override String get repeater_cliHelpGetBridgeType => - 'Получает тип моста: none, rs232, espnow'; + 'Получает тип моста: none, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Начинает запись пакетов в файловую систему.'; + 'Начинает запись пакетов в файловую систему.'; @override String get repeater_cliHelpLogStop => - 'Останавливает запись пакетов в файловую систему.'; + 'Останавливает запись пакетов в файловую систему.'; @override String get repeater_cliHelpLogErase => - 'Удаляет журналы пакетов из файловой системы.'; + 'Удаляет журналы пакетов из файловой системы.'; @override String get repeater_cliHelpNeighbors => - 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; + 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; @override String get repeater_cliHelpNeighborRemove => - 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; + 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; @override String get repeater_cliHelpRegion => - '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; + '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; @override String get repeater_cliHelpRegionLoad => - 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; + 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; @override String get repeater_cliHelpRegionGet => - 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; + 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; @override String get repeater_cliHelpRegionPut => - 'Добавляет или обновляет определение региона с заданным именем.'; + 'Добавляет или обновляет определение региона с заданным именем.'; @override String get repeater_cliHelpRegionRemove => - 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; + 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; @override String get repeater_cliHelpRegionAllowf => - 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; + 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; @override String get repeater_cliHelpRegionDenyf => - 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; + 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; @override String get repeater_cliHelpRegionHome => - 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; + 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; @override String get repeater_cliHelpRegionHomeSet => - 'Устанавливает «домашний» регион.'; + 'Устанавливает «домашний» регион.'; @override String get repeater_cliHelpRegionSave => - 'Сохраняет список/карту регионов в память.'; + 'Сохраняет список/карту регионов в память.'; @override String get repeater_cliHelpGps => - 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; + 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; @override - String get repeater_cliHelpGpsOnOff => 'Переключает состояние питания GPS.'; + String get repeater_cliHelpGpsOnOff => + 'Переключает состояние питания GPS.'; @override String get repeater_cliHelpGpsSync => - 'Синхронизирует время ноды с часами GPS.'; + 'Синхронизирует время ноды с часами GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; + 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; @override String get repeater_cliHelpGpsAdvert => - 'Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек'; + 'Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек'; @override String get repeater_cliHelpGpsAdvertSet => - 'Устанавливает конфигурацию передачи местоположения.'; + 'Устанавливает конфигурацию передачи местоположения.'; @override - String get repeater_commandsListTitle => 'Список команд'; + String get repeater_commandsListTitle => 'Список команд'; @override String get repeater_commandsListNote => - 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; + 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; @override - String get repeater_general => 'Общие'; + String get repeater_general => 'Общие'; @override - String get repeater_settingsCategory => 'Настройки'; + String get repeater_settingsCategory => 'Настройки'; @override - String get repeater_bridge => 'Мост'; + String get repeater_bridge => 'Мост'; @override - String get repeater_logging => 'Журналирование'; + String get repeater_logging => 'Журналирование'; @override - String get repeater_neighborsRepeaterOnly => 'Соседи (только для репитеров)'; + String get repeater_neighborsRepeaterOnly => + 'Соседи (только для репитеров)'; @override String get repeater_regionManagementRepeaterOnly => - 'Управление регионами (только для репитеров)'; + 'Управление регионами (только для репитеров)'; @override String get repeater_regionNote => - 'Команды регионов введены для управления определениями регионов и правами доступа.'; + 'Команды регионов введены для управления определениями регионов и правами доступа.'; @override - String get repeater_gpsManagement => 'Управление GPS'; + String get repeater_gpsManagement => 'Управление GPS'; @override String get repeater_gpsNote => - 'Команда gps введена для управления параметрами, связанными с местоположением.'; + 'Команда gps введена для управления параметрами, связанными с местоположением.'; @override - String get telemetry_receivedData => 'Полученные телеметрические данные'; + String get telemetry_receivedData => + 'Полученные телеметрические данные'; @override - String get telemetry_requestTimeout => 'Время ожидания телеметрии истекло.'; + String get telemetry_requestTimeout => + 'Время ожидания телеметрии истекло.'; @override String telemetry_errorLoading(String error) { - return 'Ошибка загрузки телеметрии: $error'; + return 'Ошибка загрузки телеметрии: $error'; } @override - String get telemetry_noData => 'Данные телеметрии недоступны.'; + String get telemetry_noData => + 'Данные телеметрии недоступны.'; @override String telemetry_channelTitle(int channel) { - return 'Канал $channel'; + return 'Канал $channel'; } @override - String get telemetry_batteryLabel => 'Батарея'; + String get telemetry_batteryLabel => 'Батарея'; @override - String get telemetry_voltageLabel => 'Напряжение'; + String get telemetry_voltageLabel => 'Напряжение'; @override - String get telemetry_mcuTemperatureLabel => 'Температура МК'; + String get telemetry_mcuTemperatureLabel => 'Температура МК'; @override - String get telemetry_temperatureLabel => 'Температура'; + String get telemetry_temperatureLabel => 'Температура'; @override - String get telemetry_currentLabel => 'Ток'; + String get telemetry_currentLabel => 'Ток'; @override String telemetry_batteryValue(int percent, String volts) { - return '$percent% / $voltsВ'; + return '$percent% / $voltsÐ’'; } @override String telemetry_voltageValue(String volts) { - return '$voltsВ'; + return '$voltsÐ’'; } @override String telemetry_currentValue(String amps) { - return '$ampsА'; + return '$ampsА'; } @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Полученные данные о соседях'; + String get neighbors_receivedData => + 'Полученные данные о соседях'; @override String get neighbors_requestTimedOut => - 'Время ожидания данных о соседях истекло.'; + 'Время ожидания данных о соседях истекло.'; @override String neighbors_errorLoading(String error) { - return 'Ошибка загрузки соседей: $error'; + return 'Ошибка загрузки соседей: $error'; } @override - String get neighbors_repeatersNeighbors => 'Соседи репитеров'; + String get neighbors_repeatersNeighbors => 'Соседи репитеров'; @override - String get neighbors_noData => 'Данные о соседях недоступны.'; + String get neighbors_noData => + 'Данные о соседях недоступны.'; @override String neighbors_unknownContact(String pubkey) { - return 'Неизвестный $pubkey'; + return 'Неизвестный $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Слушал(а): $time назад'; + return 'Слушал(а): $time назад'; } @override - String get channelPath_title => 'Путь пакета'; + String get channelPath_title => 'Путь пакета'; @override - String get channelPath_viewMap => 'Посмотреть на карте'; + String get channelPath_viewMap => 'Посмотреть на карте'; @override - String get channelPath_otherObservedPaths => 'Другие наблюдаемые пути'; + String get channelPath_otherObservedPaths => + 'Другие наблюдаемые пути'; @override - String get channelPath_repeaterHops => 'Хопы через репитеры'; + String get channelPath_repeaterHops => 'Хопы через репитеры'; @override String get channelPath_noHopDetails => - 'Детали хопов для этого пакета не предоставлены.'; + 'Детали хопов для этого пакета не предоставлены.'; @override - String get channelPath_messageDetails => 'Детали сообщения'; + String get channelPath_messageDetails => 'Детали сообщения'; @override - String get channelPath_senderLabel => 'Отправитель'; + String get channelPath_senderLabel => 'Отправитель'; @override - String get channelPath_timeLabel => 'Время'; + String get channelPath_timeLabel => 'Время'; @override - String get channelPath_repeatsLabel => 'Повторы'; + String get channelPath_repeatsLabel => 'Повторы'; @override String channelPath_pathLabel(int index) { - return 'Путь $index'; + return 'Путь $index'; } @override - String get channelPath_observedLabel => 'Наблюдаемый'; + String get channelPath_observedLabel => 'Наблюдаемый'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Наблюдаемый путь $index • $hops'; + return 'Наблюдаемый путь $index • $hops'; } @override - String get channelPath_noLocationData => 'Нет данных о местоположении'; + String get channelPath_noLocationData => + 'Нет данных о местоположении'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2556,343 +2685,361 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Неизвестный'; + String get channelPath_unknownPath => 'Неизвестный'; @override - String get channelPath_floodPath => 'Рассылка'; + String get channelPath_floodPath => 'Рассылка'; @override - String get channelPath_directPath => 'Прямой'; + String get channelPath_directPath => 'Прямой'; @override String channelPath_observedZeroOf(int total) { - return '0 из $total хопов'; + return '0 из $total хопов'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed из $total хопов'; + return '$observed из $total хопов'; } @override - String get channelPath_mapTitle => 'Карта пути'; + String get channelPath_mapTitle => 'Карта пути'; @override String get channelPath_noRepeaterLocations => - 'Нет данных о местоположении репитеров для этого пути.'; + 'Нет данных о местоположении репитеров для этого пути.'; @override String channelPath_primaryPath(int index) { - return 'Путь $index (Основной)'; + return 'Путь $index (Основной)'; } @override - String get channelPath_pathLabelTitle => 'Путь'; + String get channelPath_pathLabelTitle => 'Путь'; @override - String get channelPath_observedPathHeader => 'Наблюдаемый путь'; + String get channelPath_observedPathHeader => + 'Наблюдаемый путь'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Детали хопов для этого пакета недоступны.'; + 'Детали хопов для этого пакета недоступны.'; @override - String get channelPath_unknownRepeater => 'Неизвестный репитер'; + String get channelPath_unknownRepeater => + 'Неизвестный репитер'; @override - String get community_title => 'Сообщество'; + String get community_title => 'Сообщество'; @override - String get community_create => 'Создать сообщество'; + String get community_create => 'Создать сообщество'; @override String get community_createDesc => - 'Создать новое сообщество и поделиться через QR-код.'; + 'Создать новое сообщество и поделиться через QR-код.'; @override - String get community_join => 'Присоединиться'; + String get community_join => 'Присоединиться'; @override - String get community_joinTitle => 'Присоединиться к сообществу'; + String get community_joinTitle => + 'Присоединиться к сообществу'; @override String community_joinConfirmation(String name) { - return 'Вы хотите присоединиться к сообществу \"$name\"?'; + return 'Ð’Ñ‹ хотите присоединиться к сообществу \"$name\"?'; } @override - String get community_scanQr => 'Сканировать QR-код сообщества'; + String get community_scanQr => + 'Сканировать QR-код сообщества'; @override String get community_scanInstructions => - 'Наведите камеру на QR-код сообщества'; + 'Наведите камеру на QR-код сообщества'; @override - String get community_showQr => 'Показать QR-код'; + String get community_showQr => 'Показать QR-код'; @override - String get community_publicChannel => 'Публичный канал сообщества'; + String get community_publicChannel => + 'Публичный канал сообщества'; @override - String get community_hashtagChannel => 'Хэштег-канал сообщества'; + String get community_hashtagChannel => + 'Хэштег-канал сообщества'; @override - String get community_name => 'Имя сообщества'; + String get community_name => 'Имя сообщества'; @override - String get community_enterName => 'Введите имя сообщества'; + String get community_enterName => + 'Введите имя сообщества'; @override String community_created(String name) { - return 'Сообщество \"$name\" создано'; + return 'Сообщество \"$name\" создано'; } @override String community_joined(String name) { - return 'Присоединились к сообществу \"$name\"'; + return 'Присоединились к сообществу \"$name\"'; } @override - String get community_qrTitle => 'Поделиться сообществом'; + String get community_qrTitle => 'Поделиться сообществом'; @override String community_qrInstructions(String name) { - return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; + return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; } @override String get community_hashtagPrivacyHint => - 'Хэштег-каналы сообщества доступны только его участникам'; + 'Хэштег-каналы сообщества доступны только его участникам'; @override - String get community_invalidQrCode => 'Недопустимый QR-код сообщества'; + String get community_invalidQrCode => + 'Недопустимый QR-код сообщества'; @override - String get community_alreadyMember => 'Уже участник'; + String get community_alreadyMember => 'Уже участник'; @override String community_alreadyMemberMessage(String name) { - return 'Вы уже участник сообщества \"$name\".'; + return 'Ð’Ñ‹ уже участник сообщества \"$name\".'; } @override String get community_addPublicChannel => - 'Добавить публичный канал сообщества'; + 'Добавить публичный канал сообщества'; @override String get community_addPublicChannelHint => - 'Автоматически добавить публичный канал для этого сообщества'; + 'Автоматически добавить публичный канал для этого сообщества'; @override String get community_noCommunities => - 'Вы ещё не присоединились ни к одному сообществу'; + 'Ð’Ñ‹ ещё не присоединились ни к одному сообществу'; @override String get community_scanOrCreate => - 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; + 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; @override - String get community_manageCommunities => 'Управление сообществами'; + String get community_manageCommunities => + 'Управление сообществами'; @override - String get community_delete => 'Покинуть сообщество'; + String get community_delete => 'Покинуть сообщество'; @override String community_deleteConfirm(String name) { - return 'Покинуть \"$name\"?'; + return 'Покинуть \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Это также удалит $count канал(ов) и их сообщения.'; + return 'Это также удалит $count канал(ов) и их сообщения.'; } @override String community_deleted(String name) { - return 'Покинули сообщество \"$name\"'; + return 'Покинули сообщество \"$name\"'; } @override - String get community_regenerateSecret => 'Пересоздать секрет'; + String get community_regenerateSecret => + 'Пересоздать секрет'; @override String community_regenerateSecretConfirm(String name) { - return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; + return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; } @override - String get community_regenerate => 'Пересоздать'; + String get community_regenerate => 'Пересоздать'; @override String community_secretRegenerated(String name) { - return 'Секрет пересоздан для \"$name\"'; + return 'Секрет пересоздан для \"$name\"'; } @override - String get community_updateSecret => 'Обновить секрет'; + String get community_updateSecret => 'Обновить секрет'; @override String community_secretUpdated(String name) { - return 'Секрет обновлён для \"$name\"'; + return 'Секрет обновлён для \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; + return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; } @override - String get community_addHashtagChannel => 'Добавить хэштег-канал сообщества'; + String get community_addHashtagChannel => + 'Добавить хэштег-канал сообщества'; @override String get community_addHashtagChannelDesc => - 'Добавить хэштег-канал для этого сообщества'; + 'Добавить хэштег-канал для этого сообщества'; @override - String get community_selectCommunity => 'Выбрать сообщество'; + String get community_selectCommunity => 'Выбрать сообщество'; @override - String get community_regularHashtag => 'Обычный хэштег'; + String get community_regularHashtag => 'Обычный хэштег'; @override String get community_regularHashtagDesc => - 'Публичный хэштег (любой может присоединиться)'; + 'Публичный хэштег (любой может присоединиться)'; @override - String get community_communityHashtag => 'Хэштег сообщества'; + String get community_communityHashtag => 'Хэштег сообщества'; @override String get community_communityHashtagDesc => - 'Доступен только участникам сообщества'; + 'Доступен только участникам сообщества'; @override String community_forCommunity(String name) { - return 'Для $name'; + return 'Для $name'; } @override - String get listFilter_tooltip => 'Фильтр и сортировка'; + String get listFilter_tooltip => 'Фильтр и сортировка'; @override - String get listFilter_sortBy => 'Сортировка по'; + String get listFilter_sortBy => 'Сортировка по'; @override - String get listFilter_latestMessages => 'Последние сообщения'; + String get listFilter_latestMessages => + 'Последние сообщения'; @override - String get listFilter_heardRecently => 'Слышали недавно'; + String get listFilter_heardRecently => 'Слышали недавно'; @override - String get listFilter_az => 'По алфавиту'; + String get listFilter_az => 'По алфавиту'; @override - String get listFilter_filters => 'Фильтры'; + String get listFilter_filters => 'Фильтры'; @override - String get listFilter_all => 'Все'; + String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Избранное'; + String get listFilter_favorites => 'Избранное'; @override - String get listFilter_addToFavorites => 'Добавить в избранное'; + String get listFilter_addToFavorites => + 'Добавить в избранное'; @override - String get listFilter_removeFromFavorites => 'Удалить из избранного'; + String get listFilter_removeFromFavorites => + 'Удалить из избранного'; @override - String get listFilter_users => 'Пользователи'; + String get listFilter_users => 'Пользователи'; @override - String get listFilter_repeaters => 'Репитеры'; + String get listFilter_repeaters => 'Репитеры'; @override - String get listFilter_roomServers => 'Серверы комнат'; + String get listFilter_roomServers => 'Серверы комнат'; @override - String get listFilter_unreadOnly => 'Только непрочитанные'; + String get listFilter_unreadOnly => 'Только непрочитанные'; @override - String get listFilter_newGroup => 'Новая группа'; + String get listFilter_newGroup => 'Новая группа'; @override - String get pathTrace_you => 'Вы'; + String get pathTrace_you => 'Ð’Ñ‹'; @override - String get pathTrace_failed => 'Путь трассировки не выполнен.'; + String get pathTrace_failed => + 'Путь трассировки не выполнен.'; @override - String get pathTrace_notAvailable => 'Трассировка пути недоступна.'; + String get pathTrace_notAvailable => + 'Трассировка пути недоступна.'; @override - String get pathTrace_refreshTooltip => 'Обновить Path Trace'; + String get pathTrace_refreshTooltip => 'Обновить Path Trace'; @override String get pathTrace_someHopsNoLocation => - 'Одному или нескольким хмелям не указано местоположение!'; + 'Одному или нескольким хмелям не указано местоположение!'; @override - String get pathTrace_clearTooltip => 'Очистить путь'; + String get pathTrace_clearTooltip => 'Очистить путь'; @override - String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.'; + String get losSelectStartEnd => + 'Выберите начальный и конечный узлы для LOS.'; @override String losRunFailed(String error) { - return 'Проверка прямой видимости не удалась: $error'; + return 'Проверка прямой видимости не удалась: $error'; } @override - String get losClearAllPoints => 'Очистить все точки'; + String get losClearAllPoints => 'Очистить все точки'; @override String get losRunToViewElevationProfile => - 'Запустите LOS, чтобы просмотреть профиль высот.'; + 'Запустите LOS, чтобы просмотреть профиль высот.'; @override - String get losMenuTitle => 'ЛОС Меню'; + String get losMenuTitle => 'ЛОС Меню'; @override String get losMenuSubtitle => - 'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.'; + 'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.'; @override - String get losShowDisplayNodes => 'Показать узлы отображения'; + String get losShowDisplayNodes => + 'Показать узлы отображения'; @override - String get losCustomPoints => 'Пользовательские точки'; + String get losCustomPoints => 'Пользовательские точки'; @override String losCustomPointLabel(int index) { - return 'Пользовательский $index'; + return 'Пользовательский $index'; } @override - String get losPointA => 'Точка А'; + String get losPointA => 'Точка А'; @override - String get losPointB => 'Точка Б'; + String get losPointB => 'Точка Б'; @override String losAntennaA(String value, String unit) { - return 'Антенна А: $value $unit'; + return 'Антенна А: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Антенна Б: $value $unit'; + return 'Антенна Б: $value $unit'; } @override - String get losRun => 'Запустить ЛОС'; + String get losRun => 'Запустить ЛОС'; @override - String get losNoElevationData => 'Нет данных о высоте'; + String get losNoElevationData => 'Нет данных о высоте'; @override String losProfileClear( @@ -2901,7 +3048,7 @@ class AppLocalizationsRu extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit'; + return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit'; } @override @@ -2911,61 +3058,64 @@ class AppLocalizationsRu extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, заблокирован $obstruction $heightUnit'; + return '$distance $distanceUnit, заблокирован $obstruction $heightUnit'; } @override - String get losStatusChecking => 'ЛОС: проверяю...'; + String get losStatusChecking => 'ЛОС: проверяю...'; @override - String get losStatusNoData => 'ЛОС: нет данных'; + String get losStatusNoData => 'ЛОС: нет данных'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.'; + return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.'; } @override String get losErrorElevationUnavailable => - 'Данные о высоте недоступны для одного или нескольких образцов.'; + 'Данные о высоте недоступны для одного или нескольких образцов.'; @override String get losErrorInvalidInput => - 'Неверные данные о точках/высоте для расчета LOS.'; + 'Неверные данные о точках/высоте для расчета LOS.'; @override - String get losRenameCustomPoint => 'Переименовать пользовательскую точку'; + String get losRenameCustomPoint => + 'Переименовать пользовательскую точку'; @override - String get losPointName => 'Имя точки'; + String get losPointName => 'Имя точки'; @override - String get losShowPanelTooltip => 'Показать панель LOS'; + String get losShowPanelTooltip => 'Показать панель LOS'; @override - String get losHidePanelTooltip => 'Скрыть панель LOS'; + String get losHidePanelTooltip => 'Скрыть панель LOS'; @override String get losElevationAttribution => - 'Данные о высоте: Open-Meteo (CC BY 4.0)'; + 'Данные о высоте: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Радиогоризонт'; + String get losLegendRadioHorizon => 'Радиогоризонт'; @override - String get losLegendLosBeam => 'Линия прямой видимости'; + String get losLegendLosBeam => 'Линия прямой видимости'; @override - String get losLegendTerrain => 'Рельеф'; + String get losLegendTerrain => 'Рельеф'; @override - String get losFrequencyLabel => 'Частота'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта'; + String get losFrequencyInfoTooltip => + 'Просмотреть детали расчёта'; @override - String get losFrequencyDialogTitle => 'Расчёт радиогоризонта'; + String get losFrequencyDialogTitle => + 'Расчёт радиогоризонта'; @override String losFrequencyDialogDescription( @@ -2974,97 +3124,105 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; + return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; } @override - String get contacts_pathTrace => 'Трассировка пути'; + String get contacts_pathTrace => 'Трассировка пути'; @override - String get contacts_ping => 'Пинговать'; + String get contacts_ping => 'Пинговать'; @override - String get contacts_repeaterPathTrace => 'Отследить путь к ретранслятору'; + String get contacts_repeaterPathTrace => + 'Отследить путь к ретранслятору'; @override - String get contacts_repeaterPing => 'Пинговать повторитель'; + String get contacts_repeaterPing => + 'Пинговать повторитель'; @override - String get contacts_roomPathTrace => 'Трассировка пути к серверу комнаты'; + String get contacts_roomPathTrace => + 'Трассировка пути к серверу комнаты'; @override - String get contacts_roomPing => 'Пинговать сервер комнаты'; + String get contacts_roomPing => + 'Пинговать сервер комнаты'; @override - String get contacts_chatTraceRoute => 'Трассировка маршрута'; + String get contacts_chatTraceRoute => + 'Трассировка маршрута'; @override String contacts_pathTraceTo(String name) { - return 'Показать маршрут к $name'; + return 'Показать маршрут к $name'; } @override - String get contacts_clipboardEmpty => 'Буфер обмена пуст.'; + String get contacts_clipboardEmpty => 'Буфер обмена пуст.'; @override String get contacts_invalidAdvertFormat => - 'Недействительные контактные данные'; + 'Недействительные контактные данные'; @override - String get contacts_contactImported => 'Контакт был импортирован'; + String get contacts_contactImported => + 'Контакт был импортирован'; @override - String get contacts_contactImportFailed => 'Контакт не удалось импортировать'; + String get contacts_contactImportFailed => + 'Контакт не удалось импортировать'; @override - String get contacts_zeroHopAdvert => 'Реклама Zero Hop'; + String get contacts_zeroHopAdvert => 'Реклама Zero Hop'; @override - String get contacts_floodAdvert => 'Рекламный поток'; + String get contacts_floodAdvert => 'Рекламный поток'; @override String get contacts_copyAdvertToClipboard => - 'Копировать рекламу в буфер обмена'; + 'Копировать рекламу в буфер обмена'; @override String get contacts_addContactFromClipboard => - 'Добавить контакт из буфера обмена'; + 'Добавить контакт из буфера обмена'; @override - String get contacts_ShareContact => 'Копировать контакт в буфер обмена'; + String get contacts_ShareContact => + 'Копировать контакт в буфер обмена'; @override String get contacts_ShareContactZeroHop => - 'Поделиться контактом по объявлению'; + 'Поделиться контактом по объявлению'; @override String get contacts_zeroHopContactAdvertSent => - 'Отправлено сообщение по объявлению.'; + 'Отправлено сообщение по объявлению.'; @override String get contacts_zeroHopContactAdvertFailed => - 'Не удалось отправить контакт.'; + 'Не удалось отправить контакт.'; @override String get contacts_contactAdvertCopied => - 'Реклама скопирована в буфер обмена.'; + 'Реклама скопирована в буфер обмена.'; @override String get contacts_contactAdvertCopyFailed => - 'Копирование рекламы в буфер обмена не удалось.'; + 'Копирование рекламы в буфер обмена не удалось.'; @override - String get notification_activityTitle => 'Активность MeshCore'; + String get notification_activityTitle => 'Активность MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'сообщений', - many: 'сообщений', - few: 'сообщения', - one: 'сообщение', + other: 'сообщений', + many: 'сообщений', + few: 'сообщения', + one: 'сообщение', ); return '$count $_temp0'; } @@ -3074,10 +3232,10 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'сообщений канала', - many: 'сообщений канала', - few: 'сообщения канала', - one: 'сообщение канала', + other: 'сообщений канала', + many: 'сообщений канала', + few: 'сообщения канала', + one: 'сообщение канала', ); return '$count $_temp0'; } @@ -3087,78 +3245,87 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'новых узлов', - many: 'новых узлов', - few: 'новых узла', - one: 'новый узел', + other: 'новых узлов', + many: 'новых узлов', + few: 'новых узла', + one: 'новый узел', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Обнаружен новый $contactType'; + return 'Обнаружен новый $contactType'; } @override - String get notification_receivedNewMessage => 'Получено новое сообщение'; + String get notification_receivedNewMessage => + 'Получено новое сообщение'; @override String get settings_gpxExportRepeaters => - 'Экспортировать рипитеры / сервер комнаты в GPX'; + 'Экспортировать рипитеры / сервер комнаты в GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.'; + 'Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.'; @override - String get settings_gpxExportContacts => 'Экспортировать спутников в GPX'; + String get settings_gpxExportContacts => + 'Экспортировать спутников в GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Экспортирует спутников с местоположением в файл GPX.'; + 'Экспортирует спутников с местоположением в файл GPX.'; @override - String get settings_gpxExportAll => 'Экспортировать все контакты в GPX'; + String get settings_gpxExportAll => + 'Экспортировать все контакты в GPX'; @override String get settings_gpxExportAllSubtitle => - 'Экспортирует все контакты с местоположением в файл GPX.'; + 'Экспортирует все контакты с местоположением в файл GPX.'; @override - String get settings_gpxExportSuccess => 'Успешно экспортирован файл GPX.'; + String get settings_gpxExportSuccess => + 'Успешно экспортирован файл GPX.'; @override - String get settings_gpxExportNoContacts => 'Нет контактов для экспорта.'; + String get settings_gpxExportNoContacts => + 'Нет контактов для экспорта.'; @override String get settings_gpxExportNotAvailable => - 'Не поддерживается на вашем устройстве/ОС'; + 'Не поддерживается на вашем устройстве/ОС'; @override - String get settings_gpxExportError => 'Произошла ошибка при экспорте.'; + String get settings_gpxExportError => + 'Произошла ошибка при экспорте.'; @override String get settings_gpxExportRepeatersRoom => - 'Местоположения повторителей и серверов комнат'; + 'Местоположения повторителей и серверов комнат'; @override - String get settings_gpxExportChat => 'Местоположения спутников'; + String get settings_gpxExportChat => + 'Местоположения спутников'; @override - String get settings_gpxExportAllContacts => 'Все местоположения контактов'; + String get settings_gpxExportAllContacts => + 'Все местоположения контактов'; @override String get settings_gpxExportShareText => - 'Данные карты экспортированы из meshcore-open'; + 'Данные карты экспортированы из meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open экспорт данных карты GPX'; + 'meshcore-open экспорт данных карты GPX'; @override - String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы'; + String get snrIndicator_nearByRepeaters => + 'Ближайшие ретрансляторы'; @override - String get snrIndicator_lastSeen => 'Последний раз видели'; + String get snrIndicator_lastSeen => 'Последний раз видели'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index a120a7d..713c860 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -15,85 +15,85 @@ class AppLocalizationsSk extends AppLocalizations { String get nav_contacts => 'Kontakty'; @override - String get nav_channels => 'Kanály'; + String get nav_channels => 'Kanály'; @override String get nav_map => 'Mapa'; @override - String get common_cancel => 'Zrušiť'; + String get common_cancel => 'ZruÅ¡iÅ¥'; @override String get common_ok => 'OK\nDobre'; @override - String get common_connect => 'Pripojiť'; + String get common_connect => 'PripojiÅ¥'; @override - String get common_unknownDevice => 'Neznáme zariadenie'; + String get common_unknownDevice => 'Neznáme zariadenie'; @override - String get common_save => 'Uložiť'; + String get common_save => 'UložiÅ¥'; @override - String get common_delete => 'Odstrániť'; + String get common_delete => 'OdstrániÅ¥'; @override - String get common_close => 'Zavrieť'; + String get common_close => 'ZavrieÅ¥'; @override - String get common_edit => 'Upraviť'; + String get common_edit => 'UpraviÅ¥'; @override - String get common_add => 'Pridať'; + String get common_add => 'PridaÅ¥'; @override String get common_settings => 'Nastavenia'; @override - String get common_disconnect => 'Odpojiť'; + String get common_disconnect => 'OdpojiÅ¥'; @override - String get common_connected => 'Pripojené'; + String get common_connected => 'Pripojené'; @override - String get common_disconnected => 'Odpojené'; + String get common_disconnected => 'Odpojené'; @override - String get common_create => 'Vytvoriť'; + String get common_create => 'VytvoriÅ¥'; @override - String get common_continue => 'Pokračovať'; + String get common_continue => 'PokračovaÅ¥'; @override - String get common_share => 'Zdieľať'; + String get common_share => 'ZdieľaÅ¥'; @override - String get common_copy => 'Kopírovať'; + String get common_copy => 'KopírovaÅ¥'; @override - String get common_retry => 'Pokusť znova'; + String get common_retry => 'PokusÅ¥ znova'; @override - String get common_hide => 'Skryť'; + String get common_hide => 'SkryÅ¥'; @override - String get common_remove => 'Odstrániť'; + String get common_remove => 'OdstrániÅ¥'; @override String get common_enable => 'Povolit'; @override - String get common_disable => 'Zakázať'; + String get common_disable => 'ZakázaÅ¥'; @override - String get common_reboot => 'Restartovať'; + String get common_reboot => 'RestartovaÅ¥'; @override - String get common_loading => 'Načítavanie...'; + String get common_loading => 'Načítavanie...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Vyberte si metódu prepojenia.'; - - @override - String get connectionChoiceSubtitle => - 'Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -126,21 +119,21 @@ class AppLocalizationsSk extends AppLocalizations { @override String get usbScreenSubtitle => - 'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.'; + 'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vaÅ¡ej MeshCore uzlu.'; @override String get usbScreenStatus => 'Vyberte USB zariadenie'; @override String get usbScreenNote => - 'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.'; + 'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.'; @override String get usbScreenEmptyState => - 'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.'; + 'NenaÅ¡li sa žiadne USB zariadenia. Pripojte jedno a obnovte.'; @override - String get scanner_scanning => 'Skrívania zariadení...'; + String get scanner_scanning => 'Skrívania zariadení...'; @override String get scanner_connecting => 'Pripojujem sa...'; @@ -149,19 +142,19 @@ class AppLocalizationsSk extends AppLocalizations { String get scanner_disconnecting => 'Odpojuje sa...'; @override - String get scanner_notConnected => 'Nezriadené'; + String get scanner_notConnected => 'Nezriadené'; @override String scanner_connectedTo(String deviceName) { - return 'Pripojené k $deviceName'; + return 'Pripojené k $deviceName'; } @override - String get scanner_searchingDevices => 'Hľadám zariadenia MeshCore...'; + String get scanner_searchingDevices => 'Hľadám zariadenia MeshCore...'; @override String get scanner_tapToScan => - 'Stlač skenovanie na nájdenie zariadení MeshCore.'; + 'Stlač skenovanie na nájdenie zariadení MeshCore.'; @override String scanner_connectionFailed(String error) { @@ -172,27 +165,27 @@ class AppLocalizationsSk extends AppLocalizations { String get scanner_stop => 'Zastavte'; @override - String get scanner_scan => 'Skončiť'; + String get scanner_scan => 'SkončiÅ¥'; @override - String get scanner_bluetoothOff => 'Bluetooth je vypnutý'; + String get scanner_bluetoothOff => 'Bluetooth je vypnutý'; @override String get scanner_bluetoothOffMessage => - 'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.'; + 'Prosím, zapnite Bluetooth, aby ste mohli skenovaÅ¥ pre zariadenia.'; @override - String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome'; + String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome'; @override String get scanner_chromeRequiredMessage => - 'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.'; + 'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.'; @override String get scanner_enableBluetooth => 'Povolte Bluetooth'; @override - String get device_quickSwitch => 'Rýchle prepínač'; + String get device_quickSwitch => 'Rýchle prepínač'; @override String get device_meshcore => 'MeshCore'; @@ -201,123 +194,125 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_title => 'Nastavenia'; @override - String get settings_deviceInfo => 'Informácie o zariadení'; + String get settings_deviceInfo => 'Informácie o zariadení'; @override - String get settings_appSettings => 'Nastavenia aplikácie'; + String get settings_appSettings => 'Nastavenia aplikácie'; @override String get settings_appSettingsSubtitle => - 'Upozornenia, správy a nastavenia mapy'; + 'Upozornenia, správy a nastavenia mapy'; @override String get settings_nodeSettings => 'Nastavenia uzla'; @override - String get settings_nodeName => 'Názov uzla'; + String get settings_nodeName => 'Názov uzla'; @override - String get settings_nodeNameNotSet => 'Nezriadené'; + String get settings_nodeNameNotSet => 'Nezriadené'; @override - String get settings_nodeNameHint => 'Zadajte názov uzla'; + String get settings_nodeNameHint => 'Zadajte názov uzla'; @override - String get settings_nodeNameUpdated => 'Meno aktualizované'; + String get settings_nodeNameUpdated => 'Meno aktualizované'; @override - String get settings_radioSettings => 'Nastavenia rádia'; + String get settings_radioSettings => 'Nastavenia rádia'; @override String get settings_radioSettingsSubtitle => - 'Frekvencia, výkon, rozptylovací faktor'; + 'Frekvencia, výkon, rozptylovací faktor'; @override - String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované'; + String get settings_radioSettingsUpdated => + 'Nastavenia rádia aktualizované'; @override String get settings_location => 'Lokalita'; @override - String get settings_locationSubtitle => 'GPS súradnice'; + String get settings_locationSubtitle => 'GPS súradnice'; @override - String get settings_locationUpdated => 'Lokalita aktualizovaná'; + String get settings_locationUpdated => 'Lokalita aktualizovaná'; @override String get settings_locationBothRequired => - 'Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.'; + 'Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.'; @override - String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.'; + String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.'; @override - String get settings_locationGPSEnable => 'Aktivovať GPS'; + String get settings_locationGPSEnable => 'AktivovaÅ¥ GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Povolí automatické aktualizovanie polohy pomocou GPS.'; + 'Povolí automatické aktualizovanie polohy pomocou GPS.'; @override String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)'; @override String get settings_locationIntervalInvalid => - 'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.'; + 'Interval musí byÅ¥ aspoň 60 sekúnd a menej ako 86400 sekúnd.'; @override - String get settings_latitude => 'Súradnica'; + String get settings_latitude => 'Súradnica'; @override - String get settings_longitude => 'Dĺžka'; + String get settings_longitude => 'Dĺžka'; @override - String get settings_privacyMode => 'Režim ochrany súkromia'; + String get settings_privacyMode => 'Režim ochrany súkromia'; @override - String get settings_privacyModeSubtitle => 'Skryť meno/poloha v reklamách'; + String get settings_privacyModeSubtitle => 'SkryÅ¥ meno/poloha v reklamách'; @override String get settings_privacyModeToggle => - 'Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.'; + 'Prepínač súkromného režimu skryje vaÅ¡e meno a polohu v reklamách.'; @override - String get settings_privacyModeEnabled => 'Ochranný režim je povolený.'; + String get settings_privacyModeEnabled => 'Ochranný režim je povolený.'; @override - String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý'; + String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý'; @override - String get settings_actions => 'Možné akcie'; + String get settings_actions => 'Možné akcie'; @override - String get settings_sendAdvertisement => 'Odoslať reklamu'; + String get settings_sendAdvertisement => 'OdoslaÅ¥ reklamu'; @override - String get settings_sendAdvertisementSubtitle => 'Momentálne priezornejšie.'; + String get settings_sendAdvertisementSubtitle => + 'Momentálne priezornejÅ¡ie.'; @override - String get settings_advertisementSent => 'Reklama odeslaná'; + String get settings_advertisementSent => 'Reklama odeslaná'; @override - String get settings_syncTime => 'Čas synchronizácie'; + String get settings_syncTime => 'ÄŒas synchronizácie'; @override String get settings_syncTimeSubtitle => - 'Nastaviť hodiny zariadenia na čas telefónu'; + 'NastaviÅ¥ hodiny zariadenia na čas telefónu'; @override - String get settings_timeSynchronized => 'Čas synchronizovaný'; + String get settings_timeSynchronized => 'ÄŒas synchronizovaný'; @override - String get settings_refreshContacts => 'Načítať Kontakty'; + String get settings_refreshContacts => 'NačítaÅ¥ Kontakty'; @override String get settings_refreshContactsSubtitle => - 'Načítať zoznam kontaktov z zariadenia'; + 'NačítaÅ¥ zoznam kontaktov z zariadenia'; @override - String get settings_rebootDevice => 'Restartovať zariadenie'; + String get settings_rebootDevice => 'RestartovaÅ¥ zariadenie'; @override String get settings_rebootDeviceSubtitle => @@ -325,7 +320,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - 'Ste si istý, že chcete zariadenie reštartovať? Budete odpojení.'; + 'Ste si istý, že chcete zariadenie reÅ¡tartovaÅ¥? Budete odpojení.'; @override String get settings_debug => 'Ladenie'; @@ -335,16 +330,16 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_bleDebugLogSubtitle => - 'Príkazy BLE, odpovede a surové dáta'; + 'Príkazy BLE, odpovede a surové dáta'; @override - String get settings_appDebugLog => 'Záznam ladenia aplikácie'; + String get settings_appDebugLog => 'Záznam ladenia aplikácie'; @override - String get settings_appDebugLogSubtitle => 'Správy z ladenia aplikácie'; + String get settings_appDebugLogSubtitle => 'Správy z ladenia aplikácie'; @override - String get settings_about => 'O nás'; + String get settings_about => 'O nás'; @override String settings_aboutVersion(String version) { @@ -356,11 +351,11 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_aboutDescription => - 'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.'; + 'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieÅ¥ové zariadenia.'; @override String get settings_aboutOpenMeteoAttribution => - 'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)'; + 'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Meno'; @@ -372,16 +367,16 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_infoStatus => 'Status'; @override - String get settings_infoBattery => 'Batéria'; + String get settings_infoBattery => 'Batéria'; @override - String get settings_infoPublicKey => 'Verejný kľúč'; + String get settings_infoPublicKey => 'Verejný kľúč'; @override - String get settings_infoContactsCount => 'Počet kontaktov'; + String get settings_infoContactsCount => 'Počet kontaktov'; @override - String get settings_infoChannelCount => 'Počet kanálov'; + String get settings_infoChannelCount => 'Počet kanálov'; @override String get settings_presets => 'Prednastavenia'; @@ -390,39 +385,41 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_frequency => 'Frekvencia (MHz)'; @override - String get settings_frequencyHelper => '300,0 – 2500,0'; + String get settings_frequencyHelper => '300,0 – 2500,0'; @override - String get settings_frequencyInvalid => 'Neplatná frekvencia (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Neplatná frekvencia (300-2500 MHz)'; @override - String get settings_bandwidth => 'Šírka pásma'; + String get settings_bandwidth => 'Šírka pásma'; @override - String get settings_spreadingFactor => 'Rozptýľovací faktor'; + String get settings_spreadingFactor => 'Rozptýľovací faktor'; @override - String get settings_codingRate => 'Cenový kurz pre programovanie'; + String get settings_codingRate => 'Cenový kurz pre programovanie'; @override - String get settings_txPower => 'TX Výkon (dBm)'; + String get settings_txPower => 'TX Výkon (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)'; + String get settings_txPowerInvalid => + 'Neplatná hodnota výkonu TX (0-22 dBm)'; @override - String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete'; + String get settings_clientRepeat => + 'Opätovné použitie bez elektrickej siete'; @override String get settings_clientRepeatSubtitle => - 'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.'; + 'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.'; @override String get settings_clientRepeatFreqWarning => - 'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.'; + 'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.'; @override String settings_error(String message) { @@ -430,37 +427,37 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get appSettings_title => 'Nastavenia aplikácie'; + String get appSettings_title => 'Nastavenia aplikácie'; @override - String get appSettings_appearance => 'Vzhľad'; + String get appSettings_appearance => 'Vzhľad'; @override - String get appSettings_theme => 'Téma'; + String get appSettings_theme => 'Téma'; @override - String get appSettings_themeSystem => 'Predvolený systém'; + String get appSettings_themeSystem => 'Predvolený systém'; @override String get appSettings_themeLight => 'Svetlo'; @override - String get appSettings_themeDark => 'Tmavé'; + String get appSettings_themeDark => 'Tmavé'; @override String get appSettings_language => 'Jazyk'; @override - String get appSettings_languageSystem => 'Predvolený systém'; + String get appSettings_languageSystem => 'Predvolený systém'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -469,16 +466,16 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -487,103 +484,104 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Ruština'; + String get appSettings_languageRu => 'RuÅ¡tina'; @override - String get appSettings_languageUk => 'Ukrajinská'; + String get appSettings_languageUk => 'Ukrajinská'; @override - String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ'; + String get appSettings_enableMessageTracing => 'PovoliÅ¥ sledovanie správ'; @override String get appSettings_enableMessageTracingSubtitle => - 'Zobraziť podrobné metadáta o smerovaní a časovaní správ'; + 'ZobraziÅ¥ podrobné metadáta o smerovaní a časovaní správ'; @override String get appSettings_notifications => 'Upozornenia'; @override - String get appSettings_enableNotifications => 'Povolte Notifikácie'; + String get appSettings_enableNotifications => 'Povolte Notifikácie'; @override String get appSettings_enableNotificationsSubtitle => - 'Zísť o upozornenia na správy a inzeráty'; + 'ZísÅ¥ o upozornenia na správy a inzeráty'; @override String get appSettings_notificationPermissionDenied => - 'Odmietená povolenie notifikácií'; + 'Odmietená povolenie notifikácií'; @override - String get appSettings_notificationsEnabled => 'Upozornenia povolené'; + String get appSettings_notificationsEnabled => 'Upozornenia povolené'; @override - String get appSettings_notificationsDisabled => 'Upozornenia sú vypnuté'; + String get appSettings_notificationsDisabled => 'Upozornenia sú vypnuté'; @override - String get appSettings_messageNotifications => 'Správy od upozornení'; + String get appSettings_messageNotifications => 'Správy od upozornení'; @override String get appSettings_messageNotificationsSubtitle => - 'Zobraziť upozornenie pri prijímaní nových správ'; + 'ZobraziÅ¥ upozornenie pri prijímaní nových správ'; @override - String get appSettings_channelMessageNotifications => 'Notifikácie z kanálov'; + String get appSettings_channelMessageNotifications => + 'Notifikácie z kanálov'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Zobraziť upozornenie pri prijímaní správ z kanálu'; + 'ZobraziÅ¥ upozornenie pri prijímaní správ z kanálu'; @override String get appSettings_advertisementNotifications => 'Upozornenia na reklamy'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Zobraziť upozornenie, keď sa objavia nové uzly.'; + 'ZobraziÅ¥ upozornenie, keď sa objavia nové uzly.'; @override - String get appSettings_messaging => 'Správy'; + String get appSettings_messaging => 'Správy'; @override - String get appSettings_clearPathOnMaxRetry => 'Vyčisti cestu na Max Retry'; + String get appSettings_clearPathOnMaxRetry => 'Vyčisti cestu na Max Retry'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Resetovať kontaktný priebeh po 5 neúspešných pokusoch o doručenie'; + 'ResetovaÅ¥ kontaktný priebeh po 5 neúspeÅ¡ných pokusoch o doručenie'; @override String get appSettings_pathsWillBeCleared => - 'Cesty budú vymazané po 5 neúspešných pokusoch.'; + 'Cesty budú vymazané po 5 neúspeÅ¡ných pokusoch.'; @override String get appSettings_pathsWillNotBeCleared => - 'Cesty sa automaticky nevymazávajú.'; + 'Cesty sa automaticky nevymazávajú.'; @override - String get appSettings_autoRouteRotation => 'Automatické prechodové trasy'; + String get appSettings_autoRouteRotation => 'Automatické prechodové trasy'; @override String get appSettings_autoRouteRotationSubtitle => - 'Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.'; + 'Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.'; @override String get appSettings_autoRouteRotationEnabled => - 'Automatické otáčanie trasy povolené'; + 'Automatické otáčanie trasy povolené'; @override String get appSettings_autoRouteRotationDisabled => - 'Automatické prekladanie trás pozastavené'; + 'Automatické prekladanie trás pozastavené'; @override - String get appSettings_battery => 'Batéria'; + String get appSettings_battery => 'Batéria'; @override - String get appSettings_batteryChemistry => 'Chemická zloženie batérie'; + String get appSettings_batteryChemistry => 'Chemická zloženie batérie'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { @@ -592,13 +590,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_batteryChemistryConnectFirst => - 'Pripojte sa k zariadeniu na výber'; + 'Pripojte sa k zariadeniu na výber'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @@ -607,408 +605,410 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_mapDisplay => 'Zobrazenie mapy'; @override - String get appSettings_showRepeaters => 'Zobraziť opakovače'; + String get appSettings_showRepeaters => 'ZobraziÅ¥ opakovače'; @override String get appSettings_showRepeatersSubtitle => - 'Zobraziť opakujúce sa uzly na mape'; + 'ZobraziÅ¥ opakujúce sa uzly na mape'; @override - String get appSettings_showChatNodes => 'Zobraziť uzly chatových správ'; + String get appSettings_showChatNodes => 'ZobraziÅ¥ uzly chatových správ'; @override String get appSettings_showChatNodesSubtitle => - 'Zobraziť chatové uzly na mape'; + 'ZobraziÅ¥ chatové uzly na mape'; @override - String get appSettings_showOtherNodes => 'Zobraziť ďalšie uzly'; + String get appSettings_showOtherNodes => 'ZobraziÅ¥ ďalÅ¡ie uzly'; @override String get appSettings_showOtherNodesSubtitle => - 'Zobraziť ostatné typy uzlov na mape'; + 'ZobraziÅ¥ ostatné typy uzlov na mape'; @override - String get appSettings_timeFilter => 'Filtrovacie Časové Obdoby'; + String get appSettings_timeFilter => 'Filtrovacie ÄŒasové Obdoby'; @override - String get appSettings_timeFilterShowAll => 'Zobraziť všetky uzly'; + String get appSettings_timeFilterShowAll => 'ZobraziÅ¥ vÅ¡etky uzly'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Zobraziť uzly z posledných $hours hodín'; + return 'ZobraziÅ¥ uzly z posledných $hours hodín'; } @override - String get appSettings_mapTimeFilter => 'Filtračný čas mapy'; + String get appSettings_mapTimeFilter => 'Filtračný čas mapy'; @override String get appSettings_showNodesDiscoveredWithin => - 'Zobraziť uzly objavené v:'; + 'ZobraziÅ¥ uzly objavené v:'; @override - String get appSettings_allTime => 'Všetky časy'; + String get appSettings_allTime => 'VÅ¡etky časy'; @override - String get appSettings_lastHour => 'Posledná hodina'; + String get appSettings_lastHour => 'Posledná hodina'; @override - String get appSettings_last6Hours => 'Posledné 6 hodín'; + String get appSettings_last6Hours => 'Posledné 6 hodín'; @override - String get appSettings_last24Hours => 'Posledných 24 hodín'; + String get appSettings_last24Hours => 'Posledných 24 hodín'; @override - String get appSettings_lastWeek => 'Minul týždeň'; + String get appSettings_lastWeek => 'Minul týždeň'; @override - String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; + String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; @override String get appSettings_unitsTitle => 'Jednotky'; @override - String get appSettings_unitsMetric => 'Metrické (m / km)'; + String get appSettings_unitsMetric => 'Metrické (m / km)'; @override - String get appSettings_unitsImperial => 'Imperiálne (ft / mi)'; + String get appSettings_unitsImperial => 'Imperiálne (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť'; + String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasÅ¥'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Vyberená oblasť (zoom $minZoom-$maxZoom)'; + return 'Vyberená oblasÅ¥ (zoom $minZoom-$maxZoom)'; } @override String get appSettings_debugCard => 'Ladenie'; @override - String get appSettings_appDebugLogging => 'Záznamy ladenia aplikácie'; + String get appSettings_appDebugLogging => 'Záznamy ladenia aplikácie'; @override String get appSettings_appDebugLoggingSubtitle => - 'Logovací správy aplikácie pre ladenie'; + 'Logovací správy aplikácie pre ladenie'; @override String get appSettings_appDebugLoggingEnabled => - 'Aplikácia povolila ladenie protokolmi'; + 'Aplikácia povolila ladenie protokolmi'; @override String get appSettings_appDebugLoggingDisabled => - 'Zabudované ladenie aplikácie je vypnuté.'; + 'Zabudované ladenie aplikácie je vypnuté.'; @override String get contacts_title => 'Kontakty'; @override - String get contacts_noContacts => 'Zatiaľ žiadne kontakty.'; + String get contacts_noContacts => 'Zatiaľ žiadne kontakty.'; @override String get contacts_contactsWillAppear => - 'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.'; + 'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.'; @override - String get contacts_unread => 'Neprečítané'; + String get contacts_unread => 'Neprečítané'; @override - String get contacts_searchContactsNoNumber => 'Hľadať kontakty...'; + String get contacts_searchContactsNoNumber => 'HľadaÅ¥ kontakty...'; @override String contacts_searchContacts(int number, String str) { - return 'Vyhľadávajte kontakty...'; + return 'Vyhľadávajte kontakty...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Hľadať $number$str obľúbené...'; + return 'HľadaÅ¥ $number$str obľúbené...'; } @override String contacts_searchUsers(int number, String str) { - return 'Hľadať $number$str používateľov...'; + return 'HľadaÅ¥ $number$str používateľov...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Hľadať $number$str opakovače...'; + return 'HľadaÅ¥ $number$str opakovače...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Hľadaj $number$str serverov miestností...'; + return 'Hľadaj $number$str serverov miestností...'; } @override - String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty'; + String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty'; @override String get contacts_noContactsFound => - 'Neboli nájdených žiadnych kontaktov ani skupiny.'; + 'Neboli nájdených žiadnych kontaktov ani skupiny.'; @override - String get contacts_deleteContact => 'Odstrániť kontakt'; + String get contacts_deleteContact => 'OdstrániÅ¥ kontakt'; @override String contacts_removeConfirm(String contactName) { - return 'Odstrániť $contactName z kontaktov?'; + return 'OdstrániÅ¥ $contactName z kontaktov?'; } @override - String get contacts_manageRepeater => 'Spravovať opakované zoznamy'; + String get contacts_manageRepeater => 'SpravovaÅ¥ opakované zoznamy'; @override - String get contacts_manageRoom => 'Spravovať server miestnosti'; + String get contacts_manageRoom => 'SpravovaÅ¥ server miestnosti'; @override - String get contacts_roomLogin => 'Prihlásenie do miestnosti'; + String get contacts_roomLogin => 'Prihlásenie do miestnosti'; @override - String get contacts_openChat => 'Otvorené Chat'; + String get contacts_openChat => 'Otvorené Chat'; @override - String get contacts_editGroup => 'Upraviť skupinu'; + String get contacts_editGroup => 'UpraviÅ¥ skupinu'; @override - String get contacts_deleteGroup => 'Vymažť skupinu'; + String get contacts_deleteGroup => 'Vymažť skupinu'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Odstrániť \"$groupName\"?'; + return 'OdstrániÅ¥ \"$groupName\"?'; } @override - String get contacts_newGroup => 'Nová skupina'; + String get contacts_newGroup => 'Nová skupina'; @override - String get contacts_groupName => 'Názov skupiny'; + String get contacts_groupName => 'Názov skupiny'; @override - String get contacts_groupNameRequired => 'Skupina musí mať názov.'; + String get contacts_groupNameRequired => 'Skupina musí maÅ¥ názov.'; @override String contacts_groupAlreadyExists(String name) { - return 'Skupina \"$name\" už existuje'; + return 'Skupina \"$name\" už existuje'; } @override - String get contacts_filterContacts => 'Filtrovať kontakty...'; + String get contacts_filterContacts => 'FiltrovaÅ¥ kontakty...'; @override String get contacts_noContactsMatchFilter => - 'Žiadne kontakty neodídu vášmu filtru.'; + 'Žiadne kontakty neodídu vášmu filtru.'; @override - String get contacts_noMembers => 'Žiadni členovia'; + String get contacts_noMembers => 'Žiadni členovia'; @override - String get contacts_lastSeenNow => 'Posledné zreteľné zobrazenie teraz'; + String get contacts_lastSeenNow => 'Posledné zreteľné zobrazenie teraz'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Posledné zobrazenie $minutes min. dozadu'; + return 'Posledné zobrazenie $minutes min. dozadu'; } @override String get contacts_lastSeenHourAgo => - 'Zobral/Zabral poslednýkrát pred hodinou.'; + 'Zobral/Zabral poslednýkrát pred hodinou.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Posledné zobrazenie $hours hodín dozadu'; + return 'Posledné zobrazenie $hours hodín dozadu'; } @override String get contacts_lastSeenDayAgo => - 'Zobral/Zabral posledný raz pred 1 dňom.'; + 'Zobral/Zabral posledný raz pred 1 dňom.'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Posledné zobrazenie $days dní dozadu'; + return 'Posledné zobrazenie $days dní dozadu'; } @override - String get channels_title => 'Kanály'; + String get channels_title => 'Kanály'; @override - String get channels_noChannelsConfigured => 'Neobsiahnuté žiadne kanály'; + String get channels_noChannelsConfigured => 'Neobsiahnuté žiadne kanály'; @override - String get channels_addPublicChannel => 'Pridať verejný kanál'; + String get channels_addPublicChannel => 'PridaÅ¥ verejný kanál'; @override - String get channels_searchChannels => 'Vyhľadávajte kanály...'; + String get channels_searchChannels => 'Vyhľadávajte kanály...'; @override - String get channels_noChannelsFound => 'Neobsiahlo sa žiadnych kanálov.'; + String get channels_noChannelsFound => 'Neobsiahlo sa žiadnych kanálov.'; @override String channels_channelIndex(int index) { - return 'Kanál $index'; + return 'Kanál $index'; } @override - String get channels_hashtagChannel => 'Kanál s hashtagom'; + String get channels_hashtagChannel => 'Kanál s hashtagom'; @override - String get channels_public => 'Veľké verejné'; + String get channels_public => 'Veľké verejné'; @override - String get channels_private => 'Osobné'; + String get channels_private => 'Osobné'; @override - String get channels_publicChannel => 'Veľké verejne kanály'; + String get channels_publicChannel => 'Veľké verejne kanály'; @override - String get channels_privateChannel => 'Osobné kanál'; + String get channels_privateChannel => 'Osobné kanál'; @override - String get channels_editChannel => 'Upraviť kanál'; + String get channels_editChannel => 'UpraviÅ¥ kanál'; @override - String get channels_muteChannel => 'Stlmiť kanál'; + String get channels_muteChannel => 'StlmiÅ¥ kanál'; @override - String get channels_unmuteChannel => 'Zrušiť stlmenie kanála'; + String get channels_unmuteChannel => 'ZruÅ¡iÅ¥ stlmenie kanála'; @override - String get channels_deleteChannel => 'Odstrániť kanál'; + String get channels_deleteChannel => 'OdstrániÅ¥ kanál'; @override String channels_deleteChannelConfirm(String name) { - return 'Odstrániť \"$name\"? To sa nedá zrušiť.'; + return 'OdstrániÅ¥ \"$name\"? To sa nedá zruÅ¡iÅ¥.'; } @override String channels_channelDeleteFailed(String name) { - return 'Kanál \"$name\" sa nepodarilo odstrániť'; + return 'Kanál \"$name\" sa nepodarilo odstrániÅ¥'; } @override String channels_channelDeleted(String name) { - return 'Kanál \"$name\" bol odstránený'; + return 'Kanál \"$name\" bol odstránený'; } @override - String get channels_addChannel => 'Pridať kanál'; + String get channels_addChannel => 'PridaÅ¥ kanál'; @override - String get channels_channelIndexLabel => 'Index kanála'; + String get channels_channelIndexLabel => 'Index kanála'; @override - String get channels_channelName => 'Názov kanálu'; + String get channels_channelName => 'Názov kanálu'; @override - String get channels_usePublicChannel => 'Použite verejný kanál'; + String get channels_usePublicChannel => 'Použite verejný kanál'; @override - String get channels_standardPublicPsk => 'Štandardný verejný PSK'; + String get channels_standardPublicPsk => 'Å tandardný verejný PSK'; @override - String get channels_pskHex => 'PSK (Šifrovacia kľúčik)'; + String get channels_pskHex => 'PSK (Å ifrovacia kľúčik)'; @override - String get channels_generateRandomPsk => 'Generovať náhodný PSK'; + String get channels_generateRandomPsk => 'GenerovaÅ¥ náhodný PSK'; @override - String get channels_enterChannelName => 'Prosím, zadajte názov kanála.'; + String get channels_enterChannelName => 'Prosím, zadajte názov kanála.'; @override String get channels_pskMustBe32Hex => - 'PSK musí mať 32 hexadecimálových znakov.'; + 'PSK musí maÅ¥ 32 hexadecimálových znakov.'; @override String channels_channelAdded(String name) { - return 'Kanál \"$name\" pridaný'; + return 'Kanál \"$name\" pridaný'; } @override String channels_editChannelTitle(int index) { - return 'Upraviť kanál $index'; + return 'UpraviÅ¥ kanál $index'; } @override - String get channels_smazCompression => 'Odstránenie kompresie SMAZ'; + String get channels_smazCompression => 'Odstránenie kompresie SMAZ'; @override String channels_channelUpdated(String name) { - return 'Kanál \"$name\" bol aktualizovaný'; + return 'Kanál \"$name\" bol aktualizovaný'; } @override - String get channels_publicChannelAdded => 'Veľký kanál pridaný'; + String get channels_publicChannelAdded => 'Veľký kanál pridaný'; @override - String get channels_sortBy => 'Triediť podľa'; + String get channels_sortBy => 'TriediÅ¥ podľa'; @override - String get channels_sortManual => 'Ručne'; + String get channels_sortManual => 'Ručne'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Posledné správy'; + String get channels_sortLatestMessages => 'Posledné správy'; @override - String get channels_sortUnread => 'Nezriadené'; + String get channels_sortUnread => 'Nezriadené'; @override - String get channels_createPrivateChannel => 'Vytvorte súkromný kanál'; + String get channels_createPrivateChannel => 'Vytvorte súkromný kanál'; @override String get channels_createPrivateChannelDesc => - 'Zabezpečené pomocou tajného kľúča.'; + 'Zabezpečené pomocou tajného kľúča.'; @override - String get channels_joinPrivateChannel => 'Pripojiť sa k súkromnému kanálu'; + String get channels_joinPrivateChannel => + 'PripojiÅ¥ sa k súkromnému kanálu'; @override - String get channels_joinPrivateChannelDesc => 'Ručne zadajte tajný kľúč.'; + String get channels_joinPrivateChannelDesc => + 'Ručne zadajte tajný kľúč.'; @override - String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu'; + String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu'; @override String get channels_joinPublicChannelDesc => - 'Któvek sátó na tutó kanalizovát.'; + 'Któvek sátó na tutó kanalizovát.'; @override - String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu'; + String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu'; @override String get channels_joinHashtagChannelDesc => - 'Ktoekolikoľvek sa môže pridať do hashtag kanálov.'; + 'Ktoekolikoľvek sa môže pridaÅ¥ do hashtag kanálov.'; @override - String get channels_scanQrCode => 'Skenujte QR kód'; + String get channels_scanQrCode => 'Skenujte QR kód'; @override - String get channels_scanQrCodeComingSoon => 'Čoskoro'; + String get channels_scanQrCodeComingSoon => 'ÄŒoskoro'; @override String get channels_enterHashtag => 'Zadajte hashtag'; @override - String get channels_hashtagHint => 'napr. #tím'; + String get channels_hashtagHint => 'napr. #tím'; @override - String get chat_noMessages => 'Zatiaľ žiadne správy.'; + String get chat_noMessages => 'Zatiaľ žiadne správy.'; @override - String get chat_sendMessageToStart => 'Pošlite správu na začiatok'; + String get chat_sendMessageToStart => 'PoÅ¡lite správu na začiatok'; @override - String get chat_originalMessageNotFound => 'Neznámy pôvodný odkaz.'; + String get chat_originalMessageNotFound => 'Neznámy pôvodný odkaz.'; @override String chat_replyingTo(String name) { - return 'Odpovedám $name'; + return 'Odpovedám $name'; } @override String chat_replyTo(String name) { - return 'Odpovedať $name'; + return 'OdpovedaÅ¥ $name'; } @override @@ -1016,39 +1016,39 @@ class AppLocalizationsSk extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'Pošli správu $contactName'; + return 'PoÅ¡li správu $contactName'; } @override - String get chat_typeMessage => 'Napište správu...'; + String get chat_typeMessage => 'NapiÅ¡te správu...'; @override String chat_messageTooLong(int maxBytes) { - return 'Správa je príliš dlhá (max $maxBytes bytov).'; + return 'Správa je príliÅ¡ dlhá (max $maxBytes bytov).'; } @override - String get chat_messageCopied => 'Správa skopírovaná'; + String get chat_messageCopied => 'Správa skopírovaná'; @override - String get chat_messageDeleted => 'Posolstvo odstránené'; + String get chat_messageDeleted => 'Posolstvo odstránené'; @override String get chat_retryingMessage => 'Pokus o obnovenie'; @override String chat_retryCount(int current, int max) { - return 'Skúsiť $current/$max'; + return 'SkúsiÅ¥ $current/$max'; } @override - String get chat_sendGif => 'Odoslať GIF'; + String get chat_sendGif => 'OdoslaÅ¥ GIF'; @override - String get chat_reply => 'Odpovedať'; + String get chat_reply => 'OdpovedaÅ¥'; @override - String get chat_addReaction => 'Pridať Reakciu'; + String get chat_addReaction => 'PridaÅ¥ Reakciu'; @override String get chat_me => 'Mne'; @@ -1057,7 +1057,7 @@ class AppLocalizationsSk extends AppLocalizations { String get emojiCategorySmileys => 'Emoji'; @override - String get emojiCategoryGestures => 'Gestá'; + String get emojiCategoryGestures => 'Gestá'; @override String get emojiCategoryHearts => 'Srdcia'; @@ -1069,84 +1069,84 @@ class AppLocalizationsSk extends AppLocalizations { String get gifPicker_title => 'Vyberte GIF'; @override - String get gifPicker_searchHint => 'Vyhľadávajte GIFy...'; + String get gifPicker_searchHint => 'Vyhľadávajte GIFy...'; @override - String get gifPicker_poweredBy => 'Napájané spoločnosťou GIPHY'; + String get gifPicker_poweredBy => 'Napájané spoločnosÅ¥ou GIPHY'; @override - String get gifPicker_noGifsFound => 'Neboli nájdené žiadne GIFy.'; + String get gifPicker_noGifsFound => 'Neboli nájdené žiadne GIFy.'; @override - String get gifPicker_failedLoad => 'Nepodarilo sa načítať GIFy'; + String get gifPicker_failedLoad => 'Nepodarilo sa načítaÅ¥ GIFy'; @override - String get gifPicker_failedSearch => 'Nepodarilo sa vyhľadať GIFy'; + String get gifPicker_failedSearch => 'Nepodarilo sa vyhľadaÅ¥ GIFy'; @override - String get gifPicker_noInternet => 'Žiadna internetová konektivita'; + String get gifPicker_noInternet => 'Žiadna internetová konektivita'; @override - String get debugLog_appTitle => 'Záznam ladenia aplikácie'; + String get debugLog_appTitle => 'Záznam ladenia aplikácie'; @override String get debugLog_bleTitle => 'Log BLE Debug'; @override - String get debugLog_copyLog => 'Kopírovať záznam'; + String get debugLog_copyLog => 'KopírovaÅ¥ záznam'; @override - String get debugLog_clearLog => 'Vymažať záznam'; + String get debugLog_clearLog => 'VymažaÅ¥ záznam'; @override - String get debugLog_copied => 'Záznam ladenia skopírovaný'; + String get debugLog_copied => 'Záznam ladenia skopírovaný'; @override - String get debugLog_bleCopied => 'Kopírovaný záznam z BLE.'; + String get debugLog_bleCopied => 'Kopírovaný záznam z BLE.'; @override String get debugLog_noEntries => - 'Zatiaľ neboli zaznamenané žiadne debug logy.'; + 'Zatiaľ neboli zaznamenané žiadne debug logy.'; @override String get debugLog_enableInSettings => - 'Povolte ladicové logy v nastaveniach'; + 'Povolte ladicové logy v nastaveniach'; @override - String get debugLog_frames => 'Rámce'; + String get debugLog_frames => 'Rámce'; @override String get debugLog_rawLogRx => 'Raw Log-RX'; @override - String get debugLog_noBleActivity => 'Zatiaľ žiadna aktivita BLE.'; + String get debugLog_noBleActivity => 'Zatiaľ žiadna aktivita BLE.'; @override String debugFrame_length(int count) { - return 'Dĺžka rámca: $count bajtov'; + return 'Dĺžka rámca: $count bajtov'; } @override String debugFrame_command(String value) { - return 'Prikáž: 0x$value'; + return 'PrikázÌŒ: 0x$value'; } @override - String get debugFrame_textMessageHeader => 'Textová zvesť:'; + String get debugFrame_textMessageHeader => 'Textová zvesÅ¥:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Cieľový PubKey: $pubKey'; + return '- Cieľový PubKey: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Časové označenie: $timestamp'; + return '- ÄŒasové označenie: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Žiadne vlajky: 0x$value'; + return '- Žiadne vlajky: 0x$value'; } @override @@ -1158,7 +1158,7 @@ class AppLocalizationsSk extends AppLocalizations { String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Jednoduché'; + String get debugFrame_textTypePlain => 'Jednoduché'; @override String debugFrame_text(String text) { @@ -1169,33 +1169,33 @@ class AppLocalizationsSk extends AppLocalizations { String get debugFrame_hexDump => 'Hex Dump:'; @override - String get chat_pathManagement => 'Správa ciest'; + String get chat_pathManagement => 'Správa ciest'; @override - String get chat_ShowAllPaths => 'Zobraziť všetky cesty'; + String get chat_ShowAllPaths => 'ZobraziÅ¥ vÅ¡etky cesty'; @override - String get chat_routingMode => 'Režim trasy'; + String get chat_routingMode => 'Režim trasy'; @override - String get chat_autoUseSavedPath => 'Použiť uloženú cestu'; + String get chat_autoUseSavedPath => 'PoužiÅ¥ uloženú cestu'; @override String get chat_forceFloodMode => - 'Zavrieť režim núdzového povodňového režimu'; + 'ZavrieÅ¥ režim núdzového povodňového režimu'; @override - String get chat_recentAckPaths => 'Nedávne cesty ACK (klepni na použitie):'; + String get chat_recentAckPaths => 'Nedávne cesty ACK (klepni na použitie):'; @override String get chat_pathHistoryFull => - 'História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.'; + 'História ciest je plná. Odstráňte záznamy, aby ste mohli pridaÅ¥ nové.'; @override String get chat_hopSingular => 'Skok'; @override - String get chat_hopPlural => 'Skákať'; + String get chat_hopPlural => 'SkákaÅ¥'; @override String chat_hopsCount(int count) { @@ -1209,49 +1209,49 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get chat_successes => 'Úspechy'; + String get chat_successes => 'Úspechy'; @override - String get chat_removePath => 'Odstrániť cestu'; + String get chat_removePath => 'OdstrániÅ¥ cestu'; @override String get chat_noPathHistoryYet => - 'Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.'; + 'Zatiaľ žiadna história trás.\nPoÅ¡lite správu a objavte trasy.'; @override String get chat_pathActions => 'Cesty:'; @override - String get chat_setCustomPath => 'Nastaviť vlastnú cestu'; + String get chat_setCustomPath => 'NastaviÅ¥ vlastnú cestu'; @override - String get chat_setCustomPathSubtitle => 'Ručne zadajte trasu.'; + String get chat_setCustomPathSubtitle => 'Ručne zadajte trasu.'; @override - String get chat_clearPath => 'Vyčistiš cestu'; + String get chat_clearPath => 'VyčistiÅ¡ cestu'; @override String get chat_clearPathSubtitle => - 'Znovu nájsť vynútene pri nasledujúcej pošlite'; + 'Znovu nájsÅ¥ vynútene pri nasledujúcej poÅ¡lite'; @override String get chat_pathCleared => - 'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.'; + 'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.'; @override String get chat_floodModeSubtitle => - 'Použite prepínanie trasy v navigačnom paneli.'; + 'Použite prepínanie trasy v navigačnom paneli.'; @override String get chat_floodModeEnabled => - 'Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.'; + 'Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.'; @override - String get chat_fullPath => 'Celá cesta'; + String get chat_fullPath => 'Celá cesta'; @override String get chat_pathDetailsNotAvailable => - 'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.'; + 'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslaÅ¥ správu na obnovenie.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1261,41 +1261,41 @@ class AppLocalizationsSk extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Cesta nastavená: $hopCount $_temp0 - $status'; + return 'Cesta nastavená: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Uložené lokálne. Spojte sa na synchronizáciu.'; + 'Uložené lokálne. Spojte sa na synchronizáciu.'; @override - String get chat_pathDeviceConfirmed => 'Zariadenie potvrdené.'; + String get chat_pathDeviceConfirmed => 'Zariadenie potvrdené.'; @override String get chat_pathDeviceNotConfirmed => - 'Zariadenie zatiaľ nebolo potvrdené.'; + 'Zariadenie zatiaľ nebolo potvrdené.'; @override - String get chat_type => 'Napište'; + String get chat_type => 'NapiÅ¡te'; @override String get chat_path => 'Cesta'; @override - String get chat_publicKey => 'Verejný kľúč'; + String get chat_publicKey => 'Verejný kľúč'; @override - String get chat_compressOutgoingMessages => 'Komprimovať odoslané správy'; + String get chat_compressOutgoingMessages => 'KomprimovaÅ¥ odoslané správy'; @override - String get chat_floodForced => 'Povodňová (nutená)'; + String get chat_floodForced => 'Povodňová (nutená)'; @override - String get chat_directForced => 'Priame (donútené)'; + String get chat_directForced => 'Priame (donútené)'; @override String chat_hopsForced(int count) { - return '$count skokov (nutené)'; + return '$count skokov (nutené)'; } @override @@ -1305,30 +1305,30 @@ class AppLocalizationsSk extends AppLocalizations { String get chat_direct => 'Priamo'; @override - String get chat_poiShared => 'Zdieľané body záujmu'; + String get chat_poiShared => 'Zdieľané body záujmu'; @override String chat_unread(int count) { - return 'Nezriadené: $count'; + return 'Nezriadené: $count'; } @override - String get chat_openLink => 'Otvoriť odkaz?'; + String get chat_openLink => 'OtvoriÅ¥ odkaz?'; @override String get chat_openLinkConfirmation => - 'Chcete otvoriť tento odkaz v prehliadači?'; + 'Chcete otvoriÅ¥ tento odkaz v prehliadači?'; @override - String get chat_open => 'Otvoriť'; + String get chat_open => 'OtvoriÅ¥'; @override String chat_couldNotOpenLink(String url) { - return 'Nepodarilo sa otvoriť odkaz: $url'; + return 'Nepodarilo sa otvoriÅ¥ odkaz: $url'; } @override - String get chat_invalidLink => 'Neplatný formát odkazu'; + String get chat_invalidLink => 'Neplatný formát odkazu'; @override String get map_title => 'Mapa uzlov'; @@ -1340,11 +1340,11 @@ class AppLocalizationsSk extends AppLocalizations { String get map_losScreenTitle => 'Line of Sight'; @override - String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe'; + String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe'; @override String get map_nodesNeedGps => - 'Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.'; + 'Uholníky musia zdieľaÅ¥ svoje GPS súradnice, aby sa zobrazili na mape.'; @override String map_nodesCount(int count) { @@ -1353,7 +1353,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String map_pinsCount(int count) { - return 'Krúžky: $count'; + return 'Krúžky: $count'; } @override @@ -1372,17 +1372,17 @@ class AppLocalizationsSk extends AppLocalizations { String get map_pinDm => 'Zabudka (DM)'; @override - String get map_pinPrivate => 'Zabudka (Osobná)'; + String get map_pinPrivate => 'Zabudka (Osobná)'; @override - String get map_pinPublic => 'Zablokovať (verejne)'; + String get map_pinPublic => 'ZablokovaÅ¥ (verejne)'; @override - String get map_lastSeen => 'Posledné zreteľné zobrazenie'; + String get map_lastSeen => 'Posledné zreteľné zobrazenie'; @override String get map_disconnectConfirm => - 'Ste si istý/á, že chcete odpojiť od tohto zariadenia?'; + 'Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?'; @override String get map_from => 'Od'; @@ -1391,167 +1391,170 @@ class AppLocalizationsSk extends AppLocalizations { String get map_source => 'Zdroj'; @override - String get map_flags => 'Zástavy'; + String get map_flags => 'Zástavy'; @override - String get map_shareMarkerHere => 'Zdieľte značku tu'; + String get map_shareMarkerHere => 'Zdieľte značku tu'; @override - String get map_pinLabel => 'Označka upozornenia'; + String get map_pinLabel => 'Označka upozornenia'; @override - String get map_label => 'Značka'; + String get map_label => 'Značka'; @override - String get map_pointOfInterest => 'Bod záujmu'; + String get map_pointOfInterest => 'Bod záujmu'; @override - String get map_sendToContact => 'Pošleť na kontakt'; + String get map_sendToContact => 'PoÅ¡leÅ¥ na kontakt'; @override - String get map_sendToChannel => 'Poslať do kanálu'; + String get map_sendToChannel => 'PoslaÅ¥ do kanálu'; @override - String get map_noChannelsAvailable => 'Неexistujú žiadne kanály.'; + String get map_noChannelsAvailable => 'Неexistujú žiadne kanály.'; @override - String get map_publicLocationShare => 'Zdieľiť verejnú lokalitu'; + String get map_publicLocationShare => 'ZdieľiÅ¥ verejnú lokalitu'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Čoskoro budete zdieľať polohu v $channelLabel. Tento kanál je verejný a môže ho vidieť každý s PSK.'; + return 'ÄŒoskoro budete zdieľaÅ¥ polohu v $channelLabel. Tento kanál je verejný a môže ho vidieÅ¥ každý s PSK.'; } @override String get map_connectToShareMarkers => - 'Pripojte sa k zariadeniu na zdieľanie značiek'; + 'Pripojte sa k zariadeniu na zdieľanie značiek'; @override - String get map_filterNodes => 'Filtrovať uzly'; + String get map_filterNodes => 'FiltrovaÅ¥ uzly'; @override String get map_nodeTypes => 'Typy uzlov'; @override - String get map_chatNodes => 'Chatové uzly'; + String get map_chatNodes => 'Chatové uzly'; @override - String get map_repeaters => 'Opakovadlá'; + String get map_repeaters => 'Opakovadlá'; @override - String get map_otherNodes => 'Ostatné uzly'; + String get map_otherNodes => 'Ostatné uzly'; @override - String get map_keyPrefix => 'Päťciferné predpona'; + String get map_keyPrefix => 'Päťciferné predpona'; @override - String get map_filterByKeyPrefix => 'Filtrovať podľa predponového kľúča'; + String get map_filterByKeyPrefix => + 'FiltrovaÅ¥ podľa predponového kľúča'; @override - String get map_publicKeyPrefix => 'Prefix verejného kľúča'; + String get map_publicKeyPrefix => 'Prefix verejného kľúča'; @override - String get map_markers => 'Označkovače'; + String get map_markers => 'Označkovače'; @override - String get map_showSharedMarkers => 'Zobraziť zdieľané značky'; + String get map_showSharedMarkers => 'ZobraziÅ¥ zdieľané značky'; @override - String get map_lastSeenTime => 'Posledný čas sledovania'; + String get map_lastSeenTime => 'Posledný čas sledovania'; @override - String get map_sharedPin => 'Zdieľaný PIN'; + String get map_sharedPin => 'Zdieľaný PIN'; @override - String get map_joinRoom => 'Pripojiť miestnosť'; + String get map_joinRoom => 'PripojiÅ¥ miestnosÅ¥'; @override - String get map_manageRepeater => 'Spravovať Opakovanie'; + String get map_manageRepeater => 'SpravovaÅ¥ Opakovanie'; @override String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.'; @override - String get map_runTrace => 'Spustiť trasovaním cesty'; + String get map_runTrace => 'SpustiÅ¥ trasovaním cesty'; @override - String get map_removeLast => 'Odstrániť posledný'; + String get map_removeLast => 'OdstrániÅ¥ posledný'; @override - String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.'; + String get map_pathTraceCancelled => + 'ZruÅ¡enie stopáže cesty bolo zruÅ¡ené.'; @override - String get mapCache_title => 'Offline Mapa Pamäť'; + String get mapCache_title => 'Offline Mapa Pamäť'; @override - String get mapCache_selectAreaFirst => 'Vyberte si oblasť na predprerúčenie.'; + String get mapCache_selectAreaFirst => + 'Vyberte si oblasÅ¥ na predprerúčenie.'; @override String get mapCache_noTilesToDownload => - 'Žiadne dlaždice na stiahnutie pre toto zóna'; + 'Žiadne dlaždice na stiahnutie pre toto zóna'; @override - String get mapCache_downloadTilesTitle => 'Stiahnuť dlaždice'; + String get mapCache_downloadTilesTitle => 'StiahnuÅ¥ dlaždice'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Stiahnuť $count dlaždíc na offline použitie?'; + return 'StiahnuÅ¥ $count dlaždíc na offline použitie?'; } @override - String get mapCache_downloadAction => 'Stiahnuť'; + String get mapCache_downloadAction => 'StiahnuÅ¥'; @override String mapCache_cachedTiles(int count) { - return 'Zabudené $count dlaždíc'; + return 'Zabudené $count dlaždíc'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Uložené $downloaded dlaždice ($failed neúspešné)'; + return 'Uložené $downloaded dlaždice ($failed neúspeÅ¡né)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Vymazať offline uloženie'; + String get mapCache_clearOfflineCacheTitle => 'VymazaÅ¥ offline uloženie'; @override String get mapCache_clearOfflineCachePrompt => - 'Odstrániť všetky uložené mapové dlaždice?'; + 'OdstrániÅ¥ vÅ¡etky uložené mapové dlaždice?'; @override - String get mapCache_offlineCacheCleared => 'Offline polia vymazaná'; + String get mapCache_offlineCacheCleared => 'Offline polia vymazaná'; @override - String get mapCache_noAreaSelected => 'Neoznačila sa žiadna oblasť'; + String get mapCache_noAreaSelected => 'Neoznačila sa žiadna oblasÅ¥'; @override - String get mapCache_cacheArea => 'Obdĺžková oblasť'; + String get mapCache_cacheArea => 'Obdĺžková oblasÅ¥'; @override - String get mapCache_useCurrentView => 'Použite aktuálny zobrazenie'; + String get mapCache_useCurrentView => 'Použite aktuálny zobrazenie'; @override - String get mapCache_zoomRange => 'Rozsah zväčšenia'; + String get mapCache_zoomRange => 'Rozsah zväčšenia'; @override String mapCache_estimatedTiles(int count) { - return 'Odhadnuté dlaždice: $count'; + return 'Odhadnuté dlaždice: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Stiahnuté $completed / $total'; + return 'Stiahnuté $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Stiahnuť dlaždice'; + String get mapCache_downloadTilesButton => 'StiahnuÅ¥ dlaždice'; @override - String get mapCache_clearCacheButton => 'Vyprázdniť Vädsť'; + String get mapCache_clearCacheButton => 'VyprázdniÅ¥ VädsÅ¥'; @override String mapCache_failedDownloads(int count) { - return 'Neúspešné stiahnutia: $count'; + return 'NeúspeÅ¡né stiahnutia: $count'; } @override @@ -1565,7 +1568,7 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get time_justNow => 'Príbeh'; + String get time_justNow => 'Príbeh'; @override String time_minutesAgo(int minutes) { @@ -1579,7 +1582,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String time_daysAgo(int days) { - return '$days dní dozadu'; + return '$days dní dozadu'; } @override @@ -1589,16 +1592,16 @@ class AppLocalizationsSk extends AppLocalizations { String get time_hours => 'hodiny'; @override - String get time_day => 'deň'; + String get time_day => 'deň'; @override String get time_days => 'dni'; @override - String get time_week => 'týždeň'; + String get time_week => 'týždeň'; @override - String get time_weeks => 'týždne'; + String get time_weeks => 'týždne'; @override String get time_month => 'mesiac'; @@ -1607,23 +1610,23 @@ class AppLocalizationsSk extends AppLocalizations { String get time_months => 'mesiace'; @override - String get time_minutes => 'minúty'; + String get time_minutes => 'minúty'; @override - String get time_allTime => 'Všetko Časom'; + String get time_allTime => 'VÅ¡etko ÄŒasom'; @override - String get dialog_disconnect => 'Odpojiť'; + String get dialog_disconnect => 'OdpojiÅ¥'; @override String get dialog_disconnectConfirm => - 'Ste si istý/á, že chcete odpojiť od tohto zariadenia?'; + 'Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?'; @override - String get login_repeaterLogin => 'Opätovné prihlásenie'; + String get login_repeaterLogin => 'Opätovné prihlásenie'; @override - String get login_roomLogin => 'Prihlásenie do miestnosti'; + String get login_roomLogin => 'Prihlásenie do miestnosti'; @override String get login_password => 'Heslo'; @@ -1632,62 +1635,62 @@ class AppLocalizationsSk extends AppLocalizations { String get login_enterPassword => 'Zadajte heslo'; @override - String get login_savePassword => 'Uložiť heslo'; + String get login_savePassword => 'UložiÅ¥ heslo'; @override String get login_savePasswordSubtitle => - 'Heslo bude bezpečne uložené na tomto zariadení.'; + 'Heslo bude bezpečne uložené na tomto zariadení.'; @override String get login_repeaterDescription => - 'Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.'; + 'Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.'; @override String get login_roomDescription => - 'Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.'; + 'Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.'; @override - String get login_routing => 'Rútiace'; + String get login_routing => 'Rútiace'; @override - String get login_routingMode => 'Režim trasy'; + String get login_routingMode => 'Režim trasy'; @override - String get login_autoUseSavedPath => 'Použiť uloženú cestu'; + String get login_autoUseSavedPath => 'PoužiÅ¥ uloženú cestu'; @override String get login_forceFloodMode => - 'Zavrieť režim núdzového povodňového režimu'; + 'ZavrieÅ¥ režim núdzového povodňového režimu'; @override - String get login_managePaths => 'Spravovať Cesty'; + String get login_managePaths => 'SpravovaÅ¥ Cesty'; @override - String get login_login => 'Prihlásiť'; + String get login_login => 'PrihlásiÅ¥'; @override String login_attempt(int current, int max) { - return 'Skúšaj $current/$max'; + return 'Skúšaj $current/$max'; } @override String login_failed(String error) { - return 'Prihlásenie zlyhalo: $error'; + return 'Prihlásenie zlyhalo: $error'; } @override String get login_failedMessage => - 'Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.'; + 'Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.'; @override - String get common_reload => 'Načítať'; + String get common_reload => 'NačítaÅ¥'; @override - String get common_clear => 'Zmazať'; + String get common_clear => 'ZmazaÅ¥'; @override String path_currentPath(String path) { - return 'Aktívna cesta: $path'; + return 'Aktívna cesta: $path'; } @override @@ -1698,150 +1701,151 @@ class AppLocalizationsSk extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Používa $count $_temp0 cestu'; + return 'Používa $count $_temp0 cestu'; } @override - String get path_enterCustomPath => 'Zadajte vlastný priebeh'; + String get path_enterCustomPath => 'Zadajte vlastný priebeh'; @override - String get path_currentPathLabel => 'Aktuálny priebeh'; + String get path_currentPathLabel => 'Aktuálny priebeh'; @override String get path_hexPrefixInstructions => - 'Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.'; + 'Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)'; + 'A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)'; @override - String get path_labelHexPrefixes => 'Cesty (hexové predpony)'; + String get path_labelHexPrefixes => 'Cesty (hexové predpony)'; @override String get path_helperMaxHops => - 'Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).'; + 'Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).'; @override String get path_selectFromContacts => 'Vyberte sa z kontaktov:'; @override String get path_noRepeatersFound => - 'Nenašli sa žiadne opakovače ani serverové miestnosti.'; + 'NenaÅ¡li sa žiadne opakovače ani serverové miestnosti.'; @override String get path_customPathsRequire => - 'Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášať správky.'; + 'Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášaÅ¥ správky.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Neplatné hexové predpony: $prefixes'; + return 'Neplatné hexové predpony: $prefixes'; } @override String get path_tooLong => - 'Cesta je príliš dlhá. Umožnené je maximum 64 skokov.'; + 'Cesta je príliÅ¡ dlhá. Umožnené je maximum 64 skokov.'; @override - String get path_setPath => 'Nastaviť cestu'; + String get path_setPath => 'NastaviÅ¥ cestu'; @override - String get repeater_management => 'Správa opakérov'; + String get repeater_management => 'Správa opakérov'; @override - String get room_management => 'Správa servera miestnosti'; + String get room_management => 'Správa servera miestnosti'; @override - String get repeater_managementTools => 'Nástroje na správu'; + String get repeater_managementTools => 'Nástroje na správu'; @override String get repeater_status => 'Status'; @override String get repeater_statusSubtitle => - 'Zobraziť stav, štatistiky a susedov repeatera'; + 'ZobraziÅ¥ stav, Å¡tatistiky a susedov repeatera'; @override String get repeater_telemetry => 'Telemetria'; @override String get repeater_telemetrySubtitle => - 'Zobraziť telemetriu senzorov a systémových štatistík'; + 'ZobraziÅ¥ telemetriu senzorov a systémových Å¡tatistík'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču'; + String get repeater_cliSubtitle => 'PoÅ¡lite príkazy opakovaču'; @override - String get repeater_neighbors => 'Súsezný'; + String get repeater_neighbors => 'Súsezný'; @override - String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.'; + String get repeater_neighborsSubtitle => + 'ZobraziÅ¥ susedné body bez skokov.'; @override String get repeater_settings => 'Nastavenia'; @override - String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; + String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; @override - String get repeater_statusTitle => 'Status opakého zboru'; + String get repeater_statusTitle => 'Status opakého zboru'; @override - String get repeater_routingMode => 'Režim trasy'; + String get repeater_routingMode => 'Režim trasy'; @override - String get repeater_autoUseSavedPath => 'Použiť uloženú cestu'; + String get repeater_autoUseSavedPath => 'PoužiÅ¥ uloženú cestu'; @override String get repeater_forceFloodMode => - 'Zavrieť režim núdzového povodňového režimu'; + 'ZavrieÅ¥ režim núdzového povodňového režimu'; @override - String get repeater_pathManagement => 'Správa trás'; + String get repeater_pathManagement => 'Správa trás'; @override - String get repeater_refresh => 'Obnoviť'; + String get repeater_refresh => 'ObnoviÅ¥'; @override - String get repeater_statusRequestTimeout => 'Požiadavka stavu zlyhala.'; + String get repeater_statusRequestTimeout => 'Požiadavka stavu zlyhala.'; @override String repeater_errorLoadingStatus(String error) { - return 'Chyba pri načítaní stavu: $error'; + return 'Chyba pri načítaní stavu: $error'; } @override - String get repeater_systemInformation => 'Informácie o systéme'; + String get repeater_systemInformation => 'Informácie o systéme'; @override - String get repeater_battery => 'Batéria'; + String get repeater_battery => 'Batéria'; @override - String get repeater_clockAtLogin => 'Čas (při přihlášení)'; + String get repeater_clockAtLogin => 'ÄŒas (pÅ™i pÅ™ihlášení)'; @override - String get repeater_uptime => 'Dostupnosť'; + String get repeater_uptime => 'DostupnosÅ¥'; @override - String get repeater_queueLength => 'Dĺžka fronty'; + String get repeater_queueLength => 'Dĺžka fronty'; @override - String get repeater_debugFlags => 'Kontrolné značky'; + String get repeater_debugFlags => 'Kontrolné značky'; @override - String get repeater_radioStatistics => 'Rádio Štatistiky'; + String get repeater_radioStatistics => 'Rádio Å tatistiky'; @override - String get repeater_lastRssi => 'Posledná RSSI'; + String get repeater_lastRssi => 'Posledná RSSI'; @override - String get repeater_lastSnr => 'Posledný SNR'; + String get repeater_lastSnr => 'Posledný SNR'; @override - String get repeater_noiseFloor => 'Hladina šumu'; + String get repeater_noiseFloor => 'Hladina Å¡umu'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1850,16 +1854,16 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Statistiky balíka'; + String get repeater_packetStatistics => 'Statistiky balíka'; @override - String get repeater_sent => 'Odoslané'; + String get repeater_sent => 'Odoslané'; @override - String get repeater_received => 'Prišlo'; + String get repeater_received => 'PriÅ¡lo'; @override - String get repeater_duplicates => 'Duplikáty'; + String get repeater_duplicates => 'Duplikáty'; @override String repeater_daysHoursMinsSecs( @@ -1868,17 +1872,17 @@ class AppLocalizationsSk extends AppLocalizations { int minutes, int seconds, ) { - return '$days dní ${hours}h ${minutes}m ${seconds}s'; + return '$days dní ${hours}h ${minutes}m ${seconds}s'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; + return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; + return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; } @override @@ -1892,31 +1896,33 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Nastavenia Opakovača'; + String get repeater_settingsTitle => 'Nastavenia OpakovacÌŒa'; @override - String get repeater_basicSettings => 'Základné nastavenia'; + String get repeater_basicSettings => 'Základné nastavenia'; @override - String get repeater_repeaterName => 'Opakovacia názov'; + String get repeater_repeaterName => 'Opakovacia názov'; @override - String get repeater_repeaterNameHelper => 'Zobrazenie názvu tohto opakovača'; + String get repeater_repeaterNameHelper => + 'Zobrazenie názvu tohto opakovača'; @override - String get repeater_adminPassword => 'Heslo administrátora'; + String get repeater_adminPassword => 'Heslo administrátora'; @override - String get repeater_adminPasswordHelper => 'Celý prístupový heslo'; + String get repeater_adminPasswordHelper => 'Celý prístupový heslo'; @override - String get repeater_guestPassword => 'Heslo hosťa'; + String get repeater_guestPassword => 'Heslo hosÅ¥a'; @override - String get repeater_guestPasswordHelper => 'Prístupový heslo iba na čítanie'; + String get repeater_guestPasswordHelper => + 'Prístupový heslo iba na čítanie'; @override - String get repeater_radioSettings => 'Nastavenia rádia'; + String get repeater_radioSettings => 'Nastavenia rádia'; @override String get repeater_frequencyMhz => 'Frekvencia (MHz)'; @@ -1931,28 +1937,28 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Šírka pásma'; + String get repeater_bandwidth => 'Šírka pásma'; @override - String get repeater_spreadingFactor => 'Šírenie faktoru'; + String get repeater_spreadingFactor => 'Šírenie faktoru'; @override - String get repeater_codingRate => 'Rýchlosť kódovania'; + String get repeater_codingRate => 'RýchlosÅ¥ kódovania'; @override String get repeater_locationSettings => 'Nastavenia polohy'; @override - String get repeater_latitude => 'Súradnica'; + String get repeater_latitude => 'Súradnica'; @override - String get repeater_latitudeHelper => 'Desatinné zložky (napr. 37.7749)'; + String get repeater_latitudeHelper => 'Desatinné zložky (napr. 37.7749)'; @override - String get repeater_longitude => 'Dĺžka'; + String get repeater_longitude => 'Dĺžka'; @override - String get repeater_longitudeHelper => 'Desatinné zložky (napr. -122.4194)'; + String get repeater_longitudeHelper => 'Desatinné zložky (napr. -122.4194)'; @override String get repeater_features => 'Funkcie'; @@ -1962,173 +1968,176 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_packetForwardingSubtitle => - 'Povolte opakovač na smerovanie paketov.'; + 'Povolte opakovač na smerovanie paketov.'; @override - String get repeater_guestAccess => 'Prístup pre hostí'; + String get repeater_guestAccess => 'Prístup pre hostí'; @override String get repeater_guestAccessSubtitle => - 'Umožniť prístup hosta iba na čítanie.'; + 'UmožniÅ¥ prístup hosta iba na čítanie.'; @override - String get repeater_privacyMode => 'Režim ochrany súkromia'; + String get repeater_privacyMode => 'Režim ochrany súkromia'; @override - String get repeater_privacyModeSubtitle => 'Skryť meno/poloha v reklamách'; + String get repeater_privacyModeSubtitle => 'SkryÅ¥ meno/poloha v reklamách'; @override String get repeater_advertisementSettings => 'Nastavenia reklamy'; @override - String get repeater_localAdvertInterval => 'Lokálna reklamná časová obdoba'; + String get repeater_localAdvertInterval => + 'Lokálna reklamná časová obdoba'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes minút'; + return '$minutes minút'; } @override String get repeater_floodAdvertInterval => - 'Interval reklamnej povodňovej reklamy'; + 'Interval reklamnej povodňovej reklamy'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours hodín'; + return '$hours hodín'; } @override - String get repeater_encryptedAdvertInterval => 'Šifrovaný reklamný interval'; + String get repeater_encryptedAdvertInterval => + 'Å ifrovaný reklamný interval'; @override - String get repeater_dangerZone => 'Nebezpečná zóna'; + String get repeater_dangerZone => 'Nebezpečná zóna'; @override - String get repeater_rebootRepeater => 'Restart Repetér'; + String get repeater_rebootRepeater => 'Restart Repetér'; @override - String get repeater_rebootRepeaterSubtitle => 'Resetovať vysielací prístroj'; + String get repeater_rebootRepeaterSubtitle => + 'ResetovaÅ¥ vysielací prístroj'; @override String get repeater_rebootRepeaterConfirm => - 'Ste si istý, že chcete tento opakovač restartovať?'; + 'Ste si istý, že chcete tento opakovač restartovaÅ¥?'; @override - String get repeater_regenerateIdentityKey => 'Generovať kľúč identity'; + String get repeater_regenerateIdentityKey => 'GenerovaÅ¥ kľúč identity'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Generovať nový pár verejných/privátnych kľúčov'; + 'GenerovaÅ¥ nový pár verejných/privátnych kľúčov'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Toto vytvorí nový identitu pre opakovač. Pokračovať?'; + 'Toto vytvorí nový identitu pre opakovač. PokračovaÅ¥?'; @override - String get repeater_eraseFileSystem => 'Vymažať Systémový Reťazec'; + String get repeater_eraseFileSystem => 'VymažaÅ¥ Systémový ReÅ¥azec'; @override String get repeater_eraseFileSystemSubtitle => - 'Formátovať systém opakujúcich sa súborov'; + 'FormátovaÅ¥ systém opakujúcich sa súborov'; @override String get repeater_eraseFileSystemConfirm => - 'VAROVANIE: Toto zmaže všetky dáta na opakovači. To sa nedá zrušiť!'; + 'VAROVANIE: Toto zmaže vÅ¡etky dáta na opakovači. To sa nedá zruÅ¡iÅ¥!'; @override String get repeater_eraseSerialOnly => - 'Odstránenie je dostupné len cez sériové rozhranie.'; + 'Odstránenie je dostupné len cez sériové rozhranie.'; @override String repeater_commandSent(String command) { - return 'Poforovaný príkaz: $command'; + return 'Poforovaný príkaz: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Chyba pri odeslaní príkazu: $error'; + return 'Chyba pri odeslaní príkazu: $error'; } @override - String get repeater_confirm => 'Potvrdiť'; + String get repeater_confirm => 'PotvrdiÅ¥'; @override - String get repeater_settingsSaved => 'Nastavenia boli uložené úspešne.'; + String get repeater_settingsSaved => 'Nastavenia boli uložené úspeÅ¡ne.'; @override String repeater_errorSavingSettings(String error) { - return 'Chyba pri ukladaní nastavení: $error'; + return 'Chyba pri ukladaní nastavení: $error'; } @override - String get repeater_refreshBasicSettings => 'Obnoviť základné nastavenia'; + String get repeater_refreshBasicSettings => 'ObnoviÅ¥ základné nastavenia'; @override - String get repeater_refreshRadioSettings => 'Obnoviť Nastavenia Rádií'; + String get repeater_refreshRadioSettings => 'ObnoviÅ¥ Nastavenia Rádií'; @override - String get repeater_refreshTxPower => 'Obnoviť TX napájanie'; + String get repeater_refreshTxPower => 'ObnoviÅ¥ TX napájanie'; @override - String get repeater_refreshLocationSettings => 'Obnoviť Nastavenia Miesta'; + String get repeater_refreshLocationSettings => 'ObnoviÅ¥ Nastavenia Miesta'; @override - String get repeater_refreshPacketForwarding => 'Obnoviť smerovanie paketov'; + String get repeater_refreshPacketForwarding => 'ObnoviÅ¥ smerovanie paketov'; @override - String get repeater_refreshGuestAccess => 'Obnoviť prístup hosťa'; + String get repeater_refreshGuestAccess => 'ObnoviÅ¥ prístup hosÅ¥a'; @override - String get repeater_refreshPrivacyMode => 'Obnoviť Ochranný režim'; + String get repeater_refreshPrivacyMode => 'ObnoviÅ¥ Ochranný režim'; @override String get repeater_refreshAdvertisementSettings => - 'Obnoviť nastavenia reklamy'; + 'ObnoviÅ¥ nastavenia reklamy'; @override String repeater_refreshed(String label) { - return '$label sa znova načítalo'; + return '$label sa znova načítalo'; } @override String repeater_errorRefreshing(String label) { - return 'Chyba pri obnovení $label'; + return 'Chyba pri obnovení $label'; } @override String get repeater_cliTitle => 'Opakovacia CLI'; @override - String get repeater_debugNextCommand => 'Oprava Nasledujúceho Príkaz'; + String get repeater_debugNextCommand => 'Oprava Nasledujúceho Príkaz'; @override String get repeater_commandHelp => 'Pomoc'; @override - String get repeater_clearHistory => 'Vymazať históriu'; + String get repeater_clearHistory => 'VymazaÅ¥ históriu'; @override String get repeater_noCommandsSent => - 'Zatiaľ neboli odeslané žiadne príkazy.'; + 'Zatiaľ neboli odeslané žiadne príkazy.'; @override String get repeater_typeCommandOrUseQuick => - 'Zadajte príkaz nižšie alebo použite rýchle príkazy'; + 'Zadajte príkaz nižšie alebo použite rýchle príkazy'; @override - String get repeater_enterCommandHint => 'Zadajte príkaz...'; + String get repeater_enterCommandHint => 'Zadajte príkaz...'; @override - String get repeater_previousCommand => 'Predchádzajúci príkaz'; + String get repeater_previousCommand => 'Predchádzajúci príkaz'; @override - String get repeater_nextCommand => 'Nasledujúci príkaz'; + String get repeater_nextCommand => 'Nasledujúci príkaz'; @override - String get repeater_enterCommandFirst => 'Zadajte najprv príkaz'; + String get repeater_enterCommandFirst => 'Zadajte najprv príkaz'; @override - String get repeater_cliCommandFrameTitle => 'Rámok Príkaz CLI'; + String get repeater_cliCommandFrameTitle => 'Rámok Príkaz CLI'; @override String repeater_cliCommandError(String error) { @@ -2136,16 +2145,16 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'Zísť meno'; + String get repeater_cliQuickGetName => 'ZísÅ¥ meno'; @override - String get repeater_cliQuickGetRadio => 'Zísť po rádiu'; + String get repeater_cliQuickGetRadio => 'ZísÅ¥ po rádiu'; @override - String get repeater_cliQuickGetTx => 'Zísť TX'; + String get repeater_cliQuickGetTx => 'ZísÅ¥ TX'; @override - String get repeater_cliQuickNeighbors => 'Súsezný'; + String get repeater_cliQuickNeighbors => 'Súsezný'; @override String get repeater_cliQuickVersion => 'Verzia'; @@ -2157,224 +2166,224 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_cliQuickClock => 'Hodiny'; @override - String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; + String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; @override String get repeater_cliHelpReboot => - 'Resetuje zariadenie. (pozor, môže dôjsť k \'Timeoutu\', čo je normálne)'; + 'Resetuje zariadenie. (pozor, môže dôjsÅ¥ k \'Timeoutu\', čo je normálne)'; @override String get repeater_cliHelpClock => - 'Zobrazuje aktuálny čas podľa hodiniek zariadenia.'; + 'Zobrazuje aktuálny čas podľa hodiniek zariadenia.'; @override String get repeater_cliHelpPassword => - 'Nastaví nový administrátorský prístupový údaj pre zariadenie.'; + 'Nastaví nový administrátorský prístupový údaj pre zariadenie.'; @override String get repeater_cliHelpVersion => - 'Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.'; + 'Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.'; @override String get repeater_cliHelpClearStats => - 'Resetuje rôzne štatistické počítadlá na nulu.'; + 'Resetuje rôzne Å¡tatistické počítadlá na nulu.'; @override - String get repeater_cliHelpSetAf => 'Nastavuje časový faktor.'; + String get repeater_cliHelpSetAf => 'Nastavuje časový faktor.'; @override String get repeater_cliHelpSetTx => - 'Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reštart na aplikáciu)'; + 'Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reÅ¡tart na aplikáciu)'; @override String get repeater_cliHelpSetRepeat => - 'Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.'; + 'Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Server miestnosti) Ak je \'zapnuté\', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielať správu do miestnosti. (iba čítať).'; + '(Server miestnosti) Ak je \'zapnuté\', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielaÅ¥ správu do miestnosti. (iba čítaÅ¥).'; @override String get repeater_cliHelpSetFloodMax => - 'Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)'; + 'Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)'; @override String get repeater_cliHelpSetIntThresh => - 'Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.'; + 'Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Nastavuje interval na reštartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.'; + 'Nastavuje interval na reÅ¡tartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.'; @override String get repeater_cliHelpSetMultiAcks => - 'Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".'; + 'Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".'; @override String get repeater_cliHelpSetAdvertInterval => - 'Nastavuje interval časovača v minútach na odošle miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.'; + 'Nastavuje interval časovača v minútach na odoÅ¡le miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.'; + 'Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.'; @override String get repeater_cliHelpSetGuestPassword => - 'Nastavuje/aktualizuje heslo hosťa. (pre opakované pripojenia môžu hosťovské prihlásenia posielať požadanie \"Get Stats\")'; + 'Nastavuje/aktualizuje heslo hosÅ¥a. (pre opakované pripojenia môžu hosÅ¥ovské prihlásenia posielaÅ¥ požadanie \"Get Stats\")'; @override - String get repeater_cliHelpSetName => 'Nastaví názov reklamy.'; + String get repeater_cliHelpSetName => 'Nastaví názov reklamy.'; @override String get repeater_cliHelpSetLat => - 'Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)'; + 'Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)'; @override String get repeater_cliHelpSetLon => - 'Nastavuje longitudinu reklamnej mapy. (desatinné stupne)'; + 'Nastavuje longitudinu reklamnej mapy. (desatinné stupne)'; @override String get repeater_cliHelpSetRadio => - 'Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.'; + 'Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.'; @override String get repeater_cliHelpSetRxDelay => - 'Nastavenia (experimentálne) základné (musi byť > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.'; + 'Nastavenia (experimentálne) základné (musi byÅ¥ > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.'; @override String get repeater_cliHelpSetTxDelay => - 'Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiť pravdepodobnosť kolízii).'; + 'Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiÅ¥ pravdepodobnosÅ¥ kolízii).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.'; + 'Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Aktivovať/Zatvárať most.'; + String get repeater_cliHelpSetBridgeEnabled => 'AktivovaÅ¥/ZatváraÅ¥ most.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Nastaviť odklad pred retransmisiou paketov.'; + 'NastaviÅ¥ odklad pred retransmisiou paketov.'; @override String get repeater_cliHelpSetBridgeSource => - 'Zvolte, či bude most retransmitovať prijaté alebo vysielané balíčky.'; + 'Zvolte, či bude most retransmitovaÅ¥ prijaté alebo vysielané balíčky.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Nastavte sériový link baudrate pre rs232 mosty.'; + 'Nastavte sériový link baudrate pre rs232 mosty.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Nastaviť tajomstvo mosta pre eshnow mosty.'; + 'NastaviÅ¥ tajomstvo mosta pre eshnow mosty.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).'; + 'Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).'; @override String get repeater_cliHelpTempRadio => - 'Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).'; + 'Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).'; @override String get repeater_cliHelpSetPerm => - 'Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).'; + 'Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).'; @override String get repeater_cliHelpGetBridgeType => - 'Zísť typ mosta: žiadny, rs232, espnow'; + 'ZísÅ¥ typ mosta: žiadny, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Začína protokolovanie balíkov do systému súborov.'; + 'Začína protokolovanie balíkov do systému súborov.'; @override String get repeater_cliHelpLogStop => - 'Zastaví protokolovanie paketov do systémového súboru.'; + 'Zastaví protokolovanie paketov do systémového súboru.'; @override String get repeater_cliHelpLogErase => - 'Odstráni záznamy z balíkov z systému súborov.'; + 'Odstráni záznamy z balíkov z systému súborov.'; @override String get repeater_cliHelpNeighbors => - 'Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4'; + 'Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.'; + 'Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.'; @override String get repeater_cliHelpRegion => - '(len sériál) Zobrazuje všetky definované regióny a aktuálne povolenia pre povodňové situácie.'; + '(len sériál) Zobrazuje vÅ¡etky definované regióny a aktuálne povolenia pre povodňové situácie.'; @override String get repeater_cliHelpRegionLoad => - 'Poznámka: toto je špeciálna multi-príkázová inštancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.'; + 'Poznámka: toto je Å¡peciálna multi-príkázová inÅ¡tancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.'; @override String get repeater_cliHelpRegionGet => - 'Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) \'F\'\"'; + 'Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Pridá alebo aktualizuje definíciu regiónu s daným menom.'; + 'Pridá alebo aktualizuje definíciu regiónu s daným menom.'; @override String get repeater_cliHelpRegionRemove => - 'Odstráni definíciu oblasti s daným názvom. (musí zodpovedať presne a nemala by mať podoblasti)'; + 'Odstráni definíciu oblasti s daným názvom. (musí zodpovedaÅ¥ presne a nemala by maÅ¥ podoblasti)'; @override String get repeater_cliHelpRegionAllowf => - 'Nastavuje povolenie \'P\'lávu pre zadanú oblasť. (\'\' pre globálny/dedičský rozsah)'; + 'Nastavuje povolenie \'P\'lávu pre zadanú oblasÅ¥. (\'\' pre globálny/dedičský rozsah)'; @override String get repeater_cliHelpRegionDenyf => - 'Odstráni povolenie \'F\'lood\' pre zadanú oblasť. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používať na globálnom/dedskom rozsahu!!).'; + 'Odstráni povolenie \'F\'lood\' pre zadanú oblasÅ¥. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používaÅ¥ na globálnom/dedskom rozsahu!!).'; @override String get repeater_cliHelpRegionHome => - 'Odpovedá s aktuálnou \'domovskou\' oblasťou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)'; + 'Odpovedá s aktuálnou \'domovskou\' oblasÅ¥ou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)'; @override - String get repeater_cliHelpRegionHomeSet => 'Nastaví \'domovskú\' oblasť.'; + String get repeater_cliHelpRegionHomeSet => 'Nastaví \'domovskú\' oblasÅ¥.'; @override String get repeater_cliHelpRegionSave => - 'Uloží zoznam/mapu regiónov do úložiska.'; + 'Uloží zoznam/mapu regiónov do úložiska.'; @override String get repeater_cliHelpGps => - 'Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.'; + 'Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.'; @override - String get repeater_cliHelpGpsOnOff => 'Prepínač stavu GPS napájania.'; + String get repeater_cliHelpGpsOnOff => 'Prepínač stavu GPS napájania.'; @override String get repeater_cliHelpGpsSync => - 'Synchronizuje čas uzla s GPS hodinami.'; + 'Synchronizuje čas uzla s GPS hodinami.'; @override String get repeater_cliHelpGpsSetLoc => - 'Nastaví polohu uzla na GPS súradnice a uloží preferencie.'; + 'Nastaví polohu uzla na GPS súradnice a uloží preferencie.'; @override String get repeater_cliHelpGpsAdvert => - 'Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľať: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach'; + 'Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľaÅ¥: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach'; @override String get repeater_cliHelpGpsAdvertSet => - 'Nastavuje konfiguráciu reklamy na zadané miesto.'; + 'Nastavuje konfiguráciu reklamy na zadané miesto.'; @override - String get repeater_commandsListTitle => 'Zoznam príkazov'; + String get repeater_commandsListTitle => 'Zoznam príkazov'; @override String get repeater_commandsListNote => - 'Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".'; + 'Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".'; @override - String get repeater_general => 'Obecné'; + String get repeater_general => 'Obecné'; @override String get repeater_settingsCategory => 'Nastavenia'; @@ -2383,50 +2392,51 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_bridge => 'Most'; @override - String get repeater_logging => 'Záznamy'; + String get repeater_logging => 'Záznamy'; @override - String get repeater_neighborsRepeaterOnly => 'Súseznýci (iba opakovač)'; + String get repeater_neighborsRepeaterOnly => 'Súseznýci (iba opakovač)'; @override String get repeater_regionManagementRepeaterOnly => - 'Správa regiónov (iba opakovač)'; + 'Správa regiónov (iba opakovač)'; @override String get repeater_regionNote => - 'Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.'; + 'Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.'; @override - String get repeater_gpsManagement => 'Správa GPS'; + String get repeater_gpsManagement => 'Správa GPS'; @override String get repeater_gpsNote => - 'GPS príkaz bol zavádzaný na riadenie lokalitných tém.'; + 'GPS príkaz bol zavádzaný na riadenie lokalitných tém.'; @override - String get telemetry_receivedData => 'Obdolené Telemetrické dáta'; + String get telemetry_receivedData => 'Obdolené Telemetrické dáta'; @override - String get telemetry_requestTimeout => 'Požiadavka telemetrie zlyhala.'; + String get telemetry_requestTimeout => 'Požiadavka telemetrie zlyhala.'; @override String telemetry_errorLoading(String error) { - return 'Chyba pri načítaní telemetrie: $error'; + return 'Chyba pri načítaní telemetrie: $error'; } @override - String get telemetry_noData => 'Nejsú dostupné žiadne údaje z telemetrie.'; + String get telemetry_noData => + 'Nejsú dostupné žiadne údaje z telemetrie.'; @override String telemetry_channelTitle(int channel) { - return 'Kanál $channel'; + return 'Kanál $channel'; } @override - String get telemetry_batteryLabel => 'Batéria'; + String get telemetry_batteryLabel => 'Batéria'; @override - String get telemetry_voltageLabel => 'Napätie'; + String get telemetry_voltageLabel => 'Napätie'; @override String get telemetry_mcuTemperatureLabel => 'MCU teplota'; @@ -2435,7 +2445,7 @@ class AppLocalizationsSk extends AppLocalizations { String get telemetry_temperatureLabel => 'Teplota'; @override - String get telemetry_currentLabel => 'Aktuálne'; + String get telemetry_currentLabel => 'Aktuálne'; @override String telemetry_batteryValue(int percent, String volts) { @@ -2454,61 +2464,62 @@ class AppLocalizationsSk extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Obdielo dáta suseda'; + String get neighbors_receivedData => 'Obdielo dáta suseda'; @override - String get neighbors_requestTimedOut => 'Súďia žiadajú o časové ukončenie.'; + String get neighbors_requestTimedOut => + 'Súďia žiadajú o časové ukončenie.'; @override String neighbors_errorLoading(String error) { - return 'Chyba pri načítaní susedov: $error'; + return 'Chyba pri načítaní susedov: $error'; } @override - String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná'; + String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná'; @override String get neighbors_noData => - 'Nie je dostupná žiadna informácia o susedoch.'; + 'Nie je dostupná žiadna informácia o susedoch.'; @override String neighbors_unknownContact(String pubkey) { - return 'Neznáma $pubkey'; + return 'Neznáma $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Počuli sme to: $time dozadu'; + return 'Počuli sme to: $time dozadu'; } @override - String get channelPath_title => 'Cesta balíka'; + String get channelPath_title => 'Cesta balíka'; @override - String get channelPath_viewMap => 'Zobraziť mapu'; + String get channelPath_viewMap => 'ZobraziÅ¥ mapu'; @override - String get channelPath_otherObservedPaths => 'Ostatné pozorovacie cesty'; + String get channelPath_otherObservedPaths => 'Ostatné pozorovacie cesty'; @override - String get channelPath_repeaterHops => 'Skoky opakovača'; + String get channelPath_repeaterHops => 'Skoky opakovača'; @override String get channelPath_noHopDetails => - 'Podrobnosti o balíčku zatiaľ nie sú dostupné.'; + 'Podrobnosti o balíčku zatiaľ nie sú dostupné.'; @override - String get channelPath_messageDetails => 'Podrobnosti o zprávach'; + String get channelPath_messageDetails => 'Podrobnosti o zprávach'; @override - String get channelPath_senderLabel => 'Posielateľ'; + String get channelPath_senderLabel => 'Posielateľ'; @override - String get channelPath_timeLabel => 'Čas'; + String get channelPath_timeLabel => 'ÄŒas'; @override String get channelPath_repeatsLabel => 'Opakovanie'; @@ -2519,15 +2530,15 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get channelPath_observedLabel => 'Pozorované'; + String get channelPath_observedLabel => 'Pozorované'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Sledovaný postup $index • $hops'; + return 'Sledovaný postup $index • $hops'; } @override - String get channelPath_noLocationData => 'Žiadne údaje o polohe'; + String get channelPath_noLocationData => 'Žiadne údaje o polohe'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2540,10 +2551,10 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Neznáme'; + String get channelPath_unknownPath => 'Neznáme'; @override - String get channelPath_floodPath => 'Povodňová'; + String get channelPath_floodPath => 'Povodňová'; @override String get channelPath_directPath => 'Priamo'; @@ -2563,161 +2574,162 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Pre túto cestu nie je dostupných žiadne polohy opakovačov.'; + 'Pre túto cestu nie je dostupných žiadne polohy opakovačov.'; @override String channelPath_primaryPath(int index) { - return 'Cesta $index (Hlavná)'; + return 'Cesta $index (Hlavná)'; } @override String get channelPath_pathLabelTitle => 'Cesta'; @override - String get channelPath_observedPathHeader => 'Sledovaná cesta'; + String get channelPath_observedPathHeader => 'Sledovaná cesta'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Pre toto balíček nie sú dostupné údaje o skokoch.'; + 'Pre toto balíček nie sú dostupné údaje o skokoch.'; @override - String get channelPath_unknownRepeater => 'Neznáme opakovače'; + String get channelPath_unknownRepeater => 'Neznáme opakovače'; @override String get community_title => 'Komunita'; @override - String get community_create => 'Vytvoriť komunitu'; + String get community_create => 'VytvoriÅ¥ komunitu'; @override String get community_createDesc => - 'Vytvorte novú komunitu a zdieľajte cez QR kód.'; + 'Vytvorte novú komunitu a zdieľajte cez QR kód.'; @override - String get community_join => 'Pripojiť'; + String get community_join => 'PripojiÅ¥'; @override - String get community_joinTitle => 'Pripojiť sa k spoločenstvu'; + String get community_joinTitle => 'PripojiÅ¥ sa k spoločenstvu'; @override String community_joinConfirmation(String name) { - return 'Chceš sa pridať do komunity \"$name\"?'; + return 'ChceÅ¡ sa pridaÅ¥ do komunity \"$name\"?'; } @override - String get community_scanQr => 'Skontrolujte komunitný QR kód'; + String get community_scanQr => 'Skontrolujte komunitný QR kód'; @override String get community_scanInstructions => - 'Zamerte kameru na komunitný QR kód.'; + 'Zamerte kameru na komunitný QR kód.'; @override - String get community_showQr => 'Zobraziť QR kód'; + String get community_showQr => 'ZobraziÅ¥ QR kód'; @override - String get community_publicChannel => 'Komunita verejná'; + String get community_publicChannel => 'Komunita verejná'; @override - String get community_hashtagChannel => 'Komunitný Hashtag'; + String get community_hashtagChannel => 'Komunitný Hashtag'; @override String get community_name => 'Komunita'; @override - String get community_enterName => 'Zadajte názov komunity'; + String get community_enterName => 'Zadajte názov komunity'; @override String community_created(String name) { - return 'Komunita \"$name\" vytvorená'; + return 'Komunita \"$name\" vytvorená'; } @override String community_joined(String name) { - return 'Pripojená komunita \"$name\"'; + return 'Pripojená komunita \"$name\"'; } @override - String get community_qrTitle => 'Zdieľť komunitu'; + String get community_qrTitle => 'Zdieľť komunitu'; @override String community_qrInstructions(String name) { - return 'Skenejte tento QR kód, aby ste sa pripojili k $name.'; + return 'Skenejte tento QR kód, aby ste sa pripojili k $name.'; } @override String get community_hashtagPrivacyHint => - 'Hashtagové kanály komunity sú prístupné len členom komunity'; + 'Hashtagové kanály komunity sú prístupné len členom komunity'; @override - String get community_invalidQrCode => 'Neplatná QR kód komunity.'; + String get community_invalidQrCode => 'Neplatná QR kód komunity.'; @override - String get community_alreadyMember => 'Už ste členom.'; + String get community_alreadyMember => 'Už ste členom.'; @override String community_alreadyMemberMessage(String name) { - return 'Vy ste už členom \"$name\".'; + return 'Vy ste už členom \"$name\".'; } @override - String get community_addPublicChannel => 'Pridať verejný komunikačný kanál'; + String get community_addPublicChannel => + 'PridaÅ¥ verejný komunikačný kanál'; @override String get community_addPublicChannelHint => - 'Automaticky prida verejný kanál pre túto komunitu.'; + 'Automaticky prida verejný kanál pre túto komunitu.'; @override String get community_noCommunities => - 'Zatiaľ ste sa nepripojili k žiadnej komunite'; + 'Zatiaľ ste sa nepripojili k žiadnej komunite'; @override String get community_scanOrCreate => - 'Skene QR kód alebo vytvor komunitu na začiatok.'; + 'Skene QR kód alebo vytvor komunitu na začiatok.'; @override - String get community_manageCommunities => 'Spravovať komunity'; + String get community_manageCommunities => 'SpravovaÅ¥ komunity'; @override String get community_delete => 'Nechajte komunitu'; @override String community_deleteConfirm(String name) { - return 'Opustiť \"$name\"?'; + return 'OpustiÅ¥ \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.'; + return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.'; } @override String community_deleted(String name) { - return 'Opustená komunita \"$name\"'; + return 'Opustená komunita \"$name\"'; } @override - String get community_regenerateSecret => 'Zobraziť nový tajný kód'; + String get community_regenerateSecret => 'ZobraziÅ¥ nový tajný kód'; @override String community_regenerateSecretConfirm(String name) { - return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.'; + return 'Znovu vygenerovaÅ¥ tajný kľúč pre \"$name\"? VÅ¡etci členovia budú musieÅ¥ skanovaÅ¥ nový QR kód, aby mohli nadviazaÅ¥ komunikáciu.'; } @override - String get community_regenerate => 'Znovu vygenerovať'; + String get community_regenerate => 'Znovu vygenerovaÅ¥'; @override String community_secretRegenerated(String name) { - return 'Záznam pre \"$name\" bol regenerovaný tajne'; + return 'Záznam pre \"$name\" bol regenerovaný tajne'; } @override - String get community_updateSecret => 'Aktualizovať tajné heslo'; + String get community_updateSecret => 'AktualizovaÅ¥ tajné heslo'; @override String community_secretUpdated(String name) { @@ -2726,31 +2738,32 @@ class AppLocalizationsSk extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"'; + return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"'; } @override - String get community_addHashtagChannel => 'Pridať komunitný hashtag'; + String get community_addHashtagChannel => 'PridaÅ¥ komunitný hashtag'; @override String get community_addHashtagChannelDesc => - 'Pridajte hashtagový kanál pre túto komunitu.'; + 'Pridajte hashtagový kanál pre túto komunitu.'; @override String get community_selectCommunity => 'Vyberte komunitu'; @override - String get community_regularHashtag => 'Zvyčajný hashtag'; + String get community_regularHashtag => 'Zvyčajný hashtag'; @override String get community_regularHashtagDesc => - 'Veľký hashtag (ktočokoľvek sa môže pridať)'; + 'Veľký hashtag (ktočokoľvek sa môže pridaÅ¥)'; @override - String get community_communityHashtag => 'Komunitný Hashtag'; + String get community_communityHashtag => 'Komunitný Hashtag'; @override - String get community_communityHashtagDesc => 'Špecifické pre členov komunity'; + String get community_communityHashtagDesc => + 'Å pecifické pre členov komunity'; @override String community_forCommunity(String name) { @@ -2758,16 +2771,16 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get listFilter_tooltip => 'Filtrovať a triediť'; + String get listFilter_tooltip => 'FiltrovaÅ¥ a triediÅ¥'; @override - String get listFilter_sortBy => 'Triediť podľa'; + String get listFilter_sortBy => 'TriediÅ¥ podľa'; @override - String get listFilter_latestMessages => 'Posledné správy'; + String get listFilter_latestMessages => 'Posledné správy'; @override - String get listFilter_heardRecently => 'Nedávno počuli.'; + String get listFilter_heardRecently => 'Nedávno počuli.'; @override String get listFilter_az => 'A-Z'; @@ -2776,31 +2789,31 @@ class AppLocalizationsSk extends AppLocalizations { String get listFilter_filters => 'Filtre'; @override - String get listFilter_all => 'Všetko'; + String get listFilter_all => 'VÅ¡etko'; @override - String get listFilter_favorites => 'Obľúbené'; + String get listFilter_favorites => 'Obľúbené'; @override - String get listFilter_addToFavorites => 'Pridaj do obľúbených'; + String get listFilter_addToFavorites => 'Pridaj do obľúbených'; @override - String get listFilter_removeFromFavorites => 'Odstrániť z označení'; + String get listFilter_removeFromFavorites => 'OdstrániÅ¥ z označení'; @override - String get listFilter_users => 'Používatelia'; + String get listFilter_users => 'Používatelia'; @override - String get listFilter_repeaters => 'Opakovadlá'; + String get listFilter_repeaters => 'Opakovadlá'; @override - String get listFilter_roomServers => 'Servéry miestnosti'; + String get listFilter_roomServers => 'Servéry miestnosti'; @override - String get listFilter_unreadOnly => 'Nezaregistrované len'; + String get listFilter_unreadOnly => 'Nezaregistrované len'; @override - String get listFilter_newGroup => 'Nová skupina'; + String get listFilter_newGroup => 'Nová skupina'; @override String get pathTrace_you => 'Vy'; @@ -2809,49 +2822,50 @@ class AppLocalizationsSk extends AppLocalizations { String get pathTrace_failed => 'Sledovanie cesty zlyhalo.'; @override - String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.'; + String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.'; @override - String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.'; + String get pathTrace_refreshTooltip => 'ObnoviÅ¥ Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Jedna alebo viac chmeľov chýba lokalita!'; + 'Jedna alebo viac chmeľov chýba lokalita!'; @override - String get pathTrace_clearTooltip => 'Zmazať cestu'; + String get pathTrace_clearTooltip => 'ZmazaÅ¥ cestu'; @override - String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.'; + String get losSelectStartEnd => + 'Vyberte počiatočný a koncový uzol pre LOS.'; @override String losRunFailed(String error) { - return 'Kontrola priamej viditeľnosti zlyhala: $error'; + return 'Kontrola priamej viditeľnosti zlyhala: $error'; } @override - String get losClearAllPoints => 'Vymazať všetky body'; + String get losClearAllPoints => 'VymazaÅ¥ vÅ¡etky body'; @override String get losRunToViewElevationProfile => - 'Ak chcete zobraziť výškový profil, spustite LOS'; + 'Ak chcete zobraziÅ¥ výškový profil, spustite LOS'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body'; + 'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body'; @override - String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia'; + String get losShowDisplayNodes => 'ZobraziÅ¥ uzly zobrazenia'; @override - String get losCustomPoints => 'Vlastné body'; + String get losCustomPoints => 'Vlastné body'; @override String losCustomPointLabel(int index) { - return 'Vlastné $index'; + return 'Vlastné $index'; } @override @@ -2862,19 +2876,19 @@ class AppLocalizationsSk extends AppLocalizations { @override String losAntennaA(String value, String unit) { - return 'Anténa A: $value $unit'; + return 'Anténa A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Anténa B: $value $unit'; + return 'Anténa B: $value $unit'; } @override String get losRun => 'Spustite LOS'; @override - String get losNoElevationData => 'Žiadne údaje o nadmorskej výške'; + String get losNoElevationData => 'Žiadne údaje o nadmorskej výške'; @override String losProfileClear( @@ -2883,7 +2897,7 @@ class AppLocalizationsSk extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit'; + return '$distance $distanceUnit, vymazaÅ¥ LOS, min. vôľa $clearance $heightUnit'; } @override @@ -2893,61 +2907,61 @@ class AppLocalizationsSk extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, blokovaný $obstruction $heightUnit'; + return '$distance $distanceUnit, blokovaný $obstruction $heightUnit'; } @override String get losStatusChecking => 'LOS: kontrolujem...'; @override - String get losStatusNoData => 'LOS: žiadne údaje'; + String get losStatusNoData => 'LOS: žiadne údaje'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme'; + return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme'; } @override String get losErrorElevationUnavailable => - 'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.'; + 'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.'; @override String get losErrorInvalidInput => - 'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.'; + 'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.'; @override - String get losRenameCustomPoint => 'Premenovať vlastný bod'; + String get losRenameCustomPoint => 'PremenovaÅ¥ vlastný bod'; @override - String get losPointName => 'Názov bodu'; + String get losPointName => 'Názov bodu'; @override - String get losShowPanelTooltip => 'Zobraziť panel LOS'; + String get losShowPanelTooltip => 'ZobraziÅ¥ panel LOS'; @override - String get losHidePanelTooltip => 'Skryť panel LOS'; + String get losHidePanelTooltip => 'SkryÅ¥ panel LOS'; @override String get losElevationAttribution => - 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; + 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Rádiový horizont'; + String get losLegendRadioHorizon => 'Rádiový horizont'; @override - String get losLegendLosBeam => 'Priama viditeľnosť'; + String get losLegendLosBeam => 'Priama viditeľnosÅ¥'; @override - String get losLegendTerrain => 'Terén'; + String get losLegendTerrain => 'Terén'; @override String get losFrequencyLabel => 'Frekvencia'; @override - String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu'; + String get losFrequencyInfoTooltip => 'ZobraziÅ¥ podrobnosti výpočtu'; @override - String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; + String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; @override String losFrequencyDialogDescription( @@ -2956,20 +2970,20 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; + return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; } @override - String get contacts_pathTrace => 'Sledovanie lúčov'; + String get contacts_pathTrace => 'Sledovanie lúčov'; @override - String get contacts_ping => 'Pingovať'; + String get contacts_ping => 'PingovaÅ¥'; @override - String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču'; + String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču'; @override - String get contacts_repeaterPing => 'Pingovať opakovač'; + String get contacts_repeaterPing => 'PingovaÅ¥ opakovač'; @override String get contacts_roomPathTrace => 'Sledovanie cesty k serveru miestnosti'; @@ -2978,46 +2992,48 @@ class AppLocalizationsSk extends AppLocalizations { String get contacts_roomPing => 'Ping server miestnosti'; @override - String get contacts_chatTraceRoute => 'Sledovať trasu lúča'; + String get contacts_chatTraceRoute => 'SledovaÅ¥ trasu lúča'; @override String contacts_pathTraceTo(String name) { - return 'Sledovať trasu k $name'; + return 'SledovaÅ¥ trasu k $name'; } @override - String get contacts_clipboardEmpty => 'Schránka je prázdna.'; + String get contacts_clipboardEmpty => 'Schránka je prázdna.'; @override - String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje'; + String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje'; @override - String get contacts_contactImported => 'Kontakt bol importovaný.'; + String get contacts_contactImported => 'Kontakt bol importovaný.'; @override String get contacts_contactImportFailed => - 'Kontakt sa nepodarilo importovať.'; + 'Kontakt sa nepodarilo importovaÅ¥.'; @override - String get contacts_zeroHopAdvert => 'Inzerát Zero Hop'; + String get contacts_zeroHopAdvert => 'Inzerát Zero Hop'; @override - String get contacts_floodAdvert => 'Inzerát povodní'; + String get contacts_floodAdvert => 'Inzerát povodní'; @override - String get contacts_copyAdvertToClipboard => 'Kopírovať reklamu do schránky'; + String get contacts_copyAdvertToClipboard => + 'KopírovaÅ¥ reklamu do schránky'; @override - String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky'; + String get contacts_addContactFromClipboard => 'PridaÅ¥ kontakt z schránky'; @override - String get contacts_ShareContact => 'Kopírovať kontakt do schránky'; + String get contacts_ShareContact => 'KopírovaÅ¥ kontakt do schránky'; @override - String get contacts_ShareContactZeroHop => 'Zdieľať kontakt cez inzerát'; + String get contacts_ShareContactZeroHop => 'ZdieľaÅ¥ kontakt cez inzerát'; @override - String get contacts_zeroHopContactAdvertSent => 'Poslal kontakt cez inzerát.'; + String get contacts_zeroHopContactAdvertSent => + 'Poslal kontakt cez inzerát.'; @override String get contacts_zeroHopContactAdvertFailed => @@ -3025,11 +3041,11 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_contactAdvertCopied => - 'Inzerát bol skopírovaný do schránky.'; + 'Inzerát bol skopírovaný do schránky.'; @override String get contacts_contactAdvertCopyFailed => - 'Kopírovanie inzerátu do schránky zlyhalo.'; + 'Kopírovanie inzerátu do schránky zlyhalo.'; @override String get notification_activityTitle => 'Aktivita MeshCore'; @@ -3039,9 +3055,9 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'správ', - few: 'správy', - one: 'správa', + other: 'správ', + few: 'správy', + one: 'správa', ); return '$count $_temp0'; } @@ -3051,9 +3067,9 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'správ kanálu', - few: 'správy kanálu', - one: 'správa kanálu', + other: 'správ kanálu', + few: 'správy kanálu', + one: 'správa kanálu', ); return '$count $_temp0'; } @@ -3063,77 +3079,77 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'nových uzlov', - few: 'nové uzly', - one: 'nový uzol', + other: 'nových uzlov', + few: 'nové uzly', + one: 'nový uzol', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Nový $contactType objavený'; + return 'Nový $contactType objavený'; } @override - String get notification_receivedNewMessage => 'Prijatá nová správa'; + String get notification_receivedNewMessage => 'Prijatá nová správa'; @override String get settings_gpxExportRepeaters => - 'Exportovať repeater / server miestnosti do GPX'; + 'ExportovaÅ¥ repeater / server miestnosti do GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Exportuje repeater / roomserver s lokalitou do súboru GPX.'; + 'Exportuje repeater / roomserver s lokalitou do súboru GPX.'; @override String get settings_gpxExportContacts => 'Export sprievodcov do GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exportuje sprievodcov s umiestnením do súboru GPX.'; + 'Exportuje sprievodcov s umiestnením do súboru GPX.'; @override - String get settings_gpxExportAll => 'Exportovať všetky kontakty do GPX'; + String get settings_gpxExportAll => 'ExportovaÅ¥ vÅ¡etky kontakty do GPX'; @override String get settings_gpxExportAllSubtitle => - 'Exportuje všetky kontakty s lokalitou do súboru GPX.'; + 'Exportuje vÅ¡etky kontakty s lokalitou do súboru GPX.'; @override - String get settings_gpxExportSuccess => 'Úspešne exportovaný súbor GPX.'; + String get settings_gpxExportSuccess => 'ÚspeÅ¡ne exportovaný súbor GPX.'; @override - String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.'; + String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.'; @override String get settings_gpxExportNotAvailable => - 'Nie je podporované na vašom zariadení/operáciomnom systéme'; + 'Nie je podporované na vaÅ¡om zariadení/operáciomnom systéme'; @override - String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.'; + String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.'; @override String get settings_gpxExportRepeatersRoom => - 'Umiestnenia opakovačov a serverov miestností'; + 'Umiestnenia opakovačov a serverov miestností'; @override - String get settings_gpxExportChat => 'Lokácie sprievodcov'; + String get settings_gpxExportChat => 'Lokácie sprievodcov'; @override - String get settings_gpxExportAllContacts => 'Všetky kontaktné lokality'; + String get settings_gpxExportAllContacts => 'VÅ¡etky kontaktné lokality'; @override String get settings_gpxExportShareText => - 'Mapové údaje exportované z meshcore-open'; + 'Mapové údaje exportované z meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open export dát GPX mapových údajov'; + 'meshcore-open export dát GPX mapových údajov'; @override - String get snrIndicator_nearByRepeaters => 'Miestne opakovače'; + String get snrIndicator_nearByRepeaters => 'Miestne opakovače'; @override - String get snrIndicator_lastSeen => 'Naposledy videný'; + String get snrIndicator_lastSeen => 'Naposledy videný'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 8969898..ddc26ac 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -21,13 +21,13 @@ class AppLocalizationsSl extends AppLocalizations { String get nav_map => 'Karta'; @override - String get common_cancel => 'Prekliči'; + String get common_cancel => 'Prekliči'; @override String get common_ok => 'V redu'; @override - String get common_connect => 'Poveži se'; + String get common_connect => 'Poveži se'; @override String get common_unknownDevice => 'Nepoznano naprave'; @@ -81,7 +81,7 @@ class AppLocalizationsSl extends AppLocalizations { String get common_remove => 'Izbrisati'; @override - String get common_enable => 'Omogoči'; + String get common_enable => 'Omogoči'; @override String get common_disable => 'Izklopiti'; @@ -90,10 +90,10 @@ class AppLocalizationsSl extends AppLocalizations { String get common_reboot => 'Ponoviti'; @override - String get common_loading => 'Naložanje...'; + String get common_loading => 'Naložanje...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Izberite svoj način povezave.'; - - @override - String get connectionChoiceSubtitle => - 'Izberite, kako želite dostopati do svojega naprave MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -122,11 +115,11 @@ class AppLocalizationsSl extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Povežite preko USB'; + String get usbScreenTitle => 'Povežite preko USB'; @override String get usbScreenSubtitle => - 'Izberite zaznano serijsko napravo in se neposredno povežite z vašim MeshCore-om.'; + 'Izberite zaznano serijsko napravo in se neposredno povežite z vaÅ¡im MeshCore-om.'; @override String get usbScreenStatus => 'Izberite USB naprave.'; @@ -137,7 +130,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get usbScreenEmptyState => - 'Niti en USB naprave niso najdeni. Povežite eno in posodobite.'; + 'Niti en USB naprave niso najdeni. Povežite eno in posodobite.'; @override String get scanner_scanning => 'Skeniram za naprave...'; @@ -161,15 +154,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_tapToScan => - 'Nagneš na skeniranje za najdene naprave MeshCore.'; + 'NagneÅ¡ na skeniranje za najdene naprave MeshCore.'; @override String scanner_connectionFailed(String error) { - return 'Pošlo je z povezavo: $error'; + return 'PoÅ¡lo je z povezavo: $error'; } @override - String get scanner_stop => 'Prekliči'; + String get scanner_stop => 'Prekliči'; @override String get scanner_scan => 'Skeniraj'; @@ -179,7 +172,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_bluetoothOffMessage => - 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; + 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; @override String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome'; @@ -189,7 +182,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.'; @override - String get scanner_enableBluetooth => 'Omogočite Bluetooth'; + String get scanner_enableBluetooth => 'Omogočite Bluetooth'; @override String get device_quickSwitch => 'Hitro preklop'; @@ -208,10 +201,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_appSettingsSubtitle => - 'Obveščanja, sporoščanje in zemljevidi.'; + 'Obveščanja, sporoščanje in zemljevidi.'; @override - String get settings_nodeSettings => 'Nastavitev časa'; + String get settings_nodeSettings => 'Nastavitev časa'; @override String get settings_nodeName => 'Ime node-a'; @@ -230,7 +223,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_radioSettingsSubtitle => - 'Frekvenca, moč, razširitveni faktor'; + 'Frekvenca, moč, razÅ¡iritveni faktor'; @override String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene'; @@ -245,18 +238,18 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_locationUpdated => 'Lokacija posodobljena'; @override - String get settings_locationBothRequired => 'Vnesite širino in dolžino.'; + String get settings_locationBothRequired => 'Vnesite Å¡irino in dolžino.'; @override String get settings_locationInvalid => - 'Neveljavna zemeljska širina ali dolžina.'; + 'Neveljavna zemeljska Å¡irina ali dolžina.'; @override - String get settings_locationGPSEnable => 'Omogoči GPS'; + String get settings_locationGPSEnable => 'Omogoči GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Omogoči samodejno posodabljanje lokacije z GPS-jem.'; + 'Omogoči samodejno posodabljanje lokacije z GPS-jem.'; @override String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)'; @@ -266,10 +259,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.'; @override - String get settings_latitude => 'Širina'; + String get settings_latitude => 'Å irina'; @override - String get settings_longitude => 'Dolžina'; + String get settings_longitude => 'Dolžina'; @override String get settings_privacyMode => 'Zasebnost'; @@ -279,19 +272,19 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_privacyModeToggle => - 'Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.'; + 'Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.'; @override - String get settings_privacyModeEnabled => 'Privatni način je omogočen.'; + String get settings_privacyModeEnabled => 'Privatni način je omogočen.'; @override - String get settings_privacyModeDisabled => 'Privatni način je onemogočen.'; + String get settings_privacyModeDisabled => 'Privatni način je onemogočen.'; @override String get settings_actions => 'Akcije'; @override - String get settings_sendAdvertisement => 'Pošlji Oglas'; + String get settings_sendAdvertisement => 'PoÅ¡lji Oglas'; @override String get settings_sendAdvertisementSubtitle => @@ -304,33 +297,35 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_syncTime => 'Nastavi uro'; @override - String get settings_syncTimeSubtitle => 'Nastavi uro naprave na čas telefona'; + String get settings_syncTimeSubtitle => + 'Nastavi uro naprave na čas telefona'; @override String get settings_timeSynchronized => 'Ura sinhronizirana'; @override - String get settings_refreshContacts => 'Ponovno obišči kontakte'; + String get settings_refreshContacts => 'Ponovno obišči kontakte'; @override String get settings_refreshContactsSubtitle => - 'Ponovno naloži seznam stikov v napravi'; + 'Ponovno naloži seznam stikov v napravi'; @override String get settings_rebootDevice => 'Ponovni zagon naprave'; @override - String get settings_rebootDeviceSubtitle => 'Ponovno zaženi MeshCore napravo'; + String get settings_rebootDeviceSubtitle => + 'Ponovno zaženi MeshCore napravo'; @override String get settings_rebootDeviceConfirm => - 'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.'; + 'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.'; @override String get settings_debug => 'Debug'; @override - String get settings_bleDebugLog => 'BLE debug log (razhroščevanje)'; + String get settings_bleDebugLog => 'BLE debug log (razhroščevanje)'; @override String get settings_bleDebugLogSubtitle => @@ -340,7 +335,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_appDebugLog => 'Logi aplikacije'; @override - String get settings_appDebugLogSubtitle => 'Debug sporočila aplikacije'; + String get settings_appDebugLogSubtitle => 'Debug sporočila aplikacije'; @override String get settings_about => 'Oglejte si'; @@ -355,11 +350,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_aboutDescription => - 'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.'; + 'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Podatki o višini LOS: Open-Meteo (CC BY 4.0)'; + 'Podatki o viÅ¡ini LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Ime'; @@ -374,13 +369,13 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_infoBattery => 'Baterija'; @override - String get settings_infoPublicKey => 'Javni ključ'; + String get settings_infoPublicKey => 'Javni ključ'; @override - String get settings_infoContactsCount => 'Število stikov'; + String get settings_infoContactsCount => 'Å tevilo stikov'; @override - String get settings_infoChannelCount => 'Število kanalov'; + String get settings_infoChannelCount => 'Å tevilo kanalov'; @override String get settings_presets => 'Prednastavitve'; @@ -395,33 +390,33 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_frequencyInvalid => 'Neveljavna frekvenca (300-2500 MHz)'; @override - String get settings_bandwidth => 'Pasovna širina'; + String get settings_bandwidth => 'Pasovna Å¡irina'; @override - String get settings_spreadingFactor => 'Razširitveni faktor'; + String get settings_spreadingFactor => 'RazÅ¡iritveni faktor'; @override String get settings_codingRate => 'Programska hitrost'; @override - String get settings_txPower => 'TX Moč (dBm)'; + String get settings_txPower => 'TX Moč (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; + String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; @override String get settings_clientRepeat => 'Neovadno ponavljanje'; @override String get settings_clientRepeatSubtitle => - 'Omogočite temu naprave, da ponavlja paketne sporočila za druge.'; + 'Omogočite temu naprave, da ponavlja paketne sporočila za druge.'; @override String get settings_clientRepeatFreqWarning => - 'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.'; + 'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.'; @override String settings_error(String message) { @@ -432,7 +427,7 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_title => 'Nastavitve aplikacije'; @override - String get appSettings_appearance => 'Prikaži'; + String get appSettings_appearance => 'Prikaži'; @override String get appSettings_theme => 'Tema'; @@ -456,10 +451,10 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -468,16 +463,16 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -486,40 +481,41 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Ruščina'; + String get appSettings_languageRu => 'Ruščina'; @override String get appSettings_languageUk => 'Ukrajinsko'; @override - String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom'; + String get appSettings_enableMessageTracing => + 'Omogoči sledenje sporočilom'; @override String get appSettings_enableMessageTracingSubtitle => - 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; + 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; @override String get appSettings_notifications => 'Obvestila'; @override - String get appSettings_enableNotifications => 'Omogoči obvestila'; + String get appSettings_enableNotifications => 'Omogoči obvestila'; @override String get appSettings_enableNotificationsSubtitle => - 'Prejmite obvestila o sporočilih in oglasih'; + 'Prejmite obvestila o sporočilih in oglasih'; @override String get appSettings_notificationPermissionDenied => 'Odobritev obvestila zavrnjena'; @override - String get appSettings_notificationsEnabled => 'Obvestila omogočena'; + String get appSettings_notificationsEnabled => 'Obvestila omogočena'; @override String get appSettings_notificationsDisabled => 'Obvestila so izklopljena'; @@ -529,41 +525,41 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_messageNotificationsSubtitle => - 'Pokaži obvestilo ob prejemu novih sporočil.'; + 'Pokaži obvestilo ob prejemu novih sporočil.'; @override String get appSettings_channelMessageNotifications => - 'Obvestila o sporočilih kanala'; + 'Obvestila o sporočilih kanala'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Pokaži obvestilo ob prejemanju sporočil kanala'; + 'Pokaži obvestilo ob prejemanju sporočil kanala'; @override String get appSettings_advertisementNotifications => 'Opozorila o oglasih'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Pokaži obvestilo, ko so najdene nove naprave.'; + 'Pokaži obvestilo, ko so najdene nove naprave.'; @override String get appSettings_messaging => 'Komuniciranje'; @override String get appSettings_clearPathOnMaxRetry => - 'Ponovite pot do cilja na največjem štetju'; + 'Ponovite pot do cilja na največjem Å¡tetju'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja'; + 'Ponovi pot zimske obveščevalne poti po 5 neuspeÅ¡nih poskusih poÅ¡iljanja'; @override String get appSettings_pathsWillBeCleared => - 'Počisti pot po 5 neuspešnih poskusih.'; + 'Počisti pot po 5 neuspeÅ¡nih poskusih.'; @override String get appSettings_pathsWillNotBeCleared => - 'Poti ne bodo samodejno čiščene.'; + 'Poti ne bodo samodejno čiščene.'; @override String get appSettings_autoRouteRotation => @@ -571,15 +567,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_autoRouteRotationSubtitle => - 'Menjaj med boljšo potjo in flood načinom'; + 'Menjaj med boljÅ¡o potjo in flood načinom'; @override String get appSettings_autoRouteRotationEnabled => - 'Samodejno krmilno rotiranje omogočeno'; + 'Samodejno krmilno rotiranje omogočeno'; @override String get appSettings_autoRouteRotationDisabled => - 'Samodejno krmilno rotiranje je onemogočeno'; + 'Samodejno krmilno rotiranje je onemogočeno'; @override String get appSettings_battery => 'Baterija'; @@ -594,13 +590,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_batteryChemistryConnectFirst => - 'Za izbiro se poveži z napravo'; + 'Za izbiro se poveži z napravo'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @@ -609,42 +605,43 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_mapDisplay => 'Prikaz zemljevida'; @override - String get appSettings_showRepeaters => 'Prikaži repetitorje'; + String get appSettings_showRepeaters => 'Prikaži repetitorje'; @override - String get appSettings_showRepeatersSubtitle => 'Prikaži repetitorje na mapi'; + String get appSettings_showRepeatersSubtitle => + 'Prikaži repetitorje na mapi'; @override - String get appSettings_showChatNodes => 'Prikaži naprave za klepet'; + String get appSettings_showChatNodes => 'Prikaži naprave za klepet'; @override String get appSettings_showChatNodesSubtitle => - 'Prikaži naprave na zemljevidu'; + 'Prikaži naprave na zemljevidu'; @override - String get appSettings_showOtherNodes => 'Pokaži druge naprave'; + String get appSettings_showOtherNodes => 'Pokaži druge naprave'; @override String get appSettings_showOtherNodesSubtitle => - 'Pokaži druge vrste naprav na zemljevidu.'; + 'Pokaži druge vrste naprav na zemljevidu.'; @override - String get appSettings_timeFilter => 'Filter po času'; + String get appSettings_timeFilter => 'Filter po času'; @override - String get appSettings_timeFilterShowAll => 'Pokaži vse naprave'; + String get appSettings_timeFilterShowAll => 'Pokaži vse naprave'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Pokaži naprave v zadnjih $hours urah'; + return 'Pokaži naprave v zadnjih $hours urah'; } @override - String get appSettings_mapTimeFilter => 'Filter časa na zemljevidu'; + String get appSettings_mapTimeFilter => 'Filter časa na zemljevidu'; @override String get appSettings_showNodesDiscoveredWithin => - 'Pokaži naprave odkrite v:'; + 'Pokaži naprave odkrite v:'; @override String get appSettings_allTime => 'Brez omejitev'; @@ -659,7 +656,7 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_last24Hours => 'Zadnjih 24 ur'; @override - String get appSettings_lastWeek => 'Prejšnji teden'; + String get appSettings_lastWeek => 'PrejÅ¡nji teden'; @override String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; @@ -668,36 +665,36 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_unitsTitle => 'Enote'; @override - String get appSettings_unitsMetric => 'Metrična (m/km)'; + String get appSettings_unitsMetric => 'Metrična (m/km)'; @override String get appSettings_unitsImperial => 'Imperialno (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Območje ni izbrano'; + String get appSettings_noAreaSelected => 'Območje ni izbrano'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Izbrano območje (povečava $minZoom-$maxZoom)'; + return 'Izbrano območje (povečava $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Razhroščevanje'; + String get appSettings_debugCard => 'Razhroščevanje'; @override String get appSettings_appDebugLogging => 'Programski dnevnik'; @override String get appSettings_appDebugLoggingSubtitle => - 'Dnevnik debug sporočil za odpravljanje težav'; + 'Dnevnik debug sporočil za odpravljanje težav'; @override String get appSettings_appDebugLoggingEnabled => - 'Beleženje napak v aplikaciji omogočeno'; + 'Beleženje napak v aplikaciji omogočeno'; @override String get appSettings_appDebugLoggingDisabled => - 'Beleženje napak v aplikacije onemogočeno.'; + 'Beleženje napak v aplikacije onemogočeno.'; @override String get contacts_title => 'Stiki'; @@ -727,17 +724,17 @@ class AppLocalizationsSl extends AppLocalizations { @override String contacts_searchUsers(int number, String str) { - return 'Išči $number$str uporabnikov...'; + return 'Išči $number$str uporabnikov...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Išči $number$str ponavljalnike...'; + return 'Išči $number$str ponavljalnike...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Išči $number$str strežnikov sob...'; + return 'Išči $number$str strežnikov sob...'; } @override @@ -747,18 +744,18 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_noContactsFound => 'Stiki niso najdeni.'; @override - String get contacts_deleteContact => 'Izbriši stik'; + String get contacts_deleteContact => 'IzbriÅ¡i stik'; @override String contacts_removeConfirm(String contactName) { - return 'Izbrišem $contactName iz stikov?'; + return 'IzbriÅ¡em $contactName iz stikov?'; } @override String get contacts_manageRepeater => 'Upravljaj Ponovitve'; @override - String get contacts_manageRoom => 'Upravljajte strežnik sobe'; + String get contacts_manageRoom => 'Upravljajte strežnik sobe'; @override String get contacts_roomLogin => 'Prijava v sobo'; @@ -770,11 +767,11 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_editGroup => 'Uredi skupino'; @override - String get contacts_deleteGroup => 'Izbriši skupino'; + String get contacts_deleteGroup => 'IzbriÅ¡i skupino'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Izbriši $groupName?'; + return 'IzbriÅ¡i $groupName?'; } @override @@ -788,7 +785,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String contacts_groupAlreadyExists(String name) { - return 'Skupina \"$name\" že obstaja'; + return 'Skupina \"$name\" že obstaja'; } @override @@ -796,46 +793,46 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Noben stik ne ustreza vašemu kriteriju.'; + 'Noben stik ne ustreza vaÅ¡emu kriteriju.'; @override - String get contacts_noMembers => 'Ni članov.'; + String get contacts_noMembers => 'Ni članov.'; @override String get contacts_lastSeenNow => 'Nazadnje viden zdaj'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Zadnjič viden pred $minutes minutami'; + return 'Zadnjič viden pred $minutes minutami'; } @override - String get contacts_lastSeenHourAgo => 'Zadnjič viden pred 1 uro.'; + String get contacts_lastSeenHourAgo => 'Zadnjič viden pred 1 uro.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Zadnjič viden pred $hours urami'; + return 'Zadnjič viden pred $hours urami'; } @override - String get contacts_lastSeenDayAgo => 'Zadnjič viden pred 1 dnem'; + String get contacts_lastSeenDayAgo => 'Zadnjič viden pred 1 dnem'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Zadnjič viden pred $days dnem'; + return 'Zadnjič viden pred $days dnem'; } @override String get channels_title => 'Kanali'; @override - String get channels_noChannelsConfigured => 'Kanali še niso konfigurirani'; + String get channels_noChannelsConfigured => 'Kanali Å¡e niso konfigurirani'; @override String get channels_addPublicChannel => 'Dodaj javni kanal'; @override - String get channels_searchChannels => 'Poišči kanale...'; + String get channels_searchChannels => 'Poišči kanale...'; @override String get channels_noChannelsFound => 'Ne najdem kanalov.'; @@ -864,22 +861,22 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_editChannel => 'Uredi kanal'; @override - String get channels_muteChannel => 'Utišaj kanal'; + String get channels_muteChannel => 'UtiÅ¡aj kanal'; @override String get channels_unmuteChannel => 'Vklopi obvestila kanala'; @override - String get channels_deleteChannel => 'Pošlji kanal'; + String get channels_deleteChannel => 'PoÅ¡lji kanal'; @override String channels_deleteChannelConfirm(String name) { - return 'Izbrišem \"$name\"? To se ne da povrniti.'; + return 'IzbriÅ¡em \"$name\"? To se ne da povrniti.'; } @override String channels_channelDeleteFailed(String name) { - return 'Kanala $name ni bilo mogoče izbrisati'; + return 'Kanala $name ni bilo mogoče izbrisati'; } @override @@ -903,10 +900,10 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_standardPublicPsk => 'Standardni javni PSK'; @override - String get channels_pskHex => 'PSK (Šestnajstbinska)'; + String get channels_pskHex => 'PSK (Å estnajstbinska)'; @override - String get channels_generateRandomPsk => 'Generiraj naključni PSK'; + String get channels_generateRandomPsk => 'Generiraj naključni PSK'; @override String get channels_enterChannelName => 'Vnesi ime kanala'; @@ -940,49 +937,50 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_sortBy => 'Sortiraj po'; @override - String get channels_sortManual => 'Ročno'; + String get channels_sortManual => 'Ročno'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Najnovejše sporočilo'; + String get channels_sortLatestMessages => 'NajnovejÅ¡e sporočilo'; @override - String get channels_sortUnread => 'Nerešeno'; + String get channels_sortUnread => 'NereÅ¡eno'; @override String get channels_createPrivateChannel => 'Ustvari zasebno kanal.'; @override String get channels_createPrivateChannelDesc => - 'Varno zaklenjeno s skrivnim ključem.'; + 'Varno zaklenjeno s skrivnim ključem.'; @override - String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini'; + String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini'; @override - String get channels_joinPrivateChannelDesc => 'Ročno vnesite zaporni ključ.'; + String get channels_joinPrivateChannelDesc => + 'Ročno vnesite zaporni ključ.'; @override - String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu'; + String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu'; @override String get channels_joinPublicChannelDesc => - 'Kdor karkoli je, lahko se pridruži tej skupini.'; + 'Kdor karkoli je, lahko se pridruži tej skupini.'; @override - String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom'; + String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom'; @override String get channels_joinHashtagChannelDesc => - 'Kdor karkoli, lahko se pridruži hashtag kanalom.'; + 'Kdor karkoli, lahko se pridruži hashtag kanalom.'; @override String get channels_scanQrCode => 'Skeniraj QR kodo'; @override - String get channels_scanQrCodeComingSoon => 'Prihajajoča'; + String get channels_scanQrCodeComingSoon => 'Prihajajoča'; @override String get channels_enterHashtag => 'Vnesite hashtag'; @@ -991,14 +989,14 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_hashtagHint => 'npr. #ekipa'; @override - String get chat_noMessages => 'Še ni sporočil.'; + String get chat_noMessages => 'Å e ni sporočil.'; @override - String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.'; + String get chat_sendMessageToStart => 'PoÅ¡lji sporočilo za začetek.'; @override String get chat_originalMessageNotFound => - 'Opozorilo: Sporočilo ni bilo najdeno'; + 'Opozorilo: Sporočilo ni bilo najdeno'; @override String chat_replyingTo(String name) { @@ -1007,7 +1005,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String chat_replyTo(String name) { - return 'Odpošlji odgovor $name'; + return 'OdpoÅ¡lji odgovor $name'; } @override @@ -1015,22 +1013,22 @@ class AppLocalizationsSl extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'Pošlji sporočilo $contactName'; + return 'PoÅ¡lji sporočilo $contactName'; } @override - String get chat_typeMessage => 'Vnesi sporočilo...'; + String get chat_typeMessage => 'Vnesi sporočilo...'; @override String chat_messageTooLong(int maxBytes) { - return 'Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes byte-ov).'; + return 'PoÅ¡iljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes byte-ov).'; } @override - String get chat_messageCopied => 'Sporočilo poslano'; + String get chat_messageCopied => 'Sporočilo poslano'; @override - String get chat_messageDeleted => 'Sporočilo izbrisano'; + String get chat_messageDeleted => 'Sporočilo izbrisano'; @override String get chat_retryingMessage => 'Ponovni poskus.'; @@ -1041,7 +1039,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get chat_sendGif => 'Pošlji GIF'; + String get chat_sendGif => 'PoÅ¡lji GIF'; @override String get chat_reply => 'Odgovori'; @@ -1068,7 +1066,7 @@ class AppLocalizationsSl extends AppLocalizations { String get gifPicker_title => 'Izberi GIF'; @override - String get gifPicker_searchHint => 'Išči GIF-e...'; + String get gifPicker_searchHint => 'Išči GIF-e...'; @override String get gifPicker_poweredBy => 'Napredno z GIPHY'; @@ -1077,10 +1075,10 @@ class AppLocalizationsSl extends AppLocalizations { String get gifPicker_noGifsFound => 'Ne najdem GIF-ov.'; @override - String get gifPicker_failedLoad => 'Neuspešno nalaganje GIF-a'; + String get gifPicker_failedLoad => 'NeuspeÅ¡no nalaganje GIF-a'; @override - String get gifPicker_failedSearch => 'Iskanje neuspešno.'; + String get gifPicker_failedSearch => 'Iskanje neuspeÅ¡no.'; @override String get gifPicker_noInternet => 'Ni internetne povezave'; @@ -1095,26 +1093,26 @@ class AppLocalizationsSl extends AppLocalizations { String get debugLog_copyLog => 'Kopiraj dnevnik'; @override - String get debugLog_clearLog => 'Briši log'; + String get debugLog_clearLog => 'BriÅ¡i log'; @override - String get debugLog_copied => 'Beležka kopirana.'; + String get debugLog_copied => 'Beležka kopirana.'; @override - String get debugLog_bleCopied => 'Kopirana beležka iz BLE'; + String get debugLog_bleCopied => 'Kopirana beležka iz BLE'; @override String get debugLog_noEntries => 'Ni ustvarjenih debug zapisov.'; @override String get debugLog_enableInSettings => - 'Omogoči beleženje napak v nastavitvah aplikacije'; + 'Omogoči beleženje napak v nastavitvah aplikacije'; @override String get debugLog_frames => 'Okvirji'; @override - String get debugLog_rawLogRx => 'Svež Log-RX'; + String get debugLog_rawLogRx => 'Svež Log-RX'; @override String get debugLog_noBleActivity => 'Ni BLE aktivnosti.'; @@ -1134,12 +1132,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String debugFrame_destinationPubKey(String pubKey) { - return '- Destinirano Ključno Besedilo: $pubKey'; + return '- Destinirano Ključno Besedilo: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Časovnik: $timestamp'; + return '- ÄŒasovnik: $timestamp'; } @override @@ -1170,23 +1168,23 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_pathManagement => 'Upravljanje poti'; @override - String get chat_ShowAllPaths => 'Prikaži vse poti'; + String get chat_ShowAllPaths => 'Prikaži vse poti'; @override - String get chat_routingMode => 'Navodilo za usmerjevalni način'; + String get chat_routingMode => 'Navodilo za usmerjevalni način'; @override String get chat_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; @override - String get chat_forceFloodMode => 'Nasilje obvezati v način'; + String get chat_forceFloodMode => 'Nasilje obvezati v način'; @override String get chat_recentAckPaths => 'Nedavni poti ACK (tap za uporabo):'; @override String get chat_pathHistoryFull => - 'Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.'; + 'Zapiske o poti so popolni. IzbriÅ¡i vnose, da dodaÅ¡ nove.'; @override String get chat_hopSingular => 'skok'; @@ -1206,14 +1204,14 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get chat_successes => 'Uspešni'; + String get chat_successes => 'UspeÅ¡ni'; @override - String get chat_removePath => 'Izbriši pot'; + String get chat_removePath => 'IzbriÅ¡i pot'; @override String get chat_noPathHistoryYet => - 'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.'; + 'Ni shranjenih poti.\nPoÅ¡lji sporočilo za odkrivanje poti.'; @override String get chat_pathActions => 'Potni ukazi:'; @@ -1222,17 +1220,17 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_setCustomPath => 'Nastavi Prilozeno Pot'; @override - String get chat_setCustomPathSubtitle => 'Ročno določite potniško pot.'; + String get chat_setCustomPathSubtitle => 'Ročno določite potniÅ¡ko pot.'; @override - String get chat_clearPath => 'Počisti pot'; + String get chat_clearPath => 'Počisti pot'; @override - String get chat_clearPathSubtitle => 'Ob naslednji pošiljanju znova zbrati.'; + String get chat_clearPathSubtitle => 'Ob naslednji poÅ¡iljanju znova zbrati.'; @override String get chat_pathCleared => - 'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.'; + 'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.'; @override String get chat_floodModeSubtitle => @@ -1240,14 +1238,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.'; + 'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.'; @override String get chat_fullPath => 'Polna pot'; @override String get chat_pathDetailsNotAvailable => - 'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.'; + 'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1262,13 +1260,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_pathSavedLocally => - 'Shrano lokalno. Povežite se za sinhronizacijo.'; + 'Shrano lokalno. Povežite se za sinhronizacijo.'; @override String get chat_pathDeviceConfirmed => 'Naprave potrjeno.'; @override - String get chat_pathDeviceNotConfirmed => 'Naprave še niso potrdile.'; + String get chat_pathDeviceNotConfirmed => 'Naprave Å¡e niso potrdile.'; @override String get chat_type => 'Vnesite'; @@ -1277,16 +1275,16 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_path => 'Pot'; @override - String get chat_publicKey => 'Ključ javnega tipa'; + String get chat_publicKey => 'Ključ javnega tipa'; @override - String get chat_compressOutgoingMessages => 'Stisnite izhodne sporočila'; + String get chat_compressOutgoingMessages => 'Stisnite izhodne sporočila'; @override String get chat_floodForced => 'Porolni (nasilje).'; @override - String get chat_directForced => 'Nezglašen (nasilje)'; + String get chat_directForced => 'NezglaÅ¡en (nasilje)'; @override String chat_hopsForced(int count) { @@ -1300,11 +1298,11 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_direct => 'Neposredni'; @override - String get chat_poiShared => 'Deljeno točke MN'; + String get chat_poiShared => 'Deljeno točke MN'; @override String chat_unread(int count) { - return 'Nerešeno: $count'; + return 'NereÅ¡eno: $count'; } @override @@ -1312,21 +1310,21 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_openLinkConfirmation => - 'Ali želite odpreti to povezavo v brskalniku?'; + 'Ali želite odpreti to povezavo v brskalniku?'; @override String get chat_open => 'Odpri'; @override String chat_couldNotOpenLink(String url) { - return 'Povezave ni bilo mogoče odpreti: $url'; + return 'Povezave ni bilo mogoče odpreti: $url'; } @override String get chat_invalidLink => 'Neveljavna oblika povezave'; @override - String get map_title => 'Mapa omrežja'; + String get map_title => 'Mapa omrežja'; @override String get map_lineOfSight => 'Linija vida'; @@ -1336,11 +1334,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_noNodesWithLocation => - 'Nihče od notranjih elementov nima podatkov o lokaciji.'; + 'Nihče od notranjih elementov nima podatkov o lokaciji.'; @override String get map_nodesNeedGps => - 'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.'; + 'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.'; @override String map_nodesCount(int count) { @@ -1349,11 +1347,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String map_pinsCount(int count) { - return 'Žigovi: $count'; + return 'Žigovi: $count'; } @override - String get map_chat => 'Čistemar'; + String get map_chat => 'ÄŒistemar'; @override String get map_repeater => 'Ponovitelj'; @@ -1365,20 +1363,20 @@ class AppLocalizationsSl extends AppLocalizations { String get map_sensor => 'Senzor'; @override - String get map_pinDm => 'Zavežite (DM)'; + String get map_pinDm => 'Zavežite (DM)'; @override - String get map_pinPrivate => 'Zasebno označit'; + String get map_pinPrivate => 'Zasebno označit'; @override String get map_pinPublic => 'Oznaka (javna)'; @override - String get map_lastSeen => 'Zadnjič Zazet'; + String get map_lastSeen => 'Zadnjič Zazet'; @override String get map_disconnectConfirm => - 'Ste prepričani, da želite se odklopiti s tega naprave?'; + 'Ste prepričani, da želite se odklopiti s tega naprave?'; @override String get map_from => 'Od'; @@ -1390,7 +1388,7 @@ class AppLocalizationsSl extends AppLocalizations { String get map_flags => 'Zapestnice'; @override - String get map_shareMarkerHere => 'Delite točke tukaj.'; + String get map_shareMarkerHere => 'Delite točke tukaj.'; @override String get map_pinLabel => 'Oznaka za pritrditev'; @@ -1399,16 +1397,16 @@ class AppLocalizationsSl extends AppLocalizations { String get map_label => 'Oznaka'; @override - String get map_pointOfInterest => 'Točka zanimivosti'; + String get map_pointOfInterest => 'Točka zanimivosti'; @override - String get map_sendToContact => 'Pošlji v kontakt'; + String get map_sendToContact => 'PoÅ¡lji v kontakt'; @override - String get map_sendToChannel => 'Pošlji v kanal'; + String get map_sendToChannel => 'PoÅ¡lji v kanal'; @override - String get map_noChannelsAvailable => 'Nihče kanalov na voljo.'; + String get map_noChannelsAvailable => 'Nihče kanalov na voljo.'; @override String get map_publicLocationShare => 'Deljenje javne lokacije'; @@ -1420,37 +1418,37 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_connectToShareMarkers => - 'Povežite se z napravo za deljenje oznak.'; + 'Povežite se z napravo za deljenje oznak.'; @override - String get map_filterNodes => 'Filtirirajte člene'; + String get map_filterNodes => 'Filtirirajte člene'; @override String get map_nodeTypes => 'Vrste knope'; @override - String get map_chatNodes => 'Čuti zvezde'; + String get map_chatNodes => 'ÄŒuti zvezde'; @override String get map_repeaters => 'Ponovljalniki'; @override - String get map_otherNodes => 'Druge vozlišča'; + String get map_otherNodes => 'Druge vozlišča'; @override - String get map_keyPrefix => 'Predpona ključa'; + String get map_keyPrefix => 'Predpona ključa'; @override - String get map_filterByKeyPrefix => 'Filtri po predpomniku ključa'; + String get map_filterByKeyPrefix => 'Filtri po predpomniku ključa'; @override - String get map_publicKeyPrefix => 'Predifika javnega ključa'; + String get map_publicKeyPrefix => 'Predifika javnega ključa'; @override - String get map_markers => 'Označitelji'; + String get map_markers => 'Označitelji'; @override - String get map_showSharedMarkers => 'Pokaži skupno označenja'; + String get map_showSharedMarkers => 'Pokaži skupno označenja'; @override String get map_lastSeenTime => 'Datum zadnjega vpogleda'; @@ -1459,16 +1457,16 @@ class AppLocalizationsSl extends AppLocalizations { String get map_sharedPin => 'Deljeno naslovno geslo'; @override - String get map_joinRoom => 'Pridružiti sobo'; + String get map_joinRoom => 'Pridružiti sobo'; @override String get map_manageRepeater => 'Upravljajte Ponovitve'; @override - String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.'; + String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.'; @override - String get map_runTrace => 'Zaženi sledenje poti'; + String get map_runTrace => 'Zaženi sledenje poti'; @override String get map_removeLast => 'Odstrani Zadnji'; @@ -1478,51 +1476,51 @@ class AppLocalizationsSl extends AppLocalizations { @override String get mapCache_title => - 'Omrezni predpomnilnik zemljeških zemljejevskih slik'; + 'Omrezni predpomnilnik zemljeÅ¡kih zemljejevskih slik'; @override String get mapCache_selectAreaFirst => - 'Izberite območje za prvo predpomnilnik.'; + 'Izberite območje za prvo predpomnilnik.'; @override String get mapCache_noTilesToDownload => - 'Nihče slik ne bo naložil za to območje.'; + 'Nihče slik ne bo naložil za to območje.'; @override - String get mapCache_downloadTilesTitle => 'Naloži ploščice'; + String get mapCache_downloadTilesTitle => 'Naloži ploščice'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Naložiť $count plošč za uporabo v režimu brez povezave?'; + return 'NaložiÅ¥ $count plošč za uporabo v režimu brez povezave?'; } @override - String get mapCache_downloadAction => 'Naloži'; + String get mapCache_downloadAction => 'Naloži'; @override String mapCache_cachedTiles(int count) { - return 'Pospešeno shranjeni $count plošč'; + return 'PospeÅ¡eno shranjeni $count plošč'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Shranjeni $downloaded ploščad ($failed neuspešno)'; + return 'Shranjeni $downloaded ploščad ($failed neuspeÅ¡no)'; } @override String get mapCache_clearOfflineCacheTitle => - 'Ponovite arhiv za offline način'; + 'Ponovite arhiv za offline način'; @override String get mapCache_clearOfflineCachePrompt => - 'Izbriši vse predpomnilnikovane kartografske ploščice?'; + 'IzbriÅ¡i vse predpomnilnikovane kartografske ploščice?'; @override String get mapCache_offlineCacheCleared => 'Omrezni predpomnik je bil izbrisal.'; @override - String get mapCache_noAreaSelected => 'Nizona označena površina'; + String get mapCache_noAreaSelected => 'Nizona označena povrÅ¡ina'; @override String get mapCache_cacheArea => 'Omanski prostor'; @@ -1531,27 +1529,27 @@ class AppLocalizationsSl extends AppLocalizations { String get mapCache_useCurrentView => 'Uporabi trenutni prikaz'; @override - String get mapCache_zoomRange => 'Občutek razpona'; + String get mapCache_zoomRange => 'Občutek razpona'; @override String mapCache_estimatedTiles(int count) { - return 'Predvideni ploščadi: $count'; + return 'Predvideni ploščadi: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Naloženo $completed / $total'; + return 'Naloženo $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Naloži ploščice'; + String get mapCache_downloadTilesButton => 'Naloži ploščice'; @override String get mapCache_clearCacheButton => 'Ponoviti arhiv'; @override String mapCache_failedDownloads(int count) { - return 'Poslovniški izniki: $count'; + return 'PoslovniÅ¡ki izniki: $count'; } @override @@ -1610,14 +1608,14 @@ class AppLocalizationsSl extends AppLocalizations { String get time_minutes => 'minute'; @override - String get time_allTime => 'Vse časovno obdobje'; + String get time_allTime => 'Vse časovno obdobje'; @override String get dialog_disconnect => 'Odklopiti'; @override String get dialog_disconnectConfirm => - 'Ste prepričani, da želite se odklopiti s tega naprave?'; + 'Ste prepričani, da želite se odklopiti s tega naprave?'; @override String get login_repeaterLogin => 'Ponovni vnos'; @@ -1650,36 +1648,36 @@ class AppLocalizationsSl extends AppLocalizations { String get login_routing => 'Usmerjanje'; @override - String get login_routingMode => 'Navodilo za usmerjevalni način'; + String get login_routingMode => 'Navodilo za usmerjevalni način'; @override String get login_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; @override - String get login_forceFloodMode => 'Nasilje obvezati v način'; + String get login_forceFloodMode => 'Nasilje obvezati v način'; @override - String get login_managePaths => 'Upravljajte Potniške Proti'; + String get login_managePaths => 'Upravljajte PotniÅ¡ke Proti'; @override String get login_login => 'Prijava'; @override String login_attempt(int current, int max) { - return 'Poskušajo $current/$max'; + return 'PoskuÅ¡ajo $current/$max'; } @override String login_failed(String error) { - return 'Prijava je bila neuspešna: $error'; + return 'Prijava je bila neuspeÅ¡na: $error'; } @override String get login_failedMessage => - 'Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.'; + 'Prijava je bila neuspeÅ¡na. Geslo je napačno ali pa je repetitor nedosegljiv.'; @override - String get common_reload => 'Ponovno naloži'; + String get common_reload => 'Ponovno naloži'; @override String get common_clear => 'Ponoviti'; @@ -1708,14 +1706,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.'; + 'Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.'; @override String get path_hexPrefixExample => - 'Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)'; + 'Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)'; @override - String get path_labelHexPrefixes => 'Pot (heksafixne skrajšave)'; + String get path_labelHexPrefixes => 'Pot (heksafixne skrajÅ¡ave)'; @override String get path_helperMaxHops => @@ -1726,19 +1724,19 @@ class AppLocalizationsSl extends AppLocalizations { @override String get path_noRepeatersFound => - 'Ne najdenih ponoviteljev ali strežnikov sob.'; + 'Ne najdenih ponoviteljev ali strežnikov sob.'; @override String get path_customPathsRequire => - 'Prilojene poti zahtevajo medhodne prenose, ki lahko prenašajo sporočila.'; + 'Prilojene poti zahtevajo medhodne prenose, ki lahko prenaÅ¡ajo sporočila.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Neveljačni šesteročlenski prefiksi: $prefixes'; + return 'Neveljačni Å¡esteročlenski prefiksi: $prefixes'; } @override - String get path_tooLong => 'Pot je prevelika. Dovoljeno največ 64 skokov.'; + String get path_tooLong => 'Pot je prevelika. Dovoljeno največ 64 skokov.'; @override String get path_setPath => 'Nastavi Pot'; @@ -1747,7 +1745,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_management => 'Upravljanje ponovitve'; @override - String get room_management => 'Upravljanje stremlišča'; + String get room_management => 'Upravljanje stremlišča'; @override String get repeater_managementTools => 'Upravne orodje'; @@ -1771,13 +1769,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliSubtitle => - 'Pošlji ukazne povelje na ponovitveno enoto.'; + 'PoÅ¡lji ukazne povelje na ponovitveno enoto.'; @override String get repeater_neighbors => 'Sosedi'; @override - String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.'; + String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.'; @override String get repeater_settings => 'Nastavitve'; @@ -1790,13 +1788,13 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_statusTitle => 'Status ponovitelja'; @override - String get repeater_routingMode => 'Navodilo za usmerjevalni način'; + String get repeater_routingMode => 'Navodilo za usmerjevalni način'; @override String get repeater_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; @override - String get repeater_forceFloodMode => 'Nasilje obvezati v način'; + String get repeater_forceFloodMode => 'Nasilje obvezati v način'; @override String get repeater_pathManagement => 'Upravljanje poti'; @@ -1809,7 +1807,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String repeater_errorLoadingStatus(String error) { - return 'Napaka pri obnašanju: $error'; + return 'Napaka pri obnaÅ¡anju: $error'; } @override @@ -1822,10 +1820,10 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_clockAtLogin => 'Ure (pri prijavi)'; @override - String get repeater_uptime => 'Čas delovanja'; + String get repeater_uptime => 'ÄŒas delovanja'; @override - String get repeater_queueLength => 'Dolžina čakalne vrste'; + String get repeater_queueLength => 'Dolžina čakalne vrste'; @override String get repeater_debugFlags => 'Nastavitve odpravilnosti'; @@ -1837,10 +1835,10 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_lastRssi => 'Potredno RSSI'; @override - String get repeater_lastSnr => 'Nazadnje zabeležena SNR'; + String get repeater_lastSnr => 'Nazadnje zabeležena SNR'; @override - String get repeater_noiseFloor => 'Šumovita raven'; + String get repeater_noiseFloor => 'Å umovita raven'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1852,7 +1850,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_packetStatistics => 'Statistika paketa'; @override - String get repeater_sent => 'Pošljeno'; + String get repeater_sent => 'PoÅ¡ljeno'; @override String get repeater_received => 'Prejeto'; @@ -1909,7 +1907,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_adminPasswordHelper => 'Polni dostopno geslo'; @override - String get repeater_guestPassword => 'Geslo gostača'; + String get repeater_guestPassword => 'Geslo gostača'; @override String get repeater_guestPasswordHelper => 'Odpovedni dostopni geslo'; @@ -1924,16 +1922,16 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX Moč'; + String get repeater_txPower => 'TX Moč'; @override String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Pasovna širina'; + String get repeater_bandwidth => 'Pasovna Å¡irina'; @override - String get repeater_spreadingFactor => 'Razširitveni faktor'; + String get repeater_spreadingFactor => 'RazÅ¡iritveni faktor'; @override String get repeater_codingRate => 'Programska hitrost'; @@ -1942,37 +1940,37 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_locationSettings => 'Nastavitve lokacije'; @override - String get repeater_latitude => 'Širina'; + String get repeater_latitude => 'Å irina'; @override String get repeater_latitudeHelper => 'Desetbinske protiure (npr. 37.7749)'; @override - String get repeater_longitude => 'Dolžina'; + String get repeater_longitude => 'Dolžina'; @override String get repeater_longitudeHelper => 'Desetbinske protiure (npr. -122,4194)'; @override - String get repeater_features => 'Značilnosti'; + String get repeater_features => 'Značilnosti'; @override String get repeater_packetForwarding => 'Usmerjanje paketa'; @override String get repeater_packetForwardingSubtitle => - 'Omogoči ponovitelja za usmerjanje paketov.'; + 'Omogoči ponovitelja za usmerjanje paketov.'; @override String get repeater_guestAccess => 'Prijemnik'; @override String get repeater_guestAccessSubtitle => - 'Omogoči dostop gostom v samo bralni načinu.'; + 'Omogoči dostop gostom v samo bralni načinu.'; @override - String get repeater_privacyMode => 'Privatni način'; + String get repeater_privacyMode => 'Privatni način'; @override String get repeater_privacyModeSubtitle => 'Skrita imena/lokacije v oglasih'; @@ -1998,7 +1996,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Šifrirana Oglasovalska Trajanje'; + 'Å ifrirana Oglasovalska Trajanje'; @override String get repeater_dangerZone => 'Opozorilo'; @@ -2011,21 +2009,21 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_rebootRepeaterConfirm => - 'Ste prepričani, da želite ponovno zagon tega ponovitelja?'; + 'Ste prepričani, da želite ponovno zagon tega ponovitelja?'; @override - String get repeater_regenerateIdentityKey => 'Ponovite Ključ Identnosti'; + String get repeater_regenerateIdentityKey => 'Ponovite Ključ Identnosti'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Ustvarite novo par javnih/zasebnih ključev'; + 'Ustvarite novo par javnih/zasebnih ključev'; @override String get repeater_regenerateIdentityKeyConfirm => 'To bo ustvaril novo identiteto za ponavljalnik. Prijavite se?'; @override - String get repeater_eraseFileSystem => 'Počisti Sustav Vajah'; + String get repeater_eraseFileSystem => 'Počisti Sustav Vajah'; @override String get repeater_eraseFileSystemSubtitle => @@ -2033,7 +2031,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!'; + 'OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!'; @override String get repeater_eraseSerialOnly => @@ -2046,14 +2044,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String repeater_errorSendingCommand(String error) { - return 'Napaka pri pošiljanju ukaznega: $error'; + return 'Napaka pri poÅ¡iljanju ukaznega: $error'; } @override String get repeater_confirm => 'Potrdit'; @override - String get repeater_settingsSaved => 'Nastavitve so shranjene uspešno.'; + String get repeater_settingsSaved => 'Nastavitve so shranjene uspeÅ¡no.'; @override String repeater_errorSavingSettings(String error) { @@ -2068,7 +2066,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_refreshRadioSettings => 'Ponovno Nastavitve Radija'; @override - String get repeater_refreshTxPower => 'Ponovno nastavi TX moč'; + String get repeater_refreshTxPower => 'Ponovno nastavi TX moč'; @override String get repeater_refreshLocationSettings => @@ -2083,7 +2081,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_refreshPrivacyMode => - 'Ponovno aktiviraj način zasebnosti'; + 'Ponovno aktiviraj način zasebnosti'; @override String get repeater_refreshAdvertisementSettings => @@ -2096,14 +2094,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String repeater_errorRefreshing(String label) { - return 'Napaka pri osveževanju $label'; + return 'Napaka pri osveževanju $label'; } @override String get repeater_cliTitle => 'Ponovitelj CLI'; @override - String get repeater_debugNextCommand => 'Popravi naslednje ukazne možnosti'; + String get repeater_debugNextCommand => 'Popravi naslednje ukazne možnosti'; @override String get repeater_commandHelp => 'Pomoc'; @@ -2113,7 +2111,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_noCommandsSent => - 'Niti ena ukazne povratne informacije še ni poslana.'; + 'Niti ena ukazne povratne informacije Å¡e ni poslana.'; @override String get repeater_typeCommandOrUseQuick => @@ -2123,7 +2121,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_enterCommandHint => 'Vnesite ukaz...'; @override - String get repeater_previousCommand => 'Prejšnji ukaz'; + String get repeater_previousCommand => 'PrejÅ¡nji ukaz'; @override String get repeater_nextCommand => 'Naslednja ukazna'; @@ -2152,7 +2150,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_cliQuickNeighbors => 'Sosedi'; @override - String get repeater_cliQuickVersion => 'Različica'; + String get repeater_cliQuickVersion => 'Različica'; @override String get repeater_cliQuickAdvertise => 'Oglasite'; @@ -2161,14 +2159,14 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_cliQuickClock => 'Ura'; @override - String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; + String get repeater_cliHelpAdvert => 'PoÅ¡lje paket oglasov'; @override String get repeater_cliHelpReboot => 'Ponastavi naprave. (Opomba, lahko pride do \'Timeouta\', kar je normalno)'; @override - String get repeater_cliHelpClock => 'Prikaže trenutno uro po uri naprave.'; + String get repeater_cliHelpClock => 'Prikaže trenutno uro po uri naprave.'; @override String get repeater_cliHelpPassword => @@ -2176,65 +2174,65 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpVersion => - 'Prikaže različico naprave in datum izrabe strojne opreme.'; + 'Prikaže različico naprave in datum izrabe strojne opreme.'; @override String get repeater_cliHelpClearStats => - 'Ponastavi različne statistične števke na nič.'; + 'Ponastavi različne statistične Å¡tevke na nič.'; @override - String get repeater_cliHelpSetAf => 'Nastavi časovni koeficient.'; + String get repeater_cliHelpSetAf => 'Nastavi časovni koeficient.'; @override String get repeater_cliHelpSetTx => - 'Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)'; + 'Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)'; @override String get repeater_cliHelpSetRepeat => - 'Omogoči ali onemogoči vlogo ponovitelja za tono.'; + 'Omogoči ali onemogoči vlogo ponovitelja za tono.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Osebni strežnik) Če je \'vklopljeno\', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).'; + '(Osebni strežnik) ÄŒe je \'vklopljeno\', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).'; @override String get repeater_cliHelpSetFloodMax => - 'Nastavi največjo število skokov za vstopne poplave (če je >= maks, paket ni usmerjen)'; + 'Nastavi največjo Å¡tevilo skokov za vstopne poplave (če je >= maks, paket ni usmerjen)'; @override String get repeater_cliHelpSetIntThresh => - 'Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.'; + 'Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.'; + 'Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.'; @override String get repeater_cliHelpSetMultiAcks => - 'Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".'; + 'Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".'; @override String get repeater_cliHelpSetAdvertInterval => - 'Nastavi časovno obmesto v minutah za pošiljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.'; + 'Nastavi časovno obmesto v minutah za poÅ¡iljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Nastavi časovno obmesto v urah za pošiljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.'; + 'Nastavi časovno obmesto v urah za poÅ¡iljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.'; @override String get repeater_cliHelpSetGuestPassword => - 'Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi pošiljajo zahtevo \"Get Stats\")'; + 'Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi poÅ¡iljajo zahtevo \"Get Stats\")'; @override String get repeater_cliHelpSetName => 'Nastavi ime oglasnika.'; @override String get repeater_cliHelpSetLat => - 'Nastavi zemljepisno širino oglaševalskega zemljevida (desetdeljne).'; + 'Nastavi zemljepisno Å¡irino oglaÅ¡evalskega zemljevida (desetdeljne).'; @override String get repeater_cliHelpSetLon => - 'Nastavi zemljevidno širino oglasnika. (desetdelne stopnje)'; + 'Nastavi zemljevidno Å¡irino oglasnika. (desetdelne stopnje)'; @override String get repeater_cliHelpSetRadio => @@ -2242,18 +2240,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpSetRxDelay => - 'Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.'; + 'Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.'; @override String get repeater_cliHelpSetTxDelay => - 'Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjša verjetnost kolizij)'; + 'Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjÅ¡a verjetnost kolizij)'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.'; + 'Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Omogoči/Preklopi most.'; + String get repeater_cliHelpSetBridgeEnabled => 'Omogoči/Preklopi most.'; @override String get repeater_cliHelpSetBridgeDelay => @@ -2273,39 +2271,39 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpSetAdcMultiplier => - 'Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).'; + 'Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).'; @override String get repeater_cliHelpTempRadio => - 'Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).'; + 'Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).'; @override String get repeater_cliHelpSetPerm => - 'Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).'; + 'Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).'; @override String get repeater_cliHelpGetBridgeType => - 'Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow'; + 'DobrodoÅ¡li pri izbiri vrste mostu: brez, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Začnete beleženje paketov v datotekovni sistem.'; + 'Začnete beleženje paketov v datotekovni sistem.'; @override String get repeater_cliHelpLogStop => - 'Ustavite beleženje paketov v datotečno sistem.'; + 'Ustavite beleženje paketov v datotečno sistem.'; @override String get repeater_cliHelpLogErase => - 'Izbriše pakete zapisov iz datotek sistema.'; + 'IzbriÅ¡e pakete zapisov iz datotek sistema.'; @override String get repeater_cliHelpNeighbors => - 'Prikaže seznam drugih ponovnih knopov, do katerih je prišlo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4'; + 'Prikaže seznam drugih ponovnih knopov, do katerih je priÅ¡lo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Izbriše prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.'; + 'IzbriÅ¡e prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.'; @override String get repeater_cliHelpRegion => @@ -2313,11 +2311,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s pošiljanjem praznega reda/ukaza.'; + 'Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s poÅ¡iljanjem praznega reda/ukaza.'; @override String get repeater_cliHelpRegionGet => - 'Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) \'F\'\"'; + 'Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) \'F\'\"'; @override String get repeater_cliHelpRegionPut => @@ -2325,7 +2323,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpRegionRemove => - 'Izbriše definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)'; + 'IzbriÅ¡e definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)'; @override String get repeater_cliHelpRegionAllowf => @@ -2333,11 +2331,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpRegionDenyf => - 'Odstrani dovoljenje \'F\'lood\' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)'; + 'Odstrani dovoljenje \'F\'lood\' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)'; @override String get repeater_cliHelpRegionHome => - 'Odgovori z trenutnim \'domovim\' območjem. (Opomba je bila še nujno uporabljena, rezervirano za prihodnost)'; + 'Odgovori z trenutnim \'domovim\' območjem. (Opomba je bila Å¡e nujno uporabljena, rezervirano za prihodnost)'; @override String get repeater_cliHelpRegionHomeSet => 'Nastavi regijo \'domov\'.'; @@ -2348,36 +2346,37 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Pokaže status GPS-ja. Če je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in štetjem satelitiv.'; + 'Pokaže status GPS-ja. ÄŒe je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in Å¡tetjem satelitiv.'; @override - String get repeater_cliHelpGpsOnOff => 'Omogoči/onameni GPS način delovanja.'; + String get repeater_cliHelpGpsOnOff => + 'Omogoči/onameni GPS način delovanja.'; @override String get repeater_cliHelpGpsSync => - 'Sinhronizira čas časa ničala z gps uro.'; + 'Sinhronizira čas časa ničala z gps uro.'; @override String get repeater_cliHelpGpsSetLoc => - 'Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.'; + 'Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.'; @override String get repeater_cliHelpGpsAdvert => - 'Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaševati lokacijo shranjeno v preferencah'; + 'Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaÅ¡evati lokacijo shranjeno v preferencah'; @override String get repeater_cliHelpGpsAdvertSet => - 'Nastavi konfiguracijo oglasa na določenem mestu.'; + 'Nastavi konfiguracijo oglasa na določenem mestu.'; @override String get repeater_commandsListTitle => 'Seznam ukazov'; @override String get repeater_commandsListNote => - 'Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".'; + 'Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".'; @override - String get repeater_general => 'Općenito'; + String get repeater_general => 'Općenito'; @override String get repeater_settingsCategory => 'Nastavitve'; @@ -2404,17 +2403,17 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_gpsNote => - 'GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.'; + 'GPS ukaz je bil uveden za upravljanje z vpraÅ¡anji, povezanimi z lokacijo.'; @override - String get telemetry_receivedData => 'Prejeto Telemetrično podatke'; + String get telemetry_receivedData => 'Prejeto Telemetrično podatke'; @override String get telemetry_requestTimeout => 'Zahtev telemetrije je iztekla.'; @override String telemetry_errorLoading(String error) { - return 'Napaka pri obnašanju telemetrije: $error'; + return 'Napaka pri obnaÅ¡anju telemetrije: $error'; } @override @@ -2457,7 +2456,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2469,7 +2468,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String neighbors_errorLoading(String error) { - return 'Napaka pri obnašanju sosedov: $error'; + return 'Napaka pri obnaÅ¡anju sosedov: $error'; } @override @@ -2485,14 +2484,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Udeleženec je prejel sporočilo $time nazaj.'; + return 'Udeleženec je prejel sporočilo $time nazaj.'; } @override String get channelPath_title => 'Pot do paketa'; @override - String get channelPath_viewMap => 'Prikaži zemljeznico'; + String get channelPath_viewMap => 'Prikaži zemljeznico'; @override String get channelPath_otherObservedPaths => 'Drugi opazovani poti'; @@ -2505,10 +2504,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Podrobnosti o paketu za dostavo niso navedene.'; @override - String get channelPath_messageDetails => 'Podrobnosti sporočila'; + String get channelPath_messageDetails => 'Podrobnosti sporočila'; @override - String get channelPath_senderLabel => 'Pošiljatelj'; + String get channelPath_senderLabel => 'PoÅ¡iljatelj'; @override String get channelPath_timeLabel => 'Ura'; @@ -2526,11 +2525,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Opazovana pot $index • $hops'; + return 'Opazovana pot $index • $hops'; } @override - String get channelPath_noLocationData => 'Nihče ni določil lokacije.'; + String get channelPath_noLocationData => 'Nihče ni določil lokacije.'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2581,7 +2580,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2602,14 +2601,14 @@ class AppLocalizationsSl extends AppLocalizations { 'Ustvari novo skupnost in jo deli preko QR kode.'; @override - String get community_join => 'Pridružiti se'; + String get community_join => 'Pridružiti se'; @override - String get community_joinTitle => 'Pridružite se skupnosti'; + String get community_joinTitle => 'Pridružite se skupnosti'; @override String community_joinConfirmation(String name) { - return 'Želiš se pridružiti skupnosti \"$name\"?'; + return 'ŽeliÅ¡ se pridružiti skupnosti \"$name\"?'; } @override @@ -2620,7 +2619,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Nasmerite kamero s skupnostnim QR kodom.'; @override - String get community_showQr => 'Pokaži QR kodo'; + String get community_showQr => 'Pokaži QR kodo'; @override String get community_publicChannel => 'Skupnostna javna'; @@ -2649,22 +2648,22 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Skenirajte to QR kodo za vključitev $name.'; + return 'Skenirajte to QR kodo za vključitev $name.'; } @override String get community_hashtagPrivacyHint => - 'Hashtag kanali skupnosti so dostopni samo članom skupnosti'; + 'Hashtag kanali skupnosti so dostopni samo članom skupnosti'; @override String get community_invalidQrCode => 'Neveljaven QR koden skupnosti'; @override - String get community_alreadyMember => 'Že član'; + String get community_alreadyMember => 'Že član'; @override String community_alreadyMemberMessage(String name) { - return 'Kljub temu ste že član/ka $name.'; + return 'Kljub temu ste že član/ka $name.'; } @override @@ -2675,11 +2674,12 @@ class AppLocalizationsSl extends AppLocalizations { 'Samodejno dodaj javni kanal za to skupnost.'; @override - String get community_noCommunities => 'Še nobena skupnost se ni pridružila.'; + String get community_noCommunities => + 'Å e nobena skupnost se ni pridružila.'; @override String get community_scanOrCreate => - 'Skeniraj QR kodo ali ustvari skupnost za začetek.'; + 'Skeniraj QR kodo ali ustvari skupnost za začetek.'; @override String get community_manageCommunities => 'Upravljanje skupnosti'; @@ -2694,7 +2694,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'To bo izbrisalo tudi $count kanal/kanalov in njihova sporočila.'; + return 'To bo izbrisalo tudi $count kanal/kanalov in njihova sporočila.'; } @override @@ -2707,7 +2707,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.'; + return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.'; } @override @@ -2719,7 +2719,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get community_updateSecret => 'Ažuriraj ključ'; + String get community_updateSecret => 'Ažuriraj ključ'; @override String community_secretUpdated(String name) { @@ -2728,7 +2728,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Skeniraj novo QR kodo za posodabljanje ključa za $name'; + return 'Skeniraj novo QR kodo za posodabljanje ključa za $name'; } @override @@ -2753,7 +2753,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get community_communityHashtagDesc => - 'Izključeno za uporabnike skupnosti'; + 'Izključeno za uporabnike skupnosti'; @override String community_forCommunity(String name) { @@ -2761,16 +2761,16 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get listFilter_tooltip => 'Filtri in vrstiči'; + String get listFilter_tooltip => 'Filtri in vrstiči'; @override String get listFilter_sortBy => 'Sortiraj po'; @override - String get listFilter_latestMessages => 'Najnovejše sporočilo'; + String get listFilter_latestMessages => 'NajnovejÅ¡e sporočilo'; @override - String get listFilter_heardRecently => 'Nedavno slišan'; + String get listFilter_heardRecently => 'Nedavno sliÅ¡an'; @override String get listFilter_az => 'A-Z'; @@ -2815,17 +2815,18 @@ class AppLocalizationsSl extends AppLocalizations { String get pathTrace_notAvailable => 'Potni sled ni na voljo.'; @override - String get pathTrace_refreshTooltip => 'Osveži Path Trace.'; + String get pathTrace_refreshTooltip => 'Osveži Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Ena ali več hmelju manjka lokacija!'; + 'Ena ali več hmelju manjka lokacija!'; @override - String get pathTrace_clearTooltip => 'Počisti pot'; + String get pathTrace_clearTooltip => 'Počisti pot'; @override - String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.'; + String get losSelectStartEnd => + 'Izberite začetno in končno vozlišče za LOS.'; @override String losRunFailed(String error) { @@ -2833,24 +2834,24 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get losClearAllPoints => 'Počisti vse točke'; + String get losClearAllPoints => 'Počisti vse točke'; @override String get losRunToViewElevationProfile => - 'Zaženite LOS za ogled višinskega profila'; + 'Zaženite LOS za ogled viÅ¡inskega profila'; @override String get losMenuTitle => 'LOS meni'; @override String get losMenuSubtitle => - 'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri'; + 'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri'; @override - String get losShowDisplayNodes => 'Pokaži prikazna vozlišča'; + String get losShowDisplayNodes => 'Pokaži prikazna vozlišča'; @override - String get losCustomPoints => 'Točke po meri'; + String get losCustomPoints => 'Točke po meri'; @override String losCustomPointLabel(int index) { @@ -2858,10 +2859,10 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get losPointA => 'Točka A'; + String get losPointA => 'Točka A'; @override - String get losPointB => 'Točka B'; + String get losPointB => 'Točka B'; @override String losAntennaA(String value, String unit) { @@ -2874,10 +2875,10 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get losRun => 'Zaženi LOS'; + String get losRun => 'Zaženi LOS'; @override - String get losNoElevationData => 'Ni podatkov o višini'; + String get losNoElevationData => 'Ni podatkov o viÅ¡ini'; @override String losProfileClear( @@ -2886,7 +2887,7 @@ class AppLocalizationsSl extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit'; + return '$distance $distanceUnit, čisti LOS, najmanjÅ¡a razdalja $clearance $heightUnit'; } @override @@ -2912,27 +2913,27 @@ class AppLocalizationsSl extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.'; + 'Podatki o nadmorski viÅ¡ini niso na voljo za enega ali več vzorcev.'; @override String get losErrorInvalidInput => - 'Neveljavni podatki o točkah/višini za izračun LOS.'; + 'Neveljavni podatki o točkah/viÅ¡ini za izračun LOS.'; @override - String get losRenameCustomPoint => 'Preimenujte točko po meri'; + String get losRenameCustomPoint => 'Preimenujte točko po meri'; @override - String get losPointName => 'Ime točke'; + String get losPointName => 'Ime točke'; @override - String get losShowPanelTooltip => 'Pokaži ploščo LOS'; + String get losShowPanelTooltip => 'Pokaži ploščo LOS'; @override - String get losHidePanelTooltip => 'Skrij ploščo LOS'; + String get losHidePanelTooltip => 'Skrij ploščo LOS'; @override String get losElevationAttribution => - 'Podatki o višini: Open-Meteo (CC BY 4.0)'; + 'Podatki o viÅ¡ini: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Radijski horizont'; @@ -2947,10 +2948,10 @@ class AppLocalizationsSl extends AppLocalizations { String get losFrequencyLabel => 'Frekvenca'; @override - String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; + String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; @override - String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; + String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; @override String losFrequencyDialogDescription( @@ -2959,7 +2960,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; + return 'ZačenÅ¡i od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; } @override @@ -2975,13 +2976,13 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_repeaterPing => 'Pinguj ponavljalnik'; @override - String get contacts_roomPathTrace => 'Sledenje poti do strežnika sobe'; + String get contacts_roomPathTrace => 'Sledenje poti do strežnika sobe'; @override - String get contacts_roomPing => 'Ping strežnik sobe'; + String get contacts_roomPing => 'Ping strežnik sobe'; @override - String get contacts_chatTraceRoute => 'Slediti poti žarkov'; + String get contacts_chatTraceRoute => 'Slediti poti žarkov'; @override String contacts_pathTraceTo(String name) { @@ -2989,31 +2990,31 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'Odložišče je prazno.'; + String get contacts_clipboardEmpty => 'Odložišče je prazno.'; @override String get contacts_invalidAdvertFormat => 'Neveljavni kontaktne podatke'; @override - String get contacts_contactImported => 'Kontakt je bil uvožen.'; + String get contacts_contactImported => 'Kontakt je bil uvožen.'; @override - String get contacts_contactImportFailed => 'Kontakt ni bil uspešno uvožen.'; + String get contacts_contactImportFailed => 'Kontakt ni bil uspeÅ¡no uvožen.'; @override String get contacts_zeroHopAdvert => 'Reklama brez posrednikov'; @override - String get contacts_floodAdvert => 'Poplavna oglás'; + String get contacts_floodAdvert => 'Poplavna oglás'; @override - String get contacts_copyAdvertToClipboard => 'Kopiraj oglas v odložišče'; + String get contacts_copyAdvertToClipboard => 'Kopiraj oglas v odložišče'; @override - String get contacts_addContactFromClipboard => 'Dodaj stik iz odložišča'; + String get contacts_addContactFromClipboard => 'Dodaj stik iz odložišča'; @override - String get contacts_ShareContact => 'Kopiraj stik v Odložišče'; + String get contacts_ShareContact => 'Kopiraj stik v Odložišče'; @override String get contacts_ShareContactZeroHop => 'Deliti kontakt prek oglasa'; @@ -3023,15 +3024,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_zeroHopContactAdvertFailed => - 'Pošiljanje kontakta ni uspelo.'; + 'PoÅ¡iljanje kontakta ni uspelo.'; @override String get contacts_contactAdvertCopied => - 'Oglas je bil kopiran v odložišče.'; + 'Oglas je bil kopiran v odložišče.'; @override String get contacts_contactAdvertCopyFailed => - 'Kopiranje oglasa v odložišče je spodletelo.'; + 'Kopiranje oglasa v odložišče je spodletelo.'; @override String get notification_activityTitle => 'Aktivnost MeshCore'; @@ -3041,10 +3042,10 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'sporočil', - few: 'sporočila', - two: 'sporočili', - one: 'sporočilo', + other: 'sporočil', + few: 'sporočila', + two: 'sporočili', + one: 'sporočilo', ); return '$count $_temp0'; } @@ -3054,10 +3055,10 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'sporočil kanala', - few: 'sporočila kanala', - two: 'sporočili kanala', - one: 'sporočilo kanala', + other: 'sporočil kanala', + few: 'sporočila kanala', + two: 'sporočili kanala', + one: 'sporočilo kanala', ); return '$count $_temp0'; } @@ -3067,10 +3068,10 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'novih vozlišč', - few: 'nova vozlišča', - two: 'novi vozlišči', - one: 'novo vozlišče', + other: 'novih vozlišč', + few: 'nova vozlišča', + two: 'novi vozlišči', + one: 'novo vozlišče', ); return '$count $_temp0'; } @@ -3081,15 +3082,15 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get notification_receivedNewMessage => 'Prejeto novo sporočilo'; + String get notification_receivedNewMessage => 'Prejeto novo sporočilo'; @override String get settings_gpxExportRepeaters => - 'Izvoz ponoviteljev / strežnika sobe v GPX'; + 'Izvoz ponoviteljev / strežnika sobe v GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.'; + 'Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.'; @override String get settings_gpxExportContacts => 'Izvoz spremljevalcev v GPX'; @@ -3106,21 +3107,21 @@ class AppLocalizationsSl extends AppLocalizations { 'Izvozi vse kontakte z lokacijo v datoteko GPX.'; @override - String get settings_gpxExportSuccess => 'Uspešno izvoz GPX datoteke.'; + String get settings_gpxExportSuccess => 'UspeÅ¡no izvoz GPX datoteke.'; @override String get settings_gpxExportNoContacts => 'Ni stikov za izvoz.'; @override String get settings_gpxExportNotAvailable => - 'Ni podprto na vašem napravi/operacijskem sistemu'; + 'Ni podprto na vaÅ¡em napravi/operacijskem sistemu'; @override - String get settings_gpxExportError => 'Pri izvozu je prišlo do napake.'; + String get settings_gpxExportError => 'Pri izvozu je priÅ¡lo do napake.'; @override String get settings_gpxExportRepeatersRoom => - 'Lokacije ponovljivca in strežnika sobe'; + 'Lokacije ponovljivca in strežnika sobe'; @override String get settings_gpxExportChat => 'Lokacije spremljevalcev'; @@ -3130,15 +3131,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_gpxExportShareText => - 'Podatki kart izvoženi iz meshcore-open'; + 'Podatki kart izvoženi iz meshcore-open'; @override String get settings_gpxExportShareSubject => 'meshcore-open izvoz podatkov GPX karte'; @override - String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji'; + String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji'; @override - String get snrIndicator_lastSeen => 'Zadnjič videno'; + String get snrIndicator_lastSeen => 'Zadnjič videno'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 9bfc64d..3fab6c2 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -30,7 +30,7 @@ class AppLocalizationsSv extends AppLocalizations { String get common_connect => 'Anslut'; @override - String get common_unknownDevice => 'Okänd enhet'; + String get common_unknownDevice => 'Okänd enhet'; @override String get common_save => 'Spara'; @@ -39,19 +39,19 @@ class AppLocalizationsSv extends AppLocalizations { String get common_delete => 'Radera'; @override - String get common_close => 'Stänga'; + String get common_close => 'Stänga'; @override String get common_edit => 'Redigera'; @override - String get common_add => 'Lägg till'; + String get common_add => 'Lägg till'; @override - String get common_settings => 'Inställningar'; + String get common_settings => 'Inställningar'; @override - String get common_disconnect => 'Koppla från'; + String get common_disconnect => 'Koppla frÃ¥n'; @override String get common_connected => 'Ansluten'; @@ -63,7 +63,7 @@ class AppLocalizationsSv extends AppLocalizations { String get common_create => 'Skapa'; @override - String get common_continue => 'Fortsätt'; + String get common_continue => 'Fortsätt'; @override String get common_share => 'Dela'; @@ -72,10 +72,10 @@ class AppLocalizationsSv extends AppLocalizations { String get common_copy => 'Kopiera'; @override - String get common_retry => 'Försök igen'; + String get common_retry => 'Försök igen'; @override - String get common_hide => 'Dölj'; + String get common_hide => 'Dölj'; @override String get common_remove => 'Ta bort'; @@ -93,7 +93,7 @@ class AppLocalizationsSv extends AppLocalizations { String get common_loading => 'Laddar...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -108,13 +108,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Välj din anslutningsmetod'; - - @override - String get connectionChoiceSubtitle => - 'Välj hur du vill komma åt din MeshCore-enhet.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -126,21 +119,21 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbScreenSubtitle => - 'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.'; + 'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.'; @override - String get usbScreenStatus => 'Välj en USB-enhet'; + String get usbScreenStatus => 'Välj en USB-enhet'; @override String get usbScreenNote => - 'USB-seriell kommunikation är aktiv på kompatibla Android-enheter och datorplattformar.'; + 'USB-seriell kommunikation är aktiv pÃ¥ kompatibla Android-enheter och datorplattformar.'; @override String get usbScreenEmptyState => 'Inga USB-enheter hittades. Anslut en och uppdatera.'; @override - String get scanner_scanning => 'Söker efter enheter...'; + String get scanner_scanning => 'Söker efter enheter...'; @override String get scanner_connecting => 'Anslutning...'; @@ -157,10 +150,11 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get scanner_searchingDevices => 'Söker efter MeshCore-enheter...'; + String get scanner_searchingDevices => 'Söker efter MeshCore-enheter...'; @override - String get scanner_tapToScan => 'Tryck Skanna för att hitta MeshCore-enheter'; + String get scanner_tapToScan => + 'Tryck Skanna för att hitta MeshCore-enheter'; @override String scanner_connectionFailed(String error) { @@ -174,43 +168,43 @@ class AppLocalizationsSv extends AppLocalizations { String get scanner_scan => 'Skanna'; @override - String get scanner_bluetoothOff => 'Bluetooth är avstängt'; + String get scanner_bluetoothOff => 'Bluetooth är avstängt'; @override String get scanner_bluetoothOffMessage => - 'Vänligen aktivera Bluetooth för att söka efter enheter.'; + 'Vänligen aktivera Bluetooth för att söka efter enheter.'; @override - String get scanner_chromeRequired => 'Chrome-webbläsare krävs'; + String get scanner_chromeRequired => 'Chrome-webbläsare krävs'; @override String get scanner_chromeRequiredMessage => - 'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.'; + 'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.'; @override String get scanner_enableBluetooth => 'Aktivera Bluetooth'; @override - String get device_quickSwitch => 'Snabb växling'; + String get device_quickSwitch => 'Snabb växling'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Inställningar'; + String get settings_title => 'Inställningar'; @override String get settings_deviceInfo => 'Enhetens information'; @override - String get settings_appSettings => 'Appinställningar'; + String get settings_appSettings => 'Appinställningar'; @override String get settings_appSettingsSubtitle => - 'Meddelanden, notiser och kartinställningar'; + 'Meddelanden, notiser och kartinställningar'; @override - String get settings_nodeSettings => 'Nodinställningar'; + String get settings_nodeSettings => 'Nodinställningar'; @override String get settings_nodeName => 'Nodnamn'; @@ -225,7 +219,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_nodeNameUpdated => 'Namn uppdaterat'; @override - String get settings_radioSettings => 'Radioinställningar'; + String get settings_radioSettings => 'Radioinställningar'; @override String get settings_radioSettingsSubtitle => @@ -233,7 +227,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_radioSettingsUpdated => - 'Radioinställningarna har uppdaterats'; + 'Radioinställningarna har uppdaterats'; @override String get settings_location => 'Plats'; @@ -245,7 +239,8 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_locationUpdated => 'Plats uppdaterad'; @override - String get settings_locationBothRequired => 'Ange både latitud och longitud.'; + String get settings_locationBothRequired => + 'Ange bÃ¥de latitud och longitud.'; @override String get settings_locationInvalid => 'Ogiltig latitud eller longitud.'; @@ -255,45 +250,45 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.'; + 'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.'; @override - String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)'; + String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)'; @override String get settings_locationIntervalInvalid => - 'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.'; + 'Intervalet mÃ¥ste vara minst 60 sekunder och mindre än 86400 sekunder.'; @override String get settings_latitude => 'Latitud'; @override - String get settings_longitude => 'Längdgrad'; + String get settings_longitude => 'Längdgrad'; @override - String get settings_privacyMode => 'Privatläge'; + String get settings_privacyMode => 'Privatläge'; @override - String get settings_privacyModeSubtitle => 'Dölj namn/plats i annonser'; + String get settings_privacyModeSubtitle => 'Dölj namn/plats i annonser'; @override String get settings_privacyModeToggle => - 'Aktivera privatläge för att dölja ditt namn och din plats i annonser.'; + 'Aktivera privatläge för att dölja ditt namn och din plats i annonser.'; @override - String get settings_privacyModeEnabled => 'Privatläget är aktiverat'; + String get settings_privacyModeEnabled => 'Privatläget är aktiverat'; @override - String get settings_privacyModeDisabled => 'Privatläge är avstängt'; + String get settings_privacyModeDisabled => 'Privatläge är avstängt'; @override - String get settings_actions => 'Åtgärder'; + String get settings_actions => 'Ã…tgärder'; @override String get settings_sendAdvertisement => 'Skicka Annons'; @override - String get settings_sendAdvertisementSubtitle => 'Sändning finns nu'; + String get settings_sendAdvertisementSubtitle => 'Sändning finns nu'; @override String get settings_advertisementSent => 'Annons skickad'; @@ -302,7 +297,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_syncTime => 'Synkroniseringstid'; @override - String get settings_syncTimeSubtitle => 'Ställ enheten till telefonens tid'; + String get settings_syncTimeSubtitle => 'Ställ enheten till telefonens tid'; @override String get settings_timeSynchronized => 'Tidssynkroniserat'; @@ -312,7 +307,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_refreshContactsSubtitle => - 'Ladda om kontaktlistan från enheten'; + 'Ladda om kontaktlistan frÃ¥n enheten'; @override String get settings_rebootDevice => 'Starta om enheten'; @@ -322,23 +317,23 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - 'Är du säker på att du vill starta om enheten? Du kommer att bli avkopplad.'; + 'Är du säker pÃ¥ att du vill starta om enheten? Du kommer att bli avkopplad.'; @override - String get settings_debug => 'Felsök'; + String get settings_debug => 'Felsök'; @override - String get settings_bleDebugLog => 'BLE-felsökning'; + String get settings_bleDebugLog => 'BLE-felsökning'; @override - String get settings_bleDebugLogSubtitle => 'BLE-kommandon, svar och rådata'; + String get settings_bleDebugLogSubtitle => 'BLE-kommandon, svar och rÃ¥data'; @override - String get settings_appDebugLog => 'Appfelsökning'; + String get settings_appDebugLog => 'Appfelsökning'; @override String get settings_appDebugLogSubtitle => - 'Applikations felsökningsmeddelanden'; + 'Applikations felsökningsmeddelanden'; @override String get settings_about => 'Om'; @@ -349,15 +344,15 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get settings_aboutLegalese => '2024 MeshCore Öppen Källkodsprojekt'; + String get settings_aboutLegalese => '2024 MeshCore Öppen Källkodsprojekt'; @override String get settings_aboutDescription => - 'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.'; + 'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.'; @override String get settings_aboutOpenMeteoAttribution => - 'LOS-höjddata: Open-Meteo (CC BY 4.0)'; + 'LOS-höjddata: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Namn'; @@ -372,7 +367,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_infoBattery => 'Batteri'; @override - String get settings_infoPublicKey => 'Allmänt nyckel'; + String get settings_infoPublicKey => 'Allmänt nyckel'; @override String get settings_infoContactsCount => 'Kontakterantal'; @@ -381,7 +376,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_infoChannelCount => 'Kanalantal'; @override - String get settings_presets => 'Fördefinierade inställningar'; + String get settings_presets => 'Fördefinierade inställningar'; @override String get settings_frequency => 'Frekvens (MHz)'; @@ -411,15 +406,15 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)'; @override - String get settings_clientRepeat => 'Upprepa utan elnät'; + String get settings_clientRepeat => 'Upprepa utan elnät'; @override String get settings_clientRepeatSubtitle => - 'Låt enheten repetera nätpaket för andra användare.'; + 'LÃ¥t enheten repetera nätpaket för andra användare.'; @override String get settings_clientRepeatFreqWarning => - 'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.'; + 'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.'; @override String settings_error(String message) { @@ -427,7 +422,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get appSettings_title => 'Appinställningar'; + String get appSettings_title => 'Appinställningar'; @override String get appSettings_appearance => 'Utseende'; @@ -442,10 +437,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_themeLight => 'Ljus'; @override - String get appSettings_themeDark => 'Mörk'; + String get appSettings_themeDark => 'Mörk'; @override - String get appSettings_language => 'Språk'; + String get appSettings_language => 'SprÃ¥k'; @override String get appSettings_languageSystem => 'Systemstandard'; @@ -454,10 +449,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -466,16 +461,16 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -484,10 +479,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Ryska'; @@ -496,11 +491,11 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageUk => 'Ukrainska'; @override - String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning'; + String get appSettings_enableMessageTracing => 'Aktivera meddelandespÃ¥rning'; @override String get appSettings_enableMessageTracingSubtitle => - 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; + 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; @override String get appSettings_notifications => 'Meddelanden'; @@ -510,71 +505,71 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_enableNotificationsSubtitle => - 'Ta emot notiser för meddelanden och reklam'; + 'Ta emot notiser för meddelanden och reklam'; @override String get appSettings_notificationPermissionDenied => - 'Tillåtelse för notifikationer nekad'; + 'TillÃ¥telse för notifikationer nekad'; @override String get appSettings_notificationsEnabled => 'Notifikationer aktiverade'; @override - String get appSettings_notificationsDisabled => 'Meddelanden är avstängda'; + String get appSettings_notificationsDisabled => 'Meddelanden är avstängda'; @override String get appSettings_messageNotifications => 'Meddelandekrav'; @override String get appSettings_messageNotificationsSubtitle => - 'Visa notis när nya meddelanden tas emot'; + 'Visa notis när nya meddelanden tas emot'; @override String get appSettings_channelMessageNotifications => 'Kanalmeddelandena'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Visa notis när meddelanden i kanal mottas'; + 'Visa notis när meddelanden i kanal mottas'; @override String get appSettings_advertisementNotifications => 'Annonsmeddelanden'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Visa notis när nya noder upptäcks'; + 'Visa notis när nya noder upptäcks'; @override String get appSettings_messaging => 'Meddelanden'; @override - String get appSettings_clearPathOnMaxRetry => 'Rensa Vägen på Max Försök'; + String get appSettings_clearPathOnMaxRetry => 'Rensa Vägen pÃ¥ Max Försök'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Återställ kontaktväg efter 5 misslyckade försök att skicka'; + 'Ã…terställ kontaktväg efter 5 misslyckade försök att skicka'; @override String get appSettings_pathsWillBeCleared => - 'Sökvägar kommer att tömmas efter 5 misslyckade försök.'; + 'Sökvägar kommer att tömmas efter 5 misslyckade försök.'; @override String get appSettings_pathsWillNotBeCleared => - 'Sökvägar kommer inte att rensas automatiskt.'; + 'Sökvägar kommer inte att rensas automatiskt.'; @override - String get appSettings_autoRouteRotation => 'Automatisk Rutväxling'; + String get appSettings_autoRouteRotation => 'Automatisk Rutväxling'; @override String get appSettings_autoRouteRotationSubtitle => - 'Blixtra mellan bästa vägar och flödesläge'; + 'Blixtra mellan bästa vägar och flödesläge'; @override String get appSettings_autoRouteRotationEnabled => - 'Automatisk ruttrotation är aktiverad'; + 'Automatisk ruttrotation är aktiverad'; @override String get appSettings_autoRouteRotationDisabled => - 'Automatisk ruttrotation är avstängd'; + 'Automatisk ruttrotation är avstängd'; @override String get appSettings_battery => 'Batteri'; @@ -584,18 +579,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Ställ in per enhet ($deviceName)'; + return 'Ställ in per enhet ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Anslut till en enhet för att välja'; + 'Anslut till en enhet för att välja'; @override String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; @override String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; @@ -604,24 +599,24 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_mapDisplay => 'Kartvisning'; @override - String get appSettings_showRepeaters => 'Visa återuppslag'; + String get appSettings_showRepeaters => 'Visa Ã¥teruppslag'; @override String get appSettings_showRepeatersSubtitle => - 'Visa återspelsnoder på kartan'; + 'Visa Ã¥terspelsnoder pÃ¥ kartan'; @override String get appSettings_showChatNodes => 'Visa Chattnoder'; @override - String get appSettings_showChatNodesSubtitle => 'Visa chattnoder på kartan'; + String get appSettings_showChatNodesSubtitle => 'Visa chattnoder pÃ¥ kartan'; @override String get appSettings_showOtherNodes => 'Visa andra noder'; @override String get appSettings_showOtherNodesSubtitle => - 'Visa andra nodtyper på kartan'; + 'Visa andra nodtyper pÃ¥ kartan'; @override String get appSettings_timeFilter => 'Tidsfilter'; @@ -631,7 +626,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String appSettings_timeFilterShowLast(int hours) { - return 'Visa noder från de senaste $hours timmarna'; + return 'Visa noder frÃ¥n de senaste $hours timmarna'; } @override @@ -639,7 +634,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_showNodesDiscoveredWithin => - 'Visa noder som upptäckts inom:'; + 'Visa noder som upptäckts inom:'; @override String get appSettings_allTime => 'Totalen'; @@ -654,7 +649,7 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_last24Hours => 'De senaste 24 timmarna'; @override - String get appSettings_lastWeek => 'Förra veckan'; + String get appSettings_lastWeek => 'Förra veckan'; @override String get appSettings_offlineMapCache => 'Offline Kartcache'; @@ -673,70 +668,70 @@ class AppLocalizationsSv extends AppLocalizations { @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Område markerat (zoom $minZoom-$maxZoom)'; + return 'OmrÃ¥de markerat (zoom $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Felsök'; + String get appSettings_debugCard => 'Felsök'; @override - String get appSettings_appDebugLogging => 'App-felsökning och loggning'; + String get appSettings_appDebugLogging => 'App-felsökning och loggning'; @override String get appSettings_appDebugLoggingSubtitle => - 'Logga appens felsökningsmeddelanden för felsökning'; + 'Logga appens felsökningsmeddelanden för felsökning'; @override String get appSettings_appDebugLoggingEnabled => - 'App felsökning loggning aktiverad'; + 'App felsökning loggning aktiverad'; @override String get appSettings_appDebugLoggingDisabled => - 'App felsökning är avstängd'; + 'App felsökning är avstängd'; @override String get contacts_title => 'Kontakter'; @override - String get contacts_noContacts => 'Inga kontakter ännu'; + String get contacts_noContacts => 'Inga kontakter ännu'; @override String get contacts_contactsWillAppear => - 'Kontakter kommer att visas när enheter annonserar.'; + 'Kontakter kommer att visas när enheter annonserar.'; @override - String get contacts_unread => 'Oläst'; + String get contacts_unread => 'Oläst'; @override - String get contacts_searchContactsNoNumber => 'Sök kontakter...'; + String get contacts_searchContactsNoNumber => 'Sök kontakter...'; @override String contacts_searchContacts(int number, String str) { - return 'Sök kontakter...'; + return 'Sök kontakter...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Sök $number$str Favoriter...'; + return 'Sök $number$str Favoriter...'; } @override String contacts_searchUsers(int number, String str) { - return 'Sök $number$str användare...'; + return 'Sök $number$str användare...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Sök $number$str upprepningsenheter...'; + return 'Sök $number$str upprepningsenheter...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Sök $number$str Room-servrar...'; + return 'Sök $number$str Room-servrar...'; } @override - String get contacts_noUnreadContacts => 'Inga oinlästa kontakter'; + String get contacts_noUnreadContacts => 'Inga oinlästa kontakter'; @override String get contacts_noContactsFound => @@ -747,7 +742,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String contacts_removeConfirm(String contactName) { - return 'Ta bort $contactName från kontakter?'; + return 'Ta bort $contactName frÃ¥n kontakter?'; } @override @@ -760,7 +755,7 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_roomLogin => 'Rum Inloggning'; @override - String get contacts_openChat => 'Öppna Chatt'; + String get contacts_openChat => 'Öppna Chatt'; @override String get contacts_editGroup => 'Redigera Grupp'; @@ -780,7 +775,7 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_groupName => 'Gruppnamn'; @override - String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt'; + String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt'; @override String contacts_groupAlreadyExists(String name) { @@ -806,7 +801,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get contacts_lastSeenHourAgo => 'Senast sedd för 1 timme sedan'; + String get contacts_lastSeenHourAgo => 'Senast sedd för 1 timme sedan'; @override String contacts_lastSeenHoursAgo(int hours) { @@ -814,7 +809,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get contacts_lastSeenDayAgo => 'Senast sedd för 1 dag sedan'; + String get contacts_lastSeenDayAgo => 'Senast sedd för 1 dag sedan'; @override String contacts_lastSeenDaysAgo(int days) { @@ -828,10 +823,10 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_noChannelsConfigured => 'Inga kanaler konfigurerade'; @override - String get channels_addPublicChannel => 'Lägg till publik kanal'; + String get channels_addPublicChannel => 'Lägg till publik kanal'; @override - String get channels_searchChannels => 'Sök kanaler...'; + String get channels_searchChannels => 'Sök kanaler...'; @override String get channels_noChannelsFound => 'Inga kanaler hittades'; @@ -851,7 +846,7 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_private => 'Privat'; @override - String get channels_publicChannel => 'Allmänt kanal'; + String get channels_publicChannel => 'Allmänt kanal'; @override String get channels_privateChannel => 'Privat kanal'; @@ -863,14 +858,14 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_muteChannel => 'Tysta kanal'; @override - String get channels_unmuteChannel => 'Slå på ljud för kanal'; + String get channels_unmuteChannel => 'SlÃ¥ pÃ¥ ljud för kanal'; @override String get channels_deleteChannel => 'Ta bort kanal'; @override String channels_deleteChannelConfirm(String name) { - return 'Radera \"$name\"? Detta kan inte ångras.'; + return 'Radera \"$name\"? Detta kan inte Ã¥ngras.'; } @override @@ -884,7 +879,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channels_addChannel => 'Lägg till kanal'; + String get channels_addChannel => 'Lägg till kanal'; @override String get channels_channelIndexLabel => 'Kanalindex'; @@ -893,22 +888,23 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_channelName => 'Kanalnamn'; @override - String get channels_usePublicChannel => 'Använd Publikkanal'; + String get channels_usePublicChannel => 'Använd Publikkanal'; @override - String get channels_standardPublicPsk => 'Standard allmän PSK'; + String get channels_standardPublicPsk => 'Standard allmän PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Generera slumpmässig PSK'; + String get channels_generateRandomPsk => 'Generera slumpmässig PSK'; @override String get channels_enterChannelName => 'Ange en kanalnamn'; @override - String get channels_pskMustBe32Hex => 'PSK måste vara 32 hexadecimala tecken'; + String get channels_pskMustBe32Hex => + 'PSK mÃ¥ste vara 32 hexadecimala tecken'; @override String channels_channelAdded(String name) { @@ -929,7 +925,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Allmänt kanal tillagd'; + String get channels_publicChannelAdded => 'Allmänt kanal tillagd'; @override String get channels_sortBy => 'Sortera efter'; @@ -944,7 +940,7 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_sortLatestMessages => 'Senaste meddelanden'; @override - String get channels_sortUnread => 'Oläst'; + String get channels_sortUnread => 'Oläst'; @override String get channels_createPrivateChannel => 'Skapa en privat kanal'; @@ -954,25 +950,25 @@ class AppLocalizationsSv extends AppLocalizations { 'Skyddat med en hemlig nyckel.'; @override - String get channels_joinPrivateChannel => 'Gå med i en Privat Kanal'; + String get channels_joinPrivateChannel => 'GÃ¥ med i en Privat Kanal'; @override String get channels_joinPrivateChannelDesc => 'Ange en hemlig nyckel manuellt.'; @override - String get channels_joinPublicChannel => 'Gå med i den Offentliga Kanalen'; + String get channels_joinPublicChannel => 'GÃ¥ med i den Offentliga Kanalen'; @override String get channels_joinPublicChannelDesc => - 'Vem som helst kan gå med i denna kanal.'; + 'Vem som helst kan gÃ¥ med i denna kanal.'; @override - String get channels_joinHashtagChannel => 'Gå med i en Hashtagkanal'; + String get channels_joinHashtagChannel => 'GÃ¥ med i en Hashtagkanal'; @override String get channels_joinHashtagChannelDesc => - 'Väldigt enkelt att gå med i hashtag-kanaler.'; + 'Väldigt enkelt att gÃ¥ med i hashtag-kanaler.'; @override String get channels_scanQrCode => 'Skanna en QR-kod'; @@ -987,11 +983,11 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_hashtagHint => 't.ex. #team'; @override - String get chat_noMessages => 'Inga meddelanden ännu'; + String get chat_noMessages => 'Inga meddelanden ännu'; @override String get chat_sendMessageToStart => - 'Skicka ett meddelande för att komma igång'; + 'Skicka ett meddelande för att komma igÃ¥ng'; @override String get chat_originalMessageNotFound => @@ -1020,7 +1016,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return 'Meddelandet är för långt (max $maxBytes byte).'; + return 'Meddelandet är för lÃ¥ngt (max $maxBytes byte).'; } @override @@ -1030,11 +1026,11 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_messageDeleted => 'Meddelandet raderat'; @override - String get chat_retryingMessage => 'Försöker igen'; + String get chat_retryingMessage => 'Försöker igen'; @override String chat_retryCount(int current, int max) { - return 'Försök igen $current/$max'; + return 'Försök igen $current/$max'; } @override @@ -1044,7 +1040,7 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_reply => 'Svara'; @override - String get chat_addReaction => 'Lägg till reaktion'; + String get chat_addReaction => 'Lägg till reaktion'; @override String get chat_me => 'Mig'; @@ -1056,16 +1052,16 @@ class AppLocalizationsSv extends AppLocalizations { String get emojiCategoryGestures => 'Gestikuleringar'; @override - String get emojiCategoryHearts => 'Hjärtan'; + String get emojiCategoryHearts => 'Hjärtan'; @override String get emojiCategoryObjects => 'Objekt'; @override - String get gifPicker_title => 'Välj en GIF'; + String get gifPicker_title => 'Välj en GIF'; @override - String get gifPicker_searchHint => 'Sök GIF:ar...'; + String get gifPicker_searchHint => 'Sök GIF:ar...'; @override String get gifPicker_poweredBy => 'Drivet av GIPHY'; @@ -1077,16 +1073,16 @@ class AppLocalizationsSv extends AppLocalizations { String get gifPicker_failedLoad => 'Kunde inte ladda GIF-filer'; @override - String get gifPicker_failedSearch => 'Sökningen misslyckades.'; + String get gifPicker_failedSearch => 'Sökningen misslyckades.'; @override String get gifPicker_noInternet => 'Ingen internetanslutning'; @override - String get debugLog_appTitle => 'Appfelsökning'; + String get debugLog_appTitle => 'Appfelsökning'; @override - String get debugLog_bleTitle => 'BLE-felsökning'; + String get debugLog_bleTitle => 'BLE-felsökning'; @override String get debugLog_copyLog => 'Kopiera logg'; @@ -1095,26 +1091,26 @@ class AppLocalizationsSv extends AppLocalizations { String get debugLog_clearLog => 'Rensa logg'; @override - String get debugLog_copied => 'Felsökningslogg kopierad'; + String get debugLog_copied => 'Felsökningslogg kopierad'; @override String get debugLog_bleCopied => 'BLE-logg kopierad'; @override - String get debugLog_noEntries => 'Inga felsökningsloggar ännu'; + String get debugLog_noEntries => 'Inga felsökningsloggar ännu'; @override String get debugLog_enableInSettings => - 'Aktivera appens felsökningsloggning i inställningarna'; + 'Aktivera appens felsökningsloggning i inställningarna'; @override String get debugLog_frames => 'Rammar'; @override - String get debugLog_rawLogRx => 'Rå Log-RX'; + String get debugLog_rawLogRx => 'RÃ¥ Log-RX'; @override - String get debugLog_noBleActivity => 'Ingen BLE-aktivitet ännu'; + String get debugLog_noBleActivity => 'Ingen BLE-aktivitet ännu'; @override String debugFrame_length(int count) { @@ -1127,16 +1123,16 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => 'Textmeddelandefält:'; + String get debugFrame_textMessageHeader => 'Textmeddelandefält:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '– Destination PubKey: $pubKey'; + return '– Destination PubKey: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Tidsstämpel: $timestamp'; + return '- Tidsstämpel: $timestamp'; } @override @@ -1167,24 +1163,24 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_pathManagement => 'Stigarhantering'; @override - String get chat_ShowAllPaths => 'Visa alla vägar'; + String get chat_ShowAllPaths => 'Visa alla vägar'; @override - String get chat_routingMode => 'Ruttläge'; + String get chat_routingMode => 'Ruttläge'; @override - String get chat_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; + String get chat_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; @override - String get chat_forceFloodMode => 'Tvinga Översvämningsläge'; + String get chat_forceFloodMode => 'Tvinga Översvämningsläge'; @override String get chat_recentAckPaths => - 'Nyligen Ack-vägar (tryck för att använda):'; + 'Nyligen Ack-vägar (tryck för att använda):'; @override String get chat_pathHistoryFull => - 'Historisk sökväg är full. Ta bort poster för att lägga till nya.'; + 'Historisk sökväg är full. Ta bort poster för att lägga till nya.'; @override String get chat_hopSingular => 'hoppa'; @@ -1204,47 +1200,49 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get chat_successes => 'framgångar'; + String get chat_successes => 'framgÃ¥ngar'; @override - String get chat_removePath => 'Ta bort sökväg'; + String get chat_removePath => 'Ta bort sökväg'; @override String get chat_noPathHistoryYet => - 'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.'; + 'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spÃ¥r.'; @override String get chat_pathActions => 'Stigar:'; @override - String get chat_setCustomPath => 'Ange anpassad sökväg'; + String get chat_setCustomPath => 'Ange anpassad sökväg'; @override - String get chat_setCustomPathSubtitle => 'Ange ruttväg manuellt'; + String get chat_setCustomPathSubtitle => 'Ange ruttväg manuellt'; @override - String get chat_clearPath => 'Rensa Vägen'; + String get chat_clearPath => 'Rensa Vägen'; @override - String get chat_clearPathSubtitle => 'Tvinga fram omstart vid nästa sändning'; + String get chat_clearPathSubtitle => + 'Tvinga fram omstart vid nästa sändning'; @override String get chat_pathCleared => - 'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.'; + 'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.'; @override - String get chat_floodModeSubtitle => 'Använd routningsomkopplaren i appraden'; + String get chat_floodModeSubtitle => + 'Använd routningsomkopplaren i appraden'; @override String get chat_floodModeEnabled => - 'Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.'; + 'Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.'; @override - String get chat_fullPath => 'Fullständig sökväg'; + String get chat_fullPath => 'Fullständig sökväg'; @override String get chat_pathDetailsNotAvailable => - 'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.'; + 'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1254,33 +1252,34 @@ class AppLocalizationsSv extends AppLocalizations { other: 'hoppar', one: 'hopp', ); - return 'Sökväg inställd: $hopCount $_temp0 - $status'; + return 'Sökväg inställd: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Sparat lokalt. Anslut för att synkronisera.'; + 'Sparat lokalt. Anslut för att synkronisera.'; @override - String get chat_pathDeviceConfirmed => 'Enheten bekräftad.'; + String get chat_pathDeviceConfirmed => 'Enheten bekräftad.'; @override - String get chat_pathDeviceNotConfirmed => 'Enheten har inte bekräftats ännu.'; + String get chat_pathDeviceNotConfirmed => + 'Enheten har inte bekräftats ännu.'; @override String get chat_type => 'Skriv'; @override - String get chat_path => 'Sökväg'; + String get chat_path => 'Sökväg'; @override - String get chat_publicKey => 'Allmänt nyckel'; + String get chat_publicKey => 'Allmänt nyckel'; @override - String get chat_compressOutgoingMessages => 'Kryptera utgående meddelanden'; + String get chat_compressOutgoingMessages => 'Kryptera utgÃ¥ende meddelanden'; @override - String get chat_floodForced => 'Översvämning (tvingad)'; + String get chat_floodForced => 'Översvämning (tvingad)'; @override String get chat_directForced => 'Direkt (tvingad)'; @@ -1291,7 +1290,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get chat_floodAuto => 'Översvämning (auto)'; + String get chat_floodAuto => 'Översvämning (auto)'; @override String get chat_direct => 'Direkt'; @@ -1301,26 +1300,26 @@ class AppLocalizationsSv extends AppLocalizations { @override String chat_unread(int count) { - return 'Olästa: $count'; + return 'Olästa: $count'; } @override - String get chat_openLink => 'Öppna länk?'; + String get chat_openLink => 'Öppna länk?'; @override String get chat_openLinkConfirmation => - 'Vill du öppna den här länken i din webbläsare?'; + 'Vill du öppna den här länken i din webbläsare?'; @override - String get chat_open => 'Öppna'; + String get chat_open => 'Öppna'; @override String chat_couldNotOpenLink(String url) { - return 'Kunde inte öppna länken: $url'; + return 'Kunde inte öppna länken: $url'; } @override - String get chat_invalidLink => 'Ogiltigt länkformat'; + String get chat_invalidLink => 'Ogiltigt länkformat'; @override String get map_title => 'Nodkarta'; @@ -1336,7 +1335,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_nodesNeedGps => - 'Noder måste dela sina GPS-koordinater\nför att visas på kartan'; + 'Noder mÃ¥ste dela sina GPS-koordinater\nför att visas pÃ¥ kartan'; @override String map_nodesCount(int count) { @@ -1352,7 +1351,7 @@ class AppLocalizationsSv extends AppLocalizations { String get map_chat => 'Chat'; @override - String get map_repeater => 'Återuppspelare'; + String get map_repeater => 'Ã…teruppspelare'; @override String get map_room => 'Rum'; @@ -1361,35 +1360,35 @@ class AppLocalizationsSv extends AppLocalizations { String get map_sensor => 'Sensor'; @override - String get map_pinDm => 'Lås (DM)'; + String get map_pinDm => 'LÃ¥s (DM)'; @override - String get map_pinPrivate => 'Lås (Privat)'; + String get map_pinPrivate => 'LÃ¥s (Privat)'; @override - String get map_pinPublic => 'Anslå (Offentligt)'; + String get map_pinPublic => 'AnslÃ¥ (Offentligt)'; @override String get map_lastSeen => 'Senast sedd'; @override String get map_disconnectConfirm => - 'Är du säker på att du vill koppla från enheten?'; + 'Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?'; @override - String get map_from => 'Från'; + String get map_from => 'FrÃ¥n'; @override - String get map_source => 'Källa'; + String get map_source => 'Källa'; @override String get map_flags => 'Flaggor'; @override - String get map_shareMarkerHere => 'Dela markeringen här'; + String get map_shareMarkerHere => 'Dela markeringen här'; @override - String get map_pinLabel => 'Fästetikett'; + String get map_pinLabel => 'Fästetikett'; @override String get map_label => 'Etikett'; @@ -1404,19 +1403,19 @@ class AppLocalizationsSv extends AppLocalizations { String get map_sendToChannel => 'Skicka till kanal'; @override - String get map_noChannelsAvailable => 'Inga kanaler tillgängliga'; + String get map_noChannelsAvailable => 'Inga kanaler tillgängliga'; @override String get map_publicLocationShare => 'Dela offentlig plats'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Du håller på att dela en plats i $channelLabel. Denna kanal är offentlig och alla med PSK kan se den.'; + return 'Du hÃ¥ller pÃ¥ att dela en plats i $channelLabel. Denna kanal är offentlig och alla med PSK kan se den.'; } @override String get map_connectToShareMarkers => - 'Anslut till en enhet för att dela markörer'; + 'Anslut till en enhet för att dela markörer'; @override String get map_filterNodes => 'Filtrera noder'; @@ -1440,13 +1439,13 @@ class AppLocalizationsSv extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtrera efter nyckelprefix'; @override - String get map_publicKeyPrefix => 'Allmänt nyckelprästegenskap'; + String get map_publicKeyPrefix => 'Allmänt nyckelprästegenskap'; @override - String get map_markers => 'Markörer'; + String get map_markers => 'Markörer'; @override - String get map_showSharedMarkers => 'Visa delade markörer'; + String get map_showSharedMarkers => 'Visa delade markörer'; @override String get map_lastSeenTime => 'Senaste Visats Tid'; @@ -1455,39 +1454,40 @@ class AppLocalizationsSv extends AppLocalizations { String get map_sharedPin => 'Delad PIN'; @override - String get map_joinRoom => 'Gå med i rum'; + String get map_joinRoom => 'GÃ¥ med i rum'; @override String get map_manageRepeater => 'Hantera Upprepare'; @override - String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.'; + String get map_tapToAdd => + 'Tryck pÃ¥ noder för att lägga till dem i banan.'; @override - String get map_runTrace => 'Kör spårsökning'; + String get map_runTrace => 'Kör spÃ¥rsökning'; @override String get map_removeLast => 'Ta bort sista'; @override - String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.'; + String get map_pathTraceCancelled => 'SökvägsspÃ¥rning avbruten.'; @override String get mapCache_title => 'Offline Kartcache'; @override - String get mapCache_selectAreaFirst => 'Välj ett område att cachera först'; + String get mapCache_selectAreaFirst => 'Välj ett omrÃ¥de att cachera först'; @override String get mapCache_noTilesToDownload => - 'Inga kuber att ladda ner för detta område'; + 'Inga kuber att ladda ner för detta omrÃ¥de'; @override String get mapCache_downloadTilesTitle => 'Ladda ner klick'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Ladda ner $count kuber för offlineanvändning?'; + return 'Ladda ner $count kuber för offlineanvändning?'; } @override @@ -1516,13 +1516,13 @@ class AppLocalizationsSv extends AppLocalizations { String get mapCache_noAreaSelected => 'Ingen area markerad'; @override - String get mapCache_cacheArea => 'Cacheområde'; + String get mapCache_cacheArea => 'CacheomrÃ¥de'; @override - String get mapCache_useCurrentView => 'Använd Aktuell Visning'; + String get mapCache_useCurrentView => 'Använd Aktuell Visning'; @override - String get mapCache_zoomRange => 'Zoombegränsning'; + String get mapCache_zoomRange => 'Zoombegränsning'; @override String mapCache_estimatedTiles(int count) { @@ -1592,10 +1592,10 @@ class AppLocalizationsSv extends AppLocalizations { String get time_weeks => 'veckor'; @override - String get time_month => 'månad'; + String get time_month => 'mÃ¥nad'; @override - String get time_months => 'månader'; + String get time_months => 'mÃ¥nader'; @override String get time_minutes => 'minuter'; @@ -1604,60 +1604,60 @@ class AppLocalizationsSv extends AppLocalizations { String get time_allTime => 'Alla tider'; @override - String get dialog_disconnect => 'Koppla från'; + String get dialog_disconnect => 'Koppla frÃ¥n'; @override String get dialog_disconnectConfirm => - 'Är du säker på att du vill koppla från enheten?'; + 'Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?'; @override - String get login_repeaterLogin => 'Återuppta Inloggning'; + String get login_repeaterLogin => 'Ã…teruppta Inloggning'; @override String get login_roomLogin => 'Rum Inloggning'; @override - String get login_password => 'Lösenord'; + String get login_password => 'Lösenord'; @override - String get login_enterPassword => 'Ange lösenord'; + String get login_enterPassword => 'Ange lösenord'; @override - String get login_savePassword => 'Spara lösenord'; + String get login_savePassword => 'Spara lösenord'; @override String get login_savePasswordSubtitle => - 'Lösenord kommer att lagras säkert på enheten.'; + 'Lösenord kommer att lagras säkert pÃ¥ enheten.'; @override String get login_repeaterDescription => - 'Ange återuppspelarens lösenord för att komma åt inställningar och status.'; + 'Ange Ã¥teruppspelarens lösenord för att komma Ã¥t inställningar och status.'; @override String get login_roomDescription => - 'Ange rummets lösenord för att komma åt inställningar och status.'; + 'Ange rummets lösenord för att komma Ã¥t inställningar och status.'; @override String get login_routing => 'Ruttning'; @override - String get login_routingMode => 'Ruttläge'; + String get login_routingMode => 'Ruttläge'; @override - String get login_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; + String get login_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; @override - String get login_forceFloodMode => 'Tvinga Översvämningsläge'; + String get login_forceFloodMode => 'Tvinga Översvämningsläge'; @override - String get login_managePaths => 'Hantera Sökvägar'; + String get login_managePaths => 'Hantera Sökvägar'; @override String get login_login => 'Logga in'; @override String login_attempt(int current, int max) { - return 'Försök $current/$max'; + return 'Försök $current/$max'; } @override @@ -1667,7 +1667,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get login_failedMessage => - 'Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.'; + 'Inloggning misslyckades. Antingen är lösenordet fel eller sÃ¥ gÃ¥r det inte att nÃ¥ repeatern.'; @override String get common_reload => 'Ladda om'; @@ -1677,7 +1677,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String path_currentPath(String path) { - return 'Nuvarande sökväg: $path'; + return 'Nuvarande sökväg: $path'; } @override @@ -1688,40 +1688,40 @@ class AppLocalizationsSv extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Använda $count $_temp0 sökväg'; + return 'Använda $count $_temp0 sökväg'; } @override - String get path_enterCustomPath => 'Ange anpassad sökväg'; + String get path_enterCustomPath => 'Ange anpassad sökväg'; @override - String get path_currentPathLabel => 'Nuvarande sökväg'; + String get path_currentPathLabel => 'Nuvarande sökväg'; @override String get path_hexPrefixInstructions => - 'Ange 2-tecknets hex-prefett för varje hopp, åtskilda med komma.'; + 'Ange 2-tecknets hex-prefett för varje hopp, Ã¥tskilda med komma.'; @override String get path_hexPrefixExample => - 'Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)'; + 'Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)'; @override String get path_labelHexPrefixes => 'Hexprefixer'; @override String get path_helperMaxHops => - 'Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)'; + 'Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)'; @override - String get path_selectFromContacts => 'Välj istället från kontakter:'; + String get path_selectFromContacts => 'Välj istället frÃ¥n kontakter:'; @override String get path_noRepeatersFound => - 'Inga återuppspelare eller rumsservrar hittades.'; + 'Inga Ã¥teruppspelare eller rumsservrar hittades.'; @override String get path_customPathsRequire => - 'Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.'; + 'Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.'; @override String path_invalidHexPrefixes(String prefixes) { @@ -1729,13 +1729,14 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get path_tooLong => 'Sökvägen är för lång. Max 64 hopp tillåtna.'; + String get path_tooLong => + 'Sökvägen är för lÃ¥ng. Max 64 hopp tillÃ¥tna.'; @override - String get path_setPath => 'Ange Sökväg'; + String get path_setPath => 'Ange Sökväg'; @override - String get repeater_management => 'Återuppspelarens Hantering'; + String get repeater_management => 'Ã…teruppspelarens Hantering'; @override String get room_management => 'Rumserverhantering'; @@ -1748,14 +1749,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_statusSubtitle => - 'Visa återspolningsstatus, statistik och grannar'; + 'Visa Ã¥terspolningsstatus, statistik och grannar'; @override String get repeater_telemetry => 'Telemetry'; @override String get repeater_telemetrySubtitle => - 'Visa telemetri för sensorer och systemstatistik'; + 'Visa telemetri för sensorer och systemstatistik'; @override String get repeater_cli => 'CLI'; @@ -1770,22 +1771,23 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.'; @override - String get repeater_settings => 'Inställningar'; + String get repeater_settings => 'Inställningar'; @override - String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar'; + String get repeater_settingsSubtitle => 'Konfigurera Ã¥terspolarparametrar'; @override - String get repeater_statusTitle => 'Återspelsstatus'; + String get repeater_statusTitle => 'Ã…terspelsstatus'; @override - String get repeater_routingMode => 'Ruttläge'; + String get repeater_routingMode => 'Ruttläge'; @override - String get repeater_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; + String get repeater_autoUseSavedPath => + 'Automatisk (använd sparad sökväg)'; @override - String get repeater_forceFloodMode => 'Tvinga Översvämningsläge'; + String get repeater_forceFloodMode => 'Tvinga Översvämningsläge'; @override String get repeater_pathManagement => 'Stigarhantering'; @@ -1795,11 +1797,11 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_statusRequestTimeout => - 'Statusförfrågan gick inte att hämta.'; + 'StatusförfrÃ¥gan gick inte att hämta.'; @override String repeater_errorLoadingStatus(String error) { - return 'Fel vid inläsning av status: $error'; + return 'Fel vid inläsning av status: $error'; } @override @@ -1812,13 +1814,13 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_clockAtLogin => 'Klocka (vid inloggning)'; @override - String get repeater_uptime => 'Tillgänglighet'; + String get repeater_uptime => 'Tillgänglighet'; @override - String get repeater_queueLength => 'Köans längd'; + String get repeater_queueLength => 'Köans längd'; @override - String get repeater_debugFlags => 'Felsökningsflaggor'; + String get repeater_debugFlags => 'Felsökningsflaggor'; @override String get repeater_radioStatistics => 'Radiostatistik'; @@ -1830,7 +1832,7 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_lastSnr => 'Sista SNR'; @override - String get repeater_noiseFloor => 'Ljudnivå'; + String get repeater_noiseFloor => 'LjudnivÃ¥'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1862,17 +1864,17 @@ class AppLocalizationsSv extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; + return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; + return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Översvämning: $flood, Direkt: $direct'; + return 'Översvämning: $flood, Direkt: $direct'; } @override @@ -1881,31 +1883,32 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Återuppspelarens Inställningar'; + String get repeater_settingsTitle => 'Ã…teruppspelarens Inställningar'; @override - String get repeater_basicSettings => 'Grundinställningar'; + String get repeater_basicSettings => 'Grundinställningar'; @override String get repeater_repeaterName => 'Upprepare Namn'; @override - String get repeater_repeaterNameHelper => 'Visa namn för denna återupprepare'; + String get repeater_repeaterNameHelper => + 'Visa namn för denna Ã¥terupprepare'; @override - String get repeater_adminPassword => 'Adminlösenord'; + String get repeater_adminPassword => 'Adminlösenord'; @override - String get repeater_adminPasswordHelper => 'Fullständig åtkomstlösenord'; + String get repeater_adminPasswordHelper => 'Fullständig Ã¥tkomstlösenord'; @override - String get repeater_guestPassword => 'Gästlösenhet'; + String get repeater_guestPassword => 'Gästlösenhet'; @override - String get repeater_guestPasswordHelper => 'Läs-skyddspassord'; + String get repeater_guestPasswordHelper => 'Läs-skyddspassord'; @override - String get repeater_radioSettings => 'Radioinställningar'; + String get repeater_radioSettings => 'Radioinställningar'; @override String get repeater_frequencyMhz => 'Frekvens (MHz)'; @@ -1929,7 +1932,7 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_codingRate => 'Kodningsgrad'; @override - String get repeater_locationSettings => 'Platsinställningar'; + String get repeater_locationSettings => 'Platsinställningar'; @override String get repeater_latitude => 'Latitud'; @@ -1938,7 +1941,7 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_latitudeHelper => 'Decimalgrader (t.ex. 37.7749)'; @override - String get repeater_longitude => 'Längdgrad'; + String get repeater_longitude => 'Längdgrad'; @override String get repeater_longitudeHelper => 'Decimalgrader (t.ex. -122.4194)'; @@ -1947,27 +1950,27 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_features => 'Funktioner'; @override - String get repeater_packetForwarding => 'Paketväxling'; + String get repeater_packetForwarding => 'Paketväxling'; @override String get repeater_packetForwardingSubtitle => - 'Aktivera återuppspelaren för att vidarebefordra paket'; + 'Aktivera Ã¥teruppspelaren för att vidarebefordra paket'; @override - String get repeater_guestAccess => 'Gäståtkomst'; + String get repeater_guestAccess => 'GästÃ¥tkomst'; @override String get repeater_guestAccessSubtitle => - 'Tillåt läsbehörigheter för gäster.'; + 'TillÃ¥t läsbehörigheter för gäster.'; @override - String get repeater_privacyMode => 'Privatläge'; + String get repeater_privacyMode => 'Privatläge'; @override - String get repeater_privacyModeSubtitle => 'Dölj namn/plats i annonser'; + String get repeater_privacyModeSubtitle => 'Dölj namn/plats i annonser'; @override - String get repeater_advertisementSettings => 'Annonsinställningar'; + String get repeater_advertisementSettings => 'Annonsinställningar'; @override String get repeater_localAdvertInterval => 'Lokalt Annonsintervall'; @@ -1979,7 +1982,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Översvämnadsannonsens tidsintervall'; + 'Översvämnadsannonsens tidsintervall'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -1990,17 +1993,17 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_encryptedAdvertInterval => 'Krypterad Annonsintervall'; @override - String get repeater_dangerZone => 'Faraområde'; + String get repeater_dangerZone => 'FaraomrÃ¥de'; @override - String get repeater_rebootRepeater => 'Starta Återuppspelaren'; + String get repeater_rebootRepeater => 'Starta Ã…teruppspelaren'; @override String get repeater_rebootRepeaterSubtitle => 'Starta om repeternheten'; @override String get repeater_rebootRepeaterConfirm => - 'Är du säker på att du vill starta om denna repeater?'; + 'Är du säker pÃ¥ att du vill starta om denna repeater?'; @override String get repeater_regenerateIdentityKey => 'Generera Identitetsknyckel'; @@ -2011,22 +2014,22 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'Detta kommer att generera en ny identitet för återspelaren. Fortsätta?'; + 'Detta kommer att generera en ny identitet för Ã¥terspelaren. Fortsätta?'; @override String get repeater_eraseFileSystem => 'Radera Filsystem'; @override String get repeater_eraseFileSystemSubtitle => - 'Formatera återspelsfilsystemet'; + 'Formatera Ã¥terspelsfilsystemet'; @override String get repeater_eraseFileSystemConfirm => - 'VARNING: Detta kommer att radera all data på repeatern. Detta kan inte ångras!'; + 'VARNING: Detta kommer att radera all data pÃ¥ repeatern. Detta kan inte Ã¥ngras!'; @override String get repeater_eraseSerialOnly => - 'Rensa är endast tillgängligt via seriell konsol.'; + 'Rensa är endast tillgängligt via seriell konsol.'; @override String repeater_commandSent(String command) { @@ -2039,43 +2042,44 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_confirm => 'Bekräfta'; + String get repeater_confirm => 'Bekräfta'; @override String get repeater_settingsSaved => - 'Inställningarna sparades framgångsrikt.'; + 'Inställningarna sparades framgÃ¥ngsrikt.'; @override String repeater_errorSavingSettings(String error) { - return 'Fel vid sparande av inställningar: $error'; + return 'Fel vid sparande av inställningar: $error'; } @override String get repeater_refreshBasicSettings => - 'Återställ Grundläggande Inställningar'; + 'Ã…terställ Grundläggande Inställningar'; @override - String get repeater_refreshRadioSettings => 'Återställ Radiosinställningar'; + String get repeater_refreshRadioSettings => + 'Ã…terställ Radiosinställningar'; @override - String get repeater_refreshTxPower => 'Återställ TX-effekt'; + String get repeater_refreshTxPower => 'Ã…terställ TX-effekt'; @override String get repeater_refreshLocationSettings => - 'Uppdatera Lokationsinställningar'; + 'Uppdatera Lokationsinställningar'; @override - String get repeater_refreshPacketForwarding => 'Återställ Paketväxling'; + String get repeater_refreshPacketForwarding => 'Ã…terställ Paketväxling'; @override - String get repeater_refreshGuestAccess => 'Återställ Gäståtkomst'; + String get repeater_refreshGuestAccess => 'Ã…terställ GästÃ¥tkomst'; @override - String get repeater_refreshPrivacyMode => 'Återställ Sekretessläge'; + String get repeater_refreshPrivacyMode => 'Ã…terställ Sekretessläge'; @override String get repeater_refreshAdvertisementSettings => - 'Återställ Annonsinställningar'; + 'Ã…terställ Annonsinställningar'; @override String repeater_refreshed(String label) { @@ -2088,23 +2092,23 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_cliTitle => 'Återuppspelaren CLI'; + String get repeater_cliTitle => 'Ã…teruppspelaren CLI'; @override - String get repeater_debugNextCommand => 'Felsök Nästa Kommando'; + String get repeater_debugNextCommand => 'Felsök Nästa Kommando'; @override - String get repeater_commandHelp => 'Hjälp'; + String get repeater_commandHelp => 'Hjälp'; @override String get repeater_clearHistory => 'Rensa Historik'; @override - String get repeater_noCommandsSent => 'Inga kommandon skickats ännu'; + String get repeater_noCommandsSent => 'Inga kommandon skickats ännu'; @override String get repeater_typeCommandOrUseQuick => - 'Skriv en kommando nedan eller använd snabba kommandon'; + 'Skriv en kommando nedan eller använd snabba kommandon'; @override String get repeater_enterCommandHint => 'Ange kommando...'; @@ -2113,13 +2117,13 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_previousCommand => 'Tidigare kommando'; @override - String get repeater_nextCommand => 'Nästa kommando'; + String get repeater_nextCommand => 'Nästa kommando'; @override - String get repeater_enterCommandFirst => 'Ange en kommando först'; + String get repeater_enterCommandFirst => 'Ange en kommando först'; @override - String get repeater_cliCommandFrameTitle => 'Kommandofönster'; + String get repeater_cliCommandFrameTitle => 'Kommandofönster'; @override String repeater_cliCommandError(String error) { @@ -2127,13 +2131,13 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'Hämta namn'; + String get repeater_cliQuickGetName => 'Hämta namn'; @override - String get repeater_cliQuickGetRadio => 'Få Radio'; + String get repeater_cliQuickGetRadio => 'FÃ¥ Radio'; @override - String get repeater_cliQuickGetTx => 'Hämta TX'; + String get repeater_cliQuickGetTx => 'Hämta TX'; @override String get repeater_cliQuickNeighbors => 'Grannar'; @@ -2152,14 +2156,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpReboot => - 'Startar om enheten. (notera, du får kanske \'Timeout\' vilket är normalt)'; + 'Startar om enheten. (notera, du fÃ¥r kanske \'Timeout\' vilket är normalt)'; @override String get repeater_cliHelpClock => 'Visar aktuell tid per enhetens klocka.'; @override String get repeater_cliHelpPassword => - 'Ställer in ett nytt administratörslösenord för enheten.'; + 'Ställer in ett nytt administratörslösenord för enheten.'; @override String get repeater_cliHelpVersion => @@ -2167,34 +2171,34 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpClearStats => - 'Återställer olika statistikräknare till noll.'; + 'Ã…terställer olika statistikräknare till noll.'; @override - String get repeater_cliHelpSetAf => 'Ställer in lufttidsfaktor.'; + String get repeater_cliHelpSetAf => 'Ställer in lufttidsfaktor.'; @override String get repeater_cliHelpSetTx => - 'Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)'; + 'Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)'; @override String get repeater_cliHelpSetRepeat => - 'Aktiverar eller inaktiverar återuppspelarens roll för denna nod.'; + 'Aktiverar eller inaktiverar Ã¥teruppspelarens roll för denna nod.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Rumserver) Om \'på\', så tillåts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).'; + '(Rumserver) Om \'pÃ¥\', sÃ¥ tillÃ¥ts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).'; @override String get repeater_cliHelpSetFloodMax => - 'Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).'; + 'Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).'; @override String get repeater_cliHelpSetIntThresh => - 'Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den på 0 för att inaktivera detektion av kanalinterferens.'; + 'Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den pÃ¥ 0 för att inaktivera detektion av kanalinterferens.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Ställer in intervallet för att återställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.'; + 'Ställer in intervallet för att Ã¥terställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.'; @override String get repeater_cliHelpSetMultiAcks => @@ -2202,77 +2206,77 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.'; + 'Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in på 0 för att inaktivera.'; + 'Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in pÃ¥ 0 för att inaktivera.'; @override String get repeater_cliHelpSetGuestPassword => - 'Ställer in/uppdaterar gästlösenordet. (för återvändare kan gästloggar skicka \"Get Stats\"-förfrågan)'; + 'Ställer in/uppdaterar gästlösenordet. (för Ã¥tervändare kan gästloggar skicka \"Get Stats\"-förfrÃ¥gan)'; @override - String get repeater_cliHelpSetName => 'Ställer in annonstexterna namn.'; + String get repeater_cliHelpSetName => 'Ställer in annonstexterna namn.'; @override String get repeater_cliHelpSetLat => - 'Ställer in annonskartans latitud. (decimalgrader)'; + 'Ställer in annonskartans latitud. (decimalgrader)'; @override String get repeater_cliHelpSetLon => - 'Ställer in annonskartans longitud (decimalgrader).'; + 'Ställer in annonskartans longitud (decimalgrader).'; @override String get repeater_cliHelpSetRadio => - 'Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.'; + 'Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.'; @override String get repeater_cliHelpSetRxDelay => - 'Ställer (experimentell) basvärde (måste vara > 1 för effekt) för att applicera en liten fördröjning på mottagna paket, baserat på signalstyrka/poäng. Ställ in på 0 för att inaktivera.'; + 'Ställer (experimentell) basvärde (mÃ¥ste vara > 1 för effekt) för att applicera en liten fördröjning pÃ¥ mottagna paket, baserat pÃ¥ signalstyrka/poäng. Ställ in pÃ¥ 0 för att inaktivera.'; @override String get repeater_cliHelpSetTxDelay => - 'Ställer in en faktor som multipliceras med tid på luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).'; + 'Ställer in en faktor som multipliceras med tid pÃ¥ luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.'; + 'Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.'; @override String get repeater_cliHelpSetBridgeEnabled => 'Aktivera/Inaktivera brygga.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Ställ in fördröjning innan paket åter sänder.'; + 'Ställ in fördröjning innan paket Ã¥ter sänder.'; @override String get repeater_cliHelpSetBridgeSource => - 'Välj om bron ska återända mottagna paket eller sända paket.'; + 'Välj om bron ska Ã¥terända mottagna paket eller sända paket.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Ställ baudgränsen för rs232-bryggarna.'; + 'Ställ baudgränsen för rs232-bryggarna.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Ställ bro-hemlighet för espnow-broar.'; + 'Ställ bro-hemlighet för espnow-broar.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd på utvalda kort).'; + 'Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd pÃ¥ utvalda kort).'; @override String get repeater_cliHelpTempRadio => - 'Ställer temporära radioparametrar för det angivna antalet minuter, vilket återgår till de ursprungliga radioparametrarna efteråt. (sparar inte i inställningar).'; + 'Ställer temporära radioparametrar för det angivna antalet minuter, vilket Ã¥tergÃ¥r till de ursprungliga radioparametrarna efterÃ¥t. (sparar inte i inställningar).'; @override String get repeater_cliHelpSetPerm => - 'Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. Tillståndsbiten varierar per firmware-roll, men de låga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).'; + 'Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. TillstÃ¥ndsbiten varierar per firmware-roll, men de lÃ¥ga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).'; @override String get repeater_cliHelpGetBridgeType => - 'Får brotyperna ingen, rs232, espnow'; + 'FÃ¥r brotyperna ingen, rs232, espnow'; @override String get repeater_cliHelpLogStart => 'Starta paketloggning till filsystem.'; @@ -2282,50 +2286,50 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpLogErase => - 'Raderar pakets loggar från filsystemet.'; + 'Raderar pakets loggar frÃ¥n filsystemet.'; @override String get repeater_cliHelpNeighbors => - 'Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-g撮-4'; + 'Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-gæ’®-4'; @override String get repeater_cliHelpNeighborRemove => - 'Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) från grannlistan.'; + 'Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) frÃ¥n grannlistan.'; @override String get repeater_cliHelpRegion => - '(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.'; + '(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.'; @override String get repeater_cliHelpRegionLoad => - 'MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.'; + 'MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.'; @override String get repeater_cliHelpRegionGet => - 'Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) \'F\'\"'; + 'Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.'; + 'Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.'; @override String get repeater_cliHelpRegionRemove => - 'Tar bort en regionsdefinition med det angivna namnet. (måste matcha exakt och inte ha några barnregioner)'; + 'Tar bort en regionsdefinition med det angivna namnet. (mÃ¥ste matcha exakt och inte ha nÃ¥gra barnregioner)'; @override String get repeater_cliHelpRegionAllowf => - 'Ställer \'Flöde\'-behörighet för det angivna området. (\'\' för det globala/gamla scopet)'; + 'Ställer \'Flöde\'-behörighet för det angivna omrÃ¥det. (\'\' för det globala/gamla scopet)'; @override String get repeater_cliHelpRegionDenyf => - 'Tar bort \'F\'lood-behörigheten för det angivna området. (OBS: rekommenderas inte att använda detta i detta skede på den globala/gamla omfattningen!!).'; + 'Tar bort \'F\'lood-behörigheten för det angivna omrÃ¥det. (OBS: rekommenderas inte att använda detta i detta skede pÃ¥ den globala/gamla omfattningen!!).'; @override String get repeater_cliHelpRegionHome => - 'Svarar med den aktuella \'hem\'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).'; + 'Svarar med den aktuella \'hem\'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).'; @override - String get repeater_cliHelpRegionHomeSet => 'Ställer in \'hemregionen\'.'; + String get repeater_cliHelpRegionHomeSet => 'Ställer in \'hemregionen\'.'; @override String get repeater_cliHelpRegionSave => @@ -2333,40 +2337,40 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"på\", status, fix, antal satelliter.'; + 'Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"pÃ¥\", status, fix, antal satelliter.'; @override String get repeater_cliHelpGpsOnOff => - 'Aktiverar/inaktiverar GPS-strömsättningen.'; + 'Aktiverar/inaktiverar GPS-strömsättningen.'; @override String get repeater_cliHelpGpsSync => - 'Synkroniserar nätverks tid med GPS-klockan.'; + 'Synkroniserar nätverks tid med GPS-klockan.'; @override String get repeater_cliHelpGpsSetLoc => - 'Ställer nodens position till GPS-koordinater och sparar inställningar.'; + 'Ställer nodens position till GPS-koordinater och sparar inställningar.'; @override String get repeater_cliHelpGpsAdvert => - 'Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (från SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar'; + 'Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (frÃ¥n SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar'; @override String get repeater_cliHelpGpsAdvertSet => - 'Ställer in annonsplatskonfiguration.'; + 'Ställer in annonsplatskonfiguration.'; @override - String get repeater_commandsListTitle => 'Inställningslista'; + String get repeater_commandsListTitle => 'Inställningslista'; @override String get repeater_commandsListNote => - 'OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.'; + 'OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.'; @override - String get repeater_general => 'Allmänt'; + String get repeater_general => 'Allmänt'; @override - String get repeater_settingsCategory => 'Inställningar'; + String get repeater_settingsCategory => 'Inställningar'; @override String get repeater_bridge => 'Bro'; @@ -2375,28 +2379,28 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_logging => 'Logga'; @override - String get repeater_neighborsRepeaterOnly => 'Grannar (Endast återspelare)'; + String get repeater_neighborsRepeaterOnly => 'Grannar (Endast Ã¥terspelare)'; @override String get repeater_regionManagementRepeaterOnly => - 'Regionhantering (endast återuppspelare)'; + 'Regionhantering (endast Ã¥teruppspelare)'; @override String get repeater_regionNote => - 'Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.'; + 'Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.'; @override String get repeater_gpsManagement => 'GPS Hantering'; @override String get repeater_gpsNote => - 'GPS-kommando har introducerats för att hantera platsrelaterade ämnen.'; + 'GPS-kommando har introducerats för att hantera platsrelaterade ämnen.'; @override String get telemetry_receivedData => 'Mottagen Telemetridata'; @override - String get telemetry_requestTimeout => 'Telemetryförfrågan gick ut.'; + String get telemetry_requestTimeout => 'TelemetryförfrÃ¥gan gick ut.'; @override String telemetry_errorLoading(String error) { @@ -2404,7 +2408,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get telemetry_noData => 'Inga telemetridata tillgängliga.'; + String get telemetry_noData => 'Inga telemetridata tillgängliga.'; @override String telemetry_channelTitle(int channel) { @@ -2415,7 +2419,7 @@ class AppLocalizationsSv extends AppLocalizations { String get telemetry_batteryLabel => 'Batteri'; @override - String get telemetry_voltageLabel => 'Spänning'; + String get telemetry_voltageLabel => 'Spänning'; @override String get telemetry_mcuTemperatureLabel => 'MCU Temperatur'; @@ -2443,57 +2447,58 @@ class AppLocalizationsSv extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override String get neighbors_receivedData => 'Mottagna grannars data'; @override - String get neighbors_requestTimedOut => 'Grannar begär tidsinställd utskick.'; + String get neighbors_requestTimedOut => + 'Grannar begär tidsinställd utskick.'; @override String neighbors_errorLoading(String error) { - return 'Fel vid inläsning av grannar: $error'; + return 'Fel vid inläsning av grannar: $error'; } @override String get neighbors_repeatersNeighbors => 'Upprepar grannar'; @override - String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.'; + String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.'; @override String neighbors_unknownContact(String pubkey) { - return 'Okänd $pubkey'; + return 'Okänd $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Hördes: $time sedan'; + return 'Hördes: $time sedan'; } @override - String get channelPath_title => 'Paketväg'; + String get channelPath_title => 'Paketväg'; @override String get channelPath_viewMap => 'Visa karta'; @override - String get channelPath_otherObservedPaths => 'Övriga observerade stigar'; + String get channelPath_otherObservedPaths => 'Övriga observerade stigar'; @override - String get channelPath_repeaterHops => 'Återupptagningssteg'; + String get channelPath_repeaterHops => 'Ã…terupptagningssteg'; @override String get channelPath_noHopDetails => - 'Detaljer för denna paket är inte angivna.'; + 'Detaljer för denna paket är inte angivna.'; @override String get channelPath_messageDetails => 'Meddelandets detaljer'; @override - String get channelPath_senderLabel => 'Avsändare'; + String get channelPath_senderLabel => 'Avsändare'; @override String get channelPath_timeLabel => 'Tid'; @@ -2503,7 +2508,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String channelPath_pathLabel(int index) { - return 'Sökväg $index'; + return 'Sökväg $index'; } @override @@ -2511,7 +2516,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Observerad bana $index • $hops'; + return 'Observerad bana $index • $hops'; } @override @@ -2528,10 +2533,10 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Okänt'; + String get channelPath_unknownPath => 'Okänt'; @override - String get channelPath_floodPath => 'Översvämning'; + String get channelPath_floodPath => 'Översvämning'; @override String get channelPath_directPath => 'Direkt'; @@ -2547,34 +2552,34 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channelPath_mapTitle => 'Sökvägskarta'; + String get channelPath_mapTitle => 'Sökvägskarta'; @override String get channelPath_noRepeaterLocations => - 'Inga återupprepningsplatser finns tillgängliga för denna väg.'; + 'Inga Ã¥terupprepningsplatser finns tillgängliga för denna väg.'; @override String channelPath_primaryPath(int index) { - return 'Sökväg $index (Primär)'; + return 'Sökväg $index (Primär)'; } @override - String get channelPath_pathLabelTitle => 'Sökväg'; + String get channelPath_pathLabelTitle => 'Sökväg'; @override - String get channelPath_observedPathHeader => 'Observerad Sökväg'; + String get channelPath_observedPathHeader => 'Observerad Sökväg'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Inga hoppdetaljer finns tillgängliga för detta paket.'; + 'Inga hoppdetaljer finns tillgängliga för detta paket.'; @override - String get channelPath_unknownRepeater => 'Okänt Upprepare'; + String get channelPath_unknownRepeater => 'Okänt Upprepare'; @override String get community_title => 'Gemenskap'; @@ -2587,14 +2592,14 @@ class AppLocalizationsSv extends AppLocalizations { 'Skapa en ny gemenskap och dela via QR-kod.'; @override - String get community_join => 'Gå med'; + String get community_join => 'GÃ¥ med'; @override - String get community_joinTitle => 'Gå med i gemenskapen'; + String get community_joinTitle => 'GÃ¥ med i gemenskapen'; @override String community_joinConfirmation(String name) { - return 'Vill du gå med i communityn \"$name\"?'; + return 'Vill du gÃ¥ med i communityn \"$name\"?'; } @override @@ -2608,7 +2613,7 @@ class AppLocalizationsSv extends AppLocalizations { String get community_showQr => 'Visa QR-kod'; @override - String get community_publicChannel => 'Föreningens Offentliga'; + String get community_publicChannel => 'Föreningens Offentliga'; @override String get community_hashtagChannel => 'Community Hashtag'; @@ -2634,58 +2639,58 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Skanna denna QR-kod för att gå med i \"$name\"'; + return 'Skanna denna QR-kod för att gÃ¥ med i \"$name\"'; } @override String get community_hashtagPrivacyHint => - 'Community-hashtagkanaler kan endast nås av medlemmar i communityn'; + 'Community-hashtagkanaler kan endast nÃ¥s av medlemmar i communityn'; @override String get community_invalidQrCode => 'Ogiltig community QR-kod'; @override - String get community_alreadyMember => 'Är redan medlem'; + String get community_alreadyMember => 'Är redan medlem'; @override String community_alreadyMemberMessage(String name) { - return 'Du är redan medlem av \"$name\".'; + return 'Du är redan medlem av \"$name\".'; } @override String get community_addPublicChannel => - 'Lägg till Gemenskapskanal (Offentlig)'; + 'Lägg till Gemenskapskanal (Offentlig)'; @override String get community_addPublicChannelHint => - 'Lägg automatiskt till den offentliga kanalen för denna community'; + 'Lägg automatiskt till den offentliga kanalen för denna community'; @override - String get community_noCommunities => 'Inga gemenskaper har anslutats ännu'; + String get community_noCommunities => 'Inga gemenskaper har anslutats ännu'; @override String get community_scanOrCreate => - 'Skanna en QR-kod eller skapa en community för att komma igång'; + 'Skanna en QR-kod eller skapa en community för att komma igÃ¥ng'; @override String get community_manageCommunities => 'Hantera Gemenskaper'; @override - String get community_delete => 'Lämna Gemenskap'; + String get community_delete => 'Lämna Gemenskap'; @override String community_deleteConfirm(String name) { - return 'Lämna \"$name\"?'; + return 'Lämna \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Detta kommer också att radera $count kanal/kanaler och deras meddelanden.'; + return 'Detta kommer ocksÃ¥ att radera $count kanal/kanaler och deras meddelanden.'; } @override String community_deleted(String name) { - return 'Lämnade community \"$name\"'; + return 'Lämnade community \"$name\"'; } @override @@ -2693,7 +2698,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.'; + return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar mÃ¥ste scanna den nya QR-koden för att fortsätta kommunicera.'; } @override @@ -2701,7 +2706,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Lösenord återskapad för \"$name\"'; + return 'Lösenord Ã¥terskapad för \"$name\"'; } @override @@ -2709,40 +2714,40 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_secretUpdated(String name) { - return 'Hemlighet uppdaterad för \"$name\"'; + return 'Hemlighet uppdaterad för \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"'; + return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"'; } @override - String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag'; + String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag'; @override String get community_addHashtagChannelDesc => - 'Lägg till en hashtag-kanal för denna community'; + 'Lägg till en hashtag-kanal för denna community'; @override - String get community_selectCommunity => 'Välj Gemenskap'; + String get community_selectCommunity => 'Välj Gemenskap'; @override String get community_regularHashtag => 'Vanlig Hash Tag'; @override String get community_regularHashtagDesc => - 'Offentlig hashtag (alla kan gå med)'; + 'Offentlig hashtag (alla kan gÃ¥ med)'; @override String get community_communityHashtag => 'Community Hashtag'; @override - String get community_communityHashtagDesc => 'Endast för medlemmar'; + String get community_communityHashtagDesc => 'Endast för medlemmar'; @override String community_forCommunity(String name) { - return 'För $name'; + return 'För $name'; } @override @@ -2755,7 +2760,7 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_latestMessages => 'Senaste meddelanden'; @override - String get listFilter_heardRecently => 'Hörts nyligen'; + String get listFilter_heardRecently => 'Hörts nyligen'; @override String get listFilter_az => 'A-Z'; @@ -2770,13 +2775,13 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_favorites => 'Favoriter'; @override - String get listFilter_addToFavorites => 'Lägg till i favoriter'; + String get listFilter_addToFavorites => 'Lägg till i favoriter'; @override - String get listFilter_removeFromFavorites => 'Ta bort från favoriter'; + String get listFilter_removeFromFavorites => 'Ta bort frÃ¥n favoriter'; @override - String get listFilter_users => 'Användare'; + String get listFilter_users => 'Användare'; @override String get listFilter_repeaters => 'Upprepare'; @@ -2785,7 +2790,7 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_roomServers => 'Rumservrar'; @override - String get listFilter_unreadOnly => 'Endast oinlästa'; + String get listFilter_unreadOnly => 'Endast oinlästa'; @override String get listFilter_newGroup => 'Ny grupp'; @@ -2794,10 +2799,10 @@ class AppLocalizationsSv extends AppLocalizations { String get pathTrace_you => 'Du'; @override - String get pathTrace_failed => 'Sökvägsföljning misslyckades.'; + String get pathTrace_failed => 'Sökvägsföljning misslyckades.'; @override - String get pathTrace_notAvailable => 'Path trace ej tillgänglig.'; + String get pathTrace_notAvailable => 'Path trace ej tillgänglig.'; @override String get pathTrace_refreshTooltip => 'Uppdatera Path Trace'; @@ -2807,10 +2812,10 @@ class AppLocalizationsSv extends AppLocalizations { 'En eller flera av humlen saknar en plats!'; @override - String get pathTrace_clearTooltip => 'Rensa väg'; + String get pathTrace_clearTooltip => 'Rensa väg'; @override - String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.'; + String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.'; @override String losRunFailed(String error) { @@ -2821,20 +2826,20 @@ class AppLocalizationsSv extends AppLocalizations { String get losClearAllPoints => 'Rensa alla punkter'; @override - String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil'; + String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil'; @override String get losMenuTitle => 'LOS-menyn'; @override String get losMenuSubtitle => - 'Tryck på noder eller tryck länge på kartan för anpassade punkter'; + 'Tryck pÃ¥ noder eller tryck länge pÃ¥ kartan för anpassade punkter'; @override String get losShowDisplayNodes => 'Visa displaynoder'; @override - String get losCustomPoints => 'Anpassade poäng'; + String get losCustomPoints => 'Anpassade poäng'; @override String losCustomPointLabel(int index) { @@ -2858,10 +2863,10 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get losRun => 'Kör LOS'; + String get losRun => 'Kör LOS'; @override - String get losNoElevationData => 'Inga höjddata'; + String get losNoElevationData => 'Inga höjddata'; @override String losProfileClear( @@ -2891,19 +2896,19 @@ class AppLocalizationsSv extends AppLocalizations { @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd'; + return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd'; } @override String get losErrorElevationUnavailable => - 'Höjddata är inte tillgänglig för ett eller flera prover.'; + 'Höjddata är inte tillgänglig för ett eller flera prover.'; @override String get losErrorInvalidInput => - 'Ogiltiga poäng/höjddata för LOS-beräkning.'; + 'Ogiltiga poäng/höjddata för LOS-beräkning.'; @override - String get losRenameCustomPoint => 'Byt namn på anpassad punkt'; + String get losRenameCustomPoint => 'Byt namn pÃ¥ anpassad punkt'; @override String get losPointName => 'Punktnamn'; @@ -2912,10 +2917,10 @@ class AppLocalizationsSv extends AppLocalizations { String get losShowPanelTooltip => 'Visa LOS-panelen'; @override - String get losHidePanelTooltip => 'Dölj LOS-panelen'; + String get losHidePanelTooltip => 'Dölj LOS-panelen'; @override - String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; + String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Radiohorisont'; @@ -2924,16 +2929,16 @@ class AppLocalizationsSv extends AppLocalizations { String get losLegendLosBeam => 'Siktlinje'; @override - String get losLegendTerrain => 'Terräng'; + String get losLegendTerrain => 'Terräng'; @override String get losFrequencyLabel => 'Frekvens'; @override - String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen'; + String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen'; @override - String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten'; + String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten'; @override String losFrequencyDialogDescription( @@ -2942,7 +2947,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; + return 'Med start frÃ¥n k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; } @override @@ -2952,27 +2957,27 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => 'Vägspårning till repeater'; + String get contacts_repeaterPathTrace => 'VägspÃ¥rning till repeater'; @override String get contacts_repeaterPing => 'Ping-repeater'; @override - String get contacts_roomPathTrace => 'Vägspårning till rumserver'; + String get contacts_roomPathTrace => 'VägspÃ¥rning till rumserver'; @override String get contacts_roomPing => 'Ping rumsserver'; @override - String get contacts_chatTraceRoute => 'Spåra rutt'; + String get contacts_chatTraceRoute => 'SpÃ¥ra rutt'; @override String contacts_pathTraceTo(String name) { - return 'Spåra rutt till $name'; + return 'SpÃ¥ra rutt till $name'; } @override - String get contacts_clipboardEmpty => 'Urklipp är tomt.'; + String get contacts_clipboardEmpty => 'Urklipp är tomt.'; @override String get contacts_invalidAdvertFormat => 'Ogiltiga kontaktuppgifter'; @@ -2987,14 +2992,14 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_zeroHopAdvert => 'Reklam med nollhopp'; @override - String get contacts_floodAdvert => 'Översvämningsannons'; + String get contacts_floodAdvert => 'Översvämningsannons'; @override String get contacts_copyAdvertToClipboard => 'Kopiera annons till urklipp'; @override String get contacts_addContactFromClipboard => - 'Lägg till kontakt från urklipp'; + 'Lägg till kontakt frÃ¥n urklipp'; @override String get contacts_ShareContact => 'Kopiera kontakt till Urklipp'; @@ -3054,7 +3059,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String notification_newTypeDiscovered(String contactType) { - return 'Ny $contactType upptäckt'; + return 'Ny $contactType upptäckt'; } @override @@ -3069,11 +3074,11 @@ class AppLocalizationsSv extends AppLocalizations { 'Exporterar repeater / roomserver med plats till GPX-fil.'; @override - String get settings_gpxExportContacts => 'Exportera följeslagare till GPX'; + String get settings_gpxExportContacts => 'Exportera följeslagare till GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exporterar följeslagare med en plats till GPX-fil.'; + 'Exporterar följeslagare med en plats till GPX-fil.'; @override String get settings_gpxExportAll => 'Exportera alla kontakter till GPX'; @@ -3083,39 +3088,40 @@ class AppLocalizationsSv extends AppLocalizations { 'Exporterar alla kontakter med en plats till GPX-fil.'; @override - String get settings_gpxExportSuccess => 'Har exporterat GPX-fil med framgång'; + String get settings_gpxExportSuccess => + 'Har exporterat GPX-fil med framgÃ¥ng'; @override String get settings_gpxExportNoContacts => 'Inga kontakter att exportera.'; @override String get settings_gpxExportNotAvailable => - 'Stöds inte på din enhet/operativsystem'; + 'Stöds inte pÃ¥ din enhet/operativsystem'; @override String get settings_gpxExportError => - 'Det uppstod ett fel när data exporterades.'; + 'Det uppstod ett fel när data exporterades.'; @override String get settings_gpxExportRepeatersRoom => 'Repeater- och rumsserverplatser'; @override - String get settings_gpxExportChat => 'Medhjälparplatser'; + String get settings_gpxExportChat => 'Medhjälparplatser'; @override String get settings_gpxExportAllContacts => 'Alla kontakters platser'; @override String get settings_gpxExportShareText => - 'Kartdata exporterad från meshcore-open'; + 'Kartdata exporterad frÃ¥n meshcore-open'; @override String get settings_gpxExportShareSubject => 'meshcore-open export av GPX-kartdata'; @override - String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer'; + String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer'; @override String get snrIndicator_lastSeen => 'Senast sedd'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 08c2c0f..1a70aa2 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -12,92 +12,92 @@ class AppLocalizationsUk extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Контакти'; + String get nav_contacts => 'Контакти'; @override - String get nav_channels => 'Канали'; + String get nav_channels => 'Канали'; @override - String get nav_map => 'Карта'; + String get nav_map => 'Карта'; @override - String get common_cancel => 'Скасувати'; + String get common_cancel => 'Скасувати'; @override - String get common_ok => 'ОК'; + String get common_ok => 'ОК'; @override - String get common_connect => 'Підключити'; + String get common_connect => 'Підключити'; @override - String get common_unknownDevice => 'Невідомий пристрій'; + String get common_unknownDevice => 'Невідомий пристрій'; @override - String get common_save => 'Зберегти'; + String get common_save => 'Зберегти'; @override - String get common_delete => 'Видалити'; + String get common_delete => 'Видалити'; @override - String get common_close => 'Закрити'; + String get common_close => 'Закрити'; @override - String get common_edit => 'Редагувати'; + String get common_edit => 'Редагувати'; @override - String get common_add => 'Додати'; + String get common_add => 'Додати'; @override - String get common_settings => 'Налаштування'; + String get common_settings => 'Налаштування'; @override - String get common_disconnect => 'Відключити'; + String get common_disconnect => 'Відключити'; @override - String get common_connected => 'Підключено'; + String get common_connected => 'Підключено'; @override - String get common_disconnected => 'Відключено'; + String get common_disconnected => 'Відключено'; @override - String get common_create => 'Створити'; + String get common_create => 'Створити'; @override - String get common_continue => 'Продовжити'; + String get common_continue => 'Продовжити'; @override - String get common_share => 'Поділитися'; + String get common_share => 'Поділитися'; @override - String get common_copy => 'Копіювати'; + String get common_copy => 'Копіювати'; @override - String get common_retry => 'Повторити'; + String get common_retry => 'Повторити'; @override - String get common_hide => 'Приховати'; + String get common_hide => 'Приховати'; @override - String get common_remove => 'Прибрати'; + String get common_remove => 'Прибрати'; @override - String get common_enable => 'Увімкнути'; + String get common_enable => 'Увімкнути'; @override - String get common_disable => 'Вимкнути'; + String get common_disable => 'Вимкнути'; @override - String get common_reboot => 'Перезавантажити'; + String get common_reboot => 'Перезавантажити'; @override - String get common_loading => 'Завантаження...'; + String get common_loading => 'Завантаження...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { - return '$volts В'; + return '$volts Ð’'; } @override @@ -108,13 +108,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scanner_title => 'MeshCore Open'; - @override - String get connectionChoiceTitle => 'Виберіть спосіб зв\'язку'; - - @override - String get connectionChoiceSubtitle => - 'Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.'; - @override String get connectionChoiceUsbLabel => 'USB'; @@ -122,231 +115,245 @@ class AppLocalizationsUk extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Підключити через USB'; + String get usbScreenTitle => 'Підключити через USB'; @override String get usbScreenSubtitle => - 'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.'; + 'Виберіть виявлене серійне пристрій Ñ– підключіть його безпосередньо до вашого вузла MeshCore.'; @override - String get usbScreenStatus => 'Виберіть пристрій USB'; + String get usbScreenStatus => 'Виберіть пристрій USB'; @override String get usbScreenNote => - 'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.'; + 'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.'; @override String get usbScreenEmptyState => - 'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.'; + 'Не знайдено жодних пристроїв USB. Підключіть один Ñ– перезавантажте.'; @override - String get scanner_scanning => 'Пошук пристроїв...'; + String get scanner_scanning => 'Пошук пристроїв...'; @override - String get scanner_connecting => 'Підключення...'; + String get scanner_connecting => 'Підключення...'; @override - String get scanner_disconnecting => 'Відключення...'; + String get scanner_disconnecting => 'Відключення...'; @override - String get scanner_notConnected => 'Не підключено'; + String get scanner_notConnected => 'Не підключено'; @override String scanner_connectedTo(String deviceName) { - return 'Підключено до $deviceName'; + return 'Підключено до $deviceName'; } @override - String get scanner_searchingDevices => 'Пошук пристроїв MeshCore...'; + String get scanner_searchingDevices => + 'Пошук пристроїв MeshCore...'; @override String get scanner_tapToScan => - 'Натисніть «Сканувати», щоб знайти пристрої MeshCore'; + 'Натисніть «Сканувати», щоб знайти пристрої MeshCore'; @override String scanner_connectionFailed(String error) { - return 'Помилка підключення: $error'; + return 'Помилка підключення: $error'; } @override - String get scanner_stop => 'Стоп'; + String get scanner_stop => 'Стоп'; @override - String get scanner_scan => 'Сканувати'; + String get scanner_scan => 'Сканувати'; @override - String get scanner_bluetoothOff => 'Bluetooth вимкнено'; + String get scanner_bluetoothOff => 'Bluetooth вимкнено'; @override String get scanner_bluetoothOffMessage => - 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; + 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; @override - String get scanner_chromeRequired => 'Потрібен браузер Chrome'; + String get scanner_chromeRequired => 'Потрібен браузер Chrome'; @override String get scanner_chromeRequiredMessage => - 'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.'; + 'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.'; @override - String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; + String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; @override - String get device_quickSwitch => 'Швидке перемикання'; + String get device_quickSwitch => 'Швидке перемикання'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Налаштування'; + String get settings_title => 'Налаштування'; @override - String get settings_deviceInfo => 'Інформація про пристрій'; + String get settings_deviceInfo => + 'Інформація про пристрій'; @override - String get settings_appSettings => 'Налаштування програми'; + String get settings_appSettings => + 'Налаштування програми'; @override String get settings_appSettingsSubtitle => - 'Сповіщення, повідомлення та налаштування карти'; + 'Сповіщення, повідомлення та налаштування карти'; @override - String get settings_nodeSettings => 'Налаштування вузла'; + String get settings_nodeSettings => 'Налаштування вузла'; @override - String get settings_nodeName => 'Ім\'я вузла'; + String get settings_nodeName => 'Ім\'я вузла'; @override - String get settings_nodeNameNotSet => 'Не встановлено'; + String get settings_nodeNameNotSet => 'Не встановлено'; @override - String get settings_nodeNameHint => 'Введіть ім\'я вузла'; + String get settings_nodeNameHint => 'Введіть ім\'я вузла'; @override - String get settings_nodeNameUpdated => 'Ім\'я оновлено'; + String get settings_nodeNameUpdated => 'Ім\'я оновлено'; @override - String get settings_radioSettings => 'Налаштування радіо'; + String get settings_radioSettings => 'Налаштування радіо'; @override String get settings_radioSettingsSubtitle => - 'Частота, потужність, коефіцієнт розширення'; + 'Частота, потужність, коефіцієнт розширення'; @override - String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено'; + String get settings_radioSettingsUpdated => + 'Налаштування радіо оновлено'; @override - String get settings_location => 'Розташування'; + String get settings_location => 'Розташування'; @override - String get settings_locationSubtitle => 'GPS координати'; + String get settings_locationSubtitle => 'GPS координати'; @override - String get settings_locationUpdated => 'Розташування оновлено'; + String get settings_locationUpdated => + 'Розташування оновлено'; @override - String get settings_locationBothRequired => 'Введіть широту та довготу.'; + String get settings_locationBothRequired => + 'Введіть широту та довготу.'; @override - String get settings_locationInvalid => 'Некоректна широта або довгота.'; + String get settings_locationInvalid => + 'Некоректна широта або довгота.'; @override - String get settings_locationGPSEnable => 'Увімкнути GPS'; + String get settings_locationGPSEnable => 'Увімкнути GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Вмикає автоматичне оновлення місцезнаходження через GPS.'; + 'Вмикає автоматичне оновлення місцезнаходження через GPS.'; @override - String get settings_locationIntervalSec => 'Інтервал для GPS (Секунди)'; + String get settings_locationIntervalSec => + 'Інтервал для GPS (Секунди)'; @override String get settings_locationIntervalInvalid => - 'Інтервал має бути не менше 60 секунд і менше 86400 секунд.'; + 'Інтервал має бути не менше 60 секунд Ñ– менше 86400 секунд.'; @override - String get settings_latitude => 'Широта'; + String get settings_latitude => 'Широта'; @override - String get settings_longitude => 'Довгота'; + String get settings_longitude => 'Довгота'; @override - String get settings_privacyMode => 'Режим приватності'; + String get settings_privacyMode => 'Режим приватності'; @override String get settings_privacyModeSubtitle => - 'Приховати ім\'я/розташування в оголошеннях'; + 'Приховати ім\'я/розташування в оголошеннях'; @override String get settings_privacyModeToggle => - 'Увімкніть режим приватності, щоб приховати своє ім\'я та місцезнаходження в оголошеннях.'; + 'Увімкніть режим приватності, щоб приховати своє ім\'я та місцезнаходження в оголошеннях.'; @override - String get settings_privacyModeEnabled => 'Режим приватності увімкнено'; + String get settings_privacyModeEnabled => + 'Режим приватності увімкнено'; @override - String get settings_privacyModeDisabled => 'Режим приватності вимкнено'; + String get settings_privacyModeDisabled => + 'Режим приватності вимкнено'; @override - String get settings_actions => 'Дії'; + String get settings_actions => 'Дії'; @override - String get settings_sendAdvertisement => 'Оголосити себе'; + String get settings_sendAdvertisement => 'Оголосити себе'; @override String get settings_sendAdvertisementSubtitle => - 'Транслювати присутність зараз'; + 'Транслювати присутність зараз'; @override - String get settings_advertisementSent => 'Оголошення надіслано'; + String get settings_advertisementSent => + 'Оголошення надіслано'; @override - String get settings_syncTime => 'Синхронізація часу'; + String get settings_syncTime => 'Синхронізація часу'; @override String get settings_syncTimeSubtitle => - 'Встановити час пристрою відповідно до часу телефону.'; + 'Встановити час пристрою відповідно до часу телефону.'; @override - String get settings_timeSynchronized => 'Час синхронізовано'; + String get settings_timeSynchronized => 'Час синхронізовано'; @override - String get settings_refreshContacts => 'Оновити контакти'; + String get settings_refreshContacts => 'Оновити контакти'; @override String get settings_refreshContactsSubtitle => - 'Перезавантажити список контактів з пристрою'; + 'Перезавантажити список контактів з пристрою'; @override - String get settings_rebootDevice => 'Перезавантажити пристрій'; + String get settings_rebootDevice => + 'Перезавантажити пристрій'; @override String get settings_rebootDeviceSubtitle => - 'Перезавантажити пристрій MeshCore'; + 'Перезавантажити пристрій MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.'; + 'Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.'; @override - String get settings_debug => 'Налагодження'; + String get settings_debug => 'Налагодження'; @override - String get settings_bleDebugLog => 'Журнал налагодження BLE'; + String get settings_bleDebugLog => + 'Журнал налагодження BLE'; @override String get settings_bleDebugLogSubtitle => - 'Команди BLE, відповіді та необроблені дані'; + 'Команди BLE, відповіді та необроблені дані'; @override - String get settings_appDebugLog => 'Журнал налагодження програми'; + String get settings_appDebugLog => + 'Журнал налагодження програми'; @override String get settings_appDebugLogSubtitle => - 'Повідомлення налагодження програми'; + 'Повідомлення налагодження програми'; @override - String get settings_about => 'Про програму'; + String get settings_about => 'Про програму'; @override String settings_aboutVersion(String version) { @@ -354,115 +361,119 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get settings_aboutLegalese => 'Проєкт MeshCore Open Source 2026'; + String get settings_aboutLegalese => 'Проєкт MeshCore Open Source 2026'; @override String get settings_aboutDescription => - 'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.'; + 'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.'; @override String get settings_aboutOpenMeteoAttribution => - 'Дані про висоту LOS: Open-Meteo (CC BY 4.0)'; + 'Дані про висоту LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'Ім\'я'; + String get settings_infoName => 'Ім\'я'; @override String get settings_infoId => 'ID'; @override - String get settings_infoStatus => 'Статус'; + String get settings_infoStatus => 'Статус'; @override - String get settings_infoBattery => 'Батарея'; + String get settings_infoBattery => 'Батарея'; @override - String get settings_infoPublicKey => 'Відкритий ключ'; + String get settings_infoPublicKey => 'Відкритий ключ'; @override - String get settings_infoContactsCount => 'Кількість контактів'; + String get settings_infoContactsCount => + 'Кількість контактів'; @override - String get settings_infoChannelCount => 'Кількість каналів'; + String get settings_infoChannelCount => 'Кількість каналів'; @override - String get settings_presets => 'Попередні налаштування'; + String get settings_presets => 'Попередні налаштування'; @override - String get settings_frequency => 'Частота (МГц)'; + String get settings_frequency => 'Частота (МГц)'; @override String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => 'Некоректна частота (300-2500 МГц)'; + String get settings_frequencyInvalid => + 'Некоректна частота (300-2500 МГц)'; @override - String get settings_bandwidth => 'Смуга пропускання'; + String get settings_bandwidth => 'Смуга пропускання'; @override - String get settings_spreadingFactor => 'Коефіцієнт розширення'; + String get settings_spreadingFactor => + 'Коефіцієнт розширення'; @override - String get settings_codingRate => 'Швидкість кодування'; + String get settings_codingRate => 'Швидкість кодування'; @override - String get settings_txPower => 'Потужність TX (дБм)'; + String get settings_txPower => 'Потужність TX (дБм)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)'; + String get settings_txPowerInvalid => + 'Некоректна потужність TX (0-22 дБм)'; @override - String get settings_clientRepeat => 'Автономна система'; + String get settings_clientRepeat => 'Автономна система'; @override String get settings_clientRepeatSubtitle => - 'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.'; + 'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.'; @override String get settings_clientRepeatFreqWarning => - 'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.'; + 'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.'; @override String settings_error(String message) { - return 'Помилка: $message'; + return 'Помилка: $message'; } @override - String get appSettings_title => 'Налаштування програми'; + String get appSettings_title => 'Налаштування програми'; @override - String get appSettings_appearance => 'Вигляд'; + String get appSettings_appearance => 'Вигляд'; @override - String get appSettings_theme => 'Тема'; + String get appSettings_theme => 'Тема'; @override - String get appSettings_themeSystem => 'Системна'; + String get appSettings_themeSystem => 'Системна'; @override - String get appSettings_themeLight => 'Світла'; + String get appSettings_themeLight => 'Світла'; @override - String get appSettings_themeDark => 'Темна'; + String get appSettings_themeDark => 'Темна'; @override - String get appSettings_language => 'Мова'; + String get appSettings_language => 'Мова'; @override - String get appSettings_languageSystem => 'Як у системі'; + String get appSettings_languageSystem => 'Як у системі'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -471,16 +482,16 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -489,1076 +500,1144 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Російська'; + String get appSettings_languageRu => 'Російська'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Українська'; @override String get appSettings_enableMessageTracing => - 'Увімкнути відстеження повідомлень'; + 'Увімкнути відстеження повідомлень'; @override String get appSettings_enableMessageTracingSubtitle => - 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; + 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; @override - String get appSettings_notifications => 'Сповіщення'; + String get appSettings_notifications => 'Сповіщення'; @override - String get appSettings_enableNotifications => 'Увімкнути сповіщення'; + String get appSettings_enableNotifications => + 'Увімкнути сповіщення'; @override String get appSettings_enableNotificationsSubtitle => - 'Отримувати сповіщення про повідомлення та оголошення'; + 'Отримувати сповіщення про повідомлення та оголошення'; @override String get appSettings_notificationPermissionDenied => - 'У доступі до сповіщень відмовлено'; + 'У доступі до сповіщень відмовлено'; @override - String get appSettings_notificationsEnabled => 'Сповіщення увімкнено'; + String get appSettings_notificationsEnabled => + 'Сповіщення увімкнено'; @override - String get appSettings_notificationsDisabled => 'Сповіщення вимкнено'; + String get appSettings_notificationsDisabled => + 'Сповіщення вимкнено'; @override - String get appSettings_messageNotifications => 'Сповіщення про повідомлення'; + String get appSettings_messageNotifications => + 'Сповіщення про повідомлення'; @override String get appSettings_messageNotificationsSubtitle => - 'Показувати сповіщення при отриманні нових повідомлень'; + 'Показувати сповіщення при отриманні нових повідомлень'; @override - String get appSettings_channelMessageNotifications => 'Сповіщення каналів'; + String get appSettings_channelMessageNotifications => + 'Сповіщення каналів'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Показувати сповіщення при отриманні повідомлень каналу'; + 'Показувати сповіщення при отриманні повідомлень каналу'; @override String get appSettings_advertisementNotifications => - 'Сповіщення про оголошення'; + 'Сповіщення про оголошення'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Показувати сповіщення при виявленні нових вузлів'; + 'Показувати сповіщення при виявленні нових вузлів'; @override - String get appSettings_messaging => 'Обмін повідомленнями'; + String get appSettings_messaging => 'Обмін повідомленнями'; @override String get appSettings_clearPathOnMaxRetry => - 'Очищати шлях після макс. спроб'; + 'Очищати шлях після макс. спроб'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Скидати шлях до контакту після 5 невдалих спроб надсилання'; + 'Скидати шлях до контакту після 5 невдалих спроб надсилання'; @override String get appSettings_pathsWillBeCleared => - 'Шляхи будуть очищені після 5 невдалих спроб.'; + 'Шляхи будуть очищені після 5 невдалих спроб.'; @override String get appSettings_pathsWillNotBeCleared => - 'Шляхи не будуть очищатися автоматично.'; + 'Шляхи не будуть очищатися автоматично.'; @override - String get appSettings_autoRouteRotation => 'Авторотація маршруту'; + String get appSettings_autoRouteRotation => + 'Авторотація маршруту'; @override String get appSettings_autoRouteRotationSubtitle => - 'Чергувати найкращі шляхи та режим «на всю мережу» (flood)'; + 'Чергувати найкращі шляхи та режим «на всю мережу» (flood)'; @override String get appSettings_autoRouteRotationEnabled => - 'Авторотація маршрутизації увімкнена'; + 'Авторотація маршрутизації увімкнена'; @override String get appSettings_autoRouteRotationDisabled => - 'Авторотація маршрутизації вимкнена'; + 'Авторотація маршрутизації вимкнена'; @override - String get appSettings_battery => 'Батарея'; + String get appSettings_battery => 'Батарея'; @override - String get appSettings_batteryChemistry => 'Хімія батареї'; + String get appSettings_batteryChemistry => 'Хімія батареї'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Встановити для пристрою ($deviceName)'; + return 'Встановити для пристрою ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Підключіть пристрій, щоб вибрати'; + 'Підключіть пристрій, щоб вибрати'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2В)'; + String get appSettings_batteryNmc => '18650 NMC (3.0-4.2Ð’)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65В)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65Ð’)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)'; + String get appSettings_batteryLipo => 'LiPo (3.0-4.2Ð’)'; @override - String get appSettings_mapDisplay => 'Відображення карти'; + String get appSettings_mapDisplay => 'Відображення карти'; @override - String get appSettings_showRepeaters => 'Показувати ретранслятори'; + String get appSettings_showRepeaters => + 'Показувати ретранслятори'; @override String get appSettings_showRepeatersSubtitle => - 'Відображати вузли-ретранслятори на карті'; + 'Відображати вузли-ретранслятори на карті'; @override - String get appSettings_showChatNodes => 'Показувати вузли чату'; + String get appSettings_showChatNodes => + 'Показувати вузли чату'; @override String get appSettings_showChatNodesSubtitle => - 'Відображати вузли чату на карті'; + 'Відображати вузли чату на карті'; @override - String get appSettings_showOtherNodes => 'Показувати інші вузли'; + String get appSettings_showOtherNodes => + 'Показувати інші вузли'; @override String get appSettings_showOtherNodesSubtitle => - 'Відображати інші типи вузлів на карті'; + 'Відображати інші типи вузлів на карті'; @override - String get appSettings_timeFilter => 'Фільтр часу'; + String get appSettings_timeFilter => 'Фільтр часу'; @override - String get appSettings_timeFilterShowAll => 'Показати всі вузли'; + String get appSettings_timeFilterShowAll => + 'Показати всі вузли'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Показати вузли за останні $hours год'; + return 'Показати вузли за останні $hours год'; } @override - String get appSettings_mapTimeFilter => 'Фільтр часу карти'; + String get appSettings_mapTimeFilter => 'Фільтр часу карти'; @override String get appSettings_showNodesDiscoveredWithin => - 'Показувати вузли, виявлені за:'; + 'Показувати вузли, виявлені за:'; @override - String get appSettings_allTime => 'Весь час'; + String get appSettings_allTime => 'Весь час'; @override - String get appSettings_lastHour => 'Останню годину'; + String get appSettings_lastHour => 'Останню годину'; @override - String get appSettings_last6Hours => 'Останні 6 годин'; + String get appSettings_last6Hours => 'Останні 6 годин'; @override - String get appSettings_last24Hours => 'Останні 24 години'; + String get appSettings_last24Hours => 'Останні 24 години'; @override - String get appSettings_lastWeek => 'Минулий тиждень'; + String get appSettings_lastWeek => 'Минулий тиждень'; @override - String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; + String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; @override - String get appSettings_unitsTitle => 'одиниці'; + String get appSettings_unitsTitle => 'одиниці'; @override - String get appSettings_unitsMetric => 'Метричний (м / км)'; + String get appSettings_unitsMetric => 'Метричний (м / км)'; @override - String get appSettings_unitsImperial => 'Імперська (ft / mi)'; + String get appSettings_unitsImperial => 'Імперська (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Область не вибрано'; + String get appSettings_noAreaSelected => 'Область не вибрано'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Вибрана область (зум $minZoom-$maxZoom)'; + return 'Вибрана область (зум $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Налагодження'; + String get appSettings_debugCard => 'Налагодження'; @override - String get appSettings_appDebugLogging => 'Логування налагодження програми'; + String get appSettings_appDebugLogging => + 'Логування налагодження програми'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записувати повідомлення налагодження програми в лог для усунення несправностей.'; + 'Записувати повідомлення налагодження програми в лог для усунення несправностей.'; @override String get appSettings_appDebugLoggingEnabled => - 'Логування налагодження програми увімкнено'; + 'Логування налагодження програми увімкнено'; @override String get appSettings_appDebugLoggingDisabled => - 'Налагодження програми вимкнено.'; + 'Налагодження програми вимкнено.'; @override - String get contacts_title => 'Контакти'; + String get contacts_title => 'Контакти'; @override - String get contacts_noContacts => 'Контактів не знайдено.'; + String get contacts_noContacts => 'Контактів не знайдено.'; @override String get contacts_contactsWillAppear => - 'Контакти з\'являться, коли пристрої надішлють оголошення.'; + 'Контакти з\'являться, коли пристрої надішлють оголошення.'; @override - String get contacts_unread => 'Непрочитане'; + String get contacts_unread => 'Непрочитане'; @override - String get contacts_searchContactsNoNumber => 'Пошук контактів...'; + String get contacts_searchContactsNoNumber => + 'Пошук контактів...'; @override String contacts_searchContacts(int number, String str) { - return 'Пошук контактів...'; + return 'Пошук контактів...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Пошук $number$str улюблених...'; + return 'Пошук $number$str улюблених...'; } @override String contacts_searchUsers(int number, String str) { - return 'Пошук $number$str користувачів...'; + return 'Пошук $number$str користувачів...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Пошук $number$str ретрансляторів...'; + return 'Пошук $number$str ретрансляторів...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Пошук $number$str серверів кімнат...'; + return 'Пошук $number$str серверів кімнат...'; } @override - String get contacts_noUnreadContacts => 'Немає непрочитаних контактів'; + String get contacts_noUnreadContacts => + 'Немає непрочитаних контактів'; @override - String get contacts_noContactsFound => 'Контактів або груп не знайдено.'; + String get contacts_noContactsFound => + 'Контактів або груп не знайдено.'; @override - String get contacts_deleteContact => 'Видалити контакт'; + String get contacts_deleteContact => 'Видалити контакт'; @override String contacts_removeConfirm(String contactName) { - return 'Видалити $contactName з контактів?'; + return 'Видалити $contactName з контактів?'; } @override - String get contacts_manageRepeater => 'Керувати ретранслятором'; + String get contacts_manageRepeater => + 'Керувати ретранслятором'; @override - String get contacts_manageRoom => 'Керувати сервером кімнати'; + String get contacts_manageRoom => + 'Керувати сервером кімнати'; @override - String get contacts_roomLogin => 'Вхід у кімнату'; + String get contacts_roomLogin => 'Вхід у кімнату'; @override - String get contacts_openChat => 'Відкрити чат'; + String get contacts_openChat => 'Відкрити чат'; @override - String get contacts_editGroup => 'Редагувати групу'; + String get contacts_editGroup => 'Редагувати групу'; @override - String get contacts_deleteGroup => 'Видалити групу'; + String get contacts_deleteGroup => 'Видалити групу'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Видалити $groupName?'; + return 'Видалити $groupName?'; } @override - String get contacts_newGroup => 'Нова група'; + String get contacts_newGroup => 'Нова група'; @override - String get contacts_groupName => 'Назва групи'; + String get contacts_groupName => 'Назва групи'; @override - String get contacts_groupNameRequired => 'Назва групи обов\'язкова.'; + String get contacts_groupNameRequired => + 'Назва групи обов\'язкова.'; @override String contacts_groupAlreadyExists(String name) { - return 'Група «$name» вже існує.'; + return 'Група «$name» вже існує.'; } @override - String get contacts_filterContacts => 'Фільтрувати контакти...'; + String get contacts_filterContacts => + 'Фільтрувати контакти...'; @override String get contacts_noContactsMatchFilter => - 'Жоден контакт не відповідає фільтру.'; + 'Жоден контакт не відповідає фільтру.'; @override - String get contacts_noMembers => 'Немає учасників'; + String get contacts_noMembers => 'Немає учасників'; @override - String get contacts_lastSeenNow => 'В мережі'; + String get contacts_lastSeenNow => 'Ð’ мережі'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'В мережі $minutes хв. тому'; + return 'Ð’ мережі $minutes хв. тому'; } @override - String get contacts_lastSeenHourAgo => 'В мережі 1 годину тому'; + String get contacts_lastSeenHourAgo => + 'Ð’ мережі 1 годину тому'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'В мережі $hours год. тому'; + return 'Ð’ мережі $hours год. тому'; } @override - String get contacts_lastSeenDayAgo => 'В мережі 1 день тому'; + String get contacts_lastSeenDayAgo => 'Ð’ мережі 1 день тому'; @override String contacts_lastSeenDaysAgo(int days) { - return 'В мережі $days дн. тому'; + return 'Ð’ мережі $days дн. тому'; } @override - String get channels_title => 'Канали'; + String get channels_title => 'Канали'; @override - String get channels_noChannelsConfigured => 'Канали не налаштовані'; + String get channels_noChannelsConfigured => + 'Канали не налаштовані'; @override - String get channels_addPublicChannel => 'Додати публічний канал'; + String get channels_addPublicChannel => + 'Додати публічний канал'; @override - String get channels_searchChannels => 'Пошук каналів...'; + String get channels_searchChannels => 'Пошук каналів...'; @override - String get channels_noChannelsFound => 'Каналів не знайдено'; + String get channels_noChannelsFound => 'Каналів не знайдено'; @override String channels_channelIndex(int index) { - return 'Канал $index'; + return 'Канал $index'; } @override - String get channels_hashtagChannel => 'Канал з хештегом'; + String get channels_hashtagChannel => 'Канал з хештегом'; @override - String get channels_public => 'Публічний'; + String get channels_public => 'Публічний'; @override - String get channels_private => 'Приватний'; + String get channels_private => 'Приватний'; @override - String get channels_publicChannel => 'Публічний канал'; + String get channels_publicChannel => 'Публічний канал'; @override - String get channels_privateChannel => 'Приватний канал'; + String get channels_privateChannel => 'Приватний канал'; @override - String get channels_editChannel => 'Редагувати канал'; + String get channels_editChannel => 'Редагувати канал'; @override - String get channels_muteChannel => 'Вимкнути сповіщення каналу'; + String get channels_muteChannel => + 'Вимкнути сповіщення каналу'; @override - String get channels_unmuteChannel => 'Увімкнути сповіщення каналу'; + String get channels_unmuteChannel => + 'Увімкнути сповіщення каналу'; @override - String get channels_deleteChannel => 'Видалити канал'; + String get channels_deleteChannel => 'Видалити канал'; @override String channels_deleteChannelConfirm(String name) { - return 'Видалити $name? Це не можна скасувати.'; + return 'Видалити $name? Це не можна скасувати.'; } @override String channels_channelDeleteFailed(String name) { - return 'Не вдалося видалити канал \"$name\"'; + return 'Не вдалося видалити канал \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Канал «$name» видалено'; + return 'Канал «$name» видалено'; } @override - String get channels_addChannel => 'Додати канал'; + String get channels_addChannel => 'Додати канал'; @override - String get channels_channelIndexLabel => 'Індекс каналу'; + String get channels_channelIndexLabel => 'Індекс каналу'; @override - String get channels_channelName => 'Назва каналу'; + String get channels_channelName => 'Назва каналу'; @override - String get channels_usePublicChannel => 'Використовувати публічний канал'; + String get channels_usePublicChannel => + 'Використовувати публічний канал'; @override - String get channels_standardPublicPsk => 'Стандартний публічний PSK'; + String get channels_standardPublicPsk => + 'Стандартний публічний PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Згенерувати випадковий ключ PSK'; + String get channels_generateRandomPsk => + 'Згенерувати випадковий ключ PSK'; @override - String get channels_enterChannelName => 'Будь ласка, введіть назву каналу'; + String get channels_enterChannelName => + 'Будь ласка, введіть назву каналу'; @override String get channels_pskMustBe32Hex => - 'PSK має складатися з 32 шістнадцяткових символів.'; + 'PSK має складатися з 32 шістнадцяткових символів.'; @override String channels_channelAdded(String name) { - return 'Канал «$name» додано'; + return 'Канал «$name» додано'; } @override String channels_editChannelTitle(int index) { - return 'Редагувати канал $index'; + return 'Редагувати канал $index'; } @override - String get channels_smazCompression => 'Стиснення SMAZ'; + String get channels_smazCompression => 'Стиснення SMAZ'; @override String channels_channelUpdated(String name) { - return 'Канал «$name» оновлено'; + return 'Канал «$name» оновлено'; } @override - String get channels_publicChannelAdded => 'Публічний канал додано'; + String get channels_publicChannelAdded => + 'Публічний канал додано'; @override - String get channels_sortBy => 'Сортувати за'; + String get channels_sortBy => 'Сортувати за'; @override - String get channels_sortManual => 'Вручну'; + String get channels_sortManual => 'Вручну'; @override - String get channels_sortAZ => 'А-Я'; + String get channels_sortAZ => 'А-Я'; @override - String get channels_sortLatestMessages => 'Останні повідомлення'; + String get channels_sortLatestMessages => + 'Останні повідомлення'; @override - String get channels_sortUnread => 'Непрочитані'; + String get channels_sortUnread => 'Непрочитані'; @override - String get channels_createPrivateChannel => 'Створити приватний канал'; + String get channels_createPrivateChannel => + 'Створити приватний канал'; @override - String get channels_createPrivateChannelDesc => 'Захищено секретним ключем.'; + String get channels_createPrivateChannelDesc => + 'Захищено секретним ключем.'; @override - String get channels_joinPrivateChannel => 'Приєднатися до приватного каналу'; + String get channels_joinPrivateChannel => + 'Приєднатися до приватного каналу'; @override - String get channels_joinPrivateChannelDesc => 'Ввести секретний ключ вручну.'; + String get channels_joinPrivateChannelDesc => + 'Ввести секретний ключ вручну.'; @override - String get channels_joinPublicChannel => 'Приєднатися до публічного каналу'; + String get channels_joinPublicChannel => + 'Приєднатися до публічного каналу'; @override String get channels_joinPublicChannelDesc => - 'Будь-хто може приєднатися до цього каналу.'; + 'Будь-хто може приєднатися до цього каналу.'; @override - String get channels_joinHashtagChannel => 'Приєднатися до каналу з хештегом'; + String get channels_joinHashtagChannel => + 'Приєднатися до каналу з хештегом'; @override String get channels_joinHashtagChannelDesc => - 'Будь-хто може приєднатися до каналів #hashtag.'; + 'Будь-хто може приєднатися до каналів #hashtag.'; @override - String get channels_scanQrCode => 'Сканувати QR-код'; + String get channels_scanQrCode => 'Сканувати QR-код'; @override - String get channels_scanQrCodeComingSoon => 'Скоро буде'; + String get channels_scanQrCodeComingSoon => 'Скоро буде'; @override - String get channels_enterHashtag => 'Введіть хештег'; + String get channels_enterHashtag => 'Введіть хештег'; @override - String get channels_hashtagHint => 'напр. #команда'; + String get channels_hashtagHint => 'напр. #команда'; @override - String get chat_noMessages => 'Поки немає повідомлень.'; + String get chat_noMessages => 'Поки немає повідомлень.'; @override - String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати'; + String get chat_sendMessageToStart => + 'Надішліть повідомлення, щоб почати'; @override String get chat_originalMessageNotFound => - 'Оригінальне повідомлення не знайдено'; + 'Оригінальне повідомлення не знайдено'; @override String chat_replyingTo(String name) { - return 'Відповідь $name'; + return 'Відповідь $name'; } @override String chat_replyTo(String name) { - return 'Відповісти $name'; + return 'Відповісти $name'; } @override - String get chat_location => 'Розташування'; + String get chat_location => 'Розташування'; @override String chat_sendMessageTo(String contactName) { - return 'Надіслати повідомлення $contactName'; + return 'Надіслати повідомлення $contactName'; } @override - String get chat_typeMessage => 'Введіть повідомлення...'; + String get chat_typeMessage => 'Введіть повідомлення...'; @override String chat_messageTooLong(int maxBytes) { - return 'Повідомлення занадто довге (макс. $maxBytes байт).'; + return 'Повідомлення занадто довге (макс. $maxBytes байт).'; } @override - String get chat_messageCopied => 'Повідомлення скопійовано'; + String get chat_messageCopied => + 'Повідомлення скопійовано'; @override - String get chat_messageDeleted => 'Повідомлення видалено'; + String get chat_messageDeleted => 'Повідомлення видалено'; @override - String get chat_retryingMessage => 'Спроба відновлення.'; + String get chat_retryingMessage => 'Спроба відновлення.'; @override String chat_retryCount(int current, int max) { - return 'Повторна спроба $current/$max'; + return 'Повторна спроба $current/$max'; } @override - String get chat_sendGif => 'Надіслати GIF'; + String get chat_sendGif => 'Надіслати GIF'; @override - String get chat_reply => 'Відповісти'; + String get chat_reply => 'Відповісти'; @override - String get chat_addReaction => 'Додати реакцію'; + String get chat_addReaction => 'Додати реакцію'; @override - String get chat_me => 'Я'; + String get chat_me => 'Я'; @override - String get emojiCategorySmileys => 'Емодзі'; + String get emojiCategorySmileys => 'Емодзі'; @override - String get emojiCategoryGestures => 'Жести'; + String get emojiCategoryGestures => 'Жести'; @override - String get emojiCategoryHearts => 'Серця'; + String get emojiCategoryHearts => 'Серця'; @override - String get emojiCategoryObjects => 'Об\'єкти'; + String get emojiCategoryObjects => 'Об\'єкти'; @override - String get gifPicker_title => 'Вибрати GIF'; + String get gifPicker_title => 'Вибрати GIF'; @override - String get gifPicker_searchHint => 'Пошук GIF...'; + String get gifPicker_searchHint => 'Пошук GIF...'; @override - String get gifPicker_poweredBy => 'На базі GIPHY'; + String get gifPicker_poweredBy => 'На базі GIPHY'; @override - String get gifPicker_noGifsFound => 'GIF не знайдено'; + String get gifPicker_noGifsFound => 'GIF не знайдено'; @override - String get gifPicker_failedLoad => 'Не вдалося завантажити GIF-файли'; + String get gifPicker_failedLoad => + 'Не вдалося завантажити GIF-файли'; @override - String get gifPicker_failedSearch => 'Пошук GIF не вдався'; + String get gifPicker_failedSearch => 'Пошук GIF не вдався'; @override - String get gifPicker_noInternet => 'Немає інтернет-з\'єднання'; + String get gifPicker_noInternet => + 'Немає інтернет-з\'єднання'; @override - String get debugLog_appTitle => 'Журнал налагодження програми'; + String get debugLog_appTitle => + 'Журнал налагодження програми'; @override - String get debugLog_bleTitle => 'Журнал налагодження BLE'; + String get debugLog_bleTitle => 'Журнал налагодження BLE'; @override - String get debugLog_copyLog => 'Копіювати журнал'; + String get debugLog_copyLog => 'Копіювати журнал'; @override - String get debugLog_clearLog => 'Очистити журнал'; + String get debugLog_clearLog => 'Очистити журнал'; @override - String get debugLog_copied => 'Журнал налагодження скопійовано'; + String get debugLog_copied => + 'Журнал налагодження скопійовано'; @override - String get debugLog_bleCopied => 'Журнал BLE скопійовано'; + String get debugLog_bleCopied => 'Журнал BLE скопійовано'; @override String get debugLog_noEntries => - 'Поки що немає записів журналу налагодження.'; + 'Поки що немає записів журналу налагодження.'; @override String get debugLog_enableInSettings => - 'Увімкніть налагодження програми в налаштуваннях'; + 'Увімкніть налагодження програми в налаштуваннях'; @override - String get debugLog_frames => 'Кадри'; + String get debugLog_frames => 'Кадри'; @override - String get debugLog_rawLogRx => 'Необроблений лог - RX'; + String get debugLog_rawLogRx => 'Необроблений лог - RX'; @override - String get debugLog_noBleActivity => 'Поки що немає активності BLE.'; + String get debugLog_noBleActivity => + 'Поки що немає активності BLE.'; @override String debugFrame_length(int count) { - return 'Довжина кадру: $count байт'; + return 'Довжина кадру: $count байт'; } @override String debugFrame_command(String value) { - return 'Команда: 0x$value'; + return 'Команда: 0x$value'; } @override - String get debugFrame_textMessageHeader => 'Повідомлення:'; + String get debugFrame_textMessageHeader => 'Повідомлення:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- PubKey призначення: $pubKey'; + return '- PubKey призначення: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Мітка часу: $timestamp'; + return '- Мітка часу: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Прапорці: 0x$value'; + return '- Прапорці: 0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- Тип тексту: $type ($label)'; + return '- Тип тексту: $type ($label)'; } @override String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Звичайний'; + String get debugFrame_textTypePlain => 'Звичайний'; @override String debugFrame_text(String text) { - return '- Текст: \"$text\"'; + return '- Текст: \"$text\"'; } @override - String get debugFrame_hexDump => 'Дамп Hex:'; + String get debugFrame_hexDump => 'Дамп Hex:'; @override - String get chat_pathManagement => 'Керування шляхами'; + String get chat_pathManagement => 'Керування шляхами'; @override - String get chat_ShowAllPaths => 'Показати всі шляхи'; + String get chat_ShowAllPaths => 'Показати всі шляхи'; @override - String get chat_routingMode => 'Режим маршрутизації'; + String get chat_routingMode => 'Режим маршрутизації'; @override - String get chat_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; + String get chat_autoUseSavedPath => + 'Авто (використовувати збережений шлях)'; @override - String get chat_forceFloodMode => 'Примусово на всю мережу'; + String get chat_forceFloodMode => + 'Примусово на всю мережу'; @override String get chat_recentAckPaths => - 'Недавні шляхи ACK (натисніть, щоб використати):'; + 'Недавні шляхи ACK (натисніть, щоб використати):'; @override String get chat_pathHistoryFull => - 'Історія шляхів заповнена. Видаліть записи, щоб додати нові.'; + 'Історія шляхів заповнена. Видаліть записи, щоб додати нові.'; @override - String get chat_hopSingular => 'Стрибок'; + String get chat_hopSingular => 'Стрибок'; @override - String get chat_hopPlural => 'стрибків'; + String get chat_hopPlural => 'стрибків'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'стрибків', - many: 'стрибків', - few: 'стрибки', - one: 'стрибок', + other: 'стрибків', + many: 'стрибків', + few: 'стрибки', + one: 'стрибок', ); return '$count $_temp0'; } @override - String get chat_successes => 'Успішно'; + String get chat_successes => 'Успішно'; @override - String get chat_removePath => 'Видалити шлях'; + String get chat_removePath => 'Видалити шлях'; @override String get chat_noPathHistoryYet => - 'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.'; + 'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.'; @override - String get chat_pathActions => 'Дії зі шляхом:'; + String get chat_pathActions => 'Дії зі шляхом:'; @override - String get chat_setCustomPath => 'Встановити власний шлях'; + String get chat_setCustomPath => + 'Встановити власний шлях'; @override - String get chat_setCustomPathSubtitle => 'Вказати шлях маршрутизації вручну'; + String get chat_setCustomPathSubtitle => + 'Вказати шлях маршрутизації вручну'; @override - String get chat_clearPath => 'Очистити шлях'; + String get chat_clearPath => 'Очистити шлях'; @override String get chat_clearPathSubtitle => - 'Примусово повторити пошук при наступному надсиланні'; + 'Примусово повторити пошук при наступному надсиланні'; @override String get chat_pathCleared => - 'Шлях очищено. Наступне повідомлення оновить маршрут.'; + 'Шлях очищено. Наступне повідомлення оновить маршрут.'; @override String get chat_floodModeSubtitle => - 'Використовувати перемикач маршрутизації в панелі програми'; + 'Використовувати перемикач маршрутизації в панелі програми'; @override String get chat_floodModeEnabled => - 'Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; + 'Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; @override - String get chat_fullPath => 'Повний шлях'; + String get chat_fullPath => 'Повний шлях'; @override String get chat_pathDetailsNotAvailable => - 'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.'; + 'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.'; @override String chat_pathSetHops(int hopCount, String status) { String _temp0 = intl.Intl.pluralLogic( hopCount, locale: localeName, - other: 'стрибків', - many: 'стрибків', - few: 'стрибки', - one: 'стрибок', + other: 'стрибків', + many: 'стрибків', + few: 'стрибки', + one: 'стрибок', ); - return 'Шлях встановлено: $hopCount $_temp0 - $status'; + return 'Шлях встановлено: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Збережено локально. Підключіться для синхронізації.'; + 'Збережено локально. Підключіться для синхронізації.'; @override - String get chat_pathDeviceConfirmed => 'Пристрій підтверджено.'; + String get chat_pathDeviceConfirmed => + 'Пристрій підтверджено.'; @override - String get chat_pathDeviceNotConfirmed => 'Пристрій ще не підтверджено.'; + String get chat_pathDeviceNotConfirmed => + 'Пристрій ще не підтверджено.'; @override - String get chat_type => 'Ввід'; + String get chat_type => 'Ввід'; @override - String get chat_path => 'Шлях'; + String get chat_path => 'Шлях'; @override - String get chat_publicKey => 'Відкритий ключ'; + String get chat_publicKey => 'Відкритий ключ'; @override - String get chat_compressOutgoingMessages => 'Стискати вихідні повідомлення'; + String get chat_compressOutgoingMessages => + 'Стискати вихідні повідомлення'; @override - String get chat_floodForced => 'На всю мережу (примусово)'; + String get chat_floodForced => + 'На всю мережу (примусово)'; @override - String get chat_directForced => 'Прямий (примусово)'; + String get chat_directForced => 'Прямий (примусово)'; @override String chat_hopsForced(int count) { - return '$count стрибків (примусово)'; + return '$count стрибків (примусово)'; } @override - String get chat_floodAuto => 'На всю мережу (авто)'; + String get chat_floodAuto => 'На всю мережу (авто)'; @override - String get chat_direct => 'Прямий'; + String get chat_direct => 'Прямий'; @override - String get chat_poiShared => 'Точкою інтересу поділилися'; + String get chat_poiShared => + 'Точкою інтересу поділилися'; @override String chat_unread(int count) { - return 'Непрочитано: $count'; + return 'Непрочитано: $count'; } @override - String get chat_openLink => 'Відкрити посилання?'; + String get chat_openLink => 'Відкрити посилання?'; @override String get chat_openLinkConfirmation => - 'Ви хочете відкрити це посилання у браузері?'; + 'Ви хочете відкрити це посилання у браузері?'; @override - String get chat_open => 'Відкрити'; + String get chat_open => 'Відкрити'; @override String chat_couldNotOpenLink(String url) { - return 'Не вдалося відкрити посилання: $url'; + return 'Не вдалося відкрити посилання: $url'; } @override - String get chat_invalidLink => 'Невірний формат посилання'; + String get chat_invalidLink => + 'Невірний формат посилання'; @override - String get map_title => 'Карта вузлів'; + String get map_title => 'Карта вузлів'; @override - String get map_lineOfSight => 'Пряма видимість'; + String get map_lineOfSight => 'Пряма видимість'; @override - String get map_losScreenTitle => 'Пряма видимість'; + String get map_losScreenTitle => 'Пряма видимість'; @override String get map_noNodesWithLocation => - 'Немає вузлів з даними про розташування'; + 'Немає вузлів з даними про розташування'; @override String get map_nodesNeedGps => - 'Вузли повинні надавати свої GPS координати,\nщоб з\'явитися на карті.'; + 'Вузли повинні надавати свої GPS координати,\nщоб з\'явитися на карті.'; @override String map_nodesCount(int count) { - return 'Вузли: $count'; + return 'Вузли: $count'; } @override String map_pinsCount(int count) { - return 'Мітки: $count'; + return 'Мітки: $count'; } @override - String get map_chat => 'Чат'; + String get map_chat => 'Чат'; @override - String get map_repeater => 'Ретранслятор'; + String get map_repeater => 'Ретранслятор'; @override - String get map_room => 'Кімната'; + String get map_room => 'Кімната'; @override - String get map_sensor => 'Сенсор'; + String get map_sensor => 'Сенсор'; @override - String get map_pinDm => 'Ключ (DM)'; + String get map_pinDm => 'Ключ (DM)'; @override - String get map_pinPrivate => 'Замок (Приватний)'; + String get map_pinPrivate => 'Замок (Приватний)'; @override - String get map_pinPublic => 'Ключ (Публічний)'; + String get map_pinPublic => 'Ключ (Публічний)'; @override - String get map_lastSeen => 'Останній раз бачили'; + String get map_lastSeen => 'Останній раз бачили'; @override String get map_disconnectConfirm => - 'Ви впевнені, що хочете відключитися від цього пристрою?'; + 'Ви впевнені, що хочете відключитися від цього пристрою?'; @override - String get map_from => 'Від'; + String get map_from => 'Від'; @override - String get map_source => 'Джерело'; + String get map_source => 'Джерело'; @override - String get map_flags => 'Прапорці'; + String get map_flags => 'Прапорці'; @override - String get map_shareMarkerHere => 'Поділитися маркером тут'; + String get map_shareMarkerHere => + 'Поділитися маркером тут'; @override - String get map_pinLabel => 'Мітка піна'; + String get map_pinLabel => 'Мітка піна'; @override - String get map_label => 'Мітка'; + String get map_label => 'Мітка'; @override - String get map_pointOfInterest => 'Точка інтересу'; + String get map_pointOfInterest => 'Точка інтересу'; @override - String get map_sendToContact => 'Надіслати контакту'; + String get map_sendToContact => 'Надіслати контакту'; @override - String get map_sendToChannel => 'Надіслати в канал'; + String get map_sendToChannel => 'Надіслати в канал'; @override - String get map_noChannelsAvailable => 'Немає доступних каналів'; + String get map_noChannelsAvailable => + 'Немає доступних каналів'; @override - String get map_publicLocationShare => 'Поділитися в публічному місці'; + String get map_publicLocationShare => + 'Поділитися в публічному місці'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Ви збираєтеся поділитися розташуванням у $channelLabel. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.'; + return 'Ви збираєтеся поділитися розташуванням у $channelLabel. Цей канал публічний, Ñ– кожен, хто має ключ PSK, може це побачити.'; } @override String get map_connectToShareMarkers => - 'Підключіться до пристрою, щоб поділитися маркерами'; + 'Підключіться до пристрою, щоб поділитися маркерами'; @override - String get map_filterNodes => 'Фільтрувати вузли'; + String get map_filterNodes => 'Фільтрувати вузли'; @override - String get map_nodeTypes => 'Типи вузлів'; + String get map_nodeTypes => 'Типи вузлів'; @override - String get map_chatNodes => 'Вузли чату'; + String get map_chatNodes => 'Вузли чату'; @override - String get map_repeaters => 'Ретранслятори'; + String get map_repeaters => 'Ретранслятори'; @override - String get map_otherNodes => 'Інші вузли'; + String get map_otherNodes => 'Інші вузли'; @override - String get map_keyPrefix => 'Префікс ключа'; + String get map_keyPrefix => 'Префікс ключа'; @override - String get map_filterByKeyPrefix => 'Фільтрувати за префіксом ключа'; + String get map_filterByKeyPrefix => + 'Фільтрувати за префіксом ключа'; @override - String get map_publicKeyPrefix => 'Префікс відкритого ключа'; + String get map_publicKeyPrefix => + 'Префікс відкритого ключа'; @override - String get map_markers => 'Маркери'; + String get map_markers => 'Маркери'; @override - String get map_showSharedMarkers => 'Показувати спільні маркери'; + String get map_showSharedMarkers => + 'Показувати спільні маркери'; @override - String get map_lastSeenTime => 'Час останньої активності'; + String get map_lastSeenTime => + 'Час останньої активності'; @override - String get map_sharedPin => 'Спільний пін'; + String get map_sharedPin => 'Спільний пін'; @override - String get map_joinRoom => 'Приєднатися до кімнати'; + String get map_joinRoom => 'Приєднатися до кімнати'; @override - String get map_manageRepeater => 'Керувати ретранслятором'; + String get map_manageRepeater => + 'Керувати ретранслятором'; @override - String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху'; + String get map_tapToAdd => + 'Натисніть на вузли, щоб додати Ñ—Ñ… до шляху'; @override - String get map_runTrace => 'Виконати трасування шляху'; + String get map_runTrace => 'Виконати трасування шляху'; @override - String get map_removeLast => 'Видалити останній'; + String get map_removeLast => 'Видалити останній'; @override - String get map_pathTraceCancelled => 'Відмінується трасування шляху'; + String get map_pathTraceCancelled => + 'Відмінується трасування шляху'; @override - String get mapCache_title => 'Офлайн-кеш карти'; + String get mapCache_title => 'Офлайн-кеш карти'; @override String get mapCache_selectAreaFirst => - 'Спершу виберіть область для кешування'; + 'Спершу виберіть область для кешування'; @override String get mapCache_noTilesToDownload => - 'Немає плиток для завантаження в цій області.'; + 'Немає плиток для завантаження в цій області.'; @override - String get mapCache_downloadTilesTitle => 'Завантажити плитки'; + String get mapCache_downloadTilesTitle => + 'Завантажити плитки'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Завантажити $count плиток для використання офлайн?'; + return 'Завантажити $count плиток для використання офлайн?'; } @override - String get mapCache_downloadAction => 'Завантажити'; + String get mapCache_downloadAction => 'Завантажити'; @override String mapCache_cachedTiles(int count) { - return 'Закешовано $count плиток'; + return 'Закешовано $count плиток'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Плитки в кеші ($downloaded) ($failed помилок)'; + return 'Плитки в кеші ($downloaded) ($failed помилок)'; } @override - String get mapCache_clearOfflineCacheTitle => 'Очистити офлайн-кеш'; + String get mapCache_clearOfflineCacheTitle => + 'Очистити офлайн-кеш'; @override String get mapCache_clearOfflineCachePrompt => - 'Видалити всі закешовані плитки карти?'; + 'Видалити всі закешовані плитки карти?'; @override - String get mapCache_offlineCacheCleared => 'Офлайн-кеш очищено.'; + String get mapCache_offlineCacheCleared => + 'Офлайн-кеш очищено.'; @override - String get mapCache_noAreaSelected => 'Область не вибрано'; + String get mapCache_noAreaSelected => 'Область не вибрано'; @override - String get mapCache_cacheArea => 'Область кешування'; + String get mapCache_cacheArea => 'Область кешування'; @override - String get mapCache_useCurrentView => 'Використати поточний вигляд'; + String get mapCache_useCurrentView => + 'Використати поточний вигляд'; @override - String get mapCache_zoomRange => 'Діапазон масштабування'; + String get mapCache_zoomRange => + 'Діапазон масштабування'; @override String mapCache_estimatedTiles(int count) { - return 'Оцінка плиток: $count'; + return 'Оцінка плиток: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Завантажено $completed / $total'; + return 'Завантажено $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Завантажити плитки'; + String get mapCache_downloadTilesButton => + 'Завантажити плитки'; @override - String get mapCache_clearCacheButton => 'Очистити кеш'; + String get mapCache_clearCacheButton => 'Очистити кеш'; @override String mapCache_failedDownloads(int count) { - return 'Невдалі завантаження: $count'; + return 'Невдалі завантаження: $count'; } @override @@ -1568,132 +1647,134 @@ class AppLocalizationsUk extends AppLocalizations { String east, String west, ) { - return 'Пн $north, Пд $south, Сх $east, Зх $west'; + return 'Пн $north, Пд $south, Сх $east, Зх $west'; } @override - String get time_justNow => 'Тільки що'; + String get time_justNow => 'Тільки що'; @override String time_minutesAgo(int minutes) { - return '$minutes хв. тому'; + return '$minutes хв. тому'; } @override String time_hoursAgo(int hours) { - return '$hours год. тому'; + return '$hours год. тому'; } @override String time_daysAgo(int days) { - return '$days дн. тому'; + return '$days дн. тому'; } @override - String get time_hour => 'година'; + String get time_hour => 'година'; @override - String get time_hours => 'годин'; + String get time_hours => 'годин'; @override - String get time_day => 'день'; + String get time_day => 'день'; @override - String get time_days => 'днів'; + String get time_days => 'днів'; @override - String get time_week => 'тиждень'; + String get time_week => 'тиждень'; @override - String get time_weeks => 'тижнів'; + String get time_weeks => 'тижнів'; @override - String get time_month => 'місяць'; + String get time_month => 'місяць'; @override - String get time_months => 'місяців'; + String get time_months => 'місяців'; @override - String get time_minutes => 'хвилин'; + String get time_minutes => 'хвилин'; @override - String get time_allTime => 'Весь час'; + String get time_allTime => 'Весь час'; @override - String get dialog_disconnect => 'Відключити'; + String get dialog_disconnect => 'Відключити'; @override String get dialog_disconnectConfirm => - 'Ви впевнені, що хочете відключитися від цього пристрою?'; + 'Ви впевнені, що хочете відключитися від цього пристрою?'; @override - String get login_repeaterLogin => 'Вхід у ретранслятор'; + String get login_repeaterLogin => 'Вхід у ретранслятор'; @override - String get login_roomLogin => 'Вхід у кімнату'; + String get login_roomLogin => 'Вхід у кімнату'; @override - String get login_password => 'Пароль'; + String get login_password => 'Пароль'; @override - String get login_enterPassword => 'Введіть пароль'; + String get login_enterPassword => 'Введіть пароль'; @override - String get login_savePassword => 'Зберегти пароль'; + String get login_savePassword => 'Зберегти пароль'; @override String get login_savePasswordSubtitle => - 'Пароль буде надійно збережено на цьому пристрої.'; + 'Пароль буде надійно збережено на цьому пристрої.'; @override String get login_repeaterDescription => - 'Введіть пароль ретранслятора для доступу до налаштувань та статусу.'; + 'Введіть пароль ретранслятора для доступу до налаштувань та статусу.'; @override String get login_roomDescription => - 'Введіть пароль кімнати для доступу до налаштувань та статусу.'; + 'Введіть пароль кімнати для доступу до налаштувань та статусу.'; @override - String get login_routing => 'Маршрутизація'; + String get login_routing => 'Маршрутизація'; @override - String get login_routingMode => 'Режим маршрутизації'; + String get login_routingMode => 'Режим маршрутизації'; @override - String get login_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; + String get login_autoUseSavedPath => + 'Авто (використовувати збережений шлях)'; @override - String get login_forceFloodMode => 'Примусово на всю мережу'; + String get login_forceFloodMode => + 'Примусово на всю мережу'; @override - String get login_managePaths => 'Керувати шляхами'; + String get login_managePaths => 'Керувати шляхами'; @override - String get login_login => 'Вхід'; + String get login_login => 'Вхід'; @override String login_attempt(int current, int max) { - return 'Спроба $current/$max'; + return 'Спроба $current/$max'; } @override String login_failed(String error) { - return 'Вхід не вдався: $error'; + return 'Вхід не вдався: $error'; } @override String get login_failedMessage => - 'Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.'; + 'Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.'; @override - String get common_reload => 'Перезавантажити'; + String get common_reload => 'Перезавантажити'; @override - String get common_clear => 'Очистити'; + String get common_clear => 'Очистити'; @override String path_currentPath(String path) { - return 'Поточний шлях: $path'; + return 'Поточний шлях: $path'; } @override @@ -1701,174 +1782,182 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'стрибками', - many: 'стрибками', - few: 'стрибками', - one: 'стрибком', + other: 'стрибками', + many: 'стрибками', + few: 'стрибками', + one: 'стрибком', ); - return 'Використання шляху з $count $_temp0'; + return 'Використання шляху з $count $_temp0'; } @override - String get path_enterCustomPath => 'Ввести власний шлях'; + String get path_enterCustomPath => 'Ввести власний шлях'; @override - String get path_currentPathLabel => 'Поточний шлях'; + String get path_currentPathLabel => 'Поточний шлях'; @override String get path_hexPrefixInstructions => - 'Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.'; + 'Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.'; @override String get path_hexPrefixExample => - 'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).'; + 'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).'; @override - String get path_labelHexPrefixes => 'Hex-префікси'; + String get path_labelHexPrefixes => 'Hex-префікси'; @override String get path_helperMaxHops => - 'Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)'; + 'Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)'; @override - String get path_selectFromContacts => 'Вибрати з контактів:'; + String get path_selectFromContacts => 'Вибрати з контактів:'; @override String get path_noRepeatersFound => - 'Ретрансляторів або серверів кімнат не знайдено.'; + 'Ретрансляторів або серверів кімнат не знайдено.'; @override String get path_customPathsRequire => - 'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.'; + 'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Некоректні hex-префікси: $prefixes'; + return 'Некоректні hex-префікси: $prefixes'; } @override - String get path_tooLong => 'Шлях занадто довгий. Максимум 64 стрибки.'; + String get path_tooLong => + 'Шлях занадто довгий. Максимум 64 стрибки.'; @override - String get path_setPath => 'Встановити шлях'; + String get path_setPath => 'Встановити шлях'; @override - String get repeater_management => 'Керування ретранслятором'; + String get repeater_management => + 'Керування ретранслятором'; @override - String get room_management => 'Адміністрування сервера кімнати'; + String get room_management => + 'Адміністрування сервера кімнати'; @override - String get repeater_managementTools => 'Інструменти керування'; + String get repeater_managementTools => + 'Інструменти керування'; @override - String get repeater_status => 'Статус'; + String get repeater_status => 'Статус'; @override String get repeater_statusSubtitle => - 'Показати статус, статистику та сусідів ретранслятора'; + 'Показати статус, статистику та сусідів ретранслятора'; @override - String get repeater_telemetry => 'Телеметрія'; + String get repeater_telemetry => 'Телеметрія'; @override String get repeater_telemetrySubtitle => - 'Показати телеметрію сенсорів та статистику системи'; + 'Показати телеметрію сенсорів та статистику системи'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Надіслати команди ретранслятору'; + String get repeater_cliSubtitle => + 'Надіслати команди ретранслятору'; @override - String get repeater_neighbors => 'Сусіди'; + String get repeater_neighbors => 'Сусіди'; @override String get repeater_neighborsSubtitle => - 'Показати сусідів нульового стрибка.'; + 'Показати сусідів нульового стрибка.'; @override - String get repeater_settings => 'Налаштування'; + String get repeater_settings => 'Налаштування'; @override - String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора'; + String get repeater_settingsSubtitle => + 'Налаштувати параметри ретранслятора'; @override - String get repeater_statusTitle => 'Статус ретранслятора'; + String get repeater_statusTitle => 'Статус ретранслятора'; @override - String get repeater_routingMode => 'Режим маршрутизації'; + String get repeater_routingMode => 'Режим маршрутизації'; @override String get repeater_autoUseSavedPath => - 'Авто (використовувати збережений шлях)'; + 'Авто (використовувати збережений шлях)'; @override - String get repeater_forceFloodMode => 'Примусово на всю мережу'; + String get repeater_forceFloodMode => + 'Примусово на всю мережу'; @override - String get repeater_pathManagement => 'Керування шляхами'; + String get repeater_pathManagement => 'Керування шляхами'; @override - String get repeater_refresh => 'Оновити'; + String get repeater_refresh => 'Оновити'; @override String get repeater_statusRequestTimeout => - 'Час очікування запиту статусу вичерпано.'; + 'Час очікування запиту статусу вичерпано.'; @override String repeater_errorLoadingStatus(String error) { - return 'Помилка завантаження статусу: $error'; + return 'Помилка завантаження статусу: $error'; } @override - String get repeater_systemInformation => 'Системна інформація'; + String get repeater_systemInformation => + 'Системна інформація'; @override - String get repeater_battery => 'Батарея'; + String get repeater_battery => 'Батарея'; @override - String get repeater_clockAtLogin => 'Годинник (при вході)'; + String get repeater_clockAtLogin => 'Годинник (при вході)'; @override - String get repeater_uptime => 'Час роботи'; + String get repeater_uptime => 'Час роботи'; @override - String get repeater_queueLength => 'Довжина черги'; + String get repeater_queueLength => 'Довжина черги'; @override - String get repeater_debugFlags => 'Прапорці налагодження'; + String get repeater_debugFlags => 'Прапорці налагодження'; @override - String get repeater_radioStatistics => 'Статистика радіо'; + String get repeater_radioStatistics => 'Статистика радіо'; @override - String get repeater_lastRssi => 'Останній RSSI'; + String get repeater_lastRssi => 'Останній RSSI'; @override - String get repeater_lastSnr => 'Останній SNR'; + String get repeater_lastSnr => 'Останній SNR'; @override - String get repeater_noiseFloor => 'Рівень шуму'; + String get repeater_noiseFloor => 'Рівень шуму'; @override - String get repeater_txAirtime => 'Ефірний час TX'; + String get repeater_txAirtime => 'Ефірний час TX'; @override - String get repeater_rxAirtime => 'Ефірний час RX'; + String get repeater_rxAirtime => 'Ефірний час RX'; @override - String get repeater_packetStatistics => 'Статистика пакетів'; + String get repeater_packetStatistics => 'Статистика пакетів'; @override - String get repeater_sent => 'Надіслано'; + String get repeater_sent => 'Надіслано'; @override - String get repeater_received => 'Отримано'; + String get repeater_received => 'Отримано'; @override - String get repeater_duplicates => 'Дублікати'; + String get repeater_duplicates => 'Дублікати'; @override String repeater_daysHoursMinsSecs( @@ -1877,674 +1966,712 @@ class AppLocalizationsUk extends AppLocalizations { int minutes, int seconds, ) { - return '$days дн. $hours год $minutes хв $seconds с'; + return '$days дн. $hours год $minutes хв $seconds с'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'На всю мережу: $flood, Прямі: $direct'; + return 'На всю мережу: $flood, Прямі: $direct'; } @override String repeater_duplicatesTotal(int total) { - return 'Всього: $total'; + return 'Всього: $total'; } @override - String get repeater_settingsTitle => 'Налаштування ретранслятора'; + String get repeater_settingsTitle => + 'Налаштування ретранслятора'; @override - String get repeater_basicSettings => 'Основні налаштування'; + String get repeater_basicSettings => + 'Основні налаштування'; @override - String get repeater_repeaterName => 'Ім\'я ретранслятора'; + String get repeater_repeaterName => 'Ім\'я ретранслятора'; @override String get repeater_repeaterNameHelper => - 'Показати ім\'я цього ретранслятора'; + 'Показати ім\'я цього ретранслятора'; @override - String get repeater_adminPassword => 'Пароль адміністратора'; + String get repeater_adminPassword => + 'Пароль адміністратора'; @override - String get repeater_adminPasswordHelper => 'Пароль повного доступу'; + String get repeater_adminPasswordHelper => + 'Пароль повного доступу'; @override - String get repeater_guestPassword => 'Гостьовий пароль'; + String get repeater_guestPassword => 'Гостьовий пароль'; @override String get repeater_guestPasswordHelper => - 'Доступ лише для читання з паролем'; + 'Доступ лише для читання з паролем'; @override - String get repeater_radioSettings => 'Налаштування радіо'; + String get repeater_radioSettings => 'Налаштування радіо'; @override - String get repeater_frequencyMhz => 'Частота (МГц)'; + String get repeater_frequencyMhz => 'Частота (МГц)'; @override - String get repeater_frequencyHelper => '300-2500 МГц'; + String get repeater_frequencyHelper => '300-2500 МГц'; @override - String get repeater_txPower => 'Потужність TX'; + String get repeater_txPower => 'Потужність TX'; @override - String get repeater_txPowerHelper => '1-30 дБм'; + String get repeater_txPowerHelper => '1-30 дБм'; @override - String get repeater_bandwidth => 'Смуга пропускання'; + String get repeater_bandwidth => 'Смуга пропускання'; @override - String get repeater_spreadingFactor => 'Коефіцієнт розширення'; + String get repeater_spreadingFactor => + 'Коефіцієнт розширення'; @override - String get repeater_codingRate => 'Швидкість кодування'; + String get repeater_codingRate => 'Швидкість кодування'; @override - String get repeater_locationSettings => 'Налаштування розташування'; + String get repeater_locationSettings => + 'Налаштування розташування'; @override - String get repeater_latitude => 'Широта'; + String get repeater_latitude => 'Широта'; @override String get repeater_latitudeHelper => - 'Десяткові градуси (наприклад, 37.7749)'; + 'Десяткові градуси (наприклад, 37.7749)'; @override - String get repeater_longitude => 'Довгота'; + String get repeater_longitude => 'Довгота'; @override String get repeater_longitudeHelper => - 'Десяткові градуси (наприклад, -122.4194)'; + 'Десяткові градуси (наприклад, -122.4194)'; @override - String get repeater_features => 'Функції'; + String get repeater_features => 'Функції'; @override - String get repeater_packetForwarding => 'Пересилання пакетів'; + String get repeater_packetForwarding => + 'Пересилання пакетів'; @override String get repeater_packetForwardingSubtitle => - 'Дозволити ретранслятору пересилати пакети'; + 'Дозволити ретранслятору пересилати пакети'; @override - String get repeater_guestAccess => 'Гостьовий доступ'; + String get repeater_guestAccess => 'Гостьовий доступ'; @override String get repeater_guestAccessSubtitle => - 'Дозволити гостьовий доступ лише для читання'; + 'Дозволити гостьовий доступ лише для читання'; @override - String get repeater_privacyMode => 'Режим приватності'; + String get repeater_privacyMode => 'Режим приватності'; @override String get repeater_privacyModeSubtitle => - 'Приховати ім\'я/розташування в оголошеннях'; + 'Приховати ім\'я/розташування в оголошеннях'; @override - String get repeater_advertisementSettings => 'Налаштування оголошень'; + String get repeater_advertisementSettings => + 'Налаштування оголошень'; @override String get repeater_localAdvertInterval => - 'Інтервал локальних оголошень (0 стрибків)'; + 'Інтервал локальних оголошень (0 стрибків)'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes хвилин'; + return '$minutes хвилин'; } @override String get repeater_floodAdvertInterval => - 'Інтервал оголошень на всю мережу (flood)'; + 'Інтервал оголошень на всю мережу (flood)'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours годин'; + return '$hours годин'; } @override String get repeater_encryptedAdvertInterval => - 'Інтервал зашифрованих оголошень'; + 'Інтервал зашифрованих оголошень'; @override - String get repeater_dangerZone => 'Небезпечна зона'; + String get repeater_dangerZone => 'Небезпечна зона'; @override - String get repeater_rebootRepeater => 'Перезавантажити ретранслятор'; + String get repeater_rebootRepeater => + 'Перезавантажити ретранслятор'; @override String get repeater_rebootRepeaterSubtitle => - 'Скинути пристрій ретранслятора'; + 'Скинути пристрій ретранслятора'; @override String get repeater_rebootRepeaterConfirm => - 'Ви впевнені, що хочете перезавантажити цей ретранслятор?'; + 'Ви впевнені, що хочете перезавантажити цей ретранслятор?'; @override String get repeater_regenerateIdentityKey => - 'Перегенерувати ключ ідентичності'; + 'Перегенерувати ключ ідентичності'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Згенерувати нову пару ключів (публічний/приватний)'; + 'Згенерувати нову пару ключів (публічний/приватний)'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Це створить нову ідентичність для ретранслятора. Продовжити?'; + 'Це створить нову ідентичність для ретранслятора. Продовжити?'; @override - String get repeater_eraseFileSystem => 'Очистити файлову систему'; + String get repeater_eraseFileSystem => + 'Очистити файлову систему'; @override String get repeater_eraseFileSystemSubtitle => - 'Відформатувати файлову систему ретранслятора'; + 'Відформатувати файлову систему ретранслятора'; @override String get repeater_eraseFileSystemConfirm => - 'УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!'; + 'УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!'; @override String get repeater_eraseSerialOnly => - 'Очищення доступне лише через послідовну консоль.'; + 'Очищення доступне лише через послідовну консоль.'; @override String repeater_commandSent(String command) { - return 'Команда надіслана: $command'; + return 'Команда надіслана: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Помилка надсилання команди: $error'; + return 'Помилка надсилання команди: $error'; } @override - String get repeater_confirm => 'Підтвердити'; + String get repeater_confirm => 'Підтвердити'; @override - String get repeater_settingsSaved => 'Налаштування успішно збережено.'; + String get repeater_settingsSaved => + 'Налаштування успішно збережено.'; @override String repeater_errorSavingSettings(String error) { - return 'Помилка збереження налаштувань: $error'; + return 'Помилка збереження налаштувань: $error'; } @override - String get repeater_refreshBasicSettings => 'Оновити основні налаштування'; + String get repeater_refreshBasicSettings => + 'Оновити основні налаштування'; @override - String get repeater_refreshRadioSettings => 'Оновити налаштування радіо'; + String get repeater_refreshRadioSettings => + 'Оновити налаштування радіо'; @override - String get repeater_refreshTxPower => 'Оновити потужність TX'; + String get repeater_refreshTxPower => + 'Оновити потужність TX'; @override String get repeater_refreshLocationSettings => - 'Оновити налаштування розташування'; + 'Оновити налаштування розташування'; @override - String get repeater_refreshPacketForwarding => 'Оновити пересилання пакетів'; + String get repeater_refreshPacketForwarding => + 'Оновити пересилання пакетів'; @override - String get repeater_refreshGuestAccess => 'Оновити гостьовий доступ'; + String get repeater_refreshGuestAccess => + 'Оновити гостьовий доступ'; @override - String get repeater_refreshPrivacyMode => 'Оновити режим приватності'; + String get repeater_refreshPrivacyMode => + 'Оновити режим приватності'; @override String get repeater_refreshAdvertisementSettings => - 'Оновити налаштування оголошень'; + 'Оновити налаштування оголошень'; @override String repeater_refreshed(String label) { - return '$label оновлено'; + return '$label оновлено'; } @override String repeater_errorRefreshing(String label) { - return 'Помилка оновлення $label'; + return 'Помилка оновлення $label'; } @override - String get repeater_cliTitle => 'Ретранслятор CLI'; + String get repeater_cliTitle => 'Ретранслятор CLI'; @override - String get repeater_debugNextCommand => 'Налагодити наступну команду'; + String get repeater_debugNextCommand => + 'Налагодити наступну команду'; @override - String get repeater_commandHelp => 'Довідка'; + String get repeater_commandHelp => 'Довідка'; @override - String get repeater_clearHistory => 'Очистити історію'; + String get repeater_clearHistory => 'Очистити історію'; @override - String get repeater_noCommandsSent => 'Команди ще не надсилалися.'; + String get repeater_noCommandsSent => + 'Команди ще не надсилалися.'; @override String get repeater_typeCommandOrUseQuick => - 'Введіть команду нижче або використовуйте швидкі команди'; + 'Введіть команду нижче або використовуйте швидкі команди'; @override - String get repeater_enterCommandHint => 'Введіть команду...'; + String get repeater_enterCommandHint => 'Введіть команду...'; @override - String get repeater_previousCommand => 'Попередня команда'; + String get repeater_previousCommand => 'Попередня команда'; @override - String get repeater_nextCommand => 'Наступна команда'; + String get repeater_nextCommand => 'Наступна команда'; @override - String get repeater_enterCommandFirst => 'Спершу введіть команду'; + String get repeater_enterCommandFirst => + 'Спершу введіть команду'; @override - String get repeater_cliCommandFrameTitle => 'Фрейм команди CLI'; + String get repeater_cliCommandFrameTitle => 'Фрейм команди CLI'; @override String repeater_cliCommandError(String error) { - return 'Помилка: $error'; + return 'Помилка: $error'; } @override - String get repeater_cliQuickGetName => 'Отримати ім\'я'; + String get repeater_cliQuickGetName => 'Отримати ім\'я'; @override - String get repeater_cliQuickGetRadio => 'Отримати Радіо'; + String get repeater_cliQuickGetRadio => 'Отримати Радіо'; @override - String get repeater_cliQuickGetTx => 'Отримати TX'; + String get repeater_cliQuickGetTx => 'Отримати TX'; @override - String get repeater_cliQuickNeighbors => 'Сусіди'; + String get repeater_cliQuickNeighbors => 'Сусіди'; @override - String get repeater_cliQuickVersion => 'Версія'; + String get repeater_cliQuickVersion => 'Версія'; @override - String get repeater_cliQuickAdvertise => 'Оголосити'; + String get repeater_cliQuickAdvertise => 'Оголосити'; @override - String get repeater_cliQuickClock => 'Годинник'; + String get repeater_cliQuickClock => 'Годинник'; @override - String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; + String get repeater_cliHelpAdvert => + 'Надсилає пакет оголошення'; @override String get repeater_cliHelpReboot => - 'Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що є нормальним)'; + 'Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що Ñ” нормальним)'; @override String get repeater_cliHelpClock => - 'Відображає поточний час за годинником кожного пристрою.'; + 'Відображає поточний час за годинником кожного пристрою.'; @override String get repeater_cliHelpPassword => - 'Встановлює новий пароль адміністратора для пристрою.'; + 'Встановлює новий пароль адміністратора для пристрою.'; @override String get repeater_cliHelpVersion => - 'Відображає версію пристрою та дату збірки прошивки.'; + 'Відображає версію пристрою та дату збірки прошивки.'; @override String get repeater_cliHelpClearStats => - 'Скидає різні лічильники статистики до нуля.'; + 'Скидає різні лічильники статистики до нуля.'; @override - String get repeater_cliHelpSetAf => 'Встановлює коефіцієнт ефірного часу.'; + String get repeater_cliHelpSetAf => + 'Встановлює коефіцієнт ефірного часу.'; @override String get repeater_cliHelpSetTx => - 'Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).'; + 'Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).'; @override String get repeater_cliHelpSetRepeat => - 'Вмикає або вимикає роль ретранслятора для цього вузла.'; + 'Вмикає або вимикає роль ретранслятора для цього вузла.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)'; + '(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)'; @override String get repeater_cliHelpSetFloodMax => - 'Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).'; + 'Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).'; @override String get repeater_cliHelpSetIntThresh => - 'Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.'; + 'Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.'; + 'Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetMultiAcks => - 'Вмикає або вимикає функціональність подвійних ACK.'; + 'Вмикає або вимикає функціональність подвійних ACK.'; @override String get repeater_cliHelpSetAdvertInterval => - 'Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.'; + 'Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.'; + 'Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetGuestPassword => - 'Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)'; + 'Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)'; @override - String get repeater_cliHelpSetName => 'Встановлює ім\'я для оголошення.'; + String get repeater_cliHelpSetName => + 'Встановлює ім\'я для оголошення.'; @override String get repeater_cliHelpSetLat => - 'Встановлює широту для карти оголошень. (десяткові градуси)'; + 'Встановлює широту для карти оголошень. (десяткові градуси)'; @override String get repeater_cliHelpSetLon => - 'Встановлює довготу для карти оголошень. (десяткові градуси)'; + 'Встановлює довготу для карти оголошень. (десяткові градуси)'; @override String get repeater_cliHelpSetRadio => - 'Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.'; + 'Повністю встановлює нові параметри радіо та зберігає Ñ—Ñ… у налаштуваннях. Потребує команди «перезавантаження» для застосування.'; @override String get repeater_cliHelpSetRxDelay => - 'Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.'; + 'Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetTxDelay => - 'Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).'; + 'Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.'; + 'Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Увімкнути/Вимкнути міст.'; + String get repeater_cliHelpSetBridgeEnabled => + 'Увімкнути/Вимкнути міст.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Встановити затримку перед пересиланням пакетів.'; + 'Встановити затримку перед пересиланням пакетів.'; @override String get repeater_cliHelpSetBridgeSource => - 'Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.'; + 'Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Встановити швидкість послідовного зв\'язку для мостів Rs232.'; + 'Встановити швидкість послідовного зв\'язку для мостів Rs232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Встановити секрет мосту для мостів espnow.'; + 'Встановити секрет мосту для мостів espnow.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).'; + 'Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).'; @override String get repeater_cliHelpTempRadio => - 'Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).'; + 'Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).'; @override String get repeater_cliHelpSetPerm => - 'Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний і його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).'; + 'Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний Ñ– його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).'; @override String get repeater_cliHelpGetBridgeType => - 'Отримати тип мосту: немає, rs232, espnow'; + 'Отримати тип мосту: немає, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Починає запис пакетів у файлову систему.'; + 'Починає запис пакетів у файлову систему.'; @override String get repeater_cliHelpLogStop => - 'Зупиняє запис пакетів у файлову систему.'; + 'Зупиняє запис пакетів у файлову систему.'; @override String get repeater_cliHelpLogErase => - 'Видаляє журнали пакетів з файлової системи.'; + 'Видаляє журнали пакетів з файлової системи.'; @override String get repeater_cliHelpNeighbors => - 'Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4'; + 'Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4'; @override String get repeater_cliHelpNeighborRemove => - 'Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.'; + 'Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.'; @override String get repeater_cliHelpRegion => - '(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).'; + '(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).'; @override String get repeater_cliHelpRegionLoad => - 'ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.'; + 'ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.'; @override String get repeater_cliHelpRegionGet => - 'Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім\'я-регіону (ім\'я-батька) \'F\'»'; + 'Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім\'я-регіону (ім\'я-батька) \'F\'»'; @override String get repeater_cliHelpRegionPut => - 'Додає або оновлює визначення регіону з заданою назвою.'; + 'Додає або оновлює визначення регіону з заданою назвою.'; @override String get repeater_cliHelpRegionRemove => - 'Видаляє визначення регіону з заданою назвою.'; + 'Видаляє визначення регіону з заданою назвою.'; @override String get repeater_cliHelpRegionAllowf => - 'Встановлює дозвіл «Flood» для заданого регіону. (\'\' для глобальної/успадкованої області)'; + 'Встановлює дозвіл «Flood» для заданого регіону. (\'\' для глобальної/успадкованої області)'; @override String get repeater_cliHelpRegionDenyf => - 'Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )'; + 'Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )'; @override String get repeater_cliHelpRegionHome => - 'Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)'; + 'Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)'; @override - String get repeater_cliHelpRegionHomeSet => 'Встановлює «домашній» регіон.'; + String get repeater_cliHelpRegionHomeSet => + 'Встановлює «домашній» регіон.'; @override String get repeater_cliHelpRegionSave => - 'Зберігає список/карту регіонів у сховищі.'; + 'Зберігає список/карту регіонів у сховищі.'; @override String get repeater_cliHelpGps => - 'Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.'; + 'Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.'; @override - String get repeater_cliHelpGpsOnOff => 'Увімкнути/вимкнути GPS.'; + String get repeater_cliHelpGpsOnOff => + 'Увімкнути/вимкнути GPS.'; @override String get repeater_cliHelpGpsSync => - 'Синхронізує час вузла з годинником GPS.'; + 'Синхронізує час вузла з годинником GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.'; + 'Встановлює позицію вузла за координатами GPS Ñ– зберігає в налаштуваннях.'; @override String get repeater_cliHelpGpsAdvert => - 'Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях'; + 'Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях'; @override String get repeater_cliHelpGpsAdvertSet => - 'Встановлює конфігурацію оголошення розташування.'; + 'Встановлює конфігурацію оголошення розташування.'; @override - String get repeater_commandsListTitle => 'Список команд'; + String get repeater_commandsListTitle => 'Список команд'; @override String get repeater_commandsListNote => - 'ПРИМІТКА: для різних команд «set»... також існує команда «get»...'; + 'ПРИМІТКА: для різних команд «set»... також існує команда «get»...'; @override - String get repeater_general => 'Загальні'; + String get repeater_general => 'Загальні'; @override - String get repeater_settingsCategory => 'Налаштування'; + String get repeater_settingsCategory => 'Налаштування'; @override - String get repeater_bridge => 'Міст'; + String get repeater_bridge => 'Міст'; @override - String get repeater_logging => 'Логування'; + String get repeater_logging => 'Логування'; @override - String get repeater_neighborsRepeaterOnly => 'Сусіди (Тільки ретранслятор)'; + String get repeater_neighborsRepeaterOnly => + 'Сусіди (Тільки ретранслятор)'; @override String get repeater_regionManagementRepeaterOnly => - 'Керування регіонами (Тільки ретранслятор)'; + 'Керування регіонами (Тільки ретранслятор)'; @override String get repeater_regionNote => - 'Команди регіонів були введені для керування визначеннями та дозволами регіонів.'; + 'Команди регіонів були введені для керування визначеннями та дозволами регіонів.'; @override - String get repeater_gpsManagement => 'Керування GPS'; + String get repeater_gpsManagement => 'Керування GPS'; @override String get repeater_gpsNote => - 'Команда GPS була введена для керування питаннями, пов\'язаними з локацією.'; + 'Команда GPS була введена для керування питаннями, пов\'язаними з локацією.'; @override - String get telemetry_receivedData => 'Дані телеметрії отримано'; + String get telemetry_receivedData => + 'Дані телеметрії отримано'; @override - String get telemetry_requestTimeout => 'Час запиту телеметрії вичерпано.'; + String get telemetry_requestTimeout => + 'Час запиту телеметрії вичерпано.'; @override String telemetry_errorLoading(String error) { - return 'Помилка завантаження телеметрії: $error'; + return 'Помилка завантаження телеметрії: $error'; } @override - String get telemetry_noData => 'Дані телеметрії недоступні.'; + String get telemetry_noData => + 'Дані телеметрії недоступні.'; @override String telemetry_channelTitle(int channel) { - return 'Канал $channel'; + return 'Канал $channel'; } @override - String get telemetry_batteryLabel => 'Батарея'; + String get telemetry_batteryLabel => 'Батарея'; @override - String get telemetry_voltageLabel => 'Напруга'; + String get telemetry_voltageLabel => 'Напруга'; @override - String get telemetry_mcuTemperatureLabel => 'Температура MCU'; + String get telemetry_mcuTemperatureLabel => 'Температура MCU'; @override - String get telemetry_temperatureLabel => 'Температура'; + String get telemetry_temperatureLabel => 'Температура'; @override - String get telemetry_currentLabel => 'Поточний струм'; + String get telemetry_currentLabel => 'Поточний струм'; @override String telemetry_batteryValue(int percent, String volts) { - return '$percent% / $voltsВ'; + return '$percent% / $voltsÐ’'; } @override String telemetry_voltageValue(String volts) { - return '$voltsВ'; + return '$voltsÐ’'; } @override String telemetry_currentValue(String amps) { - return '$ampsА'; + return '$ampsА'; } @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Дані сусідів отримано'; + String get neighbors_receivedData => + 'Дані сусідів отримано'; @override - String get neighbors_requestTimedOut => 'Час запиту сусідів вичерпано.'; + String get neighbors_requestTimedOut => + 'Час запиту сусідів вичерпано.'; @override String neighbors_errorLoading(String error) { - return 'Помилка завантаження сусідів: $error'; + return 'Помилка завантаження сусідів: $error'; } @override - String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди'; + String get neighbors_repeatersNeighbors => + 'Ретранслятори-сусіди'; @override - String get neighbors_noData => 'Дані про сусідів недоступні.'; + String get neighbors_noData => + 'Дані про сусідів недоступні.'; @override String neighbors_unknownContact(String pubkey) { - return 'Невідомий відкритий ключ $pubkey'; + return 'Невідомий відкритий ключ $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Почуто: $time тому'; + return 'Почуто: $time тому'; } @override - String get channelPath_title => 'Шлях пакету'; + String get channelPath_title => 'Шлях пакету'; @override - String get channelPath_viewMap => 'Показати карту'; + String get channelPath_viewMap => 'Показати карту'; @override - String get channelPath_otherObservedPaths => 'Інші спостережувані шляхи'; + String get channelPath_otherObservedPaths => + 'Інші спостережувані шляхи'; @override - String get channelPath_repeaterHops => 'Стрибки ретранслятора'; + String get channelPath_repeaterHops => + 'Стрибки ретранслятора'; @override String get channelPath_noHopDetails => - 'Деталі відправки не надані для цього пакету.'; + 'Деталі відправки не надані для цього пакету.'; @override - String get channelPath_messageDetails => 'Деталі повідомлення'; + String get channelPath_messageDetails => + 'Деталі повідомлення'; @override - String get channelPath_senderLabel => 'Відправник'; + String get channelPath_senderLabel => 'Відправник'; @override - String get channelPath_timeLabel => 'Час'; + String get channelPath_timeLabel => 'Час'; @override - String get channelPath_repeatsLabel => 'Повторення'; + String get channelPath_repeatsLabel => 'Повторення'; @override String channelPath_pathLabel(int index) { - return 'Шлях $index'; + return 'Шлях $index'; } @override - String get channelPath_observedLabel => 'Спостережено'; + String get channelPath_observedLabel => 'Спостережено'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Спостережуваний шлях $index • $hops'; + return 'Спостережуваний шлях $index • $hops'; } @override - String get channelPath_noLocationData => 'Немає даних про розташування'; + String get channelPath_noLocationData => + 'Немає даних про розташування'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2557,153 +2684,161 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Невідомий'; + String get channelPath_unknownPath => 'Невідомий'; @override - String get channelPath_floodPath => 'На всю мережу'; + String get channelPath_floodPath => 'На всю мережу'; @override - String get channelPath_directPath => 'Прямий'; + String get channelPath_directPath => 'Прямий'; @override String channelPath_observedZeroOf(int total) { - return '0 з $total стрибків'; + return '0 з $total стрибків'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed з $total стрибків'; + return '$observed з $total стрибків'; } @override - String get channelPath_mapTitle => 'Карта шляху'; + String get channelPath_mapTitle => 'Карта шляху'; @override String get channelPath_noRepeaterLocations => - 'Позиції ретрансляторів недоступні для цього шляху.'; + 'Позиції ретрансляторів недоступні для цього шляху.'; @override String channelPath_primaryPath(int index) { - return 'Шлях $index (Основний)'; + return 'Шлях $index (Основний)'; } @override - String get channelPath_pathLabelTitle => 'Шлях'; + String get channelPath_pathLabelTitle => 'Шлях'; @override - String get channelPath_observedPathHeader => 'Спостережуваний шлях'; + String get channelPath_observedPathHeader => + 'Спостережуваний шлях'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Деталі стрибків недоступні для цього пакету.'; + 'Деталі стрибків недоступні для цього пакету.'; @override - String get channelPath_unknownRepeater => 'Невідомий ретранслятор'; + String get channelPath_unknownRepeater => + 'Невідомий ретранслятор'; @override - String get community_title => 'Спільнота'; + String get community_title => 'Спільнота'; @override - String get community_create => 'Створити спільноту'; + String get community_create => 'Створити спільноту'; @override String get community_createDesc => - 'Створити нову спільноту та поділитися через QR-код.'; + 'Створити нову спільноту та поділитися через QR-код.'; @override - String get community_join => 'Приєднатися'; + String get community_join => 'Приєднатися'; @override - String get community_joinTitle => 'Приєднатися до спільноти'; + String get community_joinTitle => + 'Приєднатися до спільноти'; @override String community_joinConfirmation(String name) { - return 'Ви бажаєте приєднатися до спільноти «$name»?'; + return 'Ви бажаєте приєднатися до спільноти «$name»?'; } @override - String get community_scanQr => 'Сканувати QR спільноти'; + String get community_scanQr => 'Сканувати QR спільноти'; @override String get community_scanInstructions => - 'Наведіть камеру на QR-код спільноти.'; + 'Наведіть камеру на QR-код спільноти.'; @override - String get community_showQr => 'Показати QR-код'; + String get community_showQr => 'Показати QR-код'; @override - String get community_publicChannel => 'Публічна спільнота'; + String get community_publicChannel => 'Публічна спільнота'; @override - String get community_hashtagChannel => 'Хештег спільноти'; + String get community_hashtagChannel => 'Хештег спільноти'; @override - String get community_name => 'Назва спільноти'; + String get community_name => 'Назва спільноти'; @override - String get community_enterName => 'Введіть назву спільноти'; + String get community_enterName => + 'Введіть назву спільноти'; @override String community_created(String name) { - return 'Спільноту «$name» створено'; + return 'Спільноту «$name» створено'; } @override String community_joined(String name) { - return 'Приєднався до спільноти «$name»'; + return 'Приєднався до спільноти «$name»'; } @override - String get community_qrTitle => 'Поділитися спільнотою'; + String get community_qrTitle => 'Поділитися спільнотою'; @override String community_qrInstructions(String name) { - return 'Відскануйте цей QR-код, щоб приєднатися до $name'; + return 'Відскануйте цей QR-код, щоб приєднатися до $name'; } @override String get community_hashtagPrivacyHint => - 'Канали хештегів спільноти доступні лише членам спільноти'; + 'Канали хештегів спільноти доступні лише членам спільноти'; @override - String get community_invalidQrCode => 'Недійсний QR-код спільноти'; + String get community_invalidQrCode => + 'Недійсний QR-код спільноти'; @override - String get community_alreadyMember => 'Вже учасник'; + String get community_alreadyMember => 'Вже учасник'; @override String community_alreadyMemberMessage(String name) { - return 'Ви вже є учасником «$name».'; + return 'Ви вже Ñ” учасником «$name».'; } @override - String get community_addPublicChannel => 'Додати публічний канал спільноти'; + String get community_addPublicChannel => + 'Додати публічний канал спільноти'; @override String get community_addPublicChannelHint => - 'Автоматично додати публічний канал для цієї спільноти'; + 'Автоматично додати публічний канал для цієї спільноти'; @override - String get community_noCommunities => 'Поки не приєднано до жодної групи.'; + String get community_noCommunities => + 'Поки не приєднано до жодної групи.'; @override String get community_scanOrCreate => - 'Відскануйте QR-код або створіть спільноту, щоб почати'; + 'Відскануйте QR-код або створіть спільноту, щоб почати'; @override - String get community_manageCommunities => 'Керувати спільнотами'; + String get community_manageCommunities => + 'Керувати спільнотами'; @override - String get community_delete => 'Покинути спільноту'; + String get community_delete => 'Покинути спільноту'; @override String community_deleteConfirm(String name) { - return 'Покинути «$name»?'; + return 'Покинути «$name»?'; } @override @@ -2711,196 +2846,205 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'каналів', - many: 'каналів', - few: 'канали', - one: 'канал', + other: 'каналів', + many: 'каналів', + few: 'канали', + one: 'канал', ); - return 'Це також видалить $count $_temp0 та їх повідомлення.'; + return 'Це також видалить $count $_temp0 та Ñ—Ñ… повідомлення.'; } @override String community_deleted(String name) { - return 'Спільноту «$name» покинуто'; + return 'Спільноту «$name» покинуто'; } @override - String get community_regenerateSecret => 'Перегенерувати секрет'; + String get community_regenerateSecret => + 'Перегенерувати секрет'; @override String community_regenerateSecretConfirm(String name) { - return 'Перегенерувати секретний ключ для «$name»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; + return 'Перегенерувати секретний ключ для «$name»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; } @override - String get community_regenerate => 'Перегенерувати'; + String get community_regenerate => 'Перегенерувати'; @override String community_secretRegenerated(String name) { - return 'Секретний пароль для «$name» перегенеровано'; + return 'Секретний пароль для «$name» перегенеровано'; } @override - String get community_updateSecret => 'Оновити секрет'; + String get community_updateSecret => 'Оновити секрет'; @override String community_secretUpdated(String name) { - return 'Зміну секрету для «$name» оновлено'; + return 'Зміну секрету для «$name» оновлено'; } @override String community_scanToUpdateSecret(String name) { - return 'Відскануйте новий QR-код, щоб оновити пароль для «$name»'; + return 'Відскануйте новий QR-код, щоб оновити пароль для «$name»'; } @override - String get community_addHashtagChannel => 'Додати хештег спільноти'; + String get community_addHashtagChannel => + 'Додати хештег спільноти'; @override String get community_addHashtagChannelDesc => - 'Додати канал хештегу для цієї спільноти'; + 'Додати канал хештегу для цієї спільноти'; @override - String get community_selectCommunity => 'Вибрати спільноту'; + String get community_selectCommunity => 'Вибрати спільноту'; @override - String get community_regularHashtag => 'Звичайний хештег'; + String get community_regularHashtag => 'Звичайний хештег'; @override String get community_regularHashtagDesc => - 'Публічний хештег (будь-хто може приєднатися)'; + 'Публічний хештег (будь-хто може приєднатися)'; @override - String get community_communityHashtag => 'Хештег спільноти'; + String get community_communityHashtag => 'Хештег спільноти'; @override String get community_communityHashtagDesc => - 'Ексклюзивно для членів спільноти'; + 'Ексклюзивно для членів спільноти'; @override String community_forCommunity(String name) { - return 'Для $name'; + return 'Для $name'; } @override - String get listFilter_tooltip => 'Фільтр та сортування'; + String get listFilter_tooltip => 'Фільтр та сортування'; @override - String get listFilter_sortBy => 'Сортувати за'; + String get listFilter_sortBy => 'Сортувати за'; @override - String get listFilter_latestMessages => 'Останні повідомлення'; + String get listFilter_latestMessages => + 'Останні повідомлення'; @override - String get listFilter_heardRecently => 'Нещодавно чули'; + String get listFilter_heardRecently => 'Нещодавно чули'; @override - String get listFilter_az => 'А-Я'; + String get listFilter_az => 'А-Я'; @override - String get listFilter_filters => 'Фільтри'; + String get listFilter_filters => 'Фільтри'; @override - String get listFilter_all => 'Все'; + String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Улюблені'; + String get listFilter_favorites => 'Улюблені'; @override - String get listFilter_addToFavorites => 'Додати до улюблених'; + String get listFilter_addToFavorites => + 'Додати до улюблених'; @override - String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених'; + String get listFilter_removeFromFavorites => + 'Видалити зі списку улюблених'; @override - String get listFilter_users => 'Користувачі'; + String get listFilter_users => 'Користувачі'; @override - String get listFilter_repeaters => 'Ретранслятори'; + String get listFilter_repeaters => 'Ретранслятори'; @override - String get listFilter_roomServers => 'Сервери кімнат'; + String get listFilter_roomServers => 'Сервери кімнат'; @override - String get listFilter_unreadOnly => 'Тільки непрочитані повідомлення'; + String get listFilter_unreadOnly => + 'Тільки непрочитані повідомлення'; @override - String get listFilter_newGroup => 'Нова група'; + String get listFilter_newGroup => 'Нова група'; @override - String get pathTrace_you => 'Ви'; + String get pathTrace_you => 'Ви'; @override - String get pathTrace_failed => 'Відстеження шляху не вдалося.'; + String get pathTrace_failed => + 'Відстеження шляху не вдалося.'; @override - String get pathTrace_notAvailable => 'Трасування шляху недоступне.'; + String get pathTrace_notAvailable => + 'Трасування шляху недоступне.'; @override - String get pathTrace_refreshTooltip => 'Оновити Path Trace'; + String get pathTrace_refreshTooltip => 'Оновити Path Trace'; @override String get pathTrace_someHopsNoLocation => - 'Одне або більше хмелів відсутнє місце розташування!'; + 'Одне або більше хмелів відсутнє місце розташування!'; @override - String get pathTrace_clearTooltip => 'Очистити шлях'; + String get pathTrace_clearTooltip => 'Очистити шлях'; @override String get losSelectStartEnd => - 'Виберіть початковий і кінцевий вузли для LOS.'; + 'Виберіть початковий Ñ– кінцевий вузли для LOS.'; @override String losRunFailed(String error) { - return 'Помилка перевірки прямої видимості: $error'; + return 'Помилка перевірки прямої видимості: $error'; } @override - String get losClearAllPoints => 'Очистити всі пункти'; + String get losClearAllPoints => 'Очистити всі пункти'; @override String get losRunToViewElevationProfile => - 'Запустіть LOS, щоб переглянути профіль висоти'; + 'Запустіть LOS, щоб переглянути профіль висоти'; @override - String get losMenuTitle => 'Меню LOS'; + String get losMenuTitle => 'Меню LOS'; @override String get losMenuSubtitle => - 'Торкніться вузлів або утримуйте карту, щоб отримати власні точки'; + 'Торкніться вузлів або утримуйте карту, щоб отримати власні точки'; @override - String get losShowDisplayNodes => 'Показати вузли відображення'; + String get losShowDisplayNodes => + 'Показати вузли відображення'; @override - String get losCustomPoints => 'Користувальницькі точки'; + String get losCustomPoints => 'Користувальницькі точки'; @override String losCustomPointLabel(int index) { - return 'Спеціальний $index'; + return 'Спеціальний $index'; } @override - String get losPointA => 'Точка А'; + String get losPointA => 'Точка А'; @override - String get losPointB => 'Точка Б'; + String get losPointB => 'Точка Б'; @override String losAntennaA(String value, String unit) { - return 'Антена A: $value $unit'; + return 'Антена A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Антена B: $value $unit'; + return 'Антена B: $value $unit'; } @override - String get losRun => 'Запустіть LOS'; + String get losRun => 'Запустіть LOS'; @override - String get losNoElevationData => 'Немає даних про висоту'; + String get losNoElevationData => 'Немає даних про висоту'; @override String losProfileClear( @@ -2909,7 +3053,7 @@ class AppLocalizationsUk extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit'; + return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit'; } @override @@ -2919,61 +3063,64 @@ class AppLocalizationsUk extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, заблоковано $obstruction $heightUnit'; + return '$distance $distanceUnit, заблоковано $obstruction $heightUnit'; } @override - String get losStatusChecking => 'LOS: перевірка...'; + String get losStatusChecking => 'LOS: перевірка...'; @override - String get losStatusNoData => 'LOS: немає даних'; + String get losStatusNoData => 'LOS: немає даних'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо'; + return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо'; } @override String get losErrorElevationUnavailable => - 'Дані про висоту недоступні для одного чи кількох зразків.'; + 'Дані про висоту недоступні для одного чи кількох зразків.'; @override String get losErrorInvalidInput => - 'Недійсні дані про точки/висоту для розрахунку LOS.'; + 'Недійсні дані про точки/висоту для розрахунку LOS.'; @override - String get losRenameCustomPoint => 'Перейменуйте спеціальну точку'; + String get losRenameCustomPoint => + 'Перейменуйте спеціальну точку'; @override - String get losPointName => 'Назва точки'; + String get losPointName => 'Назва точки'; @override - String get losShowPanelTooltip => 'Показати панель LOS'; + String get losShowPanelTooltip => 'Показати панель LOS'; @override - String get losHidePanelTooltip => 'Приховати панель LOS'; + String get losHidePanelTooltip => 'Приховати панель LOS'; @override String get losElevationAttribution => - 'Дані про висоту: Open-Meteo (CC BY 4.0)'; + 'Дані про висоту: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Радіогоризонт'; + String get losLegendRadioHorizon => 'Радіогоризонт'; @override - String get losLegendLosBeam => 'Лінія прямої видимості'; + String get losLegendLosBeam => 'Лінія прямої видимості'; @override - String get losLegendTerrain => 'Рельєф'; + String get losLegendTerrain => 'Рельєф'; @override - String get losFrequencyLabel => 'Частота'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку'; + String get losFrequencyInfoTooltip => + 'Переглянути деталі розрахунку'; @override - String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту'; + String get losFrequencyDialogTitle => + 'Розрахунок радіогоризонту'; @override String losFrequencyDialogDescription( @@ -2982,96 +3129,104 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; + return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; } @override - String get contacts_pathTrace => 'Трасування шляхів'; + String get contacts_pathTrace => 'Трасування шляхів'; @override - String get contacts_ping => 'Пінгувати'; + String get contacts_ping => 'Пінгувати'; @override - String get contacts_repeaterPathTrace => 'Трасування шляху до повторювача'; + String get contacts_repeaterPathTrace => + 'Трасування шляху до повторювача'; @override - String get contacts_repeaterPing => 'Пінгувати повторювач'; + String get contacts_repeaterPing => 'Пінгувати повторювач'; @override - String get contacts_roomPathTrace => 'Трасування шляху до серверу кімнати'; + String get contacts_roomPathTrace => + 'Трасування шляху до серверу кімнати'; @override - String get contacts_roomPing => 'Пінг сервера кімнати'; + String get contacts_roomPing => 'Пінг сервера кімнати'; @override - String get contacts_chatTraceRoute => 'Трасування шляху'; + String get contacts_chatTraceRoute => 'Трасування шляху'; @override String contacts_pathTraceTo(String name) { - return 'Відстежити маршрут до $name'; + return 'Відстежити маршрут до $name'; } @override - String get contacts_clipboardEmpty => 'Буфер обміну порожній'; + String get contacts_clipboardEmpty => + 'Буфер обміну порожній'; @override - String get contacts_invalidAdvertFormat => 'Недійсні контактні дані'; + String get contacts_invalidAdvertFormat => + 'Недійсні контактні дані'; @override - String get contacts_contactImported => 'Контакт було імпортовано.'; + String get contacts_contactImported => + 'Контакт було імпортовано.'; @override - String get contacts_contactImportFailed => 'Контакт не вдалося імпортувати'; + String get contacts_contactImportFailed => + 'Контакт не вдалося імпортувати'; @override - String get contacts_zeroHopAdvert => 'Реклама без перехоплення'; + String get contacts_zeroHopAdvert => + 'Реклама без перехоплення'; @override - String get contacts_floodAdvert => 'Залив реклами'; + String get contacts_floodAdvert => 'Залив реклами'; @override String get contacts_copyAdvertToClipboard => - 'Копіювати оголошення в буфер обміну'; + 'Копіювати оголошення в буфер обміну'; @override String get contacts_addContactFromClipboard => - 'Додати контакт з буфера обміну'; + 'Додати контакт з буфера обміну'; @override - String get contacts_ShareContact => 'Копіювати контакт у буфер обміну'; + String get contacts_ShareContact => + 'Копіювати контакт у буфер обміну'; @override String get contacts_ShareContactZeroHop => - 'Поділитися контактом за оголошенням'; + 'Поділитися контактом за оголошенням'; @override String get contacts_zeroHopContactAdvertSent => - 'Відправлено контакт за оголошенням'; + 'Відправлено контакт за оголошенням'; @override String get contacts_zeroHopContactAdvertFailed => - 'Не вдалося надіслати контакт.'; + 'Не вдалося надіслати контакт.'; @override String get contacts_contactAdvertCopied => - 'Рекламу скопійовано до буфера обміну.'; + 'Рекламу скопійовано до буфера обміну.'; @override String get contacts_contactAdvertCopyFailed => - 'Копіювання оголошення в буфер обміну завершилося невдало'; + 'Копіювання оголошення в буфер обміну завершилося невдало'; @override - String get notification_activityTitle => 'Активність MeshCore'; + String get notification_activityTitle => 'Активність MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'повідомлень', - many: 'повідомлень', - few: 'повідомлення', - one: 'повідомлення', + other: 'повідомлень', + many: 'повідомлень', + few: 'повідомлення', + one: 'повідомлення', ); return '$count $_temp0'; } @@ -3081,10 +3236,10 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'повідомлень каналу', - many: 'повідомлень каналу', - few: 'повідомлення каналу', - one: 'повідомлення каналу', + other: 'повідомлень каналу', + many: 'повідомлень каналу', + few: 'повідомлення каналу', + one: 'повідомлення каналу', ); return '$count $_temp0'; } @@ -3094,78 +3249,86 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'нових вузлів', - many: 'нових вузлів', - few: 'нових вузли', - one: 'новий вузол', + other: 'нових вузлів', + many: 'нових вузлів', + few: 'нових вузли', + one: 'новий вузол', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Виявлено новий $contactType'; + return 'Виявлено новий $contactType'; } @override - String get notification_receivedNewMessage => 'Отримано нове повідомлення'; + String get notification_receivedNewMessage => + 'Отримано нове повідомлення'; @override String get settings_gpxExportRepeaters => - 'Експортувати ретранслятори / сервер кімнати до GPX'; + 'Експортувати ретранслятори / сервер кімнати до GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.'; + 'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.'; @override - String get settings_gpxExportContacts => 'Експортувати супутників до GPX'; + String get settings_gpxExportContacts => + 'Експортувати супутників до GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Експортує супутників з місцезнаходженням у файл GPX.'; + 'Експортує супутників з місцезнаходженням у файл GPX.'; @override - String get settings_gpxExportAll => 'Експортувати всі контакти до GPX'; + String get settings_gpxExportAll => + 'Експортувати всі контакти до GPX'; @override String get settings_gpxExportAllSubtitle => - 'Експортує всі контакти з місцем розташування у файл GPX.'; + 'Експортує всі контакти з місцем розташування у файл GPX.'; @override - String get settings_gpxExportSuccess => 'Успішно експортовано файл GPX.'; + String get settings_gpxExportSuccess => + 'Успішно експортовано файл GPX.'; @override - String get settings_gpxExportNoContacts => 'Немає контактів для експорту.'; + String get settings_gpxExportNoContacts => + 'Немає контактів для експорту.'; @override String get settings_gpxExportNotAvailable => - 'Не підтримується на вашому пристрої/операційній системі'; + 'Не підтримується на вашому пристрої/операційній системі'; @override - String get settings_gpxExportError => 'Сталася помилка під час експорту.'; + String get settings_gpxExportError => + 'Сталася помилка під час експорту.'; @override String get settings_gpxExportRepeatersRoom => - 'Місцезнаходження повторювача та сервера кімнати'; + 'Місцезнаходження повторювача та сервера кімнати'; @override - String get settings_gpxExportChat => 'Місця супутників'; + String get settings_gpxExportChat => 'Місця супутників'; @override - String get settings_gpxExportAllContacts => 'Усі місця контактів'; + String get settings_gpxExportAllContacts => + 'Усі місця контактів'; @override String get settings_gpxExportShareText => - 'Дані карти експортовані з meshcore-open'; + 'Дані карти експортовані з meshcore-open'; @override String get settings_gpxExportShareSubject => - 'експорт даних карти meshcore-open у форматі GPX'; + 'експорт даних карти meshcore-open у форматі GPX'; @override - String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори'; + String get snrIndicator_nearByRepeaters => + 'Ближні ретранслятори'; @override - String get snrIndicator_lastSeen => 'Останній раз бачили'; + String get snrIndicator_lastSeen => 'Останній раз бачили'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 678c63b..ef3bafa 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -12,88 +12,88 @@ class AppLocalizationsZh extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => '联系人'; + String get nav_contacts => '联系人'; @override - String get nav_channels => '频道'; + String get nav_channels => '频道'; @override - String get nav_map => '地图'; + String get nav_map => '地图'; @override - String get common_cancel => '取消'; + String get common_cancel => '取消'; @override - String get common_ok => '确定'; + String get common_ok => '确定'; @override - String get common_connect => '连接'; + String get common_connect => '连接'; @override - String get common_unknownDevice => '未知设备'; + String get common_unknownDevice => '未知设备'; @override - String get common_save => '保存'; + String get common_save => '保存'; @override - String get common_delete => '删除'; + String get common_delete => '删除'; @override - String get common_close => '关闭'; + String get common_close => '关闭'; @override - String get common_edit => '编辑'; + String get common_edit => '编辑'; @override - String get common_add => '添加'; + String get common_add => '添加'; @override - String get common_settings => '设置'; + String get common_settings => '设置'; @override - String get common_disconnect => '断开'; + String get common_disconnect => 'æ–­å¼€'; @override - String get common_connected => '已连接'; + String get common_connected => '已连接'; @override - String get common_disconnected => '已断开'; + String get common_disconnected => '已断开'; @override - String get common_create => '创建'; + String get common_create => '创建'; @override - String get common_continue => '继续'; + String get common_continue => 'ç»§ç»­'; @override - String get common_share => '分享'; + String get common_share => '分享'; @override - String get common_copy => '复制'; + String get common_copy => '复制'; @override - String get common_retry => '重试'; + String get common_retry => '重试'; @override - String get common_hide => '隐藏'; + String get common_hide => '隐藏'; @override - String get common_remove => '移除'; + String get common_remove => '移除'; @override - String get common_enable => '启用'; + String get common_enable => '启用'; @override - String get common_disable => '禁用'; + String get common_disable => '禁用'; @override - String get common_reboot => '重启'; + String get common_reboot => '重启'; @override - String get common_loading => '正在加载...'; + String get common_loading => '正在加载...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -106,228 +106,233 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get scanner_title => '连接设备'; - - @override - String get connectionChoiceTitle => '选择您的连接方式'; - - @override - String get connectionChoiceSubtitle => '请选择您希望如何访问 MeshCore 设备的选项。'; + String get scanner_title => '连接设备'; @override String get connectionChoiceUsbLabel => 'USB'; @override - String get connectionChoiceBluetoothLabel => '蓝牙'; + String get connectionChoiceBluetoothLabel => '蓝牙'; @override - String get usbScreenTitle => '通过USB连接'; + String get usbScreenTitle => '通过USB连接'; @override - String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。'; + String get usbScreenSubtitle => + '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。'; @override - String get usbScreenStatus => '选择一个 USB 设备'; + String get usbScreenStatus => '选择一个 USB 设备'; @override - String get usbScreenNote => '在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。'; + String get usbScreenNote => + '在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。'; @override - String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。'; + String get usbScreenEmptyState => + '未找到任何 USB 设备。请插入一个,然后刷新。'; @override - String get scanner_scanning => '正在搜索设备...'; + String get scanner_scanning => '正在搜索设备...'; @override - String get scanner_connecting => '正在连接...'; + String get scanner_connecting => '正在连接...'; @override - String get scanner_disconnecting => '断开连接...'; + String get scanner_disconnecting => '断开连接...'; @override - String get scanner_notConnected => '未连接'; + String get scanner_notConnected => '未连接'; @override String scanner_connectedTo(String deviceName) { - return '已连接到 $deviceName'; + return '已连接到 $deviceName'; } @override - String get scanner_searchingDevices => '正在搜索 MeshCore 设备...'; + String get scanner_searchingDevices => '正在搜索 MeshCore 设备...'; @override - String get scanner_tapToScan => '点击“扫描”按钮以查找 MeshCore 设备。'; + String get scanner_tapToScan => + '点击“扫描”按钮以查找 MeshCore 设备。'; @override String scanner_connectionFailed(String error) { - return '连接失败:$error'; + return '连接失败:$error'; } @override - String get scanner_stop => '停止'; + String get scanner_stop => '停止'; @override - String get scanner_scan => '扫描'; + String get scanner_scan => '扫描'; @override - String get scanner_bluetoothOff => '蓝牙已关闭'; + String get scanner_bluetoothOff => '蓝牙已关闭'; @override - String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备'; + String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备'; @override - String get scanner_chromeRequired => '需要 Chrome 浏览器'; + String get scanner_chromeRequired => '需要 Chrome 浏览器'; @override String get scanner_chromeRequiredMessage => - '此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。'; + 'æ­¤ Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。'; @override - String get scanner_enableBluetooth => '启用蓝牙'; + String get scanner_enableBluetooth => '启用蓝牙'; @override - String get device_quickSwitch => '快速切换'; + String get device_quickSwitch => '快速切换'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => '设置'; + String get settings_title => '设置'; @override - String get settings_deviceInfo => '设备信息'; + String get settings_deviceInfo => '设备信息'; @override - String get settings_appSettings => '应用设置'; + String get settings_appSettings => '应用设置'; @override - String get settings_appSettingsSubtitle => '通知、消息和地图偏好'; + String get settings_appSettingsSubtitle => '通知、消息和地图偏好'; @override - String get settings_nodeSettings => '节点设置'; + String get settings_nodeSettings => '节点设置'; @override - String get settings_nodeName => '节点名称'; + String get settings_nodeName => '节点名称'; @override - String get settings_nodeNameNotSet => '未设置'; + String get settings_nodeNameNotSet => '未设置'; @override - String get settings_nodeNameHint => '请输入节点名称'; + String get settings_nodeNameHint => '请输入节点名称'; @override - String get settings_nodeNameUpdated => '节点名称已更新'; + String get settings_nodeNameUpdated => '节点名称已更新'; @override - String get settings_radioSettings => '无线电设置'; + String get settings_radioSettings => '无线电设置'; @override - String get settings_radioSettingsSubtitle => '频率、功率、扩频因子'; + String get settings_radioSettingsSubtitle => '频率、功率、扩频因子'; @override - String get settings_radioSettingsUpdated => '无线电设置已更新'; + String get settings_radioSettingsUpdated => '无线电设置已更新'; @override - String get settings_location => '位置'; + String get settings_location => '位置'; @override - String get settings_locationSubtitle => 'GPS 坐标'; + String get settings_locationSubtitle => 'GPS 坐标'; @override - String get settings_locationUpdated => '位置和 GPS 设置已更新'; + String get settings_locationUpdated => '位置和 GPS 设置已更新'; @override - String get settings_locationBothRequired => '请输入经度和纬度'; + String get settings_locationBothRequired => '请输入经度和纬度'; @override - String get settings_locationInvalid => '无效的经度和纬度'; + String get settings_locationInvalid => '无效的经度和纬度'; @override - String get settings_locationGPSEnable => '启用 GPS'; + String get settings_locationGPSEnable => '启用 GPS'; @override - String get settings_locationGPSEnableSubtitle => '启用 GPS 以自动更新位置。'; + String get settings_locationGPSEnableSubtitle => + '启用 GPS 以自动更新位置。'; @override - String get settings_locationIntervalSec => 'GPS 间隔(秒)'; + String get settings_locationIntervalSec => 'GPS 间隔(秒)'; @override - String get settings_locationIntervalInvalid => '间隔时间必须至少为 60 秒,但不超过 86400 秒。'; + String get settings_locationIntervalInvalid => + '间隔时间必须至少为 60 秒,但不超过 86400 秒。'; @override - String get settings_latitude => '纬度'; + String get settings_latitude => '纬度'; @override - String get settings_longitude => '经度'; + String get settings_longitude => '经度'; @override - String get settings_privacyMode => '隐私模式'; + String get settings_privacyMode => '隐私模式'; @override - String get settings_privacyModeSubtitle => '在广告中隐藏姓名/位置'; + String get settings_privacyModeSubtitle => '在广告中隐藏姓名/位置'; @override - String get settings_privacyModeToggle => '切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。'; + String get settings_privacyModeToggle => + '切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。'; @override - String get settings_privacyModeEnabled => '隐私模式已启用'; + String get settings_privacyModeEnabled => '隐私模式已启用'; @override - String get settings_privacyModeDisabled => '隐私模式已关闭'; + String get settings_privacyModeDisabled => '隐私模式已关闭'; @override - String get settings_actions => '操作'; + String get settings_actions => '操作'; @override - String get settings_sendAdvertisement => '发送广播'; + String get settings_sendAdvertisement => '发送广播'; @override - String get settings_sendAdvertisementSubtitle => '立即发送广播'; + String get settings_sendAdvertisementSubtitle => '立即发送广播'; @override - String get settings_advertisementSent => '已发送广播'; + String get settings_advertisementSent => '已发送广播'; @override - String get settings_syncTime => '同步时间'; + String get settings_syncTime => '同步时间'; @override - String get settings_syncTimeSubtitle => '将设备时钟设置为与手机时间一致'; + String get settings_syncTimeSubtitle => + '将设备时钟设置为与手机时间一致'; @override - String get settings_timeSynchronized => '时间已同步'; + String get settings_timeSynchronized => '时间已同步'; @override - String get settings_refreshContacts => '刷新联系人'; + String get settings_refreshContacts => '刷新联系人'; @override - String get settings_refreshContactsSubtitle => '从设备重新加载联系人列表'; + String get settings_refreshContactsSubtitle => + '从设备重新加载联系人列表'; @override - String get settings_rebootDevice => '重启设备'; + String get settings_rebootDevice => '重启设备'; @override - String get settings_rebootDeviceSubtitle => '重启 MeshCore 设备'; + String get settings_rebootDeviceSubtitle => '重启 MeshCore 设备'; @override - String get settings_rebootDeviceConfirm => '确定要重启设备吗?这将断开与设备的连接。'; + String get settings_rebootDeviceConfirm => + '确定要重启设备吗?这将断开与设备的连接。'; @override - String get settings_debug => '调试'; + String get settings_debug => '调试'; @override - String get settings_bleDebugLog => 'BLE 调试日志'; + String get settings_bleDebugLog => 'BLE 调试日志'; @override - String get settings_bleDebugLogSubtitle => 'BLE 命令、响应和原始数据'; + String get settings_bleDebugLogSubtitle => + 'BLE 命令、响应和原始数据'; @override - String get settings_appDebugLog => '应用调试日志'; + String get settings_appDebugLog => '应用调试日志'; @override - String get settings_appDebugLogSubtitle => '应用调试消息'; + String get settings_appDebugLogSubtitle => '应用调试消息'; @override - String get settings_about => '关于'; + String get settings_about => '关于'; @override String settings_aboutVersion(String version) { @@ -335,1146 +340,1180 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get settings_aboutLegalese => '2026 MeshCore 开源项目'; + String get settings_aboutLegalese => '2026 MeshCore 开源项目'; @override String get settings_aboutDescription => - '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; + '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; @override String get settings_aboutOpenMeteoAttribution => - 'LOS 高程数据:Open-Meteo (CC BY 4.0)'; + 'LOS 高程数据:Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => '名称'; + String get settings_infoName => '名称'; @override String get settings_infoId => 'MAC ID'; @override - String get settings_infoStatus => '状态'; + String get settings_infoStatus => '状态'; @override - String get settings_infoBattery => '电池'; + String get settings_infoBattery => '电池'; @override - String get settings_infoPublicKey => '公钥'; + String get settings_infoPublicKey => '公钥'; @override - String get settings_infoContactsCount => '联系人数量'; + String get settings_infoContactsCount => '联系人数量'; @override - String get settings_infoChannelCount => '频道数量'; + String get settings_infoChannelCount => '频道数量'; @override - String get settings_presets => '预设'; + String get settings_presets => '预设'; @override - String get settings_frequency => '频率 (MHz)'; + String get settings_frequency => '频率 (MHz)'; @override String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => '无效频率范围(300-2500 MHz)'; + String get settings_frequencyInvalid => + '无效频率范围(300-2500 MHz)'; @override - String get settings_bandwidth => '带宽'; + String get settings_bandwidth => '带宽'; @override - String get settings_spreadingFactor => '扩频因子'; + String get settings_spreadingFactor => '扩频因子'; @override - String get settings_codingRate => '编码速率'; + String get settings_codingRate => '编码速率'; @override - String get settings_txPower => 'TX 功率 (dBm)'; + String get settings_txPower => 'TX 功率 (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; + String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; @override - String get settings_clientRepeat => '离网重复'; + String get settings_clientRepeat => '离网重复'; @override - String get settings_clientRepeatSubtitle => '允许此设备重复发送网状数据包给其他设备'; + String get settings_clientRepeatSubtitle => + '允许此设备重复发送网状数据包给其他设备'; @override String get settings_clientRepeatFreqWarning => - '离网重复通信需要使用 433、869 或 918 兆赫兹的频率。'; + '离网重复通信需要使用 433、869 或 918 兆赫兹的频率。'; @override String settings_error(String message) { - return '错误:$message'; + return '错误:$message'; } @override - String get appSettings_title => '应用设置'; + String get appSettings_title => '应用设置'; @override - String get appSettings_appearance => '外观'; + String get appSettings_appearance => '外观'; @override - String get appSettings_theme => '主题'; + String get appSettings_theme => '主题'; @override - String get appSettings_themeSystem => '跟随系统'; + String get appSettings_themeSystem => '跟随系统'; @override - String get appSettings_themeLight => '浅色'; + String get appSettings_themeLight => '浅色'; @override - String get appSettings_themeDark => '深色'; + String get appSettings_themeDark => '深色'; @override - String get appSettings_language => '语言'; + String get appSettings_language => '语言'; @override - String get appSettings_languageSystem => '跟随系统'; + String get appSettings_languageSystem => '跟随系统'; @override - String get appSettings_languageEn => '英语'; + String get appSettings_languageEn => '英语'; @override - String get appSettings_languageFr => '法语'; + String get appSettings_languageFr => '法语'; @override - String get appSettings_languageEs => '西班牙语'; + String get appSettings_languageEs => '西班牙语'; @override - String get appSettings_languageDe => '德语'; + String get appSettings_languageDe => '德语'; @override - String get appSettings_languagePl => '波兰语'; + String get appSettings_languagePl => '波兰语'; @override - String get appSettings_languageSl => '斯洛文尼亚语'; + String get appSettings_languageSl => '斯洛文尼亚语'; @override - String get appSettings_languagePt => '葡萄牙语'; + String get appSettings_languagePt => '葡萄牙语'; @override - String get appSettings_languageIt => '意大利语'; + String get appSettings_languageIt => '意大利语'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override - String get appSettings_languageSv => '瑞典语'; + String get appSettings_languageSv => '瑞典语'; @override - String get appSettings_languageNl => '荷兰语'; + String get appSettings_languageNl => '荷兰语'; @override - String get appSettings_languageSk => '斯洛伐克语'; + String get appSettings_languageSk => '斯洛伐克语'; @override - String get appSettings_languageBg => '保加利亚语'; + String get appSettings_languageBg => '保加利亚语'; @override - String get appSettings_languageRu => '俄语'; + String get appSettings_languageRu => '俄语'; @override - String get appSettings_languageUk => '乌克兰语'; + String get appSettings_languageUk => '乌克兰语'; @override - String get appSettings_enableMessageTracing => '启用消息追踪'; + String get appSettings_enableMessageTracing => '启用消息追踪'; @override - String get appSettings_enableMessageTracingSubtitle => '显示消息的详细路由和时间元数据'; + String get appSettings_enableMessageTracingSubtitle => + '显示消息的详细路由和时间元数据'; @override - String get appSettings_notifications => '通知'; + String get appSettings_notifications => '通知'; @override - String get appSettings_enableNotifications => '启用通知'; + String get appSettings_enableNotifications => '启用通知'; @override - String get appSettings_enableNotificationsSubtitle => '接收消息和广播的通知'; + String get appSettings_enableNotificationsSubtitle => + '接收消息和广播的通知'; @override - String get appSettings_notificationPermissionDenied => '权限被拒绝'; + String get appSettings_notificationPermissionDenied => '权限被拒绝'; @override - String get appSettings_notificationsEnabled => '通知已启用'; + String get appSettings_notificationsEnabled => '通知已启用'; @override - String get appSettings_notificationsDisabled => '通知已关闭'; + String get appSettings_notificationsDisabled => '通知已关闭'; @override - String get appSettings_messageNotifications => '消息通知'; + String get appSettings_messageNotifications => '消息通知'; @override - String get appSettings_messageNotificationsSubtitle => '收到新消息时显示通知'; + String get appSettings_messageNotificationsSubtitle => + '收到新消息时显示通知'; @override - String get appSettings_channelMessageNotifications => '频道消息通知'; + String get appSettings_channelMessageNotifications => '频道消息通知'; @override - String get appSettings_channelMessageNotificationsSubtitle => '收到频道消息时显示通知'; + String get appSettings_channelMessageNotificationsSubtitle => + '收到频道消息时显示通知'; @override - String get appSettings_advertisementNotifications => '广播通知'; + String get appSettings_advertisementNotifications => '广播通知'; @override - String get appSettings_advertisementNotificationsSubtitle => '发现新节点时显示通知'; + String get appSettings_advertisementNotificationsSubtitle => + '发现新节点时显示通知'; @override - String get appSettings_messaging => '消息'; + String get appSettings_messaging => '消息'; @override - String get appSettings_clearPathOnMaxRetry => '达到最大重试次数时清除路径'; + String get appSettings_clearPathOnMaxRetry => + '达到最大重试次数时清除路径'; @override - String get appSettings_clearPathOnMaxRetrySubtitle => '在5次发送失败后重置联系路径。'; + String get appSettings_clearPathOnMaxRetrySubtitle => + '在5次发送失败后重置联系路径。'; @override - String get appSettings_pathsWillBeCleared => '5次失败后将重新路由'; + String get appSettings_pathsWillBeCleared => '5次失败后将重新路由'; @override - String get appSettings_pathsWillNotBeCleared => '路径不会自动清除'; + String get appSettings_pathsWillNotBeCleared => '路径不会自动清除'; @override - String get appSettings_autoRouteRotation => '自动路径轮换'; + String get appSettings_autoRouteRotation => '自动路径轮换'; @override - String get appSettings_autoRouteRotationSubtitle => '在最佳路径和泛洪模式之间切换'; + String get appSettings_autoRouteRotationSubtitle => + '在最佳路径和泛洪模式之间切换'; @override - String get appSettings_autoRouteRotationEnabled => '自动路径轮换已启用'; + String get appSettings_autoRouteRotationEnabled => + '自动路径轮换已启用'; @override - String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用'; + String get appSettings_autoRouteRotationDisabled => + '自动路径轮换已禁用'; @override - String get appSettings_battery => '电池'; + String get appSettings_battery => '电池'; @override - String get appSettings_batteryChemistry => '电池类型'; + String get appSettings_batteryChemistry => '电池类型'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return '为每个设备设置 ($deviceName)'; + return '为每个设备设置 ($deviceName)'; } @override - String get appSettings_batteryChemistryConnectFirst => '请先连接设备'; + String get appSettings_batteryChemistryConnectFirst => '请先连接设备'; @override - String get appSettings_batteryNmc => '18650 NMC 电池 (3.0-4.2V)'; + String get appSettings_batteryNmc => '18650 NMC 电池 (3.0-4.2V)'; @override - String get appSettings_batteryLifepo4 => '磷酸铁锂 (2.6-3.65V)'; + String get appSettings_batteryLifepo4 => '磷酸铁锂 (2.6-3.65V)'; @override - String get appSettings_batteryLipo => '锂聚合物电池 (3.0-4.2V)'; + String get appSettings_batteryLipo => '锂聚合物电池 (3.0-4.2V)'; @override - String get appSettings_mapDisplay => '地图显示'; + String get appSettings_mapDisplay => '地图显示'; @override - String get appSettings_showRepeaters => '显示转发节点'; + String get appSettings_showRepeaters => '显示转发节点'; @override - String get appSettings_showRepeatersSubtitle => '在地图上显示转发节点'; + String get appSettings_showRepeatersSubtitle => + '在地图上显示转发节点'; @override - String get appSettings_showChatNodes => '显示聊天节点'; + String get appSettings_showChatNodes => '显示聊天节点'; @override - String get appSettings_showChatNodesSubtitle => '在地图上显示聊天节点'; + String get appSettings_showChatNodesSubtitle => + '在地图上显示聊天节点'; @override - String get appSettings_showOtherNodes => '显示其他节点'; + String get appSettings_showOtherNodes => '显示其他节点'; @override - String get appSettings_showOtherNodesSubtitle => '在地图上显示其他节点类型'; + String get appSettings_showOtherNodesSubtitle => + '在地图上显示其他节点类型'; @override - String get appSettings_timeFilter => '时间过滤器'; + String get appSettings_timeFilter => '时间过滤器'; @override - String get appSettings_timeFilterShowAll => '显示所有节点'; + String get appSettings_timeFilterShowAll => '显示所有节点'; @override String appSettings_timeFilterShowLast(int hours) { - return '显示过去 $hours 小时内的节点'; + return '显示过去 $hours 小时内的节点'; } @override - String get appSettings_mapTimeFilter => '地图时间筛选'; + String get appSettings_mapTimeFilter => '地图时间筛选'; @override - String get appSettings_showNodesDiscoveredWithin => '显示在此时间段内发现的节点:'; + String get appSettings_showNodesDiscoveredWithin => + '显示在此时间段内发现的节点:'; @override - String get appSettings_allTime => '所有时间'; + String get appSettings_allTime => '所有时间'; @override - String get appSettings_lastHour => '过去一小时'; + String get appSettings_lastHour => '过去一小时'; @override - String get appSettings_last6Hours => '过去6小时'; + String get appSettings_last6Hours => '过去6小时'; @override - String get appSettings_last24Hours => '过去24小时'; + String get appSettings_last24Hours => '过去24小时'; @override - String get appSettings_lastWeek => '上周'; + String get appSettings_lastWeek => '上周'; @override - String get appSettings_offlineMapCache => '离线地图缓存'; + String get appSettings_offlineMapCache => '离线地图缓存'; @override - String get appSettings_unitsTitle => '单位'; + String get appSettings_unitsTitle => '单位'; @override - String get appSettings_unitsMetric => '公制(米/公里)'; + String get appSettings_unitsMetric => '公制(米/公里)'; @override - String get appSettings_unitsImperial => '英制 (ft / mi)'; + String get appSettings_unitsImperial => '英制 (ft / mi)'; @override - String get appSettings_noAreaSelected => '未选择任何区域'; + String get appSettings_noAreaSelected => '未选择任何区域'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return '已选择区域(缩放 $minZoom - $maxZoom)'; + return '已选择区域(缩放 $minZoom - $maxZoom)'; } @override - String get appSettings_debugCard => '调试'; + String get appSettings_debugCard => '调试'; @override - String get appSettings_appDebugLogging => '应用调试日志'; + String get appSettings_appDebugLogging => '应用调试日志'; @override - String get appSettings_appDebugLoggingSubtitle => '记录应用调试消息以进行故障排除。'; + String get appSettings_appDebugLoggingSubtitle => + '记录应用调试消息以进行故障排除。'; @override - String get appSettings_appDebugLoggingEnabled => '调试日志已启用'; + String get appSettings_appDebugLoggingEnabled => '调试日志已启用'; @override - String get appSettings_appDebugLoggingDisabled => '应用调试日志已禁用'; + String get appSettings_appDebugLoggingDisabled => + '应用调试日志已禁用'; @override - String get contacts_title => '联系人'; + String get contacts_title => '联系人'; @override - String get contacts_noContacts => '暂无联系人'; + String get contacts_noContacts => '暂无联系人'; @override - String get contacts_contactsWillAppear => '当设备发送广播时,联系人将显示。'; + String get contacts_contactsWillAppear => + '当设备发送广播时,联系人将显示。'; @override - String get contacts_unread => '未读'; + String get contacts_unread => '未读'; @override - String get contacts_searchContactsNoNumber => '搜索联系人...'; + String get contacts_searchContactsNoNumber => '搜索联系人...'; @override String contacts_searchContacts(int number, String str) { - return '搜索联系人...'; + return '搜索联系人...'; } @override String contacts_searchFavorites(int number, String str) { - return '搜索 $number$str 收藏...'; + return '搜索 $number$str 收藏...'; } @override String contacts_searchUsers(int number, String str) { - return '搜索 $number$str 位用户...'; + return '搜索 $number$str 位用户...'; } @override String contacts_searchRepeaters(int number, String str) { - return '搜索 $number$str 重复器...'; + return '搜索 $number$str 重复器...'; } @override String contacts_searchRoomServers(int number, String str) { - return '搜索 $number$str 房间服务器...'; + return '搜索 $number$str 房间服务器...'; } @override - String get contacts_noUnreadContacts => '没有未读内容'; + String get contacts_noUnreadContacts => '没有未读内容'; @override - String get contacts_noContactsFound => '未找到任何联系人或群聊'; + String get contacts_noContactsFound => '未找到任何联系人或群聊'; @override - String get contacts_deleteContact => '删除联系人'; + String get contacts_deleteContact => '删除联系人'; @override String contacts_removeConfirm(String contactName) { - return '从联系人中移除 $contactName?'; + return '从联系人中移除 $contactName?'; } @override - String get contacts_manageRepeater => '管理转发节点'; + String get contacts_manageRepeater => '管理转发节点'; @override - String get contacts_manageRoom => '管理房间服务器'; + String get contacts_manageRoom => '管理房间服务器'; @override - String get contacts_roomLogin => '服务器登录'; + String get contacts_roomLogin => '服务器登录'; @override - String get contacts_openChat => '打开聊天'; + String get contacts_openChat => '打开聊天'; @override - String get contacts_editGroup => '编辑群聊'; + String get contacts_editGroup => '编辑群聊'; @override - String get contacts_deleteGroup => '删除群聊'; + String get contacts_deleteGroup => '删除群聊'; @override String contacts_deleteGroupConfirm(String groupName) { - return '删除群聊 \"$groupName\"?'; + return '删除群聊 \"$groupName\"?'; } @override - String get contacts_newGroup => '新建群聊'; + String get contacts_newGroup => '新建群聊'; @override - String get contacts_groupName => '群聊名称'; + String get contacts_groupName => '群聊名称'; @override - String get contacts_groupNameRequired => '请输入群聊名称'; + String get contacts_groupNameRequired => '请输入群聊名称'; @override String contacts_groupAlreadyExists(String name) { - return '名为 \"$name\" 的群聊已存在'; + return '名为 \"$name\" 的群聊已存在'; } @override - String get contacts_filterContacts => '筛选联系人...'; + String get contacts_filterContacts => '筛选联系人...'; @override - String get contacts_noContactsMatchFilter => '没有符合条件的联系人'; + String get contacts_noContactsMatchFilter => '没有符合条件的联系人'; @override - String get contacts_noMembers => '暂无成员'; + String get contacts_noMembers => '暂无成员'; @override - String get contacts_lastSeenNow => '刚刚'; + String get contacts_lastSeenNow => '刚刚'; @override String contacts_lastSeenMinsAgo(int minutes) { - return '最后在线 $minutes 分钟前'; + return '最后在线 $minutes 分钟前'; } @override - String get contacts_lastSeenHourAgo => '最后在线 1小时前'; + String get contacts_lastSeenHourAgo => '最后在线 1小时前'; @override String contacts_lastSeenHoursAgo(int hours) { - return '最后在线 $hours 小时前'; + return '最后在线 $hours 小时前'; } @override - String get contacts_lastSeenDayAgo => '最后在线 1天前'; + String get contacts_lastSeenDayAgo => '最后在线 1天前'; @override String contacts_lastSeenDaysAgo(int days) { - return '最后在线 $days 天前'; + return '最后在线 $days 天前'; } @override - String get channels_title => '频道'; + String get channels_title => '频道'; @override - String get channels_noChannelsConfigured => '未配置任何频道'; + String get channels_noChannelsConfigured => '未配置任何频道'; @override - String get channels_addPublicChannel => '添加公共频道'; + String get channels_addPublicChannel => '添加公共频道'; @override - String get channels_searchChannels => '搜索频道...'; + String get channels_searchChannels => '搜索频道...'; @override - String get channels_noChannelsFound => '未找到任何频道'; + String get channels_noChannelsFound => '未找到任何频道'; @override String channels_channelIndex(int index) { - return '频道 $index'; + return '频道 $index'; } @override - String get channels_hashtagChannel => '标签频道'; + String get channels_hashtagChannel => '标签频道'; @override - String get channels_public => '公共'; + String get channels_public => '公共'; @override - String get channels_private => '私有'; + String get channels_private => '私有'; @override - String get channels_publicChannel => '公共频道'; + String get channels_publicChannel => '公共频道'; @override - String get channels_privateChannel => '私有频道'; + String get channels_privateChannel => '私有频道'; @override - String get channels_editChannel => '编辑频道'; + String get channels_editChannel => '编辑频道'; @override - String get channels_muteChannel => '静音频道'; + String get channels_muteChannel => '静音频道'; @override - String get channels_unmuteChannel => '取消静音频道'; + String get channels_unmuteChannel => '取消静音频道'; @override - String get channels_deleteChannel => '删除频道'; + String get channels_deleteChannel => '删除频道'; @override String channels_deleteChannelConfirm(String name) { - return '删除频道 \"$name\"?此操作不可撤销。'; + return '删除频道 \"$name\"?此操作不可撤销。'; } @override String channels_channelDeleteFailed(String name) { - return '无法删除频道 \"$name\"'; + return '无法删除频道 \"$name\"'; } @override String channels_channelDeleted(String name) { - return '已删除频道 \"$name\"'; + return '已删除频道 \"$name\"'; } @override - String get channels_addChannel => '添加频道'; + String get channels_addChannel => '添加频道'; @override - String get channels_channelIndexLabel => '频道索引'; + String get channels_channelIndexLabel => '频道索引'; @override - String get channels_channelName => '频道名称'; + String get channels_channelName => '频道名称'; @override - String get channels_usePublicChannel => '使用公共频道'; + String get channels_usePublicChannel => '使用公共频道'; @override - String get channels_standardPublicPsk => '标准公共 PSK'; + String get channels_standardPublicPsk => '标准公共 PSK'; @override - String get channels_pskHex => 'PSK (十六进制)'; + String get channels_pskHex => 'PSK (十六进制)'; @override - String get channels_generateRandomPsk => '生成随机 PSK'; + String get channels_generateRandomPsk => '生成随机 PSK'; @override - String get channels_enterChannelName => '请输入频道名称'; + String get channels_enterChannelName => '请输入频道名称'; @override - String get channels_pskMustBe32Hex => 'PSK 必须为 32 个十六进制字符'; + String get channels_pskMustBe32Hex => + 'PSK 必须为 32 个十六进制字符'; @override String channels_channelAdded(String name) { - return '已添加频道 \"$name\"'; + return '已添加频道 \"$name\"'; } @override String channels_editChannelTitle(int index) { - return '编辑频道 $index'; + return '编辑频道 $index'; } @override - String get channels_smazCompression => 'SMAZ 压缩'; + String get channels_smazCompression => 'SMAZ 压缩'; @override String channels_channelUpdated(String name) { - return '频道 \"$name\" 已更新'; + return '频道 \"$name\" 已更新'; } @override - String get channels_publicChannelAdded => '已添加公共频道'; + String get channels_publicChannelAdded => '已添加公共频道'; @override - String get channels_sortBy => '排序方式'; + String get channels_sortBy => '排序方式'; @override - String get channels_sortManual => '手动'; + String get channels_sortManual => '手动'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => '最新消息'; + String get channels_sortLatestMessages => '最新消息'; @override - String get channels_sortUnread => '未读'; + String get channels_sortUnread => '未读'; @override - String get channels_createPrivateChannel => '创建私有频道'; + String get channels_createPrivateChannel => '创建私有频道'; @override - String get channels_createPrivateChannelDesc => '使用密钥保护。'; + String get channels_createPrivateChannelDesc => '使用密钥保护。'; @override - String get channels_joinPrivateChannel => '加入私有频道'; + String get channels_joinPrivateChannel => '加入私有频道'; @override - String get channels_joinPrivateChannelDesc => '手动输入密钥。'; + String get channels_joinPrivateChannelDesc => '手动输入密钥。'; @override - String get channels_joinPublicChannel => '加入公共频道'; + String get channels_joinPublicChannel => '加入公共频道'; @override - String get channels_joinPublicChannelDesc => '任何人都可以加入。'; + String get channels_joinPublicChannelDesc => '任何人都可以加入。'; @override - String get channels_joinHashtagChannel => '加入标签频道'; + String get channels_joinHashtagChannel => '加入标签频道'; @override - String get channels_joinHashtagChannelDesc => '任何人都可以加入标签频道。'; + String get channels_joinHashtagChannelDesc => + '任何人都可以加入标签频道。'; @override - String get channels_scanQrCode => '扫描二维码'; + String get channels_scanQrCode => '扫描二维码'; @override - String get channels_scanQrCodeComingSoon => '即将推出'; + String get channels_scanQrCodeComingSoon => '即将推出'; @override - String get channels_enterHashtag => '输入标签'; + String get channels_enterHashtag => '输入标签'; @override - String get channels_hashtagHint => '例如:#团队'; + String get channels_hashtagHint => '例如:#团队'; @override - String get chat_noMessages => '暂无消息'; + String get chat_noMessages => '暂无消息'; @override - String get chat_sendMessageToStart => '发送消息开始对话'; + String get chat_sendMessageToStart => '发送消息开始对话'; @override - String get chat_originalMessageNotFound => '找不到原始消息'; + String get chat_originalMessageNotFound => '找不到原始消息'; @override String chat_replyingTo(String name) { - return '正在回复 $name'; + return '正在回复 $name'; } @override String chat_replyTo(String name) { - return '回复 $name'; + return '回复 $name'; } @override - String get chat_location => '位置'; + String get chat_location => '位置'; @override String chat_sendMessageTo(String contactName) { - return '发送消息给 $contactName'; + return '发送消息给 $contactName'; } @override - String get chat_typeMessage => '输入消息...'; + String get chat_typeMessage => '输入消息...'; @override String chat_messageTooLong(int maxBytes) { - return '消息过长(最多 $maxBytes 字节)'; + return '消息过长(最多 $maxBytes 字节)'; } @override - String get chat_messageCopied => '消息已复制'; + String get chat_messageCopied => '消息已复制'; @override - String get chat_messageDeleted => '消息已删除'; + String get chat_messageDeleted => '消息已删除'; @override - String get chat_retryingMessage => '正在重试消息'; + String get chat_retryingMessage => '正在重试消息'; @override String chat_retryCount(int current, int max) { - return '重试 $current/$max'; + return '重试 $current/$max'; } @override - String get chat_sendGif => '发送 GIF'; + String get chat_sendGif => '发送 GIF'; @override - String get chat_reply => '回复'; + String get chat_reply => '回复'; @override - String get chat_addReaction => '添加表情'; + String get chat_addReaction => '添加表情'; @override - String get chat_me => '我'; + String get chat_me => '我'; @override - String get emojiCategorySmileys => '表情'; + String get emojiCategorySmileys => '表情'; @override - String get emojiCategoryGestures => '手势'; + String get emojiCategoryGestures => '手势'; @override - String get emojiCategoryHearts => '爱心'; + String get emojiCategoryHearts => '爱心'; @override - String get emojiCategoryObjects => '物品'; + String get emojiCategoryObjects => '物品'; @override - String get gifPicker_title => '选择 GIF'; + String get gifPicker_title => '选择 GIF'; @override - String get gifPicker_searchHint => '搜索 GIF...'; + String get gifPicker_searchHint => '搜索 GIF...'; @override - String get gifPicker_poweredBy => '由 GIPHY 提供'; + String get gifPicker_poweredBy => 'ç”± GIPHY 提供'; @override - String get gifPicker_noGifsFound => '未找到 GIF'; + String get gifPicker_noGifsFound => '未找到 GIF'; @override - String get gifPicker_failedLoad => '加载 GIF 失败'; + String get gifPicker_failedLoad => '加载 GIF 失败'; @override - String get gifPicker_failedSearch => '搜索 GIF 失败'; + String get gifPicker_failedSearch => '搜索 GIF 失败'; @override - String get gifPicker_noInternet => '无网络连接'; + String get gifPicker_noInternet => '无网络连接'; @override - String get debugLog_appTitle => '应用调试日志'; + String get debugLog_appTitle => '应用调试日志'; @override - String get debugLog_bleTitle => 'BLE 调试日志'; + String get debugLog_bleTitle => 'BLE 调试日志'; @override - String get debugLog_copyLog => '复制日志'; + String get debugLog_copyLog => '复制日志'; @override - String get debugLog_clearLog => '清除日志'; + String get debugLog_clearLog => '清除日志'; @override - String get debugLog_copied => '调试日志已复制'; + String get debugLog_copied => '调试日志已复制'; @override - String get debugLog_bleCopied => 'BLE 日志已复制'; + String get debugLog_bleCopied => 'BLE 日志已复制'; @override - String get debugLog_noEntries => '暂无调试日志'; + String get debugLog_noEntries => '暂无调试日志'; @override - String get debugLog_enableInSettings => '请在设置中启用应用调试日志。'; + String get debugLog_enableInSettings => + '请在设置中启用应用调试日志。'; @override - String get debugLog_frames => '帧'; + String get debugLog_frames => '帧'; @override - String get debugLog_rawLogRx => '原始日志 RX'; + String get debugLog_rawLogRx => '原始日志 RX'; @override - String get debugLog_noBleActivity => '暂无 BLE 活动'; + String get debugLog_noBleActivity => '暂无 BLE 活动'; @override String debugFrame_length(int count) { - return '帧长度:$count 字节'; + return '帧长度:$count 字节'; } @override String debugFrame_command(String value) { - return '命令:0x$value'; + return '命令:0x$value'; } @override - String get debugFrame_textMessageHeader => '文本消息:'; + String get debugFrame_textMessageHeader => '文本消息:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- 目标公钥:$pubKey'; + return '- 目标公钥:$pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- 时间戳:$timestamp'; + return '- 时间戳:$timestamp'; } @override String debugFrame_flags(String value) { - return '- 标志:0x$value'; + return '- 标志:0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- 文本类型:$type ($label)'; + return '- 文本类型:$type ($label)'; } @override - String get debugFrame_textTypeCli => '命令行'; + String get debugFrame_textTypeCli => '命令行'; @override - String get debugFrame_textTypePlain => '纯文本'; + String get debugFrame_textTypePlain => '纯文本'; @override String debugFrame_text(String text) { - return '- 文本:“$text”'; + return '- 文本:“$text”'; } @override - String get debugFrame_hexDump => '十六进制数据:'; + String get debugFrame_hexDump => '十六进制数据:'; @override - String get chat_pathManagement => '路径管理'; + String get chat_pathManagement => '路径管理'; @override - String get chat_ShowAllPaths => '显示所有路径'; + String get chat_ShowAllPaths => '显示所有路径'; @override - String get chat_routingMode => '路由模式'; + String get chat_routingMode => '路由模式'; @override - String get chat_autoUseSavedPath => '自动(使用保存的路径)'; + String get chat_autoUseSavedPath => '自动(使用保存的路径)'; @override - String get chat_forceFloodMode => '强制泛洪模式'; + String get chat_forceFloodMode => '强制泛洪模式'; @override - String get chat_recentAckPaths => '最近使用的 ACK 路径(点击使用):'; + String get chat_recentAckPaths => + '最近使用的 ACK 路径(点击使用):'; @override - String get chat_pathHistoryFull => '路径历史已满,请删除后再添加。'; + String get chat_pathHistoryFull => + '路径历史已满,请删除后再添加。'; @override - String get chat_hopSingular => '跳'; + String get chat_hopSingular => 'è·³'; @override - String get chat_hopPlural => '跳'; + String get chat_hopPlural => 'è·³'; @override String chat_hopsCount(int count) { - return '$count 跳'; + return '$count è·³'; } @override - String get chat_successes => '成功'; + String get chat_successes => '成功'; @override - String get chat_removePath => '移除路径'; + String get chat_removePath => '移除路径'; @override - String get chat_noPathHistoryYet => '暂无路径历史。\n发送消息以探索路径。'; + String get chat_noPathHistoryYet => + '暂无路径历史。\n发送消息以探索路径。'; @override - String get chat_pathActions => '路径操作:'; + String get chat_pathActions => '路径操作:'; @override - String get chat_setCustomPath => '设置自定义路径'; + String get chat_setCustomPath => '设置自定义路径'; @override - String get chat_setCustomPathSubtitle => '手动指定路由路径'; + String get chat_setCustomPathSubtitle => '手动指定路由路径'; @override - String get chat_clearPath => '清除路径'; + String get chat_clearPath => '清除路径'; @override - String get chat_clearPathSubtitle => '清除当前路径,下次发送将重新尝试。'; + String get chat_clearPathSubtitle => + '清除当前路径,下次发送将重新尝试。'; @override - String get chat_pathCleared => '路径已清除。下一条消息将重新路由。'; + String get chat_pathCleared => + '路径已清除。下一条消息将重新路由。'; @override - String get chat_floodModeSubtitle => '在应用栏中切换路由模式。'; + String get chat_floodModeSubtitle => '在应用栏中切换路由模式。'; @override - String get chat_floodModeEnabled => '泛洪模式已启用。可通过应用栏的路由图标切换。'; + String get chat_floodModeEnabled => + '泛洪模式已启用。可通过应用栏的路由图标切换。'; @override - String get chat_fullPath => '完整路径'; + String get chat_fullPath => '完整路径'; @override - String get chat_pathDetailsNotAvailable => '路径信息暂不可用,请尝试发送消息刷新。'; + String get chat_pathDetailsNotAvailable => + '路径信息暂不可用,请尝试发送消息刷新。'; @override String chat_pathSetHops(int hopCount, String status) { - return '路径设置:$hopCount 跳 - $status'; + return '路径设置:$hopCount è·³ - $status'; } @override - String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。'; + String get chat_pathSavedLocally => + '已本地保存,连接设备后可同步。'; @override - String get chat_pathDeviceConfirmed => '设备已确认。'; + String get chat_pathDeviceConfirmed => '设备已确认。'; @override - String get chat_pathDeviceNotConfirmed => '设备尚未确认。'; + String get chat_pathDeviceNotConfirmed => '设备尚未确认。'; @override - String get chat_type => '类型'; + String get chat_type => '类型'; @override - String get chat_path => '路径'; + String get chat_path => '路径'; @override - String get chat_publicKey => '公钥'; + String get chat_publicKey => '公钥'; @override - String get chat_compressOutgoingMessages => '压缩发送的消息'; + String get chat_compressOutgoingMessages => '压缩发送的消息'; @override - String get chat_floodForced => '泛洪(强制)'; + String get chat_floodForced => '泛洪(强制)'; @override - String get chat_directForced => '直连(强制)'; + String get chat_directForced => '直连(强制)'; @override String chat_hopsForced(int count) { - return '$count 跳(强制)'; + return '$count 跳(强制)'; } @override - String get chat_floodAuto => '自动泛洪'; + String get chat_floodAuto => '自动泛洪'; @override - String get chat_direct => '直连'; + String get chat_direct => '直连'; @override - String get chat_poiShared => '共享位置'; + String get chat_poiShared => '共享位置'; @override String chat_unread(int count) { - return '未读:$count'; + return '未读:$count'; } @override - String get chat_openLink => '打开链接?'; + String get chat_openLink => '打开链接?'; @override - String get chat_openLinkConfirmation => '是否使用浏览器打开此链接?'; + String get chat_openLinkConfirmation => + '是否使用浏览器打开此链接?'; @override - String get chat_open => '打开'; + String get chat_open => '打开'; @override String chat_couldNotOpenLink(String url) { - return '无法打开链接:$url'; + return '无法打开链接:$url'; } @override - String get chat_invalidLink => '无效的链接格式'; + String get chat_invalidLink => '无效的链接格式'; @override - String get map_title => '节点地图'; + String get map_title => '节点地图'; @override - String get map_lineOfSight => '视线'; + String get map_lineOfSight => '视线'; @override - String get map_losScreenTitle => '视线'; + String get map_losScreenTitle => '视线'; @override - String get map_noNodesWithLocation => '没有包含位置信息的节点'; + String get map_noNodesWithLocation => '没有包含位置信息的节点'; @override - String get map_nodesNeedGps => '节点需要共享 GPS 坐标才能在地图上显示'; + String get map_nodesNeedGps => + '节点需要共享 GPS 坐标才能在地图上显示'; @override String map_nodesCount(int count) { - return '节点:$count'; + return '节点:$count'; } @override String map_pinsCount(int count) { - return '标记:$count'; + return '标记:$count'; } @override - String get map_chat => '聊天'; + String get map_chat => '聊天'; @override - String get map_repeater => '转发节点'; + String get map_repeater => '转发节点'; @override - String get map_room => '房间'; + String get map_room => '房间'; @override - String get map_sensor => '传感器'; + String get map_sensor => '传感器'; @override - String get map_pinDm => '标记(私信)'; + String get map_pinDm => '标记(私信)'; @override - String get map_pinPrivate => '私有'; + String get map_pinPrivate => '私有'; @override - String get map_pinPublic => '公共'; + String get map_pinPublic => '公共'; @override - String get map_lastSeen => '最后在线'; + String get map_lastSeen => '最后在线'; @override - String get map_disconnectConfirm => '确定要断开与此设备的连接吗?'; + String get map_disconnectConfirm => + '确定要断开与此设备的连接吗?'; @override - String get map_from => '来自'; + String get map_from => '来自'; @override - String get map_source => '来源'; + String get map_source => '来源'; @override - String get map_flags => '标志'; + String get map_flags => '标志'; @override - String get map_shareMarkerHere => '在此分享标记'; + String get map_shareMarkerHere => '在此分享标记'; @override - String get map_pinLabel => '标签'; + String get map_pinLabel => '标签'; @override - String get map_label => '标签'; + String get map_label => '标签'; @override - String get map_pointOfInterest => '兴趣点'; + String get map_pointOfInterest => '兴趣点'; @override - String get map_sendToContact => '发送给联系人'; + String get map_sendToContact => '发送给联系人'; @override - String get map_sendToChannel => '发送到频道'; + String get map_sendToChannel => '发送到频道'; @override - String get map_noChannelsAvailable => '没有可用的频道'; + String get map_noChannelsAvailable => '没有可用的频道'; @override - String get map_publicLocationShare => '公共位置共享'; + String get map_publicLocationShare => '公共位置共享'; @override String map_publicLocationShareConfirm(String channelLabel) { - return '您即将在 $channelLabel 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。'; + return '您即将在 $channelLabel 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。'; } @override - String get map_connectToShareMarkers => '连接设备以共享标记'; + String get map_connectToShareMarkers => '连接设备以共享标记'; @override - String get map_filterNodes => '过滤节点'; + String get map_filterNodes => '过滤节点'; @override - String get map_nodeTypes => '节点类型'; + String get map_nodeTypes => '节点类型'; @override - String get map_chatNodes => '聊天节点'; + String get map_chatNodes => '聊天节点'; @override - String get map_repeaters => '转发节点'; + String get map_repeaters => '转发节点'; @override - String get map_otherNodes => '其他节点'; + String get map_otherNodes => '其他节点'; @override - String get map_keyPrefix => '关键字前缀'; + String get map_keyPrefix => '关键字前缀'; @override - String get map_filterByKeyPrefix => '按关键字前缀筛选'; + String get map_filterByKeyPrefix => '按关键字前缀筛选'; @override - String get map_publicKeyPrefix => '关键字前缀'; + String get map_publicKeyPrefix => '关键字前缀'; @override - String get map_markers => '标记'; + String get map_markers => '标记'; @override - String get map_showSharedMarkers => '显示共享标记'; + String get map_showSharedMarkers => '显示共享标记'; @override - String get map_lastSeenTime => '最后在线时间'; + String get map_lastSeenTime => '最后在线时间'; @override - String get map_sharedPin => '共享标记'; + String get map_sharedPin => '共享标记'; @override - String get map_joinRoom => '加入房间'; + String get map_joinRoom => '加入房间'; @override - String get map_manageRepeater => '管理转发节点'; + String get map_manageRepeater => '管理转发节点'; @override - String get map_tapToAdd => '点击节点以添加到路径'; + String get map_tapToAdd => '点击节点以添加到路径'; @override - String get map_runTrace => '运行路径追踪'; + String get map_runTrace => '运行路径追踪'; @override - String get map_removeLast => '移除最后一个'; + String get map_removeLast => '移除最后一个'; @override - String get map_pathTraceCancelled => '路径追踪已取消'; + String get map_pathTraceCancelled => '路径追踪已取消'; @override - String get mapCache_title => '离线地图缓存'; + String get mapCache_title => '离线地图缓存'; @override - String get mapCache_selectAreaFirst => '请先选择要缓存的区域'; + String get mapCache_selectAreaFirst => '请先选择要缓存的区域'; @override - String get mapCache_noTilesToDownload => '此区域没有可下载的瓦片'; + String get mapCache_noTilesToDownload => '此区域没有可下载的瓦片'; @override - String get mapCache_downloadTilesTitle => '下载瓦片'; + String get mapCache_downloadTilesTitle => '下载瓦片'; @override String mapCache_downloadTilesPrompt(int count) { - return '这需要下载 $count 个瓦片'; + return '这需要下载 $count 个瓦片'; } @override - String get mapCache_downloadAction => '下载'; + String get mapCache_downloadAction => '下载'; @override String mapCache_cachedTiles(int count) { - return '已缓存 $count 个瓦片'; + return '已缓存 $count 个瓦片'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return '已缓存 $downloaded 个瓦片($failed 个失败)'; + return '已缓存 $downloaded 个瓦片($failed 个失败)'; } @override - String get mapCache_clearOfflineCacheTitle => '清除离线缓存'; + String get mapCache_clearOfflineCacheTitle => '清除离线缓存'; @override - String get mapCache_clearOfflineCachePrompt => '清除所有缓存的地图瓦片'; + String get mapCache_clearOfflineCachePrompt => + '清除所有缓存的地图瓦片'; @override - String get mapCache_offlineCacheCleared => '离线缓存已清除'; + String get mapCache_offlineCacheCleared => '离线缓存已清除'; @override - String get mapCache_noAreaSelected => '未选择区域'; + String get mapCache_noAreaSelected => '未选择区域'; @override - String get mapCache_cacheArea => '缓存区域'; + String get mapCache_cacheArea => '缓存区域'; @override - String get mapCache_useCurrentView => '使用当前视图'; + String get mapCache_useCurrentView => '使用当前视图'; @override - String get mapCache_zoomRange => '缩放范围'; + String get mapCache_zoomRange => '缩放范围'; @override String mapCache_estimatedTiles(int count) { - return '估计瓦片数:$count'; + return '估计瓦片数:$count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return '已下载 $completed/$total'; + return '已下载 $completed/$total'; } @override - String get mapCache_downloadTilesButton => '下载瓦片'; + String get mapCache_downloadTilesButton => '下载瓦片'; @override - String get mapCache_clearCacheButton => '清除缓存'; + String get mapCache_clearCacheButton => '清除缓存'; @override String mapCache_failedDownloads(int count) { - return '下载失败:$count'; + return '下载失败:$count'; } @override @@ -1484,284 +1523,296 @@ class AppLocalizationsZh extends AppLocalizations { String east, String west, ) { - return '北 $north, 南 $south, 东 $east, 西 $west'; + return '北 $north, 南 $south, 东 $east, 西 $west'; } @override - String get time_justNow => '刚才'; + String get time_justNow => '刚才'; @override String time_minutesAgo(int minutes) { - return '$minutes分钟前'; + return '$minutes分钟前'; } @override String time_hoursAgo(int hours) { - return '$hours小时前'; + return '$hours小时前'; } @override String time_daysAgo(int days) { - return '$days天前'; + return '$days天前'; } @override - String get time_hour => '小时'; + String get time_hour => '小时'; @override - String get time_hours => '小时'; + String get time_hours => '小时'; @override - String get time_day => '天'; + String get time_day => '天'; @override - String get time_days => '天'; + String get time_days => '天'; @override - String get time_week => '周'; + String get time_week => '周'; @override - String get time_weeks => '周'; + String get time_weeks => '周'; @override - String get time_month => '月'; + String get time_month => '月'; @override - String get time_months => '月'; + String get time_months => '月'; @override - String get time_minutes => '分钟'; + String get time_minutes => '分钟'; @override - String get time_allTime => '所有时间'; + String get time_allTime => '所有时间'; @override - String get dialog_disconnect => '断开'; + String get dialog_disconnect => 'æ–­å¼€'; @override - String get dialog_disconnectConfirm => '确定要断开与此设备的连接吗?'; + String get dialog_disconnectConfirm => + '确定要断开与此设备的连接吗?'; @override - String get login_repeaterLogin => '转发节点登录'; + String get login_repeaterLogin => '转发节点登录'; @override - String get login_roomLogin => '房间服务器登录'; + String get login_roomLogin => '房间服务器登录'; @override - String get login_password => '密码'; + String get login_password => '密码'; @override - String get login_enterPassword => '请输入密码'; + String get login_enterPassword => '请输入密码'; @override - String get login_savePassword => '保存密码'; + String get login_savePassword => '保存密码'; @override - String get login_savePasswordSubtitle => '密码将安全地存储在此设备上'; + String get login_savePasswordSubtitle => + '密码将安全地存储在此设备上'; @override - String get login_repeaterDescription => '输入转发节点密码以访问设置和状态。'; + String get login_repeaterDescription => + '输入转发节点密码以访问设置和状态。'; @override - String get login_roomDescription => '输入房间服务器密码以访问设置和状态。'; + String get login_roomDescription => + '输入房间服务器密码以访问设置和状态。'; @override - String get login_routing => '路由'; + String get login_routing => '路由'; @override - String get login_routingMode => '路由模式'; + String get login_routingMode => '路由模式'; @override - String get login_autoUseSavedPath => '自动(使用保存的路径)'; + String get login_autoUseSavedPath => '自动(使用保存的路径)'; @override - String get login_forceFloodMode => '强制泛洪模式'; + String get login_forceFloodMode => '强制泛洪模式'; @override - String get login_managePaths => '管理路径'; + String get login_managePaths => '管理路径'; @override - String get login_login => '登录'; + String get login_login => '登录'; @override String login_attempt(int current, int max) { - return '尝试 $current/$max'; + return '尝试 $current/$max'; } @override String login_failed(String error) { - return '登录失败:$error'; + return '登录失败:$error'; } @override - String get login_failedMessage => '登录失败。可能是密码错误或无法连接到服务器。'; + String get login_failedMessage => + '登录失败。可能是密码错误或无法连接到服务器。'; @override - String get common_reload => '重新加载'; + String get common_reload => '重新加载'; @override - String get common_clear => '清除'; + String get common_clear => '清除'; @override String path_currentPath(String path) { - return '当前路径:$path'; + return '当前路径:$path'; } @override String path_usingHopsPath(int count) { - return '使用 $count 跳路径'; + return '使用 $count 跳路径'; } @override - String get path_enterCustomPath => '输入自定义路径'; + String get path_enterCustomPath => '输入自定义路径'; @override - String get path_currentPathLabel => '当前路径'; + String get path_currentPathLabel => '当前路径'; @override - String get path_hexPrefixInstructions => '请输入每个中继节点的2字符十六进制前缀,用逗号分隔。'; + String get path_hexPrefixInstructions => + '请输入每个中继节点的2字符十六进制前缀,用逗号分隔。'; @override - String get path_hexPrefixExample => '例如:A1, F2, 3C(每个节点使用其公钥的第一字节)'; + String get path_hexPrefixExample => + '例如:A1, F2, 3C(每个节点使用其公钥的第一字节)'; @override - String get path_labelHexPrefixes => '路径(十六进制前缀)'; + String get path_labelHexPrefixes => '路径(十六进制前缀)'; @override - String get path_helperMaxHops => '最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。'; + String get path_helperMaxHops => + '最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。'; @override - String get path_selectFromContacts => '或从联系人列表中选择:'; + String get path_selectFromContacts => '或从联系人列表中选择:'; @override - String get path_noRepeatersFound => '未找到任何转发节点或房间服务器。'; + String get path_noRepeatersFound => + '未找到任何转发节点或房间服务器。'; @override - String get path_customPathsRequire => '自定义路径需要中间节点转发消息。'; + String get path_customPathsRequire => + '自定义路径需要中间节点转发消息。'; @override String path_invalidHexPrefixes(String prefixes) { - return '无效的十六进制前缀:$prefixes'; + return '无效的十六进制前缀:$prefixes'; } @override - String get path_tooLong => '路径过长,最多允许 64 跳。'; + String get path_tooLong => '路径过长,最多允许 64 跳。'; @override - String get path_setPath => '设置路径'; + String get path_setPath => '设置路径'; @override - String get repeater_management => '转发节点管理'; + String get repeater_management => '转发节点管理'; @override - String get room_management => '房间服务器管理'; + String get room_management => '房间服务器管理'; @override - String get repeater_managementTools => '管理工具'; + String get repeater_managementTools => '管理工具'; @override - String get repeater_status => '状态'; + String get repeater_status => '状态'; @override - String get repeater_statusSubtitle => '查看转发节点状态、统计和邻居'; + String get repeater_statusSubtitle => + '查看转发节点状态、统计和邻居'; @override - String get repeater_telemetry => '遥测'; + String get repeater_telemetry => '遥测'; @override - String get repeater_telemetrySubtitle => '查看传感器和系统状态数据'; + String get repeater_telemetrySubtitle => + '查看传感器和系统状态数据'; @override - String get repeater_cli => '命令行'; + String get repeater_cli => '命令行'; @override - String get repeater_cliSubtitle => '向转发节点发送命令'; + String get repeater_cliSubtitle => '向转发节点发送命令'; @override - String get repeater_neighbors => '邻居'; + String get repeater_neighbors => '邻居'; @override - String get repeater_neighborsSubtitle => '查看邻居节点(零跳)'; + String get repeater_neighborsSubtitle => '查看邻居节点(零跳)'; @override - String get repeater_settings => '设置'; + String get repeater_settings => '设置'; @override - String get repeater_settingsSubtitle => '配置转发节点参数'; + String get repeater_settingsSubtitle => '配置转发节点参数'; @override - String get repeater_statusTitle => '转发节点状态'; + String get repeater_statusTitle => '转发节点状态'; @override - String get repeater_routingMode => '路由模式'; + String get repeater_routingMode => '路由模式'; @override - String get repeater_autoUseSavedPath => '自动(使用保存的路径)'; + String get repeater_autoUseSavedPath => '自动(使用保存的路径)'; @override - String get repeater_forceFloodMode => '强制泛洪模式'; + String get repeater_forceFloodMode => '强制泛洪模式'; @override - String get repeater_pathManagement => '路径管理'; + String get repeater_pathManagement => '路径管理'; @override - String get repeater_refresh => '刷新'; + String get repeater_refresh => '刷新'; @override - String get repeater_statusRequestTimeout => '状态请求超时'; + String get repeater_statusRequestTimeout => '状态请求超时'; @override String repeater_errorLoadingStatus(String error) { - return '加载状态时出错:$error'; + return '加载状态时出错:$error'; } @override - String get repeater_systemInformation => '系统信息'; + String get repeater_systemInformation => '系统信息'; @override - String get repeater_battery => '电池'; + String get repeater_battery => '电池'; @override - String get repeater_clockAtLogin => '登录时的时钟'; + String get repeater_clockAtLogin => '登录时的时钟'; @override - String get repeater_uptime => '运行时间'; + String get repeater_uptime => '运行时间'; @override - String get repeater_queueLength => '队列长度'; + String get repeater_queueLength => '队列长度'; @override - String get repeater_debugFlags => '调试标志'; + String get repeater_debugFlags => '调试标志'; @override - String get repeater_radioStatistics => '无线电统计'; + String get repeater_radioStatistics => '无线电统计'; @override - String get repeater_lastRssi => '上次 RSSI'; + String get repeater_lastRssi => '上次 RSSI'; @override - String get repeater_lastSnr => '上次 SNR'; + String get repeater_lastSnr => '上次 SNR'; @override - String get repeater_noiseFloor => '底噪'; + String get repeater_noiseFloor => '底噪'; @override - String get repeater_txAirtime => '发送空中时间'; + String get repeater_txAirtime => '发送空中时间'; @override - String get repeater_rxAirtime => '接收空中时间'; + String get repeater_rxAirtime => '接收空中时间'; @override - String get repeater_packetStatistics => '数据包统计'; + String get repeater_packetStatistics => '数据包统计'; @override - String get repeater_sent => '发送'; + String get repeater_sent => '发送'; @override - String get repeater_received => '接收'; + String get repeater_received => '接收'; @override - String get repeater_duplicates => '重复'; + String get repeater_duplicates => '重复'; @override String repeater_daysHoursMinsSecs( @@ -1770,511 +1821,550 @@ class AppLocalizationsZh extends AppLocalizations { int minutes, int seconds, ) { - return '$days天 $hours小时 $minutes分 $seconds秒'; + return '$days天 $hours小时 $minutes分 $secondsç§’'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return '总计:$total,泛洪:$flood,直连:$direct'; + return '总计:$total,泛洪:$flood,直连:$direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return '总计:$total,泛洪:$flood,直连:$direct'; + return '总计:$total,泛洪:$flood,直连:$direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return '泛洪:$flood,直连:$direct'; + return '泛洪:$flood,直连:$direct'; } @override String repeater_duplicatesTotal(int total) { - return '总计:$total'; + return '总计:$total'; } @override - String get repeater_settingsTitle => '转发节点设置'; + String get repeater_settingsTitle => '转发节点设置'; @override - String get repeater_basicSettings => '基本设置'; + String get repeater_basicSettings => '基本设置'; @override - String get repeater_repeaterName => '转发节点名称'; + String get repeater_repeaterName => '转发节点名称'; @override - String get repeater_repeaterNameHelper => '此转发节点的显示名称'; + String get repeater_repeaterNameHelper => '此转发节点的显示名称'; @override - String get repeater_adminPassword => '管理员密码'; + String get repeater_adminPassword => '管理员密码'; @override - String get repeater_adminPasswordHelper => '完整访问密码'; + String get repeater_adminPasswordHelper => '完整访问密码'; @override - String get repeater_guestPassword => '访客密码'; + String get repeater_guestPassword => '访客密码'; @override - String get repeater_guestPasswordHelper => '只读访问密码'; + String get repeater_guestPasswordHelper => '只读访问密码'; @override - String get repeater_radioSettings => '无线电设置'; + String get repeater_radioSettings => '无线电设置'; @override - String get repeater_frequencyMhz => '频率 (MHz)'; + String get repeater_frequencyMhz => '频率 (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX 功率'; + String get repeater_txPower => 'TX 功率'; @override String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => '带宽'; + String get repeater_bandwidth => '带宽'; @override - String get repeater_spreadingFactor => '扩频因子'; + String get repeater_spreadingFactor => '扩频因子'; @override - String get repeater_codingRate => '编码速率'; + String get repeater_codingRate => '编码速率'; @override - String get repeater_locationSettings => '位置设置'; + String get repeater_locationSettings => '位置设置'; @override - String get repeater_latitude => '纬度'; + String get repeater_latitude => '纬度'; @override - String get repeater_latitudeHelper => '十进制,例如 37.7749'; + String get repeater_latitudeHelper => '十进制,例如 37.7749'; @override - String get repeater_longitude => '经度'; + String get repeater_longitude => '经度'; @override - String get repeater_longitudeHelper => '十进制,例如 -122.4194'; + String get repeater_longitudeHelper => '十进制,例如 -122.4194'; @override - String get repeater_features => '功能'; + String get repeater_features => '功能'; @override - String get repeater_packetForwarding => '数据包转发'; + String get repeater_packetForwarding => '数据包转发'; @override - String get repeater_packetForwardingSubtitle => '启用转发节点转发数据包'; + String get repeater_packetForwardingSubtitle => + '启用转发节点转发数据包'; @override - String get repeater_guestAccess => '访客访问'; + String get repeater_guestAccess => '访客访问'; @override - String get repeater_guestAccessSubtitle => '允许访客只读权限'; + String get repeater_guestAccessSubtitle => '允许访客只读权限'; @override - String get repeater_privacyMode => '隐私模式'; + String get repeater_privacyMode => '隐私模式'; @override - String get repeater_privacyModeSubtitle => '在广播中隐藏姓名/位置'; + String get repeater_privacyModeSubtitle => '在广播中隐藏姓名/位置'; @override - String get repeater_advertisementSettings => '广播设置'; + String get repeater_advertisementSettings => '广播设置'; @override - String get repeater_localAdvertInterval => '本地广播间隔'; + String get repeater_localAdvertInterval => '本地广播间隔'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes 分钟'; + return '$minutes 分钟'; } @override - String get repeater_floodAdvertInterval => '泛洪广播间隔'; + String get repeater_floodAdvertInterval => '泛洪广播间隔'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours 小时'; + return '$hours 小时'; } @override - String get repeater_encryptedAdvertInterval => '加密广播间隔'; + String get repeater_encryptedAdvertInterval => '加密广播间隔'; @override - String get repeater_dangerZone => '危险设置'; + String get repeater_dangerZone => '危险设置'; @override - String get repeater_rebootRepeater => '重启转发节点'; + String get repeater_rebootRepeater => '重启转发节点'; @override - String get repeater_rebootRepeaterSubtitle => '重启转发节点设备'; + String get repeater_rebootRepeaterSubtitle => '重启转发节点设备'; @override - String get repeater_rebootRepeaterConfirm => '确定要重启此转发节点吗?'; + String get repeater_rebootRepeaterConfirm => + '确定要重启此转发节点吗?'; @override - String get repeater_regenerateIdentityKey => '重新生成身份密钥'; + String get repeater_regenerateIdentityKey => '重新生成身份密钥'; @override - String get repeater_regenerateIdentityKeySubtitle => '生成新的公钥/私钥对'; + String get repeater_regenerateIdentityKeySubtitle => + '生成新的公钥/私钥对'; @override - String get repeater_regenerateIdentityKeyConfirm => '这将为转发节点生成新身份,继续吗?'; + String get repeater_regenerateIdentityKeyConfirm => + '这将为转发节点生成新身份,继续吗?'; @override - String get repeater_eraseFileSystem => '擦除文件系统'; + String get repeater_eraseFileSystem => '擦除文件系统'; @override - String get repeater_eraseFileSystemSubtitle => '格式化转发节点文件系统'; + String get repeater_eraseFileSystemSubtitle => + '格式化转发节点文件系统'; @override - String get repeater_eraseFileSystemConfirm => '警告:此操作将清除转发节点上的所有数据,且无法恢复!'; + String get repeater_eraseFileSystemConfirm => + '警告:此操作将清除转发节点上的所有数据,且无法恢复!'; @override - String get repeater_eraseSerialOnly => '擦除功能仅可通过串行控制台使用。'; + String get repeater_eraseSerialOnly => + '擦除功能仅可通过串行控制台使用。'; @override String repeater_commandSent(String command) { - return '命令已发送:$command'; + return '命令已发送:$command'; } @override String repeater_errorSendingCommand(String error) { - return '发送命令时出错:$error'; + return '发送命令时出错:$error'; } @override - String get repeater_confirm => '确认'; + String get repeater_confirm => '确认'; @override - String get repeater_settingsSaved => '设置保存成功'; + String get repeater_settingsSaved => '设置保存成功'; @override String repeater_errorSavingSettings(String error) { - return '保存设置时出错:$error'; + return '保存设置时出错:$error'; } @override - String get repeater_refreshBasicSettings => '刷新基本设置'; + String get repeater_refreshBasicSettings => '刷新基本设置'; @override - String get repeater_refreshRadioSettings => '刷新无线电设置'; + String get repeater_refreshRadioSettings => '刷新无线电设置'; @override - String get repeater_refreshTxPower => '刷新 TX 功率'; + String get repeater_refreshTxPower => '刷新 TX 功率'; @override - String get repeater_refreshLocationSettings => '刷新位置设置'; + String get repeater_refreshLocationSettings => '刷新位置设置'; @override - String get repeater_refreshPacketForwarding => '刷新包转发'; + String get repeater_refreshPacketForwarding => '刷新包转发'; @override - String get repeater_refreshGuestAccess => '刷新访客权限'; + String get repeater_refreshGuestAccess => '刷新访客权限'; @override - String get repeater_refreshPrivacyMode => '刷新隐私模式'; + String get repeater_refreshPrivacyMode => '刷新隐私模式'; @override - String get repeater_refreshAdvertisementSettings => '刷新广播设置'; + String get repeater_refreshAdvertisementSettings => '刷新广播设置'; @override String repeater_refreshed(String label) { - return '$label 已刷新'; + return '$label 已刷新'; } @override String repeater_errorRefreshing(String label) { - return '刷新 $label 时出错'; + return '刷新 $label 时出错'; } @override - String get repeater_cliTitle => '转发节点命令行'; + String get repeater_cliTitle => '转发节点命令行'; @override - String get repeater_debugNextCommand => '调试下一条命令'; + String get repeater_debugNextCommand => '调试下一条命令'; @override - String get repeater_commandHelp => '帮助'; + String get repeater_commandHelp => '帮助'; @override - String get repeater_clearHistory => '清除历史'; + String get repeater_clearHistory => '清除历史'; @override - String get repeater_noCommandsSent => '尚未发送命令'; + String get repeater_noCommandsSent => '尚未发送命令'; @override - String get repeater_typeCommandOrUseQuick => '输入命令或使用快捷命令'; + String get repeater_typeCommandOrUseQuick => + '输入命令或使用快捷命令'; @override - String get repeater_enterCommandHint => '输入命令...'; + String get repeater_enterCommandHint => '输入命令...'; @override - String get repeater_previousCommand => '上一条命令'; + String get repeater_previousCommand => '上一条命令'; @override - String get repeater_nextCommand => '下一条命令'; + String get repeater_nextCommand => '下一条命令'; @override - String get repeater_enterCommandFirst => '请先输入命令'; + String get repeater_enterCommandFirst => '请先输入命令'; @override - String get repeater_cliCommandFrameTitle => 'CLI 命令帧'; + String get repeater_cliCommandFrameTitle => 'CLI 命令帧'; @override String repeater_cliCommandError(String error) { - return '错误:$error'; + return '错误:$error'; } @override - String get repeater_cliQuickGetName => '获取名称'; + String get repeater_cliQuickGetName => '获取名称'; @override - String get repeater_cliQuickGetRadio => '获取无线电设置'; + String get repeater_cliQuickGetRadio => '获取无线电设置'; @override - String get repeater_cliQuickGetTx => '获取 TX'; + String get repeater_cliQuickGetTx => '获取 TX'; @override - String get repeater_cliQuickNeighbors => '邻居'; + String get repeater_cliQuickNeighbors => '邻居'; @override - String get repeater_cliQuickVersion => '版本'; + String get repeater_cliQuickVersion => '版本'; @override - String get repeater_cliQuickAdvertise => '发送广播'; + String get repeater_cliQuickAdvertise => '发送广播'; @override - String get repeater_cliQuickClock => '时钟'; + String get repeater_cliQuickClock => 'æ—¶é’Ÿ'; @override - String get repeater_cliHelpAdvert => '发送广播包'; + String get repeater_cliHelpAdvert => '发送广播包'; @override - String get repeater_cliHelpReboot => '重启设备。(注意:可能会收到超时错误,属于正常现象)'; + String get repeater_cliHelpReboot => + '重启设备。(注意:可能会收到超时错误,属于正常现象)'; @override - String get repeater_cliHelpClock => '显示设备当前时间'; + String get repeater_cliHelpClock => '显示设备当前时间'; @override - String get repeater_cliHelpPassword => '设置新的管理员密码'; + String get repeater_cliHelpPassword => '设置新的管理员密码'; @override - String get repeater_cliHelpVersion => '显示设备版本和固件构建日期'; + String get repeater_cliHelpVersion => + '显示设备版本和固件构建日期'; @override - String get repeater_cliHelpClearStats => '重置各种统计数据'; + String get repeater_cliHelpClearStats => '重置各种统计数据'; @override - String get repeater_cliHelpSetAf => '设置时间因子'; + String get repeater_cliHelpSetAf => '设置时间因子'; @override - String get repeater_cliHelpSetTx => '设置 LoRa 发射功率 (dBm)(重启生效)'; + String get repeater_cliHelpSetTx => + '设置 LoRa 发射功率 (dBm)(重启生效)'; @override - String get repeater_cliHelpSetRepeat => '启用或禁用此节点的转发功能'; + String get repeater_cliHelpSetRepeat => + '启用或禁用此节点的转发功能'; @override String get repeater_cliHelpSetAllowReadOnly => - '(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)'; + '(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)'; @override - String get repeater_cliHelpSetFloodMax => '设置最大传入数据包跳数(≥该值则不转发)'; + String get repeater_cliHelpSetFloodMax => + '设置最大传入数据包跳数(≥该值则不转发)'; @override - String get repeater_cliHelpSetIntThresh => '设置干扰阈值 (dB),默认14,设为0禁用'; + String get repeater_cliHelpSetIntThresh => + '设置干扰阈值 (dB),默认14,设为0禁用'; @override - String get repeater_cliHelpSetAgcResetInterval => '设置 AGC 重置间隔(秒),设为0禁用'; + String get repeater_cliHelpSetAgcResetInterval => + '设置 AGC 重置间隔(秒),设为0禁用'; @override - String get repeater_cliHelpSetMultiAcks => '启用或禁用“多重确认”功能'; + String get repeater_cliHelpSetMultiAcks => + '启用或禁用“多重确认”功能'; @override - String get repeater_cliHelpSetAdvertInterval => '设置本地广播间隔(分钟),设为0禁用'; + String get repeater_cliHelpSetAdvertInterval => + '设置本地广播间隔(分钟),设为0禁用'; @override - String get repeater_cliHelpSetFloodAdvertInterval => '设置泛洪广播间隔(小时),设为0禁用'; + String get repeater_cliHelpSetFloodAdvertInterval => + '设置泛洪广播间隔(小时),设为0禁用'; @override - String get repeater_cliHelpSetGuestPassword => '设置/更新访客密码'; + String get repeater_cliHelpSetGuestPassword => '设置/更新访客密码'; @override - String get repeater_cliHelpSetName => '设置广播名称'; + String get repeater_cliHelpSetName => '设置广播名称'; @override - String get repeater_cliHelpSetLat => '设置广播纬度(十进制)'; + String get repeater_cliHelpSetLat => '设置广播纬度(十进制)'; @override - String get repeater_cliHelpSetLon => '设置广播经度(十进制)'; + String get repeater_cliHelpSetLon => '设置广播经度(十进制)'; @override - String get repeater_cliHelpSetRadio => '完全重设无线电参数并保存,需重启生效'; + String get repeater_cliHelpSetRadio => + '完全重设无线电参数并保存,需重启生效'; @override - String get repeater_cliHelpSetRxDelay => '(实验性)设置接收延迟基数,设为0禁用'; + String get repeater_cliHelpSetRxDelay => + '(实验性)设置接收延迟基数,设为0禁用'; @override - String get repeater_cliHelpSetTxDelay => '通过因子和随机时隙延迟泛洪数据包转发,降低冲突'; + String get repeater_cliHelpSetTxDelay => + '通过因子和随机时隙延迟泛洪数据包转发,降低冲突'; @override - String get repeater_cliHelpSetDirectTxDelay => '同 txdelay,用于直连模式数据包'; + String get repeater_cliHelpSetDirectTxDelay => + '同 txdelay,用于直连模式数据包'; @override - String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥接'; + String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥接'; @override - String get repeater_cliHelpSetBridgeDelay => '设置桥接转发延迟'; + String get repeater_cliHelpSetBridgeDelay => '设置桥接转发延迟'; @override - String get repeater_cliHelpSetBridgeSource => '选择桥接器转发接收或发送的数据包'; + String get repeater_cliHelpSetBridgeSource => + '选择桥接器转发接收或发送的数据包'; @override - String get repeater_cliHelpSetBridgeBaud => '设置 RS232 桥接串口波特率'; + String get repeater_cliHelpSetBridgeBaud => + '设置 RS232 桥接串口波特率'; @override - String get repeater_cliHelpSetBridgeSecret => '设置 ESPNOW 桥接密钥'; + String get repeater_cliHelpSetBridgeSecret => '设置 ESPNOW 桥接密钥'; @override - String get repeater_cliHelpSetAdcMultiplier => '设置电池电压校正系数(特定板支持)'; + String get repeater_cliHelpSetAdcMultiplier => + '设置电池电压校正系数(特定板支持)'; @override - String get repeater_cliHelpTempRadio => '临时设置无线电参数指定分钟,之后恢复(不保存)'; + String get repeater_cliHelpTempRadio => + '临时设置无线电参数指定分钟,之后恢复(不保存)'; @override - String get repeater_cliHelpSetPerm => '修改 ACL,权限位:0访客、1只读、2读写、3管理员'; + String get repeater_cliHelpSetPerm => + '修改 ACL,权限位:0访客、1只读、2读写、3管理员'; @override - String get repeater_cliHelpGetBridgeType => '支持桥接模式:RS232、ESPNOW'; + String get repeater_cliHelpGetBridgeType => + '支持桥接模式:RS232、ESPNOW'; @override - String get repeater_cliHelpLogStart => '开始记录数据包到文件系统'; + String get repeater_cliHelpLogStart => '开始记录数据包到文件系统'; @override - String get repeater_cliHelpLogStop => '停止记录数据包'; + String get repeater_cliHelpLogStop => '停止记录数据包'; @override - String get repeater_cliHelpLogErase => '删除所有记录的数据包'; + String get repeater_cliHelpLogErase => '删除所有记录的数据包'; @override - String get repeater_cliHelpNeighbors => '显示零跳广播收到的其他转发节点列表'; + String get repeater_cliHelpNeighbors => + '显示零跳广播收到的其他转发节点列表'; @override - String get repeater_cliHelpNeighborRemove => '从邻居列表删除第一个匹配项(通过公钥前缀)'; + String get repeater_cliHelpNeighborRemove => + '从邻居列表删除第一个匹配项(通过公钥前缀)'; @override - String get repeater_cliHelpRegion => '(仅串口)列出所有定义区域及当前泛洪权限'; + String get repeater_cliHelpRegion => + '(仅串口)列出所有定义区域及当前泛洪权限'; @override - String get repeater_cliHelpRegionLoad => '特殊多命令调用,以空行结束'; + String get repeater_cliHelpRegionLoad => + '特殊多命令调用,以空行结束'; @override - String get repeater_cliHelpRegionGet => '搜索指定前缀的区域'; + String get repeater_cliHelpRegionGet => '搜索指定前缀的区域'; @override - String get repeater_cliHelpRegionPut => '添加或更新区域定义'; + String get repeater_cliHelpRegionPut => '添加或更新区域定义'; @override - String get repeater_cliHelpRegionRemove => '删除指定区域定义'; + String get repeater_cliHelpRegionRemove => '删除指定区域定义'; @override - String get repeater_cliHelpRegionAllowf => '为区域设置“泛洪”权限'; + String get repeater_cliHelpRegionAllowf => + '为区域设置“泛洪”权限'; @override - String get repeater_cliHelpRegionDenyf => '移除区域的“泛洪”权限'; + String get repeater_cliHelpRegionDenyf => '移除区域的“泛洪”权限'; @override - String get repeater_cliHelpRegionHome => '返回当前“主区域”(预留)'; + String get repeater_cliHelpRegionHome => + '返回当前“主区域”(预留)'; @override - String get repeater_cliHelpRegionHomeSet => '设置“主”区域'; + String get repeater_cliHelpRegionHomeSet => '设置“主”区域'; @override - String get repeater_cliHelpRegionSave => '保存区域列表到存储'; + String get repeater_cliHelpRegionSave => '保存区域列表到存储'; @override - String get repeater_cliHelpGps => '显示 GPS 状态'; + String get repeater_cliHelpGps => '显示 GPS 状态'; @override - String get repeater_cliHelpGpsOnOff => '切换 GPS 电源'; + String get repeater_cliHelpGpsOnOff => '切换 GPS 电源'; @override - String get repeater_cliHelpGpsSync => '将节点时间与 GPS 同步'; + String get repeater_cliHelpGpsSync => '将节点时间与 GPS 同步'; @override - String get repeater_cliHelpGpsSetLoc => '将节点坐标设为 GPS 坐标并保存'; + String get repeater_cliHelpGpsSetLoc => + '将节点坐标设为 GPS 坐标并保存'; @override - String get repeater_cliHelpGpsAdvert => '设置位置广播配置:none/share/prefs'; + String get repeater_cliHelpGpsAdvert => + '设置位置广播配置:none/share/prefs'; @override - String get repeater_cliHelpGpsAdvertSet => '设置广播位置配置'; + String get repeater_cliHelpGpsAdvertSet => '设置广播位置配置'; @override - String get repeater_commandsListTitle => '命令列表'; + String get repeater_commandsListTitle => '命令列表'; @override - String get repeater_commandsListNote => '注意:多数 set 命令也有对应的 get 命令'; + String get repeater_commandsListNote => + '注意:多数 set 命令也有对应的 get 命令'; @override - String get repeater_general => '通用'; + String get repeater_general => '通用'; @override - String get repeater_settingsCategory => '设置'; + String get repeater_settingsCategory => '设置'; @override - String get repeater_bridge => '桥接'; + String get repeater_bridge => '桥接'; @override - String get repeater_logging => '日志'; + String get repeater_logging => '日志'; @override - String get repeater_neighborsRepeaterOnly => '邻居(仅转发节点)'; + String get repeater_neighborsRepeaterOnly => '邻居(仅转发节点)'; @override - String get repeater_regionManagementRepeaterOnly => '区域管理(仅转发节点)'; + String get repeater_regionManagementRepeaterOnly => + '区域管理(仅转发节点)'; @override - String get repeater_regionNote => '区域命令用于管理区域定义和权限'; + String get repeater_regionNote => + '区域命令用于管理区域定义和权限'; @override - String get repeater_gpsManagement => 'GPS 管理'; + String get repeater_gpsManagement => 'GPS 管理'; @override - String get repeater_gpsNote => 'GPS 命令用于位置相关任务'; + String get repeater_gpsNote => 'GPS 命令用于位置相关任务'; @override - String get telemetry_receivedData => '接收到的遥测数据'; + String get telemetry_receivedData => '接收到的遥测数据'; @override - String get telemetry_requestTimeout => '遥测请求超时'; + String get telemetry_requestTimeout => '遥测请求超时'; @override String telemetry_errorLoading(String error) { - return '加载遥测数据时出错:$error'; + return '加载遥测数据时出错:$error'; } @override - String get telemetry_noData => '暂无遥测数据'; + String get telemetry_noData => '暂无遥测数据'; @override String telemetry_channelTitle(int channel) { - return '频道 $channel'; + return '频道 $channel'; } @override - String get telemetry_batteryLabel => '电池'; + String get telemetry_batteryLabel => '电池'; @override - String get telemetry_voltageLabel => '电压'; + String get telemetry_voltageLabel => '电压'; @override - String get telemetry_mcuTemperatureLabel => 'MCU 温度'; + String get telemetry_mcuTemperatureLabel => 'MCU 温度'; @override - String get telemetry_temperatureLabel => '温度'; + String get telemetry_temperatureLabel => '温度'; @override - String get telemetry_currentLabel => '电流'; + String get telemetry_currentLabel => '电流'; @override String telemetry_batteryValue(int percent, String volts) { @@ -2293,78 +2383,78 @@ class AppLocalizationsZh extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => '已接收邻居信息'; + String get neighbors_receivedData => '已接收邻居信息'; @override - String get neighbors_requestTimedOut => '邻居请求超时'; + String get neighbors_requestTimedOut => '邻居请求超时'; @override String neighbors_errorLoading(String error) { - return '加载邻居时出错:$error'; + return '加载邻居时出错:$error'; } @override - String get neighbors_repeatersNeighbors => '转发节点的邻居'; + String get neighbors_repeatersNeighbors => '转发节点的邻居'; @override - String get neighbors_noData => '暂无邻居信息'; + String get neighbors_noData => '暂无邻居信息'; @override String neighbors_unknownContact(String pubkey) { - return '未知 $pubkey'; + return '未知 $pubkey'; } @override String neighbors_heardAgo(String time) { - return '听到:$time前'; + return '听到:$time前'; } @override - String get channelPath_title => '数据包路径'; + String get channelPath_title => '数据包路径'; @override - String get channelPath_viewMap => '查看地图'; + String get channelPath_viewMap => '查看地图'; @override - String get channelPath_otherObservedPaths => '其他观察到的路径'; + String get channelPath_otherObservedPaths => '其他观察到的路径'; @override - String get channelPath_repeaterHops => '转发节点跳数'; + String get channelPath_repeaterHops => '转发节点跳数'; @override - String get channelPath_noHopDetails => '此数据包未提供详细信息'; + String get channelPath_noHopDetails => '此数据包未提供详细信息'; @override - String get channelPath_messageDetails => '消息详情'; + String get channelPath_messageDetails => '消息详情'; @override - String get channelPath_senderLabel => '发送者'; + String get channelPath_senderLabel => '发送者'; @override - String get channelPath_timeLabel => '时间'; + String get channelPath_timeLabel => 'æ—¶é—´'; @override - String get channelPath_repeatsLabel => '重复'; + String get channelPath_repeatsLabel => '重复'; @override String channelPath_pathLabel(int index) { - return '路径 $index'; + return '路径 $index'; } @override - String get channelPath_observedLabel => '观察到的'; + String get channelPath_observedLabel => '观察到的'; @override String channelPath_observedPathTitle(int index, String hops) { - return '观察到的路径 $index • $hops'; + return '观察到的路径 $index • $hops'; } @override - String get channelPath_noLocationData => '无位置信息'; + String get channelPath_noLocationData => '无位置信息'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2377,328 +2467,339 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channelPath_unknownPath => '未知'; + String get channelPath_unknownPath => '未知'; @override - String get channelPath_floodPath => '泛洪'; + String get channelPath_floodPath => '泛洪'; @override - String get channelPath_directPath => '直连'; + String get channelPath_directPath => '直连'; @override String channelPath_observedZeroOf(int total) { - return '0 / $total 跳'; + return '0 / $total è·³'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed / $total 跳'; + return '$observed / $total è·³'; } @override - String get channelPath_mapTitle => '路径地图'; + String get channelPath_mapTitle => '路径地图'; @override - String get channelPath_noRepeaterLocations => '此路径上没有可用的转发节点位置信息'; + String get channelPath_noRepeaterLocations => + '此路径上没有可用的转发节点位置信息'; @override String channelPath_primaryPath(int index) { - return '路径 $index(主要)'; + return '路径 $index(主要)'; } @override - String get channelPath_pathLabelTitle => '路径'; + String get channelPath_pathLabelTitle => '路径'; @override - String get channelPath_observedPathHeader => '观察到的路径'; + String get channelPath_observedPathHeader => '观察到的路径'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override - String get channelPath_noHopDetailsAvailable => '此数据包暂无详细信息'; + String get channelPath_noHopDetailsAvailable => + '此数据包暂无详细信息'; @override - String get channelPath_unknownRepeater => '未知转发节点'; + String get channelPath_unknownRepeater => '未知转发节点'; @override - String get community_title => '社区'; + String get community_title => '社区'; @override - String get community_create => '创建社区'; + String get community_create => '创建社区'; @override - String get community_createDesc => '创建新社区并通过二维码分享。'; + String get community_createDesc => + '创建新社区并通过二维码分享。'; @override - String get community_join => '加入'; + String get community_join => '加入'; @override - String get community_joinTitle => '加入社区'; + String get community_joinTitle => '加入社区'; @override String community_joinConfirmation(String name) { - return '是否加入社区 \"$name\"?'; + return '是否加入社区 \"$name\"?'; } @override - String get community_scanQr => '扫描社区二维码'; + String get community_scanQr => '扫描社区二维码'; @override - String get community_scanInstructions => '将摄像头对准社区的二维码'; + String get community_scanInstructions => + '将摄像头对准社区的二维码'; @override - String get community_showQr => '显示二维码'; + String get community_showQr => '显示二维码'; @override - String get community_publicChannel => '社区公共频道'; + String get community_publicChannel => '社区公共频道'; @override - String get community_hashtagChannel => '社区标签频道'; + String get community_hashtagChannel => '社区标签频道'; @override - String get community_name => '社区名称'; + String get community_name => '社区名称'; @override - String get community_enterName => '请输入社区名称'; + String get community_enterName => '请输入社区名称'; @override String community_created(String name) { - return '社区 \"$name\" 已创建'; + return '社区 \"$name\" 已创建'; } @override String community_joined(String name) { - return '已加入社区 \"$name\"'; + return '已加入社区 \"$name\"'; } @override - String get community_qrTitle => '分享社区'; + String get community_qrTitle => '分享社区'; @override String community_qrInstructions(String name) { - return '扫描此二维码加入 \"$name\"'; + return '扫描此二维码加入 \"$name\"'; } @override - String get community_hashtagPrivacyHint => '仅社区成员可加入社区标签频道。'; + String get community_hashtagPrivacyHint => + '仅社区成员可加入社区标签频道。'; @override - String get community_invalidQrCode => '无效的社区二维码'; + String get community_invalidQrCode => '无效的社区二维码'; @override - String get community_alreadyMember => '已是成员'; + String get community_alreadyMember => '已是成员'; @override String community_alreadyMemberMessage(String name) { - return '您已是 \"$name\" 的成员。'; + return '您已是 \"$name\" 的成员。'; } @override - String get community_addPublicChannel => '添加公共频道'; + String get community_addPublicChannel => '添加公共频道'; @override - String get community_addPublicChannelHint => '自动添加此社区的公共频道'; + String get community_addPublicChannelHint => + '自动添加此社区的公共频道'; @override - String get community_noCommunities => '尚未加入任何社区。'; + String get community_noCommunities => '尚未加入任何社区。'; @override - String get community_scanOrCreate => '扫描二维码或创建社区以开始。'; + String get community_scanOrCreate => + '扫描二维码或创建社区以开始。'; @override - String get community_manageCommunities => '管理社区'; + String get community_manageCommunities => '管理社区'; @override - String get community_delete => '退出社区'; + String get community_delete => '退出社区'; @override String community_deleteConfirm(String name) { - return '是否退出 \"$name\"?'; + return '是否退出 \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return '这将同时删除 $count 个频道及其所有消息。'; + return '这将同时删除 $count 个频道及其所有消息。'; } @override String community_deleted(String name) { - return '已退出社区 \"$name\"'; + return '已退出社区 \"$name\"'; } @override - String get community_regenerateSecret => '重新生成密钥'; + String get community_regenerateSecret => '重新生成密钥'; @override String community_regenerateSecretConfirm(String name) { - return '是否为 \"$name\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。'; + return '是否为 \"$name\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。'; } @override - String get community_regenerate => '重新生成'; + String get community_regenerate => '重新生成'; @override String community_secretRegenerated(String name) { - return '已为 \"$name\" 重新生成密钥'; + return '已为 \"$name\" 重新生成密钥'; } @override - String get community_updateSecret => '更新密钥'; + String get community_updateSecret => '更新密钥'; @override String community_secretUpdated(String name) { - return '“$name”的密钥已更新'; + return '“$name”的密钥已更新'; } @override String community_scanToUpdateSecret(String name) { - return '扫描新二维码以更新 \"$name\" 的密钥'; + return '扫描新二维码以更新 \"$name\" 的密钥'; } @override - String get community_addHashtagChannel => '添加标签频道'; + String get community_addHashtagChannel => '添加标签频道'; @override - String get community_addHashtagChannelDesc => '为此社区创建标签频道'; + String get community_addHashtagChannelDesc => + '为此社区创建标签频道'; @override - String get community_selectCommunity => '选择社区'; + String get community_selectCommunity => '选择社区'; @override - String get community_regularHashtag => '普通标签'; + String get community_regularHashtag => '普通标签'; @override - String get community_regularHashtagDesc => '公共标签频道(任何人都可参与)'; + String get community_regularHashtagDesc => + '公共标签频道(任何人都可参与)'; @override - String get community_communityHashtag => '社区标签'; + String get community_communityHashtag => '社区标签'; @override - String get community_communityHashtagDesc => '仅限社区成员'; + String get community_communityHashtagDesc => '仅限社区成员'; @override String community_forCommunity(String name) { - return '为 $name'; + return '为 $name'; } @override - String get listFilter_tooltip => '筛选与排序'; + String get listFilter_tooltip => '筛选与排序'; @override - String get listFilter_sortBy => '排序方式'; + String get listFilter_sortBy => '排序方式'; @override - String get listFilter_latestMessages => '最新消息'; + String get listFilter_latestMessages => '最新消息'; @override - String get listFilter_heardRecently => '最近听到'; + String get listFilter_heardRecently => '最近听到'; @override String get listFilter_az => 'A-Z'; @override - String get listFilter_filters => '筛选'; + String get listFilter_filters => '筛选'; @override - String get listFilter_all => '全部'; + String get listFilter_all => '全部'; @override - String get listFilter_favorites => '收藏'; + String get listFilter_favorites => '收藏'; @override - String get listFilter_addToFavorites => '添加到收藏'; + String get listFilter_addToFavorites => '添加到收藏'; @override - String get listFilter_removeFromFavorites => '从收藏中移除'; + String get listFilter_removeFromFavorites => '从收藏中移除'; @override - String get listFilter_users => '用户'; + String get listFilter_users => '用户'; @override - String get listFilter_repeaters => '转发节点'; + String get listFilter_repeaters => '转发节点'; @override - String get listFilter_roomServers => '房间服务器'; + String get listFilter_roomServers => '房间服务器'; @override - String get listFilter_unreadOnly => '仅显示未读'; + String get listFilter_unreadOnly => '仅显示未读'; @override - String get listFilter_newGroup => '新建群聊'; + String get listFilter_newGroup => '新建群聊'; @override - String get pathTrace_you => '我自己'; + String get pathTrace_you => '我自己'; @override - String get pathTrace_failed => '路径追踪失败。'; + String get pathTrace_failed => '路径追踪失败。'; @override - String get pathTrace_notAvailable => '无法获取路径信息。'; + String get pathTrace_notAvailable => '无法获取路径信息。'; @override - String get pathTrace_refreshTooltip => '刷新路径追踪'; + String get pathTrace_refreshTooltip => '刷新路径追踪'; @override - String get pathTrace_someHopsNoLocation => '某些跳缺少位置信息!'; + String get pathTrace_someHopsNoLocation => '某些跳缺少位置信息!'; @override - String get pathTrace_clearTooltip => '清除路径'; + String get pathTrace_clearTooltip => '清除路径'; @override - String get losSelectStartEnd => '选择 LOS 的起始节点和结束节点。'; + String get losSelectStartEnd => + '选择 LOS 的起始节点和结束节点。'; @override String losRunFailed(String error) { - return '视线检查失败:$error'; + return '视线检查失败:$error'; } @override - String get losClearAllPoints => '清除所有点'; + String get losClearAllPoints => '清除所有点'; @override - String get losRunToViewElevationProfile => '运行 LOS 查看高程剖面'; + String get losRunToViewElevationProfile => '运行 LOS 查看高程剖面'; @override - String get losMenuTitle => '服务水平菜单'; + String get losMenuTitle => '服务水平菜单'; @override - String get losMenuSubtitle => '点击节点或长按地图以获取自定义点'; + String get losMenuSubtitle => + '点击节点或长按地图以获取自定义点'; @override - String get losShowDisplayNodes => '显示显示节点'; + String get losShowDisplayNodes => '显示显示节点'; @override - String get losCustomPoints => '自定义积分'; + String get losCustomPoints => '自定义积分'; @override String losCustomPointLabel(int index) { - return '自定义 $index'; + return '自定义 $index'; } @override - String get losPointA => 'A点'; + String get losPointA => 'A点'; @override - String get losPointB => 'B点'; + String get losPointB => 'B点'; @override String losAntennaA(String value, String unit) { - return '天线 A: $value $unit'; + return '天线 A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return '天线 B:$value $unit'; + return '天线 B:$value $unit'; } @override - String get losRun => '运行视距'; + String get losRun => '运行视距'; @override - String get losNoElevationData => '无海拔数据'; + String get losNoElevationData => '无海拔数据'; @override String losProfileClear( @@ -2707,7 +2808,7 @@ class AppLocalizationsZh extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit,清除 LOS,最小间隙 $clearance $heightUnit'; + return '$distance $distanceUnit,清除 LOS,最小间隙 $clearance $heightUnit'; } @override @@ -2717,58 +2818,60 @@ class AppLocalizationsZh extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit,被 $obstruction $heightUnit 阻止'; + return '$distance $distanceUnit,被 $obstruction $heightUnit 阻止'; } @override - String get losStatusChecking => '洛斯:正在检查...'; + String get losStatusChecking => '洛斯:正在检查...'; @override - String get losStatusNoData => 'LOS:无数据'; + String get losStatusNoData => 'LOS:无数据'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS:$clear/$total 清除,$blocked 阻塞,$unknown 未知'; + return 'LOS:$clear/$total 清除,$blocked 阻塞,$unknown 未知'; } @override - String get losErrorElevationUnavailable => '一个或多个样本的海拔数据不可用。'; + String get losErrorElevationUnavailable => + '一个或多个样本的海拔数据不可用。'; @override - String get losErrorInvalidInput => '用于 LOS 计算的点/高程数据无效。'; + String get losErrorInvalidInput => + '用于 LOS 计算的点/高程数据无效。'; @override - String get losRenameCustomPoint => '重命名自定义点'; + String get losRenameCustomPoint => '重命名自定义点'; @override - String get losPointName => '点名称'; + String get losPointName => '点名称'; @override - String get losShowPanelTooltip => '显示 LOS 面板'; + String get losShowPanelTooltip => '显示 LOS 面板'; @override - String get losHidePanelTooltip => '隐藏 LOS 面板'; + String get losHidePanelTooltip => '隐藏 LOS 面板'; @override - String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; + String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => '无线电地平线'; + String get losLegendRadioHorizon => '无线电地平线'; @override - String get losLegendLosBeam => '视距波束'; + String get losLegendLosBeam => '视距波束'; @override - String get losLegendTerrain => '地形'; + String get losLegendTerrain => '地形'; @override - String get losFrequencyLabel => '频率'; + String get losFrequencyLabel => '频率'; @override - String get losFrequencyInfoTooltip => '查看计算详情'; + String get losFrequencyInfoTooltip => '查看计算详情'; @override - String get losFrequencyDialogTitle => '无线电地平线计算'; + String get losFrequencyDialogTitle => '无线电地平线计算'; @override String losFrequencyDialogDescription( @@ -2777,151 +2880,161 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return '从 $baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; + return '从 $baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; } @override - String get contacts_pathTrace => '路径追踪'; + String get contacts_pathTrace => '路径追踪'; @override String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => 'Trace 转发节点'; + String get contacts_repeaterPathTrace => 'Trace 转发节点'; @override - String get contacts_repeaterPing => 'Ping 转发节点'; + String get contacts_repeaterPing => 'Ping 转发节点'; @override - String get contacts_roomPathTrace => 'Trace 房间服务器'; + String get contacts_roomPathTrace => 'Trace 房间服务器'; @override - String get contacts_roomPing => 'Ping 房间服务器'; + String get contacts_roomPing => 'Ping 房间服务器'; @override - String get contacts_chatTraceRoute => '路由追踪'; + String get contacts_chatTraceRoute => '路由追踪'; @override String contacts_pathTraceTo(String name) { - return '追踪至 $name 的路径'; + return '追踪至 $name 的路径'; } @override - String get contacts_clipboardEmpty => '剪贴板为空'; + String get contacts_clipboardEmpty => '剪贴板为空'; @override - String get contacts_invalidAdvertFormat => '无效的联系人信息格式'; + String get contacts_invalidAdvertFormat => '无效的联系人信息格式'; @override - String get contacts_contactImported => '联系人已导入'; + String get contacts_contactImported => '联系人已导入'; @override - String get contacts_contactImportFailed => '导入联系人失败。'; + String get contacts_contactImportFailed => '导入联系人失败。'; @override - String get contacts_zeroHopAdvert => '发送零跳广播'; + String get contacts_zeroHopAdvert => '发送零跳广播'; @override - String get contacts_floodAdvert => '发送泛洪广播'; + String get contacts_floodAdvert => '发送泛洪广播'; @override - String get contacts_copyAdvertToClipboard => '复制广播到剪贴板'; + String get contacts_copyAdvertToClipboard => '复制广播到剪贴板'; @override - String get contacts_addContactFromClipboard => '从剪贴板添加联系人'; + String get contacts_addContactFromClipboard => '从剪贴板添加联系人'; @override - String get contacts_ShareContact => '复制联系人信息到剪贴板'; + String get contacts_ShareContact => '复制联系人信息到剪贴板'; @override - String get contacts_ShareContactZeroHop => '通过广播分享联系人'; + String get contacts_ShareContactZeroHop => '通过广播分享联系人'; @override - String get contacts_zeroHopContactAdvertSent => '零跳广播已发送'; + String get contacts_zeroHopContactAdvertSent => '零跳广播已发送'; @override - String get contacts_zeroHopContactAdvertFailed => '发送联系人广播失败。'; + String get contacts_zeroHopContactAdvertFailed => + '发送联系人广播失败。'; @override - String get contacts_contactAdvertCopied => '广播已复制到剪贴板。'; + String get contacts_contactAdvertCopied => '广播已复制到剪贴板。'; @override - String get contacts_contactAdvertCopyFailed => '复制广播到剪贴板失败。'; + String get contacts_contactAdvertCopyFailed => + '复制广播到剪贴板失败。'; @override - String get notification_activityTitle => 'MeshCore 活动'; + String get notification_activityTitle => 'MeshCore 活动'; @override String notification_messagesCount(int count) { - return '$count 条消息'; + return '$count 条消息'; } @override String notification_channelMessagesCount(int count) { - return '$count 条频道消息'; + return '$count 条频道消息'; } @override String notification_newNodesCount(int count) { - return '$count 个新节点'; + return '$count 个新节点'; } @override String notification_newTypeDiscovered(String contactType) { - return '发现新 $contactType'; + return '发现新 $contactType'; } @override - String get notification_receivedNewMessage => '收到新消息'; + String get notification_receivedNewMessage => '收到新消息'; @override - String get settings_gpxExportRepeaters => '导出转发节点/房间服务器到 GPX'; + String get settings_gpxExportRepeaters => + '导出转发节点/房间服务器到 GPX'; @override - String get settings_gpxExportRepeatersSubtitle => '导出带位置的转发节点/房间服务器到 GPX 文件'; + String get settings_gpxExportRepeatersSubtitle => + '导出带位置的转发节点/房间服务器到 GPX 文件'; @override - String get settings_gpxExportContacts => '导出伙伴到 GPX'; + String get settings_gpxExportContacts => '导出伙伴到 GPX'; @override - String get settings_gpxExportContactsSubtitle => '导出带位置的伙伴到 GPX 文件'; + String get settings_gpxExportContactsSubtitle => + '导出带位置的伙伴到 GPX 文件'; @override - String get settings_gpxExportAll => '导出所有联系人到 GPX'; + String get settings_gpxExportAll => '导出所有联系人到 GPX'; @override - String get settings_gpxExportAllSubtitle => '导出所有带位置的联系人到 GPX 文件'; + String get settings_gpxExportAllSubtitle => + '导出所有带位置的联系人到 GPX 文件'; @override - String get settings_gpxExportSuccess => 'GPX 文件导出成功'; + String get settings_gpxExportSuccess => 'GPX 文件导出成功'; @override - String get settings_gpxExportNoContacts => '没有可导出的联系人'; + String get settings_gpxExportNoContacts => '没有可导出的联系人'; @override - String get settings_gpxExportNotAvailable => '您的设备/操作系统不支持'; + String get settings_gpxExportNotAvailable => + '您的设备/操作系统不支持'; @override - String get settings_gpxExportError => '导出时出错'; + String get settings_gpxExportError => '导出时出错'; @override - String get settings_gpxExportRepeatersRoom => '转发节点与房间服务器位置'; + String get settings_gpxExportRepeatersRoom => + '转发节点与房间服务器位置'; @override - String get settings_gpxExportChat => '伙伴位置'; + String get settings_gpxExportChat => '伙伴位置'; @override - String get settings_gpxExportAllContacts => '所有联系人位置'; + String get settings_gpxExportAllContacts => '所有联系人位置'; @override - String get settings_gpxExportShareText => '来自 MeshCore Open 的地图数据导出'; + String get settings_gpxExportShareText => + '来自 MeshCore Open 的地图数据导出'; @override - String get settings_gpxExportShareSubject => 'MeshCore Open GPX 地图数据导出'; + String get settings_gpxExportShareSubject => + 'MeshCore Open GPX 地图数据导出'; @override - String get snrIndicator_nearByRepeaters => '附近的重复器'; + String get snrIndicator_nearByRepeaters => '附近的重复器'; @override - String get snrIndicator_lastSeen => '最近访问'; + String get snrIndicator_lastSeen => '最近访问'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 338a3a2..d325209 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", "@channels_channelDeleteFailed": { "placeholders": { @@ -27,7 +27,7 @@ "common_create": "Maak", "common_continue": "Doorgaan", "common_share": "Delen", - "common_copy": "Kopiëren", + "common_copy": "Kopiëren", "common_retry": "Nogmaals proberen", "common_hide": "Verbergen", "common_remove": "Verwijderen", @@ -35,7 +35,7 @@ "common_disable": "Uitschakelen", "common_reboot": "Herstarten", "common_loading": "Laden...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -92,7 +92,7 @@ "settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor", "settings_radioSettingsUpdated": "Radio instellingen bijgewerkt", "settings_location": "Locatie", - "settings_locationSubtitle": "GPS coördinaten", + "settings_locationSubtitle": "GPS coördinaten", "settings_locationUpdated": "Locatie bijgewerkt", "settings_locationBothRequired": "Voer zowel breedte- als lengtegraad in.", "settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.", @@ -165,18 +165,18 @@ "appSettings_language": "Taal", "appSettings_languageSystem": "Standaardinstelling", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notificaties", "appSettings_enableNotifications": "Notificaties inschakelen", "appSettings_enableNotificationsSubtitle": "Ontvang meldingen voor berichten en advertenties", @@ -338,7 +338,7 @@ }, "channels_hashtagChannel": "Hashtag kanaal", "channels_public": "Openbaar", - "channels_private": "Privé", + "channels_private": "Privé", "channels_publicChannel": "Open kanaal", "channels_privateChannel": "Private kanaal", "channels_editChannel": "Kanaal bewerken", @@ -623,7 +623,7 @@ "chat_invalidLink": "Ongeldig linkformaat", "map_title": "Node Map", "map_noNodesWithLocation": "Geen nodes met locatiegegevens", - "map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen", + "map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen", "map_nodesCount": "Nodes: {count}", "@map_nodesCount": { "placeholders": { @@ -645,7 +645,7 @@ "map_room": "Ruimte", "map_sensor": "Sensor", "map_pinDm": "Verzenden als bericht (DM)", - "map_pinPrivate": "Beveiligd (Privé)", + "map_pinPrivate": "Beveiligd (Privé)", "map_pinPublic": "Openbaar spikken", "map_lastSeen": "Laaste keer gezien", "map_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?", @@ -1039,7 +1039,7 @@ "repeater_eraseFileSystem": "Verwijder Besturingssysteem", "repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater", "repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!", - "repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.", + "repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.", "repeater_commandSent": "Commando verzonden: {command}", "@repeater_commandSent": { "placeholders": { @@ -1143,11 +1143,11 @@ "repeater_cliHelpSetBridgeEnabled": "Poort inschakelen/uitschakelen.", "repeater_cliHelpSetBridgeDelay": "Verzend vertraging instellen voor pakketten.", "repeater_cliHelpSetBridgeSource": "Kies of de brug ontvangen pakketten of verzonden pakketten opnieuw moet versturen.", - "repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.", + "repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.", "repeater_cliHelpSetBridgeSecret": "Stel bridge-geheim in voor espnow bridges.", "repeater_cliHelpSetAdcMultiplier": "Stelt een aangepaste factor in om de gerapporteerde batterijspanning aan te passen (alleen ondersteund op selecte borden).", "repeater_cliHelpTempRadio": "Stelt tijdelijke radio parameters in voor het opgegeven aantal minuten, en keert daarna terug naar de originele radio parameters. (wordt niet opgeslagen in de voorkeuren).", - "repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)", + "repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)", "repeater_cliHelpGetBridgeType": "Ontvang brugtype: geen, rs232, espnow", "repeater_cliHelpLogStart": "Start pakketlogging naar het bestandssysteem.", "repeater_cliHelpLogStop": "Stoppen met het loggen van pakketten naar het bestandssysteem.", @@ -1155,7 +1155,7 @@ "repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4", "repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.", "repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.", - "repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.", + "repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.", "repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"", "repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.", "repeater_cliHelpRegionRemove": "Verwijdert een regio-definitie met de gegeven naam. (moet exact overeenkomen en geen kindregio's hebben)", @@ -1167,7 +1167,7 @@ "repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.", "repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.", "repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.", - "repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.", + "repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.", "repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen", "repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.", "repeater_commandsListTitle": "Commandenlijst", @@ -1178,9 +1178,9 @@ "repeater_logging": "Logging", "repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)", "repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)", - "repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.", + "repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.", "repeater_gpsManagement": "Beheer GPS", - "repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.", + "repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.", "telemetry_receivedData": "Ontvangen Telemetriedata", "telemetry_requestTimeout": "Telemetryverzoek is uitgevallen.", "telemetry_errorLoading": "Fout bij het laden van de telemetrie: {error}", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1254,7 +1254,7 @@ "channelPath_repeatsLabel": "Repeats", "channelPath_pathLabel": "Pad {index}", "channelPath_observedLabel": "Waargenomen", - "channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}", + "channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Pad", "channelPath_observedPathHeader": "Waargenomen Pad", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1369,8 +1369,8 @@ "neighbors_repeatersNeighbors": "Herhalingen Buren", "neighbors_noData": "Geen gegevens van buren beschikbaar.", "channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.", - "channels_createPrivateChannel": "Maak een Privé Kanaal", - "channels_joinPrivateChannel": "Sluit een Privé Kanaal aan", + "channels_createPrivateChannel": "Maak een Privé Kanaal", + "channels_joinPrivateChannel": "Sluit een Privé Kanaal aan", "channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.", "channels_joinPublicChannel": "Sluit het Open Kanaal", "channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.", @@ -1558,22 +1558,22 @@ "contacts_roomPing": "Ping kamer server", "contacts_chatTraceRoute": "Route traceren", "contacts_pathTraceTo": "Trace route to {name}", - "appSettings_languageUk": "Oekraïens", + "appSettings_languageUk": "Oekraïens", "contacts_invalidAdvertFormat": "Ongeldige contactgegevens", - "contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.", + "contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.", "contacts_zeroHopAdvert": "Zero Hop Reclame", "contacts_floodAdvert": "Overstromingsadvertentie", - "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", + "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", "appSettings_languageRu": "Russisch", "appSettings_enableMessageTracing": "Berichttracking inschakelen", "appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven", "contacts_clipboardEmpty": "Knipbord is leeg.", "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", - "contacts_contactImported": "Contact is geïmporteerd.", + "contacts_contactImported": "Contact is geïmporteerd.", "contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie", "contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.", - "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", - "contacts_ShareContact": "Kontakt naar Klembord kopiëren", + "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", + "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", "notification_activityTitle": "MeshCore Activiteit", @@ -1584,7 +1584,7 @@ "notification_receivedNewMessage": "Nieuw bericht ontvangen", "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", - "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", + "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", "settings_gpxExportNoContacts": "Geen contacten om te exporteren.", "settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem", "settings_gpxExportError": "Er was een fout bij het exporteren.", @@ -1595,7 +1595,7 @@ "settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties", "settings_gpxExportChat": "Locaties van metgezellen", "settings_gpxExportAllContacts": "Alle contactlocaties", - "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", + "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren", "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!", "map_removeLast": "Verwijder Laatste", @@ -1802,11 +1802,9 @@ "contacts_searchUsers": "Zoek {number}{str} gebruikers...", "contacts_searchFavorites": "Zoek {number}{str} favorieten...", "contacts_searchRoomServers": "Zoek {number}{str} Room servers...", - "connectionChoiceTitle": "Kies uw verbindingsmethode", "connectionChoiceUsbLabel": "USB", - "connectionChoiceSubtitle": "Kies hoe u uw MeshCore-apparaat wilt bereiken.", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", + "usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", "usbScreenStatus": "Selecteer een USB-apparaat", "usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.", "usbScreenTitle": "Verbind via USB", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 6b47ad5..dd7b133 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", +{ + "channels_channelDeleteFailed": "Nie udaÅ‚o siÄ™ usunąć kanaÅ‚u \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -10,32 +10,32 @@ "@@locale": "pl", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", - "nav_channels": "Kanały", + "nav_channels": "KanaÅ‚y", "nav_map": "Mapa", "common_cancel": "Anuluj", - "common_connect": "Połącz", - "common_unknownDevice": "Nieznane urządzenie", + "common_connect": "Połącz", + "common_unknownDevice": "Nieznane urzÄ…dzenie", "common_save": "Zapisz", - "common_delete": "Usuń", - "common_close": "Zamknąć", + "common_delete": "UsuÅ„", + "common_close": "Zamknąć", "common_edit": "Edytuj", "common_add": "Dodaj", "common_settings": "Ustawienia", - "common_disconnect": "Odłącz", - "common_connected": "Połączono", - "common_disconnected": "Odłączony", - "common_create": "Utwórz", + "common_disconnect": "Odłącz", + "common_connected": "Połączono", + "common_disconnected": "Odłączony", + "common_create": "Utwórz", "common_continue": "Kontynuuj", - "common_share": "Udostępnij", + "common_share": "UdostÄ™pnij", "common_copy": "Kopiuj", - "common_retry": "Spróbować", + "common_retry": "Spróbować", "common_hide": "Ukryj", - "common_remove": "Usuń", - "common_enable": "Włącz", - "common_disable": "Wyłączyć", - "common_reboot": "Zrestartować", - "common_loading": "Ładowanie...", - "common_notAvailable": "—", + "common_remove": "UsuÅ„", + "common_enable": "Włącz", + "common_disable": "Wyłączyć", + "common_reboot": "Zrestartować", + "common_loading": "Ładowanie...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Skanowanie urządzeń...", - "scanner_connecting": "Łączenie...", - "scanner_disconnecting": "Odłączanie...", - "scanner_notConnected": "Niepołączony", - "scanner_connectedTo": "Połączono z {deviceName}", + "scanner_scanning": "Skanowanie urzÄ…dzeÅ„...", + "scanner_connecting": "Łączenie...", + "scanner_disconnecting": "Odłączanie...", + "scanner_notConnected": "Niepołączony", + "scanner_connectedTo": "Połączono z {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,9 +65,9 @@ } } }, - "scanner_searchingDevices": "Wyszukiwanie urządzeń MeshCore...", - "scanner_tapToScan": "Naciśnij Skan, aby znaleźć urządzenia MeshCore", - "scanner_connectionFailed": "Połączenie nieudane: {error}", + "scanner_searchingDevices": "Wyszukiwanie urzÄ…dzeÅ„ MeshCore...", + "scanner_tapToScan": "NaciÅ›nij Skan, aby znaleźć urzÄ…dzenia MeshCore", + "scanner_connectionFailed": "Połączenie nieudane: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -80,43 +80,43 @@ "device_quickSwitch": "Szybka zmiana", "device_meshcore": "MeshCore", "settings_title": "Ustawienia", - "settings_deviceInfo": "Informacje o urządzeniu", + "settings_deviceInfo": "Informacje o urzÄ…dzeniu", "settings_appSettings": "Ustawienia aplikacji", - "settings_appSettingsSubtitle": "Powiadomienia, wiadomości i preferencje mapy", - "settings_nodeSettings": "Ustawienia węzła", - "settings_nodeName": "Nazwa węzła", + "settings_appSettingsSubtitle": "Powiadomienia, wiadomoÅ›ci i preferencje mapy", + "settings_nodeSettings": "Ustawienia wÄ™zÅ‚a", + "settings_nodeName": "Nazwa wÄ™zÅ‚a", "settings_nodeNameNotSet": "Nie ustawione", - "settings_nodeNameHint": "Wprowadź nazwę węzła", - "settings_nodeNameUpdated": "Imię zaktualizowane", + "settings_nodeNameHint": "Wprowadź nazwÄ™ wÄ™zÅ‚a", + "settings_nodeNameUpdated": "ImiÄ™ zaktualizowane", "settings_radioSettings": "Ustawienia radia", - "settings_radioSettingsSubtitle": "Częstotliwość, moc, współczynnik rozpraszania", - "settings_radioSettingsUpdated": "Ustawienia radia zostały zaktualizowane", + "settings_radioSettingsSubtitle": "CzÄ™stotliwość, moc, współczynnik rozpraszania", + "settings_radioSettingsUpdated": "Ustawienia radia zostaÅ‚y zaktualizowane", "settings_location": "Lokalizacja", "settings_locationSubtitle": "Koordynaty GPS", "settings_locationUpdated": "Lokalizacja zaktualizowana", - "settings_locationBothRequired": "Wprowadź zarówno szerokość, jak i długość geograficzną.", - "settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.", - "settings_latitude": "Szerokość", - "settings_longitude": "Długość", + "settings_locationBothRequired": "Wprowadź zarówno szerokość, jak i dÅ‚ugość geograficznÄ….", + "settings_locationInvalid": "NieprawidÅ‚owa szerokość geograficzna lub dÅ‚ugość geograficzna.", + "settings_latitude": "Szerokość", + "settings_longitude": "DÅ‚ugość", "settings_privacyMode": "Tryb Prywatny", - "settings_privacyModeSubtitle": "Ukryj imię/lokalizację w reklamach", - "settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w reklamach.", - "settings_privacyModeEnabled": "Tryb prywatności włączony", - "settings_privacyModeDisabled": "Tryb prywatności wyłączony", - "settings_actions": "Działania", - "settings_sendAdvertisement": "Wyślij Reklamę", - "settings_sendAdvertisementSubtitle": "Obecność transmisji jest teraz", - "settings_advertisementSent": "Reklama wysłana", + "settings_privacyModeSubtitle": "Ukryj imiÄ™/lokalizacjÄ™ w reklamach", + "settings_privacyModeToggle": "Włącz tryb prywatnoÅ›ci, aby ukryć swoje imiÄ™ i lokalizacjÄ™ w reklamach.", + "settings_privacyModeEnabled": "Tryb prywatnoÅ›ci włączony", + "settings_privacyModeDisabled": "Tryb prywatnoÅ›ci wyłączony", + "settings_actions": "DziaÅ‚ania", + "settings_sendAdvertisement": "WyÅ›lij ReklamÄ™", + "settings_sendAdvertisementSubtitle": "Obecność transmisji jest teraz", + "settings_advertisementSent": "Reklama wysÅ‚ana", "settings_syncTime": "Czas synchronizacji", - "settings_syncTimeSubtitle": "Ustaw zegar urządzenia na czas telefonu.", + "settings_syncTimeSubtitle": "Ustaw zegar urzÄ…dzenia na czas telefonu.", "settings_timeSynchronized": "Synchronizacja czasu", - "settings_refreshContacts": "Odśwież Kontakty", - "settings_refreshContactsSubtitle": "Odśwież listę kontaktów z urządzenia", - "settings_rebootDevice": "Zrestartuj Urządzenie", - "settings_rebootDeviceSubtitle": "Zrestartuj urządzenie MeshCore", - "settings_rebootDeviceConfirm": "Czy na pewno chcesz zrestartować urządzenie? Będziesz odłączony.", + "settings_refreshContacts": "OdÅ›wież Kontakty", + "settings_refreshContactsSubtitle": "OdÅ›wież listÄ™ kontaktów z urzÄ…dzenia", + "settings_rebootDevice": "Zrestartuj UrzÄ…dzenie", + "settings_rebootDeviceSubtitle": "Zrestartuj urzÄ…dzenie MeshCore", + "settings_rebootDeviceConfirm": "Czy na pewno chcesz zrestartować urzÄ…dzenie? BÄ™dziesz odłączony.", "settings_debug": "Debug", - "settings_bleDebugLog": "Log błędów BLE", + "settings_bleDebugLog": "Log błędów BLE", "settings_bleDebugLogSubtitle": "Polecenia BLE, odpowiedzi i surowe dane", "settings_appDebugLog": "Log Wykonywania Aplikacji", "settings_appDebugLogSubtitle": "Komunikaty debugowania aplikacji", @@ -130,25 +130,25 @@ } }, "settings_aboutLegalese": "Projekt MeshCore Open Source 2026", - "settings_aboutDescription": "Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.", - "settings_infoName": "Imię", + "settings_aboutDescription": "Otwarty kod źródÅ‚owy klient Flutter dla urzÄ…dzeÅ„ do sieci mesh LoRa MeshCore.", + "settings_infoName": "ImiÄ™", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Bateria", "settings_infoPublicKey": "Klucz Publiczny", - "settings_infoContactsCount": "Liczba kontaktów", - "settings_infoChannelCount": "Liczba kanałów", + "settings_infoContactsCount": "Liczba kontaktów", + "settings_infoChannelCount": "Liczba kanałów", "settings_presets": "Preset", - "settings_frequency": "Częstotliwość (MHz)", + "settings_frequency": "CzÄ™stotliwość (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", - "settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)", - "settings_bandwidth": "Przepustowość", - "settings_spreadingFactor": "Rozkład Czynnika", + "settings_frequencyInvalid": "NieprawidÅ‚owa czÄ™stotliwość (300-2500 MHz)", + "settings_bandwidth": "Przepustowość", + "settings_spreadingFactor": "RozkÅ‚ad Czynnika", "settings_codingRate": "Stawka Kodowania", "settings_txPower": "TX Moc (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)", - "settings_error": "Błąd: {message}", + "settings_txPowerInvalid": "NieprawidÅ‚owa moc TX (0-22 dBm)", + "settings_error": "Błąd: {message}", "@settings_error": { "placeholders": { "message": { @@ -157,50 +157,50 @@ } }, "appSettings_title": "Ustawienia aplikacji", - "appSettings_appearance": "Wygląd", + "appSettings_appearance": "WyglÄ…d", "appSettings_theme": "Motyw", - "appSettings_themeSystem": "Domyślne ustawienia systemu", + "appSettings_themeSystem": "DomyÅ›lne ustawienia systemu", "appSettings_themeLight": "Jasne", "appSettings_themeDark": "Ciemny", - "appSettings_language": "Język", - "appSettings_languageSystem": "Domyślny systemowy", + "appSettings_language": "JÄ™zyk", + "appSettings_languageSystem": "DomyÅ›lny systemowy", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Powiadomienia", - "appSettings_enableNotifications": "Włącz Powiadomienia", - "appSettings_enableNotificationsSubtitle": "Otrzymuj powiadomienia o wiadomościach i reklamach.", + "appSettings_enableNotifications": "Włącz Powiadomienia", + "appSettings_enableNotificationsSubtitle": "Otrzymuj powiadomienia o wiadomoÅ›ciach i reklamach.", "appSettings_notificationPermissionDenied": "Odmowa zezwolenia na powiadomienia", - "appSettings_notificationsEnabled": "Powiadomienia włączone", - "appSettings_notificationsDisabled": "Powiadomienia wyłączone", - "appSettings_messageNotifications": "Powiadomienia o wiadomościach", - "appSettings_messageNotificationsSubtitle": "Pokaż powiadomienie przy otrzymywaniu nowych wiadomości", - "appSettings_channelMessageNotifications": "Powiadomienia o Wiadomościach na Kanałach", - "appSettings_channelMessageNotificationsSubtitle": "Pokaż powiadomienie przy odbieraniu wiadomości z kanału", + "appSettings_notificationsEnabled": "Powiadomienia włączone", + "appSettings_notificationsDisabled": "Powiadomienia wyłączone", + "appSettings_messageNotifications": "Powiadomienia o wiadomoÅ›ciach", + "appSettings_messageNotificationsSubtitle": "Pokaż powiadomienie przy otrzymywaniu nowych wiadomoÅ›ci", + "appSettings_channelMessageNotifications": "Powiadomienia o WiadomoÅ›ciach na KanaÅ‚ach", + "appSettings_channelMessageNotificationsSubtitle": "Pokaż powiadomienie przy odbieraniu wiadomoÅ›ci z kanaÅ‚u", "appSettings_advertisementNotifications": "Powiadomienia Reklamowe", - "appSettings_advertisementNotificationsSubtitle": "Wyświetl powiadomienie, gdy zostaną odkryte nowe węzły.", - "appSettings_messaging": "Wiadomości", - "appSettings_clearPathOnMaxRetry": "Wyczyść Ścieżkę na Maksymalnej Próbie", - "appSettings_clearPathOnMaxRetrySubtitle": "Resetuj ścieżkę kontaktu po 5 nieudanych próbach wysłania", - "appSettings_pathsWillBeCleared": "Droga będzie wyczyszczona po 5 nieudanych próbach.", + "appSettings_advertisementNotificationsSubtitle": "WyÅ›wietl powiadomienie, gdy zostanÄ… odkryte nowe wÄ™zÅ‚y.", + "appSettings_messaging": "WiadomoÅ›ci", + "appSettings_clearPathOnMaxRetry": "Wyczyść ÅšcieżkÄ™ na Maksymalnej Próbie", + "appSettings_clearPathOnMaxRetrySubtitle": "Resetuj Å›cieżkÄ™ kontaktu po 5 nieudanych próbach wysÅ‚ania", + "appSettings_pathsWillBeCleared": "Droga bÄ™dzie wyczyszczona po 5 nieudanych próbach.", "appSettings_pathsWillNotBeCleared": "Droga nie zostanie automatycznie wyczyszczona.", "appSettings_autoRouteRotation": "Automatyczne Rotowanie Trasy", - "appSettings_autoRouteRotationSubtitle": "Przełączaj się między najlepszymi ścieżkami a trybem zalewowym.", - "appSettings_autoRouteRotationEnabled": "Automatyczne obracanie tras włączone", - "appSettings_autoRouteRotationDisabled": "Automatyczne obracanie tras wyłączone", + "appSettings_autoRouteRotationSubtitle": "Przełączaj siÄ™ miÄ™dzy najlepszymi Å›cieżkami a trybem zalewowym.", + "appSettings_autoRouteRotationEnabled": "Automatyczne obracanie tras włączone", + "appSettings_autoRouteRotationDisabled": "Automatyczne obracanie tras wyłączone", "appSettings_battery": "Bateria", "appSettings_batteryChemistry": "Chemia Baterii", - "appSettings_batteryChemistryPerDevice": "Ustawione na urządzenie ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Ustawione na urzÄ…dzenie ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Połącz się z urządzeniem, aby wybrać", + "appSettings_batteryChemistryConnectFirst": "Połącz siÄ™ z urzÄ…dzeniem, aby wybrać", "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65 V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", - "appSettings_mapDisplay": "Wyświetlanie mapy", - "appSettings_showRepeaters": "Pokaż Powtórniki", - "appSettings_showRepeatersSubtitle": "Wyświetl węzły powtarzające się na mapie", - "appSettings_showChatNodes": "Pokaż Węzły Rozmowy", - "appSettings_showChatNodesSubtitle": "Wyświetl węzły czatu na mapie", - "appSettings_showOtherNodes": "Pokaż inne węzły", - "appSettings_showOtherNodesSubtitle": "Wyświetl inne typy węzłów na mapie", + "appSettings_mapDisplay": "WyÅ›wietlanie mapy", + "appSettings_showRepeaters": "Pokaż Powtórniki", + "appSettings_showRepeatersSubtitle": "WyÅ›wietl wÄ™zÅ‚y powtarzajÄ…ce siÄ™ na mapie", + "appSettings_showChatNodes": "Pokaż WÄ™zÅ‚y Rozmowy", + "appSettings_showChatNodesSubtitle": "WyÅ›wietl wÄ™zÅ‚y czatu na mapie", + "appSettings_showOtherNodes": "Pokaż inne wÄ™zÅ‚y", + "appSettings_showOtherNodesSubtitle": "WyÅ›wietl inne typy wÄ™złów na mapie", "appSettings_timeFilter": "Filtrowanie Czasu", - "appSettings_timeFilterShowAll": "Pokaż wszystkie węzły", - "appSettings_timeFilterShowLast": "Pokaż węzły z ostatnich {hours} godzin", + "appSettings_timeFilterShowAll": "Pokaż wszystkie wÄ™zÅ‚y", + "appSettings_timeFilterShowLast": "Pokaż wÄ™zÅ‚y z ostatnich {hours} godzin", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,14 +230,14 @@ } }, "appSettings_mapTimeFilter": "Filtrowanie Czasu Mapy", - "appSettings_showNodesDiscoveredWithin": "Pokaż węzły odkryte w:", + "appSettings_showNodesDiscoveredWithin": "Pokaż wÄ™zÅ‚y odkryte w:", "appSettings_allTime": "Wszystko czasowo", "appSettings_lastHour": "Ostatnia godzina", "appSettings_last6Hours": "Ostatnie 6 godzin", "appSettings_last24Hours": "Ostatnie 24 godziny", - "appSettings_lastWeek": "Tydzień temu", + "appSettings_lastWeek": "TydzieÅ„ temu", "appSettings_offlineMapCache": "Bufor Map Offline", - "appSettings_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", + "appSettings_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", "appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { @@ -251,17 +251,17 @@ }, "appSettings_debugCard": "Debug", "appSettings_appDebugLogging": "Logowanie Debugowania Aplikacji", - "appSettings_appDebugLoggingSubtitle": "Loguj wiadomości debugowania aplikacji w celu rozwiązywania problemów.", - "appSettings_appDebugLoggingEnabled": "Zdebugowanie aplikacji włączone", - "appSettings_appDebugLoggingDisabled": "Zasubskrybowane logi debugowania aplikacji wyłączone.", + "appSettings_appDebugLoggingSubtitle": "Loguj wiadomoÅ›ci debugowania aplikacji w celu rozwiÄ…zywania problemów.", + "appSettings_appDebugLoggingEnabled": "Zdebugowanie aplikacji włączone", + "appSettings_appDebugLoggingDisabled": "Zasubskrybowane logi debugowania aplikacji wyłączone.", "contacts_title": "Kontakty", - "contacts_noContacts": "Brak jeszcze kontaktów.", - "contacts_contactsWillAppear": "Kontakty będą wyświetlane, gdy urządzenia reklamują się.", + "contacts_noContacts": "Brak jeszcze kontaktów.", + "contacts_contactsWillAppear": "Kontakty bÄ™dÄ… wyÅ›wietlane, gdy urzÄ…dzenia reklamujÄ… siÄ™.", "contacts_searchContacts": "Wyszukaj kontakty...", - "contacts_noUnreadContacts": "Brak nieprzeczytanych kontaktów", - "contacts_noContactsFound": "Brak znalezionych kontaktów ani grup.", - "contacts_deleteContact": "Usuń Kontakt", - "contacts_removeConfirm": "Usuń {contactName} z kontaktów?", + "contacts_noUnreadContacts": "Brak nieprzeczytanych kontaktów", + "contacts_noContactsFound": "Brak znalezionych kontaktów ani grup.", + "contacts_deleteContact": "UsuÅ„ Kontakt", + "contacts_removeConfirm": "UsuÅ„ {contactName} z kontaktów?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -269,12 +269,12 @@ } } }, - "contacts_manageRepeater": "Zarządzaj Powtórzami", + "contacts_manageRepeater": "ZarzÄ…dzaj Powtórzami", "contacts_roomLogin": "Logowanie do pokoju", - "contacts_openChat": "Otwórz czat", - "contacts_editGroup": "Edytuj Grupę", - "contacts_deleteGroup": "Usuń Grupę", - "contacts_deleteGroupConfirm": "Usuń \"{groupName}\"?", + "contacts_openChat": "Otwórz czat", + "contacts_editGroup": "Edytuj GrupÄ™", + "contacts_deleteGroup": "UsuÅ„ GrupÄ™", + "contacts_deleteGroupConfirm": "UsuÅ„ \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -285,7 +285,7 @@ "contacts_newGroup": "Nowa Grupa", "contacts_groupName": "Nazwa grupy", "contacts_groupNameRequired": "Nazwa grupy jest wymagana", - "contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje", + "contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,10 +294,10 @@ } }, "contacts_filterContacts": "Filtruj kontakty...", - "contacts_noContactsMatchFilter": "Brak pasujących kontaktów do Twojego filtra", - "contacts_noMembers": "Brak członków", - "contacts_lastSeenNow": "Ostatnie połączenie", - "contacts_lastSeenMinsAgo": "Ostatnie połączenie {minutes} min temu", + "contacts_noContactsMatchFilter": "Brak pasujÄ…cych kontaktów do Twojego filtra", + "contacts_noMembers": "Brak czÅ‚onków", + "contacts_lastSeenNow": "Ostatnie połączenie", + "contacts_lastSeenMinsAgo": "Ostatnie połączenie {minutes} min temu", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Ostatni raz widziany 1 godzinę temu", - "contacts_lastSeenHoursAgo": "Ostatnie połączenie {hours} godzin temu", + "contacts_lastSeenHourAgo": "Ostatni raz widziany 1 godzinÄ™ temu", + "contacts_lastSeenHoursAgo": "Ostatnie połączenie {hours} godzin temu", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Ostatni raz widziany 1 dzień temu", - "contacts_lastSeenDaysAgo": "Ostatnie połączenie {days} dni temu", + "contacts_lastSeenDayAgo": "Ostatni raz widziany 1 dzieÅ„ temu", + "contacts_lastSeenDaysAgo": "Ostatnie połączenie {days} dni temu", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -323,12 +323,12 @@ } } }, - "channels_title": "Kanały", - "channels_noChannelsConfigured": "Brak skonfigurowanych kanałów", - "channels_addPublicChannel": "Dodaj kanał publiczny", - "channels_searchChannels": "Wyszukaj kanały...", - "channels_noChannelsFound": "Brak znalezionych kanałów", - "channels_channelIndex": "Kanał {index}", + "channels_title": "KanaÅ‚y", + "channels_noChannelsConfigured": "Brak skonfigurowanych kanałów", + "channels_addPublicChannel": "Dodaj kanaÅ‚ publiczny", + "channels_searchChannels": "Wyszukaj kanaÅ‚y...", + "channels_noChannelsFound": "Brak znalezionych kanałów", + "channels_channelIndex": "KanaÅ‚ {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -336,16 +336,16 @@ } } }, - "channels_hashtagChannel": "Kanał z hashtagami", + "channels_hashtagChannel": "KanaÅ‚ z hashtagami", "channels_public": "Publiczny", "channels_private": "Prywatne", - "channels_publicChannel": "Kanał publiczny", - "channels_privateChannel": "Prywatny kanał", - "channels_editChannel": "Edytuj kanał", - "channels_muteChannel": "Wycisz kanał", - "channels_unmuteChannel": "Wyłącz wyciszenie kanału", - "channels_deleteChannel": "Usuń kanał", - "channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.", + "channels_publicChannel": "KanaÅ‚ publiczny", + "channels_privateChannel": "Prywatny kanaÅ‚", + "channels_editChannel": "Edytuj kanaÅ‚", + "channels_muteChannel": "Wycisz kanaÅ‚", + "channels_unmuteChannel": "Wyłącz wyciszenie kanaÅ‚u", + "channels_deleteChannel": "UsuÅ„ kanaÅ‚", + "channels_deleteChannelConfirm": "UsuÅ„ \"{name}\"? Nie można tego cofnąć.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Kanał \"{name}\" usunięto", + "channels_channelDeleted": "KanaÅ‚ \"{name}\" usuniÄ™to", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Dodaj Kanał", - "channels_channelIndexLabel": "Indeks kanału", - "channels_channelName": "Nazwa kanału", - "channels_usePublicChannel": "Użyj kanału publicznego", + "channels_addChannel": "Dodaj KanaÅ‚", + "channels_channelIndexLabel": "Indeks kanaÅ‚u", + "channels_channelName": "Nazwa kanaÅ‚u", + "channels_usePublicChannel": "Użyj kanaÅ‚u publicznego", "channels_standardPublicPsk": "Standard public PSK", "channels_pskHex": "PSK (Hex)", "channels_generateRandomPsk": "Wygeneruj losowy klucz PSK", - "channels_enterChannelName": "Proszę podać nazwę kanału.", - "channels_pskMustBe32Hex": "PSK musi mieć 32 znaki szesnastkowe.", - "channels_channelAdded": "Kanał \"{name}\" dodany", + "channels_enterChannelName": "ProszÄ™ podać nazwÄ™ kanaÅ‚u.", + "channels_pskMustBe32Hex": "PSK musi mieć 32 znaki szesnastkowe.", + "channels_channelAdded": "KanaÅ‚ \"{name}\" dodany", "@channels_channelAdded": { "placeholders": { "name": { @@ -378,7 +378,7 @@ } } }, - "channels_editChannelTitle": "Edytuj Kanał {index}", + "channels_editChannelTitle": "Edytuj KanaÅ‚ {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -387,7 +387,7 @@ } }, "channels_smazCompression": "Kompresja SMAZ", - "channels_channelUpdated": "Kanał \"{name}\" został zaktualizowany", + "channels_channelUpdated": "KanaÅ‚ \"{name}\" zostaÅ‚ zaktualizowany", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,15 +395,15 @@ } } }, - "channels_publicChannelAdded": "Kanał publiczny dodany", + "channels_publicChannelAdded": "KanaÅ‚ publiczny dodany", "channels_sortBy": "Sortuj po", - "channels_sortManual": "Ręczna", + "channels_sortManual": "RÄ™czna", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Najnowsze wiadomości", - "channels_sortUnread": "Niezgłoszone", - "chat_noMessages": "Brak jeszcze wiadomości", - "chat_sendMessageToStart": "Wyślij wiadomość, aby rozpocząć.", - "chat_originalMessageNotFound": "Błąd: Nie znaleziono oryginalnego komunikatu", + "channels_sortLatestMessages": "Najnowsze wiadomoÅ›ci", + "channels_sortUnread": "NiezgÅ‚oszone", + "chat_noMessages": "Brak jeszcze wiadomoÅ›ci", + "chat_sendMessageToStart": "WyÅ›lij wiadomość, aby rozpocząć.", + "chat_originalMessageNotFound": "Błąd: Nie znaleziono oryginalnego komunikatu", "chat_replyingTo": "Odpowiadanie na {name}", "@chat_replyingTo": { "placeholders": { @@ -421,7 +421,7 @@ } }, "chat_location": "Lokalizacja", - "chat_sendMessageTo": "Wyślij wiadomość do {contactName}", + "chat_sendMessageTo": "WyÅ›lij wiadomość do {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "Wpisz wiadomość...", - "chat_messageTooLong": "Wiadomość jest za długa (maksymalnie {maxBytes} bajtów).", + "chat_typeMessage": "Wpisz wiadomość...", + "chat_messageTooLong": "Wiadomość jest za dÅ‚uga (maksymalnie {maxBytes} bajtów).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,10 +438,10 @@ } } }, - "chat_messageCopied": "Wiadomość skopiowana", - "chat_messageDeleted": "Wiadomość usunięta", - "chat_retryingMessage": "Próba ponowienia", - "chat_retryCount": "Spróbuj {current}/{max}", + "chat_messageCopied": "Wiadomość skopiowana", + "chat_messageDeleted": "Wiadomość usuniÄ™ta", + "chat_retryingMessage": "Próba ponowienia", + "chat_retryCount": "Spróbuj {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -452,9 +452,9 @@ } } }, - "chat_sendGif": "Wyślij GIF", + "chat_sendGif": "WyÅ›lij GIF", "chat_reply": "Odpowiedz", - "chat_addReaction": "Dodaj Reakcję", + "chat_addReaction": "Dodaj ReakcjÄ™", "chat_me": "Ja", "emojiCategorySmileys": "Emoji", "emojiCategoryGestures": "Gestikulacje", @@ -463,22 +463,22 @@ "gifPicker_title": "Wybierz GIF", "gifPicker_searchHint": "Wyszukaj GIF-y...", "gifPicker_poweredBy": "Zasilane przez GIPHY", - "gifPicker_noGifsFound": "Nie znaleziono GIF-ów", - "gifPicker_failedLoad": "Nie udało się załadować GIF-ów", - "gifPicker_failedSearch": "Nie udało się znaleźć GIF-ów", - "gifPicker_noInternet": "Brak połączenia internetowego", + "gifPicker_noGifsFound": "Nie znaleziono GIF-ów", + "gifPicker_failedLoad": "Nie udaÅ‚o siÄ™ zaÅ‚adować GIF-ów", + "gifPicker_failedSearch": "Nie udaÅ‚o siÄ™ znaleźć GIF-ów", + "gifPicker_noInternet": "Brak połączenia internetowego", "debugLog_appTitle": "Log Wykonywania Aplikacji", - "debugLog_bleTitle": "Log błędów BLE", + "debugLog_bleTitle": "Log błędów BLE", "debugLog_copyLog": "Kopiuj log", - "debugLog_clearLog": "Wyczyść dziennik", + "debugLog_clearLog": "Wyczyść dziennik", "debugLog_copied": "Skopiowano dziennik debugowania", "debugLog_bleCopied": "Skopiowany log BLE", - "debugLog_noEntries": "Nie ma jeszcze żadnych logów debugowania.", - "debugLog_enableInSettings": "Włącz logowanie debugowania aplikacji w ustawieniach", + "debugLog_noEntries": "Nie ma jeszcze żadnych logów debugowania.", + "debugLog_enableInSettings": "Włącz logowanie debugowania aplikacji w ustawieniach", "debugLog_frames": "Ramy", "debugLog_rawLogRx": "Surowe Log-RX", - "debugLog_noBleActivity": "Brak aktywności BLE jeszcze.", - "debugFrame_length": "Długość ramy: {count} bajtów", + "debugLog_noBleActivity": "Brak aktywnoÅ›ci BLE jeszcze.", + "debugFrame_length": "DÅ‚ugość ramy: {count} bajtów", "@debugFrame_length": { "placeholders": { "count": { @@ -494,7 +494,7 @@ } } }, - "debugFrame_textMessageHeader": "Wiadomość tekstowa:", + "debugFrame_textMessageHeader": "Wiadomość tekstowa:", "debugFrame_destinationPubKey": "- Oznaczenie PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { @@ -540,13 +540,13 @@ } } }, - "debugFrame_hexDump": "Wyjście SzESZCZNULNE:", - "chat_pathManagement": "Zarządzanie ścieżkami", + "debugFrame_hexDump": "WyjÅ›cie SzESZCZNULNE:", + "chat_pathManagement": "ZarzÄ…dzanie Å›cieżkami", "chat_routingMode": "Tryb routingu", - "chat_autoUseSavedPath": "Automatyczne (użyj zapisanej ścieżki)", + "chat_autoUseSavedPath": "Automatyczne (użyj zapisanej Å›cieżki)", "chat_forceFloodMode": "Wymusz Tryb Powodowany", - "chat_recentAckPaths": "Ostatnie ścieżki ACK (naciśnij, aby użyć):", - "chat_pathHistoryFull": "Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.", + "chat_recentAckPaths": "Ostatnie Å›cieżki ACK (naciÅ›nij, aby użyć):", + "chat_pathHistoryFull": "Historia Å›cieżek jest peÅ‚na. UsuÅ„ wpisy, aby dodać nowe.", "chat_hopSingular": "Skacz", "chat_hopPlural": "skoczkowie", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -558,19 +558,19 @@ } }, "chat_successes": "Sukcesy", - "chat_removePath": "Usuń ścieżkę", - "chat_noPathHistoryYet": "Brak jeszcze historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.", - "chat_pathActions": "Działania ścieżki:", - "chat_setCustomPath": "Ustaw Ścieżkę Dostosowaną", - "chat_setCustomPathSubtitle": "Ręcznie określ trasę.", - "chat_clearPath": "Wyczyść Ścieżkę", - "chat_clearPathSubtitle": "Zmusz do ponownej identyfikacji przy następnym wysłaniu", - "chat_pathCleared": "Ścieżka oczyszczona. Kolejne powiadomienie odnajdzie trasę.", - "chat_floodModeSubtitle": "Użyj przełącznika routingu w pasku narzędzi.", - "chat_floodModeEnabled": "Tryb powodziowy włączony. Włącz ponownie za pomocą ikony routingu w pasku narzędzi.", - "chat_fullPath": "Pełna ścieżka", - "chat_pathDetailsNotAvailable": "Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.", - "chat_pathSetHops": "Ścieżka ustawiona: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_removePath": "UsuÅ„ Å›cieżkÄ™", + "chat_noPathHistoryYet": "Brak jeszcze historii Å›cieżek.\nWyÅ›lij wiadomość, aby odkryć Å›cieżki.", + "chat_pathActions": "DziaÅ‚ania Å›cieżki:", + "chat_setCustomPath": "Ustaw ÅšcieżkÄ™ DostosowanÄ…", + "chat_setCustomPathSubtitle": "RÄ™cznie okreÅ›l trasÄ™.", + "chat_clearPath": "Wyczyść ÅšcieżkÄ™", + "chat_clearPathSubtitle": "Zmusz do ponownej identyfikacji przy nastÄ™pnym wysÅ‚aniu", + "chat_pathCleared": "Åšcieżka oczyszczona. Kolejne powiadomienie odnajdzie trasÄ™.", + "chat_floodModeSubtitle": "Użyj przełącznika routingu w pasku narzÄ™dzi.", + "chat_floodModeEnabled": "Tryb powodziowy włączony. Włącz ponownie za pomocÄ… ikony routingu w pasku narzÄ™dzi.", + "chat_fullPath": "PeÅ‚na Å›cieżka", + "chat_pathDetailsNotAvailable": "Szczegóły Å›cieżki jeszcze niedostÄ™pne. Spróbuj wysÅ‚ać wiadomość, aby odÅ›wieżyć.", + "chat_pathSetHops": "Åšcieżka ustawiona: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Zapisano lokalnie. Połącz się, aby zsynchronizować.", - "chat_pathDeviceConfirmed": "Urządzenie potwierdzone.", - "chat_pathDeviceNotConfirmed": "Urządzenie nie zostało jeszcze potwierdzone.", - "chat_type": "Wprowadź", - "chat_path": "Ścieżka", + "chat_pathSavedLocally": "Zapisano lokalnie. Połącz siÄ™, aby zsynchronizować.", + "chat_pathDeviceConfirmed": "UrzÄ…dzenie potwierdzone.", + "chat_pathDeviceNotConfirmed": "UrzÄ…dzenie nie zostaÅ‚o jeszcze potwierdzone.", + "chat_type": "Wprowadź", + "chat_path": "Åšcieżka", "chat_publicKey": "Klucz Publiczny", - "chat_compressOutgoingMessages": "Kompresuj wychodzące wiadomości", - "chat_floodForced": "Powodowana Powódź", - "chat_directForced": "Bezpośrednio (wymuszono)", - "chat_hopsForced": "{count} skoków (wymuszonych)", + "chat_compressOutgoingMessages": "Kompresuj wychodzÄ…ce wiadomoÅ›ci", + "chat_floodForced": "Powodowana Powódź", + "chat_directForced": "BezpoÅ›rednio (wymuszono)", + "chat_hopsForced": "{count} skoków (wymuszonych)", "@chat_hopsForced": { "placeholders": { "count": { @@ -599,9 +599,9 @@ } }, "chat_floodAuto": "Powodzie (automatyczne)", - "chat_direct": "Bezpośrednio", - "chat_poiShared": "Wspólny POI", - "chat_unread": "Niezgłoszone: {count}", + "chat_direct": "BezpoÅ›rednio", + "chat_poiShared": "Wspólny POI", + "chat_unread": "NiezgÅ‚oszone: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Otworzyć link?", - "chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?", - "chat_open": "Otwórz", - "chat_couldNotOpenLink": "Nie można otworzyć linku: {url}", + "chat_openLink": "Otworzyć link?", + "chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglÄ…darce?", + "chat_open": "Otwórz", + "chat_couldNotOpenLink": "Nie można otworzyć linku: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,11 +620,11 @@ } } }, - "chat_invalidLink": "Nieprawidłowy format linku", - "map_title": "Mapa węzłów", - "map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi", - "map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.", - "map_nodesCount": "Węzły: {count}", + "chat_invalidLink": "NieprawidÅ‚owy format linku", + "map_title": "Mapa wÄ™złów", + "map_noNodesWithLocation": "Brak wÄ™złów z danymi lokalizacyjnymi", + "map_nodesNeedGps": "WÄ™zÅ‚y muszÄ… udostÄ™pniać swoje współrzÄ™dne GPS,\naby pojawić siÄ™ na mapie.", + "map_nodesCount": "WÄ™zÅ‚y: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -641,26 +641,26 @@ } }, "map_chat": "Rozmowa", - "map_repeater": "Powtórzacz", - "map_room": "Pokój", + "map_repeater": "Powtórzacz", + "map_room": "Pokój", "map_sensor": "Czujnik", "map_pinDm": "Zablokuj (DM)", "map_pinPrivate": "Zablokuj (Prywatnie)", "map_pinPublic": "Oznacz jako publiczne", "map_lastSeen": "Ostatni raz widziany", - "map_disconnectConfirm": "Czy na pewno chcesz się odłączyć od tego urządzenia?", + "map_disconnectConfirm": "Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?", "map_from": "Od", - "map_source": "Źródło", + "map_source": "ŹródÅ‚o", "map_flags": "Flagi", - "map_shareMarkerHere": "Udostępnij znacznik tutaj", - "map_pinLabel": "Oznacz etykietę", + "map_shareMarkerHere": "UdostÄ™pnij znacznik tutaj", + "map_pinLabel": "Oznacz etykietÄ™", "map_label": "Etykieta", - "map_pointOfInterest": "Punkt zainteresowań", - "map_sendToContact": "Wyślij do kontaktu", - "map_sendToChannel": "Wyślij do kanału", - "map_noChannelsAvailable": "Brak dostępnych kanałów", - "map_publicLocationShare": "Udostępnij lokalizację publicznie", - "map_publicLocationShareConfirm": "Wkrótce udostępnisz lokalizację w {channelLabel}. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.", + "map_pointOfInterest": "Punkt zainteresowaÅ„", + "map_sendToContact": "WyÅ›lij do kontaktu", + "map_sendToChannel": "WyÅ›lij do kanaÅ‚u", + "map_noChannelsAvailable": "Brak dostÄ™pnych kanałów", + "map_publicLocationShare": "UdostÄ™pnij lokalizacjÄ™ publicznie", + "map_publicLocationShareConfirm": "Wkrótce udostÄ™pnisz lokalizacjÄ™ w {channelLabel}. Ten kanaÅ‚ jest publiczny i każdy z PSK może go zobaczyć.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Połącz się z urządzeniem, aby udostępniać znacznik.", - "map_filterNodes": "Filtruj Węzły", - "map_nodeTypes": "Typy węzłów", - "map_chatNodes": "Węzły czatu", + "map_connectToShareMarkers": "Połącz siÄ™ z urzÄ…dzeniem, aby udostÄ™pniać znacznik.", + "map_filterNodes": "Filtruj WÄ™zÅ‚y", + "map_nodeTypes": "Typy wÄ™złów", + "map_chatNodes": "WÄ™zÅ‚y czatu", "map_repeaters": "Powtarzacze", - "map_otherNodes": "Inne węzły", + "map_otherNodes": "Inne wÄ™zÅ‚y", "map_keyPrefix": "Prefiks klucza", "map_filterByKeyPrefix": "Filtruj po prefiksie klucza", - "map_publicKeyPrefix": "Przewód klucza publicznego", + "map_publicKeyPrefix": "Przewód klucza publicznego", "map_markers": "Oznaczarki", - "map_showSharedMarkers": "Pokaż współdzielone znaki.", + "map_showSharedMarkers": "Pokaż współdzielone znaki.", "map_lastSeenTime": "Ostatni raz widiany", "map_sharedPin": "Podzielony PIN", - "map_joinRoom": "Dołącz do pokoju", - "map_manageRepeater": "Zarządzaj Powtórzami", + "map_joinRoom": "Dołącz do pokoju", + "map_manageRepeater": "ZarzÄ…dzaj Powtórzami", "mapCache_title": "Bufor Map Offline", - "mapCache_selectAreaFirst": "Wybierz obszar do wstępnego pobrania.", - "mapCache_noTilesToDownload": "Brak dostępnych płytek do pobrania dla tego obszaru.", - "mapCache_downloadTilesTitle": "Pobierz płytki", - "mapCache_downloadTilesPrompt": "Pobierz {count} płytek do użytku offline?", + "mapCache_selectAreaFirst": "Wybierz obszar do wstÄ™pnego pobrania.", + "mapCache_noTilesToDownload": "Brak dostÄ™pnych pÅ‚ytek do pobrania dla tego obszaru.", + "mapCache_downloadTilesTitle": "Pobierz pÅ‚ytki", + "mapCache_downloadTilesPrompt": "Pobierz {count} pÅ‚ytek do użytku offline?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -696,7 +696,7 @@ } }, "mapCache_downloadAction": "Pobierz", - "mapCache_cachedTiles": "Pamiętanych {count} płytek", + "mapCache_cachedTiles": "PamiÄ™tanych {count} pÅ‚ytek", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Pamiętane {downloaded} płytki ({failed} nieudane)", + "mapCache_cachedTilesWithFailed": "PamiÄ™tane {downloaded} pÅ‚ytki ({failed} nieudane)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Wyczyść pamięć podręczną offline", - "mapCache_clearOfflineCachePrompt": "Usuń wszystkie tymczasowe kafelki mapy?", - "mapCache_offlineCacheCleared": "Pamięć podręczna offline została wyczyszczona", - "mapCache_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", - "mapCache_cacheArea": "Obszar pamięci podręcznej", - "mapCache_useCurrentView": "Użyj aktualnego widoku", - "mapCache_zoomRange": "Zakres powiększenia", - "mapCache_estimatedTiles": "Szacunkowa liczba płytek: {count}", + "mapCache_clearOfflineCacheTitle": "Wyczyść pamięć podrÄ™cznÄ… offline", + "mapCache_clearOfflineCachePrompt": "UsuÅ„ wszystkie tymczasowe kafelki mapy?", + "mapCache_offlineCacheCleared": "Pamięć podrÄ™czna offline zostaÅ‚a wyczyszczona", + "mapCache_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", + "mapCache_cacheArea": "Obszar pamiÄ™ci podrÄ™cznej", + "mapCache_useCurrentView": "Użyj aktualnego widoku", + "mapCache_zoomRange": "Zakres powiÄ™kszenia", + "mapCache_estimatedTiles": "Szacunkowa liczba pÅ‚ytek: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -742,7 +742,7 @@ } }, "mapCache_downloadTilesButton": "Pobierz Paski", - "mapCache_clearCacheButton": "Wyczyść pamięć podręczną", + "mapCache_clearCacheButton": "Wyczyść pamięć podrÄ™cznÄ…", "mapCache_failedDownloads": "Nieudane pobrania: {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -768,7 +768,7 @@ } } }, - "time_justNow": "Właśnie teraz", + "time_justNow": "WÅ‚aÅ›nie teraz", "time_minutesAgo": "{minutes} minut temu", "@time_minutesAgo": { "placeholders": { @@ -795,31 +795,31 @@ }, "time_hour": "godzina", "time_hours": "godziny", - "time_day": "dzień", + "time_day": "dzieÅ„", "time_days": "dni", - "time_week": "tydzień", + "time_week": "tydzieÅ„", "time_weeks": "tygodnie", - "time_month": "miesiąc", + "time_month": "miesiÄ…c", "time_months": "miesiace", "time_minutes": "minuty", "time_allTime": "Wszystko czasowo", - "dialog_disconnect": "Odłącz", - "dialog_disconnectConfirm": "Czy na pewno chcesz się odłączyć od tego urządzenia?", - "login_repeaterLogin": "Powtórz Logowanie", + "dialog_disconnect": "Odłącz", + "dialog_disconnectConfirm": "Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?", + "login_repeaterLogin": "Powtórz Logowanie", "login_roomLogin": "Logowanie do pokoju", - "login_password": "Hasło", - "login_enterPassword": "Wprowadź hasło", - "login_savePassword": "Zapisz hasło", - "login_savePasswordSubtitle": "Hasło będzie bezpiecznie przechowywane na tym urządzeniu.", - "login_repeaterDescription": "Wprowadź hasło do powtarzacza, aby uzyskać dostęp do ustawień i statusu.", - "login_roomDescription": "Wprowadź hasło do pokoju, aby uzyskać dostęp do ustawień i statusu.", + "login_password": "HasÅ‚o", + "login_enterPassword": "Wprowadź hasÅ‚o", + "login_savePassword": "Zapisz hasÅ‚o", + "login_savePasswordSubtitle": "HasÅ‚o bÄ™dzie bezpiecznie przechowywane na tym urzÄ…dzeniu.", + "login_repeaterDescription": "Wprowadź hasÅ‚o do powtarzacza, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.", + "login_roomDescription": "Wprowadź hasÅ‚o do pokoju, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.", "login_routing": "Przekierowanie", "login_routingMode": "Tryb routingu", - "login_autoUseSavedPath": "Automatycznie (użyj zapisanej ścieżki)", + "login_autoUseSavedPath": "Automatycznie (użyj zapisanej Å›cieżki)", "login_forceFloodMode": "Wymusz Tryb Powodowany", - "login_managePaths": "Zarządzaj Ścieżkami", - "login_login": "Zaloguj się", - "login_attempt": "Próba {current}/{max}", + "login_managePaths": "ZarzÄ…dzaj Åšcieżkami", + "login_login": "Zaloguj siÄ™", + "login_attempt": "Próba {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Zalogowanie się nie powiodło: {error}", + "login_failed": "Zalogowanie siÄ™ nie powiodÅ‚o: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.", - "common_reload": "Ponownie załadować", - "common_clear": "Wyczyść", - "path_currentPath": "Aktualny ścieżka: {path}", + "login_failedMessage": "Logowanie nie powiodÅ‚o siÄ™. HasÅ‚o jest nieprawidÅ‚owe albo repeater jest nieosiÄ…galny.", + "common_reload": "Ponownie zaÅ‚adować", + "common_clear": "Wyczyść", + "path_currentPath": "Aktualny Å›cieżka: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Użyj ścieżki {count} {count, plural, =1{hop} other{hops}}.", + "path_usingHopsPath": "Użyj Å›cieżki {count} {count, plural, =1{hop} other{hops}}.", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Wprowadź własną ścieżkę", - "path_currentPathLabel": "Aktualny ścieżka", - "path_hexPrefixInstructions": "Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.", - "path_hexPrefixExample": "A1,F2,3C (każedy węzeł używa pierwszego bajtu swojego klucza publicznego)", - "path_labelHexPrefixes": "Ścieżka (przesunięcia bitowe)", - "path_helperMaxHops": "Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).", - "path_selectFromContacts": "Albo wybierz z kontaktów:", - "path_noRepeatersFound": "Nie znaleziono repeaterów ani serwerów pokoi.", - "path_customPathsRequire": "Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.", - "path_invalidHexPrefixes": "Nieprawidłowe prefiksy szesnastkowe: {prefixes}", + "path_enterCustomPath": "Wprowadź wÅ‚asnÄ… Å›cieżkÄ™", + "path_currentPathLabel": "Aktualny Å›cieżka", + "path_hexPrefixInstructions": "Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.", + "path_hexPrefixExample": "A1,F2,3C (każedy wÄ™zeÅ‚ używa pierwszego bajtu swojego klucza publicznego)", + "path_labelHexPrefixes": "Åšcieżka (przesuniÄ™cia bitowe)", + "path_helperMaxHops": "Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).", + "path_selectFromContacts": "Albo wybierz z kontaktów:", + "path_noRepeatersFound": "Nie znaleziono repeaterów ani serwerów pokoi.", + "path_customPathsRequire": "Dostosowane Å›cieżki wymagajÄ… poÅ›rednich skoków, które mogÄ… przekazywać wiadomoÅ›ci.", + "path_invalidHexPrefixes": "NieprawidÅ‚owe prefiksy szesnastkowe: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.", - "path_setPath": "Ustaw Ścieżkę", - "repeater_management": "Zarządzanie Powtórzami", - "repeater_managementTools": "Narzędzia Zarządzania", + "path_tooLong": "Åšcieżka jest zbyt dÅ‚uga. Dozwolonych skoków wynosi 64.", + "path_setPath": "Ustaw ÅšcieżkÄ™", + "repeater_management": "ZarzÄ…dzanie Powtórzami", + "repeater_managementTools": "NarzÄ™dzia ZarzÄ…dzania", "repeater_status": "Status", - "repeater_statusSubtitle": "Wyświetl status powtarzacza, statystyki i sąsiadów.", + "repeater_statusSubtitle": "WyÅ›wietl status powtarzacza, statystyki i sÄ…siadów.", "repeater_telemetry": "Telemetry", - "repeater_telemetrySubtitle": "Wyświetl dane telemetryczne z czujników i statystyki systemu", + "repeater_telemetrySubtitle": "WyÅ›wietl dane telemetryczne z czujników i statystyki systemu", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Wyślij polecenia do powielacza", + "repeater_cliSubtitle": "WyÅ›lij polecenia do powielacza", "repeater_settings": "Ustawienia", "repeater_settingsSubtitle": "Skonfiguruj parametry powtarzacza", "repeater_statusTitle": "Status powtarzacza", "repeater_routingMode": "Tryb routingu", - "repeater_autoUseSavedPath": "Automatycznie (użyj zapisanej ścieżki)", + "repeater_autoUseSavedPath": "Automatycznie (użyj zapisanej Å›cieżki)", "repeater_forceFloodMode": "Wymusz Tryb Powodowany", - "repeater_pathManagement": "Zarządzanie ścieżkami", - "repeater_refresh": "Odśwież", - "repeater_statusRequestTimeout": "Życzenie statusu timed out.", - "repeater_errorLoadingStatus": "Błąd podczas ładowania statusu: {error}", + "repeater_pathManagement": "ZarzÄ…dzanie Å›cieżkami", + "repeater_refresh": "OdÅ›wież", + "repeater_statusRequestTimeout": "Å»yczenie statusu timed out.", + "repeater_errorLoadingStatus": "Błąd podczas Å‚adowania statusu: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -904,19 +904,19 @@ "repeater_systemInformation": "Informacje o systemie", "repeater_battery": "Bateria", "repeater_clockAtLogin": "Godzina (przy logowaniu)", - "repeater_uptime": "Dostępność", - "repeater_queueLength": "Długość kolejki", + "repeater_uptime": "DostÄ™pność", + "repeater_queueLength": "DÅ‚ugość kolejki", "repeater_debugFlags": "Opcje debugowania", "repeater_radioStatistics": "Statystyki Radia", "repeater_lastRssi": "Ostatni RSSI", "repeater_lastSnr": "Ostatnie SNR", - "repeater_noiseFloor": "Poziom Szumów", + "repeater_noiseFloor": "Poziom Szumów", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Statystyki pakietów", - "repeater_sent": "Wysłane", + "repeater_packetStatistics": "Statystyki pakietów", + "repeater_sent": "WysÅ‚ane", "repeater_received": "Otrzymano", - "repeater_duplicates": "Powtórzenia", + "repeater_duplicates": "Powtórzenia", "repeater_daysHoursMinsSecs": "{days} dni {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Razem: {total}, Powodzenie: {flood}, Bezpośrednio: {direct}", + "repeater_packetTxTotal": "Razem: {total}, Powodzenie: {flood}, BezpoÅ›rednio: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Razem: {total}, Powodzenie: {flood}, Bezpośrednio: {direct}", + "repeater_packetRxTotal": "Razem: {total}, Powodzenie: {flood}, BezpoÅ›rednio: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Powodzie: {flood}, Bezpośrednie: {direct}", + "repeater_duplicatesFloodDirect": "Powodzie: {flood}, BezpoÅ›rednie: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,36 +981,36 @@ } } }, - "repeater_settingsTitle": "Ustawienia Powtórki", + "repeater_settingsTitle": "Ustawienia Powtórki", "repeater_basicSettings": "Podstawowe Ustawienia", - "repeater_repeaterName": "Nazwa Powtórnika", - "repeater_repeaterNameHelper": "Wyświetl nazwę tego powtarzacza", - "repeater_adminPassword": "Hasło Administracyjne", - "repeater_adminPasswordHelper": "Pełny dostęp hasło", - "repeater_guestPassword": "Hasło gościa", - "repeater_guestPasswordHelper": "Dostęp tylko do odczytu hasło", + "repeater_repeaterName": "Nazwa Powtórnika", + "repeater_repeaterNameHelper": "WyÅ›wietl nazwÄ™ tego powtarzacza", + "repeater_adminPassword": "HasÅ‚o Administracyjne", + "repeater_adminPasswordHelper": "PeÅ‚ny dostÄ™p hasÅ‚o", + "repeater_guestPassword": "HasÅ‚o goÅ›cia", + "repeater_guestPasswordHelper": "DostÄ™p tylko do odczytu hasÅ‚o", "repeater_radioSettings": "Ustawienia radia", - "repeater_frequencyMhz": "Częstotliwość (MHz)", + "repeater_frequencyMhz": "CzÄ™stotliwość (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Przepustowość", - "repeater_spreadingFactor": "Rozkład Czynnika", + "repeater_bandwidth": "Przepustowość", + "repeater_spreadingFactor": "RozkÅ‚ad Czynnika", "repeater_codingRate": "Stawka kodowania", "repeater_locationSettings": "Ustawienia Lokalizacji", - "repeater_latitude": "Szerokość", - "repeater_latitudeHelper": "Stopnie dziesiętne (np. 37.7749)", - "repeater_longitude": "Długość", - "repeater_longitudeHelper": "Stopnie dziesiętne (np. -122,4194)", + "repeater_latitude": "Szerokość", + "repeater_latitudeHelper": "Stopnie dziesiÄ™tne (np. 37.7749)", + "repeater_longitude": "DÅ‚ugość", + "repeater_longitudeHelper": "Stopnie dziesiÄ™tne (np. -122,4194)", "repeater_features": "Funkcje", - "repeater_packetForwarding": "Przekierowanie pakietów", - "repeater_packetForwardingSubtitle": "Włącz repeater, aby przekazywać pakiety.", - "repeater_guestAccess": "Dostęp dla gości", - "repeater_guestAccessSubtitle": "Umożliw dostęp tylko do odczytu dla gości.", - "repeater_privacyMode": "Tryb Prywatności", - "repeater_privacyModeSubtitle": "Ukryj imię/lokalizację w reklamach", + "repeater_packetForwarding": "Przekierowanie pakietów", + "repeater_packetForwardingSubtitle": "Włącz repeater, aby przekazywać pakiety.", + "repeater_guestAccess": "DostÄ™p dla goÅ›ci", + "repeater_guestAccessSubtitle": "Umożliw dostÄ™p tylko do odczytu dla goÅ›ci.", + "repeater_privacyMode": "Tryb PrywatnoÅ›ci", + "repeater_privacyModeSubtitle": "Ukryj imiÄ™/lokalizacjÄ™ w reklamach", "repeater_advertisementSettings": "Ustawienia Reklam", - "repeater_localAdvertInterval": "Interwał Reklamy Lokalnej", + "repeater_localAdvertInterval": "InterwaÅ‚ Reklamy Lokalnej", "repeater_localAdvertIntervalMinutes": "{minutes} minut", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Interwał Reklamy Powodziowej", + "repeater_floodAdvertInterval": "InterwaÅ‚ Reklamy Powodziowej", "repeater_floodAdvertIntervalHours": "{hours} godzin", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Zaszyfrowany Interwał Reklamowy", - "repeater_dangerZone": "Strefa Zagrożeń", + "repeater_encryptedAdvertInterval": "Zaszyfrowany InterwaÅ‚ Reklamowy", + "repeater_dangerZone": "Strefa ZagrożeÅ„", "repeater_rebootRepeater": "Zrestartuj Powtarzacz", - "repeater_rebootRepeaterSubtitle": "Zrestartuj urządzenie powtarzające.", - "repeater_rebootRepeaterConfirm": "Czy na pewno chcesz zrestartować ten repeater?", - "repeater_regenerateIdentityKey": "Wygeneruj klucz tożsamości", - "repeater_regenerateIdentityKeySubtitle": "Wygeneruj nową parę kluczy publicznych/prywatnych", - "repeater_regenerateIdentityKeyConfirm": "To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?", - "repeater_eraseFileSystem": "Wyczyść System Plików", - "repeater_eraseFileSystemSubtitle": "Sformatuj system plików powielacza", - "repeater_eraseFileSystemConfirm": "OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z powtarzacza. Nie da się tego cofnąć!", - "repeater_eraseSerialOnly": "Usunięcie jest dostępne tylko przez konsolę szeregową.", - "repeater_commandSent": "Polecenie wysłane: {command}", + "repeater_rebootRepeaterSubtitle": "Zrestartuj urzÄ…dzenie powtarzajÄ…ce.", + "repeater_rebootRepeaterConfirm": "Czy na pewno chcesz zrestartować ten repeater?", + "repeater_regenerateIdentityKey": "Wygeneruj klucz tożsamoÅ›ci", + "repeater_regenerateIdentityKeySubtitle": "Wygeneruj nowÄ… parÄ™ kluczy publicznych/prywatnych", + "repeater_regenerateIdentityKeyConfirm": "To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?", + "repeater_eraseFileSystem": "Wyczyść System Plików", + "repeater_eraseFileSystemSubtitle": "Sformatuj system plików powielacza", + "repeater_eraseFileSystemConfirm": "OSTRZEÅ»ENIE: To spowoduje usuniÄ™cie wszystkich danych z powtarzacza. Nie da siÄ™ tego cofnąć!", + "repeater_eraseSerialOnly": "UsuniÄ™cie jest dostÄ™pne tylko przez konsolÄ™ szeregowÄ….", + "repeater_commandSent": "Polecenie wysÅ‚ane: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Błąd podczas wysyłania polecenia: {error}", + "repeater_errorSendingCommand": "Błąd podczas wysyÅ‚ania polecenia: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "Potwierdź", - "repeater_settingsSaved": "Ustawienia zostały pomyślnie zapisane.", - "repeater_errorSavingSettings": "Błąd zapisu ustawień: {error}", + "repeater_confirm": "Potwierdź", + "repeater_settingsSaved": "Ustawienia zostaÅ‚y pomyÅ›lnie zapisane.", + "repeater_errorSavingSettings": "Błąd zapisu ustawieÅ„: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "Odśwież Podstawowe Ustawienia", - "repeater_refreshRadioSettings": "Odśwież Ustawienia Radio", - "repeater_refreshTxPower": "Odśwież TX power", - "repeater_refreshLocationSettings": "Odśwież Ustawienia Lokalizacji", - "repeater_refreshPacketForwarding": "Odśwież trasowanie pakietów", - "repeater_refreshGuestAccess": "Odśwież dostęp gościa", - "repeater_refreshPrivacyMode": "Odśwież Tryb Prywatności", - "repeater_refreshAdvertisementSettings": "Odśwież Ustawienia Reklamy", - "repeater_refreshed": "{label} odświeżone", + "repeater_refreshBasicSettings": "OdÅ›wież Podstawowe Ustawienia", + "repeater_refreshRadioSettings": "OdÅ›wież Ustawienia Radio", + "repeater_refreshTxPower": "OdÅ›wież TX power", + "repeater_refreshLocationSettings": "OdÅ›wież Ustawienia Lokalizacji", + "repeater_refreshPacketForwarding": "OdÅ›wież trasowanie pakietów", + "repeater_refreshGuestAccess": "OdÅ›wież dostÄ™p goÅ›cia", + "repeater_refreshPrivacyMode": "OdÅ›wież Tryb PrywatnoÅ›ci", + "repeater_refreshAdvertisementSettings": "OdÅ›wież Ustawienia Reklamy", + "repeater_refreshed": "{label} odÅ›wieżone", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Błąd podczas odświeżania {label}", + "repeater_errorRefreshing": "Błąd podczas odÅ›wieżania {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,17 +1091,17 @@ } }, "repeater_cliTitle": "Powtarzacz CLI", - "repeater_debugNextCommand": "Debug Następną Komendę", + "repeater_debugNextCommand": "Debug NastÄ™pnÄ… KomendÄ™", "repeater_commandHelp": "Pomoc", - "repeater_clearHistory": "Wyczyść historię", - "repeater_noCommandsSent": "Nie wysłano jeszcze żadnych poleceń", - "repeater_typeCommandOrUseQuick": "Wprowadź polecenie poniżej lub użyj szybkich poleceń", - "repeater_enterCommandHint": "Wprowadź polecenie...", + "repeater_clearHistory": "Wyczyść historiÄ™", + "repeater_noCommandsSent": "Nie wysÅ‚ano jeszcze żadnych poleceÅ„", + "repeater_typeCommandOrUseQuick": "Wprowadź polecenie poniżej lub użyj szybkich poleceÅ„", + "repeater_enterCommandHint": "Wprowadź polecenie...", "repeater_previousCommand": "Poprzednia komenda", - "repeater_nextCommand": "Następna komenda", - "repeater_enterCommandFirst": "Wprowadź najpierw polecenie", - "repeater_cliCommandFrameTitle": "Określony Wyraz Polecenia CLI", - "repeater_cliCommandError": "Błąd: {error}", + "repeater_nextCommand": "NastÄ™pna komenda", + "repeater_enterCommandFirst": "Wprowadź najpierw polecenie", + "repeater_cliCommandFrameTitle": "OkreÅ›lony Wyraz Polecenia CLI", + "repeater_cliCommandError": "Błąd: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1109,81 +1109,81 @@ } } }, - "repeater_cliQuickGetName": "Pobierz imię", + "repeater_cliQuickGetName": "Pobierz imiÄ™", "repeater_cliQuickGetRadio": "Uzyskaj Radio", "repeater_cliQuickGetTx": "Pobierz TX", - "repeater_cliQuickNeighbors": "Sąsiedzi", + "repeater_cliQuickNeighbors": "SÄ…siedzi", "repeater_cliQuickVersion": "Wersja", "repeater_cliQuickAdvertise": "Reklama", "repeater_cliQuickClock": "Godzina", - "repeater_cliHelpAdvert": "Wysyła pakiet reklamowy", - "repeater_cliHelpReboot": "Zresetuj urządzenie. (Uwaga, może pojawić się 'Timeout', co jest normalne)", - "repeater_cliHelpClock": "Wyświetla aktualny czas zgodnie z zegarem urządzenia.", - "repeater_cliHelpPassword": "Ustawia nowe hasło administratora dla urządzenia.", - "repeater_cliHelpVersion": "Wyświetla wersję urządzenia i datę budowy oprogramowania.", - "repeater_cliHelpClearStats": "Resetuje różne wskaźniki statystyk do zera.", + "repeater_cliHelpAdvert": "WysyÅ‚a pakiet reklamowy", + "repeater_cliHelpReboot": "Zresetuj urzÄ…dzenie. (Uwaga, może pojawić siÄ™ 'Timeout', co jest normalne)", + "repeater_cliHelpClock": "WyÅ›wietla aktualny czas zgodnie z zegarem urzÄ…dzenia.", + "repeater_cliHelpPassword": "Ustawia nowe hasÅ‚o administratora dla urzÄ…dzenia.", + "repeater_cliHelpVersion": "WyÅ›wietla wersjÄ™ urzÄ…dzenia i datÄ™ budowy oprogramowania.", + "repeater_cliHelpClearStats": "Resetuje różne wskaźniki statystyk do zera.", "repeater_cliHelpSetAf": "Ustawia czynnik czasu powietrznego.", - "repeater_cliHelpSetTx": "Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)", - "repeater_cliHelpSetRepeat": "Włącza lub wyłącza rolę powtarzacza dla tego węzła.", - "repeater_cliHelpSetAllowReadOnly": "(Serwer pokoju) Jeśli 'włączone', to logowanie z pustym hasłem będzie dozwolone, ale nie można publikować w pokoju (tylko czytać).", - "repeater_cliHelpSetFloodMax": "Ustawia maksymalną liczbę skoków pakietu powrotnego (jeśli >= max, pakiet nie jest przekierowywany)", - "repeater_cliHelpSetIntThresh": "Ustawia Próg Interferencji (w dB). Domyślnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceń kanału.", - "repeater_cliHelpSetAgcResetInterval": "Ustawia interwał do zresetowania Automatycznego Sterownika Głośności. Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetMultiAcks": "Włącza lub wyłącza funkcję 'podwójnych potwierdzeń'.", - "repeater_cliHelpSetAdvertInterval": "Ustawia interwał timera w minutach do wysyłania pakietu reklamy lokalnej (bezpośredniej). Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetFloodAdvertInterval": "Ustawia interwał timera w godzinach do wysłania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetGuestPassword": "Ustawia/aktualizuje hasło gościa. (dla repeaterów, loginy gości mogą wysyłać żądanie \"Get Stats\")", - "repeater_cliHelpSetName": "Ustawia nazwę reklamy.", - "repeater_cliHelpSetLat": "Ustawia współrzędną geograficzne (w stopniach dziesiętnych) mapy reklam.", - "repeater_cliHelpSetLon": "Ustawia współrzędną długościową mapy reklamy. (stopnie dziesiętne)", + "repeater_cliHelpSetTx": "Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)", + "repeater_cliHelpSetRepeat": "Włącza lub wyłącza rolÄ™ powtarzacza dla tego wÄ™zÅ‚a.", + "repeater_cliHelpSetAllowReadOnly": "(Serwer pokoju) JeÅ›li 'włączone', to logowanie z pustym hasÅ‚em bÄ™dzie dozwolone, ale nie można publikować w pokoju (tylko czytać).", + "repeater_cliHelpSetFloodMax": "Ustawia maksymalnÄ… liczbÄ™ skoków pakietu powrotnego (jeÅ›li >= max, pakiet nie jest przekierowywany)", + "repeater_cliHelpSetIntThresh": "Ustawia Próg Interferencji (w dB). DomyÅ›lnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceÅ„ kanaÅ‚u.", + "repeater_cliHelpSetAgcResetInterval": "Ustawia interwaÅ‚ do zresetowania Automatycznego Sterownika GÅ‚oÅ›noÅ›ci. Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetMultiAcks": "Włącza lub wyłącza funkcjÄ™ 'podwójnych potwierdzeÅ„'.", + "repeater_cliHelpSetAdvertInterval": "Ustawia interwaÅ‚ timera w minutach do wysyÅ‚ania pakietu reklamy lokalnej (bezpoÅ›redniej). Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetFloodAdvertInterval": "Ustawia interwaÅ‚ timera w godzinach do wysÅ‚ania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetGuestPassword": "Ustawia/aktualizuje hasÅ‚o goÅ›cia. (dla repeaterów, loginy goÅ›ci mogÄ… wysyÅ‚ać żądanie \"Get Stats\")", + "repeater_cliHelpSetName": "Ustawia nazwÄ™ reklamy.", + "repeater_cliHelpSetLat": "Ustawia współrzÄ™dnÄ… geograficzne (w stopniach dziesiÄ™tnych) mapy reklam.", + "repeater_cliHelpSetLon": "Ustawia współrzÄ™dnÄ… dÅ‚ugoÅ›ciowÄ… mapy reklamy. (stopnie dziesiÄ™tne)", "repeater_cliHelpSetRadio": "Ustawia nowe parametry radia i zapisuje je w preferencjach. Wymaga polecenia \"reboot\" do zastosowania.", - "repeater_cliHelpSetRxDelay": "Ustawienia (eksperymentalne) bazowe (muszą być > 1, aby działać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnału/wynik. Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetTxDelay": "Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyłanie (zmniejszając prawdopodobieństwo kolizji).", - "repeater_cliHelpSetDirectTxDelay": "Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpośrednim.", - "repeater_cliHelpSetBridgeEnabled": "Włącz/Wyłącz mostek.", - "repeater_cliHelpSetBridgeDelay": "Ustaw czas opóźnienia przed ponownym wysyłaniem pakietów.", - "repeater_cliHelpSetBridgeSource": "Wybierz, czy most będzie ponownie transmitował otrzymywane pakiety, czy też wysyłane.", - "repeater_cliHelpSetBridgeBaud": "Ustaw prędkość transmisji magistrali szeregowej dla mostów rs232.", - "repeater_cliHelpSetBridgeSecret": "Ustaw sekret dla mostów ESPNOW.", - "repeater_cliHelpSetAdcMultiplier": "Ustawia niestandardowy współczynnik do korekty zgłaszanego napięcia baterii (obsługa tylko na wybranych płytach).", - "repeater_cliHelpTempRadio": "Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a następnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).", - "repeater_cliHelpSetPerm": "Modyfikuje ACL. Usuwa dopasowaną wpis (z prefiksem pubkey), jeśli \"permissions\" wynosi zero. Dodaje nowy wpis, jeśli pubkey-hex ma pełną długość i nie znajduje się obecnie w ACL. Aktualizuje wpis, dopasowując prefiks pubkey. Bit uprawnień zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).", + "repeater_cliHelpSetRxDelay": "Ustawienia (eksperymentalne) bazowe (muszÄ… być > 1, aby dziaÅ‚ać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnaÅ‚u/wynik. Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetTxDelay": "Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyÅ‚anie (zmniejszajÄ…c prawdopodobieÅ„stwo kolizji).", + "repeater_cliHelpSetDirectTxDelay": "Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpoÅ›rednim.", + "repeater_cliHelpSetBridgeEnabled": "Włącz/Wyłącz mostek.", + "repeater_cliHelpSetBridgeDelay": "Ustaw czas opóźnienia przed ponownym wysyÅ‚aniem pakietów.", + "repeater_cliHelpSetBridgeSource": "Wybierz, czy most bÄ™dzie ponownie transmitowaÅ‚ otrzymywane pakiety, czy też wysyÅ‚ane.", + "repeater_cliHelpSetBridgeBaud": "Ustaw prÄ™dkość transmisji magistrali szeregowej dla mostów rs232.", + "repeater_cliHelpSetBridgeSecret": "Ustaw sekret dla mostów ESPNOW.", + "repeater_cliHelpSetAdcMultiplier": "Ustawia niestandardowy współczynnik do korekty zgÅ‚aszanego napiÄ™cia baterii (obsÅ‚uga tylko na wybranych pÅ‚ytach).", + "repeater_cliHelpTempRadio": "Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a nastÄ™pnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).", + "repeater_cliHelpSetPerm": "Modyfikuje ACL. Usuwa dopasowanÄ… wpis (z prefiksem pubkey), jeÅ›li \"permissions\" wynosi zero. Dodaje nowy wpis, jeÅ›li pubkey-hex ma peÅ‚nÄ… dÅ‚ugość i nie znajduje siÄ™ obecnie w ACL. Aktualizuje wpis, dopasowujÄ…c prefiks pubkey. Bit uprawnieÅ„ zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).", "repeater_cliHelpGetBridgeType": "Uzyskano typ mostu: brak, rs232, espnow", - "repeater_cliHelpLogStart": "Rozpoczyna się logowanie pakietów do systemu plików.", - "repeater_cliHelpLogStop": "Zatrzymuje logowanie pakietów do systemu plików.", - "repeater_cliHelpLogErase": "Usuwa logi pakietów z systemu plików.", - "repeater_cliHelpNeighbors": "Wyświetla listę innych węzłów powtarzających się, które usłyszano dzięki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Usuwa pierwszy pasujący wpis (z prefiksem pubkey (hex)) z listy sąsiadów.", - "repeater_cliHelpRegion": "(tylko seria) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.", - "repeater_cliHelpRegionLoad": "ZAPOMNIJ: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.", + "repeater_cliHelpLogStart": "Rozpoczyna siÄ™ logowanie pakietów do systemu plików.", + "repeater_cliHelpLogStop": "Zatrzymuje logowanie pakietów do systemu plików.", + "repeater_cliHelpLogErase": "Usuwa logi pakietów z systemu plików.", + "repeater_cliHelpNeighbors": "WyÅ›wietla listÄ™ innych wÄ™złów powtarzajÄ…cych siÄ™, które usÅ‚yszano dziÄ™ki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Usuwa pierwszy pasujÄ…cy wpis (z prefiksem pubkey (hex)) z listy sÄ…siadów.", + "repeater_cliHelpRegion": "(tylko seria) WyÅ›wietla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.", + "repeater_cliHelpRegionLoad": "ZAPOMNIJ: to jest specjalne wywoÅ‚anie wielokomendowe. Każda nastÄ™pna komenda jest nazwÄ… regionu (wciÄ™ta spacjami, aby wskazywać hierarchiÄ™ nadrzÄ™dnÄ…, z minimum jednÄ… spacjÄ…). ZakoÅ„czona wysÅ‚aniem pustej linii/komendy.", "repeater_cliHelpRegionGet": "Wyszukuje region o podanej nazwie prefiksu (lub \"\" dla zakresu globalnego). Odpowiada \"-> region-name (parent-name) 'F'\"", - "repeater_cliHelpRegionPut": "Dodaje lub aktualizuje definicję regionu z podaną nazwą.", - "repeater_cliHelpRegionRemove": "Usuwa definicję regionu o podanej nazwie. (musi się dokładnie zgadzać i nie może mieć podregionów).", - "repeater_cliHelpRegionAllowf": "Ustawia uprawnienia 'P'łytkowe dla podanego regionu. ('' dla zakresu globalnego/starszego)", - "repeater_cliHelpRegionDenyf": "Usuwa uprawnienie 'Pływające' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).", - "repeater_cliHelpRegionHome": "Odpowiada z aktualnej 'home' region. (Uwaga: nie zostało jeszcze zastosowane, zarezerwowane na przyszłość).", + "repeater_cliHelpRegionPut": "Dodaje lub aktualizuje definicjÄ™ regionu z podanÄ… nazwÄ….", + "repeater_cliHelpRegionRemove": "Usuwa definicjÄ™ regionu o podanej nazwie. (musi siÄ™ dokÅ‚adnie zgadzać i nie może mieć podregionów).", + "repeater_cliHelpRegionAllowf": "Ustawia uprawnienia 'P'Å‚ytkowe dla podanego regionu. ('' dla zakresu globalnego/starszego)", + "repeater_cliHelpRegionDenyf": "Usuwa uprawnienie 'PÅ‚ywajÄ…ce' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca siÄ™ używania tego na globalnym/starszym zakresie!!).", + "repeater_cliHelpRegionHome": "Odpowiada z aktualnej 'home' region. (Uwaga: nie zostaÅ‚o jeszcze zastosowane, zarezerwowane na przyszÅ‚ość).", "repeater_cliHelpRegionHomeSet": "Ustawia region 'domowe'.", - "repeater_cliHelpRegionSave": "Zapisuje listę/mapę regionów do pamięci.", - "repeater_cliHelpGps": "Wyświetla status GPS. Jeśli GPS jest wyłączony, odpowiada tylko \"off\", jeśli jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbą satelitów.", - "repeater_cliHelpGpsOnOff": "Włącza/wyłącza nawigację GPS.", - "repeater_cliHelpGpsSync": "Synchronizuje czas węzła z zegarem GPS.", - "repeater_cliHelpGpsSetLoc": "Ustawia pozycję węzła na współrzędne GPS i zapisuje preferencje.", - "repeater_cliHelpGpsAdvert": "Udostępnia konfigurację reklamy lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w reklamach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: reklamuj lokalizację przechowywaną w ustawieniach", - "repeater_cliHelpGpsAdvertSet": "Ustawia konfigurację reklamy w lokalizacji.", - "repeater_commandsListTitle": "Lista poleceń", - "repeater_commandsListNote": "ZAPAMIĘTAJ: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".", - "repeater_general": "Ogólne", + "repeater_cliHelpRegionSave": "Zapisuje listÄ™/mapÄ™ regionów do pamiÄ™ci.", + "repeater_cliHelpGps": "WyÅ›wietla status GPS. JeÅ›li GPS jest wyłączony, odpowiada tylko \"off\", jeÅ›li jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbÄ… satelitów.", + "repeater_cliHelpGpsOnOff": "Włącza/wyłącza nawigacjÄ™ GPS.", + "repeater_cliHelpGpsSync": "Synchronizuje czas wÄ™zÅ‚a z zegarem GPS.", + "repeater_cliHelpGpsSetLoc": "Ustawia pozycjÄ™ wÄ™zÅ‚a na współrzÄ™dne GPS i zapisuje preferencje.", + "repeater_cliHelpGpsAdvert": "UdostÄ™pnia konfiguracjÄ™ reklamy lokalizacji wÄ™zÅ‚a:\n- brak: nie uwzglÄ™dniaj lokalizacji w reklamach\n- udostÄ™pnia: udostÄ™pnia lokalizacjÄ™ GPS (z SensorManager)\n- ustawienia: reklamuj lokalizacjÄ™ przechowywanÄ… w ustawieniach", + "repeater_cliHelpGpsAdvertSet": "Ustawia konfiguracjÄ™ reklamy w lokalizacji.", + "repeater_commandsListTitle": "Lista poleceÅ„", + "repeater_commandsListNote": "ZAPAMIĘTAJ: dla różnych poleceÅ„ \"set ...\" istnieje również polecenie \"get ...\".", + "repeater_general": "Ogólne", "repeater_settingsCategory": "Ustawienia", "repeater_bridge": "Most", "repeater_logging": "Rejestrowanie", - "repeater_neighborsRepeaterOnly": "Sąsiedzi (tylko powtarzacz)", - "repeater_regionManagementRepeaterOnly": "Zarządzanie Regionem (tylko Powtarzacz)", - "repeater_regionNote": "Wprowadzono komendy regionalne w celu zarządzania definicjami i uprawnieniami regionów.", - "repeater_gpsManagement": "Zarządzanie GPS", - "repeater_gpsNote": "Polecenie GPS zostało wprowadzone w celu zarządzania tematami związanymi z lokalizacją.", + "repeater_neighborsRepeaterOnly": "SÄ…siedzi (tylko powtarzacz)", + "repeater_regionManagementRepeaterOnly": "ZarzÄ…dzanie Regionem (tylko Powtarzacz)", + "repeater_regionNote": "Wprowadzono komendy regionalne w celu zarzÄ…dzania definicjami i uprawnieniami regionów.", + "repeater_gpsManagement": "ZarzÄ…dzanie GPS", + "repeater_gpsNote": "Polecenie GPS zostaÅ‚o wprowadzone w celu zarzÄ…dzania tematami zwiÄ…zanymi z lokalizacjÄ….", "telemetry_receivedData": "Otrzymano Dane Telemetrii", - "telemetry_requestTimeout": "Życzenie o danych telemetrycznych nie udało się.", - "telemetry_errorLoading": "Błąd podczas ładowania telemetry: {error}", + "telemetry_requestTimeout": "Å»yczenie o danych telemetrycznych nie udaÅ‚o siÄ™.", + "telemetry_errorLoading": "Błąd podczas Å‚adowania telemetry: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,8 +1191,8 @@ } } }, - "telemetry_noData": "Brak dostępnych danych telemetrycznych.", - "telemetry_channelTitle": "Kanał {channel}", + "telemetry_noData": "Brak dostÄ™pnych danych telemetrycznych.", + "telemetry_channelTitle": "KanaÅ‚ {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1201,7 +1201,7 @@ } }, "telemetry_batteryLabel": "Bateria", - "telemetry_voltageLabel": "Napięcie", + "telemetry_voltageLabel": "NapiÄ™cie", "telemetry_mcuTemperatureLabel": "Temperatura MCU", "telemetry_temperatureLabel": "Temperatura", "telemetry_currentLabel": "Obecny", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Ścieżka pakietu", - "channelPath_viewMap": "Wyświetl mapę", - "channelPath_otherObservedPaths": "Inne Zauważone Ścieżki", - "channelPath_repeaterHops": "Skoki Powtórki", - "channelPath_noHopDetails": "Szczegóły dotyczące tego pakietu nie zostały podane.", - "channelPath_messageDetails": "Szczegóły wiadomości", + "channelPath_title": "Åšcieżka pakietu", + "channelPath_viewMap": "WyÅ›wietl mapÄ™", + "channelPath_otherObservedPaths": "Inne Zauważone Åšcieżki", + "channelPath_repeaterHops": "Skoki Powtórki", + "channelPath_noHopDetails": "Szczegóły dotyczÄ…ce tego pakietu nie zostaÅ‚y podane.", + "channelPath_messageDetails": "Szczegóły wiadomoÅ›ci", "channelPath_senderLabel": "Nadawca", "channelPath_timeLabel": "Czas", - "channelPath_repeatsLabel": "Powtórzenia", - "channelPath_pathLabel": "Ścieżka {index}", + "channelPath_repeatsLabel": "Powtórzenia", + "channelPath_pathLabel": "Åšcieżka {index}", "channelPath_observedLabel": "Obserwowane", - "channelPath_observedPathTitle": "Obserwowany ścieżka {index} • {hops}", + "channelPath_observedPathTitle": "Obserwowany Å›cieżka {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1290,8 +1290,8 @@ }, "channelPath_unknownPath": "Nieznane", "channelPath_floodPath": "Powodzenie", - "channelPath_directPath": "Bezpośrednio", - "channelPath_observedZeroOf": "0 z {total} skoków", + "channelPath_directPath": "BezpoÅ›rednio", + "channelPath_observedZeroOf": "0 z {total} skoków", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1299,7 +1299,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} z {total} skoków", + "channelPath_observedSomeOf": "{observed} z {total} skoków", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1310,9 +1310,9 @@ } } }, - "channelPath_mapTitle": "Mapa ścieżek", - "channelPath_noRepeaterLocations": "Brak dostępnych lokalizacji powtarzaczy dla tego ścieżki.", - "channelPath_primaryPath": "Ścieżka {index} (Główna)", + "channelPath_mapTitle": "Mapa Å›cieżek", + "channelPath_noRepeaterLocations": "Brak dostÄ™pnych lokalizacji powtarzaczy dla tego Å›cieżki.", + "channelPath_primaryPath": "Åšcieżka {index} (Główna)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1327,9 +1327,9 @@ } } }, - "channelPath_pathLabelTitle": "Ścieżka", - "channelPath_observedPathHeader": "Obserwowana ścieżka", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Åšcieżka", + "channelPath_observedPathHeader": "Obserwowana Å›cieżka", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,16 +1340,16 @@ } } }, - "channelPath_noHopDetailsAvailable": "Brak dostępnych szczegółów hopa dla tego pakietu.", + "channelPath_noHopDetailsAvailable": "Brak dostÄ™pnych szczegółów hopa dla tego pakietu.", "channelPath_unknownRepeater": "Nieznany Powtarzacz", "listFilter_tooltip": "Filtruj i sortuj", "listFilter_sortBy": "Sortuj po", - "listFilter_latestMessages": "Najnowsze wiadomości", - "listFilter_heardRecently": "Słyszano niedawno", + "listFilter_latestMessages": "Najnowsze wiadomoÅ›ci", + "listFilter_heardRecently": "SÅ‚yszano niedawno", "listFilter_az": "A-Z", "listFilter_filters": "Filtry", "listFilter_all": "Wszystko", - "listFilter_users": "Użytkownicy", + "listFilter_users": "Użytkownicy", "listFilter_repeaters": "Powtarzacze", "listFilter_roomServers": "Serwery pokoju", "listFilter_unreadOnly": "Tylko nieprzeczytane", @@ -1361,25 +1361,25 @@ } } }, - "repeater_neighbors": "Sąsiedzi", - "repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.", - "neighbors_receivedData": "Otrzymano dane sąsiedztwa", - "neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.", - "neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}", - "neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi", - "neighbors_noData": "Brak danych dotyczących sąsiadów.", - "channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.", - "channels_createPrivateChannel": "Utwórz Prywatny Kanał", + "repeater_neighbors": "SÄ…siedzi", + "repeater_neighborsSubtitle": "WyÅ›wietl sÄ…siedztwo zerowych hopów.", + "neighbors_receivedData": "Otrzymano dane sÄ…siedztwa", + "neighbors_requestTimedOut": "SÄ…siedzi proszÄ… o wyłączenie timingu.", + "neighbors_errorLoading": "Błąd podczas Å‚adowania sÄ…siadów: {error}", + "neighbors_repeatersNeighbors": "Powtarzacze SÄ…siedzi", + "neighbors_noData": "Brak danych dotyczÄ…cych sÄ…siadów.", + "channels_joinPrivateChannelDesc": "RÄ™cznie wprowadź klucz tajny.", + "channels_createPrivateChannel": "Utwórz Prywatny KanaÅ‚", "channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.", - "channels_joinPrivateChannel": "Dołącz do Prywatnego Kanału", - "channels_joinPublicChannel": "Dołącz do kanału publicznego.", - "channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanału.", - "channels_joinHashtagChannel": "Dołącz do kanału oznaczanego hashtagiem", - "channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.", + "channels_joinPrivateChannel": "Dołącz do Prywatnego KanaÅ‚u", + "channels_joinPublicChannel": "Dołącz do kanaÅ‚u publicznego.", + "channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanaÅ‚u.", + "channels_joinHashtagChannel": "Dołącz do kanaÅ‚u oznaczanego hashtagiem", + "channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.", "channels_scanQrCode": "Skanuj kod QR", - "channels_scanQrCodeComingSoon": "Wkrótce", - "channels_enterHashtag": "Wprowadź hashtag", - "channels_hashtagHint": "np. #zespół", + "channels_scanQrCodeComingSoon": "Wkrótce", + "channels_enterHashtag": "Wprowadź hashtag", + "channels_hashtagHint": "np. #zespół", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_heardAgo": "Usłyszano: {time} temu", + "neighbors_heardAgo": "UsÅ‚yszano: {time} temu", "neighbors_unknownContact": "Nieznana {pubkey}", - "settings_locationGPSEnable": "Włącz GPS", - "settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.", - "settings_locationIntervalSec": "Interwał dla GPS (Sekundy)", - "settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.", - "contacts_manageRoom": "Zarządzaj Serwerem Pokoju", - "room_management": "Zarządzanie Serwerem Pokoju", + "settings_locationGPSEnable": "Włącz GPS", + "settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocÄ… GPS.", + "settings_locationIntervalSec": "InterwaÅ‚ dla GPS (Sekundy)", + "settings_locationIntervalInvalid": "InterwaÅ‚ musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.", + "contacts_manageRoom": "ZarzÄ…dzaj Serwerem Pokoju", + "room_management": "ZarzÄ…dzanie Serwerem Pokoju", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1458,36 +1458,36 @@ } } }, - "community_createDesc": "Utwórz nową społeczność i udostępnij za pomocą kodu QR.", - "community_title": "Społeczność", - "community_create": "Utwórz Społeczność", + "community_createDesc": "Utwórz nowÄ… spoÅ‚eczność i udostÄ™pnij za pomocÄ… kodu QR.", + "community_title": "SpoÅ‚eczność", + "community_create": "Utwórz SpoÅ‚eczność", "common_ok": "OK", - "community_join": "Dołącz", - "community_joinTitle": "Dołącz do społeczności", - "community_joinConfirmation": "Czy chcesz dołączyć do społeczności \"{name}\"?", - "community_scanQr": "Skanuj QR kod społeczności", - "community_scanInstructions": "Skieruj kamerę w kierunku kodu QR społeczności.", - "community_showQr": "Pokaż kod QR", - "community_publicChannel": "Społeczność Publiczna", - "community_hashtagChannel": "Hashtag Społeczności", - "community_name": "Nazwa Społeczności", - "community_enterName": "Wprowadź nazwę społeczności", - "community_created": "Społeczność \"{name}\" została utworzona", - "community_joined": "Dołączył do społeczności \"{name}\"", - "community_qrTitle": "Dziel się Społecznością", - "community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}", - "community_hashtagPrivacyHint": "Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności", - "community_invalidQrCode": "Nieprawidłowy kod QR społeczności.", - "community_alreadyMember": "Już jesteś członkiem.", - "community_alreadyMemberMessage": "Jesteś już członkiem \"{name}\".", - "community_addPublicChannel": "Dodaj Kanał Publiczny Społeczności", - "community_addPublicChannelHint": "Automatycznie dodaj kanał publiczny dla tej społeczności.", - "community_noCommunities": "Nie dołączono jeszcze żadnych społeczności.", - "community_scanOrCreate": "Skanuj kod QR lub utwórz społeczność, aby zacząć.", - "community_manageCommunities": "Zarządzaj Grupami", - "community_delete": "Opuszczenie Społeczności", - "community_deleteConfirm": "Opuścić \"{name}\"?", - "community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.", + "community_join": "Dołącz", + "community_joinTitle": "Dołącz do spoÅ‚ecznoÅ›ci", + "community_joinConfirmation": "Czy chcesz dołączyć do spoÅ‚ecznoÅ›ci \"{name}\"?", + "community_scanQr": "Skanuj QR kod spoÅ‚ecznoÅ›ci", + "community_scanInstructions": "Skieruj kamerÄ™ w kierunku kodu QR spoÅ‚ecznoÅ›ci.", + "community_showQr": "Pokaż kod QR", + "community_publicChannel": "SpoÅ‚eczność Publiczna", + "community_hashtagChannel": "Hashtag SpoÅ‚ecznoÅ›ci", + "community_name": "Nazwa SpoÅ‚ecznoÅ›ci", + "community_enterName": "Wprowadź nazwÄ™ spoÅ‚ecznoÅ›ci", + "community_created": "SpoÅ‚eczność \"{name}\" zostaÅ‚a utworzona", + "community_joined": "DołączyÅ‚ do spoÅ‚ecznoÅ›ci \"{name}\"", + "community_qrTitle": "Dziel siÄ™ SpoÅ‚ecznoÅ›ciÄ…", + "community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}", + "community_hashtagPrivacyHint": "KanaÅ‚y hashtagowe spoÅ‚ecznoÅ›ci sÄ… dostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci", + "community_invalidQrCode": "NieprawidÅ‚owy kod QR spoÅ‚ecznoÅ›ci.", + "community_alreadyMember": "Już jesteÅ› czÅ‚onkiem.", + "community_alreadyMemberMessage": "JesteÅ› już czÅ‚onkiem \"{name}\".", + "community_addPublicChannel": "Dodaj KanaÅ‚ Publiczny SpoÅ‚ecznoÅ›ci", + "community_addPublicChannelHint": "Automatycznie dodaj kanaÅ‚ publiczny dla tej spoÅ‚ecznoÅ›ci.", + "community_noCommunities": "Nie dołączono jeszcze żadnych spoÅ‚ecznoÅ›ci.", + "community_scanOrCreate": "Skanuj kod QR lub utwórz spoÅ‚eczność, aby zacząć.", + "community_manageCommunities": "ZarzÄ…dzaj Grupami", + "community_delete": "Opuszczenie SpoÅ‚ecznoÅ›ci", + "community_deleteConfirm": "OpuÅ›cić \"{name}\"?", + "community_deleteChannelsWarning": "Spowoduje to również usuniÄ™cie {count} kanaÅ‚u/kanałów i ich wiadomoÅ›ci.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Opuszczono społeczność \"{name}\"", - "community_addHashtagChannel": "Dodaj hashtag społeczności", - "community_addHashtagChannelDesc": "Dodaj kanał z hashtagiem dla tej społeczności", - "community_selectCommunity": "Wybierz społeczność", + "community_deleted": "Opuszczono spoÅ‚eczność \"{name}\"", + "community_addHashtagChannel": "Dodaj hashtag spoÅ‚ecznoÅ›ci", + "community_addHashtagChannelDesc": "Dodaj kanaÅ‚ z hashtagiem dla tej spoÅ‚ecznoÅ›ci", + "community_selectCommunity": "Wybierz spoÅ‚eczność", "community_regularHashtag": "Hashtag regular", - "community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)", - "community_communityHashtag": "Hashtag Społeczności", - "community_communityHashtagDesc": "Dostępne tylko dla członków społeczności", + "community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)", + "community_communityHashtag": "Hashtag SpoÅ‚ecznoÅ›ci", + "community_communityHashtagDesc": "DostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci", "community_forCommunity": "Dla {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1533,11 +1533,11 @@ } }, "community_regenerate": "Zregeneruj", - "community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"", + "community_secretRegenerated": "HasÅ‚o ponownie wygenerowane dla \"{name}\"", "community_regenerateSecret": "Zregeneruj sekret", - "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.", - "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"", - "community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"", + "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy czÅ‚onkowie bÄ™dÄ… musieli zeskanować nowy kod QR, aby kontynuować komunikacjÄ™.", + "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"", + "community_secretUpdated": "HasÅ‚o zaktualizowane dla \"{name}\"", "community_updateSecret": "Zaktualizuj tajny klucz", "@contacts_pathTraceTo": { "placeholders": { @@ -1547,81 +1547,81 @@ } }, "pathTrace_you": "Ty", - "pathTrace_failed": "Śledzenie ścieżki nie powiodło się.", - "pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.", - "contacts_pathTrace": "Śledzenie Ścieżek", - "contacts_ping": "Pingować", - "contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera", - "contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego", + "pathTrace_failed": "Åšledzenie Å›cieżki nie powiodÅ‚o siÄ™.", + "pathTrace_notAvailable": "Åšcieżka Å›ledzenia niedostÄ™pna.", + "contacts_pathTrace": "Åšledzenie Åšcieżek", + "contacts_ping": "Pingować", + "contacts_repeaterPathTrace": "Åšledzenie Å›cieżki do repeatera", + "contacts_roomPathTrace": "Åšledzenie Å›cieżki do serwera pokojowego", "contacts_roomPing": "Pinguj serwer pokoju", - "pathTrace_refreshTooltip": "Odśwież ścieżkę.", + "pathTrace_refreshTooltip": "OdÅ›wież Å›cieżkÄ™.", "contacts_repeaterPing": "Repeater pingowy", - "contacts_pathTraceTo": "Śledź trasę do {name}", - "contacts_chatTraceRoute": "Śledź trasę promienia", + "contacts_pathTraceTo": "Åšledź trasÄ™ do {name}", + "contacts_chatTraceRoute": "Åšledź trasÄ™ promienia", "appSettings_languageRu": "Rosyjski", - "appSettings_languageUk": "Ukraińska", - "appSettings_enableMessageTracing": "Włącz śledzenie wiadomości", - "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości", - "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", + "appSettings_languageUk": "UkraiÅ„ska", + "appSettings_enableMessageTracing": "Włącz Å›ledzenie wiadomoÅ›ci", + "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomoÅ›ci", + "contacts_contactImportFailed": "Kontakt nie zostaÅ‚ zaimportowany.", "contacts_zeroHopAdvert": "Reklama Zero Hop", "contacts_floodAdvert": "Reklama powodziowa", - "contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka", + "contacts_copyAdvertToClipboard": "Kopiuj ogÅ‚oszenie do schowka", "contacts_clipboardEmpty": "Schowek jest pusty.", - "contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe", + "contacts_invalidAdvertFormat": "NieprawidÅ‚owe dane kontaktowe", "contacts_addContactFromClipboard": "Dodaj kontakt z schowka", - "contacts_contactImported": "Kontakt został zaimportowany.", - "contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.", + "contacts_contactImported": "Kontakt zostaÅ‚ zaimportowany.", + "contacts_zeroHopContactAdvertSent": "WysÅ‚ano kontakt przez ogÅ‚oszenie.", "contacts_contactAdvertCopied": "Reklama skopiowana do schowka.", - "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", - "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", + "contacts_contactAdvertCopyFailed": "Kopiowanie ogÅ‚oszenia do schowka nie powiodÅ‚o siÄ™.", + "contacts_ShareContactZeroHop": "UdostÄ™pnij kontakt przez ogÅ‚oszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", - "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", - "notification_activityTitle": "Aktywność MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}", - "notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}", + "contacts_zeroHopContactAdvertFailed": "Nie udaÅ‚o siÄ™ wysÅ‚ać kontaktu.", + "notification_activityTitle": "Aktywność MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomoÅ›ci} many{wiadomoÅ›ci} other{wiadomoÅ›ci}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanaÅ‚u} few{wiadomoÅ›ci kanaÅ‚u} many{wiadomoÅ›ci kanaÅ‚u} other{wiadomoÅ›ci kanaÅ‚u}}", + "notification_newNodesCount": "{count} {count, plural, =1{nowy wÄ™zeÅ‚} few{nowe wÄ™zÅ‚y} many{nowych wÄ™złów} other{nowych wÄ™złów}}", "notification_newTypeDiscovered": "Nowy {contactType} wykryty", - "notification_receivedNewMessage": "Otrzymano nową wiadomość", + "notification_receivedNewMessage": "Otrzymano nowÄ… wiadomość", "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", - "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", - "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", - "settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.", - "settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym", - "settings_gpxExportError": "Wystąpił błąd podczas eksportowania.", - "settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzających i pomieszczeń", - "settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.", + "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", + "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacjÄ… do pliku GPX.", + "settings_gpxExportSuccess": "PomyÅ›lnie wyeksportowano plik GPX.", + "settings_gpxExportNotAvailable": "Nie obsÅ‚ugiwane na Twoim urzÄ…dzeniu/systemie operacyjnym", + "settings_gpxExportError": "WystÄ…piÅ‚ błąd podczas eksportowania.", + "settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzajÄ…cych i pomieszczeÅ„", + "settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacjÄ… do pliku GPX.", "settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX", - "settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.", - "settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów", - "settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.", + "settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacjÄ… do pliku GPX.", + "settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów", + "settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.", "settings_gpxExportChat": "Lokalizacje towarzyszy", "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open", - "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!", - "map_pathTraceCancelled": "Śledzenie ścieżki anulowano.", - "map_runTrace": "Uruchom ślad ścieżki", - "pathTrace_clearTooltip": "Wyczyść ścieżkę", - "map_removeLast": "Usuń ostatni", - "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", - "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", - "scanner_chromeRequired": "Wymagana przeglądarka Chrome", - "scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.", - "scanner_bluetoothOff": "Bluetooth jest wyłączony", - "scanner_enableBluetooth": "Włącz Bluetooth", + "pathTrace_someHopsNoLocation": "Jeden lub wiÄ™cej z chmieli nie ma okreÅ›lonej lokalizacji!", + "map_pathTraceCancelled": "Åšledzenie Å›cieżki anulowano.", + "map_runTrace": "Uruchom Å›lad Å›cieżki", + "pathTrace_clearTooltip": "Wyczyść Å›cieżkÄ™", + "map_removeLast": "UsuÅ„ ostatni", + "map_tapToAdd": "Kliknij na wÄ™zÅ‚y, aby dodać je do Å›cieżki.", + "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urzÄ…dzenia.", + "scanner_chromeRequired": "Wymagana przeglÄ…darka Chrome", + "scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglÄ…darki Google Chrome lub opartej na Chromium do obsÅ‚ugi Bluetooth.", + "scanner_bluetoothOff": "Bluetooth jest wyłączony", + "scanner_enableBluetooth": "Włącz Bluetooth", "snrIndicator_lastSeen": "Ostatnio widziany", - "snrIndicator_nearByRepeaters": "Nadajniki w pobliżu", - "chat_ShowAllPaths": "Pokaż wszystkie ścieżki", - "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", - "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", - "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.", - "settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)", + "snrIndicator_nearByRepeaters": "Nadajniki w pobliżu", + "chat_ShowAllPaths": "Pokaż wszystkie Å›cieżki", + "settings_clientRepeatSubtitle": "Pozwól temu urzÄ…dzeniu powtarzać pakiety danych dla innych urzÄ…dzeÅ„.", + "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", + "settings_clientRepeatFreqWarning": "Powtórka poza sieciÄ… wymaga czÄ™stotliwoÅ›ci 433, 869 lub 918 MHz.", + "settings_aboutOpenMeteoAttribution": "Dane wysokoÅ›ciowe LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Jednostki", "appSettings_unitsMetric": "Metryczne (m / km)", "appSettings_unitsImperial": "Imperialne (ft / mi)", "map_lineOfSight": "Linia wzroku", "map_losScreenTitle": "Linia wzroku", - "losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.", - "losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}", + "losSelectStartEnd": "Wybierz wÄ™zÅ‚y poczÄ…tkowe i koÅ„cowe dla LOS.", + "losRunFailed": "Sprawdzenie pola widzenia nie powiodÅ‚o siÄ™: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,11 +1629,11 @@ } } }, - "losClearAllPoints": "Wyczyść wszystkie punkty", - "losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości", + "losClearAllPoints": "Wyczyść wszystkie punkty", + "losRunToViewElevationProfile": "Uruchom LOS, aby wyÅ›wietlić profil wysokoÅ›ci", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty", - "losShowDisplayNodes": "Pokaż węzły wyświetlające", + "losMenuSubtitle": "Stuknij wÄ™zÅ‚y lub naciÅ›nij i przytrzymaj mapÄ™, aby uzyskać niestandardowe punkty", + "losShowDisplayNodes": "Pokaż wÄ™zÅ‚y wyÅ›wietlajÄ…ce", "losCustomPoints": "Punkty niestandardowe", "losCustomPointLabel": "Niestandardowe {index}", "@losCustomPointLabel": { @@ -1668,8 +1668,8 @@ } }, "losRun": "Uruchom LOS-a", - "losNoElevationData": "Brak danych o wysokości", - "losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}", + "losNoElevationData": "Brak danych o wysokoÅ›ci", + "losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny przeÅ›wit {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.", - "losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.", - "losRenameCustomPoint": "Zmień nazwę punktu niestandardowego", + "losErrorElevationUnavailable": "Dane dotyczÄ…ce wysokoÅ›ci sÄ… niedostÄ™pne dla jednej lub wiÄ™kszej liczby próbek.", + "losErrorInvalidInput": "NieprawidÅ‚owe dane punktów/wysokoÅ›ci do obliczenia LOS.", + "losRenameCustomPoint": "ZmieÅ„ nazwÄ™ punktu niestandardowego", "losPointName": "Nazwa punktu", - "losShowPanelTooltip": "Pokaż panel LOS", + "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", - "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)", + "losElevationAttribution": "Dane dotyczÄ…ce wysokoÅ›ci: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Horyzont radiowy", - "losLegendLosBeam": "Linia widoczności", + "losLegendLosBeam": "Linia widocznoÅ›ci", "losLegendTerrain": "Teren", - "losFrequencyLabel": "Częstotliwość", - "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", + "losFrequencyLabel": "CzÄ™stotliwość", + "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", - "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", + "losFrequencyDialogDescription": "ZaczynajÄ…c od k={baselineK} przy {baselineFreq} MHz, obliczenia korygujÄ… współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,7 +1753,7 @@ } } }, - "listFilter_removeFromFavorites": "Usuń z ulubionych", + "listFilter_removeFromFavorites": "UsuÅ„ z ulubionych", "listFilter_addToFavorites": "Dodaj do ulubionych", "listFilter_favorites": "Ulubione", "@contacts_searchFavorites": { @@ -1799,16 +1799,14 @@ "contacts_unread": "Nieprzeczytane", "contacts_searchContactsNoNumber": "Wyszukaj kontakty...", "contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...", - "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", - "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", - "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...", + "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", + "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", + "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...", "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceSubtitle": "Wybierz, w jaki sposób chcesz uzyskać dostęp do swojego urządzenia MeshCore.", - "connectionChoiceTitle": "Wybierz metodę połączenia.", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "Połącz przez USB", - "usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.", - "usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.", - "usbScreenStatus": "Wybierz urządzenie USB", - "usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj." + "usbScreenTitle": "Połącz przez USB", + "usbScreenNote": "Port szeregowy USB jest aktywny na urzÄ…dzeniach z Androidem i platformach stacjonarnych, które obsÅ‚ugujÄ… tÄ™ funkcjÄ™.", + "usbScreenSubtitle": "Wybierz wykryty urzÄ…dzenie szeregowe i podłącz je bezpoÅ›rednio do swojego wÄ™zÅ‚a MeshCore.", + "usbScreenStatus": "Wybierz urzÄ…dzenie USB", + "usbScreenEmptyState": "Nie znaleziono żadnych urzÄ…dzeÅ„ USB. Podłącz jedno i zaktualizuj." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index d6d50e8..883b222 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -20,7 +20,7 @@ "common_close": "Fechar", "common_edit": "Editar", "common_add": "Adicionar", - "common_settings": "Configurações", + "common_settings": "Configurações", "common_disconnect": "Desconectar", "common_connected": "Conectado", "common_disconnected": "Desconectado", @@ -35,7 +35,7 @@ "common_disable": "Desativar", "common_reboot": "Reiniciar", "common_loading": "Carregando...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -56,7 +56,7 @@ "scanner_scanning": "Procurando por dispositivos...", "scanner_connecting": "Conectando...", "scanner_disconnecting": "Desconectando...", - "scanner_notConnected": "Não está conectado", + "scanner_notConnected": "Não está conectado", "scanner_connectedTo": "Conectado a {deviceName}", "@scanner_connectedTo": { "placeholders": { @@ -67,7 +67,7 @@ }, "scanner_searchingDevices": "Procurando dispositivos MeshCore...", "scanner_tapToScan": "Toque em \"Escanear\" para encontrar dispositivos MeshCore", - "scanner_connectionFailed": "Falha na conexão: {error}", + "scanner_connectionFailed": "Falha na conexão: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -79,47 +79,47 @@ "scanner_scan": "Digitalizar", "device_quickSwitch": "Mudar rapidamente", "device_meshcore": "MeshCore", - "settings_title": "Configurações", - "settings_deviceInfo": "Informações do Dispositivo", - "settings_appSettings": "Configurações do App", - "settings_appSettingsSubtitle": "Notificações, mensagens e preferências de mapa", - "settings_nodeSettings": "Configurações do Nó", - "settings_nodeName": "Nome do Nó", - "settings_nodeNameNotSet": "Não definido", - "settings_nodeNameHint": "Insira o nome do nó", + "settings_title": "Configurações", + "settings_deviceInfo": "Informações do Dispositivo", + "settings_appSettings": "Configurações do App", + "settings_appSettingsSubtitle": "Notificações, mensagens e preferências de mapa", + "settings_nodeSettings": "Configurações do Nó", + "settings_nodeName": "Nome do Nó", + "settings_nodeNameNotSet": "Não definido", + "settings_nodeNameHint": "Insira o nome do nó", "settings_nodeNameUpdated": "Nome atualizado", - "settings_radioSettings": "Configurações de Rádio", - "settings_radioSettingsSubtitle": "Frequência, potência, fator de espalhamento", - "settings_radioSettingsUpdated": "Configurações de rádio atualizadas", - "settings_location": "Localização", + "settings_radioSettings": "Configurações de Rádio", + "settings_radioSettingsSubtitle": "Frequência, potência, fator de espalhamento", + "settings_radioSettingsUpdated": "Configurações de rádio atualizadas", + "settings_location": "Localização", "settings_locationSubtitle": "Coordenadas GPS", - "settings_locationUpdated": "Localização atualizada", + "settings_locationUpdated": "Localização atualizada", "settings_locationBothRequired": "Insira a latitude e a longitude.", - "settings_locationInvalid": "Latitude ou longitude inválidos.", + "settings_locationInvalid": "Latitude ou longitude inválidos.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", "settings_privacyMode": "Modo de Privacidade", - "settings_privacyModeSubtitle": "Esconder nome/localização em anúncios", - "settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.", + "settings_privacyModeSubtitle": "Esconder nome/localização em anúncios", + "settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.", "settings_privacyModeEnabled": "Modo de privacidade ativado", "settings_privacyModeDisabled": "Modo de privacidade desativado", - "settings_actions": "Ações", + "settings_actions": "Ações", "settings_sendAdvertisement": "Enviar Publicidade", - "settings_sendAdvertisementSubtitle": "Presença de transmissão agora", - "settings_advertisementSent": "Anúncio enviado", - "settings_syncTime": "Tempo de Sincronização", - "settings_syncTimeSubtitle": "Definir o relógio do dispositivo para o horário do telefone", + "settings_sendAdvertisementSubtitle": "Presença de transmissão agora", + "settings_advertisementSent": "Anúncio enviado", + "settings_syncTime": "Tempo de Sincronização", + "settings_syncTimeSubtitle": "Definir o relógio do dispositivo para o horário do telefone", "settings_timeSynchronized": "Sincronizado com o tempo", "settings_refreshContacts": "Atualizar Contatos", "settings_refreshContactsSubtitle": "Recarregar a lista de contatos do dispositivo", "settings_rebootDevice": "Reiniciar Dispositivo", "settings_rebootDeviceSubtitle": "Reiniciar o dispositivo MeshCore", - "settings_rebootDeviceConfirm": "Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.", + "settings_rebootDeviceConfirm": "Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.", "settings_debug": "Depurar", - "settings_bleDebugLog": "Log de Depuração BLE", + "settings_bleDebugLog": "Log de Depuração BLE", "settings_bleDebugLogSubtitle": "Comandos, respostas e dados brutos do BLE", - "settings_appDebugLog": "Log de Depuração do Aplicativo", - "settings_appDebugLogSubtitle": "Mensagens de depuração do aplicativo", + "settings_appDebugLog": "Log de Depuração do Aplicativo", + "settings_appDebugLogSubtitle": "Mensagens de depuração do aplicativo", "settings_about": "Sobre", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -129,25 +129,25 @@ } } }, - "settings_aboutLegalese": "Projeto MeshCore de Código Aberto 2024", - "settings_aboutDescription": "Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.", + "settings_aboutLegalese": "Projeto MeshCore de Código Aberto 2024", + "settings_aboutDescription": "Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.", "settings_infoName": "Nome", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Bateria", - "settings_infoPublicKey": "Chave Pública", - "settings_infoContactsCount": "Número de Contatos", - "settings_infoChannelCount": "Número do Canal", + "settings_infoPublicKey": "Chave Pública", + "settings_infoContactsCount": "Número de Contatos", + "settings_infoChannelCount": "Número do Canal", "settings_presets": "Presets", - "settings_frequency": "Frequência (MHz)", + "settings_frequency": "Frequência (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", - "settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)", + "settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)", "settings_bandwidth": "Largura de banda", - "settings_spreadingFactor": "Fator de Dispersão", - "settings_codingRate": "Taxa de Codificação", - "settings_txPower": "TX Potência (dBm)", + "settings_spreadingFactor": "Fator de Dispersão", + "settings_codingRate": "Taxa de Codificação", + "settings_txPower": "TX Potência (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)", + "settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)", "settings_error": "Erro: {message}", "@settings_error": { "placeholders": { @@ -156,50 +156,50 @@ } } }, - "appSettings_title": "Configurações do App", - "appSettings_appearance": "Aparência", + "appSettings_title": "Configurações do App", + "appSettings_appearance": "Aparência", "appSettings_theme": "Tema", - "appSettings_themeSystem": "Padrão do sistema", + "appSettings_themeSystem": "Padrão do sistema", "appSettings_themeLight": "Luz", "appSettings_themeDark": "Escuro", "appSettings_language": "Idioma", - "appSettings_languageSystem": "Padrão do sistema", + "appSettings_languageSystem": "Padrão do sistema", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_notifications": "Notificações", - "appSettings_enableNotifications": "Ativar Notificações", - "appSettings_enableNotificationsSubtitle": "Receber notificações para mensagens e anúncios", - "appSettings_notificationPermissionDenied": "Permissão de notificação negada", - "appSettings_notificationsEnabled": "Notificações ativadas", - "appSettings_notificationsDisabled": "Notificações desativadas", - "appSettings_messageNotifications": "Notificações de Mensagem", - "appSettings_messageNotificationsSubtitle": "Mostrar notificação ao receber novas mensagens", - "appSettings_channelMessageNotifications": "Notificações de Mensagens do Canal", - "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificação ao receber mensagens do canal", - "appSettings_advertisementNotifications": "Notificações de Anúncios", - "appSettings_advertisementNotificationsSubtitle": "Mostrar notificação quando novos nós forem descobertos", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_notifications": "Notificações", + "appSettings_enableNotifications": "Ativar Notificações", + "appSettings_enableNotificationsSubtitle": "Receber notificações para mensagens e anúncios", + "appSettings_notificationPermissionDenied": "Permissão de notificação negada", + "appSettings_notificationsEnabled": "Notificações ativadas", + "appSettings_notificationsDisabled": "Notificações desativadas", + "appSettings_messageNotifications": "Notificações de Mensagem", + "appSettings_messageNotificationsSubtitle": "Mostrar notificação ao receber novas mensagens", + "appSettings_channelMessageNotifications": "Notificações de Mensagens do Canal", + "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificação ao receber mensagens do canal", + "appSettings_advertisementNotifications": "Notificações de Anúncios", + "appSettings_advertisementNotificationsSubtitle": "Mostrar notificação quando novos nós forem descobertos", "appSettings_messaging": "Mensagens", - "appSettings_clearPathOnMaxRetry": "Limpar Caminho em Tentativas Máximas", - "appSettings_clearPathOnMaxRetrySubtitle": "Redefinir o caminho de contato após 5 tentativas de envio falhas", - "appSettings_pathsWillBeCleared": "Os caminhos serão limpos após 5 tentativas falhas.", - "appSettings_pathsWillNotBeCleared": "Os caminhos não serão limpos automaticamente.", - "appSettings_autoRouteRotation": "Rotação de Rota Automática", - "appSettings_autoRouteRotationSubtitle": "Alternar entre os melhores caminhos e o modo inundação", - "appSettings_autoRouteRotationEnabled": "Rotação de roteamento automático habilitada", - "appSettings_autoRouteRotationDisabled": "Rotação de roteamento automático desativada", + "appSettings_clearPathOnMaxRetry": "Limpar Caminho em Tentativas Máximas", + "appSettings_clearPathOnMaxRetrySubtitle": "Redefinir o caminho de contato após 5 tentativas de envio falhas", + "appSettings_pathsWillBeCleared": "Os caminhos serão limpos após 5 tentativas falhas.", + "appSettings_pathsWillNotBeCleared": "Os caminhos não serão limpos automaticamente.", + "appSettings_autoRouteRotation": "Rotação de Rota Automática", + "appSettings_autoRouteRotationSubtitle": "Alternar entre os melhores caminhos e o modo inundação", + "appSettings_autoRouteRotationEnabled": "Rotação de roteamento automático habilitada", + "appSettings_autoRouteRotationDisabled": "Rotação de roteamento automático desativada", "appSettings_battery": "Bateria", - "appSettings_batteryChemistry": "Química da Bateria", + "appSettings_batteryChemistry": "Química da Bateria", "appSettings_batteryChemistryPerDevice": "Definir por dispositivo ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { @@ -212,16 +212,16 @@ "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", - "appSettings_mapDisplay": "Exibição do Mapa", + "appSettings_mapDisplay": "Exibição do Mapa", "appSettings_showRepeaters": "Mostrar Repetidores", - "appSettings_showRepeatersSubtitle": "Exibir nós de repetidor no mapa", - "appSettings_showChatNodes": "Mostrar Nós de Chat", - "appSettings_showChatNodesSubtitle": "Exibir nós de chat no mapa", - "appSettings_showOtherNodes": "Mostrar Outros Nós", - "appSettings_showOtherNodesSubtitle": "Exibir outros tipos de nó no mapa", + "appSettings_showRepeatersSubtitle": "Exibir nós de repetidor no mapa", + "appSettings_showChatNodes": "Mostrar Nós de Chat", + "appSettings_showChatNodesSubtitle": "Exibir nós de chat no mapa", + "appSettings_showOtherNodes": "Mostrar Outros Nós", + "appSettings_showOtherNodesSubtitle": "Exibir outros tipos de nó no mapa", "appSettings_timeFilter": "Filtro de Tempo", - "appSettings_timeFilterShowAll": "Mostrar todos os nós", - "appSettings_timeFilterShowLast": "Mostrar nós das últimas {hours} horas", + "appSettings_timeFilterShowAll": "Mostrar todos os nós", + "appSettings_timeFilterShowLast": "Mostrar nós das últimas {hours} horas", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,15 +230,15 @@ } }, "appSettings_mapTimeFilter": "Filtro de Tempo do Mapa", - "appSettings_showNodesDiscoveredWithin": "Mostrar nós descobertos dentro de:", + "appSettings_showNodesDiscoveredWithin": "Mostrar nós descobertos dentro de:", "appSettings_allTime": "Todos os tempos", - "appSettings_lastHour": "Última hora", - "appSettings_last6Hours": "Últimos 6 horas", - "appSettings_last24Hours": "Últimas 24 horas", - "appSettings_lastWeek": "Da última semana", + "appSettings_lastHour": "Última hora", + "appSettings_last6Hours": "Últimos 6 horas", + "appSettings_last24Hours": "Últimas 24 horas", + "appSettings_lastWeek": "Da última semana", "appSettings_offlineMapCache": "Cache de Mapa Offline", - "appSettings_noAreaSelected": "Nenhuma área selecionada", - "appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Nenhuma área selecionada", + "appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,16 +250,16 @@ } }, "appSettings_debugCard": "Depurar", - "appSettings_appDebugLogging": "Rastreamento de Depuração do Aplicativo", - "appSettings_appDebugLoggingSubtitle": "Registrar mensagens de depuração do aplicativo Log para solucionar problemas", - "appSettings_appDebugLoggingEnabled": "Log de depuração do aplicativo habilitado", - "appSettings_appDebugLoggingDisabled": "O registro de depuração do aplicativo está desativado.", + "appSettings_appDebugLogging": "Rastreamento de Depuração do Aplicativo", + "appSettings_appDebugLoggingSubtitle": "Registrar mensagens de depuração do aplicativo Log para solucionar problemas", + "appSettings_appDebugLoggingEnabled": "Log de depuração do aplicativo habilitado", + "appSettings_appDebugLoggingDisabled": "O registro de depuração do aplicativo está desativado.", "contacts_title": "Contactos", - "contacts_noContacts": "Ainda não existem contatos.", - "contacts_contactsWillAppear": "Os contatos serão exibidos quando os dispositivos anunciarem.", + "contacts_noContacts": "Ainda não existem contatos.", + "contacts_contactsWillAppear": "Os contatos serão exibidos quando os dispositivos anunciarem.", "contacts_searchContacts": "Pesquisar contatos...", - "contacts_noUnreadContacts": "Sem contatos não lidos.", - "contacts_noContactsFound": "Não foram encontrados contatos ou grupos.", + "contacts_noUnreadContacts": "Sem contatos não lidos.", + "contacts_noContactsFound": "Não foram encontrados contatos ou grupos.", "contacts_deleteContact": "Excluir Contato", "contacts_removeConfirm": "Remover {contactName} dos contatos?", "@contacts_removeConfirm": { @@ -284,8 +284,8 @@ }, "contacts_newGroup": "Novo Grupo", "contacts_groupName": "Nome do grupo", - "contacts_groupNameRequired": "O nome do grupo é obrigatório.", - "contacts_groupAlreadyExists": "O grupo \"{name}\" já existe", + "contacts_groupNameRequired": "O nome do grupo é obrigatório.", + "contacts_groupAlreadyExists": "O grupo \"{name}\" já existe", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,10 +294,10 @@ } }, "contacts_filterContacts": "Filtrar contatos...", - "contacts_noContactsMatchFilter": "Não existem contatos que correspondam ao seu filtro", + "contacts_noContactsMatchFilter": "Não existem contatos que correspondam ao seu filtro", "contacts_noMembers": "Nenhum membro", - "contacts_lastSeenNow": "Última vez que foi visto agora", - "contacts_lastSeenMinsAgo": "Última vez que foi visto {minutes} minutos atrás", + "contacts_lastSeenNow": "Última vez que foi visto agora", + "contacts_lastSeenMinsAgo": "Última vez que foi visto {minutes} minutos atrás", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Última vez que foi visto há 1 hora.", - "contacts_lastSeenHoursAgo": "Última vez visto {hours} horas atrás", + "contacts_lastSeenHourAgo": "Última vez que foi visto há 1 hora.", + "contacts_lastSeenHoursAgo": "Última vez visto {hours} horas atrás", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Última vez que foi visto 1 dia atrás", - "contacts_lastSeenDaysAgo": "Última vez visto {days} dias atrás", + "contacts_lastSeenDayAgo": "Última vez que foi visto 1 dia atrás", + "contacts_lastSeenDaysAgo": "Última vez visto {days} dias atrás", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -324,8 +324,8 @@ } }, "channels_title": "Canais", - "channels_noChannelsConfigured": "Nenhuma canalização configurada", - "channels_addPublicChannel": "Adicionar Canal Público", + "channels_noChannelsConfigured": "Nenhuma canalização configurada", + "channels_addPublicChannel": "Adicionar Canal Público", "channels_searchChannels": "Pesquisar canais...", "channels_noChannelsFound": "Nenhum canal encontrado", "channels_channelIndex": "Canal {index}", @@ -337,15 +337,15 @@ } }, "channels_hashtagChannel": "Canal com hashtag", - "channels_public": "Público", + "channels_public": "Público", "channels_private": "Privado", - "channels_publicChannel": "Canal público", + "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", "channels_muteChannel": "Silenciar canal", "channels_unmuteChannel": "Ativar canal", "channels_deleteChannel": "Excluir canal", - "channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.", + "channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Canal \"{name}\" excluído", + "channels_channelDeleted": "Canal \"{name}\" excluído", "@channels_channelDeleted": { "placeholders": { "name": { @@ -362,12 +362,12 @@ } }, "channels_addChannel": "Adicionar Canal", - "channels_channelIndexLabel": "Índice do Canal", + "channels_channelIndexLabel": "Índice do Canal", "channels_channelName": "Nome do Canal", - "channels_usePublicChannel": "Usar Canal Público", - "channels_standardPublicPsk": "PSK público padrão", + "channels_usePublicChannel": "Usar Canal Público", + "channels_standardPublicPsk": "PSK público padrão", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Gerar PSK aleatório", + "channels_generateRandomPsk": "Gerar PSK aleatório", "channels_enterChannelName": "Por favor, insira um nome de canal", "channels_pskMustBe32Hex": "O PSK deve ter 32 caracteres hexadecimais.", "channels_channelAdded": "Canal \"{name}\" adicionado", @@ -386,7 +386,7 @@ } } }, - "channels_smazCompression": "Compressão SMAZ", + "channels_smazCompression": "Compressão SMAZ", "channels_channelUpdated": "Canal \"{name}\" atualizado", "@channels_channelUpdated": { "placeholders": { @@ -395,15 +395,15 @@ } } }, - "channels_publicChannelAdded": "Canal público adicionado", + "channels_publicChannelAdded": "Canal público adicionado", "channels_sortBy": "Ordenar por", "channels_sortManual": "Manual", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Últimas mensagens", - "channels_sortUnread": "Não lido", - "chat_noMessages": "Ainda não existem mensagens.", - "chat_sendMessageToStart": "Enviar uma mensagem para começar", - "chat_originalMessageNotFound": "Mensagem original não encontrada", + "channels_sortLatestMessages": "Últimas mensagens", + "channels_sortUnread": "Não lido", + "chat_noMessages": "Ainda não existem mensagens.", + "chat_sendMessageToStart": "Enviar uma mensagem para começar", + "chat_originalMessageNotFound": "Mensagem original não encontrada", "chat_replyingTo": "Responder a {name}", "@chat_replyingTo": { "placeholders": { @@ -420,7 +420,7 @@ } } }, - "chat_location": "Localização", + "chat_location": "Localização", "chat_sendMessageTo": "Enviar uma mensagem para {contactName}", "@chat_sendMessageTo": { "placeholders": { @@ -430,7 +430,7 @@ } }, "chat_typeMessage": "Digite uma mensagem...", - "chat_messageTooLong": "Mensagem muito longa (máximo {maxBytes} bytes).", + "chat_messageTooLong": "Mensagem muito longa (máximo {maxBytes} bytes).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -439,7 +439,7 @@ } }, "chat_messageCopied": "Mensagem copiada", - "chat_messageDeleted": "Mensagem excluída", + "chat_messageDeleted": "Mensagem excluída", "chat_retryingMessage": "Tentando novamente", "chat_retryCount": "Tentar {current}/{max}", "@chat_retryCount": { @@ -454,30 +454,30 @@ }, "chat_sendGif": "Enviar GIF", "chat_reply": "Responder", - "chat_addReaction": "Adicionar Reação", + "chat_addReaction": "Adicionar Reação", "chat_me": "Eu", "emojiCategorySmileys": "Emojis", "emojiCategoryGestures": "Gestos", - "emojiCategoryHearts": "Corações", + "emojiCategoryHearts": "Corações", "emojiCategoryObjects": "Objetos", "gifPicker_title": "Escolher um GIF", "gifPicker_searchHint": "Pesquisar GIFs...", "gifPicker_poweredBy": "Desenvolvido por GIPHY", "gifPicker_noGifsFound": "Nenhum GIF encontrado", - "gifPicker_failedLoad": "Não foi possível carregar os GIFs", + "gifPicker_failedLoad": "Não foi possível carregar os GIFs", "gifPicker_failedSearch": "Falha na pesquisa de GIFs", - "gifPicker_noInternet": "Sem conexão com a internet", - "debugLog_appTitle": "Log de Depuração do Aplicativo", - "debugLog_bleTitle": "Log de Depuração BLE", + "gifPicker_noInternet": "Sem conexão com a internet", + "debugLog_appTitle": "Log de Depuração do Aplicativo", + "debugLog_bleTitle": "Log de Depuração BLE", "debugLog_copyLog": "Copiar log", "debugLog_clearLog": "Limpar log", - "debugLog_copied": "Log de depuração copiado", + "debugLog_copied": "Log de depuração copiado", "debugLog_bleCopied": "Log BLE copiado", - "debugLog_noEntries": "Ainda não existem logs de depuração.", - "debugLog_enableInSettings": "Ativar o log de depuração do aplicativo nas configurações", + "debugLog_noEntries": "Ainda não existem logs de depuração.", + "debugLog_enableInSettings": "Ativar o log de depuração do aplicativo nas configurações", "debugLog_frames": "Estruturas", "debugLog_rawLogRx": "Log Raw-RX", - "debugLog_noBleActivity": "Ainda não há atividade BLE.", + "debugLog_noBleActivity": "Ainda não há atividade BLE.", "debugFrame_length": "Comprimento do Quadro: {count} bytes", "@debugFrame_length": { "placeholders": { @@ -540,13 +540,13 @@ } } }, - "debugFrame_hexDump": "Espaço Hexadecimal:", + "debugFrame_hexDump": "Espaço Hexadecimal:", "chat_pathManagement": "Gerenciamento de Caminhos", "chat_routingMode": "Modo de roteamento", "chat_autoUseSavedPath": "Auto (usar caminho salvo)", - "chat_forceFloodMode": "Modo de Inundação Forçado", + "chat_forceFloodMode": "Modo de Inundação Forçado", "chat_recentAckPaths": "Rotas de ACK Recentes (toque para usar):", - "chat_pathHistoryFull": "O histórico está cheio. Remova entradas para adicionar novas.", + "chat_pathHistoryFull": "O histórico está cheio. Remova entradas para adicionar novas.", "chat_hopSingular": "pule", "chat_hopPlural": "salta", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -559,17 +559,17 @@ }, "chat_successes": "Sucessos", "chat_removePath": "Remover caminho", - "chat_noPathHistoryYet": "Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.", - "chat_pathActions": "Ações do Caminho:", + "chat_noPathHistoryYet": "Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.", + "chat_pathActions": "Ações do Caminho:", "chat_setCustomPath": "Definir Caminho Personalizado", "chat_setCustomPathSubtitle": "Especifique manualmente o caminho de roteamento", "chat_clearPath": "Limpar Caminho", - "chat_clearPathSubtitle": "Forçar a descoberta na próxima transmissão", - "chat_pathCleared": "Caminho limpo. A próxima mensagem redescobrirá a rota.", + "chat_clearPathSubtitle": "Forçar a descoberta na próxima transmissão", + "chat_pathCleared": "Caminho limpo. A próxima mensagem redescobrirá a rota.", "chat_floodModeSubtitle": "Use a chave de roteamento na barra de ferramentas", - "chat_floodModeEnabled": "Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.", + "chat_floodModeEnabled": "Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.", "chat_fullPath": "Caminho Completo", - "chat_pathDetailsNotAvailable": "Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.", + "chat_pathDetailsNotAvailable": "Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.", "chat_pathSetHops": "Caminho definido: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -583,14 +583,14 @@ }, "chat_pathSavedLocally": "Salvo localmente. Conectar para sincronizar.", "chat_pathDeviceConfirmed": "Dispositivo confirmado.", - "chat_pathDeviceNotConfirmed": "Dispositivo ainda não confirmado.", + "chat_pathDeviceNotConfirmed": "Dispositivo ainda não confirmado.", "chat_type": "Digite", "chat_path": "Caminho", - "chat_publicKey": "Chave Pública", + "chat_publicKey": "Chave Pública", "chat_compressOutgoingMessages": "Comprimir mensagens enviadas", - "chat_floodForced": "Inundação (forçada)", - "chat_directForced": "Direto (forçado)", - "chat_hopsForced": "{count} saltos (forçado)", + "chat_floodForced": "Inundação (forçada)", + "chat_directForced": "Direto (forçado)", + "chat_hopsForced": "{count} saltos (forçado)", "@chat_hopsForced": { "placeholders": { "count": { @@ -598,10 +598,10 @@ } } }, - "chat_floodAuto": "Inundação (automática)", + "chat_floodAuto": "Inundação (automática)", "chat_direct": "Salvar", "chat_poiShared": "Ponto de Interesse Compartilhado", - "chat_unread": "Não lido: {count}", + "chat_unread": "Não lido: {count}", "@chat_unread": { "placeholders": { "count": { @@ -612,7 +612,7 @@ "chat_openLink": "Abrir link?", "chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?", "chat_open": "Abrir", - "chat_couldNotOpenLink": "Não foi possível abrir o link: {url}", + "chat_couldNotOpenLink": "Não foi possível abrir o link: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,11 +620,11 @@ } } }, - "chat_invalidLink": "Formato de link inválido", - "map_title": "Mapa de Nós", - "map_noNodesWithLocation": "Não existem nós com dados de localização.", - "map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa", - "map_nodesCount": "Nós: {count}", + "chat_invalidLink": "Formato de link inválido", + "map_title": "Mapa de Nós", + "map_noNodesWithLocation": "Não existem nós com dados de localização.", + "map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa", + "map_nodesCount": "Nós: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -646,21 +646,21 @@ "map_sensor": "Sensor", "map_pinDm": "Gatilho (DM)", "map_pinPrivate": "Bloquear (Privado)", - "map_pinPublic": "Pin (Público)", - "map_lastSeen": "Última Visão", + "map_pinPublic": "Pin (Público)", + "map_lastSeen": "Última Visão", "map_disconnectConfirm": "Tem certeza de que deseja desconectar deste dispositivo?", "map_from": "De", "map_source": "Fonte", "map_flags": "Bandeiras", "map_shareMarkerHere": "Compartilhar marcador aqui", - "map_pinLabel": "Rótulo de marcador", - "map_label": "Rótulo", + "map_pinLabel": "Rótulo de marcador", + "map_label": "Rótulo", "map_pointOfInterest": "Ponto de interesse", "map_sendToContact": "Enviar para o contato", "map_sendToChannel": "Enviar para o canal", - "map_noChannelsAvailable": "Não existem canais disponíveis.", - "map_publicLocationShare": "Compartilhar local público", - "map_publicLocationShareConfirm": "Você está prestes a compartilhar uma localização em {channelLabel}. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.", + "map_noChannelsAvailable": "Não existem canais disponíveis.", + "map_publicLocationShare": "Compartilhar local público", + "map_publicLocationShareConfirm": "Você está prestes a compartilhar uma localização em {channelLabel}. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -669,23 +669,23 @@ } }, "map_connectToShareMarkers": "Conecte-se a um dispositivo para compartilhar marcadores", - "map_filterNodes": "Filtrar Nós", - "map_nodeTypes": "Tipos de Nó", - "map_chatNodes": "Nós de Chat", + "map_filterNodes": "Filtrar Nós", + "map_nodeTypes": "Tipos de Nó", + "map_chatNodes": "Nós de Chat", "map_repeaters": "Repetidores", - "map_otherNodes": "Outros Nós", + "map_otherNodes": "Outros Nós", "map_keyPrefix": "Prefixo Chave", "map_filterByKeyPrefix": "Filtrar por prefixo-chave", - "map_publicKeyPrefix": "Prefixo de chave pública", + "map_publicKeyPrefix": "Prefixo de chave pública", "map_markers": "Marcadores", "map_showSharedMarkers": "Mostrar marcadores compartilhados", - "map_lastSeenTime": "Último Tempo de Visualização", + "map_lastSeenTime": "Último Tempo de Visualização", "map_sharedPin": "Pin compartilhado", - "map_joinRoom": "Junte-se à Sala", + "map_joinRoom": "Junte-se à Sala", "map_manageRepeater": "Gerenciar Repetidor", "mapCache_title": "Cache de Mapa Offline", - "mapCache_selectAreaFirst": "Selecione uma área para armazenar em cache primeiro", - "mapCache_noTilesToDownload": "Não há tiles para baixar para esta área.", + "mapCache_selectAreaFirst": "Selecione uma área para armazenar em cache primeiro", + "mapCache_noTilesToDownload": "Não há tiles para baixar para esta área.", "mapCache_downloadTilesTitle": "Baixar tiles", "mapCache_downloadTilesPrompt": "Baixar {count} tiles para uso offline?", "@mapCache_downloadTilesPrompt": { @@ -718,9 +718,9 @@ "mapCache_clearOfflineCacheTitle": "Limpar cache offline", "mapCache_clearOfflineCachePrompt": "Remover todas as telhas de mapa em cache?", "mapCache_offlineCacheCleared": "Cache offline limpa", - "mapCache_noAreaSelected": "Nenhuma área selecionada", - "mapCache_cacheArea": "Área de Cache", - "mapCache_useCurrentView": "Usar a Visualização Atual", + "mapCache_noAreaSelected": "Nenhuma área selecionada", + "mapCache_cacheArea": "Área de Cache", + "mapCache_useCurrentView": "Usar a Visualização Atual", "mapCache_zoomRange": "Intervalo de Zoom", "mapCache_estimatedTiles": "Estimados azulejos: {count}", "@mapCache_estimatedTiles": { @@ -769,7 +769,7 @@ } }, "time_justNow": "Agora", - "time_minutesAgo": "{minutes} minutos atrás", + "time_minutesAgo": "{minutes} minutos atrás", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -777,7 +777,7 @@ } } }, - "time_hoursAgo": "{hours}h atrás", + "time_hoursAgo": "{hours}h atrás", "@time_hoursAgo": { "placeholders": { "hours": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} dias atrás", + "time_daysAgo": "{days} dias atrás", "@time_daysAgo": { "placeholders": { "days": { @@ -799,7 +799,7 @@ "time_days": "dias", "time_week": "semana", "time_weeks": "semanas", - "time_month": "mês", + "time_month": "mês", "time_months": "meses", "time_minutes": "minutos", "time_allTime": "Todos os tempos", @@ -810,13 +810,13 @@ "login_password": "Senha", "login_enterPassword": "Insira a senha", "login_savePassword": "Salvar senha", - "login_savePasswordSubtitle": "A senha será armazenada com segurança neste dispositivo.", - "login_repeaterDescription": "Insira a senha do repetidor para acessar as configurações e o status.", - "login_roomDescription": "Insira a senha da sala para acessar as configurações e o status.", + "login_savePasswordSubtitle": "A senha será armazenada com segurança neste dispositivo.", + "login_repeaterDescription": "Insira a senha do repetidor para acessar as configurações e o status.", + "login_roomDescription": "Insira a senha da sala para acessar as configurações e o status.", "login_routing": "Rotas", "login_routingMode": "Modo de roteamento", "login_autoUseSavedPath": "Auto (usar caminho salvo)", - "login_forceFloodMode": "Modo de Inundação Forçado", + "login_forceFloodMode": "Modo de Inundação Forçado", "login_managePaths": "Gerenciar Caminhos", "login_login": "Login", "login_attempt": "Tentar {current}/{max}", @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.", + "login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.", "common_reload": "Recarregar", "common_clear": "Limpar", "path_currentPath": "Caminho atual: {path}", @@ -859,14 +859,14 @@ }, "path_enterCustomPath": "Insira Caminho Personalizado", "path_currentPathLabel": "Caminho atual", - "path_hexPrefixInstructions": "Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.", - "path_hexPrefixExample": "A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)", + "path_hexPrefixInstructions": "Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.", + "path_hexPrefixExample": "A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)", "path_labelHexPrefixes": "Prefixo Hexadecimal", - "path_helperMaxHops": "Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)", + "path_helperMaxHops": "Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)", "path_selectFromContacts": "Ou selecione de contatos:", - "path_noRepeatersFound": "Não foram encontrados repetidores ou servidores de sala.", - "path_customPathsRequire": "Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.", - "path_invalidHexPrefixes": "Prefixos hexadecimais inválidos: {prefixes}", + "path_noRepeatersFound": "Não foram encontrados repetidores ou servidores de sala.", + "path_customPathsRequire": "Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.", + "path_invalidHexPrefixes": "Prefixos hexadecimais inválidos: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,25 +874,25 @@ } } }, - "path_tooLong": "Caminho muito longo. Máximo de 64 saltos permitidos.", + "path_tooLong": "Caminho muito longo. Máximo de 64 saltos permitidos.", "path_setPath": "Definir Caminho", "repeater_management": "Gerenciamento de Repetidor", "repeater_managementTools": "Ferramentas de Gerenciamento", "repeater_status": "Status", - "repeater_statusSubtitle": "Visualizar status do repetidor, estatísticas e vizinhos.", + "repeater_statusSubtitle": "Visualizar status do repetidor, estatísticas e vizinhos.", "repeater_telemetry": "Telemetria", - "repeater_telemetrySubtitle": "Visualizar telemetria de sensores e estatísticas do sistema", + "repeater_telemetrySubtitle": "Visualizar telemetria de sensores e estatísticas do sistema", "repeater_cli": "CLI", "repeater_cliSubtitle": "Enviar comandos ao repetidor", - "repeater_settings": "Configurações", - "repeater_settingsSubtitle": "Configurar parâmetros do repetidor", + "repeater_settings": "Configurações", + "repeater_settingsSubtitle": "Configurar parâmetros do repetidor", "repeater_statusTitle": "Status do Repetidor", "repeater_routingMode": "Modo de roteamento", "repeater_autoUseSavedPath": "Auto (usar caminho salvo)", - "repeater_forceFloodMode": "Modo de Inundação Forçado", + "repeater_forceFloodMode": "Modo de Inundação Forçado", "repeater_pathManagement": "Gerenciamento de caminhos", "repeater_refresh": "Atualizar", - "repeater_statusRequestTimeout": "Solicitação de status expirou.", + "repeater_statusRequestTimeout": "Solicitação de status expirou.", "repeater_errorLoadingStatus": "Erro ao carregar o status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -901,19 +901,19 @@ } } }, - "repeater_systemInformation": "Informações do Sistema", + "repeater_systemInformation": "Informações do Sistema", "repeater_battery": "Bateria", - "repeater_clockAtLogin": "Relógio (no login)", + "repeater_clockAtLogin": "Relógio (no login)", "repeater_uptime": "Disponibilidade", "repeater_queueLength": "Comprimento da Fila", - "repeater_debugFlags": "Marcar Flags de Depuração", - "repeater_radioStatistics": "Estatísticas de Rádio", - "repeater_lastRssi": "Último RSSI", - "repeater_lastSnr": "Último SNR", - "repeater_noiseFloor": "Nível de Ruído", + "repeater_debugFlags": "Marcar Flags de Depuração", + "repeater_radioStatistics": "Estatísticas de Rádio", + "repeater_lastRssi": "Último RSSI", + "repeater_lastSnr": "Último SNR", + "repeater_noiseFloor": "Nível de Ruído", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Estatísticas de Pacote", + "repeater_packetStatistics": "Estatísticas de Pacote", "repeater_sent": "Enviado", "repeater_received": "Recebido", "repeater_duplicates": "Duplicatas", @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", + "repeater_packetTxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", + "repeater_packetRxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Inundação: {flood}, Direto: {direct}", + "repeater_duplicatesFloodDirect": "Inundação: {flood}, Direto: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,23 +981,23 @@ } } }, - "repeater_settingsTitle": "Configurações do Repetidor", - "repeater_basicSettings": "Configurações Básicas", + "repeater_settingsTitle": "Configurações do Repetidor", + "repeater_basicSettings": "Configurações Básicas", "repeater_repeaterName": "Nome do Repetidor", "repeater_repeaterNameHelper": "Exibir nome para este repetidor", "repeater_adminPassword": "Senha de Administrador", "repeater_adminPasswordHelper": "Acesso completo de senha", "repeater_guestPassword": "Senha de convidado", "repeater_guestPasswordHelper": "Acesso com senha de leitura somente", - "repeater_radioSettings": "Configurações de Rádio", - "repeater_frequencyMhz": "Frequência (MHz)", + "repeater_radioSettings": "Configurações de Rádio", + "repeater_frequencyMhz": "Frequência (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Largura de banda", - "repeater_spreadingFactor": "Fator de Dispersão", - "repeater_codingRate": "Taxa de Codificação", - "repeater_locationSettings": "Configurações de Localização", + "repeater_spreadingFactor": "Fator de Dispersão", + "repeater_codingRate": "Taxa de Codificação", + "repeater_locationSettings": "Configurações de Localização", "repeater_latitude": "Latitude", "repeater_latitudeHelper": "Graus decimais (por exemplo, 37,7749)", "repeater_longitude": "Longitude", @@ -1008,9 +1008,9 @@ "repeater_guestAccess": "Acesso de Convidado", "repeater_guestAccessSubtitle": "Permitir acesso de convidado somente leitura", "repeater_privacyMode": "Modo de Privacidade", - "repeater_privacyModeSubtitle": "Esconder nome/localização em anúncios", - "repeater_advertisementSettings": "Configurações de Anúncios", - "repeater_localAdvertInterval": "Intervalo de Anúncio Local", + "repeater_privacyModeSubtitle": "Esconder nome/localização em anúncios", + "repeater_advertisementSettings": "Configurações de Anúncios", + "repeater_localAdvertInterval": "Intervalo de Anúncio Local", "repeater_localAdvertIntervalMinutes": "{minutes} minutos", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervalo de Anúncio de Inundação", + "repeater_floodAdvertInterval": "Intervalo de Anúncio de Inundação", "repeater_floodAdvertIntervalHours": "{hours} horas", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,18 +1028,18 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervalo de Anúncio Criptografado", + "repeater_encryptedAdvertInterval": "Intervalo de Anúncio Criptografado", "repeater_dangerZone": "Zona de Perigo", "repeater_rebootRepeater": "Reiniciar Repetidor", "repeater_rebootRepeaterSubtitle": "Reiniciar o dispositivo repetidor", "repeater_rebootRepeaterConfirm": "Tem certeza de que deseja reiniciar este repetidor?", "repeater_regenerateIdentityKey": "Gerar Chave de Identidade", - "repeater_regenerateIdentityKeySubtitle": "Gerar nova chave pública/privada", - "repeater_regenerateIdentityKeyConfirm": "Isso gerará uma nova identidade para o repetidor. Continuar?", + "repeater_regenerateIdentityKeySubtitle": "Gerar nova chave pública/privada", + "repeater_regenerateIdentityKeyConfirm": "Isso gerará uma nova identidade para o repetidor. Continuar?", "repeater_eraseFileSystem": "Excluir Sistema de Arquivos", "repeater_eraseFileSystemSubtitle": "Formatar o sistema de arquivos do repetidor", - "repeater_eraseFileSystemConfirm": "AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!", - "repeater_eraseSerialOnly": "Apagar está disponível apenas via console serial.", + "repeater_eraseFileSystemConfirm": "AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!", + "repeater_eraseSerialOnly": "Apagar está disponível apenas via console serial.", "repeater_commandSent": "Comando enviado: {command}", "@repeater_commandSent": { "placeholders": { @@ -1057,8 +1057,8 @@ } }, "repeater_confirm": "Confirmar", - "repeater_settingsSaved": "Configurações salvas com sucesso", - "repeater_errorSavingSettings": "Erro ao salvar as configurações: {error}", + "repeater_settingsSaved": "Configurações salvas com sucesso", + "repeater_errorSavingSettings": "Erro ao salvar as configurações: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,14 +1066,14 @@ } } }, - "repeater_refreshBasicSettings": "Atualizar Configurações Básicas", - "repeater_refreshRadioSettings": "Atualizar Configurações de Rádio", + "repeater_refreshBasicSettings": "Atualizar Configurações Básicas", + "repeater_refreshRadioSettings": "Atualizar Configurações de Rádio", "repeater_refreshTxPower": "Atualizar TX de energia", - "repeater_refreshLocationSettings": "Atualizar Configurações de Localização", + "repeater_refreshLocationSettings": "Atualizar Configurações de Localização", "repeater_refreshPacketForwarding": "Atualizar Roteamento de Pacotes", "repeater_refreshGuestAccess": "Atualizar Acesso de Convidados", "repeater_refreshPrivacyMode": "Atualizar Modo Privacidade", - "repeater_refreshAdvertisementSettings": "Atualizar Configurações do Anúncio", + "repeater_refreshAdvertisementSettings": "Atualizar Configurações do Anúncio", "repeater_refreshed": "{label} atualizado", "@repeater_refreshed": { "placeholders": { @@ -1091,14 +1091,14 @@ } }, "repeater_cliTitle": "Repetidor CLI", - "repeater_debugNextCommand": "Depurar Próximo Comando", + "repeater_debugNextCommand": "Depurar Próximo Comando", "repeater_commandHelp": "Ajuda", - "repeater_clearHistory": "Limpar Histórico", - "repeater_noCommandsSent": "Ainda não foram enviadas comandos.", - "repeater_typeCommandOrUseQuick": "Digite um comando abaixo ou use comandos rápidos", + "repeater_clearHistory": "Limpar Histórico", + "repeater_noCommandsSent": "Ainda não foram enviadas comandos.", + "repeater_typeCommandOrUseQuick": "Digite um comando abaixo ou use comandos rápidos", "repeater_enterCommandHint": "Insira o comando...", "repeater_previousCommand": "Comando anterior", - "repeater_nextCommand": "Próxima ação", + "repeater_nextCommand": "Próxima ação", "repeater_enterCommandFirst": "Insira um comando primeiro", "repeater_cliCommandFrameTitle": "Frame de Comando CLI", "repeater_cliCommandError": "Erro: {error}", @@ -1110,79 +1110,79 @@ } }, "repeater_cliQuickGetName": "Obter Nome", - "repeater_cliQuickGetRadio": "Obter Rádio", + "repeater_cliQuickGetRadio": "Obter Rádio", "repeater_cliQuickGetTx": "Obter TX", "repeater_cliQuickNeighbors": "Vizinhos", - "repeater_cliQuickVersion": "Versão", + "repeater_cliQuickVersion": "Versão", "repeater_cliQuickAdvertise": "Anunciar", - "repeater_cliQuickClock": "Relógio", - "repeater_cliHelpAdvert": "Envia um pacote de anúncios", - "repeater_cliHelpReboot": "Reinicia o dispositivo. (note, você pode obter 'Timeout' que é normal)", - "repeater_cliHelpClock": "Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.", + "repeater_cliQuickClock": "Relógio", + "repeater_cliHelpAdvert": "Envia um pacote de anúncios", + "repeater_cliHelpReboot": "Reinicia o dispositivo. (note, você pode obter 'Timeout' que é normal)", + "repeater_cliHelpClock": "Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.", "repeater_cliHelpPassword": "Define uma nova senha de administrador para o dispositivo.", - "repeater_cliHelpVersion": "Mostra a versão do dispositivo e a data de construção do firmware.", - "repeater_cliHelpClearStats": "Reseta vários contadores de estatísticas para zero.", + "repeater_cliHelpVersion": "Mostra a versão do dispositivo e a data de construção do firmware.", + "repeater_cliHelpClearStats": "Reseta vários contadores de estatísticas para zero.", "repeater_cliHelpSetAf": "Define o fator de tempo de ar.", - "repeater_cliHelpSetTx": "Define a potência de transmissão LoRa em dBm (redefinir para aplicar).", - "repeater_cliHelpSetRepeat": "Habilita ou desabilita o papel do repetidor para este nó.", - "repeater_cliHelpSetAllowReadOnly": "(Servidor de sala) Se 'ligado', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).", - "repeater_cliHelpSetFloodMax": "Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)", - "repeater_cliHelpSetIntThresh": "Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.", - "repeater_cliHelpSetAgcResetInterval": "Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.", + "repeater_cliHelpSetTx": "Define a potência de transmissão LoRa em dBm (redefinir para aplicar).", + "repeater_cliHelpSetRepeat": "Habilita ou desabilita o papel do repetidor para este nó.", + "repeater_cliHelpSetAllowReadOnly": "(Servidor de sala) Se 'ligado', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).", + "repeater_cliHelpSetFloodMax": "Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)", + "repeater_cliHelpSetIntThresh": "Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.", + "repeater_cliHelpSetAgcResetInterval": "Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.", "repeater_cliHelpSetMultiAcks": "Habilita ou desabilita a funcionalidade de \"double ACKs\".", - "repeater_cliHelpSetAdvertInterval": "Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.", - "repeater_cliHelpSetFloodAdvertInterval": "Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.", - "repeater_cliHelpSetGuestPassword": "Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")", - "repeater_cliHelpSetName": "Define o nome do anúncio.", - "repeater_cliHelpSetLat": "Define a latitude do mapa de anúncios. (graus decimais)", - "repeater_cliHelpSetLon": "Define a longitude do mapa de anúncios. (graus decimais)", - "repeater_cliHelpSetRadio": "Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.", - "repeater_cliHelpSetRxDelay": "Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.", - "repeater_cliHelpSetTxDelay": "Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)", - "repeater_cliHelpSetDirectTxDelay": "Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.", + "repeater_cliHelpSetAdvertInterval": "Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.", + "repeater_cliHelpSetFloodAdvertInterval": "Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.", + "repeater_cliHelpSetGuestPassword": "Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")", + "repeater_cliHelpSetName": "Define o nome do anúncio.", + "repeater_cliHelpSetLat": "Define a latitude do mapa de anúncios. (graus decimais)", + "repeater_cliHelpSetLon": "Define a longitude do mapa de anúncios. (graus decimais)", + "repeater_cliHelpSetRadio": "Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.", + "repeater_cliHelpSetRxDelay": "Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.", + "repeater_cliHelpSetTxDelay": "Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)", + "repeater_cliHelpSetDirectTxDelay": "Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.", "repeater_cliHelpSetBridgeEnabled": "Ativar/Desativar ponte.", "repeater_cliHelpSetBridgeDelay": "Definir atraso antes de retransmitir pacotes.", - "repeater_cliHelpSetBridgeSource": "Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.", + "repeater_cliHelpSetBridgeSource": "Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.", "repeater_cliHelpSetBridgeBaud": "Definir a taxa de baud para as pontes rs232.", "repeater_cliHelpSetBridgeSecret": "Definir segredo de ponte para pontes espnow.", "repeater_cliHelpSetAdcMultiplier": "Define um fator personalizado para ajustar a voltagem de bateria relatada (apenas suportado em placas selecionadas).", - "repeater_cliHelpTempRadio": "Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).", - "repeater_cliHelpSetPerm": "Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)", - "repeater_cliHelpGetBridgeType": "Obtém tipo de ponte nenhum, rs232, espnow", + "repeater_cliHelpTempRadio": "Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).", + "repeater_cliHelpSetPerm": "Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)", + "repeater_cliHelpGetBridgeType": "Obtém tipo de ponte nenhum, rs232, espnow", "repeater_cliHelpLogStart": "Inicia o registro de pacotes no sistema de arquivos.", "repeater_cliHelpLogStop": "Para interromper o registro de pacotes no sistema de arquivos.", "repeater_cliHelpLogErase": "Apaga os logs do pacote do sistema de arquivos.", - "repeater_cliHelpNeighbors": "Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4", - "repeater_cliHelpNeighborRemove": "Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.", - "repeater_cliHelpRegion": "(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.", - "repeater_cliHelpRegionLoad": "NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.", - "repeater_cliHelpRegionGet": "Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) 'F'\"", - "repeater_cliHelpRegionPut": "Adiciona ou atualiza uma definição de região com o nome fornecido.", - "repeater_cliHelpRegionRemove": "Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)", - "repeater_cliHelpRegionAllowf": "Define a permissão de 'F'luido para a região especificada. ('' para o escopo global/legado)", - "repeater_cliHelpRegionDenyf": "Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)", - "repeater_cliHelpRegionHome": "Responde com a região 'home' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)", - "repeater_cliHelpRegionHomeSet": "Define a região 'casa'.", - "repeater_cliHelpRegionSave": "Persiste a lista/mapa de regiões para o armazenamento.", - "repeater_cliHelpGps": "Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.", + "repeater_cliHelpNeighbors": "Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4", + "repeater_cliHelpNeighborRemove": "Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.", + "repeater_cliHelpRegion": "(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.", + "repeater_cliHelpRegionLoad": "NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.", + "repeater_cliHelpRegionGet": "Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) 'F'\"", + "repeater_cliHelpRegionPut": "Adiciona ou atualiza uma definição de região com o nome fornecido.", + "repeater_cliHelpRegionRemove": "Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)", + "repeater_cliHelpRegionAllowf": "Define a permissão de 'F'luido para a região especificada. ('' para o escopo global/legado)", + "repeater_cliHelpRegionDenyf": "Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)", + "repeater_cliHelpRegionHome": "Responde com a região 'home' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)", + "repeater_cliHelpRegionHomeSet": "Define a região 'casa'.", + "repeater_cliHelpRegionSave": "Persiste a lista/mapa de regiões para o armazenamento.", + "repeater_cliHelpGps": "Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.", "repeater_cliHelpGpsOnOff": "Alterna o estado de energia do GPS.", - "repeater_cliHelpGpsSync": "Sincroniza o tempo do nó com o relógio GPS.", - "repeater_cliHelpGpsSetLoc": "Define a posição do nó para coordenadas GPS e salvar preferências.", - "repeater_cliHelpGpsAdvert": "Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências", - "repeater_cliHelpGpsAdvertSet": "Define a configuração do anúncio de localização.", + "repeater_cliHelpGpsSync": "Sincroniza o tempo do nó com o relógio GPS.", + "repeater_cliHelpGpsSetLoc": "Define a posição do nó para coordenadas GPS e salvar preferências.", + "repeater_cliHelpGpsAdvert": "Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências", + "repeater_cliHelpGpsAdvertSet": "Define a configuração do anúncio de localização.", "repeater_commandsListTitle": "Lista de Comandos", - "repeater_commandsListNote": "NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".", + "repeater_commandsListNote": "NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".", "repeater_general": "Geral", - "repeater_settingsCategory": "Configurações", + "repeater_settingsCategory": "Configurações", "repeater_bridge": "Ponte", "repeater_logging": "Registrar", "repeater_neighborsRepeaterOnly": "Vizinhos (apenas repetidor)", - "repeater_regionManagementRepeaterOnly": "Gerenciamento de Região (Apenas Repetidor)", - "repeater_regionNote": "Os comandos de região foram introduzidos para gerenciar definições e permissões de região.", + "repeater_regionManagementRepeaterOnly": "Gerenciamento de Região (Apenas Repetidor)", + "repeater_regionNote": "Os comandos de região foram introduzidos para gerenciar definições e permissões de região.", "repeater_gpsManagement": "Gerenciamento GPS", - "repeater_gpsNote": "O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.", + "repeater_gpsNote": "O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.", "telemetry_receivedData": "Dados de Telemetria Recebidos", - "telemetry_requestTimeout": "Solicitação de telemetria expirou o tempo.", + "telemetry_requestTimeout": "Solicitação de telemetria expirou o tempo.", "telemetry_errorLoading": "Erro ao carregar a telemetria: {error}", "@telemetry_errorLoading": { "placeholders": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Não estão disponíveis dados de telemetria.", + "telemetry_noData": "Não estão disponíveis dados de telemetria.", "telemetry_channelTitle": "Canal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1201,7 +1201,7 @@ } }, "telemetry_batteryLabel": "Bateria", - "telemetry_voltageLabel": "Tensão", + "telemetry_voltageLabel": "Tensão", "telemetry_mcuTemperatureLabel": "Temperatura do MCU", "telemetry_temperatureLabel": "Temperatura", "telemetry_currentLabel": "Atual", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Rótulo de Caminho de Pacote", + "channelPath_title": "Rótulo de Caminho de Pacote", "channelPath_viewMap": "Ver mapa", "channelPath_otherObservedPaths": "Outros Caminhos Observados", "channelPath_repeaterHops": "Saltos do Repetidor", - "channelPath_noHopDetails": "Os detalhes do pacote não estão disponíveis.", + "channelPath_noHopDetails": "Os detalhes do pacote não estão disponíveis.", "channelPath_messageDetails": "Detalhes da Mensagem", "channelPath_senderLabel": "Remetente", "channelPath_timeLabel": "Tempo", "channelPath_repeatsLabel": "Repete", "channelPath_pathLabel": "Caminho {index}", "channelPath_observedLabel": "Observado", - "channelPath_observedPathTitle": "Rastreamento observado {index} • {hops}", + "channelPath_observedPathTitle": "Rastreamento observado {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Não há dados de localização.", + "channelPath_noLocationData": "Não há dados de localização.", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,7 +1289,7 @@ } }, "channelPath_unknownPath": "Desconhecido", - "channelPath_floodPath": "Inundação", + "channelPath_floodPath": "Inundação", "channelPath_directPath": "Salvar", "channelPath_observedZeroOf": "0 de {total} saltos", "@channelPath_observedZeroOf": { @@ -1311,8 +1311,8 @@ } }, "channelPath_mapTitle": "Mapa de Caminhos", - "channelPath_noRepeaterLocations": "Não estão disponíveis localizações de repetidores para este caminho.", - "channelPath_primaryPath": "Caminho {index} (Primário)", + "channelPath_noRepeaterLocations": "Não estão disponíveis localizações de repetidores para este caminho.", + "channelPath_primaryPath": "Caminho {index} (Primário)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Caminho", "channelPath_observedPathHeader": "Rastreamento Observado", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,19 +1340,19 @@ } } }, - "channelPath_noHopDetailsAvailable": "Não estão disponíveis detalhes de voo para este pacote.", + "channelPath_noHopDetailsAvailable": "Não estão disponíveis detalhes de voo para este pacote.", "channelPath_unknownRepeater": "Repetidor Desconhecido", "listFilter_tooltip": "Filtrar e ordenar", "listFilter_sortBy": "Ordenar por", - "listFilter_latestMessages": "Últimas mensagens", + "listFilter_latestMessages": "Últimas mensagens", "listFilter_heardRecently": "Ouvido recentemente", "listFilter_az": "A-Z", "listFilter_filters": "Filtros", "listFilter_all": "Tudo", - "listFilter_users": "Usuários", + "listFilter_users": "Usuários", "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de sala", - "listFilter_unreadOnly": "Apenas não lido", + "listFilter_unreadOnly": "Apenas não lido", "listFilter_newGroup": "Novo grupo", "@neighbors_errorLoading": { "placeholders": { @@ -1367,16 +1367,16 @@ "neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.", "neighbors_errorLoading": "Erro ao carregar vizinhos: {error}", "neighbors_repeatersNeighbors": "Repetidores Vizinhos", - "neighbors_noData": "Não estão disponíveis dados de vizinhos.", + "neighbors_noData": "Não estão disponíveis dados de vizinhos.", "channels_createPrivateChannelDesc": "Protegido com uma chave secreta.", "channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.", "channels_createPrivateChannel": "Criar um Canal Privado", "channels_joinPrivateChannel": "Junte-se a um Canal Privado", - "channels_joinPublicChannel": "Junte-se ao Canal Público", + "channels_joinPublicChannel": "Junte-se ao Canal Público", "channels_joinPublicChannelDesc": "Qualquer pessoa pode entrar neste canal.", "channels_joinHashtagChannel": "Junte-se a um Canal com Hashtag", "channels_joinHashtagChannelDesc": "Qualquer pessoa pode participar de canais com hashtag.", - "channels_scanQrCode": "Digitalizar um Código QR", + "channels_scanQrCode": "Digitalizar um Código QR", "channels_scanQrCodeComingSoon": "Em breve", "channels_enterHashtag": "Insira hashtag", "channels_hashtagHint": "ex. #equipe", @@ -1394,10 +1394,10 @@ } } }, - "neighbors_heardAgo": "Ouvido: {time} atrás", + "neighbors_heardAgo": "Ouvido: {time} atrás", "neighbors_unknownContact": "{pubkey} Desconhecido", "settings_locationGPSEnable": "Ativar GPS", - "settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.", + "settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.", "settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.", "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", "contacts_manageRoom": "Gerenciar Servidor de Sala", @@ -1459,35 +1459,35 @@ } }, "community_title": "Comunidade", - "community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.", + "community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.", "common_ok": "OK", "community_create": "Criar Comunidade", "community_join": "Junte-se", - "community_joinTitle": "Junte-se à Comunidade", - "community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?", + "community_joinTitle": "Junte-se à Comunidade", + "community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?", "community_scanQr": "Digitalizar a QR Code da Comunidade", - "community_scanInstructions": "Aponte a câmera para um código QR da comunidade", - "community_showQr": "Mostrar Código QR", - "community_publicChannel": "Comunidade Pública", + "community_scanInstructions": "Aponte a câmera para um código QR da comunidade", + "community_showQr": "Mostrar Código QR", + "community_publicChannel": "Comunidade Pública", "community_hashtagChannel": "Hashtag da Comunidade", "community_name": "Nome da Comunidade", "community_enterName": "Insira o nome da comunidade", "community_created": "Comunidade \"{name}\" criada", - "community_joined": "Juntou-se à comunidade \"{name}\"", + "community_joined": "Juntou-se à comunidade \"{name}\"", "community_qrTitle": "Partilhar Comunidade", - "community_qrInstructions": "Escanear este código QR para juntar-se a {name}", - "community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade", - "community_invalidQrCode": "Código QR da comunidade inválido", - "community_alreadyMember": "Já é Membro", - "community_alreadyMemberMessage": "Você já é membro de \"{name}\".", - "community_addPublicChannel": "Adicionar Canal Público da Comunidade", - "community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade", - "community_noCommunities": "Ainda não foram adicionadas comunidades.", - "community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.", + "community_qrInstructions": "Escanear este código QR para juntar-se a {name}", + "community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade", + "community_invalidQrCode": "Código QR da comunidade inválido", + "community_alreadyMember": "Já é Membro", + "community_alreadyMemberMessage": "Você já é membro de \"{name}\".", + "community_addPublicChannel": "Adicionar Canal Público da Comunidade", + "community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade", + "community_noCommunities": "Ainda não foram adicionadas comunidades.", + "community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.", "community_manageCommunities": "Gerenciar Comunidades", "community_delete": "Deixar Comunidade", "community_deleteConfirm": "Sair de \"{name}\"?", - "community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.", + "community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1500,7 +1500,7 @@ "community_addHashtagChannelDesc": "Adicionar um canal de hashtag para esta comunidade", "community_selectCommunity": "Selecione Comunidade", "community_regularHashtag": "Hashtag Regular", - "community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)", + "community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)", "community_communityHashtag": "Hashtag da Comunidade", "community_communityHashtagDesc": "Apenas para membros da comunidade", "community_forCommunity": "Para {name}", @@ -1532,12 +1532,12 @@ } } }, - "community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.", + "community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.", "community_regenerateSecret": "Regenerar Senha Segura", "community_secretRegenerated": "Senha secreta regenerada para \"{name}\"", "community_regenerate": "Regenerar", "community_secretUpdated": "Segredo atualizado para \"{name}\"", - "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++", + "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++", "community_updateSecret": "Atualizar Segredo", "@contacts_pathTraceTo": { "placeholders": { @@ -1546,82 +1546,82 @@ } } }, - "pathTrace_you": "Você", + "pathTrace_you": "Você", "pathTrace_failed": "Falha no rastreamento de caminho.", - "pathTrace_notAvailable": "Traçado de caminho não disponível.", + "pathTrace_notAvailable": "Traçado de caminho não disponível.", "pathTrace_refreshTooltip": "Atualizar Path Trace.", - "contacts_pathTrace": "Traçado de Caminho", + "contacts_pathTrace": "Traçado de Caminho", "contacts_ping": "Pingar", - "contacts_repeaterPathTrace": "Traçar caminho para repetidor", + "contacts_repeaterPathTrace": "Traçar caminho para repetidor", "contacts_repeaterPing": "Pingar repetidor", - "contacts_roomPathTrace": "Traçar caminho para o servidor da sala", + "contacts_roomPathTrace": "Traçar caminho para o servidor da sala", "contacts_roomPing": "Pingar servidor da sala", "contacts_chatTraceRoute": "Rastrear rota do caminho", "contacts_pathTraceTo": "Rastrear rota para {name}", - "contacts_invalidAdvertFormat": "Dados de Contato Inválidos", - "contacts_clipboardEmpty": "Área de Transferência Está Vazia.", + "contacts_invalidAdvertFormat": "Dados de Contato Inválidos", + "contacts_clipboardEmpty": "Área de Transferência Está Vazia.", "appSettings_languageUk": "Ucraniano", "contacts_contactImported": "Contato foi importado.", - "contacts_zeroHopAdvert": "Anúncio Zero Hop", - "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", - "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", + "contacts_zeroHopAdvert": "Anúncio Zero Hop", + "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", + "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", "appSettings_languageRu": "Russo", "appSettings_enableMessageTracing": "Ativar rastreamento de mensagens", "appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens", - "contacts_ShareContact": "Copiar contato para Área de Transferência", + "contacts_ShareContact": "Copiar contato para Área de Transferência", "contacts_contactImportFailed": "Contato falhou ao ser importado.", - "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", - "contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.", - "contacts_floodAdvert": "Anúncio de Inundação", - "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", - "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", + "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", + "contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.", + "contacts_floodAdvert": "Anúncio de Inundação", + "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", + "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", "notification_activityTitle": "Atividade MeshCore", "notification_messagesCount": "{count} {count, plural, =1{mensagem} other{mensagens}}", "notification_channelMessagesCount": "{count} {count, plural, =1{mensagem de canal} other{mensagens de canal}}", - "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", "notification_newTypeDiscovered": "Novo {contactType} descoberto", "notification_receivedNewMessage": "Nova mensagem recebida", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", - "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", - "settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.", - "settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO", + "settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.", + "settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO", "settings_gpxExportError": "Ocorreu um erro ao exportar.", "settings_gpxExportAll": "Exportar todos os contatos para GPX", "settings_gpxExportContacts": "Exportar companheiros para GPX", - "settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.", - "settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala", - "settings_gpxExportChat": "Localizações de companheiros", + "settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.", + "settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala", + "settings_gpxExportChat": "Localizações de companheiros", "settings_gpxExportNoContacts": "Nenhum contato para exportar.", "settings_gpxExportAllContacts": "Todos os locais de contatos", "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", - "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!", - "map_runTrace": "Executar Traçado de Caminho", + "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", + "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!", + "map_runTrace": "Executar Traçado de Caminho", "map_pathTraceCancelled": "Rastreamento de caminho cancelado.", "pathTrace_clearTooltip": "Limpar caminho", - "map_removeLast": "Remover Último", - "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", + "map_removeLast": "Remover Último", + "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", "scanner_enableBluetooth": "Ative o Bluetooth", - "scanner_bluetoothOff": "Bluetooth está desativado", + "scanner_bluetoothOff": "Bluetooth está desativado", "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", - "scanner_chromeRequired": "Navegador Chrome necessário", - "scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.", - "snrIndicator_nearByRepeaters": "Repetidores Próximos", - "snrIndicator_lastSeen": "Visto pela última vez", + "scanner_chromeRequired": "Navegador Chrome necessário", + "scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.", + "snrIndicator_nearByRepeaters": "Repetidores Próximos", + "snrIndicator_lastSeen": "Visto pela última vez", "chat_ShowAllPaths": "Mostrar todos os caminhos", - "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", - "settings_clientRepeat": "Repetição sem rede", + "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", + "settings_clientRepeat": "Repetição sem rede", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.", - "settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)", + "settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Unidades", - "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsMetric": "Métrico (m/km)", "appSettings_unitsImperial": "Imperial (ft/mi)", - "map_lineOfSight": "Linha de visão", - "map_losScreenTitle": "Linha de visão", - "losSelectStartEnd": "Selecione nós iniciais e finais para LOS.", - "losRunFailed": "Falha na verificação da linha de visão: {error}", + "map_lineOfSight": "Linha de visão", + "map_losScreenTitle": "Linha de visão", + "losSelectStartEnd": "Selecione nós iniciais e finais para LOS.", + "losRunFailed": "Falha na verificação da linha de visão: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1630,10 +1630,10 @@ } }, "losClearAllPoints": "Limpe todos os pontos", - "losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação", + "losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados", - "losShowDisplayNodes": "Mostrar nós de exibição", + "losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados", + "losShowDisplayNodes": "Mostrar nós de exibição", "losCustomPoints": "Pontos personalizados", "losCustomPointLabel": "{index} personalizado", "@losCustomPointLabel": { @@ -1668,8 +1668,8 @@ } }, "losRun": "Executar LOS", - "losNoElevationData": "Sem dados de elevação", - "losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}", + "losNoElevationData": "Sem dados de elevação", + "losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.", - "losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.", + "losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.", + "losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.", "losRenameCustomPoint": "Renomear ponto personalizado", "losPointName": "Nome do ponto", "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", - "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Horizonte de rádio", + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte de rádio", "losLegendLosBeam": "Linha de visada", "losLegendTerrain": "Terreno", - "losFrequencyLabel": "Frequência", - "losFrequencyInfoTooltip": "Ver detalhes do cálculo", - "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", - "losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", + "losFrequencyLabel": "Frequência", + "losFrequencyInfoTooltip": "Ver detalhes do cálculo", + "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", + "losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1798,16 +1798,14 @@ }, "contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...", "contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...", - "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", + "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", "contacts_searchContactsNoNumber": "Pesquisar Contatos...", - "contacts_unread": "Não lido", + "contacts_unread": "Não lido", "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...", - "connectionChoiceSubtitle": "Selecione a forma como você deseja acessar seu dispositivo MeshCore.", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceTitle": "Escolha o método de conexão desejado.", - "usbScreenNote": "A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.", - "usbScreenSubtitle": "Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.", + "usbScreenNote": "A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.", + "usbScreenSubtitle": "Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.", "usbScreenStatus": "Selecione um dispositivo USB", "usbScreenTitle": "Conecte via USB", "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize." diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 2b8ab81..b0d7d50 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", +{ + "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,757 +9,757 @@ }, "@@locale": "ru", "appTitle": "MeshCore Open", - "nav_contacts": "Контакты", - "nav_channels": "Каналы", - "nav_map": "Карта", - "common_cancel": "Отмена", + "nav_contacts": "Контакты", + "nav_channels": "Каналы", + "nav_map": "Карта", + "common_cancel": "Отмена", "common_ok": "OK", - "common_connect": "Коннект", - "common_unknownDevice": "Неизвестное устройство", - "common_save": "Сохранить", - "common_delete": "Удалить", - "common_close": "Закрыть", - "common_edit": "Изменить", - "common_add": "Добавить", - "common_settings": "Настройки", - "common_disconnect": "Отключить", - "common_connected": "Подключено", - "common_disconnected": "Отключено", - "common_create": "Создать", - "common_continue": "Продолжить", - "common_share": "Поделиться", - "common_copy": "Копировать", - "common_retry": "Повторить", - "common_hide": "Скрыть", - "common_remove": "Убрать", - "common_enable": "Включить", - "common_disable": "Выключить", - "common_reboot": "Перезагрузить", - "common_loading": "Загрузка...", - "common_notAvailable": "—", - "common_voltageValue": "{volts} В", + "common_connect": "Коннект", + "common_unknownDevice": "Неизвестное устройство", + "common_save": "Сохранить", + "common_delete": "Удалить", + "common_close": "Закрыть", + "common_edit": "Изменить", + "common_add": "Добавить", + "common_settings": "Настройки", + "common_disconnect": "Отключить", + "common_connected": "Подключено", + "common_disconnected": "Отключено", + "common_create": "Создать", + "common_continue": "Продолжить", + "common_share": "Поделиться", + "common_copy": "Копировать", + "common_retry": "Повторить", + "common_hide": "Скрыть", + "common_remove": "Убрать", + "common_enable": "Включить", + "common_disable": "Выключить", + "common_reboot": "Перезагрузить", + "common_loading": "Загрузка...", + "common_notAvailable": "—", + "common_voltageValue": "{volts} Ð’", "common_percentValue": "{percent}%", "scanner_title": "MeshCore Open", - "scanner_scanning": "Поиск устройств...", - "scanner_connecting": "Подключение...", - "scanner_disconnecting": "Отключение...", - "scanner_notConnected": "Не подключено", - "scanner_connectedTo": "Подключено к {deviceName}", - "scanner_searchingDevices": "Поиск устройств MeshCore...", - "scanner_tapToScan": "Нажмите для поиска MeshCore устройств", - "scanner_connectionFailed": "Подключение не удалось: {error}", - "scanner_stop": "Стоп", - "scanner_scan": "Сканирование", - "device_quickSwitch": "Быстрое переключение", + "scanner_scanning": "Поиск устройств...", + "scanner_connecting": "Подключение...", + "scanner_disconnecting": "Отключение...", + "scanner_notConnected": "Не подключено", + "scanner_connectedTo": "Подключено к {deviceName}", + "scanner_searchingDevices": "Поиск устройств MeshCore...", + "scanner_tapToScan": "Нажмите для поиска MeshCore устройств", + "scanner_connectionFailed": "Подключение не удалось: {error}", + "scanner_stop": "Стоп", + "scanner_scan": "Сканирование", + "device_quickSwitch": "Быстрое переключение", "device_meshcore": "MeshCore", - "settings_title": "Настройки", - "settings_deviceInfo": "Информация об устройстве", - "settings_appSettings": "Настройки приложения", - "settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты", - "settings_nodeSettings": "Настройки ноды", - "settings_nodeName": "Имя ноды", - "settings_nodeNameNotSet": "Не установлено", - "settings_nodeNameHint": "Введите имя ноды", - "settings_nodeNameUpdated": "Имя обновлено", - "settings_radioSettings": "Настройки радио", - "settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения", - "settings_radioSettingsUpdated": "Настройки радио обновлены", - "settings_location": "Позиция", - "settings_locationSubtitle": "Координаты GPS", - "settings_locationUpdated": "Позиция и настройки GPS обновлены", - "settings_locationBothRequired": "Введите широту и долготу.", - "settings_locationInvalid": "Неверная широта или долгота.", - "settings_locationGPSEnable": "Включить GPS", - "settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.", - "settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)", - "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", - "settings_latitude": "Широта", - "settings_longitude": "Долгота", - "settings_privacyMode": "Режим конфиденциальности", - "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", - "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", - "settings_privacyModeEnabled": "Режим конфиденциальности включен", - "settings_privacyModeDisabled": "Режим конфиденциальности выключен", - "settings_actions": "Действия", - "settings_sendAdvertisement": "Отправить анонсирование", - "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", - "settings_advertisementSent": "Анонсирование отправлено", - "settings_syncTime": "Синхронизация времени", - "settings_syncTimeSubtitle": "Синхронизировать время с телефоном", - "settings_timeSynchronized": "Время синхронизировано", - "settings_refreshContacts": "Обновить контакты", - "settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства", - "settings_rebootDevice": "Перезагрузить устройство", - "settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore", - "settings_rebootDeviceConfirm": "Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.", - "settings_debug": "Отладка", - "settings_bleDebugLog": "Журнал отладки BLE", - "settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные", - "settings_appDebugLog": "Журнал отладки приложения", - "settings_appDebugLogSubtitle": "Сообщения отладки приложения", - "settings_about": "О программе", + "settings_title": "Настройки", + "settings_deviceInfo": "Информация об устройстве", + "settings_appSettings": "Настройки приложения", + "settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты", + "settings_nodeSettings": "Настройки ноды", + "settings_nodeName": "Имя ноды", + "settings_nodeNameNotSet": "Не установлено", + "settings_nodeNameHint": "Введите имя ноды", + "settings_nodeNameUpdated": "Имя обновлено", + "settings_radioSettings": "Настройки радио", + "settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения", + "settings_radioSettingsUpdated": "Настройки радио обновлены", + "settings_location": "Позиция", + "settings_locationSubtitle": "Координаты GPS", + "settings_locationUpdated": "Позиция и настройки GPS обновлены", + "settings_locationBothRequired": "Введите широту и долготу.", + "settings_locationInvalid": "Неверная широта или долгота.", + "settings_locationGPSEnable": "Включить GPS", + "settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.", + "settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)", + "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", + "settings_latitude": "Широта", + "settings_longitude": "Долгота", + "settings_privacyMode": "Режим конфиденциальности", + "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", + "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", + "settings_privacyModeEnabled": "Режим конфиденциальности включен", + "settings_privacyModeDisabled": "Режим конфиденциальности выключен", + "settings_actions": "Действия", + "settings_sendAdvertisement": "Отправить анонсирование", + "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", + "settings_advertisementSent": "Анонсирование отправлено", + "settings_syncTime": "Синхронизация времени", + "settings_syncTimeSubtitle": "Синхронизировать время с телефоном", + "settings_timeSynchronized": "Время синхронизировано", + "settings_refreshContacts": "Обновить контакты", + "settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства", + "settings_rebootDevice": "Перезагрузить устройство", + "settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore", + "settings_rebootDeviceConfirm": "Ð’Ñ‹ уверены, что хотите перезагрузить устройство? Ð’Ñ‹ будете отключены.", + "settings_debug": "Отладка", + "settings_bleDebugLog": "Журнал отладки BLE", + "settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные", + "settings_appDebugLog": "Журнал отладки приложения", + "settings_appDebugLogSubtitle": "Сообщения отладки приложения", + "settings_about": "О программе", "settings_aboutVersion": "MeshCore Open v{version}", "settings_aboutLegalese": "2026 MeshCore Open Source Project", - "settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.", - "settings_infoName": "Имя", + "settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.", + "settings_infoName": "Имя", "settings_infoId": "ID", - "settings_infoStatus": "Статус", - "settings_infoBattery": "Батарея", - "settings_infoPublicKey": "Публичный ключ", - "settings_infoContactsCount": "Количество контактов", - "settings_infoChannelCount": "Количество каналов", - "settings_presets": "Пресеты", - "settings_frequency": "Частота (МГц)", - "settings_frequencyHelper": "300.0 – 2500.0", - "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", - "settings_bandwidth": "Полоса пропускания", - "settings_spreadingFactor": "Коэффициент расширения", - "settings_codingRate": "Коэффициент кодирования", - "settings_txPower": "Мощность передачи (дБм)", - "settings_txPowerHelper": "0 – 22", - "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", - "settings_error": "Ошибка: {message}", - "appSettings_title": "Настройки приложения", - "appSettings_appearance": "Внешний вид", - "appSettings_theme": "Тема", - "appSettings_themeSystem": "Как в системе", - "appSettings_themeLight": "Светлая", - "appSettings_themeDark": "Тёмная", - "appSettings_language": "Язык", - "appSettings_languageSystem": "Как в системе", - "appSettings_languageEn": "Английский", - "appSettings_languageFr": "Французский", - "appSettings_languageEs": "Испанский", - "appSettings_languageDe": "Немецкий", - "appSettings_languagePl": "Польский", - "appSettings_languageSl": "Словенский", - "appSettings_languagePt": "Португальский", - "appSettings_languageIt": "Итальянский", - "appSettings_languageZh": "Китайский", - "appSettings_languageSv": "Шведский", - "appSettings_languageNl": "Нидерландский", - "appSettings_languageSk": "Словацкий", - "appSettings_languageBg": "Болгарский", - "appSettings_languageRu": "Русский", - "appSettings_notifications": "Уведомления", - "appSettings_enableNotifications": "Включить уведомления", - "appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях", - "appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено", - "appSettings_notificationsEnabled": "Уведомления включены", - "appSettings_notificationsDisabled": "Уведомления отключены", - "appSettings_messageNotifications": "Уведомления о сообщениях", - "appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений", - "appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах", - "appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах", - "appSettings_advertisementNotifications": "Уведомления об анонсированиях", - "appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод", - "appSettings_messaging": "Обмен сообщениями", - "appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток", - "appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки", - "appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток", - "appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться", - "appSettings_autoRouteRotation": "Автоматическое переключение маршрутов", - "appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки", - "appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено", - "appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено", - "appSettings_battery": "Батарея", - "appSettings_batteryChemistry": "Химия батареи", - "appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})", - "appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать", - "appSettings_batteryNmc": "18650 NMC (3.0–4.2 В)", - "appSettings_batteryLifepo4": "LiFePO4 (2.6–3.65 В)", - "appSettings_batteryLipo": "LiPo (3.0–4.2 В)", - "appSettings_mapDisplay": "Отображение карты", - "appSettings_showRepeaters": "Показывать репитеры", - "appSettings_showRepeatersSubtitle": "Отображать репитеры на карте", - "appSettings_showChatNodes": "Показывать чат-ноды", - "appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте", - "appSettings_showOtherNodes": "Показывать другие ноды", - "appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте", - "appSettings_timeFilter": "Фильтр по времени", - "appSettings_timeFilterShowAll": "Показывать все ноды", - "appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч", - "appSettings_mapTimeFilter": "Временной фильтр карты", - "appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:", - "appSettings_allTime": "Всё время", - "appSettings_lastHour": "Последний час", - "appSettings_last6Hours": "Последние 6 часов", - "appSettings_last24Hours": "Последние 24 часа", - "appSettings_lastWeek": "Последнюю неделю", - "appSettings_offlineMapCache": "Кэш офлайн-карты", - "appSettings_noAreaSelected": "Область не выбрана", - "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", - "appSettings_debugCard": "Отладка", - "appSettings_appDebugLogging": "Журнал отладки приложения", - "appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики", - "appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён", - "appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён", - "contacts_title": "Контакты", - "contacts_noContacts": "Контактов пока нет", - "contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения", - "contacts_searchContacts": "Поиск контактов...", - "contacts_noUnreadContacts": "Нет непрочитанных контактов", - "contacts_noContactsFound": "Контакты или группы не найдены", - "contacts_deleteContact": "Удалить контакт", - "contacts_removeConfirm": "Удалить {contactName} из контактов?", - "contacts_manageRepeater": "Управление репитером", - "contacts_manageRoom": "Управление сервером комнат", - "contacts_roomLogin": "Вход на сервер комнат", - "contacts_openChat": "Открыть чат", - "contacts_editGroup": "Изменить группу", - "contacts_deleteGroup": "Удалить группу", - "contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?", - "contacts_newGroup": "Новая группа", - "contacts_groupName": "Имя группы", - "contacts_groupNameRequired": "Имя группы обязательно", - "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", - "contacts_filterContacts": "Фильтр контактов...", - "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", - "contacts_noMembers": "Нет участников", - "contacts_lastSeenNow": "Видели только что", - "contacts_lastSeenMinsAgo": "Видели {minutes} мин назад", - "contacts_lastSeenHourAgo": "Видели 1 час назад", - "contacts_lastSeenHoursAgo": "Видели {hours} ч назад", - "contacts_lastSeenDayAgo": "Видели 1 день назад", - "contacts_lastSeenDaysAgo": "Видели {days} дн. назад", - "channels_title": "Каналы", - "channels_noChannelsConfigured": "Каналы не настроены", - "channels_addPublicChannel": "Добавить публичный канал", - "channels_searchChannels": "Поиск каналов...", - "channels_noChannelsFound": "Каналы не найдены", - "channels_channelIndex": "Канал {index}", - "channels_hashtagChannel": "Хэштег-канал", - "channels_public": "Публичный", - "channels_private": "Приватный", - "channels_publicChannel": "Публичный канал", - "channels_privateChannel": "Приватный канал", - "channels_editChannel": "Изменить канал", - "channels_muteChannel": "Отключить уведомления канала", - "channels_unmuteChannel": "Включить уведомления канала", - "channels_deleteChannel": "Удалить канал", - "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", - "channels_channelDeleted": "Канал \"{name}\" удалён", - "channels_addChannel": "Добавить канал", - "channels_channelIndexLabel": "Индекс канала", - "channels_channelName": "Имя канала", - "channels_usePublicChannel": "Использовать публичный канал", - "channels_standardPublicPsk": "Стандартный публичный PSK", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батарея", + "settings_infoPublicKey": "Публичный ключ", + "settings_infoContactsCount": "Количество контактов", + "settings_infoChannelCount": "Количество каналов", + "settings_presets": "Пресеты", + "settings_frequency": "Частота (МГц)", + "settings_frequencyHelper": "300.0 – 2500.0", + "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", + "settings_bandwidth": "Полоса пропускания", + "settings_spreadingFactor": "Коэффициент расширения", + "settings_codingRate": "Коэффициент кодирования", + "settings_txPower": "Мощность передачи (дБм)", + "settings_txPowerHelper": "0 – 22", + "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", + "settings_error": "Ошибка: {message}", + "appSettings_title": "Настройки приложения", + "appSettings_appearance": "Внешний вид", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Как в системе", + "appSettings_themeLight": "Светлая", + "appSettings_themeDark": "Тёмная", + "appSettings_language": "Язык", + "appSettings_languageSystem": "Как в системе", + "appSettings_languageEn": "Английский", + "appSettings_languageFr": "Французский", + "appSettings_languageEs": "Испанский", + "appSettings_languageDe": "Немецкий", + "appSettings_languagePl": "Польский", + "appSettings_languageSl": "Словенский", + "appSettings_languagePt": "Португальский", + "appSettings_languageIt": "Итальянский", + "appSettings_languageZh": "Китайский", + "appSettings_languageSv": "Шведский", + "appSettings_languageNl": "Нидерландский", + "appSettings_languageSk": "Словацкий", + "appSettings_languageBg": "Болгарский", + "appSettings_languageRu": "Русский", + "appSettings_notifications": "Уведомления", + "appSettings_enableNotifications": "Включить уведомления", + "appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях", + "appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено", + "appSettings_notificationsEnabled": "Уведомления включены", + "appSettings_notificationsDisabled": "Уведомления отключены", + "appSettings_messageNotifications": "Уведомления о сообщениях", + "appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений", + "appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах", + "appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах", + "appSettings_advertisementNotifications": "Уведомления об анонсированиях", + "appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод", + "appSettings_messaging": "Обмен сообщениями", + "appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток", + "appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки", + "appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток", + "appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться", + "appSettings_autoRouteRotation": "Автоматическое переключение маршрутов", + "appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки", + "appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено", + "appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено", + "appSettings_battery": "Батарея", + "appSettings_batteryChemistry": "Химия батареи", + "appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})", + "appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать", + "appSettings_batteryNmc": "18650 NMC (3.0–4.2 Ð’)", + "appSettings_batteryLifepo4": "LiFePO4 (2.6–3.65 Ð’)", + "appSettings_batteryLipo": "LiPo (3.0–4.2 Ð’)", + "appSettings_mapDisplay": "Отображение карты", + "appSettings_showRepeaters": "Показывать репитеры", + "appSettings_showRepeatersSubtitle": "Отображать репитеры на карте", + "appSettings_showChatNodes": "Показывать чат-ноды", + "appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте", + "appSettings_showOtherNodes": "Показывать другие ноды", + "appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте", + "appSettings_timeFilter": "Фильтр по времени", + "appSettings_timeFilterShowAll": "Показывать все ноды", + "appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч", + "appSettings_mapTimeFilter": "Временной фильтр карты", + "appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:", + "appSettings_allTime": "Всё время", + "appSettings_lastHour": "Последний час", + "appSettings_last6Hours": "Последние 6 часов", + "appSettings_last24Hours": "Последние 24 часа", + "appSettings_lastWeek": "Последнюю неделю", + "appSettings_offlineMapCache": "Кэш офлайн-карты", + "appSettings_noAreaSelected": "Область не выбрана", + "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", + "appSettings_debugCard": "Отладка", + "appSettings_appDebugLogging": "Журнал отладки приложения", + "appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики", + "appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён", + "appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён", + "contacts_title": "Контакты", + "contacts_noContacts": "Контактов пока нет", + "contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения", + "contacts_searchContacts": "Поиск контактов...", + "contacts_noUnreadContacts": "Нет непрочитанных контактов", + "contacts_noContactsFound": "Контакты или группы не найдены", + "contacts_deleteContact": "Удалить контакт", + "contacts_removeConfirm": "Удалить {contactName} из контактов?", + "contacts_manageRepeater": "Управление репитером", + "contacts_manageRoom": "Управление сервером комнат", + "contacts_roomLogin": "Вход на сервер комнат", + "contacts_openChat": "Открыть чат", + "contacts_editGroup": "Изменить группу", + "contacts_deleteGroup": "Удалить группу", + "contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?", + "contacts_newGroup": "Новая группа", + "contacts_groupName": "Имя группы", + "contacts_groupNameRequired": "Имя группы обязательно", + "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", + "contacts_filterContacts": "Фильтр контактов...", + "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", + "contacts_noMembers": "Нет участников", + "contacts_lastSeenNow": "Видели только что", + "contacts_lastSeenMinsAgo": "Видели {minutes} мин назад", + "contacts_lastSeenHourAgo": "Видели 1 час назад", + "contacts_lastSeenHoursAgo": "Видели {hours} ч назад", + "contacts_lastSeenDayAgo": "Видели 1 день назад", + "contacts_lastSeenDaysAgo": "Видели {days} дн. назад", + "channels_title": "Каналы", + "channels_noChannelsConfigured": "Каналы не настроены", + "channels_addPublicChannel": "Добавить публичный канал", + "channels_searchChannels": "Поиск каналов...", + "channels_noChannelsFound": "Каналы не найдены", + "channels_channelIndex": "Канал {index}", + "channels_hashtagChannel": "Хэштег-канал", + "channels_public": "Публичный", + "channels_private": "Приватный", + "channels_publicChannel": "Публичный канал", + "channels_privateChannel": "Приватный канал", + "channels_editChannel": "Изменить канал", + "channels_muteChannel": "Отключить уведомления канала", + "channels_unmuteChannel": "Включить уведомления канала", + "channels_deleteChannel": "Удалить канал", + "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", + "channels_channelDeleted": "Канал \"{name}\" удалён", + "channels_addChannel": "Добавить канал", + "channels_channelIndexLabel": "Индекс канала", + "channels_channelName": "Имя канала", + "channels_usePublicChannel": "Использовать публичный канал", + "channels_standardPublicPsk": "Стандартный публичный PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Сгенерировать случайный PSK", - "channels_enterChannelName": "Введите имя канала", - "channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа", - "channels_channelAdded": "Канал \"{name}\" добавлен", - "channels_editChannelTitle": "Изменить канал {index}", - "channels_smazCompression": "Сжатие SMAZ", - "channels_channelUpdated": "Канал \"{name}\" обновлён", - "channels_publicChannelAdded": "Публичный канал добавлен", - "channels_sortBy": "Сортировка", - "channels_sortManual": "Вручную", - "channels_sortAZ": "По алфавиту", - "channels_sortLatestMessages": "По последним сообщениям", - "channels_sortUnread": "По непрочитанным", - "channels_createPrivateChannel": "Создать приватный канал", - "channels_createPrivateChannelDesc": "Защищён секретным ключом.", - "channels_joinPrivateChannel": "Присоединиться к приватному каналу", - "channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.", - "channels_joinPublicChannel": "Присоединиться к публичному каналу", - "channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.", - "channels_joinHashtagChannel": "Присоединиться к хэштег-каналу", - "channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.", - "channels_scanQrCode": "Сканировать QR-код", - "channels_scanQrCodeComingSoon": "Скоро будет", - "channels_enterHashtag": "Введите хэштег", - "channels_hashtagHint": "например, #команда", - "chat_noMessages": "Сообщений пока нет", - "chat_sendMessageToStart": "Отправьте сообщение, чтобы начать", - "chat_originalMessageNotFound": "Исходное сообщение не найдено", - "chat_replyingTo": "Ответ для {name}", - "chat_replyTo": "Ответить {name}", - "chat_location": "Местоположение", - "chat_sendMessageTo": "Отправить сообщение {contactName}", - "chat_typeMessage": "Напишите сообщение...", - "chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).", - "chat_messageCopied": "Сообщение скопировано", - "chat_messageDeleted": "Сообщение удалено", - "chat_retryingMessage": "Повтор отправки сообщения", - "chat_retryCount": "Попытка {current}/{max}", - "chat_sendGif": "Отправить GIF", - "chat_reply": "Ответить", - "chat_addReaction": "Добавить реакцию", - "chat_me": "Я", - "emojiCategorySmileys": "Смайлы", - "emojiCategoryGestures": "Жесты", - "emojiCategoryHearts": "Сердечки", - "emojiCategoryObjects": "Предметы", - "gifPicker_title": "Выберите GIF", - "gifPicker_searchHint": "Поиск GIF...", - "gifPicker_poweredBy": "Работает на GIPHY", - "gifPicker_noGifsFound": "GIF не найдены", - "gifPicker_failedLoad": "Не удалось загрузить GIF", - "gifPicker_failedSearch": "Не удалось выполнить поиск GIF", - "gifPicker_noInternet": "Нет подключения к интернету", - "debugLog_appTitle": "Журнал отладки приложения", - "debugLog_bleTitle": "Журнал отладки BLE", - "debugLog_copyLog": "Копировать журнал", - "debugLog_clearLog": "Очистить журнал", - "debugLog_copied": "Журнал отладки скопирован", - "debugLog_bleCopied": "Журнал BLE скопирован", - "debugLog_noEntries": "Журнал отладки пока пуст", - "debugLog_enableInSettings": "Включите запись журнала отладки в настройках", - "debugLog_frames": "Фреймы", - "debugLog_rawLogRx": "Сырой журнал приёма", - "debugLog_noBleActivity": "Активность BLE пока отсутствует", - "debugFrame_length": "Длина фрейма: {count} байт", - "debugFrame_command": "Команда: 0x{value}", - "debugFrame_textMessageHeader": "Фрейм текстового сообщения:", - "debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}", - "debugFrame_timestamp": "- Временная метка: {timestamp}", - "debugFrame_flags": "- Флаги: 0x{value}", - "debugFrame_textType": "- Тип текста: {type} ({label})", + "channels_generateRandomPsk": "Сгенерировать случайный PSK", + "channels_enterChannelName": "Введите имя канала", + "channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа", + "channels_channelAdded": "Канал \"{name}\" добавлен", + "channels_editChannelTitle": "Изменить канал {index}", + "channels_smazCompression": "Сжатие SMAZ", + "channels_channelUpdated": "Канал \"{name}\" обновлён", + "channels_publicChannelAdded": "Публичный канал добавлен", + "channels_sortBy": "Сортировка", + "channels_sortManual": "Вручную", + "channels_sortAZ": "По алфавиту", + "channels_sortLatestMessages": "По последним сообщениям", + "channels_sortUnread": "По непрочитанным", + "channels_createPrivateChannel": "Создать приватный канал", + "channels_createPrivateChannelDesc": "Защищён секретным ключом.", + "channels_joinPrivateChannel": "Присоединиться к приватному каналу", + "channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.", + "channels_joinPublicChannel": "Присоединиться к публичному каналу", + "channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.", + "channels_joinHashtagChannel": "Присоединиться к хэштег-каналу", + "channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.", + "channels_scanQrCode": "Сканировать QR-код", + "channels_scanQrCodeComingSoon": "Скоро будет", + "channels_enterHashtag": "Введите хэштег", + "channels_hashtagHint": "например, #команда", + "chat_noMessages": "Сообщений пока нет", + "chat_sendMessageToStart": "Отправьте сообщение, чтобы начать", + "chat_originalMessageNotFound": "Исходное сообщение не найдено", + "chat_replyingTo": "Ответ для {name}", + "chat_replyTo": "Ответить {name}", + "chat_location": "Местоположение", + "chat_sendMessageTo": "Отправить сообщение {contactName}", + "chat_typeMessage": "Напишите сообщение...", + "chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).", + "chat_messageCopied": "Сообщение скопировано", + "chat_messageDeleted": "Сообщение удалено", + "chat_retryingMessage": "Повтор отправки сообщения", + "chat_retryCount": "Попытка {current}/{max}", + "chat_sendGif": "Отправить GIF", + "chat_reply": "Ответить", + "chat_addReaction": "Добавить реакцию", + "chat_me": "Я", + "emojiCategorySmileys": "Смайлы", + "emojiCategoryGestures": "Жесты", + "emojiCategoryHearts": "Сердечки", + "emojiCategoryObjects": "Предметы", + "gifPicker_title": "Выберите GIF", + "gifPicker_searchHint": "Поиск GIF...", + "gifPicker_poweredBy": "Работает на GIPHY", + "gifPicker_noGifsFound": "GIF не найдены", + "gifPicker_failedLoad": "Не удалось загрузить GIF", + "gifPicker_failedSearch": "Не удалось выполнить поиск GIF", + "gifPicker_noInternet": "Нет подключения к интернету", + "debugLog_appTitle": "Журнал отладки приложения", + "debugLog_bleTitle": "Журнал отладки BLE", + "debugLog_copyLog": "Копировать журнал", + "debugLog_clearLog": "Очистить журнал", + "debugLog_copied": "Журнал отладки скопирован", + "debugLog_bleCopied": "Журнал BLE скопирован", + "debugLog_noEntries": "Журнал отладки пока пуст", + "debugLog_enableInSettings": "Включите запись журнала отладки в настройках", + "debugLog_frames": "Фреймы", + "debugLog_rawLogRx": "Сырой журнал приёма", + "debugLog_noBleActivity": "Активность BLE пока отсутствует", + "debugFrame_length": "Длина фрейма: {count} байт", + "debugFrame_command": "Команда: 0x{value}", + "debugFrame_textMessageHeader": "Фрейм текстового сообщения:", + "debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}", + "debugFrame_timestamp": "- Временная метка: {timestamp}", + "debugFrame_flags": "- Флаги: 0x{value}", + "debugFrame_textType": "- Тип текста: {type} ({label})", "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Обычный", - "debugFrame_text": "- Текст: \"{text}\"", - "debugFrame_hexDump": "Шестнадцатеричный дамп:", - "chat_pathManagement": "Управление маршрутами", - "chat_routingMode": "Режим маршрутизации", - "chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", - "chat_forceFloodMode": "Принудительный режим рассылки", - "chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):", - "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", - "chat_hopSingular": "хоп", - "chat_hopPlural": "хопов", - "chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", - "chat_successes": "успешно", - "chat_removePath": "Удалить маршрут", - "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", - "chat_pathActions": "Действия с маршрутом:", - "chat_setCustomPath": "Указать маршрут вручную", - "chat_setCustomPathSubtitle": "Вручную задать маршрут передачи", - "chat_clearPath": "Очистить маршрут", - "chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке", - "chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.", - "chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения", - "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", - "chat_fullPath": "Полный маршрут", - "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", - "chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}", - "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", - "chat_pathDeviceConfirmed": "Подтверждено устройством.", - "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", - "chat_type": "Тип", - "chat_path": "Маршрут", - "chat_publicKey": "Публичный ключ", - "chat_compressOutgoingMessages": "Сжимать исходящие сообщения", - "chat_floodForced": "Рассылка (принудительно)", - "chat_directForced": "Прямой (принудительно)", - "chat_hopsForced": "{count} хоп(ов) (принудительно)", - "chat_floodAuto": "Рассылка (авто)", - "chat_direct": "Прямой", - "chat_poiShared": "Точка интереса отправлена", - "chat_unread": "Непрочитанных: {count}", - "map_title": "Карта нод", - "map_noNodesWithLocation": "Нет нод с данными о местоположении", - "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте", - "map_nodesCount": "Нод: {count}", - "map_pinsCount": "Меток: {count}", - "map_chat": "Чат", - "map_repeater": "Репитер", - "map_room": "Комната", - "map_sensor": "Сенсор", - "map_pinDm": "Метка (ЛС)", - "map_pinPrivate": "Метка (Приватная)", - "map_pinPublic": "Метка (Публичная)", - "map_lastSeen": "Последнее появление", - "map_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", - "map_from": "От", - "map_source": "Источник", - "map_flags": "Флаги", - "map_shareMarkerHere": "Поделиться меткой здесь", - "map_pinLabel": "Метка", - "map_label": "Подпись", - "map_pointOfInterest": "Точка интереса", - "map_sendToContact": "Отправить контакту", - "map_sendToChannel": "Отправить в канал", - "map_noChannelsAvailable": "Нет доступных каналов", - "map_publicLocationShare": "Публичная передача местоположения", - "map_publicLocationShareConfirm": "Вы собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.", - "map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками", - "map_filterNodes": "Фильтр нод", - "map_nodeTypes": "Типы нод", - "map_chatNodes": "Чат-ноды", - "map_repeaters": "Репитеры", - "map_otherNodes": "Другие ноды", - "map_keyPrefix": "Префикс ключа", - "map_filterByKeyPrefix": "Фильтр по префиксу ключа", - "map_publicKeyPrefix": "Префикс публичного ключа", - "map_markers": "Метки", - "map_showSharedMarkers": "Показывать общие метки", - "map_lastSeenTime": "Время последнего появления", - "map_sharedPin": "Общая метка", - "map_joinRoom": "Присоединиться к комнате", - "map_manageRepeater": "Управление репитером", - "mapCache_title": "Кэш офлайн-карты", - "mapCache_selectAreaFirst": "Сначала выберите область для кэширования", - "mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области", - "mapCache_downloadTilesTitle": "Загрузить плитки", - "mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?", - "mapCache_downloadAction": "Загрузить", - "mapCache_cachedTiles": "Закэшировано {count} плиток", - "mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)", - "mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш", - "mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?", - "mapCache_offlineCacheCleared": "Офлайн-кэш очищен", - "mapCache_noAreaSelected": "Область не выбрана", - "mapCache_cacheArea": "Область кэширования", - "mapCache_useCurrentView": "Использовать текущий вид", - "mapCache_zoomRange": "Диапазон масштаба", - "mapCache_estimatedTiles": "Оценочное количество плиток: {count}", - "mapCache_downloadedTiles": "Загружено {completed} из {total}", - "mapCache_downloadTilesButton": "Загрузить плитки", - "mapCache_clearCacheButton": "Очистить кэш", - "mapCache_failedDownloads": "Неудачных загрузок: {count}", - "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}", - "time_justNow": "Только что", - "time_minutesAgo": "{minutes} мин назад", - "time_hoursAgo": "{hours} ч назад", - "time_daysAgo": "{days} дн. назад", - "time_hour": "час", - "time_hours": "часов", - "time_day": "день", - "time_days": "дней", - "time_week": "неделя", - "time_weeks": "недель", - "time_month": "месяц", - "time_months": "месяцев", - "time_minutes": "минут", - "time_allTime": "Всё время", - "dialog_disconnect": "Отключиться", - "dialog_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", - "login_repeaterLogin": "Вход в репитер", - "login_roomLogin": "Вход на сервер комнат", - "login_password": "Пароль", - "login_enterPassword": "Введите пароль", - "login_savePassword": "Сохранить пароль", - "login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве", - "login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.", - "login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.", - "login_routing": "Маршрутизация", - "login_routingMode": "Режим маршрутизации", - "login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", - "login_forceFloodMode": "Принудительный режим рассылки", - "login_managePaths": "Управление маршрутами", - "login_login": "Войти", - "login_attempt": "Попытка {current}/{max}", - "login_failed": "Ошибка входа: {error}", - "login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.", - "common_reload": "Обновить", - "common_clear": "Очистить", - "path_currentPath": "Текущий маршрут: {path}", - "path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", - "path_enterCustomPath": "Введите маршрут вручную", - "path_currentPathLabel": "Текущий маршрут", - "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", - "path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)", - "path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)", - "path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)", - "path_selectFromContacts": "Или выберите из контактов:", - "path_noRepeatersFound": "Репитеры или серверы комнат не найдены.", - "path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.", - "path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}", - "path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.", - "path_setPath": "Установить маршрут", - "repeater_management": "Управление репитером", - "room_management": "Управление сервером комнат", - "repeater_managementTools": "Инструменты управления", - "repeater_status": "Статус", - "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера", - "repeater_telemetry": "Телеметрия", - "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", + "debugFrame_textTypePlain": "Обычный", + "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_hexDump": "Шестнадцатеричный дамп:", + "chat_pathManagement": "Управление маршрутами", + "chat_routingMode": "Режим маршрутизации", + "chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "chat_forceFloodMode": "Принудительный режим рассылки", + "chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):", + "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", + "chat_hopSingular": "хоп", + "chat_hopPlural": "хопов", + "chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", + "chat_successes": "успешно", + "chat_removePath": "Удалить маршрут", + "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", + "chat_pathActions": "Действия с маршрутом:", + "chat_setCustomPath": "Указать маршрут вручную", + "chat_setCustomPathSubtitle": "Вручную задать маршрут передачи", + "chat_clearPath": "Очистить маршрут", + "chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке", + "chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.", + "chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения", + "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", + "chat_fullPath": "Полный маршрут", + "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", + "chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}", + "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", + "chat_pathDeviceConfirmed": "Подтверждено устройством.", + "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", + "chat_type": "Тип", + "chat_path": "Маршрут", + "chat_publicKey": "Публичный ключ", + "chat_compressOutgoingMessages": "Сжимать исходящие сообщения", + "chat_floodForced": "Рассылка (принудительно)", + "chat_directForced": "Прямой (принудительно)", + "chat_hopsForced": "{count} хоп(ов) (принудительно)", + "chat_floodAuto": "Рассылка (авто)", + "chat_direct": "Прямой", + "chat_poiShared": "Точка интереса отправлена", + "chat_unread": "Непрочитанных: {count}", + "map_title": "Карта нод", + "map_noNodesWithLocation": "Нет нод с данными о местоположении", + "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте", + "map_nodesCount": "Нод: {count}", + "map_pinsCount": "Меток: {count}", + "map_chat": "Чат", + "map_repeater": "Репитер", + "map_room": "Комната", + "map_sensor": "Сенсор", + "map_pinDm": "Метка (ЛС)", + "map_pinPrivate": "Метка (Приватная)", + "map_pinPublic": "Метка (Публичная)", + "map_lastSeen": "Последнее появление", + "map_disconnectConfirm": "Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?", + "map_from": "От", + "map_source": "Источник", + "map_flags": "Флаги", + "map_shareMarkerHere": "Поделиться меткой здесь", + "map_pinLabel": "Метка", + "map_label": "Подпись", + "map_pointOfInterest": "Точка интереса", + "map_sendToContact": "Отправить контакту", + "map_sendToChannel": "Отправить в канал", + "map_noChannelsAvailable": "Нет доступных каналов", + "map_publicLocationShare": "Публичная передача местоположения", + "map_publicLocationShareConfirm": "Ð’Ñ‹ собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.", + "map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками", + "map_filterNodes": "Фильтр нод", + "map_nodeTypes": "Типы нод", + "map_chatNodes": "Чат-ноды", + "map_repeaters": "Репитеры", + "map_otherNodes": "Другие ноды", + "map_keyPrefix": "Префикс ключа", + "map_filterByKeyPrefix": "Фильтр по префиксу ключа", + "map_publicKeyPrefix": "Префикс публичного ключа", + "map_markers": "Метки", + "map_showSharedMarkers": "Показывать общие метки", + "map_lastSeenTime": "Время последнего появления", + "map_sharedPin": "Общая метка", + "map_joinRoom": "Присоединиться к комнате", + "map_manageRepeater": "Управление репитером", + "mapCache_title": "Кэш офлайн-карты", + "mapCache_selectAreaFirst": "Сначала выберите область для кэширования", + "mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области", + "mapCache_downloadTilesTitle": "Загрузить плитки", + "mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?", + "mapCache_downloadAction": "Загрузить", + "mapCache_cachedTiles": "Закэшировано {count} плиток", + "mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)", + "mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш", + "mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?", + "mapCache_offlineCacheCleared": "Офлайн-кэш очищен", + "mapCache_noAreaSelected": "Область не выбрана", + "mapCache_cacheArea": "Область кэширования", + "mapCache_useCurrentView": "Использовать текущий вид", + "mapCache_zoomRange": "Диапазон масштаба", + "mapCache_estimatedTiles": "Оценочное количество плиток: {count}", + "mapCache_downloadedTiles": "Загружено {completed} из {total}", + "mapCache_downloadTilesButton": "Загрузить плитки", + "mapCache_clearCacheButton": "Очистить кэш", + "mapCache_failedDownloads": "Неудачных загрузок: {count}", + "mapCache_boundsLabel": "С {north}, Ю {south}, Ð’ {east}, З {west}", + "time_justNow": "Только что", + "time_minutesAgo": "{minutes} мин назад", + "time_hoursAgo": "{hours} ч назад", + "time_daysAgo": "{days} дн. назад", + "time_hour": "час", + "time_hours": "часов", + "time_day": "день", + "time_days": "дней", + "time_week": "неделя", + "time_weeks": "недель", + "time_month": "месяц", + "time_months": "месяцев", + "time_minutes": "минут", + "time_allTime": "Всё время", + "dialog_disconnect": "Отключиться", + "dialog_disconnectConfirm": "Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?", + "login_repeaterLogin": "Вход в репитер", + "login_roomLogin": "Вход на сервер комнат", + "login_password": "Пароль", + "login_enterPassword": "Введите пароль", + "login_savePassword": "Сохранить пароль", + "login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве", + "login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.", + "login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.", + "login_routing": "Маршрутизация", + "login_routingMode": "Режим маршрутизации", + "login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "login_forceFloodMode": "Принудительный режим рассылки", + "login_managePaths": "Управление маршрутами", + "login_login": "Войти", + "login_attempt": "Попытка {current}/{max}", + "login_failed": "Ошибка входа: {error}", + "login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.", + "common_reload": "Обновить", + "common_clear": "Очистить", + "path_currentPath": "Текущий маршрут: {path}", + "path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", + "path_enterCustomPath": "Введите маршрут вручную", + "path_currentPathLabel": "Текущий маршрут", + "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", + "path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)", + "path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)", + "path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)", + "path_selectFromContacts": "Или выберите из контактов:", + "path_noRepeatersFound": "Репитеры или серверы комнат не найдены.", + "path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.", + "path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}", + "path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.", + "path_setPath": "Установить маршрут", + "repeater_management": "Управление репитером", + "room_management": "Управление сервером комнат", + "repeater_managementTools": "Инструменты управления", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера", + "repeater_telemetry": "Телеметрия", + "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Отправка команд репитеру", - "repeater_neighbors": "Соседи", - "repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.", - "repeater_settings": "Настройки", - "repeater_settingsSubtitle": "Настройка параметров репитера", - "repeater_statusTitle": "Статус репитера", - "repeater_routingMode": "Режим маршрутизации", - "repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", - "repeater_forceFloodMode": "Принудительный режим рассылки", - "repeater_pathManagement": "Управление маршрутами", - "repeater_refresh": "Обновить", - "repeater_statusRequestTimeout": "Время ожидания статуса истекло.", - "repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}", - "repeater_systemInformation": "Системная информация", - "repeater_battery": "Батарея", - "repeater_clockAtLogin": "Время (при входе)", - "repeater_uptime": "Время работы", - "repeater_queueLength": "Длина очереди", - "repeater_debugFlags": "Флаги отладки", - "repeater_radioStatistics": "Радиостатистика", - "repeater_lastRssi": "Последний RSSI", - "repeater_lastSnr": "Последний SNR", - "repeater_noiseFloor": "Уровень шума", - "repeater_txAirtime": "Время эфира (передача)", - "repeater_rxAirtime": "Время эфира (приём)", - "repeater_packetStatistics": "Статистика пакетов", - "repeater_sent": "Отправлено", - "repeater_received": "Получено", - "repeater_duplicates": "Дубликаты", - "repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с", - "repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", - "repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", - "repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}", - "repeater_duplicatesTotal": "Всего: {total}", - "repeater_settingsTitle": "Настройки репитера", - "repeater_basicSettings": "Основные настройки", - "repeater_repeaterName": "Имя репитера", - "repeater_repeaterNameHelper": "Отображаемое имя этого репитера", - "repeater_adminPassword": "Пароль администратора", - "repeater_adminPasswordHelper": "Пароль с полным доступом", - "repeater_guestPassword": "Гостевой пароль", - "repeater_guestPasswordHelper": "Пароль для доступа только для чтения", - "repeater_radioSettings": "Настройки радио", - "repeater_frequencyMhz": "Частота (МГц)", - "repeater_frequencyHelper": "300–2500 МГц", - "repeater_txPower": "Мощность передачи", - "repeater_txPowerHelper": "1–30 дБм", - "repeater_bandwidth": "Полоса пропускания", - "repeater_spreadingFactor": "Коэффициент расширения", - "repeater_codingRate": "Коэффициент кодирования", - "repeater_locationSettings": "Настройки местоположения", - "repeater_latitude": "Широта", - "repeater_latitudeHelper": "В десятичных градусах (напр., 37.7749)", - "repeater_longitude": "Долгота", - "repeater_longitudeHelper": "В десятичных градусах (напр., -122.4194)", - "repeater_features": "Функции", - "repeater_packetForwarding": "Пересылка пакетов", - "repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты", - "repeater_guestAccess": "Гостевой доступ", - "repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения", - "repeater_privacyMode": "Режим конфиденциальности", - "repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях", - "repeater_advertisementSettings": "Настройки анонсирования", - "repeater_localAdvertInterval": "Интервал локальных анонсирований", - "repeater_localAdvertIntervalMinutes": "{minutes} минут", - "repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)", - "repeater_floodAdvertIntervalHours": "{hours} часов", - "repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований", - "repeater_dangerZone": "Опасная зона", - "repeater_rebootRepeater": "Перезагрузить репитер", - "repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера", - "repeater_rebootRepeaterConfirm": "Вы уверены, что хотите перезагрузить этот репитер?", - "repeater_regenerateIdentityKey": "Пересоздать ключ идентификации", - "repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей", - "repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?", - "repeater_eraseFileSystem": "Стереть файловую систему", - "repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера", - "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!", - "repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.", - "repeater_commandSent": "Команда отправлена: {command}", - "repeater_errorSendingCommand": "Ошибка отправки команды: {error}", - "repeater_confirm": "Подтвердить", - "repeater_settingsSaved": "Настройки успешно сохранены", - "repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}", - "repeater_refreshBasicSettings": "Обновить основные настройки", - "repeater_refreshRadioSettings": "Обновить настройки радио", - "repeater_refreshTxPower": "Обновить мощность передачи", - "repeater_refreshLocationSettings": "Обновить настройки местоположения", - "repeater_refreshPacketForwarding": "Обновить пересылку пакетов", - "repeater_refreshGuestAccess": "Обновить гостевой доступ", - "repeater_refreshPrivacyMode": "Обновить режим конфиденциальности", - "repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований", - "repeater_refreshed": "{label} обновлён", - "repeater_errorRefreshing": "Ошибка обновления {label}", - "repeater_cliTitle": "CLI репитера", - "repeater_debugNextCommand": "Отладка следующей команды", - "repeater_commandHelp": "Справка по командам", - "repeater_clearHistory": "Очистить историю", - "repeater_noCommandsSent": "Команды ещё не отправлялись", - "repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды", - "repeater_enterCommandHint": "Введите команду...", - "repeater_previousCommand": "Предыдущая команда", - "repeater_nextCommand": "Следующая команда", - "repeater_enterCommandFirst": "Сначала введите команду", - "repeater_cliCommandFrameTitle": "Фрейм CLI-команды", - "repeater_cliCommandError": "Ошибка: {error}", - "repeater_cliQuickGetName": "Получить имя", - "repeater_cliQuickGetRadio": "Получить радио", - "repeater_cliQuickGetTx": "Получить TX", - "repeater_cliQuickNeighbors": "Соседи", - "repeater_cliQuickVersion": "Версия", - "repeater_cliQuickAdvertise": "Анонсировать", - "repeater_cliQuickClock": "Время", - "repeater_cliHelpAdvert": "Отправляет пакет анонсирования", - "repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)", - "repeater_cliHelpClock": "Показывает текущее время по часам устройства.", - "repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.", - "repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.", - "repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.", - "repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.", - "repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)", - "repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.", - "repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)", - "repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)", - "repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.", - "repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.", - "repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».", - "repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.", - "repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.", - "repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)", - "repeater_cliHelpSetName": "Устанавливает имя в оповещениях.", - "repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)", - "repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)", - "repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.", - "repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.", - "repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).", - "repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.", - "repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.", - "repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.", - "repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.", - "repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.", - "repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.", - "repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).", - "repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).", - "repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)", - "repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow", - "repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.", - "repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.", - "repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.", - "repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4", - "repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.", - "repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.", - "repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.", - "repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»", - "repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.", - "repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)", - "repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)", - "repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)", - "repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)", - "repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.", - "repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.", - "repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.", - "repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.", - "repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.", - "repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.", - "repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек", - "repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.", - "repeater_commandsListTitle": "Список команд", - "repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».", - "repeater_general": "Общие", - "repeater_settingsCategory": "Настройки", - "repeater_bridge": "Мост", - "repeater_logging": "Журналирование", - "repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)", - "repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)", - "repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.", - "repeater_gpsManagement": "Управление GPS", - "repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.", - "telemetry_receivedData": "Полученные телеметрические данные", - "telemetry_requestTimeout": "Время ожидания телеметрии истекло.", - "telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}", - "telemetry_noData": "Данные телеметрии недоступны.", - "telemetry_channelTitle": "Канал {channel}", - "telemetry_batteryLabel": "Батарея", - "telemetry_voltageLabel": "Напряжение", - "telemetry_mcuTemperatureLabel": "Температура МК", - "telemetry_temperatureLabel": "Температура", - "telemetry_currentLabel": "Ток", - "telemetry_batteryValue": "{percent}% / {volts}В", - "telemetry_voltageValue": "{volts}В", - "telemetry_currentValue": "{amps}А", - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", - "neighbors_receivedData": "Полученные данные о соседях", - "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", - "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", - "neighbors_repeatersNeighbors": "Соседи репитеров", - "neighbors_noData": "Данные о соседях недоступны.", - "neighbors_unknownContact": "Неизвестный {pubkey}", - "neighbors_heardA ago": "Слышали: {time} назад", - "channelPath_title": "Путь пакета", - "channelPath_viewMap": "Посмотреть на карте", - "channelPath_otherObservedPaths": "Другие наблюдаемые пути", - "channelPath_repeaterHops": "Хопы через репитеры", - "channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.", - "channelPath_messageDetails": "Детали сообщения", - "channelPath_senderLabel": "Отправитель", - "channelPath_timeLabel": "Время", - "channelPath_repeatsLabel": "Повторы", - "channelPath_pathLabel": "Путь {index}", - "channelPath_observedLabel": "Наблюдаемый", - "channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}", - "channelPath_noLocationData": "Нет данных о местоположении", + "repeater_cliSubtitle": "Отправка команд репитеру", + "repeater_neighbors": "Соседи", + "repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.", + "repeater_settings": "Настройки", + "repeater_settingsSubtitle": "Настройка параметров репитера", + "repeater_statusTitle": "Статус репитера", + "repeater_routingMode": "Режим маршрутизации", + "repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "repeater_forceFloodMode": "Принудительный режим рассылки", + "repeater_pathManagement": "Управление маршрутами", + "repeater_refresh": "Обновить", + "repeater_statusRequestTimeout": "Время ожидания статуса истекло.", + "repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}", + "repeater_systemInformation": "Системная информация", + "repeater_battery": "Батарея", + "repeater_clockAtLogin": "Время (при входе)", + "repeater_uptime": "Время работы", + "repeater_queueLength": "Длина очереди", + "repeater_debugFlags": "Флаги отладки", + "repeater_radioStatistics": "Радиостатистика", + "repeater_lastRssi": "Последний RSSI", + "repeater_lastSnr": "Последний SNR", + "repeater_noiseFloor": "Уровень шума", + "repeater_txAirtime": "Время эфира (передача)", + "repeater_rxAirtime": "Время эфира (приём)", + "repeater_packetStatistics": "Статистика пакетов", + "repeater_sent": "Отправлено", + "repeater_received": "Получено", + "repeater_duplicates": "Дубликаты", + "repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с", + "repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesTotal": "Всего: {total}", + "repeater_settingsTitle": "Настройки репитера", + "repeater_basicSettings": "Основные настройки", + "repeater_repeaterName": "Имя репитера", + "repeater_repeaterNameHelper": "Отображаемое имя этого репитера", + "repeater_adminPassword": "Пароль администратора", + "repeater_adminPasswordHelper": "Пароль с полным доступом", + "repeater_guestPassword": "Гостевой пароль", + "repeater_guestPasswordHelper": "Пароль для доступа только для чтения", + "repeater_radioSettings": "Настройки радио", + "repeater_frequencyMhz": "Частота (МГц)", + "repeater_frequencyHelper": "300–2500 МГц", + "repeater_txPower": "Мощность передачи", + "repeater_txPowerHelper": "1–30 дБм", + "repeater_bandwidth": "Полоса пропускания", + "repeater_spreadingFactor": "Коэффициент расширения", + "repeater_codingRate": "Коэффициент кодирования", + "repeater_locationSettings": "Настройки местоположения", + "repeater_latitude": "Широта", + "repeater_latitudeHelper": "Ð’ десятичных градусах (напр., 37.7749)", + "repeater_longitude": "Долгота", + "repeater_longitudeHelper": "Ð’ десятичных градусах (напр., -122.4194)", + "repeater_features": "Функции", + "repeater_packetForwarding": "Пересылка пакетов", + "repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты", + "repeater_guestAccess": "Гостевой доступ", + "repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения", + "repeater_privacyMode": "Режим конфиденциальности", + "repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях", + "repeater_advertisementSettings": "Настройки анонсирования", + "repeater_localAdvertInterval": "Интервал локальных анонсирований", + "repeater_localAdvertIntervalMinutes": "{minutes} минут", + "repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)", + "repeater_floodAdvertIntervalHours": "{hours} часов", + "repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований", + "repeater_dangerZone": "Опасная зона", + "repeater_rebootRepeater": "Перезагрузить репитер", + "repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера", + "repeater_rebootRepeaterConfirm": "Ð’Ñ‹ уверены, что хотите перезагрузить этот репитер?", + "repeater_regenerateIdentityKey": "Пересоздать ключ идентификации", + "repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей", + "repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?", + "repeater_eraseFileSystem": "Стереть файловую систему", + "repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера", + "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!", + "repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.", + "repeater_commandSent": "Команда отправлена: {command}", + "repeater_errorSendingCommand": "Ошибка отправки команды: {error}", + "repeater_confirm": "Подтвердить", + "repeater_settingsSaved": "Настройки успешно сохранены", + "repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}", + "repeater_refreshBasicSettings": "Обновить основные настройки", + "repeater_refreshRadioSettings": "Обновить настройки радио", + "repeater_refreshTxPower": "Обновить мощность передачи", + "repeater_refreshLocationSettings": "Обновить настройки местоположения", + "repeater_refreshPacketForwarding": "Обновить пересылку пакетов", + "repeater_refreshGuestAccess": "Обновить гостевой доступ", + "repeater_refreshPrivacyMode": "Обновить режим конфиденциальности", + "repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований", + "repeater_refreshed": "{label} обновлён", + "repeater_errorRefreshing": "Ошибка обновления {label}", + "repeater_cliTitle": "CLI репитера", + "repeater_debugNextCommand": "Отладка следующей команды", + "repeater_commandHelp": "Справка по командам", + "repeater_clearHistory": "Очистить историю", + "repeater_noCommandsSent": "Команды ещё не отправлялись", + "repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды", + "repeater_enterCommandHint": "Введите команду...", + "repeater_previousCommand": "Предыдущая команда", + "repeater_nextCommand": "Следующая команда", + "repeater_enterCommandFirst": "Сначала введите команду", + "repeater_cliCommandFrameTitle": "Фрейм CLI-команды", + "repeater_cliCommandError": "Ошибка: {error}", + "repeater_cliQuickGetName": "Получить имя", + "repeater_cliQuickGetRadio": "Получить радио", + "repeater_cliQuickGetTx": "Получить TX", + "repeater_cliQuickNeighbors": "Соседи", + "repeater_cliQuickVersion": "Версия", + "repeater_cliQuickAdvertise": "Анонсировать", + "repeater_cliQuickClock": "Время", + "repeater_cliHelpAdvert": "Отправляет пакет анонсирования", + "repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)", + "repeater_cliHelpClock": "Показывает текущее время по часам устройства.", + "repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.", + "repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.", + "repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.", + "repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.", + "repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)", + "repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.", + "repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)", + "repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)", + "repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.", + "repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.", + "repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».", + "repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.", + "repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.", + "repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)", + "repeater_cliHelpSetName": "Устанавливает имя в оповещениях.", + "repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.", + "repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.", + "repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).", + "repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.", + "repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.", + "repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.", + "repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.", + "repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.", + "repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.", + "repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).", + "repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).", + "repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)", + "repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow", + "repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.", + "repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.", + "repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.", + "repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4", + "repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.", + "repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.", + "repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.", + "repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»", + "repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.", + "repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)", + "repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)", + "repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)", + "repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)", + "repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.", + "repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.", + "repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.", + "repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.", + "repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.", + "repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.", + "repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек", + "repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.", + "repeater_commandsListTitle": "Список команд", + "repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».", + "repeater_general": "Общие", + "repeater_settingsCategory": "Настройки", + "repeater_bridge": "Мост", + "repeater_logging": "Журналирование", + "repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)", + "repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)", + "repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.", + "repeater_gpsManagement": "Управление GPS", + "repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.", + "telemetry_receivedData": "Полученные телеметрические данные", + "telemetry_requestTimeout": "Время ожидания телеметрии истекло.", + "telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}", + "telemetry_noData": "Данные телеметрии недоступны.", + "telemetry_channelTitle": "Канал {channel}", + "telemetry_batteryLabel": "Батарея", + "telemetry_voltageLabel": "Напряжение", + "telemetry_mcuTemperatureLabel": "Температура МК", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Ток", + "telemetry_batteryValue": "{percent}% / {volts}Ð’", + "telemetry_voltageValue": "{volts}Ð’", + "telemetry_currentValue": "{amps}А", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "neighbors_receivedData": "Полученные данные о соседях", + "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", + "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", + "neighbors_repeatersNeighbors": "Соседи репитеров", + "neighbors_noData": "Данные о соседях недоступны.", + "neighbors_unknownContact": "Неизвестный {pubkey}", + "neighbors_heardA ago": "Слышали: {time} назад", + "channelPath_title": "Путь пакета", + "channelPath_viewMap": "Посмотреть на карте", + "channelPath_otherObservedPaths": "Другие наблюдаемые пути", + "channelPath_repeaterHops": "Хопы через репитеры", + "channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.", + "channelPath_messageDetails": "Детали сообщения", + "channelPath_senderLabel": "Отправитель", + "channelPath_timeLabel": "Время", + "channelPath_repeatsLabel": "Повторы", + "channelPath_pathLabel": "Путь {index}", + "channelPath_observedLabel": "Наблюдаемый", + "channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}", + "channelPath_noLocationData": "Нет данных о местоположении", "channelPath_timeWithDate": "{day}/{month} {time}", "channelPath_timeOnly": "{time}", - "channelPath_unknownPath": "Неизвестный", - "channelPath_floodPath": "Рассылка", - "channelPath_directPath": "Прямой", - "channelPath_observedZeroOf": "0 из {total} хопов", - "channelPath_observedSomeOf": "{observed} из {total} хопов", - "channelPath_mapTitle": "Карта пути", - "channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.", - "channelPath_primaryPath": "Путь {index} (Основной)", - "channelPath_pathLabelTitle": "Путь", - "channelPath_observedPathHeader": "Наблюдаемый путь", - "channelPath_selectedPathLabel": "{label} • {prefixes}", - "channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.", - "channelPath_unknownRepeater": "Неизвестный репитер", - "community_title": "Сообщество", - "community_create": "Создать сообщество", - "community_createDesc": "Создать новое сообщество и поделиться через QR-код.", - "community_join": "Присоединиться", - "community_joinTitle": "Присоединиться к сообществу", - "community_joinConfirmation": "Вы хотите присоединиться к сообществу \"{name}\"?", - "community_scanQr": "Сканировать QR-код сообщества", - "community_scanInstructions": "Наведите камеру на QR-код сообщества", - "community_showQr": "Показать QR-код", - "community_publicChannel": "Публичный канал сообщества", - "community_hashtagChannel": "Хэштег-канал сообщества", - "community_name": "Имя сообщества", - "community_enterName": "Введите имя сообщества", - "community_created": "Сообщество \"{name}\" создано", - "community_joined": "Присоединились к сообществу \"{name}\"", - "community_qrTitle": "Поделиться сообществом", - "community_qrInstructions": "Отсканируйте этот QR-код, чтобы присоединиться к \"{name}\"", - "community_hashtagPrivacyHint": "Хэштег-каналы сообщества доступны только его участникам", - "community_invalidQrCode": "Недопустимый QR-код сообщества", - "community_alreadyMember": "Уже участник", - "community_alreadyMemberMessage": "Вы уже участник сообщества \"{name}\".", - "community_addPublicChannel": "Добавить публичный канал сообщества", - "community_addPublicChannelHint": "Автоматически добавить публичный канал для этого сообщества", - "community_noCommunities": "Вы ещё не присоединились ни к одному сообществу", - "community_scanOrCreate": "Отсканируйте QR-код или создайте сообщество, чтобы начать", - "community_manageCommunities": "Управление сообществами", - "community_delete": "Покинуть сообщество", - "community_deleteConfirm": "Покинуть \"{name}\"?", - "community_deleteChannelsWarning": "Это также удалит {count} канал(ов) и их сообщения.", - "community_deleted": "Покинули сообщество \"{name}\"", - "community_regenerateSecret": "Пересоздать секрет", - "community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.", - "community_regenerate": "Пересоздать", - "community_secretRegenerated": "Секрет пересоздан для \"{name}\"", - "community_updateSecret": "Обновить секрет", - "community_secretUpdated": "Секрет обновлён для \"{name}\"", - "community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"", - "community_addHashtagChannel": "Добавить хэштег-канал сообщества", - "community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества", - "community_selectCommunity": "Выбрать сообщество", - "community_regularHashtag": "Обычный хэштег", - "community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)", - "community_communityHashtag": "Хэштег сообщества", - "community_communityHashtagDesc": "Доступен только участникам сообщества", - "community_forCommunity": "Для {name}", - "listFilter_tooltip": "Фильтр и сортировка", - "listFilter_sortBy": "Сортировка по", - "listFilter_latestMessages": "Последние сообщения", - "listFilter_heardRecently": "Слышали недавно", - "listFilter_az": "По алфавиту", - "listFilter_filters": "Фильтры", - "listFilter_all": "Все", - "listFilter_users": "Пользователи", - "listFilter_repeaters": "Репитеры", - "listFilter_roomServers": "Серверы комнат", - "listFilter_unreadOnly": "Только непрочитанные", - "listFilter_newGroup": "Новая группа", + "channelPath_unknownPath": "Неизвестный", + "channelPath_floodPath": "Рассылка", + "channelPath_directPath": "Прямой", + "channelPath_observedZeroOf": "0 из {total} хопов", + "channelPath_observedSomeOf": "{observed} из {total} хопов", + "channelPath_mapTitle": "Карта пути", + "channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.", + "channelPath_primaryPath": "Путь {index} (Основной)", + "channelPath_pathLabelTitle": "Путь", + "channelPath_observedPathHeader": "Наблюдаемый путь", + "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.", + "channelPath_unknownRepeater": "Неизвестный репитер", + "community_title": "Сообщество", + "community_create": "Создать сообщество", + "community_createDesc": "Создать новое сообщество и поделиться через QR-код.", + "community_join": "Присоединиться", + "community_joinTitle": "Присоединиться к сообществу", + "community_joinConfirmation": "Ð’Ñ‹ хотите присоединиться к сообществу \"{name}\"?", + "community_scanQr": "Сканировать QR-код сообщества", + "community_scanInstructions": "Наведите камеру на QR-код сообщества", + "community_showQr": "Показать QR-код", + "community_publicChannel": "Публичный канал сообщества", + "community_hashtagChannel": "Хэштег-канал сообщества", + "community_name": "Имя сообщества", + "community_enterName": "Введите имя сообщества", + "community_created": "Сообщество \"{name}\" создано", + "community_joined": "Присоединились к сообществу \"{name}\"", + "community_qrTitle": "Поделиться сообществом", + "community_qrInstructions": "Отсканируйте этот QR-код, чтобы присоединиться к \"{name}\"", + "community_hashtagPrivacyHint": "Хэштег-каналы сообщества доступны только его участникам", + "community_invalidQrCode": "Недопустимый QR-код сообщества", + "community_alreadyMember": "Уже участник", + "community_alreadyMemberMessage": "Ð’Ñ‹ уже участник сообщества \"{name}\".", + "community_addPublicChannel": "Добавить публичный канал сообщества", + "community_addPublicChannelHint": "Автоматически добавить публичный канал для этого сообщества", + "community_noCommunities": "Ð’Ñ‹ ещё не присоединились ни к одному сообществу", + "community_scanOrCreate": "Отсканируйте QR-код или создайте сообщество, чтобы начать", + "community_manageCommunities": "Управление сообществами", + "community_delete": "Покинуть сообщество", + "community_deleteConfirm": "Покинуть \"{name}\"?", + "community_deleteChannelsWarning": "Это также удалит {count} канал(ов) и их сообщения.", + "community_deleted": "Покинули сообщество \"{name}\"", + "community_regenerateSecret": "Пересоздать секрет", + "community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.", + "community_regenerate": "Пересоздать", + "community_secretRegenerated": "Секрет пересоздан для \"{name}\"", + "community_updateSecret": "Обновить секрет", + "community_secretUpdated": "Секрет обновлён для \"{name}\"", + "community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"", + "community_addHashtagChannel": "Добавить хэштег-канал сообщества", + "community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества", + "community_selectCommunity": "Выбрать сообщество", + "community_regularHashtag": "Обычный хэштег", + "community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)", + "community_communityHashtag": "Хэштег сообщества", + "community_communityHashtagDesc": "Доступен только участникам сообщества", + "community_forCommunity": "Для {name}", + "listFilter_tooltip": "Фильтр и сортировка", + "listFilter_sortBy": "Сортировка по", + "listFilter_latestMessages": "Последние сообщения", + "listFilter_heardRecently": "Слышали недавно", + "listFilter_az": "По алфавиту", + "listFilter_filters": "Фильтры", + "listFilter_all": "Все", + "listFilter_users": "Пользователи", + "listFilter_repeaters": "Репитеры", + "listFilter_roomServers": "Серверы комнат", + "listFilter_unreadOnly": "Только непрочитанные", + "listFilter_newGroup": "Новая группа", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -774,12 +774,12 @@ } } }, - "chat_open": "Открыть", - "chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}", - "chat_openLink": "Открыть ссылку?", - "chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?", - "neighbors_heardAgo": "Слушал(а): {time} назад", - "chat_invalidLink": "Неправильный формат ссылки", + "chat_open": "Открыть", + "chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}", + "chat_openLink": "Открыть ссылку?", + "chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?", + "neighbors_heardAgo": "Слушал(а): {time} назад", + "chat_invalidLink": "Неправильный формат ссылки", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -787,81 +787,81 @@ } } }, - "pathTrace_you": "Вы", - "pathTrace_failed": "Путь трассировки не выполнен.", - "pathTrace_notAvailable": "Трассировка пути недоступна.", - "pathTrace_refreshTooltip": "Обновить Path Trace", - "contacts_pathTrace": "Трассировка пути", - "contacts_ping": "Пинговать", - "contacts_repeaterPathTrace": "Отследить путь к ретранслятору", - "contacts_repeaterPing": "Пинговать повторитель", - "contacts_roomPathTrace": "Трассировка пути к серверу комнаты", - "contacts_roomPing": "Пинговать сервер комнаты", - "contacts_chatTraceRoute": "Трассировка маршрута", - "contacts_pathTraceTo": "Показать маршрут к {name}", - "contacts_contactImported": "Контакт был импортирован", - "contacts_contactImportFailed": "Контакт не удалось импортировать", - "contacts_invalidAdvertFormat": "Недействительные контактные данные", - "contacts_zeroHopAdvert": "Реклама Zero Hop", - "appSettings_languageUk": "Українська", - "appSettings_enableMessageTracing": "Включить трассировку сообщений", - "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", - "contacts_floodAdvert": "Рекламный поток", - "contacts_clipboardEmpty": "Буфер обмена пуст.", - "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", - "contacts_ShareContact": "Копировать контакт в буфер обмена", - "contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.", - "contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.", - "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", - "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", - "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", - "notification_activityTitle": "Активность MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}", - "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", - "notification_newTypeDiscovered": "Обнаружен новый {contactType}", - "notification_receivedNewMessage": "Получено новое сообщение", - "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", - "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", - "settings_gpxExportContacts": "Экспортировать спутников в GPX", - "settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС", - "settings_gpxExportError": "Произошла ошибка при экспорте.", - "settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат", - "settings_gpxExportChat": "Местоположения спутников", - "settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.", - "settings_gpxExportAll": "Экспортировать все контакты в GPX", - "settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.", - "settings_gpxExportAllContacts": "Все местоположения контактов", - "settings_gpxExportSuccess": "Успешно экспортирован файл GPX.", - "settings_gpxExportNoContacts": "Нет контактов для экспорта.", - "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", - "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!", - "map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.", - "map_removeLast": "Удалить последний", - "map_pathTraceCancelled": "Отмена трассировки пути", - "pathTrace_clearTooltip": "Очистить путь", - "map_runTrace": "Запустить трассировку пути", - "scanner_enableBluetooth": "Включите Bluetooth", - "scanner_bluetoothOff": "Bluetooth выключен", - "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", - "scanner_chromeRequired": "Требуется браузер Chrome", - "scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.", - "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", - "snrIndicator_lastSeen": "Последний раз видели", - "chat_ShowAllPaths": "Показать все пути", - "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", - "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", - "settings_clientRepeat": "Повторение \"вне сети\"", - "settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "Единицы", - "appSettings_unitsMetric": "Метрическая (м/км)", - "appSettings_unitsImperial": "Имперская (ft / mi)", - "map_lineOfSight": "Линия видимости", - "map_losScreenTitle": "Линия видимости", - "losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.", - "losRunFailed": "Проверка прямой видимости не удалась: {error}", + "pathTrace_you": "Ð’Ñ‹", + "pathTrace_failed": "Путь трассировки не выполнен.", + "pathTrace_notAvailable": "Трассировка пути недоступна.", + "pathTrace_refreshTooltip": "Обновить Path Trace", + "contacts_pathTrace": "Трассировка пути", + "contacts_ping": "Пинговать", + "contacts_repeaterPathTrace": "Отследить путь к ретранслятору", + "contacts_repeaterPing": "Пинговать повторитель", + "contacts_roomPathTrace": "Трассировка пути к серверу комнаты", + "contacts_roomPing": "Пинговать сервер комнаты", + "contacts_chatTraceRoute": "Трассировка маршрута", + "contacts_pathTraceTo": "Показать маршрут к {name}", + "contacts_contactImported": "Контакт был импортирован", + "contacts_contactImportFailed": "Контакт не удалось импортировать", + "contacts_invalidAdvertFormat": "Недействительные контактные данные", + "contacts_zeroHopAdvert": "Реклама Zero Hop", + "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Включить трассировку сообщений", + "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", + "contacts_floodAdvert": "Рекламный поток", + "contacts_clipboardEmpty": "Буфер обмена пуст.", + "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", + "contacts_ShareContact": "Копировать контакт в буфер обмена", + "contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.", + "contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.", + "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", + "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", + "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "notification_activityTitle": "Активность MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}", + "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", + "notification_newTypeDiscovered": "Обнаружен новый {contactType}", + "notification_receivedNewMessage": "Получено новое сообщение", + "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", + "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", + "settings_gpxExportContacts": "Экспортировать спутников в GPX", + "settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС", + "settings_gpxExportError": "Произошла ошибка при экспорте.", + "settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат", + "settings_gpxExportChat": "Местоположения спутников", + "settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.", + "settings_gpxExportAll": "Экспортировать все контакты в GPX", + "settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.", + "settings_gpxExportAllContacts": "Все местоположения контактов", + "settings_gpxExportSuccess": "Успешно экспортирован файл GPX.", + "settings_gpxExportNoContacts": "Нет контактов для экспорта.", + "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", + "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!", + "map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.", + "map_removeLast": "Удалить последний", + "map_pathTraceCancelled": "Отмена трассировки пути", + "pathTrace_clearTooltip": "Очистить путь", + "map_runTrace": "Запустить трассировку пути", + "scanner_enableBluetooth": "Включите Bluetooth", + "scanner_bluetoothOff": "Bluetooth выключен", + "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "scanner_chromeRequired": "Требуется браузер Chrome", + "scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.", + "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", + "snrIndicator_lastSeen": "Последний раз видели", + "chat_ShowAllPaths": "Показать все пути", + "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", + "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", + "settings_clientRepeat": "Повторение \"вне сети\"", + "settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Единицы", + "appSettings_unitsMetric": "Метрическая (м/км)", + "appSettings_unitsImperial": "Имперская (ft / mi)", + "map_lineOfSight": "Линия видимости", + "map_losScreenTitle": "Линия видимости", + "losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.", + "losRunFailed": "Проверка прямой видимости не удалась: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -869,13 +869,13 @@ } } }, - "losClearAllPoints": "Очистить все точки", - "losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.", - "losMenuTitle": "ЛОС Меню", - "losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.", - "losShowDisplayNodes": "Показать узлы отображения", - "losCustomPoints": "Пользовательские точки", - "losCustomPointLabel": "Пользовательский {index}", + "losClearAllPoints": "Очистить все точки", + "losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.", + "losMenuTitle": "ЛОС Меню", + "losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.", + "losShowDisplayNodes": "Показать узлы отображения", + "losCustomPoints": "Пользовательские точки", + "losCustomPointLabel": "Пользовательский {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -883,9 +883,9 @@ } } }, - "losPointA": "Точка А", - "losPointB": "Точка Б", - "losAntennaA": "Антенна А: {value} {unit}", + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антенна А: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -896,7 +896,7 @@ } } }, - "losAntennaB": "Антенна Б: {value} {unit}", + "losAntennaB": "Антенна Б: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -907,9 +907,9 @@ } } }, - "losRun": "Запустить ЛОС", - "losNoElevationData": "Нет данных о высоте", - "losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}", + "losRun": "Запустить ЛОС", + "losNoElevationData": "Нет данных о высоте", + "losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -926,7 +926,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -943,9 +943,9 @@ } } }, - "losStatusChecking": "ЛОС: проверяю...", - "losStatusNoData": "ЛОС: нет данных", - "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.", + "losStatusChecking": "ЛОС: проверяю...", + "losStatusNoData": "ЛОС: нет данных", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.", "@losStatusSummary": { "placeholders": { "clear": { @@ -962,20 +962,20 @@ } } }, - "losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.", - "losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.", - "losRenameCustomPoint": "Переименовать пользовательскую точку", - "losPointName": "Имя точки", - "losShowPanelTooltip": "Показать панель LOS", - "losHidePanelTooltip": "Скрыть панель LOS", - "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Радиогоризонт", - "losLegendLosBeam": "Линия прямой видимости", - "losLegendTerrain": "Рельеф", - "losFrequencyLabel": "Частота", - "losFrequencyInfoTooltip": "Просмотреть детали расчёта", - "losFrequencyDialogTitle": "Расчёт радиогоризонта", - "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", + "losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.", + "losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.", + "losRenameCustomPoint": "Переименовать пользовательскую точку", + "losPointName": "Имя точки", + "losShowPanelTooltip": "Показать панель LOS", + "losHidePanelTooltip": "Скрыть панель LOS", + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиогоризонт", + "losLegendLosBeam": "Линия прямой видимости", + "losLegendTerrain": "Рельеф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Просмотреть детали расчёта", + "losFrequencyDialogTitle": "Расчёт радиогоризонта", + "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -993,9 +993,9 @@ } } }, - "listFilter_addToFavorites": "Добавить в избранное", - "listFilter_favorites": "Избранное", - "listFilter_removeFromFavorites": "Удалить из избранного", + "listFilter_addToFavorites": "Добавить в избранное", + "listFilter_favorites": "Избранное", + "listFilter_removeFromFavorites": "Удалить из избранного", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1036,19 +1036,17 @@ } } }, - "contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...", - "contacts_searchContactsNoNumber": "Поиск контактов...", - "contacts_unread": "Непрочитанное", - "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", - "contacts_searchFavorites": "Поиск {number}{str} избранного...", - "contacts_searchUsers": "Поиск {number}{str} пользователей...", - "connectionChoiceSubtitle": "Выберите, каким способом вы хотите получить свой устройство MeshCore.", - "connectionChoiceTitle": "Выберите способ подключения", + "contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...", + "contacts_searchContactsNoNumber": "Поиск контактов...", + "contacts_unread": "Непрочитанное", + "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", + "contacts_searchFavorites": "Поиск {number}{str} избранного...", + "contacts_searchUsers": "Поиск {number}{str} пользователей...", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.", - "usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.", - "usbScreenStatus": "Выберите USB-устройство", - "usbScreenTitle": "Подключение через USB", - "usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список." + "usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.", + "usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.", + "usbScreenStatus": "Выберите USB-устройство", + "usbScreenTitle": "Подключение через USB", + "usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 46fe7e7..183f2fc 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", +{ + "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániÅ¥", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -10,32 +10,32 @@ "@@locale": "sk", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", - "nav_channels": "Kanály", + "nav_channels": "Kanály", "nav_map": "Mapa", - "common_cancel": "Zrušiť", - "common_connect": "Pripojiť", - "common_unknownDevice": "Neznáme zariadenie", - "common_save": "Uložiť", - "common_delete": "Odstrániť", - "common_close": "Zavrieť", - "common_edit": "Upraviť", - "common_add": "Pridať", + "common_cancel": "ZruÅ¡iÅ¥", + "common_connect": "PripojiÅ¥", + "common_unknownDevice": "Neznáme zariadenie", + "common_save": "UložiÅ¥", + "common_delete": "OdstrániÅ¥", + "common_close": "ZavrieÅ¥", + "common_edit": "UpraviÅ¥", + "common_add": "PridaÅ¥", "common_settings": "Nastavenia", - "common_disconnect": "Odpojiť", - "common_connected": "Pripojené", - "common_disconnected": "Odpojené", - "common_create": "Vytvoriť", - "common_continue": "Pokračovať", - "common_share": "Zdieľať", - "common_copy": "Kopírovať", - "common_retry": "Pokusť znova", - "common_hide": "Skryť", - "common_remove": "Odstrániť", + "common_disconnect": "OdpojiÅ¥", + "common_connected": "Pripojené", + "common_disconnected": "Odpojené", + "common_create": "VytvoriÅ¥", + "common_continue": "PokračovaÅ¥", + "common_share": "ZdieľaÅ¥", + "common_copy": "KopírovaÅ¥", + "common_retry": "PokusÅ¥ znova", + "common_hide": "SkryÅ¥", + "common_remove": "OdstrániÅ¥", "common_enable": "Povolit", - "common_disable": "Zakázať", - "common_reboot": "Restartovať", - "common_loading": "Načítavanie...", - "common_notAvailable": "—", + "common_disable": "ZakázaÅ¥", + "common_reboot": "RestartovaÅ¥", + "common_loading": "Načítavanie...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Skrívania zariadení...", + "scanner_scanning": "Skrívania zariadení...", "scanner_connecting": "Pripojujem sa...", "scanner_disconnecting": "Odpojuje sa...", - "scanner_notConnected": "Nezriadené", - "scanner_connectedTo": "Pripojené k {deviceName}", + "scanner_notConnected": "Nezriadené", + "scanner_connectedTo": "Pripojené k {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,8 +65,8 @@ } } }, - "scanner_searchingDevices": "Hľadám zariadenia MeshCore...", - "scanner_tapToScan": "Stlač skenovanie na nájdenie zariadení MeshCore.", + "scanner_searchingDevices": "Hľadám zariadenia MeshCore...", + "scanner_tapToScan": "Stlač skenovanie na nájdenie zariadení MeshCore.", "scanner_connectionFailed": "Pripojenie zlyhalo: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -76,51 +76,51 @@ } }, "scanner_stop": "Zastavte", - "scanner_scan": "Skončiť", - "device_quickSwitch": "Rýchle prepínač", + "scanner_scan": "SkončiÅ¥", + "device_quickSwitch": "Rýchle prepínač", "device_meshcore": "MeshCore", "settings_title": "Nastavenia", - "settings_deviceInfo": "Informácie o zariadení", - "settings_appSettings": "Nastavenia aplikácie", - "settings_appSettingsSubtitle": "Upozornenia, správy a nastavenia mapy", + "settings_deviceInfo": "Informácie o zariadení", + "settings_appSettings": "Nastavenia aplikácie", + "settings_appSettingsSubtitle": "Upozornenia, správy a nastavenia mapy", "settings_nodeSettings": "Nastavenia uzla", - "settings_nodeName": "Názov uzla", - "settings_nodeNameNotSet": "Nezriadené", - "settings_nodeNameHint": "Zadajte názov uzla", - "settings_nodeNameUpdated": "Meno aktualizované", - "settings_radioSettings": "Nastavenia rádia", - "settings_radioSettingsSubtitle": "Frekvencia, výkon, rozptylovací faktor", - "settings_radioSettingsUpdated": "Nastavenia rádia aktualizované", + "settings_nodeName": "Názov uzla", + "settings_nodeNameNotSet": "Nezriadené", + "settings_nodeNameHint": "Zadajte názov uzla", + "settings_nodeNameUpdated": "Meno aktualizované", + "settings_radioSettings": "Nastavenia rádia", + "settings_radioSettingsSubtitle": "Frekvencia, výkon, rozptylovací faktor", + "settings_radioSettingsUpdated": "Nastavenia rádia aktualizované", "settings_location": "Lokalita", - "settings_locationSubtitle": "GPS súradnice", - "settings_locationUpdated": "Lokalita aktualizovaná", - "settings_locationBothRequired": "Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.", - "settings_locationInvalid": "Neplatná šírka alebo dĺžka.", - "settings_latitude": "Súradnica", - "settings_longitude": "Dĺžka", - "settings_privacyMode": "Režim ochrany súkromia", - "settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách", - "settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.", - "settings_privacyModeEnabled": "Ochranný režim je povolený.", - "settings_privacyModeDisabled": "Ochranný režim je vypnutý", - "settings_actions": "Možné akcie", - "settings_sendAdvertisement": "Odoslať reklamu", - "settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.", - "settings_advertisementSent": "Reklama odeslaná", - "settings_syncTime": "Čas synchronizácie", - "settings_syncTimeSubtitle": "Nastaviť hodiny zariadenia na čas telefónu", - "settings_timeSynchronized": "Čas synchronizovaný", - "settings_refreshContacts": "Načítať Kontakty", - "settings_refreshContactsSubtitle": "Načítať zoznam kontaktov z zariadenia", - "settings_rebootDevice": "Restartovať zariadenie", + "settings_locationSubtitle": "GPS súradnice", + "settings_locationUpdated": "Lokalita aktualizovaná", + "settings_locationBothRequired": "Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.", + "settings_locationInvalid": "Neplatná šírka alebo dĺžka.", + "settings_latitude": "Súradnica", + "settings_longitude": "Dĺžka", + "settings_privacyMode": "Režim ochrany súkromia", + "settings_privacyModeSubtitle": "SkryÅ¥ meno/poloha v reklamách", + "settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaÅ¡e meno a polohu v reklamách.", + "settings_privacyModeEnabled": "Ochranný režim je povolený.", + "settings_privacyModeDisabled": "Ochranný režim je vypnutý", + "settings_actions": "Možné akcie", + "settings_sendAdvertisement": "OdoslaÅ¥ reklamu", + "settings_sendAdvertisementSubtitle": "Momentálne priezornejÅ¡ie.", + "settings_advertisementSent": "Reklama odeslaná", + "settings_syncTime": "ÄŒas synchronizácie", + "settings_syncTimeSubtitle": "NastaviÅ¥ hodiny zariadenia na čas telefónu", + "settings_timeSynchronized": "ÄŒas synchronizovaný", + "settings_refreshContacts": "NačítaÅ¥ Kontakty", + "settings_refreshContactsSubtitle": "NačítaÅ¥ zoznam kontaktov z zariadenia", + "settings_rebootDevice": "RestartovaÅ¥ zariadenie", "settings_rebootDeviceSubtitle": "Restartujte zariadenie MeshCore.", - "settings_rebootDeviceConfirm": "Ste si istý, že chcete zariadenie reštartovať? Budete odpojení.", + "settings_rebootDeviceConfirm": "Ste si istý, že chcete zariadenie reÅ¡tartovaÅ¥? Budete odpojení.", "settings_debug": "Ladenie", "settings_bleDebugLog": "Log BLE Debug", - "settings_bleDebugLogSubtitle": "Príkazy BLE, odpovede a surové dáta", - "settings_appDebugLog": "Záznam ladenia aplikácie", - "settings_appDebugLogSubtitle": "Správy z ladenia aplikácie", - "settings_about": "O nás", + "settings_bleDebugLogSubtitle": "Príkazy BLE, odpovede a surové dáta", + "settings_appDebugLog": "Záznam ladenia aplikácie", + "settings_appDebugLogSubtitle": "Správy z ladenia aplikácie", + "settings_about": "O nás", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "MeshCore Open Source Projekt 2024", - "settings_aboutDescription": "Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.", + "settings_aboutDescription": "Otvorený zdrojový Flutter klient pre MeshCore LoRa sieÅ¥ové zariadenia.", "settings_infoName": "Meno", "settings_infoId": "ID", "settings_infoStatus": "Status", - "settings_infoBattery": "Batéria", - "settings_infoPublicKey": "Verejný kľúč", - "settings_infoContactsCount": "Počet kontaktov", - "settings_infoChannelCount": "Počet kanálov", + "settings_infoBattery": "Batéria", + "settings_infoPublicKey": "Verejný kľúč", + "settings_infoContactsCount": "Počet kontaktov", + "settings_infoChannelCount": "Počet kanálov", "settings_presets": "Prednastavenia", "settings_frequency": "Frekvencia (MHz)", - "settings_frequencyHelper": "300,0 – 2500,0", - "settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)", - "settings_bandwidth": "Šírka pásma", - "settings_spreadingFactor": "Rozptýľovací faktor", - "settings_codingRate": "Cenový kurz pre programovanie", - "settings_txPower": "TX Výkon (dBm)", + "settings_frequencyHelper": "300,0 – 2500,0", + "settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)", + "settings_bandwidth": "Šírka pásma", + "settings_spreadingFactor": "Rozptýľovací faktor", + "settings_codingRate": "Cenový kurz pre programovanie", + "settings_txPower": "TX Výkon (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)", + "settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)", "settings_error": "Chyba: {message}", "@settings_error": { "placeholders": { @@ -156,50 +156,50 @@ } } }, - "appSettings_title": "Nastavenia aplikácie", - "appSettings_appearance": "Vzhľad", - "appSettings_theme": "Téma", - "appSettings_themeSystem": "Predvolený systém", + "appSettings_title": "Nastavenia aplikácie", + "appSettings_appearance": "Vzhľad", + "appSettings_theme": "Téma", + "appSettings_themeSystem": "Predvolený systém", "appSettings_themeLight": "Svetlo", - "appSettings_themeDark": "Tmavé", + "appSettings_themeDark": "Tmavé", "appSettings_language": "Jazyk", - "appSettings_languageSystem": "Predvolený systém", + "appSettings_languageSystem": "Predvolený systém", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Upozornenia", - "appSettings_enableNotifications": "Povolte Notifikácie", - "appSettings_enableNotificationsSubtitle": "Zísť o upozornenia na správy a inzeráty", - "appSettings_notificationPermissionDenied": "Odmietená povolenie notifikácií", - "appSettings_notificationsEnabled": "Upozornenia povolené", - "appSettings_notificationsDisabled": "Upozornenia sú vypnuté", - "appSettings_messageNotifications": "Správy od upozornení", - "appSettings_messageNotificationsSubtitle": "Zobraziť upozornenie pri prijímaní nových správ", - "appSettings_channelMessageNotifications": "Notifikácie z kanálov", - "appSettings_channelMessageNotificationsSubtitle": "Zobraziť upozornenie pri prijímaní správ z kanálu", + "appSettings_enableNotifications": "Povolte Notifikácie", + "appSettings_enableNotificationsSubtitle": "ZísÅ¥ o upozornenia na správy a inzeráty", + "appSettings_notificationPermissionDenied": "Odmietená povolenie notifikácií", + "appSettings_notificationsEnabled": "Upozornenia povolené", + "appSettings_notificationsDisabled": "Upozornenia sú vypnuté", + "appSettings_messageNotifications": "Správy od upozornení", + "appSettings_messageNotificationsSubtitle": "ZobraziÅ¥ upozornenie pri prijímaní nových správ", + "appSettings_channelMessageNotifications": "Notifikácie z kanálov", + "appSettings_channelMessageNotificationsSubtitle": "ZobraziÅ¥ upozornenie pri prijímaní správ z kanálu", "appSettings_advertisementNotifications": "Upozornenia na reklamy", - "appSettings_advertisementNotificationsSubtitle": "Zobraziť upozornenie, keď sa objavia nové uzly.", - "appSettings_messaging": "Správy", - "appSettings_clearPathOnMaxRetry": "Vyčisti cestu na Max Retry", - "appSettings_clearPathOnMaxRetrySubtitle": "Resetovať kontaktný priebeh po 5 neúspešných pokusoch o doručenie", - "appSettings_pathsWillBeCleared": "Cesty budú vymazané po 5 neúspešných pokusoch.", - "appSettings_pathsWillNotBeCleared": "Cesty sa automaticky nevymazávajú.", - "appSettings_autoRouteRotation": "Automatické prechodové trasy", - "appSettings_autoRouteRotationSubtitle": "Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.", - "appSettings_autoRouteRotationEnabled": "Automatické otáčanie trasy povolené", - "appSettings_autoRouteRotationDisabled": "Automatické prekladanie trás pozastavené", - "appSettings_battery": "Batéria", - "appSettings_batteryChemistry": "Chemická zloženie batérie", + "appSettings_advertisementNotificationsSubtitle": "ZobraziÅ¥ upozornenie, keď sa objavia nové uzly.", + "appSettings_messaging": "Správy", + "appSettings_clearPathOnMaxRetry": "Vyčisti cestu na Max Retry", + "appSettings_clearPathOnMaxRetrySubtitle": "ResetovaÅ¥ kontaktný priebeh po 5 neúspeÅ¡ných pokusoch o doručenie", + "appSettings_pathsWillBeCleared": "Cesty budú vymazané po 5 neúspeÅ¡ných pokusoch.", + "appSettings_pathsWillNotBeCleared": "Cesty sa automaticky nevymazávajú.", + "appSettings_autoRouteRotation": "Automatické prechodové trasy", + "appSettings_autoRouteRotationSubtitle": "Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.", + "appSettings_autoRouteRotationEnabled": "Automatické otáčanie trasy povolené", + "appSettings_autoRouteRotationDisabled": "Automatické prekladanie trás pozastavené", + "appSettings_battery": "Batéria", + "appSettings_batteryChemistry": "Chemická zloženie batérie", "appSettings_batteryChemistryPerDevice": "Nastavenie pre {deviceName}", "@appSettings_batteryChemistryPerDevice": { "placeholders": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Pripojte sa k zariadeniu na výber", + "appSettings_batteryChemistryConnectFirst": "Pripojte sa k zariadeniu na výber", "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Zobrazenie mapy", - "appSettings_showRepeaters": "Zobraziť opakovače", - "appSettings_showRepeatersSubtitle": "Zobraziť opakujúce sa uzly na mape", - "appSettings_showChatNodes": "Zobraziť uzly chatových správ", - "appSettings_showChatNodesSubtitle": "Zobraziť chatové uzly na mape", - "appSettings_showOtherNodes": "Zobraziť ďalšie uzly", - "appSettings_showOtherNodesSubtitle": "Zobraziť ostatné typy uzlov na mape", - "appSettings_timeFilter": "Filtrovacie Časové Obdoby", - "appSettings_timeFilterShowAll": "Zobraziť všetky uzly", - "appSettings_timeFilterShowLast": "Zobraziť uzly z posledných {hours} hodín", + "appSettings_showRepeaters": "ZobraziÅ¥ opakovače", + "appSettings_showRepeatersSubtitle": "ZobraziÅ¥ opakujúce sa uzly na mape", + "appSettings_showChatNodes": "ZobraziÅ¥ uzly chatových správ", + "appSettings_showChatNodesSubtitle": "ZobraziÅ¥ chatové uzly na mape", + "appSettings_showOtherNodes": "ZobraziÅ¥ ďalÅ¡ie uzly", + "appSettings_showOtherNodesSubtitle": "ZobraziÅ¥ ostatné typy uzlov na mape", + "appSettings_timeFilter": "Filtrovacie ÄŒasové Obdoby", + "appSettings_timeFilterShowAll": "ZobraziÅ¥ vÅ¡etky uzly", + "appSettings_timeFilterShowLast": "ZobraziÅ¥ uzly z posledných {hours} hodín", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -229,16 +229,16 @@ } } }, - "appSettings_mapTimeFilter": "Filtračný čas mapy", - "appSettings_showNodesDiscoveredWithin": "Zobraziť uzly objavené v:", - "appSettings_allTime": "Všetky časy", - "appSettings_lastHour": "Posledná hodina", - "appSettings_last6Hours": "Posledné 6 hodín", - "appSettings_last24Hours": "Posledných 24 hodín", - "appSettings_lastWeek": "Minul týždeň", - "appSettings_offlineMapCache": "Offline Mapa Pamäť", - "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť", - "appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})", + "appSettings_mapTimeFilter": "Filtračný čas mapy", + "appSettings_showNodesDiscoveredWithin": "ZobraziÅ¥ uzly objavené v:", + "appSettings_allTime": "VÅ¡etky časy", + "appSettings_lastHour": "Posledná hodina", + "appSettings_last6Hours": "Posledné 6 hodín", + "appSettings_last24Hours": "Posledných 24 hodín", + "appSettings_lastWeek": "Minul týždeň", + "appSettings_offlineMapCache": "Offline Mapa Pamäť", + "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasÅ¥", + "appSettings_areaSelectedZoom": "Vyberená oblasÅ¥ (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,18 +250,18 @@ } }, "appSettings_debugCard": "Ladenie", - "appSettings_appDebugLogging": "Záznamy ladenia aplikácie", - "appSettings_appDebugLoggingSubtitle": "Logovací správy aplikácie pre ladenie", - "appSettings_appDebugLoggingEnabled": "Aplikácia povolila ladenie protokolmi", - "appSettings_appDebugLoggingDisabled": "Zabudované ladenie aplikácie je vypnuté.", + "appSettings_appDebugLogging": "Záznamy ladenia aplikácie", + "appSettings_appDebugLoggingSubtitle": "Logovací správy aplikácie pre ladenie", + "appSettings_appDebugLoggingEnabled": "Aplikácia povolila ladenie protokolmi", + "appSettings_appDebugLoggingDisabled": "Zabudované ladenie aplikácie je vypnuté.", "contacts_title": "Kontakty", - "contacts_noContacts": "Zatiaľ žiadne kontakty.", - "contacts_contactsWillAppear": "Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.", - "contacts_searchContacts": "Vyhľadávajte kontakty...", - "contacts_noUnreadContacts": "Žiadne neprečítané kontakty", - "contacts_noContactsFound": "Neboli nájdených žiadnych kontaktov ani skupiny.", - "contacts_deleteContact": "Odstrániť kontakt", - "contacts_removeConfirm": "Odstrániť {contactName} z kontaktov?", + "contacts_noContacts": "Zatiaľ žiadne kontakty.", + "contacts_contactsWillAppear": "Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.", + "contacts_searchContacts": "Vyhľadávajte kontakty...", + "contacts_noUnreadContacts": "Žiadne neprečítané kontakty", + "contacts_noContactsFound": "Neboli nájdených žiadnych kontaktov ani skupiny.", + "contacts_deleteContact": "OdstrániÅ¥ kontakt", + "contacts_removeConfirm": "OdstrániÅ¥ {contactName} z kontaktov?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -269,12 +269,12 @@ } } }, - "contacts_manageRepeater": "Spravovať opakované zoznamy", - "contacts_roomLogin": "Prihlásenie do miestnosti", - "contacts_openChat": "Otvorené Chat", - "contacts_editGroup": "Upraviť skupinu", - "contacts_deleteGroup": "Vymažť skupinu", - "contacts_deleteGroupConfirm": "Odstrániť \"{groupName}\"?", + "contacts_manageRepeater": "SpravovaÅ¥ opakované zoznamy", + "contacts_roomLogin": "Prihlásenie do miestnosti", + "contacts_openChat": "Otvorené Chat", + "contacts_editGroup": "UpraviÅ¥ skupinu", + "contacts_deleteGroup": "Vymažť skupinu", + "contacts_deleteGroupConfirm": "OdstrániÅ¥ \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -282,10 +282,10 @@ } } }, - "contacts_newGroup": "Nová skupina", - "contacts_groupName": "Názov skupiny", - "contacts_groupNameRequired": "Skupina musí mať názov.", - "contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje", + "contacts_newGroup": "Nová skupina", + "contacts_groupName": "Názov skupiny", + "contacts_groupNameRequired": "Skupina musí maÅ¥ názov.", + "contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -293,11 +293,11 @@ } } }, - "contacts_filterContacts": "Filtrovať kontakty...", - "contacts_noContactsMatchFilter": "Žiadne kontakty neodídu vášmu filtru.", - "contacts_noMembers": "Žiadni členovia", - "contacts_lastSeenNow": "Posledné zreteľné zobrazenie teraz", - "contacts_lastSeenMinsAgo": "Posledné zobrazenie {minutes} min. dozadu", + "contacts_filterContacts": "FiltrovaÅ¥ kontakty...", + "contacts_noContactsMatchFilter": "Žiadne kontakty neodídu vášmu filtru.", + "contacts_noMembers": "Žiadni členovia", + "contacts_lastSeenNow": "Posledné zreteľné zobrazenie teraz", + "contacts_lastSeenMinsAgo": "Posledné zobrazenie {minutes} min. dozadu", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Zobral/Zabral poslednýkrát pred hodinou.", - "contacts_lastSeenHoursAgo": "Posledné zobrazenie {hours} hodín dozadu", + "contacts_lastSeenHourAgo": "Zobral/Zabral poslednýkrát pred hodinou.", + "contacts_lastSeenHoursAgo": "Posledné zobrazenie {hours} hodín dozadu", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Zobral/Zabral posledný raz pred 1 dňom.", - "contacts_lastSeenDaysAgo": "Posledné zobrazenie {days} dní dozadu", + "contacts_lastSeenDayAgo": "Zobral/Zabral posledný raz pred 1 dňom.", + "contacts_lastSeenDaysAgo": "Posledné zobrazenie {days} dní dozadu", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -323,12 +323,12 @@ } } }, - "channels_title": "Kanály", - "channels_noChannelsConfigured": "Neobsiahnuté žiadne kanály", - "channels_addPublicChannel": "Pridať verejný kanál", - "channels_searchChannels": "Vyhľadávajte kanály...", - "channels_noChannelsFound": "Neobsiahlo sa žiadnych kanálov.", - "channels_channelIndex": "Kanál {index}", + "channels_title": "Kanály", + "channels_noChannelsConfigured": "Neobsiahnuté žiadne kanály", + "channels_addPublicChannel": "PridaÅ¥ verejný kanál", + "channels_searchChannels": "Vyhľadávajte kanály...", + "channels_noChannelsFound": "Neobsiahlo sa žiadnych kanálov.", + "channels_channelIndex": "Kanál {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -336,16 +336,16 @@ } } }, - "channels_hashtagChannel": "Kanál s hashtagom", - "channels_public": "Veľké verejné", - "channels_private": "Osobné", - "channels_publicChannel": "Veľké verejne kanály", - "channels_privateChannel": "Osobné kanál", - "channels_editChannel": "Upraviť kanál", - "channels_muteChannel": "Stlmiť kanál", - "channels_unmuteChannel": "Zrušiť stlmenie kanála", - "channels_deleteChannel": "Odstrániť kanál", - "channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.", + "channels_hashtagChannel": "Kanál s hashtagom", + "channels_public": "Veľké verejné", + "channels_private": "Osobné", + "channels_publicChannel": "Veľké verejne kanály", + "channels_privateChannel": "Osobné kanál", + "channels_editChannel": "UpraviÅ¥ kanál", + "channels_muteChannel": "StlmiÅ¥ kanál", + "channels_unmuteChannel": "ZruÅ¡iÅ¥ stlmenie kanála", + "channels_deleteChannel": "OdstrániÅ¥ kanál", + "channels_deleteChannelConfirm": "OdstrániÅ¥ \"{name}\"? To sa nedá zruÅ¡iÅ¥.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Kanál \"{name}\" bol odstránený", + "channels_channelDeleted": "Kanál \"{name}\" bol odstránený", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Pridať kanál", - "channels_channelIndexLabel": "Index kanála", - "channels_channelName": "Názov kanálu", - "channels_usePublicChannel": "Použite verejný kanál", - "channels_standardPublicPsk": "Štandardný verejný PSK", - "channels_pskHex": "PSK (Šifrovacia kľúčik)", - "channels_generateRandomPsk": "Generovať náhodný PSK", - "channels_enterChannelName": "Prosím, zadajte názov kanála.", - "channels_pskMustBe32Hex": "PSK musí mať 32 hexadecimálových znakov.", - "channels_channelAdded": "Kanál \"{name}\" pridaný", + "channels_addChannel": "PridaÅ¥ kanál", + "channels_channelIndexLabel": "Index kanála", + "channels_channelName": "Názov kanálu", + "channels_usePublicChannel": "Použite verejný kanál", + "channels_standardPublicPsk": "Å tandardný verejný PSK", + "channels_pskHex": "PSK (Å ifrovacia kľúčik)", + "channels_generateRandomPsk": "GenerovaÅ¥ náhodný PSK", + "channels_enterChannelName": "Prosím, zadajte názov kanála.", + "channels_pskMustBe32Hex": "PSK musí maÅ¥ 32 hexadecimálových znakov.", + "channels_channelAdded": "Kanál \"{name}\" pridaný", "@channels_channelAdded": { "placeholders": { "name": { @@ -378,7 +378,7 @@ } } }, - "channels_editChannelTitle": "Upraviť kanál {index}", + "channels_editChannelTitle": "UpraviÅ¥ kanál {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -386,8 +386,8 @@ } } }, - "channels_smazCompression": "Odstránenie kompresie SMAZ", - "channels_channelUpdated": "Kanál \"{name}\" bol aktualizovaný", + "channels_smazCompression": "Odstránenie kompresie SMAZ", + "channels_channelUpdated": "Kanál \"{name}\" bol aktualizovaný", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,16 +395,16 @@ } } }, - "channels_publicChannelAdded": "Veľký kanál pridaný", - "channels_sortBy": "Triediť podľa", - "channels_sortManual": "Ručne", + "channels_publicChannelAdded": "Veľký kanál pridaný", + "channels_sortBy": "TriediÅ¥ podľa", + "channels_sortManual": "Ručne", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Posledné správy", - "channels_sortUnread": "Nezriadené", - "chat_noMessages": "Zatiaľ žiadne správy.", - "chat_sendMessageToStart": "Pošlite správu na začiatok", - "chat_originalMessageNotFound": "Neznámy pôvodný odkaz.", - "chat_replyingTo": "Odpovedám {name}", + "channels_sortLatestMessages": "Posledné správy", + "channels_sortUnread": "Nezriadené", + "chat_noMessages": "Zatiaľ žiadne správy.", + "chat_sendMessageToStart": "PoÅ¡lite správu na začiatok", + "chat_originalMessageNotFound": "Neznámy pôvodný odkaz.", + "chat_replyingTo": "Odpovedám {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "Odpovedať {name}", + "chat_replyTo": "OdpovedaÅ¥ {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,7 +421,7 @@ } }, "chat_location": "Lokalita", - "chat_sendMessageTo": "Pošli správu {contactName}", + "chat_sendMessageTo": "PoÅ¡li správu {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "Napište správu...", - "chat_messageTooLong": "Správa je príliš dlhá (max {maxBytes} bytov).", + "chat_typeMessage": "NapiÅ¡te správu...", + "chat_messageTooLong": "Správa je príliÅ¡ dlhá (max {maxBytes} bytov).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,10 +438,10 @@ } } }, - "chat_messageCopied": "Správa skopírovaná", - "chat_messageDeleted": "Posolstvo odstránené", + "chat_messageCopied": "Správa skopírovaná", + "chat_messageDeleted": "Posolstvo odstránené", "chat_retryingMessage": "Pokus o obnovenie", - "chat_retryCount": "Skúsiť {current}/{max}", + "chat_retryCount": "SkúsiÅ¥ {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -452,33 +452,33 @@ } } }, - "chat_sendGif": "Odoslať GIF", - "chat_reply": "Odpovedať", - "chat_addReaction": "Pridať Reakciu", + "chat_sendGif": "OdoslaÅ¥ GIF", + "chat_reply": "OdpovedaÅ¥", + "chat_addReaction": "PridaÅ¥ Reakciu", "chat_me": "Mne", "emojiCategorySmileys": "Emoji", - "emojiCategoryGestures": "Gestá", + "emojiCategoryGestures": "Gestá", "emojiCategoryHearts": "Srdcia", "emojiCategoryObjects": "Objekty", "gifPicker_title": "Vyberte GIF", - "gifPicker_searchHint": "Vyhľadávajte GIFy...", - "gifPicker_poweredBy": "Napájané spoločnosťou GIPHY", - "gifPicker_noGifsFound": "Neboli nájdené žiadne GIFy.", - "gifPicker_failedLoad": "Nepodarilo sa načítať GIFy", - "gifPicker_failedSearch": "Nepodarilo sa vyhľadať GIFy", - "gifPicker_noInternet": "Žiadna internetová konektivita", - "debugLog_appTitle": "Záznam ladenia aplikácie", + "gifPicker_searchHint": "Vyhľadávajte GIFy...", + "gifPicker_poweredBy": "Napájané spoločnosÅ¥ou GIPHY", + "gifPicker_noGifsFound": "Neboli nájdené žiadne GIFy.", + "gifPicker_failedLoad": "Nepodarilo sa načítaÅ¥ GIFy", + "gifPicker_failedSearch": "Nepodarilo sa vyhľadaÅ¥ GIFy", + "gifPicker_noInternet": "Žiadna internetová konektivita", + "debugLog_appTitle": "Záznam ladenia aplikácie", "debugLog_bleTitle": "Log BLE Debug", - "debugLog_copyLog": "Kopírovať záznam", - "debugLog_clearLog": "Vymažať záznam", - "debugLog_copied": "Záznam ladenia skopírovaný", - "debugLog_bleCopied": "Kopírovaný záznam z BLE.", - "debugLog_noEntries": "Zatiaľ neboli zaznamenané žiadne debug logy.", - "debugLog_enableInSettings": "Povolte ladicové logy v nastaveniach", - "debugLog_frames": "Rámce", + "debugLog_copyLog": "KopírovaÅ¥ záznam", + "debugLog_clearLog": "VymažaÅ¥ záznam", + "debugLog_copied": "Záznam ladenia skopírovaný", + "debugLog_bleCopied": "Kopírovaný záznam z BLE.", + "debugLog_noEntries": "Zatiaľ neboli zaznamenané žiadne debug logy.", + "debugLog_enableInSettings": "Povolte ladicové logy v nastaveniach", + "debugLog_frames": "Rámce", "debugLog_rawLogRx": "Raw Log-RX", - "debugLog_noBleActivity": "Zatiaľ žiadna aktivita BLE.", - "debugFrame_length": "Dĺžka rámca: {count} bajtov", + "debugLog_noBleActivity": "Zatiaľ žiadna aktivita BLE.", + "debugFrame_length": "Dĺžka rámca: {count} bajtov", "@debugFrame_length": { "placeholders": { "count": { @@ -486,7 +486,7 @@ } } }, - "debugFrame_command": "Prikáž: 0x{value}", + "debugFrame_command": "PrikázÌŒ: 0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -494,8 +494,8 @@ } } }, - "debugFrame_textMessageHeader": "Textová zvesť:", - "debugFrame_destinationPubKey": "- Cieľový PubKey: {pubKey}", + "debugFrame_textMessageHeader": "Textová zvesÅ¥:", + "debugFrame_destinationPubKey": "- Cieľový PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- Časové označenie: {timestamp}", + "debugFrame_timestamp": "- ÄŒasové označenie: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -511,7 +511,7 @@ } } }, - "debugFrame_flags": "- Žiadne vlajky: 0x{value}", + "debugFrame_flags": "- Žiadne vlajky: 0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -531,7 +531,7 @@ } }, "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Jednoduché", + "debugFrame_textTypePlain": "Jednoduché", "debugFrame_text": "- Text: \"{text}\"", "@debugFrame_text": { "placeholders": { @@ -541,14 +541,14 @@ } }, "debugFrame_hexDump": "Hex Dump:", - "chat_pathManagement": "Správa ciest", - "chat_routingMode": "Režim trasy", - "chat_autoUseSavedPath": "Použiť uloženú cestu", - "chat_forceFloodMode": "Zavrieť režim núdzového povodňového režimu", - "chat_recentAckPaths": "Nedávne cesty ACK (klepni na použitie):", - "chat_pathHistoryFull": "História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.", + "chat_pathManagement": "Správa ciest", + "chat_routingMode": "Režim trasy", + "chat_autoUseSavedPath": "PoužiÅ¥ uloženú cestu", + "chat_forceFloodMode": "ZavrieÅ¥ režim núdzového povodňového režimu", + "chat_recentAckPaths": "Nedávne cesty ACK (klepni na použitie):", + "chat_pathHistoryFull": "História ciest je plná. Odstráňte záznamy, aby ste mohli pridaÅ¥ nové.", "chat_hopSingular": "Skok", - "chat_hopPlural": "Skákať", + "chat_hopPlural": "SkákaÅ¥", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { @@ -557,20 +557,20 @@ } } }, - "chat_successes": "Úspechy", - "chat_removePath": "Odstrániť cestu", - "chat_noPathHistoryYet": "Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.", + "chat_successes": "Úspechy", + "chat_removePath": "OdstrániÅ¥ cestu", + "chat_noPathHistoryYet": "Zatiaľ žiadna história trás.\nPoÅ¡lite správu a objavte trasy.", "chat_pathActions": "Cesty:", - "chat_setCustomPath": "Nastaviť vlastnú cestu", - "chat_setCustomPathSubtitle": "Ručne zadajte trasu.", - "chat_clearPath": "Vyčistiš cestu", - "chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite", - "chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.", - "chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.", - "chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.", - "chat_fullPath": "Celá cesta", - "chat_pathDetailsNotAvailable": "Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.", - "chat_pathSetHops": "Cesta nastavená: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_setCustomPath": "NastaviÅ¥ vlastnú cestu", + "chat_setCustomPathSubtitle": "Ručne zadajte trasu.", + "chat_clearPath": "VyčistiÅ¡ cestu", + "chat_clearPathSubtitle": "Znovu nájsÅ¥ vynútene pri nasledujúcej poÅ¡lite", + "chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.", + "chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.", + "chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.", + "chat_fullPath": "Celá cesta", + "chat_pathDetailsNotAvailable": "Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslaÅ¥ správu na obnovenie.", + "chat_pathSetHops": "Cesta nastavená: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Uložené lokálne. Spojte sa na synchronizáciu.", - "chat_pathDeviceConfirmed": "Zariadenie potvrdené.", - "chat_pathDeviceNotConfirmed": "Zariadenie zatiaľ nebolo potvrdené.", - "chat_type": "Napište", + "chat_pathSavedLocally": "Uložené lokálne. Spojte sa na synchronizáciu.", + "chat_pathDeviceConfirmed": "Zariadenie potvrdené.", + "chat_pathDeviceNotConfirmed": "Zariadenie zatiaľ nebolo potvrdené.", + "chat_type": "NapiÅ¡te", "chat_path": "Cesta", - "chat_publicKey": "Verejný kľúč", - "chat_compressOutgoingMessages": "Komprimovať odoslané správy", - "chat_floodForced": "Povodňová (nutená)", - "chat_directForced": "Priame (donútené)", - "chat_hopsForced": "{count} skokov (nutené)", + "chat_publicKey": "Verejný kľúč", + "chat_compressOutgoingMessages": "KomprimovaÅ¥ odoslané správy", + "chat_floodForced": "Povodňová (nutená)", + "chat_directForced": "Priame (donútené)", + "chat_hopsForced": "{count} skokov (nutené)", "@chat_hopsForced": { "placeholders": { "count": { @@ -600,8 +600,8 @@ }, "chat_floodAuto": "Povod (automaticky)", "chat_direct": "Priamo", - "chat_poiShared": "Zdieľané body záujmu", - "chat_unread": "Nezriadené: {count}", + "chat_poiShared": "Zdieľané body záujmu", + "chat_unread": "Nezriadené: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Otvoriť odkaz?", - "chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?", - "chat_open": "Otvoriť", - "chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}", + "chat_openLink": "OtvoriÅ¥ odkaz?", + "chat_openLinkConfirmation": "Chcete otvoriÅ¥ tento odkaz v prehliadači?", + "chat_open": "OtvoriÅ¥", + "chat_couldNotOpenLink": "Nepodarilo sa otvoriÅ¥ odkaz: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,10 +620,10 @@ } } }, - "chat_invalidLink": "Neplatný formát odkazu", + "chat_invalidLink": "Neplatný formát odkazu", "map_title": "Mapa uzlov", - "map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe", - "map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.", + "map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe", + "map_nodesNeedGps": "Uholníky musia zdieľaÅ¥ svoje GPS súradnice, aby sa zobrazili na mape.", "map_nodesCount": "Uzly: {count}", "@map_nodesCount": { "placeholders": { @@ -632,7 +632,7 @@ } } }, - "map_pinsCount": "Krúžky: {count}", + "map_pinsCount": "Krúžky: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -645,22 +645,22 @@ "map_room": "Izba", "map_sensor": "Senzor", "map_pinDm": "Zabudka (DM)", - "map_pinPrivate": "Zabudka (Osobná)", - "map_pinPublic": "Zablokovať (verejne)", - "map_lastSeen": "Posledné zreteľné zobrazenie", - "map_disconnectConfirm": "Ste si istý/á, že chcete odpojiť od tohto zariadenia?", + "map_pinPrivate": "Zabudka (Osobná)", + "map_pinPublic": "ZablokovaÅ¥ (verejne)", + "map_lastSeen": "Posledné zreteľné zobrazenie", + "map_disconnectConfirm": "Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?", "map_from": "Od", "map_source": "Zdroj", - "map_flags": "Zástavy", - "map_shareMarkerHere": "Zdieľte značku tu", - "map_pinLabel": "Označka upozornenia", - "map_label": "Značka", - "map_pointOfInterest": "Bod záujmu", - "map_sendToContact": "Pošleť na kontakt", - "map_sendToChannel": "Poslať do kanálu", - "map_noChannelsAvailable": "Неexistujú žiadne kanály.", - "map_publicLocationShare": "Zdieľiť verejnú lokalitu", - "map_publicLocationShareConfirm": "Čoskoro budete zdieľať polohu v {channelLabel}. Tento kanál je verejný a môže ho vidieť každý s PSK.", + "map_flags": "Zástavy", + "map_shareMarkerHere": "Zdieľte značku tu", + "map_pinLabel": "Označka upozornenia", + "map_label": "Značka", + "map_pointOfInterest": "Bod záujmu", + "map_sendToContact": "PoÅ¡leÅ¥ na kontakt", + "map_sendToChannel": "PoslaÅ¥ do kanálu", + "map_noChannelsAvailable": "Неexistujú žiadne kanály.", + "map_publicLocationShare": "ZdieľiÅ¥ verejnú lokalitu", + "map_publicLocationShareConfirm": "ÄŒoskoro budete zdieľaÅ¥ polohu v {channelLabel}. Tento kanál je verejný a môže ho vidieÅ¥ každý s PSK.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Pripojte sa k zariadeniu na zdieľanie značiek", - "map_filterNodes": "Filtrovať uzly", + "map_connectToShareMarkers": "Pripojte sa k zariadeniu na zdieľanie značiek", + "map_filterNodes": "FiltrovaÅ¥ uzly", "map_nodeTypes": "Typy uzlov", - "map_chatNodes": "Chatové uzly", - "map_repeaters": "Opakovadlá", - "map_otherNodes": "Ostatné uzly", - "map_keyPrefix": "Päťciferné predpona", - "map_filterByKeyPrefix": "Filtrovať podľa predponového kľúča", - "map_publicKeyPrefix": "Prefix verejného kľúča", - "map_markers": "Označkovače", - "map_showSharedMarkers": "Zobraziť zdieľané značky", - "map_lastSeenTime": "Posledný čas sledovania", - "map_sharedPin": "Zdieľaný PIN", - "map_joinRoom": "Pripojiť miestnosť", - "map_manageRepeater": "Spravovať Opakovanie", - "mapCache_title": "Offline Mapa Pamäť", - "mapCache_selectAreaFirst": "Vyberte si oblasť na predprerúčenie.", - "mapCache_noTilesToDownload": "Žiadne dlaždice na stiahnutie pre toto zóna", - "mapCache_downloadTilesTitle": "Stiahnuť dlaždice", - "mapCache_downloadTilesPrompt": "Stiahnuť {count} dlaždíc na offline použitie?", + "map_chatNodes": "Chatové uzly", + "map_repeaters": "Opakovadlá", + "map_otherNodes": "Ostatné uzly", + "map_keyPrefix": "Päťciferné predpona", + "map_filterByKeyPrefix": "FiltrovaÅ¥ podľa predponového kľúča", + "map_publicKeyPrefix": "Prefix verejného kľúča", + "map_markers": "Označkovače", + "map_showSharedMarkers": "ZobraziÅ¥ zdieľané značky", + "map_lastSeenTime": "Posledný čas sledovania", + "map_sharedPin": "Zdieľaný PIN", + "map_joinRoom": "PripojiÅ¥ miestnosÅ¥", + "map_manageRepeater": "SpravovaÅ¥ Opakovanie", + "mapCache_title": "Offline Mapa Pamäť", + "mapCache_selectAreaFirst": "Vyberte si oblasÅ¥ na predprerúčenie.", + "mapCache_noTilesToDownload": "Žiadne dlaždice na stiahnutie pre toto zóna", + "mapCache_downloadTilesTitle": "StiahnuÅ¥ dlaždice", + "mapCache_downloadTilesPrompt": "StiahnuÅ¥ {count} dlaždíc na offline použitie?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,8 +695,8 @@ } } }, - "mapCache_downloadAction": "Stiahnuť", - "mapCache_cachedTiles": "Zabudené {count} dlaždíc", + "mapCache_downloadAction": "StiahnuÅ¥", + "mapCache_cachedTiles": "Zabudené {count} dlaždíc", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Uložené {downloaded} dlaždice ({failed} neúspešné)", + "mapCache_cachedTilesWithFailed": "Uložené {downloaded} dlaždice ({failed} neúspeÅ¡né)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Vymazať offline uloženie", - "mapCache_clearOfflineCachePrompt": "Odstrániť všetky uložené mapové dlaždice?", - "mapCache_offlineCacheCleared": "Offline polia vymazaná", - "mapCache_noAreaSelected": "Neoznačila sa žiadna oblasť", - "mapCache_cacheArea": "Obdĺžková oblasť", - "mapCache_useCurrentView": "Použite aktuálny zobrazenie", - "mapCache_zoomRange": "Rozsah zväčšenia", - "mapCache_estimatedTiles": "Odhadnuté dlaždice: {count}", + "mapCache_clearOfflineCacheTitle": "VymazaÅ¥ offline uloženie", + "mapCache_clearOfflineCachePrompt": "OdstrániÅ¥ vÅ¡etky uložené mapové dlaždice?", + "mapCache_offlineCacheCleared": "Offline polia vymazaná", + "mapCache_noAreaSelected": "Neoznačila sa žiadna oblasÅ¥", + "mapCache_cacheArea": "Obdĺžková oblasÅ¥", + "mapCache_useCurrentView": "Použite aktuálny zobrazenie", + "mapCache_zoomRange": "Rozsah zväčšenia", + "mapCache_estimatedTiles": "Odhadnuté dlaždice: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Stiahnuté {completed} / {total}", + "mapCache_downloadedTiles": "Stiahnuté {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "Stiahnuť dlaždice", - "mapCache_clearCacheButton": "Vyprázdniť Vädsť", - "mapCache_failedDownloads": "Neúspešné stiahnutia: {count}", + "mapCache_downloadTilesButton": "StiahnuÅ¥ dlaždice", + "mapCache_clearCacheButton": "VyprázdniÅ¥ VädsÅ¥", + "mapCache_failedDownloads": "NeúspeÅ¡né stiahnutia: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -768,7 +768,7 @@ } } }, - "time_justNow": "Príbeh", + "time_justNow": "Príbeh", "time_minutesAgo": "{minutes} min dozadu", "@time_minutesAgo": { "placeholders": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} dní dozadu", + "time_daysAgo": "{days} dní dozadu", "@time_daysAgo": { "placeholders": { "days": { @@ -795,31 +795,31 @@ }, "time_hour": "hodina", "time_hours": "hodiny", - "time_day": "deň", + "time_day": "deň", "time_days": "dni", - "time_week": "týždeň", - "time_weeks": "týždne", + "time_week": "týždeň", + "time_weeks": "týždne", "time_month": "mesiac", "time_months": "mesiace", - "time_minutes": "minúty", - "time_allTime": "Všetko Časom", - "dialog_disconnect": "Odpojiť", - "dialog_disconnectConfirm": "Ste si istý/á, že chcete odpojiť od tohto zariadenia?", - "login_repeaterLogin": "Opätovné prihlásenie", - "login_roomLogin": "Prihlásenie do miestnosti", + "time_minutes": "minúty", + "time_allTime": "VÅ¡etko ÄŒasom", + "dialog_disconnect": "OdpojiÅ¥", + "dialog_disconnectConfirm": "Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?", + "login_repeaterLogin": "Opätovné prihlásenie", + "login_roomLogin": "Prihlásenie do miestnosti", "login_password": "Heslo", "login_enterPassword": "Zadajte heslo", - "login_savePassword": "Uložiť heslo", - "login_savePasswordSubtitle": "Heslo bude bezpečne uložené na tomto zariadení.", - "login_repeaterDescription": "Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.", - "login_roomDescription": "Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.", - "login_routing": "Rútiace", - "login_routingMode": "Režim trasy", - "login_autoUseSavedPath": "Použiť uloženú cestu", - "login_forceFloodMode": "Zavrieť režim núdzového povodňového režimu", - "login_managePaths": "Spravovať Cesty", - "login_login": "Prihlásiť", - "login_attempt": "Skúšaj {current}/{max}", + "login_savePassword": "UložiÅ¥ heslo", + "login_savePasswordSubtitle": "Heslo bude bezpečne uložené na tomto zariadení.", + "login_repeaterDescription": "Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.", + "login_roomDescription": "Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.", + "login_routing": "Rútiace", + "login_routingMode": "Režim trasy", + "login_autoUseSavedPath": "PoužiÅ¥ uloženú cestu", + "login_forceFloodMode": "ZavrieÅ¥ režim núdzového povodňového režimu", + "login_managePaths": "SpravovaÅ¥ Cesty", + "login_login": "PrihlásiÅ¥", + "login_attempt": "Skúšaj {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Prihlásenie zlyhalo: {error}", + "login_failed": "Prihlásenie zlyhalo: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.", - "common_reload": "Načítať", - "common_clear": "Zmazať", - "path_currentPath": "Aktívna cesta: {path}", + "login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.", + "common_reload": "NačítaÅ¥", + "common_clear": "ZmazaÅ¥", + "path_currentPath": "Aktívna cesta: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Používa {count} {count, plural, =1{hop} other{hops}} cestu", + "path_usingHopsPath": "Používa {count} {count, plural, =1{hop} other{hops}} cestu", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Zadajte vlastný priebeh", - "path_currentPathLabel": "Aktuálny priebeh", - "path_hexPrefixInstructions": "Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.", - "path_hexPrefixExample": "A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)", - "path_labelHexPrefixes": "Cesty (hexové predpony)", - "path_helperMaxHops": "Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).", + "path_enterCustomPath": "Zadajte vlastný priebeh", + "path_currentPathLabel": "Aktuálny priebeh", + "path_hexPrefixInstructions": "Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.", + "path_hexPrefixExample": "A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)", + "path_labelHexPrefixes": "Cesty (hexové predpony)", + "path_helperMaxHops": "Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).", "path_selectFromContacts": "Vyberte sa z kontaktov:", - "path_noRepeatersFound": "Nenašli sa žiadne opakovače ani serverové miestnosti.", - "path_customPathsRequire": "Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášať správky.", - "path_invalidHexPrefixes": "Neplatné hexové predpony: {prefixes}", + "path_noRepeatersFound": "NenaÅ¡li sa žiadne opakovače ani serverové miestnosti.", + "path_customPathsRequire": "Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášaÅ¥ správky.", + "path_invalidHexPrefixes": "Neplatné hexové predpony: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Cesta je príliš dlhá. Umožnené je maximum 64 skokov.", - "path_setPath": "Nastaviť cestu", - "repeater_management": "Správa opakérov", - "repeater_managementTools": "Nástroje na správu", + "path_tooLong": "Cesta je príliÅ¡ dlhá. Umožnené je maximum 64 skokov.", + "path_setPath": "NastaviÅ¥ cestu", + "repeater_management": "Správa opakérov", + "repeater_managementTools": "Nástroje na správu", "repeater_status": "Status", - "repeater_statusSubtitle": "Zobraziť stav, štatistiky a susedov repeatera", + "repeater_statusSubtitle": "ZobraziÅ¥ stav, Å¡tatistiky a susedov repeatera", "repeater_telemetry": "Telemetria", - "repeater_telemetrySubtitle": "Zobraziť telemetriu senzorov a systémových štatistík", + "repeater_telemetrySubtitle": "ZobraziÅ¥ telemetriu senzorov a systémových Å¡tatistík", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Pošlite príkazy opakovaču", + "repeater_cliSubtitle": "PoÅ¡lite príkazy opakovaču", "repeater_settings": "Nastavenia", - "repeater_settingsSubtitle": "Konfigurujte parametre opakovača", - "repeater_statusTitle": "Status opakého zboru", - "repeater_routingMode": "Režim trasy", - "repeater_autoUseSavedPath": "Použiť uloženú cestu", - "repeater_forceFloodMode": "Zavrieť režim núdzového povodňového režimu", - "repeater_pathManagement": "Správa trás", - "repeater_refresh": "Obnoviť", - "repeater_statusRequestTimeout": "Požiadavka stavu zlyhala.", - "repeater_errorLoadingStatus": "Chyba pri načítaní stavu: {error}", + "repeater_settingsSubtitle": "Konfigurujte parametre opakovača", + "repeater_statusTitle": "Status opakého zboru", + "repeater_routingMode": "Režim trasy", + "repeater_autoUseSavedPath": "PoužiÅ¥ uloženú cestu", + "repeater_forceFloodMode": "ZavrieÅ¥ režim núdzového povodňového režimu", + "repeater_pathManagement": "Správa trás", + "repeater_refresh": "ObnoviÅ¥", + "repeater_statusRequestTimeout": "Požiadavka stavu zlyhala.", + "repeater_errorLoadingStatus": "Chyba pri načítaní stavu: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -901,23 +901,23 @@ } } }, - "repeater_systemInformation": "Informácie o systéme", - "repeater_battery": "Batéria", - "repeater_clockAtLogin": "Čas (při přihlášení)", - "repeater_uptime": "Dostupnosť", - "repeater_queueLength": "Dĺžka fronty", - "repeater_debugFlags": "Kontrolné značky", - "repeater_radioStatistics": "Rádio Štatistiky", - "repeater_lastRssi": "Posledná RSSI", - "repeater_lastSnr": "Posledný SNR", - "repeater_noiseFloor": "Hladina šumu", + "repeater_systemInformation": "Informácie o systéme", + "repeater_battery": "Batéria", + "repeater_clockAtLogin": "ÄŒas (pÅ™i pÅ™ihlášení)", + "repeater_uptime": "DostupnosÅ¥", + "repeater_queueLength": "Dĺžka fronty", + "repeater_debugFlags": "Kontrolné značky", + "repeater_radioStatistics": "Rádio Å tatistiky", + "repeater_lastRssi": "Posledná RSSI", + "repeater_lastSnr": "Posledný SNR", + "repeater_noiseFloor": "Hladina Å¡umu", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Statistiky balíka", - "repeater_sent": "Odoslané", - "repeater_received": "Prišlo", - "repeater_duplicates": "Duplikáty", - "repeater_daysHoursMinsSecs": "{days} dní {hours}h {minutes}m {seconds}s", + "repeater_packetStatistics": "Statistiky balíka", + "repeater_sent": "Odoslané", + "repeater_received": "PriÅ¡lo", + "repeater_duplicates": "Duplikáty", + "repeater_daysHoursMinsSecs": "{days} dní {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", + "repeater_packetTxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", + "repeater_packetRxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -981,37 +981,37 @@ } } }, - "repeater_settingsTitle": "Nastavenia Opakovača", - "repeater_basicSettings": "Základné nastavenia", - "repeater_repeaterName": "Opakovacia názov", - "repeater_repeaterNameHelper": "Zobrazenie názvu tohto opakovača", - "repeater_adminPassword": "Heslo administrátora", - "repeater_adminPasswordHelper": "Celý prístupový heslo", - "repeater_guestPassword": "Heslo hosťa", - "repeater_guestPasswordHelper": "Prístupový heslo iba na čítanie", - "repeater_radioSettings": "Nastavenia rádia", + "repeater_settingsTitle": "Nastavenia OpakovacÌŒa", + "repeater_basicSettings": "Základné nastavenia", + "repeater_repeaterName": "Opakovacia názov", + "repeater_repeaterNameHelper": "Zobrazenie názvu tohto opakovača", + "repeater_adminPassword": "Heslo administrátora", + "repeater_adminPasswordHelper": "Celý prístupový heslo", + "repeater_guestPassword": "Heslo hosÅ¥a", + "repeater_guestPasswordHelper": "Prístupový heslo iba na čítanie", + "repeater_radioSettings": "Nastavenia rádia", "repeater_frequencyMhz": "Frekvencia (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Šírka pásma", - "repeater_spreadingFactor": "Šírenie faktoru", - "repeater_codingRate": "Rýchlosť kódovania", + "repeater_bandwidth": "Šírka pásma", + "repeater_spreadingFactor": "Šírenie faktoru", + "repeater_codingRate": "RýchlosÅ¥ kódovania", "repeater_locationSettings": "Nastavenia polohy", - "repeater_latitude": "Súradnica", - "repeater_latitudeHelper": "Desatinné zložky (napr. 37.7749)", - "repeater_longitude": "Dĺžka", - "repeater_longitudeHelper": "Desatinné zložky (napr. -122.4194)", + "repeater_latitude": "Súradnica", + "repeater_latitudeHelper": "Desatinné zložky (napr. 37.7749)", + "repeater_longitude": "Dĺžka", + "repeater_longitudeHelper": "Desatinné zložky (napr. -122.4194)", "repeater_features": "Funkcie", "repeater_packetForwarding": "Riadenie prienikov", - "repeater_packetForwardingSubtitle": "Povolte opakovač na smerovanie paketov.", - "repeater_guestAccess": "Prístup pre hostí", - "repeater_guestAccessSubtitle": "Umožniť prístup hosta iba na čítanie.", - "repeater_privacyMode": "Režim ochrany súkromia", - "repeater_privacyModeSubtitle": "Skryť meno/poloha v reklamách", + "repeater_packetForwardingSubtitle": "Povolte opakovač na smerovanie paketov.", + "repeater_guestAccess": "Prístup pre hostí", + "repeater_guestAccessSubtitle": "UmožniÅ¥ prístup hosta iba na čítanie.", + "repeater_privacyMode": "Režim ochrany súkromia", + "repeater_privacyModeSubtitle": "SkryÅ¥ meno/poloha v reklamách", "repeater_advertisementSettings": "Nastavenia reklamy", - "repeater_localAdvertInterval": "Lokálna reklamná časová obdoba", - "repeater_localAdvertIntervalMinutes": "{minutes} minút", + "repeater_localAdvertInterval": "Lokálna reklamná časová obdoba", + "repeater_localAdvertIntervalMinutes": "{minutes} minút", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1019,8 +1019,8 @@ } } }, - "repeater_floodAdvertInterval": "Interval reklamnej povodňovej reklamy", - "repeater_floodAdvertIntervalHours": "{hours} hodín", + "repeater_floodAdvertInterval": "Interval reklamnej povodňovej reklamy", + "repeater_floodAdvertIntervalHours": "{hours} hodín", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Šifrovaný reklamný interval", - "repeater_dangerZone": "Nebezpečná zóna", - "repeater_rebootRepeater": "Restart Repetér", - "repeater_rebootRepeaterSubtitle": "Resetovať vysielací prístroj", - "repeater_rebootRepeaterConfirm": "Ste si istý, že chcete tento opakovač restartovať?", - "repeater_regenerateIdentityKey": "Generovať kľúč identity", - "repeater_regenerateIdentityKeySubtitle": "Generovať nový pár verejných/privátnych kľúčov", - "repeater_regenerateIdentityKeyConfirm": "Toto vytvorí nový identitu pre opakovač. Pokračovať?", - "repeater_eraseFileSystem": "Vymažať Systémový Reťazec", - "repeater_eraseFileSystemSubtitle": "Formátovať systém opakujúcich sa súborov", - "repeater_eraseFileSystemConfirm": "VAROVANIE: Toto zmaže všetky dáta na opakovači. To sa nedá zrušiť!", - "repeater_eraseSerialOnly": "Odstránenie je dostupné len cez sériové rozhranie.", - "repeater_commandSent": "Poforovaný príkaz: {command}", + "repeater_encryptedAdvertInterval": "Å ifrovaný reklamný interval", + "repeater_dangerZone": "Nebezpečná zóna", + "repeater_rebootRepeater": "Restart Repetér", + "repeater_rebootRepeaterSubtitle": "ResetovaÅ¥ vysielací prístroj", + "repeater_rebootRepeaterConfirm": "Ste si istý, že chcete tento opakovač restartovaÅ¥?", + "repeater_regenerateIdentityKey": "GenerovaÅ¥ kľúč identity", + "repeater_regenerateIdentityKeySubtitle": "GenerovaÅ¥ nový pár verejných/privátnych kľúčov", + "repeater_regenerateIdentityKeyConfirm": "Toto vytvorí nový identitu pre opakovač. PokračovaÅ¥?", + "repeater_eraseFileSystem": "VymažaÅ¥ Systémový ReÅ¥azec", + "repeater_eraseFileSystemSubtitle": "FormátovaÅ¥ systém opakujúcich sa súborov", + "repeater_eraseFileSystemConfirm": "VAROVANIE: Toto zmaže vÅ¡etky dáta na opakovači. To sa nedá zruÅ¡iÅ¥!", + "repeater_eraseSerialOnly": "Odstránenie je dostupné len cez sériové rozhranie.", + "repeater_commandSent": "Poforovaný príkaz: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Chyba pri odeslaní príkazu: {error}", + "repeater_errorSendingCommand": "Chyba pri odeslaní príkazu: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "Potvrdiť", - "repeater_settingsSaved": "Nastavenia boli uložené úspešne.", - "repeater_errorSavingSettings": "Chyba pri ukladaní nastavení: {error}", + "repeater_confirm": "PotvrdiÅ¥", + "repeater_settingsSaved": "Nastavenia boli uložené úspeÅ¡ne.", + "repeater_errorSavingSettings": "Chyba pri ukladaní nastavení: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "Obnoviť základné nastavenia", - "repeater_refreshRadioSettings": "Obnoviť Nastavenia Rádií", - "repeater_refreshTxPower": "Obnoviť TX napájanie", - "repeater_refreshLocationSettings": "Obnoviť Nastavenia Miesta", - "repeater_refreshPacketForwarding": "Obnoviť smerovanie paketov", - "repeater_refreshGuestAccess": "Obnoviť prístup hosťa", - "repeater_refreshPrivacyMode": "Obnoviť Ochranný režim", - "repeater_refreshAdvertisementSettings": "Obnoviť nastavenia reklamy", - "repeater_refreshed": "{label} sa znova načítalo", + "repeater_refreshBasicSettings": "ObnoviÅ¥ základné nastavenia", + "repeater_refreshRadioSettings": "ObnoviÅ¥ Nastavenia Rádií", + "repeater_refreshTxPower": "ObnoviÅ¥ TX napájanie", + "repeater_refreshLocationSettings": "ObnoviÅ¥ Nastavenia Miesta", + "repeater_refreshPacketForwarding": "ObnoviÅ¥ smerovanie paketov", + "repeater_refreshGuestAccess": "ObnoviÅ¥ prístup hosÅ¥a", + "repeater_refreshPrivacyMode": "ObnoviÅ¥ Ochranný režim", + "repeater_refreshAdvertisementSettings": "ObnoviÅ¥ nastavenia reklamy", + "repeater_refreshed": "{label} sa znova načítalo", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Chyba pri obnovení {label}", + "repeater_errorRefreshing": "Chyba pri obnovení {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,16 +1091,16 @@ } }, "repeater_cliTitle": "Opakovacia CLI", - "repeater_debugNextCommand": "Oprava Nasledujúceho Príkaz", + "repeater_debugNextCommand": "Oprava Nasledujúceho Príkaz", "repeater_commandHelp": "Pomoc", - "repeater_clearHistory": "Vymazať históriu", - "repeater_noCommandsSent": "Zatiaľ neboli odeslané žiadne príkazy.", - "repeater_typeCommandOrUseQuick": "Zadajte príkaz nižšie alebo použite rýchle príkazy", - "repeater_enterCommandHint": "Zadajte príkaz...", - "repeater_previousCommand": "Predchádzajúci príkaz", - "repeater_nextCommand": "Nasledujúci príkaz", - "repeater_enterCommandFirst": "Zadajte najprv príkaz", - "repeater_cliCommandFrameTitle": "Rámok Príkaz CLI", + "repeater_clearHistory": "VymazaÅ¥ históriu", + "repeater_noCommandsSent": "Zatiaľ neboli odeslané žiadne príkazy.", + "repeater_typeCommandOrUseQuick": "Zadajte príkaz nižšie alebo použite rýchle príkazy", + "repeater_enterCommandHint": "Zadajte príkaz...", + "repeater_previousCommand": "Predchádzajúci príkaz", + "repeater_nextCommand": "Nasledujúci príkaz", + "repeater_enterCommandFirst": "Zadajte najprv príkaz", + "repeater_cliCommandFrameTitle": "Rámok Príkaz CLI", "repeater_cliCommandError": "Chyba: {error}", "@repeater_cliCommandError": { "placeholders": { @@ -1109,81 +1109,81 @@ } } }, - "repeater_cliQuickGetName": "Zísť meno", - "repeater_cliQuickGetRadio": "Zísť po rádiu", - "repeater_cliQuickGetTx": "Zísť TX", - "repeater_cliQuickNeighbors": "Súsezný", + "repeater_cliQuickGetName": "ZísÅ¥ meno", + "repeater_cliQuickGetRadio": "ZísÅ¥ po rádiu", + "repeater_cliQuickGetTx": "ZísÅ¥ TX", + "repeater_cliQuickNeighbors": "Súsezný", "repeater_cliQuickVersion": "Verzia", "repeater_cliQuickAdvertise": "Reklama", "repeater_cliQuickClock": "Hodiny", - "repeater_cliHelpAdvert": "Odosiela reklamnú balíček.", - "repeater_cliHelpReboot": "Resetuje zariadenie. (pozor, môže dôjsť k 'Timeoutu', čo je normálne)", - "repeater_cliHelpClock": "Zobrazuje aktuálny čas podľa hodiniek zariadenia.", - "repeater_cliHelpPassword": "Nastaví nový administrátorský prístupový údaj pre zariadenie.", - "repeater_cliHelpVersion": "Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.", - "repeater_cliHelpClearStats": "Resetuje rôzne štatistické počítadlá na nulu.", - "repeater_cliHelpSetAf": "Nastavuje časový faktor.", - "repeater_cliHelpSetTx": "Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reštart na aplikáciu)", - "repeater_cliHelpSetRepeat": "Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.", - "repeater_cliHelpSetAllowReadOnly": "(Server miestnosti) Ak je 'zapnuté', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielať správu do miestnosti. (iba čítať).", - "repeater_cliHelpSetFloodMax": "Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)", - "repeater_cliHelpSetIntThresh": "Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.", - "repeater_cliHelpSetAgcResetInterval": "Nastavuje interval na reštartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.", - "repeater_cliHelpSetMultiAcks": "Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".", - "repeater_cliHelpSetAdvertInterval": "Nastavuje interval časovača v minútach na odošle miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.", - "repeater_cliHelpSetFloodAdvertInterval": "Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.", - "repeater_cliHelpSetGuestPassword": "Nastavuje/aktualizuje heslo hosťa. (pre opakované pripojenia môžu hosťovské prihlásenia posielať požadanie \"Get Stats\")", - "repeater_cliHelpSetName": "Nastaví názov reklamy.", - "repeater_cliHelpSetLat": "Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)", - "repeater_cliHelpSetLon": "Nastavuje longitudinu reklamnej mapy. (desatinné stupne)", - "repeater_cliHelpSetRadio": "Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.", - "repeater_cliHelpSetRxDelay": "Nastavenia (experimentálne) základné (musi byť > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.", - "repeater_cliHelpSetTxDelay": "Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiť pravdepodobnosť kolízii).", - "repeater_cliHelpSetDirectTxDelay": "Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.", - "repeater_cliHelpSetBridgeEnabled": "Aktivovať/Zatvárať most.", - "repeater_cliHelpSetBridgeDelay": "Nastaviť odklad pred retransmisiou paketov.", - "repeater_cliHelpSetBridgeSource": "Zvolte, či bude most retransmitovať prijaté alebo vysielané balíčky.", - "repeater_cliHelpSetBridgeBaud": "Nastavte sériový link baudrate pre rs232 mosty.", - "repeater_cliHelpSetBridgeSecret": "Nastaviť tajomstvo mosta pre eshnow mosty.", - "repeater_cliHelpSetAdcMultiplier": "Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).", - "repeater_cliHelpTempRadio": "Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).", - "repeater_cliHelpSetPerm": "Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).", - "repeater_cliHelpGetBridgeType": "Zísť typ mosta: žiadny, rs232, espnow", - "repeater_cliHelpLogStart": "Začína protokolovanie balíkov do systému súborov.", - "repeater_cliHelpLogStop": "Zastaví protokolovanie paketov do systémového súboru.", - "repeater_cliHelpLogErase": "Odstráni záznamy z balíkov z systému súborov.", - "repeater_cliHelpNeighbors": "Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.", - "repeater_cliHelpRegion": "(len sériál) Zobrazuje všetky definované regióny a aktuálne povolenia pre povodňové situácie.", - "repeater_cliHelpRegionLoad": "Poznámka: toto je špeciálna multi-príkázová inštancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.", - "repeater_cliHelpRegionGet": "Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) 'F'\"", - "repeater_cliHelpRegionPut": "Pridá alebo aktualizuje definíciu regiónu s daným menom.", - "repeater_cliHelpRegionRemove": "Odstráni definíciu oblasti s daným názvom. (musí zodpovedať presne a nemala by mať podoblasti)", - "repeater_cliHelpRegionAllowf": "Nastavuje povolenie 'P'lávu pre zadanú oblasť. ('' pre globálny/dedičský rozsah)", - "repeater_cliHelpRegionDenyf": "Odstráni povolenie 'F'lood' pre zadanú oblasť. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používať na globálnom/dedskom rozsahu!!).", - "repeater_cliHelpRegionHome": "Odpovedá s aktuálnou 'domovskou' oblasťou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)", - "repeater_cliHelpRegionHomeSet": "Nastaví 'domovskú' oblasť.", - "repeater_cliHelpRegionSave": "Uloží zoznam/mapu regiónov do úložiska.", - "repeater_cliHelpGps": "Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.", - "repeater_cliHelpGpsOnOff": "Prepínač stavu GPS napájania.", - "repeater_cliHelpGpsSync": "Synchronizuje čas uzla s GPS hodinami.", - "repeater_cliHelpGpsSetLoc": "Nastaví polohu uzla na GPS súradnice a uloží preferencie.", - "repeater_cliHelpGpsAdvert": "Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľať: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach", - "repeater_cliHelpGpsAdvertSet": "Nastavuje konfiguráciu reklamy na zadané miesto.", - "repeater_commandsListTitle": "Zoznam príkazov", - "repeater_commandsListNote": "Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".", - "repeater_general": "Obecné", + "repeater_cliHelpAdvert": "Odosiela reklamnú balíček.", + "repeater_cliHelpReboot": "Resetuje zariadenie. (pozor, môže dôjsÅ¥ k 'Timeoutu', čo je normálne)", + "repeater_cliHelpClock": "Zobrazuje aktuálny čas podľa hodiniek zariadenia.", + "repeater_cliHelpPassword": "Nastaví nový administrátorský prístupový údaj pre zariadenie.", + "repeater_cliHelpVersion": "Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.", + "repeater_cliHelpClearStats": "Resetuje rôzne Å¡tatistické počítadlá na nulu.", + "repeater_cliHelpSetAf": "Nastavuje časový faktor.", + "repeater_cliHelpSetTx": "Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reÅ¡tart na aplikáciu)", + "repeater_cliHelpSetRepeat": "Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.", + "repeater_cliHelpSetAllowReadOnly": "(Server miestnosti) Ak je 'zapnuté', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielaÅ¥ správu do miestnosti. (iba čítaÅ¥).", + "repeater_cliHelpSetFloodMax": "Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)", + "repeater_cliHelpSetIntThresh": "Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.", + "repeater_cliHelpSetAgcResetInterval": "Nastavuje interval na reÅ¡tartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.", + "repeater_cliHelpSetMultiAcks": "Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".", + "repeater_cliHelpSetAdvertInterval": "Nastavuje interval časovača v minútach na odoÅ¡le miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.", + "repeater_cliHelpSetFloodAdvertInterval": "Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.", + "repeater_cliHelpSetGuestPassword": "Nastavuje/aktualizuje heslo hosÅ¥a. (pre opakované pripojenia môžu hosÅ¥ovské prihlásenia posielaÅ¥ požadanie \"Get Stats\")", + "repeater_cliHelpSetName": "Nastaví názov reklamy.", + "repeater_cliHelpSetLat": "Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)", + "repeater_cliHelpSetLon": "Nastavuje longitudinu reklamnej mapy. (desatinné stupne)", + "repeater_cliHelpSetRadio": "Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.", + "repeater_cliHelpSetRxDelay": "Nastavenia (experimentálne) základné (musi byÅ¥ > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.", + "repeater_cliHelpSetTxDelay": "Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiÅ¥ pravdepodobnosÅ¥ kolízii).", + "repeater_cliHelpSetDirectTxDelay": "Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.", + "repeater_cliHelpSetBridgeEnabled": "AktivovaÅ¥/ZatváraÅ¥ most.", + "repeater_cliHelpSetBridgeDelay": "NastaviÅ¥ odklad pred retransmisiou paketov.", + "repeater_cliHelpSetBridgeSource": "Zvolte, či bude most retransmitovaÅ¥ prijaté alebo vysielané balíčky.", + "repeater_cliHelpSetBridgeBaud": "Nastavte sériový link baudrate pre rs232 mosty.", + "repeater_cliHelpSetBridgeSecret": "NastaviÅ¥ tajomstvo mosta pre eshnow mosty.", + "repeater_cliHelpSetAdcMultiplier": "Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).", + "repeater_cliHelpTempRadio": "Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).", + "repeater_cliHelpSetPerm": "Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).", + "repeater_cliHelpGetBridgeType": "ZísÅ¥ typ mosta: žiadny, rs232, espnow", + "repeater_cliHelpLogStart": "Začína protokolovanie balíkov do systému súborov.", + "repeater_cliHelpLogStop": "Zastaví protokolovanie paketov do systémového súboru.", + "repeater_cliHelpLogErase": "Odstráni záznamy z balíkov z systému súborov.", + "repeater_cliHelpNeighbors": "Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.", + "repeater_cliHelpRegion": "(len sériál) Zobrazuje vÅ¡etky definované regióny a aktuálne povolenia pre povodňové situácie.", + "repeater_cliHelpRegionLoad": "Poznámka: toto je Å¡peciálna multi-príkázová inÅ¡tancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.", + "repeater_cliHelpRegionGet": "Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) 'F'\"", + "repeater_cliHelpRegionPut": "Pridá alebo aktualizuje definíciu regiónu s daným menom.", + "repeater_cliHelpRegionRemove": "Odstráni definíciu oblasti s daným názvom. (musí zodpovedaÅ¥ presne a nemala by maÅ¥ podoblasti)", + "repeater_cliHelpRegionAllowf": "Nastavuje povolenie 'P'lávu pre zadanú oblasÅ¥. ('' pre globálny/dedičský rozsah)", + "repeater_cliHelpRegionDenyf": "Odstráni povolenie 'F'lood' pre zadanú oblasÅ¥. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používaÅ¥ na globálnom/dedskom rozsahu!!).", + "repeater_cliHelpRegionHome": "Odpovedá s aktuálnou 'domovskou' oblasÅ¥ou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)", + "repeater_cliHelpRegionHomeSet": "Nastaví 'domovskú' oblasÅ¥.", + "repeater_cliHelpRegionSave": "Uloží zoznam/mapu regiónov do úložiska.", + "repeater_cliHelpGps": "Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.", + "repeater_cliHelpGpsOnOff": "Prepínač stavu GPS napájania.", + "repeater_cliHelpGpsSync": "Synchronizuje čas uzla s GPS hodinami.", + "repeater_cliHelpGpsSetLoc": "Nastaví polohu uzla na GPS súradnice a uloží preferencie.", + "repeater_cliHelpGpsAdvert": "Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľaÅ¥: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach", + "repeater_cliHelpGpsAdvertSet": "Nastavuje konfiguráciu reklamy na zadané miesto.", + "repeater_commandsListTitle": "Zoznam príkazov", + "repeater_commandsListNote": "Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".", + "repeater_general": "Obecné", "repeater_settingsCategory": "Nastavenia", "repeater_bridge": "Most", - "repeater_logging": "Záznamy", - "repeater_neighborsRepeaterOnly": "Súseznýci (iba opakovač)", - "repeater_regionManagementRepeaterOnly": "Správa regiónov (iba opakovač)", - "repeater_regionNote": "Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.", - "repeater_gpsManagement": "Správa GPS", - "repeater_gpsNote": "GPS príkaz bol zavádzaný na riadenie lokalitných tém.", - "telemetry_receivedData": "Obdolené Telemetrické dáta", - "telemetry_requestTimeout": "Požiadavka telemetrie zlyhala.", - "telemetry_errorLoading": "Chyba pri načítaní telemetrie: {error}", + "repeater_logging": "Záznamy", + "repeater_neighborsRepeaterOnly": "Súseznýci (iba opakovač)", + "repeater_regionManagementRepeaterOnly": "Správa regiónov (iba opakovač)", + "repeater_regionNote": "Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.", + "repeater_gpsManagement": "Správa GPS", + "repeater_gpsNote": "GPS príkaz bol zavádzaný na riadenie lokalitných tém.", + "telemetry_receivedData": "Obdolené Telemetrické dáta", + "telemetry_requestTimeout": "Požiadavka telemetrie zlyhala.", + "telemetry_errorLoading": "Chyba pri načítaní telemetrie: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,8 +1191,8 @@ } } }, - "telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.", - "telemetry_channelTitle": "Kanál {channel}", + "telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.", + "telemetry_channelTitle": "Kanál {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1200,11 +1200,11 @@ } } }, - "telemetry_batteryLabel": "Batéria", - "telemetry_voltageLabel": "Napätie", + "telemetry_batteryLabel": "Batéria", + "telemetry_voltageLabel": "Napätie", "telemetry_mcuTemperatureLabel": "MCU teplota", "telemetry_temperatureLabel": "Teplota", - "telemetry_currentLabel": "Aktuálne", + "telemetry_currentLabel": "Aktuálne", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Cesta balíka", - "channelPath_viewMap": "Zobraziť mapu", - "channelPath_otherObservedPaths": "Ostatné pozorovacie cesty", - "channelPath_repeaterHops": "Skoky opakovača", - "channelPath_noHopDetails": "Podrobnosti o balíčku zatiaľ nie sú dostupné.", - "channelPath_messageDetails": "Podrobnosti o zprávach", - "channelPath_senderLabel": "Posielateľ", - "channelPath_timeLabel": "Čas", + "channelPath_title": "Cesta balíka", + "channelPath_viewMap": "ZobraziÅ¥ mapu", + "channelPath_otherObservedPaths": "Ostatné pozorovacie cesty", + "channelPath_repeaterHops": "Skoky opakovača", + "channelPath_noHopDetails": "Podrobnosti o balíčku zatiaľ nie sú dostupné.", + "channelPath_messageDetails": "Podrobnosti o zprávach", + "channelPath_senderLabel": "Posielateľ", + "channelPath_timeLabel": "ÄŒas", "channelPath_repeatsLabel": "Opakovanie", "channelPath_pathLabel": "Cesta {index}", - "channelPath_observedLabel": "Pozorované", - "channelPath_observedPathTitle": "Sledovaný postup {index} • {hops}", + "channelPath_observedLabel": "Pozorované", + "channelPath_observedPathTitle": "Sledovaný postup {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Žiadne údaje o polohe", + "channelPath_noLocationData": "Žiadne údaje o polohe", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1288,8 +1288,8 @@ } } }, - "channelPath_unknownPath": "Neznáme", - "channelPath_floodPath": "Povodňová", + "channelPath_unknownPath": "Neznáme", + "channelPath_floodPath": "Povodňová", "channelPath_directPath": "Priamo", "channelPath_observedZeroOf": "0 z {total} skokov", "@channelPath_observedZeroOf": { @@ -1311,8 +1311,8 @@ } }, "channelPath_mapTitle": "Mapa Trasy", - "channelPath_noRepeaterLocations": "Pre túto cestu nie je dostupných žiadne polohy opakovačov.", - "channelPath_primaryPath": "Cesta {index} (Hlavná)", + "channelPath_noRepeaterLocations": "Pre túto cestu nie je dostupných žiadne polohy opakovačov.", + "channelPath_primaryPath": "Cesta {index} (Hlavná)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1328,8 +1328,8 @@ } }, "channelPath_pathLabelTitle": "Cesta", - "channelPath_observedPathHeader": "Sledovaná cesta", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_observedPathHeader": "Sledovaná cesta", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,20 +1340,20 @@ } } }, - "channelPath_noHopDetailsAvailable": "Pre toto balíček nie sú dostupné údaje o skokoch.", - "channelPath_unknownRepeater": "Neznáme opakovače", - "listFilter_tooltip": "Filtrovať a triediť", - "listFilter_sortBy": "Triediť podľa", - "listFilter_latestMessages": "Posledné správy", - "listFilter_heardRecently": "Nedávno počuli.", + "channelPath_noHopDetailsAvailable": "Pre toto balíček nie sú dostupné údaje o skokoch.", + "channelPath_unknownRepeater": "Neznáme opakovače", + "listFilter_tooltip": "FiltrovaÅ¥ a triediÅ¥", + "listFilter_sortBy": "TriediÅ¥ podľa", + "listFilter_latestMessages": "Posledné správy", + "listFilter_heardRecently": "Nedávno počuli.", "listFilter_az": "A-Z", "listFilter_filters": "Filtre", - "listFilter_all": "Všetko", - "listFilter_users": "Používatelia", - "listFilter_repeaters": "Opakovadlá", - "listFilter_roomServers": "Servéry miestnosti", - "listFilter_unreadOnly": "Nezaregistrované len", - "listFilter_newGroup": "Nová skupina", + "listFilter_all": "VÅ¡etko", + "listFilter_users": "Používatelia", + "listFilter_repeaters": "Opakovadlá", + "listFilter_roomServers": "Servéry miestnosti", + "listFilter_unreadOnly": "Nezaregistrované len", + "listFilter_newGroup": "Nová skupina", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1361,25 +1361,25 @@ } } }, - "repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.", - "neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.", - "neighbors_receivedData": "Obdielo dáta suseda", - "repeater_neighbors": "Súsezný", - "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", - "neighbors_repeatersNeighbors": "Opakovadlá Súsezná", - "neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.", - "channels_createPrivateChannel": "Vytvorte súkromný kanál", - "channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu", - "channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.", - "channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.", - "channels_joinPublicChannel": "Pripojte sa k verejnému kanálu", - "channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.", - "channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu", - "channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridať do hashtag kanálov.", - "channels_scanQrCode": "Skenujte QR kód", - "channels_scanQrCodeComingSoon": "Čoskoro", + "repeater_neighborsSubtitle": "ZobraziÅ¥ susedné body bez skokov.", + "neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.", + "neighbors_receivedData": "Obdielo dáta suseda", + "repeater_neighbors": "Súsezný", + "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", + "neighbors_repeatersNeighbors": "Opakovadlá Súsezná", + "neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.", + "channels_createPrivateChannel": "Vytvorte súkromný kanál", + "channels_joinPrivateChannel": "PripojiÅ¥ sa k súkromnému kanálu", + "channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.", + "channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.", + "channels_joinPublicChannel": "Pripojte sa k verejnému kanálu", + "channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.", + "channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu", + "channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridaÅ¥ do hashtag kanálov.", + "channels_scanQrCode": "Skenujte QR kód", + "channels_scanQrCodeComingSoon": "ÄŒoskoro", "channels_enterHashtag": "Zadajte hashtag", - "channels_hashtagHint": "napr. #tím", + "channels_hashtagHint": "napr. #tím", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_heardAgo": "Počuli sme to: {time} dozadu", - "neighbors_unknownContact": "Neznáma {pubkey}", - "settings_locationGPSEnable": "Aktivovať GPS", - "settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.", + "neighbors_heardAgo": "Počuli sme to: {time} dozadu", + "neighbors_unknownContact": "Neznáma {pubkey}", + "settings_locationGPSEnable": "AktivovaÅ¥ GPS", + "settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.", "settings_locationIntervalSec": "Interval pre GPS (Sekundy)", - "settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.", - "contacts_manageRoom": "Spravovať server miestnosti", - "room_management": "Správa servera miestnosti", + "settings_locationIntervalInvalid": "Interval musí byÅ¥ aspoň 60 sekúnd a menej ako 86400 sekúnd.", + "contacts_manageRoom": "SpravovaÅ¥ server miestnosti", + "room_management": "Správa servera miestnosti", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1458,36 +1458,36 @@ } } }, - "community_create": "Vytvoriť komunitu", + "community_create": "VytvoriÅ¥ komunitu", "community_title": "Komunita", - "community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.", - "community_join": "Pripojiť", - "community_joinTitle": "Pripojiť sa k spoločenstvu", - "community_joinConfirmation": "Chceš sa pridať do komunity \"{name}\"?", - "community_scanQr": "Skontrolujte komunitný QR kód", - "community_scanInstructions": "Zamerte kameru na komunitný QR kód.", - "community_showQr": "Zobraziť QR kód", + "community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.", + "community_join": "PripojiÅ¥", + "community_joinTitle": "PripojiÅ¥ sa k spoločenstvu", + "community_joinConfirmation": "ChceÅ¡ sa pridaÅ¥ do komunity \"{name}\"?", + "community_scanQr": "Skontrolujte komunitný QR kód", + "community_scanInstructions": "Zamerte kameru na komunitný QR kód.", + "community_showQr": "ZobraziÅ¥ QR kód", "common_ok": "OK\nDobre", - "community_publicChannel": "Komunita verejná", - "community_hashtagChannel": "Komunitný Hashtag", + "community_publicChannel": "Komunita verejná", + "community_hashtagChannel": "Komunitný Hashtag", "community_name": "Komunita", - "community_enterName": "Zadajte názov komunity", - "community_created": "Komunita \"{name}\" vytvorená", - "community_joined": "Pripojená komunita \"{name}\"", - "community_qrTitle": "Zdieľť komunitu", - "community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.", - "community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity", - "community_invalidQrCode": "Neplatná QR kód komunity.", - "community_alreadyMember": "Už ste členom.", - "community_alreadyMemberMessage": "Vy ste už členom \"{name}\".", - "community_addPublicChannel": "Pridať verejný komunikačný kanál", - "community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.", - "community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite", - "community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.", - "community_manageCommunities": "Spravovať komunity", + "community_enterName": "Zadajte názov komunity", + "community_created": "Komunita \"{name}\" vytvorená", + "community_joined": "Pripojená komunita \"{name}\"", + "community_qrTitle": "Zdieľť komunitu", + "community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.", + "community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity", + "community_invalidQrCode": "Neplatná QR kód komunity.", + "community_alreadyMember": "Už ste členom.", + "community_alreadyMemberMessage": "Vy ste už členom \"{name}\".", + "community_addPublicChannel": "PridaÅ¥ verejný komunikačný kanál", + "community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.", + "community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite", + "community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.", + "community_manageCommunities": "SpravovaÅ¥ komunity", "community_delete": "Nechajte komunitu", - "community_deleteConfirm": "Opustiť \"{name}\"?", - "community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.", + "community_deleteConfirm": "OpustiÅ¥ \"{name}\"?", + "community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Opustená komunita \"{name}\"", - "community_addHashtagChannel": "Pridať komunitný hashtag", - "community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.", + "community_deleted": "Opustená komunita \"{name}\"", + "community_addHashtagChannel": "PridaÅ¥ komunitný hashtag", + "community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.", "community_selectCommunity": "Vyberte komunitu", - "community_regularHashtag": "Zvyčajný hashtag", - "community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)", - "community_communityHashtag": "Komunitný Hashtag", - "community_communityHashtagDesc": "Špecifické pre členov komunity", + "community_regularHashtag": "Zvyčajný hashtag", + "community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridaÅ¥)", + "community_communityHashtag": "Komunitný Hashtag", + "community_communityHashtagDesc": "Å pecifické pre členov komunity", "community_forCommunity": "Pre {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1532,12 +1532,12 @@ } } }, - "community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne", - "community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.", - "community_regenerate": "Znovu vygenerovať", - "community_regenerateSecret": "Zobraziť nový tajný kód", - "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"", - "community_updateSecret": "Aktualizovať tajné heslo", + "community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne", + "community_regenerateSecretConfirm": "Znovu vygenerovaÅ¥ tajný kľúč pre \"{name}\"? VÅ¡etci členovia budú musieÅ¥ skanovaÅ¥ nový QR kód, aby mohli nadviazaÅ¥ komunikáciu.", + "community_regenerate": "Znovu vygenerovaÅ¥", + "community_regenerateSecret": "ZobraziÅ¥ nový tajný kód", + "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"", + "community_updateSecret": "AktualizovaÅ¥ tajné heslo", "community_secretUpdated": "Zmena tajnej slova pre \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { @@ -1548,80 +1548,80 @@ }, "pathTrace_you": "Vy", "pathTrace_failed": "Sledovanie cesty zlyhalo.", - "pathTrace_notAvailable": "Path trace nie je k dispozícii.", - "pathTrace_refreshTooltip": "Obnoviť Path Trace.", - "contacts_pathTrace": "Sledovanie lúčov", - "contacts_ping": "Pingovať", - "contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču", - "contacts_repeaterPing": "Pingovať opakovač", + "pathTrace_notAvailable": "Path trace nie je k dispozícii.", + "pathTrace_refreshTooltip": "ObnoviÅ¥ Path Trace.", + "contacts_pathTrace": "Sledovanie lúčov", + "contacts_ping": "PingovaÅ¥", + "contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču", + "contacts_repeaterPing": "PingovaÅ¥ opakovač", "contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti", "contacts_roomPing": "Ping server miestnosti", - "contacts_chatTraceRoute": "Sledovať trasu lúča", - "contacts_pathTraceTo": "Sledovať trasu k {name}", - "contacts_clipboardEmpty": "Schránka je prázdna.", - "appSettings_languageUk": "Ukrajinská", - "contacts_contactImportFailed": "Kontakt sa nepodarilo importovať.", - "contacts_zeroHopAdvert": "Inzerát Zero Hop", - "contacts_floodAdvert": "Inzerát povodní", - "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", - "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", - "appSettings_languageRu": "Ruština", - "appSettings_enableMessageTracing": "Povoliť sledovanie správ", - "appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ", - "contacts_addContactFromClipboard": "Pridať kontakt z schránky", - "contacts_contactImported": "Kontakt bol importovaný.", - "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", - "contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.", - "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", + "contacts_chatTraceRoute": "SledovaÅ¥ trasu lúča", + "contacts_pathTraceTo": "SledovaÅ¥ trasu k {name}", + "contacts_clipboardEmpty": "Schránka je prázdna.", + "appSettings_languageUk": "Ukrajinská", + "contacts_contactImportFailed": "Kontakt sa nepodarilo importovaÅ¥.", + "contacts_zeroHopAdvert": "Inzerát Zero Hop", + "contacts_floodAdvert": "Inzerát povodní", + "contacts_copyAdvertToClipboard": "KopírovaÅ¥ reklamu do schránky", + "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", + "appSettings_languageRu": "RuÅ¡tina", + "appSettings_enableMessageTracing": "PovoliÅ¥ sledovanie správ", + "appSettings_enableMessageTracingSubtitle": "ZobraziÅ¥ podrobné metadáta o smerovaní a časovaní správ", + "contacts_addContactFromClipboard": "PridaÅ¥ kontakt z schránky", + "contacts_contactImported": "Kontakt bol importovaný.", + "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", + "contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.", + "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", - "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", - "contacts_ShareContact": "Kopírovať kontakt do schránky", + "contacts_ShareContactZeroHop": "ZdieľaÅ¥ kontakt cez inzerát", + "contacts_ShareContact": "KopírovaÅ¥ kontakt do schránky", "notification_activityTitle": "Aktivita MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}", - "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", - "notification_newTypeDiscovered": "Nový {contactType} objavený", - "notification_receivedNewMessage": "Prijatá nová správa", - "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", + "notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}", + "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", + "notification_newTypeDiscovered": "Nový {contactType} objavený", + "notification_receivedNewMessage": "Prijatá nová správa", + "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", "settings_gpxExportContacts": "Export sprievodcov do GPX", - "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", - "settings_gpxExportNoContacts": "Žiadne kontakty na export.", - "settings_gpxExportNotAvailable": "Nie je podporované na vašom zariadení/operáciomnom systéme", - "settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností", - "settings_gpxExportError": "Vyskytol sa chyba počas exportu.", - "settings_gpxExportAllSubtitle": "Exportuje všetky kontakty s lokalitou do súboru GPX.", - "settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.", - "settings_gpxExportRepeaters": "Exportovať repeater / server miestnosti do GPX", - "settings_gpxExportAll": "Exportovať všetky kontakty do GPX", - "settings_gpxExportAllContacts": "Všetky kontaktné lokality", - "settings_gpxExportChat": "Lokácie sprievodcov", - "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", - "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!", - "pathTrace_clearTooltip": "Zmazať cestu", + "settings_gpxExportSuccess": "ÚspeÅ¡ne exportovaný súbor GPX.", + "settings_gpxExportNoContacts": "Žiadne kontakty na export.", + "settings_gpxExportNotAvailable": "Nie je podporované na vaÅ¡om zariadení/operáciomnom systéme", + "settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností", + "settings_gpxExportError": "Vyskytol sa chyba počas exportu.", + "settings_gpxExportAllSubtitle": "Exportuje vÅ¡etky kontakty s lokalitou do súboru GPX.", + "settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.", + "settings_gpxExportRepeaters": "ExportovaÅ¥ repeater / server miestnosti do GPX", + "settings_gpxExportAll": "ExportovaÅ¥ vÅ¡etky kontakty do GPX", + "settings_gpxExportAllContacts": "VÅ¡etky kontaktné lokality", + "settings_gpxExportChat": "Lokácie sprievodcov", + "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", + "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!", + "pathTrace_clearTooltip": "ZmazaÅ¥ cestu", "map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.", - "map_removeLast": "Odstrániť posledný", - "map_runTrace": "Spustiť trasovaním cesty", - "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", - "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", - "scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome", - "scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.", - "scanner_bluetoothOff": "Bluetooth je vypnutý", + "map_removeLast": "OdstrániÅ¥ posledný", + "map_runTrace": "SpustiÅ¥ trasovaním cesty", + "map_pathTraceCancelled": "ZruÅ¡enie stopáže cesty bolo zruÅ¡ené.", + "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovaÅ¥ pre zariadenia.", + "scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome", + "scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.", + "scanner_bluetoothOff": "Bluetooth je vypnutý", "scanner_enableBluetooth": "Povolte Bluetooth", - "snrIndicator_lastSeen": "Naposledy videný", - "snrIndicator_nearByRepeaters": "Miestne opakovače", - "chat_ShowAllPaths": "Zobraziť všetky cesty", - "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", - "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", - "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.", - "settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)", + "snrIndicator_lastSeen": "Naposledy videný", + "snrIndicator_nearByRepeaters": "Miestne opakovače", + "chat_ShowAllPaths": "ZobraziÅ¥ vÅ¡etky cesty", + "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", + "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", + "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.", + "settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Jednotky", - "appSettings_unitsMetric": "Metrické (m / km)", - "appSettings_unitsImperial": "Imperiálne (ft / mi)", + "appSettings_unitsMetric": "Metrické (m / km)", + "appSettings_unitsImperial": "Imperiálne (ft / mi)", "map_lineOfSight": "Line of Sight", "map_losScreenTitle": "Line of Sight", - "losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.", - "losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}", + "losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.", + "losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,13 +1629,13 @@ } } }, - "losClearAllPoints": "Vymazať všetky body", - "losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS", + "losClearAllPoints": "VymazaÅ¥ vÅ¡etky body", + "losRunToViewElevationProfile": "Ak chcete zobraziÅ¥ výškový profil, spustite LOS", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body", - "losShowDisplayNodes": "Zobraziť uzly zobrazenia", - "losCustomPoints": "Vlastné body", - "losCustomPointLabel": "Vlastné {index}", + "losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body", + "losShowDisplayNodes": "ZobraziÅ¥ uzly zobrazenia", + "losCustomPoints": "Vlastné body", + "losCustomPointLabel": "Vlastné {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1645,7 +1645,7 @@ }, "losPointA": "Bod A", "losPointB": "Bod B", - "losAntennaA": "Anténa A: {value} {unit}", + "losAntennaA": "Anténa A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Anténa B: {value} {unit}", + "losAntennaB": "Anténa B: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1668,8 +1668,8 @@ } }, "losRun": "Spustite LOS", - "losNoElevationData": "Žiadne údaje o nadmorskej výške", - "losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}", + "losNoElevationData": "Žiadne údaje o nadmorskej výške", + "losProfileClear": "{distance} {distanceUnit}, vymazaÅ¥ LOS, min. vôľa {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1704,8 +1704,8 @@ } }, "losStatusChecking": "LOS: kontrolujem...", - "losStatusNoData": "LOS: žiadne údaje", - "losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme", + "losStatusNoData": "LOS: žiadne údaje", + "losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.", - "losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.", - "losRenameCustomPoint": "Premenovať vlastný bod", - "losPointName": "Názov bodu", - "losShowPanelTooltip": "Zobraziť panel LOS", - "losHidePanelTooltip": "Skryť panel LOS", - "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Rádiový horizont", - "losLegendLosBeam": "Priama viditeľnosť", - "losLegendTerrain": "Terén", + "losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.", + "losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.", + "losRenameCustomPoint": "PremenovaÅ¥ vlastný bod", + "losPointName": "Názov bodu", + "losShowPanelTooltip": "ZobraziÅ¥ panel LOS", + "losHidePanelTooltip": "SkryÅ¥ panel LOS", + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádiový horizont", + "losLegendLosBeam": "Priama viditeľnosÅ¥", + "losLegendTerrain": "Terén", "losFrequencyLabel": "Frekvencia", - "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", - "losFrequencyDialogTitle": "Výpočet rádiového horizontu", - "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", + "losFrequencyInfoTooltip": "ZobraziÅ¥ podrobnosti výpočtu", + "losFrequencyDialogTitle": "Výpočet rádiového horizontu", + "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_removeFromFavorites": "Odstrániť z označení", - "listFilter_addToFavorites": "Pridaj do obľúbených", - "listFilter_favorites": "Obľúbené", + "listFilter_removeFromFavorites": "OdstrániÅ¥ z označení", + "listFilter_addToFavorites": "Pridaj do obľúbených", + "listFilter_favorites": "Obľúbené", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1796,19 +1796,17 @@ } } }, - "contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...", - "contacts_searchFavorites": "Hľadať {number}{str} obľúbené...", - "contacts_searchRepeaters": "Hľadať {number}{str} opakovače...", - "contacts_searchUsers": "Hľadať {number}{str} používateľov...", - "contacts_searchContactsNoNumber": "Hľadať kontakty...", - "contacts_unread": "Neprečítané", + "contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...", + "contacts_searchFavorites": "HľadaÅ¥ {number}{str} obľúbené...", + "contacts_searchRepeaters": "HľadaÅ¥ {number}{str} opakovače...", + "contacts_searchUsers": "HľadaÅ¥ {number}{str} používateľov...", + "contacts_searchContactsNoNumber": "HľadaÅ¥ kontakty...", + "contacts_unread": "Neprečítané", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", - "connectionChoiceTitle": "Vyberte si metódu prepojenia.", - "connectionChoiceSubtitle": "Vyberte si, ako chcete dosiahnuť váš zariadenie MeshCore.", "usbScreenStatus": "Vyberte USB zariadenie", - "usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.", - "usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.", + "usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vaÅ¡ej MeshCore uzlu.", + "usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.", "usbScreenTitle": "Pripojte cez USB", - "usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte." + "usbScreenEmptyState": "NenaÅ¡li sa žiadne USB zariadenia. Pripojte jedno a obnovte." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 1d8943e..b9bc264 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", +{ + "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -12,8 +12,8 @@ "nav_contacts": "Stiki", "nav_channels": "Kanali", "nav_map": "Karta", - "common_cancel": "Prekliči", - "common_connect": "Poveži se", + "common_cancel": "Prekliči", + "common_connect": "Poveži se", "common_unknownDevice": "Nepoznano naprave", "common_save": "Shrani", "common_delete": "Izbrisati", @@ -31,11 +31,11 @@ "common_retry": "Ponoviti", "common_hide": "Skrita", "common_remove": "Izbrisati", - "common_enable": "Omogoči", + "common_enable": "Omogoči", "common_disable": "Izklopiti", "common_reboot": "Ponoviti", - "common_loading": "Naložanje...", - "common_notAvailable": "—", + "common_loading": "Naložanje...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -66,8 +66,8 @@ } }, "scanner_searchingDevices": "Iskanje naprav MeshCore...", - "scanner_tapToScan": "Nagneš na skeniranje za najdene naprave MeshCore.", - "scanner_connectionFailed": "Pošlo je z povezavo: {error}", + "scanner_tapToScan": "NagneÅ¡ na skeniranje za najdene naprave MeshCore.", + "scanner_connectionFailed": "PoÅ¡lo je z povezavo: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,51 +75,51 @@ } } }, - "scanner_stop": "Prekliči", + "scanner_stop": "Prekliči", "scanner_scan": "Skeniraj", "device_quickSwitch": "Hitro preklop", "device_meshcore": "MeshCore", "settings_title": "Nastavitve", "settings_deviceInfo": "Informacije o napravei", "settings_appSettings": "Nastavitve aplikacije", - "settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.", - "settings_nodeSettings": "Nastavitev časa", + "settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.", + "settings_nodeSettings": "Nastavitev časa", "settings_nodeName": "Ime node-a", "settings_nodeNameNotSet": "Ni nastavljeno", "settings_nodeNameHint": "Vnesite ime node-a", "settings_nodeNameUpdated": "Ime posodobljeno", "settings_radioSettings": "Nastavitve radija", - "settings_radioSettingsSubtitle": "Frekvenca, moč, razširitveni faktor", + "settings_radioSettingsSubtitle": "Frekvenca, moč, razÅ¡iritveni faktor", "settings_radioSettingsUpdated": "Radio nastavitve posodobljene", "settings_location": "Lokacija", "settings_locationSubtitle": "GPS koordinate", "settings_locationUpdated": "Lokacija posodobljena", - "settings_locationBothRequired": "Vnesite širino in dolžino.", - "settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.", - "settings_latitude": "Širina", - "settings_longitude": "Dolžina", + "settings_locationBothRequired": "Vnesite Å¡irino in dolžino.", + "settings_locationInvalid": "Neveljavna zemeljska Å¡irina ali dolžina.", + "settings_latitude": "Å irina", + "settings_longitude": "Dolžina", "settings_privacyMode": "Zasebnost", "settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", - "settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.", - "settings_privacyModeEnabled": "Privatni način je omogočen.", - "settings_privacyModeDisabled": "Privatni način je onemogočen.", + "settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.", + "settings_privacyModeEnabled": "Privatni način je omogočen.", + "settings_privacyModeDisabled": "Privatni način je onemogočen.", "settings_actions": "Akcije", - "settings_sendAdvertisement": "Pošlji Oglas", + "settings_sendAdvertisement": "PoÅ¡lji Oglas", "settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah", "settings_advertisementSent": "Oglas poslan", "settings_syncTime": "Nastavi uro", - "settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona", + "settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona", "settings_timeSynchronized": "Ura sinhronizirana", - "settings_refreshContacts": "Ponovno obišči kontakte", - "settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi", + "settings_refreshContacts": "Ponovno obišči kontakte", + "settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi", "settings_rebootDevice": "Ponovni zagon naprave", - "settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo", - "settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.", + "settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo", + "settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.", "settings_debug": "Debug", - "settings_bleDebugLog": "BLE debug log (razhroščevanje)", + "settings_bleDebugLog": "BLE debug log (razhroščevanje)", "settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki", "settings_appDebugLog": "Logi aplikacije", - "settings_appDebugLogSubtitle": "Debug sporočila aplikacije", + "settings_appDebugLogSubtitle": "Debug sporočila aplikacije", "settings_about": "Oglejte si", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "Odprtokodni projekt MeshCore 2024", - "settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.", + "settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.", "settings_infoName": "Ime", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Baterija", - "settings_infoPublicKey": "Javni ključ", - "settings_infoContactsCount": "Število stikov", - "settings_infoChannelCount": "Število kanalov", + "settings_infoPublicKey": "Javni ključ", + "settings_infoContactsCount": "Å tevilo stikov", + "settings_infoChannelCount": "Å tevilo kanalov", "settings_presets": "Prednastavitve", "settings_frequency": "Frekvenca (MHz)", "settings_frequencyHelper": "300,00 - 2500,00", "settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)", - "settings_bandwidth": "Pasovna širina", - "settings_spreadingFactor": "Razširitveni faktor", + "settings_bandwidth": "Pasovna Å¡irina", + "settings_spreadingFactor": "RazÅ¡iritveni faktor", "settings_codingRate": "Programska hitrost", - "settings_txPower": "TX Moč (dBm)", + "settings_txPower": "TX Moč (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)", + "settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)", "settings_error": "Napaka: {message}", "@settings_error": { "placeholders": { @@ -157,7 +157,7 @@ } }, "appSettings_title": "Nastavitve aplikacije", - "appSettings_appearance": "Prikaži", + "appSettings_appearance": "Prikaži", "appSettings_theme": "Tema", "appSettings_themeSystem": "Sistemska tema", "appSettings_themeLight": "Svetlo", @@ -165,39 +165,39 @@ "appSettings_language": "Jezik", "appSettings_languageSystem": "Sistemska privzeta vrednost", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Obvestila", - "appSettings_enableNotifications": "Omogoči obvestila", - "appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih", + "appSettings_enableNotifications": "Omogoči obvestila", + "appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih", "appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena", - "appSettings_notificationsEnabled": "Obvestila omogočena", + "appSettings_notificationsEnabled": "Obvestila omogočena", "appSettings_notificationsDisabled": "Obvestila so izklopljena", "appSettings_messageNotifications": "Obvestila", - "appSettings_messageNotificationsSubtitle": "Pokaži obvestilo ob prejemu novih sporočil.", - "appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala", - "appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala", + "appSettings_messageNotificationsSubtitle": "Pokaži obvestilo ob prejemu novih sporočil.", + "appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala", + "appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala", "appSettings_advertisementNotifications": "Opozorila o oglasih", - "appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.", + "appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.", "appSettings_messaging": "Komuniciranje", - "appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju", - "appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja", - "appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspešnih poskusih.", - "appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.", + "appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem Å¡tetju", + "appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspeÅ¡nih poskusih poÅ¡iljanja", + "appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspeÅ¡nih poskusih.", + "appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.", "appSettings_autoRouteRotation": "Avtomatsko rotacija prenosne poti", - "appSettings_autoRouteRotationSubtitle": "Menjaj med boljšo potjo in flood načinom", - "appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno", - "appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno", + "appSettings_autoRouteRotationSubtitle": "Menjaj med boljÅ¡o potjo in flood načinom", + "appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno", + "appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno", "appSettings_battery": "Baterija", "appSettings_batteryChemistry": "Kemija baterije", "appSettings_batteryChemistryPerDevice": "Nastavitev za napravo ({deviceName})", @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo", + "appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo", "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Prikaz zemljevida", - "appSettings_showRepeaters": "Prikaži repetitorje", - "appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi", - "appSettings_showChatNodes": "Prikaži naprave za klepet", - "appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu", - "appSettings_showOtherNodes": "Pokaži druge naprave", - "appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.", - "appSettings_timeFilter": "Filter po času", - "appSettings_timeFilterShowAll": "Pokaži vse naprave", - "appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah", + "appSettings_showRepeaters": "Prikaži repetitorje", + "appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi", + "appSettings_showChatNodes": "Prikaži naprave za klepet", + "appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu", + "appSettings_showOtherNodes": "Pokaži druge naprave", + "appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.", + "appSettings_timeFilter": "Filter po času", + "appSettings_timeFilterShowAll": "Pokaži vse naprave", + "appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -229,16 +229,16 @@ } } }, - "appSettings_mapTimeFilter": "Filter časa na zemljevidu", - "appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:", + "appSettings_mapTimeFilter": "Filter časa na zemljevidu", + "appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:", "appSettings_allTime": "Brez omejitev", "appSettings_lastHour": "V zadnji uri", "appSettings_last6Hours": "Zadnjih 6 ur", "appSettings_last24Hours": "Zadnjih 24 ur", - "appSettings_lastWeek": "Prejšnji teden", + "appSettings_lastWeek": "PrejÅ¡nji teden", "appSettings_offlineMapCache": "Shramba zemljevidov brez povezave", - "appSettings_noAreaSelected": "Območje ni izbrano", - "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Območje ni izbrano", + "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,19 +249,19 @@ } } }, - "appSettings_debugCard": "Razhroščevanje", + "appSettings_debugCard": "Razhroščevanje", "appSettings_appDebugLogging": "Programski dnevnik", - "appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav", - "appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno", - "appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.", + "appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav", + "appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno", + "appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.", "contacts_title": "Stiki", "contacts_noContacts": "Ni stikov.", "contacts_contactsWillAppear": "Stiki se bodo prikazali, ko se naprave oglasijo.", "contacts_searchContacts": "Iskanje stikov...", "contacts_noUnreadContacts": "Ne prebrani stiki.", "contacts_noContactsFound": "Stiki niso najdeni.", - "contacts_deleteContact": "Izbriši stik", - "contacts_removeConfirm": "Izbrišem {contactName} iz stikov?", + "contacts_deleteContact": "IzbriÅ¡i stik", + "contacts_removeConfirm": "IzbriÅ¡em {contactName} iz stikov?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -273,8 +273,8 @@ "contacts_roomLogin": "Prijava v sobo", "contacts_openChat": "Odpri klepet", "contacts_editGroup": "Uredi skupino", - "contacts_deleteGroup": "Izbriši skupino", - "contacts_deleteGroupConfirm": "Izbriši {groupName}?", + "contacts_deleteGroup": "IzbriÅ¡i skupino", + "contacts_deleteGroupConfirm": "IzbriÅ¡i {groupName}?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -285,7 +285,7 @@ "contacts_newGroup": "Nova skupina", "contacts_groupName": "Ime skupine", "contacts_groupNameRequired": "Ime skupine je obvezno.", - "contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja", + "contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,10 +294,10 @@ } }, "contacts_filterContacts": "Filtriraj stik\\,...", - "contacts_noContactsMatchFilter": "Noben stik ne ustreza vašemu kriteriju.", - "contacts_noMembers": "Ni članov.", + "contacts_noContactsMatchFilter": "Noben stik ne ustreza vaÅ¡emu kriteriju.", + "contacts_noMembers": "Ni članov.", "contacts_lastSeenNow": "Nazadnje viden zdaj", - "contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami", + "contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.", - "contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami", + "contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.", + "contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem", - "contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem", + "contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem", + "contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -324,9 +324,9 @@ } }, "channels_title": "Kanali", - "channels_noChannelsConfigured": "Kanali še niso konfigurirani", + "channels_noChannelsConfigured": "Kanali Å¡e niso konfigurirani", "channels_addPublicChannel": "Dodaj javni kanal", - "channels_searchChannels": "Poišči kanale...", + "channels_searchChannels": "Poišči kanale...", "channels_noChannelsFound": "Ne najdem kanalov.", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { @@ -342,10 +342,10 @@ "channels_publicChannel": "Javni kanal", "channels_privateChannel": "Zasebni kanal", "channels_editChannel": "Uredi kanal", - "channels_muteChannel": "Utišaj kanal", + "channels_muteChannel": "UtiÅ¡aj kanal", "channels_unmuteChannel": "Vklopi obvestila kanala", - "channels_deleteChannel": "Pošlji kanal", - "channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.", + "channels_deleteChannel": "PoÅ¡lji kanal", + "channels_deleteChannelConfirm": "IzbriÅ¡em \"{name}\"? To se ne da povrniti.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -366,8 +366,8 @@ "channels_channelName": "Ime kanala", "channels_usePublicChannel": "Uporabi javni kanal", "channels_standardPublicPsk": "Standardni javni PSK", - "channels_pskHex": "PSK (Šestnajstbinska)", - "channels_generateRandomPsk": "Generiraj naključni PSK", + "channels_pskHex": "PSK (Å estnajstbinska)", + "channels_generateRandomPsk": "Generiraj naključni PSK", "channels_enterChannelName": "Vnesi ime kanala", "channels_pskMustBe32Hex": "PSK mora biti 32 heksadecimalnih znakov.", "channels_channelAdded": "Kanal \"{name}\" dodan", @@ -397,13 +397,13 @@ }, "channels_publicChannelAdded": "javna skupnost dodana", "channels_sortBy": "Sortiraj po", - "channels_sortManual": "Ročno", + "channels_sortManual": "Ročno", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Najnovejše sporočilo", - "channels_sortUnread": "Nerešeno", - "chat_noMessages": "Še ni sporočil.", - "chat_sendMessageToStart": "Pošlji sporočilo za začetek.", - "chat_originalMessageNotFound": "Opozorilo: Sporočilo ni bilo najdeno", + "channels_sortLatestMessages": "NajnovejÅ¡e sporočilo", + "channels_sortUnread": "NereÅ¡eno", + "chat_noMessages": "Å e ni sporočil.", + "chat_sendMessageToStart": "PoÅ¡lji sporočilo za začetek.", + "chat_originalMessageNotFound": "Opozorilo: Sporočilo ni bilo najdeno", "chat_replyingTo": "Odgovarjanje {name}", "@chat_replyingTo": { "placeholders": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "Odpošlji odgovor {name}", + "chat_replyTo": "OdpoÅ¡lji odgovor {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,7 +421,7 @@ } }, "chat_location": "Lokacija", - "chat_sendMessageTo": "Pošlji sporočilo {contactName}", + "chat_sendMessageTo": "PoÅ¡lji sporočilo {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "Vnesi sporočilo...", - "chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).", + "chat_typeMessage": "Vnesi sporočilo...", + "chat_messageTooLong": "PoÅ¡iljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,8 +438,8 @@ } } }, - "chat_messageCopied": "Sporočilo poslano", - "chat_messageDeleted": "Sporočilo izbrisano", + "chat_messageCopied": "Sporočilo poslano", + "chat_messageDeleted": "Sporočilo izbrisano", "chat_retryingMessage": "Ponovni poskus.", "chat_retryCount": "Ponovit {current}/{max}", "@chat_retryCount": { @@ -452,7 +452,7 @@ } } }, - "chat_sendGif": "Pošlji GIF", + "chat_sendGif": "PoÅ¡lji GIF", "chat_reply": "Odgovori", "chat_addReaction": "Dodaj reakcijo", "chat_me": "jaz", @@ -461,22 +461,22 @@ "emojiCategoryHearts": "Srce", "emojiCategoryObjects": "Predmeti", "gifPicker_title": "Izberi GIF", - "gifPicker_searchHint": "Išči GIF-e...", + "gifPicker_searchHint": "Išči GIF-e...", "gifPicker_poweredBy": "Napredno z GIPHY", "gifPicker_noGifsFound": "Ne najdem GIF-ov.", - "gifPicker_failedLoad": "Neuspešno nalaganje GIF-a", - "gifPicker_failedSearch": "Iskanje neuspešno.", + "gifPicker_failedLoad": "NeuspeÅ¡no nalaganje GIF-a", + "gifPicker_failedSearch": "Iskanje neuspeÅ¡no.", "gifPicker_noInternet": "Ni internetne povezave", "debugLog_appTitle": "Log zapiske aplikacije", "debugLog_bleTitle": "Log zapis BLE", "debugLog_copyLog": "Kopiraj dnevnik", - "debugLog_clearLog": "Briši log", - "debugLog_copied": "Beležka kopirana.", - "debugLog_bleCopied": "Kopirana beležka iz BLE", + "debugLog_clearLog": "BriÅ¡i log", + "debugLog_copied": "Beležka kopirana.", + "debugLog_bleCopied": "Kopirana beležka iz BLE", "debugLog_noEntries": "Ni ustvarjenih debug zapisov.", - "debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije", + "debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije", "debugLog_frames": "Okvirji", - "debugLog_rawLogRx": "Svež Log-RX", + "debugLog_rawLogRx": "Svež Log-RX", "debugLog_noBleActivity": "Ni BLE aktivnosti.", "debugFrame_length": "Izhodni rob: {count} bajtov", "@debugFrame_length": { @@ -495,7 +495,7 @@ } }, "debugFrame_textMessageHeader": "Obvestilo:", - "debugFrame_destinationPubKey": "- Destinirano Ključno Besedilo: {pubKey}", + "debugFrame_destinationPubKey": "- Destinirano Ključno Besedilo: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- Časovnik: {timestamp}", + "debugFrame_timestamp": "- ÄŒasovnik: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -542,11 +542,11 @@ }, "debugFrame_hexDump": "Hex Dump:", "chat_pathManagement": "Upravljanje poti", - "chat_routingMode": "Navodilo za usmerjevalni način", + "chat_routingMode": "Navodilo za usmerjevalni način", "chat_autoUseSavedPath": "Avto (uporabi shranjeno pot)", - "chat_forceFloodMode": "Nasilje obvezati v način", + "chat_forceFloodMode": "Nasilje obvezati v način", "chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):", - "chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.", + "chat_pathHistoryFull": "Zapiske o poti so popolni. IzbriÅ¡i vnose, da dodaÅ¡ nove.", "chat_hopSingular": "skok", "chat_hopPlural": "skokov", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -557,19 +557,19 @@ } } }, - "chat_successes": "Uspešni", - "chat_removePath": "Izbriši pot", - "chat_noPathHistoryYet": "Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.", + "chat_successes": "UspeÅ¡ni", + "chat_removePath": "IzbriÅ¡i pot", + "chat_noPathHistoryYet": "Ni shranjenih poti.\nPoÅ¡lji sporočilo za odkrivanje poti.", "chat_pathActions": "Potni ukazi:", "chat_setCustomPath": "Nastavi Prilozeno Pot", - "chat_setCustomPathSubtitle": "Ročno določite potniško pot.", - "chat_clearPath": "Počisti pot", - "chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.", - "chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.", + "chat_setCustomPathSubtitle": "Ročno določite potniÅ¡ko pot.", + "chat_clearPath": "Počisti pot", + "chat_clearPathSubtitle": "Ob naslednji poÅ¡iljanju znova zbrati.", + "chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.", "chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.", - "chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.", + "chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.", "chat_fullPath": "Polna pot", - "chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.", + "chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.", "chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -581,15 +581,15 @@ } } }, - "chat_pathSavedLocally": "Shrano lokalno. Povežite se za sinhronizacijo.", + "chat_pathSavedLocally": "Shrano lokalno. Povežite se za sinhronizacijo.", "chat_pathDeviceConfirmed": "Naprave potrjeno.", - "chat_pathDeviceNotConfirmed": "Naprave še niso potrdile.", + "chat_pathDeviceNotConfirmed": "Naprave Å¡e niso potrdile.", "chat_type": "Vnesite", "chat_path": "Pot", - "chat_publicKey": "Ključ javnega tipa", - "chat_compressOutgoingMessages": "Stisnite izhodne sporočila", + "chat_publicKey": "Ključ javnega tipa", + "chat_compressOutgoingMessages": "Stisnite izhodne sporočila", "chat_floodForced": "Porolni (nasilje).", - "chat_directForced": "Nezglašen (nasilje)", + "chat_directForced": "NezglaÅ¡en (nasilje)", "chat_hopsForced": "{count} skoki (nasilje)", "@chat_hopsForced": { "placeholders": { @@ -600,8 +600,8 @@ }, "chat_floodAuto": "Preplavljenje (avtomatizirano)", "chat_direct": "Neposredni", - "chat_poiShared": "Deljeno točke MN", - "chat_unread": "Nerešeno: {count}", + "chat_poiShared": "Deljeno točke MN", + "chat_unread": "NereÅ¡eno: {count}", "@chat_unread": { "placeholders": { "count": { @@ -610,9 +610,9 @@ } }, "chat_openLink": "Odpreti povezavo?", - "chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?", + "chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?", "chat_open": "Odpri", - "chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}", + "chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -621,9 +621,9 @@ } }, "chat_invalidLink": "Neveljavna oblika povezave", - "map_title": "Mapa omrežja", - "map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.", - "map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.", + "map_title": "Mapa omrežja", + "map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.", + "map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.", "map_nodesCount": "Omize: {count}", "@map_nodesCount": { "placeholders": { @@ -632,7 +632,7 @@ } } }, - "map_pinsCount": "Žigovi: {count}", + "map_pinsCount": "Žigovi: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -640,25 +640,25 @@ } } }, - "map_chat": "Čistemar", + "map_chat": "ÄŒistemar", "map_repeater": "Ponovitelj", "map_room": "Soba", "map_sensor": "Senzor", - "map_pinDm": "Zavežite (DM)", - "map_pinPrivate": "Zasebno označit", + "map_pinDm": "Zavežite (DM)", + "map_pinPrivate": "Zasebno označit", "map_pinPublic": "Oznaka (javna)", - "map_lastSeen": "Zadnjič Zazet", - "map_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", + "map_lastSeen": "Zadnjič Zazet", + "map_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", "map_from": "Od", "map_source": "Vir", "map_flags": "Zapestnice", - "map_shareMarkerHere": "Delite točke tukaj.", + "map_shareMarkerHere": "Delite točke tukaj.", "map_pinLabel": "Oznaka za pritrditev", "map_label": "Oznaka", - "map_pointOfInterest": "Točka zanimivosti", - "map_sendToContact": "Pošlji v kontakt", - "map_sendToChannel": "Pošlji v kanal", - "map_noChannelsAvailable": "Nihče kanalov na voljo.", + "map_pointOfInterest": "Točka zanimivosti", + "map_sendToContact": "PoÅ¡lji v kontakt", + "map_sendToChannel": "PoÅ¡lji v kanal", + "map_noChannelsAvailable": "Nihče kanalov na voljo.", "map_publicLocationShare": "Deljenje javne lokacije", "map_publicLocationShareConfirm": "Kljubite boste delili lokacijo v {channelLabel}. Ta kanal je javno dostopen in vsak, ki ima PSK, ga lahko vidi.", "@map_publicLocationShareConfirm": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Povežite se z napravo za deljenje oznak.", - "map_filterNodes": "Filtirirajte člene", + "map_connectToShareMarkers": "Povežite se z napravo za deljenje oznak.", + "map_filterNodes": "Filtirirajte člene", "map_nodeTypes": "Vrste knope", - "map_chatNodes": "Čuti zvezde", + "map_chatNodes": "ÄŒuti zvezde", "map_repeaters": "Ponovljalniki", - "map_otherNodes": "Druge vozlišča", - "map_keyPrefix": "Predpona ključa", - "map_filterByKeyPrefix": "Filtri po predpomniku ključa", - "map_publicKeyPrefix": "Predifika javnega ključa", - "map_markers": "Označitelji", - "map_showSharedMarkers": "Pokaži skupno označenja", + "map_otherNodes": "Druge vozlišča", + "map_keyPrefix": "Predpona ključa", + "map_filterByKeyPrefix": "Filtri po predpomniku ključa", + "map_publicKeyPrefix": "Predifika javnega ključa", + "map_markers": "Označitelji", + "map_showSharedMarkers": "Pokaži skupno označenja", "map_lastSeenTime": "Datum zadnjega vpogleda", "map_sharedPin": "Deljeno naslovno geslo", - "map_joinRoom": "Pridružiti sobo", + "map_joinRoom": "Pridružiti sobo", "map_manageRepeater": "Upravljajte Ponovitve", - "mapCache_title": "Omrezni predpomnilnik zemljeških zemljejevskih slik", - "mapCache_selectAreaFirst": "Izberite območje za prvo predpomnilnik.", - "mapCache_noTilesToDownload": "Nihče slik ne bo naložil za to območje.", - "mapCache_downloadTilesTitle": "Naloži ploščice", - "mapCache_downloadTilesPrompt": "Naložiť {count} plošč za uporabo v režimu brez povezave?", + "mapCache_title": "Omrezni predpomnilnik zemljeÅ¡kih zemljejevskih slik", + "mapCache_selectAreaFirst": "Izberite območje za prvo predpomnilnik.", + "mapCache_noTilesToDownload": "Nihče slik ne bo naložil za to območje.", + "mapCache_downloadTilesTitle": "Naloži ploščice", + "mapCache_downloadTilesPrompt": "NaložiÅ¥ {count} plošč za uporabo v režimu brez povezave?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,8 +695,8 @@ } } }, - "mapCache_downloadAction": "Naloži", - "mapCache_cachedTiles": "Pospešeno shranjeni {count} plošč", + "mapCache_downloadAction": "Naloži", + "mapCache_cachedTiles": "PospeÅ¡eno shranjeni {count} plošč", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Shranjeni {downloaded} ploščad ({failed} neuspešno)", + "mapCache_cachedTilesWithFailed": "Shranjeni {downloaded} ploščad ({failed} neuspeÅ¡no)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Ponovite arhiv za offline način", - "mapCache_clearOfflineCachePrompt": "Izbriši vse predpomnilnikovane kartografske ploščice?", + "mapCache_clearOfflineCacheTitle": "Ponovite arhiv za offline način", + "mapCache_clearOfflineCachePrompt": "IzbriÅ¡i vse predpomnilnikovane kartografske ploščice?", "mapCache_offlineCacheCleared": "Omrezni predpomnik je bil izbrisal.", - "mapCache_noAreaSelected": "Nizona označena površina", + "mapCache_noAreaSelected": "Nizona označena povrÅ¡ina", "mapCache_cacheArea": "Omanski prostor", "mapCache_useCurrentView": "Uporabi trenutni prikaz", - "mapCache_zoomRange": "Občutek razpona", - "mapCache_estimatedTiles": "Predvideni ploščadi: {count}", + "mapCache_zoomRange": "Občutek razpona", + "mapCache_estimatedTiles": "Predvideni ploščadi: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Naloženo {completed} / {total}", + "mapCache_downloadedTiles": "Naloženo {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "Naloži ploščice", + "mapCache_downloadTilesButton": "Naloži ploščice", "mapCache_clearCacheButton": "Ponoviti arhiv", - "mapCache_failedDownloads": "Poslovniški izniki: {count}", + "mapCache_failedDownloads": "PoslovniÅ¡ki izniki: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -802,9 +802,9 @@ "time_month": "mesec", "time_months": "mesi", "time_minutes": "minute", - "time_allTime": "Vse časovno obdobje", + "time_allTime": "Vse časovno obdobje", "dialog_disconnect": "Odklopiti", - "dialog_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", + "dialog_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", "login_repeaterLogin": "Ponovni vnos", "login_roomLogin": "Vnos v sobo", "login_password": "Geslo", @@ -814,12 +814,12 @@ "login_repeaterDescription": "Vnesite geslo za ponovljalnik, da dostopite do nastavitev in statusa.", "login_roomDescription": "Vnesite geslo v sobo za dostop do nastavitev in statusa.", "login_routing": "Usmerjanje", - "login_routingMode": "Navodilo za usmerjevalni način", + "login_routingMode": "Navodilo za usmerjevalni način", "login_autoUseSavedPath": "Avto (uporabi shranjeno pot)", - "login_forceFloodMode": "Nasilje obvezati v način", - "login_managePaths": "Upravljajte Potniške Proti", + "login_forceFloodMode": "Nasilje obvezati v način", + "login_managePaths": "Upravljajte PotniÅ¡ke Proti", "login_login": "Prijava", - "login_attempt": "Poskušajo {current}/{max}", + "login_attempt": "PoskuÅ¡ajo {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Prijava je bila neuspešna: {error}", + "login_failed": "Prijava je bila neuspeÅ¡na: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,8 +838,8 @@ } } }, - "login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.", - "common_reload": "Ponovno naloži", + "login_failedMessage": "Prijava je bila neuspeÅ¡na. Geslo je napačno ali pa je repetitor nedosegljiv.", + "common_reload": "Ponovno naloži", "common_clear": "Ponoviti", "path_currentPath": "Trenutna pot: {path}", "@path_currentPath": { @@ -859,14 +859,14 @@ }, "path_enterCustomPath": "Vnesite prilagojeno pot", "path_currentPathLabel": "Trenutna pot", - "path_hexPrefixInstructions": "Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.", - "path_hexPrefixExample": "Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)", - "path_labelHexPrefixes": "Pot (heksafixne skrajšave)", + "path_hexPrefixInstructions": "Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.", + "path_hexPrefixExample": "Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)", + "path_labelHexPrefixes": "Pot (heksafixne skrajÅ¡ave)", "path_helperMaxHops": "Maksimalno 64 skokov. Vsak prefiks je 2 heksadecimalna znamenja (1 bajt).", "path_selectFromContacts": "Izberi iz kontaktov:", - "path_noRepeatersFound": "Ne najdenih ponoviteljev ali strežnikov sob.", - "path_customPathsRequire": "Prilojene poti zahtevajo medhodne prenose, ki lahko prenašajo sporočila.", - "path_invalidHexPrefixes": "Neveljačni šesteročlenski prefiksi: {prefixes}", + "path_noRepeatersFound": "Ne najdenih ponoviteljev ali strežnikov sob.", + "path_customPathsRequire": "Prilojene poti zahtevajo medhodne prenose, ki lahko prenaÅ¡ajo sporočila.", + "path_invalidHexPrefixes": "Neveljačni Å¡esteročlenski prefiksi: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,7 +874,7 @@ } } }, - "path_tooLong": "Pot je prevelika. Dovoljeno največ 64 skokov.", + "path_tooLong": "Pot je prevelika. Dovoljeno največ 64 skokov.", "path_setPath": "Nastavi Pot", "repeater_management": "Upravljanje ponovitve", "repeater_managementTools": "Upravne orodje", @@ -883,17 +883,17 @@ "repeater_telemetry": "Telemetrija", "repeater_telemetrySubtitle": "Pogledate telemetrijo senzorjev in sistemske statistike", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Pošlji ukazne povelje na ponovitveno enoto.", + "repeater_cliSubtitle": "PoÅ¡lji ukazne povelje na ponovitveno enoto.", "repeater_settings": "Nastavitve", "repeater_settingsSubtitle": "Konfigurirajte parametre ponovitelja", "repeater_statusTitle": "Status ponovitelja", - "repeater_routingMode": "Navodilo za usmerjevalni način", + "repeater_routingMode": "Navodilo za usmerjevalni način", "repeater_autoUseSavedPath": "Avto (uporabi shranjeno pot)", - "repeater_forceFloodMode": "Nasilje obvezati v način", + "repeater_forceFloodMode": "Nasilje obvezati v način", "repeater_pathManagement": "Upravljanje poti", "repeater_refresh": "Ponovno obnavljati", "repeater_statusRequestTimeout": "Zahtev statusa je iztekla.", - "repeater_errorLoadingStatus": "Napaka pri obnašanju: {error}", + "repeater_errorLoadingStatus": "Napaka pri obnaÅ¡anju: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -904,17 +904,17 @@ "repeater_systemInformation": "Informacije o sistemu", "repeater_battery": "Baterija", "repeater_clockAtLogin": "Ure (pri prijavi)", - "repeater_uptime": "Čas delovanja", - "repeater_queueLength": "Dolžina čakalne vrste", + "repeater_uptime": "ÄŒas delovanja", + "repeater_queueLength": "Dolžina čakalne vrste", "repeater_debugFlags": "Nastavitve odpravilnosti", "repeater_radioStatistics": "Radio Statistika", "repeater_lastRssi": "Potredno RSSI", - "repeater_lastSnr": "Nazadnje zabeležena SNR", - "repeater_noiseFloor": "Šumovita raven", + "repeater_lastSnr": "Nazadnje zabeležena SNR", + "repeater_noiseFloor": "Å umovita raven", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", "repeater_packetStatistics": "Statistika paketa", - "repeater_sent": "Pošljeno", + "repeater_sent": "PoÅ¡ljeno", "repeater_received": "Prejeto", "repeater_duplicates": "Duplikati", "repeater_daysHoursMinsSecs": "{days} dni {hours}h {minutes}m {seconds}s", @@ -987,27 +987,27 @@ "repeater_repeaterNameHelper": "Prikaz imena za ta ponovitelj.", "repeater_adminPassword": "Admin geslo", "repeater_adminPasswordHelper": "Polni dostopno geslo", - "repeater_guestPassword": "Geslo gostača", + "repeater_guestPassword": "Geslo gostača", "repeater_guestPasswordHelper": "Odpovedni dostopni geslo", "repeater_radioSettings": "Nastavitve Radija", "repeater_frequencyMhz": "Frekvenca (MHz)", "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX Moč", + "repeater_txPower": "TX Moč", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Pasovna širina", - "repeater_spreadingFactor": "Razširitveni faktor", + "repeater_bandwidth": "Pasovna Å¡irina", + "repeater_spreadingFactor": "RazÅ¡iritveni faktor", "repeater_codingRate": "Programska hitrost", "repeater_locationSettings": "Nastavitve lokacije", - "repeater_latitude": "Širina", + "repeater_latitude": "Å irina", "repeater_latitudeHelper": "Desetbinske protiure (npr. 37.7749)", - "repeater_longitude": "Dolžina", + "repeater_longitude": "Dolžina", "repeater_longitudeHelper": "Desetbinske protiure (npr. -122,4194)", - "repeater_features": "Značilnosti", + "repeater_features": "Značilnosti", "repeater_packetForwarding": "Usmerjanje paketa", - "repeater_packetForwardingSubtitle": "Omogoči ponovitelja za usmerjanje paketov.", + "repeater_packetForwardingSubtitle": "Omogoči ponovitelja za usmerjanje paketov.", "repeater_guestAccess": "Prijemnik", - "repeater_guestAccessSubtitle": "Omogoči dostop gostom v samo bralni načinu.", - "repeater_privacyMode": "Privatni način", + "repeater_guestAccessSubtitle": "Omogoči dostop gostom v samo bralni načinu.", + "repeater_privacyMode": "Privatni način", "repeater_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", "repeater_advertisementSettings": "Nastavitve oglasnika", "repeater_localAdvertInterval": "Lokalen Oglasovni Razpon", @@ -1028,17 +1028,17 @@ } } }, - "repeater_encryptedAdvertInterval": "Šifrirana Oglasovalska Trajanje", + "repeater_encryptedAdvertInterval": "Å ifrirana Oglasovalska Trajanje", "repeater_dangerZone": "Opozorilo", "repeater_rebootRepeater": "Ponovni zagon Repeaterja", "repeater_rebootRepeaterSubtitle": "Ponovni zagon ponovitelja.", - "repeater_rebootRepeaterConfirm": "Ste prepričani, da želite ponovno zagon tega ponovitelja?", - "repeater_regenerateIdentityKey": "Ponovite Ključ Identnosti", - "repeater_regenerateIdentityKeySubtitle": "Ustvarite novo par javnih/zasebnih ključev", + "repeater_rebootRepeaterConfirm": "Ste prepričani, da želite ponovno zagon tega ponovitelja?", + "repeater_regenerateIdentityKey": "Ponovite Ključ Identnosti", + "repeater_regenerateIdentityKeySubtitle": "Ustvarite novo par javnih/zasebnih ključev", "repeater_regenerateIdentityKeyConfirm": "To bo ustvaril novo identiteto za ponavljalnik. Prijavite se?", - "repeater_eraseFileSystem": "Počisti Sustav Vajah", + "repeater_eraseFileSystem": "Počisti Sustav Vajah", "repeater_eraseFileSystemSubtitle": "Oblikuj datoteko ponovitve sistema", - "repeater_eraseFileSystemConfirm": "OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!", + "repeater_eraseFileSystemConfirm": "OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!", "repeater_eraseSerialOnly": "Brisanje je na voljo samo preko serijske konzole.", "repeater_commandSent": "Navodilo poslano: {command}", "@repeater_commandSent": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Napaka pri pošiljanju ukaznega: {error}", + "repeater_errorSendingCommand": "Napaka pri poÅ¡iljanju ukaznega: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1057,7 +1057,7 @@ } }, "repeater_confirm": "Potrdit", - "repeater_settingsSaved": "Nastavitve so shranjene uspešno.", + "repeater_settingsSaved": "Nastavitve so shranjene uspeÅ¡no.", "repeater_errorSavingSettings": "Napaka pri shranjevanju nastavitev: {error}", "@repeater_errorSavingSettings": { "placeholders": { @@ -1068,11 +1068,11 @@ }, "repeater_refreshBasicSettings": "Ponovno nastavi osnovne nastavitve", "repeater_refreshRadioSettings": "Ponovno Nastavitve Radija", - "repeater_refreshTxPower": "Ponovno nastavi TX moč", + "repeater_refreshTxPower": "Ponovno nastavi TX moč", "repeater_refreshLocationSettings": "Ponovno Nastavi Nastavitve Lokacije", "repeater_refreshPacketForwarding": "Ponovno nastavitve usmerjevanja paketa", "repeater_refreshGuestAccess": "Ponovno nastavitve dostopa gostov", - "repeater_refreshPrivacyMode": "Ponovno aktiviraj način zasebnosti", + "repeater_refreshPrivacyMode": "Ponovno aktiviraj način zasebnosti", "repeater_refreshAdvertisementSettings": "Ponovno nastavi Oglede Oglasi", "repeater_refreshed": "{label} je bil/a posodobljen/a", "@repeater_refreshed": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Napaka pri osveževanju {label}", + "repeater_errorRefreshing": "Napaka pri osveževanju {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,13 +1091,13 @@ } }, "repeater_cliTitle": "Ponovitelj CLI", - "repeater_debugNextCommand": "Popravi naslednje ukazne možnosti", + "repeater_debugNextCommand": "Popravi naslednje ukazne možnosti", "repeater_commandHelp": "Pomoc", "repeater_clearHistory": "Ponovi zgodovino", - "repeater_noCommandsSent": "Niti ena ukazne povratne informacije še ni poslana.", + "repeater_noCommandsSent": "Niti ena ukazne povratne informacije Å¡e ni poslana.", "repeater_typeCommandOrUseQuick": "Vnesite ukaz spodaj ali uporabite hitre ukaze", "repeater_enterCommandHint": "Vnesite ukaz...", - "repeater_previousCommand": "Prejšnji ukaz", + "repeater_previousCommand": "PrejÅ¡nji ukaz", "repeater_nextCommand": "Naslednja ukazna", "repeater_enterCommandFirst": "Vnesite ukaz najprej", "repeater_cliCommandFrameTitle": "Okno ukazne vrstice", @@ -1113,66 +1113,66 @@ "repeater_cliQuickGetRadio": "Dobiti Radiopravo", "repeater_cliQuickGetTx": "Pridobi TX", "repeater_cliQuickNeighbors": "Sosedi", - "repeater_cliQuickVersion": "Različica", + "repeater_cliQuickVersion": "Različica", "repeater_cliQuickAdvertise": "Oglasite", "repeater_cliQuickClock": "Ura", - "repeater_cliHelpAdvert": "Pošlje paket oglasov", + "repeater_cliHelpAdvert": "PoÅ¡lje paket oglasov", "repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)", - "repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.", + "repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.", "repeater_cliHelpPassword": "Nastavi novo administracijsko geslo za naprave.", - "repeater_cliHelpVersion": "Prikaže različico naprave in datum izrabe strojne opreme.", - "repeater_cliHelpClearStats": "Ponastavi različne statistične števke na nič.", - "repeater_cliHelpSetAf": "Nastavi časovni koeficient.", - "repeater_cliHelpSetTx": "Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)", - "repeater_cliHelpSetRepeat": "Omogoči ali onemogoči vlogo ponovitelja za tono.", - "repeater_cliHelpSetAllowReadOnly": "(Osebni strežnik) Če je 'vklopljeno', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).", - "repeater_cliHelpSetFloodMax": "Nastavi največjo število skokov za vstopne poplave (če je >= maks, paket ni usmerjen)", - "repeater_cliHelpSetIntThresh": "Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.", - "repeater_cliHelpSetAgcResetInterval": "Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.", - "repeater_cliHelpSetMultiAcks": "Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".", - "repeater_cliHelpSetAdvertInterval": "Nastavi časovno obmesto v minutah za pošiljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.", - "repeater_cliHelpSetFloodAdvertInterval": "Nastavi časovno obmesto v urah za pošiljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.", - "repeater_cliHelpSetGuestPassword": "Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi pošiljajo zahtevo \"Get Stats\")", + "repeater_cliHelpVersion": "Prikaže različico naprave in datum izrabe strojne opreme.", + "repeater_cliHelpClearStats": "Ponastavi različne statistične Å¡tevke na nič.", + "repeater_cliHelpSetAf": "Nastavi časovni koeficient.", + "repeater_cliHelpSetTx": "Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)", + "repeater_cliHelpSetRepeat": "Omogoči ali onemogoči vlogo ponovitelja za tono.", + "repeater_cliHelpSetAllowReadOnly": "(Osebni strežnik) ÄŒe je 'vklopljeno', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).", + "repeater_cliHelpSetFloodMax": "Nastavi največjo Å¡tevilo skokov za vstopne poplave (če je >= maks, paket ni usmerjen)", + "repeater_cliHelpSetIntThresh": "Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.", + "repeater_cliHelpSetAgcResetInterval": "Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.", + "repeater_cliHelpSetMultiAcks": "Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".", + "repeater_cliHelpSetAdvertInterval": "Nastavi časovno obmesto v minutah za poÅ¡iljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.", + "repeater_cliHelpSetFloodAdvertInterval": "Nastavi časovno obmesto v urah za poÅ¡iljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.", + "repeater_cliHelpSetGuestPassword": "Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi poÅ¡iljajo zahtevo \"Get Stats\")", "repeater_cliHelpSetName": "Nastavi ime oglasnika.", - "repeater_cliHelpSetLat": "Nastavi zemljepisno širino oglaševalskega zemljevida (desetdeljne).", - "repeater_cliHelpSetLon": "Nastavi zemljevidno širino oglasnika. (desetdelne stopnje)", + "repeater_cliHelpSetLat": "Nastavi zemljepisno Å¡irino oglaÅ¡evalskega zemljevida (desetdeljne).", + "repeater_cliHelpSetLon": "Nastavi zemljevidno Å¡irino oglasnika. (desetdelne stopnje)", "repeater_cliHelpSetRadio": "Nastavi popolnoma nove radijske parametre in jih shranjuje v nastavitve. Za uporabo je potrebna \"restart\" ukaz.", - "repeater_cliHelpSetRxDelay": "Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.", - "repeater_cliHelpSetTxDelay": "Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjša verjetnost kolizij)", - "repeater_cliHelpSetDirectTxDelay": "Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.", - "repeater_cliHelpSetBridgeEnabled": "Omogoči/Preklopi most.", + "repeater_cliHelpSetRxDelay": "Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.", + "repeater_cliHelpSetTxDelay": "Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjÅ¡a verjetnost kolizij)", + "repeater_cliHelpSetDirectTxDelay": "Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.", + "repeater_cliHelpSetBridgeEnabled": "Omogoči/Preklopi most.", "repeater_cliHelpSetBridgeDelay": "Nastavi zamik pred ponovnim poslanjem paketov.", "repeater_cliHelpSetBridgeSource": "Izberite, ali bodo most ponavljali prejeto ali poslan paket.", "repeater_cliHelpSetBridgeBaud": "Nastavi hitrost serijske povezave za mostove rs232.", "repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.", - "repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).", - "repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).", - "repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).", - "repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow", - "repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.", - "repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.", - "repeater_cliHelpLogErase": "Izbriše pakete zapisov iz datotek sistema.", - "repeater_cliHelpNeighbors": "Prikaže seznam drugih ponovnih knopov, do katerih je prišlo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Izbriše prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.", + "repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).", + "repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).", + "repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).", + "repeater_cliHelpGetBridgeType": "DobrodoÅ¡li pri izbiri vrste mostu: brez, rs232, espnow", + "repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.", + "repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.", + "repeater_cliHelpLogErase": "IzbriÅ¡e pakete zapisov iz datotek sistema.", + "repeater_cliHelpNeighbors": "Prikaže seznam drugih ponovnih knopov, do katerih je priÅ¡lo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "IzbriÅ¡e prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.", "repeater_cliHelpRegion": "(Serija samo) Navaja vse definirane regije in trenutne poplave dovolilnosti.", - "repeater_cliHelpRegionLoad": "Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s pošiljanjem praznega reda/ukaza.", - "repeater_cliHelpRegionGet": "Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) 'F'\"", + "repeater_cliHelpRegionLoad": "Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s poÅ¡iljanjem praznega reda/ukaza.", + "repeater_cliHelpRegionGet": "Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) 'F'\"", "repeater_cliHelpRegionPut": "Dodaja ali posodobi regijsko definicijo s podanim imenom.", - "repeater_cliHelpRegionRemove": "Izbriše definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)", + "repeater_cliHelpRegionRemove": "IzbriÅ¡e definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)", "repeater_cliHelpRegionAllowf": "Nastavi dovoljenje 'Nere' za podano regijo. ('' za globalni/dedni obseg)", - "repeater_cliHelpRegionDenyf": "Odstrani dovoljenje 'F'lood' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)", - "repeater_cliHelpRegionHome": "Odgovori z trenutnim 'domovim' območjem. (Opomba je bila še nujno uporabljena, rezervirano za prihodnost)", + "repeater_cliHelpRegionDenyf": "Odstrani dovoljenje 'F'lood' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)", + "repeater_cliHelpRegionHome": "Odgovori z trenutnim 'domovim' območjem. (Opomba je bila Å¡e nujno uporabljena, rezervirano za prihodnost)", "repeater_cliHelpRegionHomeSet": "Nastavi regijo 'domov'.", "repeater_cliHelpRegionSave": "Shrani seznam/ zemljevzemi regij v shranjevanje.", - "repeater_cliHelpGps": "Pokaže status GPS-ja. Če je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in štetjem satelitiv.", - "repeater_cliHelpGpsOnOff": "Omogoči/onameni GPS način delovanja.", - "repeater_cliHelpGpsSync": "Sinhronizira čas časa ničala z gps uro.", - "repeater_cliHelpGpsSetLoc": "Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.", - "repeater_cliHelpGpsAdvert": "Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaševati lokacijo shranjeno v preferencah", - "repeater_cliHelpGpsAdvertSet": "Nastavi konfiguracijo oglasa na določenem mestu.", + "repeater_cliHelpGps": "Pokaže status GPS-ja. ÄŒe je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in Å¡tetjem satelitiv.", + "repeater_cliHelpGpsOnOff": "Omogoči/onameni GPS način delovanja.", + "repeater_cliHelpGpsSync": "Sinhronizira čas časa ničala z gps uro.", + "repeater_cliHelpGpsSetLoc": "Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.", + "repeater_cliHelpGpsAdvert": "Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaÅ¡evati lokacijo shranjeno v preferencah", + "repeater_cliHelpGpsAdvertSet": "Nastavi konfiguracijo oglasa na določenem mestu.", "repeater_commandsListTitle": "Seznam ukazov", - "repeater_commandsListNote": "Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".", - "repeater_general": "Općenito", + "repeater_commandsListNote": "Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".", + "repeater_general": "Općenito", "repeater_settingsCategory": "Nastavitve", "repeater_bridge": "Most", "repeater_logging": "Logiranje", @@ -1180,10 +1180,10 @@ "repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za repetitorje)", "repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.", "repeater_gpsManagement": "Upravljanje GPS", - "repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.", - "telemetry_receivedData": "Prejeto Telemetrično podatke", + "repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vpraÅ¡anji, povezanimi z lokacijo.", + "telemetry_receivedData": "Prejeto Telemetrično podatke", "telemetry_requestTimeout": "Zahtev telemetrije je iztekla.", - "telemetry_errorLoading": "Napaka pri obnašanju telemetrije: {error}", + "telemetry_errorLoading": "Napaka pri obnaÅ¡anju telemetrije: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1244,17 +1244,17 @@ } }, "channelPath_title": "Pot do paketa", - "channelPath_viewMap": "Prikaži zemljeznico", + "channelPath_viewMap": "Prikaži zemljeznico", "channelPath_otherObservedPaths": "Drugi opazovani poti", "channelPath_repeaterHops": "Skoki ponovitelja", "channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.", - "channelPath_messageDetails": "Podrobnosti sporočila", - "channelPath_senderLabel": "Pošiljatelj", + "channelPath_messageDetails": "Podrobnosti sporočila", + "channelPath_senderLabel": "PoÅ¡iljatelj", "channelPath_timeLabel": "Ura", "channelPath_repeatsLabel": "Ponovitve", "channelPath_pathLabel": "Pot {index}", "channelPath_observedLabel": "Opazovani", - "channelPath_observedPathTitle": "Opazovana pot {index} • {hops}", + "channelPath_observedPathTitle": "Opazovana pot {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Nihče ni določil lokacije.", + "channelPath_noLocationData": "Nihče ni določil lokacije.", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Pot", "channelPath_observedPathHeader": "Opazovana pot", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1342,10 +1342,10 @@ }, "channelPath_noHopDetailsAvailable": "Niso na voljo podrobnosti o letu.", "channelPath_unknownRepeater": "Nepoznati ponovitelj", - "listFilter_tooltip": "Filtri in vrstiči", + "listFilter_tooltip": "Filtri in vrstiči", "listFilter_sortBy": "Sortiraj po", - "listFilter_latestMessages": "Najnovejše sporočilo", - "listFilter_heardRecently": "Nedavno slišan", + "listFilter_latestMessages": "NajnovejÅ¡e sporočilo", + "listFilter_heardRecently": "Nedavno sliÅ¡an", "listFilter_az": "A-Z", "listFilter_filters": "Filtri", "listFilter_all": "Vse", @@ -1361,23 +1361,23 @@ } } }, - "repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.", + "repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.", "repeater_neighbors": "Sosedi", "neighbors_receivedData": "Prejeto podatke o sosedih", "neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.", - "neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}", + "neighbors_errorLoading": "Napaka pri obnaÅ¡anju sosedov: {error}", "neighbors_repeatersNeighbors": "Ponovitve Sosedi", "neighbors_noData": "Niso na voljo podatki o sosedih.", - "channels_joinPrivateChannel": "Pridružite se zasebni skupini", - "channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.", - "channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.", + "channels_joinPrivateChannel": "Pridružite se zasebni skupini", + "channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.", + "channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.", "channels_createPrivateChannel": "Ustvari zasebno kanal.", - "channels_joinPublicChannel": "Pridružite se javnemu kanalu", - "channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.", - "channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom", - "channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.", + "channels_joinPublicChannel": "Pridružite se javnemu kanalu", + "channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.", + "channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom", + "channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.", "channels_scanQrCode": "Skeniraj QR kodo", - "channels_scanQrCodeComingSoon": "Prihajajoča", + "channels_scanQrCodeComingSoon": "Prihajajoča", "channels_enterHashtag": "Vnesite hashtag", "channels_hashtagHint": "npr. #ekipa", "@neighbors_unknownContact": { @@ -1395,13 +1395,13 @@ } }, "neighbors_unknownContact": "Nepoznano {pubkey}", - "neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.", - "settings_locationGPSEnable": "Omogoči GPS", - "settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.", + "neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.", + "settings_locationGPSEnable": "Omogoči GPS", + "settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.", "settings_locationIntervalSec": "Interval za GPS (Sekunde)", "settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.", - "contacts_manageRoom": "Upravljajte strežnik sobe", - "room_management": "Upravljanje stremlišča", + "contacts_manageRoom": "Upravljajte strežnik sobe", + "room_management": "Upravljanje stremlišča", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1462,32 +1462,32 @@ "community_title": "Skupnost", "common_ok": "V redu", "community_create": "Ustvari skupnost", - "community_joinTitle": "Pridružite se skupnosti", - "community_joinConfirmation": "Želiš se pridružiti skupnosti \"{name}\"?", + "community_joinTitle": "Pridružite se skupnosti", + "community_joinConfirmation": "ŽeliÅ¡ se pridružiti skupnosti \"{name}\"?", "community_scanQr": "Skeniraj QR kode skupnosti", "community_scanInstructions": "Nasmerite kamero s skupnostnim QR kodom.", - "community_showQr": "Pokaži QR kodo", + "community_showQr": "Pokaži QR kodo", "community_publicChannel": "Skupnostna javna", "community_hashtagChannel": "Skupnostni hashtag", "community_name": "Komunitarne ime", "community_enterName": "Vnesite ime skupnosti", - "community_join": "Pridružiti se", + "community_join": "Pridružiti se", "community_created": "Skupnost \"{name}\" je bila ustvarila.", "community_joined": "Prilojen k skupnosti \"{name}\"", "community_qrTitle": "Delite skupnost", - "community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.", - "community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti", + "community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.", + "community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti", "community_invalidQrCode": "Neveljaven QR koden skupnosti", - "community_alreadyMember": "Že član", - "community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.", + "community_alreadyMember": "Že član", + "community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.", "community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja", "community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.", - "community_noCommunities": "Še nobena skupnost se ni pridružila.", - "community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.", + "community_noCommunities": "Å e nobena skupnost se ni pridružila.", + "community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.", "community_manageCommunities": "Upravljanje skupnosti", "community_delete": "Opusti skupnost", "community_deleteConfirm": "Zapusti \"{name}\"?", - "community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.", + "community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1502,7 +1502,7 @@ "community_regularHashtag": "Oznaka s hashtagom", "community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)", "community_communityHashtag": "Skupnostni hashtag", - "community_communityHashtagDesc": "Izključeno za uporabnike skupnosti", + "community_communityHashtagDesc": "Izključeno za uporabnike skupnosti", "community_forCommunity": "Za {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1534,10 +1534,10 @@ }, "community_secretRegenerated": "Geslo za \"{name}\" ponovno ustvarjeno", "community_regenerateSecret": "Ponovno ustvari geslo", - "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.", + "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.", "community_regenerate": "Preberi znova", - "community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}", - "community_updateSecret": "Ažuriraj ključ", + "community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}", + "community_updateSecret": "Ažuriraj ključ", "community_secretUpdated": "Skrivnostno spremembo za \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { @@ -1549,78 +1549,78 @@ "pathTrace_you": "Ti", "pathTrace_failed": "Sledenje poti ni uspelo.", "pathTrace_notAvailable": "Potni sled ni na voljo.", - "pathTrace_refreshTooltip": "Osveži Path Trace.", + "pathTrace_refreshTooltip": "Osveži Path Trace.", "contacts_pathTrace": "Sledenje poti", "contacts_ping": "Pingati", "contacts_repeaterPathTrace": "Sledi poti do ponavljalnika", "contacts_repeaterPing": "Pinguj ponavljalnik", - "contacts_roomPathTrace": "Sledenje poti do strežnika sobe", - "contacts_roomPing": "Ping strežnik sobe", - "contacts_chatTraceRoute": "Slediti poti žarkov", + "contacts_roomPathTrace": "Sledenje poti do strežnika sobe", + "contacts_roomPing": "Ping strežnik sobe", + "contacts_chatTraceRoute": "Slediti poti žarkov", "contacts_pathTraceTo": "Trace route to {name}", - "appSettings_languageRu": "Ruščina", + "appSettings_languageRu": "Ruščina", "appSettings_languageUk": "Ukrajinsko", - "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", - "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", - "contacts_contactImported": "Kontakt je bil uvožen.", - "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", + "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", + "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", + "contacts_contactImported": "Kontakt je bil uvožen.", + "contacts_contactImportFailed": "Kontakt ni bil uspeÅ¡no uvožen.", "contacts_zeroHopAdvert": "Reklama brez posrednikov", - "contacts_floodAdvert": "Poplavna oglás", + "contacts_floodAdvert": "Poplavna oglás", "contacts_invalidAdvertFormat": "Neveljavni kontaktne podatke", - "contacts_clipboardEmpty": "Odložišče je prazno.", - "contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče", - "contacts_addContactFromClipboard": "Dodaj stik iz odložišča", + "contacts_clipboardEmpty": "Odložišče je prazno.", + "contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče", + "contacts_addContactFromClipboard": "Dodaj stik iz odložišča", "contacts_zeroHopContactAdvertSent": "Poslano po oglasu.", - "contacts_zeroHopContactAdvertFailed": "Pošiljanje kontakta ni uspelo.", - "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", - "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", + "contacts_zeroHopContactAdvertFailed": "PoÅ¡iljanje kontakta ni uspelo.", + "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", + "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", - "contacts_ShareContact": "Kopiraj stik v Odložišče", + "contacts_ShareContact": "Kopiraj stik v Odložišče", "notification_activityTitle": "Aktivnost MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}", - "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", + "notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", "notification_newTypeDiscovered": "Odkrito novo {contactType}", - "notification_receivedNewMessage": "Prejeto novo sporočilo", + "notification_receivedNewMessage": "Prejeto novo sporočilo", "settings_gpxExportAll": "Izvozi vse kontakte v GPX", "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", - "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", - "settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX", - "settings_gpxExportError": "Pri izvozu je prišlo do napake.", - "settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe", + "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", + "settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX", + "settings_gpxExportError": "Pri izvozu je priÅ¡lo do napake.", + "settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe", "settings_gpxExportChat": "Lokacije spremljevalcev", "settings_gpxExportAllContacts": "Lokacije vseh stikov", "settings_gpxExportContactsSubtitle": "Izvozi spremljevalce z lokacijo v datoteko GPX.", "settings_gpxExportAllSubtitle": "Izvozi vse kontakte z lokacijo v datoteko GPX.", - "settings_gpxExportSuccess": "Uspešno izvoz GPX datoteke.", - "settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open", + "settings_gpxExportSuccess": "UspeÅ¡no izvoz GPX datoteke.", + "settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open", "settings_gpxExportNoContacts": "Ni stikov za izvoz.", - "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", + "settings_gpxExportNotAvailable": "Ni podprto na vaÅ¡em napravi/operacijskem sistemu", "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte", - "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!", - "map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.", + "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!", + "map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.", "map_removeLast": "Odstrani Zadnji", - "map_runTrace": "Zaženi sledenje poti", - "pathTrace_clearTooltip": "Počisti pot", + "map_runTrace": "Zaženi sledenje poti", + "pathTrace_clearTooltip": "Počisti pot", "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", - "scanner_enableBluetooth": "Omogočite Bluetooth", - "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", + "scanner_enableBluetooth": "Omogočite Bluetooth", + "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", "scanner_chromeRequired": "Zahtevan brskalnik Chrome", "scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.", "scanner_bluetoothOff": "Bluetooth je izklopljen", - "snrIndicator_lastSeen": "Zadnjič videno", - "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", - "chat_ShowAllPaths": "Prikaži vse poti", - "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", - "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", + "snrIndicator_lastSeen": "Zadnjič videno", + "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", + "chat_ShowAllPaths": "Prikaži vse poti", + "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", + "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", "settings_clientRepeat": "Neovadno ponavljanje", - "settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)", + "settings_aboutOpenMeteoAttribution": "Podatki o viÅ¡ini LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Enote", - "appSettings_unitsMetric": "Metrična (m/km)", + "appSettings_unitsMetric": "Metrična (m/km)", "appSettings_unitsImperial": "Imperialno (ft / mi)", "map_lineOfSight": "Linija vida", "map_losScreenTitle": "Linija vida", - "losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.", + "losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.", "losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}", "@losRunFailed": { "placeholders": { @@ -1629,12 +1629,12 @@ } } }, - "losClearAllPoints": "Počisti vse točke", - "losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila", + "losClearAllPoints": "Počisti vse točke", + "losRunToViewElevationProfile": "Zaženite LOS za ogled viÅ¡inskega profila", "losMenuTitle": "LOS meni", - "losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri", - "losShowDisplayNodes": "Pokaži prikazna vozlišča", - "losCustomPoints": "Točke po meri", + "losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri", + "losShowDisplayNodes": "Pokaži prikazna vozlišča", + "losCustomPoints": "Točke po meri", "losCustomPointLabel": "Po meri {index}", "@losCustomPointLabel": { "placeholders": { @@ -1643,8 +1643,8 @@ } } }, - "losPointA": "Točka A", - "losPointB": "Točka B", + "losPointA": "Točka A", + "losPointB": "Točka B", "losAntennaA": "Antena A: {value} {unit}", "@losAntennaA": { "placeholders": { @@ -1667,9 +1667,9 @@ } } }, - "losRun": "Zaženi LOS", - "losNoElevationData": "Ni podatkov o višini", - "losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}", + "losRun": "Zaženi LOS", + "losNoElevationData": "Ni podatkov o viÅ¡ini", + "losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjÅ¡a razdalja {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.", - "losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.", - "losRenameCustomPoint": "Preimenujte točko po meri", - "losPointName": "Ime točke", - "losShowPanelTooltip": "Pokaži ploščo LOS", - "losHidePanelTooltip": "Skrij ploščo LOS", - "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)", + "losErrorElevationUnavailable": "Podatki o nadmorski viÅ¡ini niso na voljo za enega ali več vzorcev.", + "losErrorInvalidInput": "Neveljavni podatki o točkah/viÅ¡ini za izračun LOS.", + "losRenameCustomPoint": "Preimenujte točko po meri", + "losPointName": "Ime točke", + "losShowPanelTooltip": "Pokaži ploščo LOS", + "losHidePanelTooltip": "Skrij ploščo LOS", + "losElevationAttribution": "Podatki o viÅ¡ini: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Radijski horizont", "losLegendLosBeam": "Linija vidnosti", "losLegendTerrain": "Teren", "losFrequencyLabel": "Frekvenca", - "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", - "losFrequencyDialogTitle": "Izračun radijskega horizonta", - "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", + "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", + "losFrequencyDialogTitle": "Izračun radijskega horizonta", + "losFrequencyDialogDescription": "ZačenÅ¡i od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1798,17 +1798,15 @@ }, "contacts_unread": "Neprebrano", "contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...", - "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", + "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", "contacts_searchContactsNoNumber": "Iskanje stikov...", - "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", - "contacts_searchUsers": "Išči {number}{str} uporabnikov...", + "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", + "contacts_searchUsers": "Išči {number}{str} uporabnikov...", "connectionChoiceBluetoothLabel": "Bluetooth", "connectionChoiceUsbLabel": "USB", - "connectionChoiceTitle": "Izberite svoj način povezave.", - "connectionChoiceSubtitle": "Izberite, kako želite dostopati do svojega naprave MeshCore.", - "usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašim MeshCore-om.", - "usbScreenTitle": "Povežite preko USB", + "usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vaÅ¡im MeshCore-om.", + "usbScreenTitle": "Povežite preko USB", "usbScreenStatus": "Izberite USB naprave.", "usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.", - "usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite." + "usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index f3e7dbd..8a74087 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -14,28 +14,28 @@ "nav_map": "Karta", "common_cancel": "Avbryt", "common_connect": "Anslut", - "common_unknownDevice": "Okänd enhet", + "common_unknownDevice": "Okänd enhet", "common_save": "Spara", "common_delete": "Radera", - "common_close": "Stänga", + "common_close": "Stänga", "common_edit": "Redigera", - "common_add": "Lägg till", - "common_settings": "Inställningar", - "common_disconnect": "Koppla från", + "common_add": "Lägg till", + "common_settings": "Inställningar", + "common_disconnect": "Koppla frÃ¥n", "common_connected": "Ansluten", "common_disconnected": "Ansluten", "common_create": "Skapa", - "common_continue": "Fortsätt", + "common_continue": "Fortsätt", "common_share": "Dela", "common_copy": "Kopiera", - "common_retry": "Försök igen", - "common_hide": "Dölj", + "common_retry": "Försök igen", + "common_hide": "Dölj", "common_remove": "Ta bort", "common_enable": "Aktivera", "common_disable": "Inaktivera", "common_reboot": "Start om", "common_loading": "Laddar...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,7 +53,7 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Söker efter enheter...", + "scanner_scanning": "Söker efter enheter...", "scanner_connecting": "Anslutning...", "scanner_disconnecting": "Anslutning bryts...", "scanner_notConnected": "Inte ansluten", @@ -65,8 +65,8 @@ } } }, - "scanner_searchingDevices": "Söker efter MeshCore-enheter...", - "scanner_tapToScan": "Tryck Skanna för att hitta MeshCore-enheter", + "scanner_searchingDevices": "Söker efter MeshCore-enheter...", + "scanner_tapToScan": "Tryck Skanna för att hitta MeshCore-enheter", "scanner_connectionFailed": "Anslutning misslyckades: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -77,49 +77,49 @@ }, "scanner_stop": "Stoppa", "scanner_scan": "Skanna", - "device_quickSwitch": "Snabb växling", + "device_quickSwitch": "Snabb växling", "device_meshcore": "MeshCore", - "settings_title": "Inställningar", + "settings_title": "Inställningar", "settings_deviceInfo": "Enhetens information", - "settings_appSettings": "Appinställningar", - "settings_appSettingsSubtitle": "Meddelanden, notiser och kartinställningar", - "settings_nodeSettings": "Nodinställningar", + "settings_appSettings": "Appinställningar", + "settings_appSettingsSubtitle": "Meddelanden, notiser och kartinställningar", + "settings_nodeSettings": "Nodinställningar", "settings_nodeName": "Nodnamn", "settings_nodeNameNotSet": "Inte angivet", "settings_nodeNameHint": "Ange nodnamn", "settings_nodeNameUpdated": "Namn uppdaterat", - "settings_radioSettings": "Radioinställningar", + "settings_radioSettings": "Radioinställningar", "settings_radioSettingsSubtitle": "Frekvens, effekt, spridningsfaktor", - "settings_radioSettingsUpdated": "Radioinställningarna har uppdaterats", + "settings_radioSettingsUpdated": "Radioinställningarna har uppdaterats", "settings_location": "Plats", "settings_locationSubtitle": "GPS koordinater", "settings_locationUpdated": "Plats uppdaterad", - "settings_locationBothRequired": "Ange både latitud och longitud.", + "settings_locationBothRequired": "Ange bÃ¥de latitud och longitud.", "settings_locationInvalid": "Ogiltig latitud eller longitud.", "settings_latitude": "Latitud", - "settings_longitude": "Längdgrad", - "settings_privacyMode": "Privatläge", - "settings_privacyModeSubtitle": "Dölj namn/plats i annonser", - "settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.", - "settings_privacyModeEnabled": "Privatläget är aktiverat", - "settings_privacyModeDisabled": "Privatläge är avstängt", - "settings_actions": "Åtgärder", + "settings_longitude": "Längdgrad", + "settings_privacyMode": "Privatläge", + "settings_privacyModeSubtitle": "Dölj namn/plats i annonser", + "settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.", + "settings_privacyModeEnabled": "Privatläget är aktiverat", + "settings_privacyModeDisabled": "Privatläge är avstängt", + "settings_actions": "Ã…tgärder", "settings_sendAdvertisement": "Skicka Annons", - "settings_sendAdvertisementSubtitle": "Sändning finns nu", + "settings_sendAdvertisementSubtitle": "Sändning finns nu", "settings_advertisementSent": "Annons skickad", "settings_syncTime": "Synkroniseringstid", - "settings_syncTimeSubtitle": "Ställ enheten till telefonens tid", + "settings_syncTimeSubtitle": "Ställ enheten till telefonens tid", "settings_timeSynchronized": "Tidssynkroniserat", "settings_refreshContacts": "Uppdatera Kontakter", - "settings_refreshContactsSubtitle": "Ladda om kontaktlistan från enheten", + "settings_refreshContactsSubtitle": "Ladda om kontaktlistan frÃ¥n enheten", "settings_rebootDevice": "Starta om enheten", "settings_rebootDeviceSubtitle": "Starta MeshCore-enheten", - "settings_rebootDeviceConfirm": "Är du säker på att du vill starta om enheten? Du kommer att bli avkopplad.", - "settings_debug": "Felsök", - "settings_bleDebugLog": "BLE-felsökning", - "settings_bleDebugLogSubtitle": "BLE-kommandon, svar och rådata", - "settings_appDebugLog": "Appfelsökning", - "settings_appDebugLogSubtitle": "Applikations felsökningsmeddelanden", + "settings_rebootDeviceConfirm": "Är du säker pÃ¥ att du vill starta om enheten? Du kommer att bli avkopplad.", + "settings_debug": "Felsök", + "settings_bleDebugLog": "BLE-felsökning", + "settings_bleDebugLogSubtitle": "BLE-kommandon, svar och rÃ¥data", + "settings_appDebugLog": "Appfelsökning", + "settings_appDebugLogSubtitle": "Applikations felsökningsmeddelanden", "settings_about": "Om", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -129,16 +129,16 @@ } } }, - "settings_aboutLegalese": "2024 MeshCore Öppen Källkodsprojekt", - "settings_aboutDescription": "En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.", + "settings_aboutLegalese": "2024 MeshCore Öppen Källkodsprojekt", + "settings_aboutDescription": "En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.", "settings_infoName": "Namn", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Batteri", - "settings_infoPublicKey": "Allmänt nyckel", + "settings_infoPublicKey": "Allmänt nyckel", "settings_infoContactsCount": "Kontakterantal", "settings_infoChannelCount": "Kanalantal", - "settings_presets": "Fördefinierade inställningar", + "settings_presets": "Fördefinierade inställningar", "settings_frequency": "Frekvens (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)", @@ -156,51 +156,51 @@ } } }, - "appSettings_title": "Appinställningar", + "appSettings_title": "Appinställningar", "appSettings_appearance": "Utseende", "appSettings_theme": "Tema", "appSettings_themeSystem": "Systemstandard", "appSettings_themeLight": "Ljus", - "appSettings_themeDark": "Mörk", - "appSettings_language": "Språk", + "appSettings_themeDark": "Mörk", + "appSettings_language": "SprÃ¥k", "appSettings_languageSystem": "Systemstandard", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Meddelanden", "appSettings_enableNotifications": "Aktivera Notifikationer", - "appSettings_enableNotificationsSubtitle": "Ta emot notiser för meddelanden och reklam", - "appSettings_notificationPermissionDenied": "Tillåtelse för notifikationer nekad", + "appSettings_enableNotificationsSubtitle": "Ta emot notiser för meddelanden och reklam", + "appSettings_notificationPermissionDenied": "TillÃ¥telse för notifikationer nekad", "appSettings_notificationsEnabled": "Notifikationer aktiverade", - "appSettings_notificationsDisabled": "Meddelanden är avstängda", + "appSettings_notificationsDisabled": "Meddelanden är avstängda", "appSettings_messageNotifications": "Meddelandekrav", - "appSettings_messageNotificationsSubtitle": "Visa notis när nya meddelanden tas emot", + "appSettings_messageNotificationsSubtitle": "Visa notis när nya meddelanden tas emot", "appSettings_channelMessageNotifications": "Kanalmeddelandena", - "appSettings_channelMessageNotificationsSubtitle": "Visa notis när meddelanden i kanal mottas", + "appSettings_channelMessageNotificationsSubtitle": "Visa notis när meddelanden i kanal mottas", "appSettings_advertisementNotifications": "Annonsmeddelanden", - "appSettings_advertisementNotificationsSubtitle": "Visa notis när nya noder upptäcks", + "appSettings_advertisementNotificationsSubtitle": "Visa notis när nya noder upptäcks", "appSettings_messaging": "Meddelanden", - "appSettings_clearPathOnMaxRetry": "Rensa Vägen på Max Försök", - "appSettings_clearPathOnMaxRetrySubtitle": "Återställ kontaktväg efter 5 misslyckade försök att skicka", - "appSettings_pathsWillBeCleared": "Sökvägar kommer att tömmas efter 5 misslyckade försök.", - "appSettings_pathsWillNotBeCleared": "Sökvägar kommer inte att rensas automatiskt.", - "appSettings_autoRouteRotation": "Automatisk Rutväxling", - "appSettings_autoRouteRotationSubtitle": "Blixtra mellan bästa vägar och flödesläge", - "appSettings_autoRouteRotationEnabled": "Automatisk ruttrotation är aktiverad", - "appSettings_autoRouteRotationDisabled": "Automatisk ruttrotation är avstängd", + "appSettings_clearPathOnMaxRetry": "Rensa Vägen pÃ¥ Max Försök", + "appSettings_clearPathOnMaxRetrySubtitle": "Ã…terställ kontaktväg efter 5 misslyckade försök att skicka", + "appSettings_pathsWillBeCleared": "Sökvägar kommer att tömmas efter 5 misslyckade försök.", + "appSettings_pathsWillNotBeCleared": "Sökvägar kommer inte att rensas automatiskt.", + "appSettings_autoRouteRotation": "Automatisk Rutväxling", + "appSettings_autoRouteRotationSubtitle": "Blixtra mellan bästa vägar och flödesläge", + "appSettings_autoRouteRotationEnabled": "Automatisk ruttrotation är aktiverad", + "appSettings_autoRouteRotationDisabled": "Automatisk ruttrotation är avstängd", "appSettings_battery": "Batteri", "appSettings_batteryChemistry": "Batterikemi", - "appSettings_batteryChemistryPerDevice": "Ställ in per enhet ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Ställ in per enhet ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Anslut till en enhet för att välja", + "appSettings_batteryChemistryConnectFirst": "Anslut till en enhet för att välja", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", "appSettings_batteryLipo": "LiPo (3.0-4.2V)", "appSettings_mapDisplay": "Kartvisning", - "appSettings_showRepeaters": "Visa återuppslag", - "appSettings_showRepeatersSubtitle": "Visa återspelsnoder på kartan", + "appSettings_showRepeaters": "Visa Ã¥teruppslag", + "appSettings_showRepeatersSubtitle": "Visa Ã¥terspelsnoder pÃ¥ kartan", "appSettings_showChatNodes": "Visa Chattnoder", - "appSettings_showChatNodesSubtitle": "Visa chattnoder på kartan", + "appSettings_showChatNodesSubtitle": "Visa chattnoder pÃ¥ kartan", "appSettings_showOtherNodes": "Visa andra noder", - "appSettings_showOtherNodesSubtitle": "Visa andra nodtyper på kartan", + "appSettings_showOtherNodesSubtitle": "Visa andra nodtyper pÃ¥ kartan", "appSettings_timeFilter": "Tidsfilter", "appSettings_timeFilterShowAll": "Visa alla noder", - "appSettings_timeFilterShowLast": "Visa noder från de senaste {hours} timmarna", + "appSettings_timeFilterShowLast": "Visa noder frÃ¥n de senaste {hours} timmarna", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,15 +230,15 @@ } }, "appSettings_mapTimeFilter": "Karttid Filter", - "appSettings_showNodesDiscoveredWithin": "Visa noder som upptäckts inom:", + "appSettings_showNodesDiscoveredWithin": "Visa noder som upptäckts inom:", "appSettings_allTime": "Totalen", "appSettings_lastHour": "Sista timmen", "appSettings_last6Hours": "De senaste 6 timmarna", "appSettings_last24Hours": "De senaste 24 timmarna", - "appSettings_lastWeek": "Förra veckan", + "appSettings_lastWeek": "Förra veckan", "appSettings_offlineMapCache": "Offline Kartcache", "appSettings_noAreaSelected": "Ingen area markerad", - "appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})", + "appSettings_areaSelectedZoom": "OmrÃ¥de markerat (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,19 +249,19 @@ } } }, - "appSettings_debugCard": "Felsök", - "appSettings_appDebugLogging": "App-felsökning och loggning", - "appSettings_appDebugLoggingSubtitle": "Logga appens felsökningsmeddelanden för felsökning", - "appSettings_appDebugLoggingEnabled": "App felsökning loggning aktiverad", - "appSettings_appDebugLoggingDisabled": "App felsökning är avstängd", + "appSettings_debugCard": "Felsök", + "appSettings_appDebugLogging": "App-felsökning och loggning", + "appSettings_appDebugLoggingSubtitle": "Logga appens felsökningsmeddelanden för felsökning", + "appSettings_appDebugLoggingEnabled": "App felsökning loggning aktiverad", + "appSettings_appDebugLoggingDisabled": "App felsökning är avstängd", "contacts_title": "Kontakter", - "contacts_noContacts": "Inga kontakter ännu", - "contacts_contactsWillAppear": "Kontakter kommer att visas när enheter annonserar.", - "contacts_searchContacts": "Sök kontakter...", - "contacts_noUnreadContacts": "Inga oinlästa kontakter", + "contacts_noContacts": "Inga kontakter ännu", + "contacts_contactsWillAppear": "Kontakter kommer att visas när enheter annonserar.", + "contacts_searchContacts": "Sök kontakter...", + "contacts_noUnreadContacts": "Inga oinlästa kontakter", "contacts_noContactsFound": "Inga kontakter eller grupper hittades.", "contacts_deleteContact": "Ta bort Kontakt", - "contacts_removeConfirm": "Ta bort {contactName} från kontakter?", + "contacts_removeConfirm": "Ta bort {contactName} frÃ¥n kontakter?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -271,7 +271,7 @@ }, "contacts_manageRepeater": "Hantera Upprepare", "contacts_roomLogin": "Rum Inloggning", - "contacts_openChat": "Öppna Chatt", + "contacts_openChat": "Öppna Chatt", "contacts_editGroup": "Redigera Grupp", "contacts_deleteGroup": "Ta bort Grupp", "contacts_deleteGroupConfirm": "Ta bort {groupName}?", @@ -284,7 +284,7 @@ }, "contacts_newGroup": "Ny grupp", "contacts_groupName": "Gruppnamn", - "contacts_groupNameRequired": "Gruppnamnet är obligatoriskt", + "contacts_groupNameRequired": "Gruppnamnet är obligatoriskt", "contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.", "@contacts_groupAlreadyExists": { "placeholders": { @@ -305,7 +305,7 @@ } } }, - "contacts_lastSeenHourAgo": "Senast sedd för 1 timme sedan", + "contacts_lastSeenHourAgo": "Senast sedd för 1 timme sedan", "contacts_lastSeenHoursAgo": "Senast sedd {hours} timmar sedan", "@contacts_lastSeenHoursAgo": { "placeholders": { @@ -314,7 +314,7 @@ } } }, - "contacts_lastSeenDayAgo": "Senast sedd för 1 dag sedan", + "contacts_lastSeenDayAgo": "Senast sedd för 1 dag sedan", "contacts_lastSeenDaysAgo": "Senast synlig {days} dagar sedan", "@contacts_lastSeenDaysAgo": { "placeholders": { @@ -325,8 +325,8 @@ }, "channels_title": "Kanaler", "channels_noChannelsConfigured": "Inga kanaler konfigurerade", - "channels_addPublicChannel": "Lägg till publik kanal", - "channels_searchChannels": "Sök kanaler...", + "channels_addPublicChannel": "Lägg till publik kanal", + "channels_searchChannels": "Sök kanaler...", "channels_noChannelsFound": "Inga kanaler hittades", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { @@ -339,13 +339,13 @@ "channels_hashtagChannel": "Hashtagkanal", "channels_public": "Offentligt", "channels_private": "Privat", - "channels_publicChannel": "Allmänt kanal", + "channels_publicChannel": "Allmänt kanal", "channels_privateChannel": "Privat kanal", "channels_editChannel": "Redigera kanal", "channels_muteChannel": "Tysta kanal", - "channels_unmuteChannel": "Slå på ljud för kanal", + "channels_unmuteChannel": "SlÃ¥ pÃ¥ ljud för kanal", "channels_deleteChannel": "Ta bort kanal", - "channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.", + "channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte Ã¥ngras.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -361,15 +361,15 @@ } } }, - "channels_addChannel": "Lägg till kanal", + "channels_addChannel": "Lägg till kanal", "channels_channelIndexLabel": "Kanalindex", "channels_channelName": "Kanalnamn", - "channels_usePublicChannel": "Använd Publikkanal", - "channels_standardPublicPsk": "Standard allmän PSK", + "channels_usePublicChannel": "Använd Publikkanal", + "channels_standardPublicPsk": "Standard allmän PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Generera slumpmässig PSK", + "channels_generateRandomPsk": "Generera slumpmässig PSK", "channels_enterChannelName": "Ange en kanalnamn", - "channels_pskMustBe32Hex": "PSK måste vara 32 hexadecimala tecken", + "channels_pskMustBe32Hex": "PSK mÃ¥ste vara 32 hexadecimala tecken", "channels_channelAdded": "Kanalen \"{name}\" har lagts till", "@channels_channelAdded": { "placeholders": { @@ -395,14 +395,14 @@ } } }, - "channels_publicChannelAdded": "Allmänt kanal tillagd", + "channels_publicChannelAdded": "Allmänt kanal tillagd", "channels_sortBy": "Sortera efter", "channels_sortManual": "Manuell", "channels_sortAZ": "A-Z", "channels_sortLatestMessages": "Senaste meddelanden", - "channels_sortUnread": "Oläst", - "chat_noMessages": "Inga meddelanden ännu", - "chat_sendMessageToStart": "Skicka ett meddelande för att komma igång", + "channels_sortUnread": "Oläst", + "chat_noMessages": "Inga meddelanden ännu", + "chat_sendMessageToStart": "Skicka ett meddelande för att komma igÃ¥ng", "chat_originalMessageNotFound": "Originalt meddelande hittades inte", "chat_replyingTo": "Svara till {name}", "@chat_replyingTo": { @@ -430,7 +430,7 @@ } }, "chat_typeMessage": "Skriv ett meddelande...", - "chat_messageTooLong": "Meddelandet är för långt (max {maxBytes} byte).", + "chat_messageTooLong": "Meddelandet är för lÃ¥ngt (max {maxBytes} byte).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -440,8 +440,8 @@ }, "chat_messageCopied": "Meddelandet kopierades", "chat_messageDeleted": "Meddelandet raderat", - "chat_retryingMessage": "Försöker igen", - "chat_retryCount": "Försök igen {current}/{max}", + "chat_retryingMessage": "Försöker igen", + "chat_retryCount": "Försök igen {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -454,30 +454,30 @@ }, "chat_sendGif": "Skicka GIF", "chat_reply": "Svara", - "chat_addReaction": "Lägg till reaktion", + "chat_addReaction": "Lägg till reaktion", "chat_me": "Mig", "emojiCategorySmileys": "Emojis", "emojiCategoryGestures": "Gestikuleringar", - "emojiCategoryHearts": "Hjärtan", + "emojiCategoryHearts": "Hjärtan", "emojiCategoryObjects": "Objekt", - "gifPicker_title": "Välj en GIF", - "gifPicker_searchHint": "Sök GIF:ar...", + "gifPicker_title": "Välj en GIF", + "gifPicker_searchHint": "Sök GIF:ar...", "gifPicker_poweredBy": "Drivet av GIPHY", "gifPicker_noGifsFound": "Inga GIF-filer hittades", "gifPicker_failedLoad": "Kunde inte ladda GIF-filer", - "gifPicker_failedSearch": "Sökningen misslyckades.", + "gifPicker_failedSearch": "Sökningen misslyckades.", "gifPicker_noInternet": "Ingen internetanslutning", - "debugLog_appTitle": "Appfelsökning", - "debugLog_bleTitle": "BLE-felsökning", + "debugLog_appTitle": "Appfelsökning", + "debugLog_bleTitle": "BLE-felsökning", "debugLog_copyLog": "Kopiera logg", "debugLog_clearLog": "Rensa logg", - "debugLog_copied": "Felsökningslogg kopierad", + "debugLog_copied": "Felsökningslogg kopierad", "debugLog_bleCopied": "BLE-logg kopierad", - "debugLog_noEntries": "Inga felsökningsloggar ännu", - "debugLog_enableInSettings": "Aktivera appens felsökningsloggning i inställningarna", + "debugLog_noEntries": "Inga felsökningsloggar ännu", + "debugLog_enableInSettings": "Aktivera appens felsökningsloggning i inställningarna", "debugLog_frames": "Rammar", - "debugLog_rawLogRx": "Rå Log-RX", - "debugLog_noBleActivity": "Ingen BLE-aktivitet ännu", + "debugLog_rawLogRx": "RÃ¥ Log-RX", + "debugLog_noBleActivity": "Ingen BLE-aktivitet ännu", "debugFrame_length": "Ramstorlek: {count} byte", "@debugFrame_length": { "placeholders": { @@ -494,8 +494,8 @@ } } }, - "debugFrame_textMessageHeader": "Textmeddelandefält:", - "debugFrame_destinationPubKey": "– Destination PubKey: {pubKey}", + "debugFrame_textMessageHeader": "Textmeddelandefält:", + "debugFrame_destinationPubKey": "– Destination PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- Tidsstämpel: {timestamp}", + "debugFrame_timestamp": "- Tidsstämpel: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -542,11 +542,11 @@ }, "debugFrame_hexDump": "Hexdump:", "chat_pathManagement": "Stigarhantering", - "chat_routingMode": "Ruttläge", - "chat_autoUseSavedPath": "Automatisk (använd sparad sökväg)", - "chat_forceFloodMode": "Tvinga Översvämningsläge", - "chat_recentAckPaths": "Nyligen Ack-vägar (tryck för att använda):", - "chat_pathHistoryFull": "Historisk sökväg är full. Ta bort poster för att lägga till nya.", + "chat_routingMode": "Ruttläge", + "chat_autoUseSavedPath": "Automatisk (använd sparad sökväg)", + "chat_forceFloodMode": "Tvinga Översvämningsläge", + "chat_recentAckPaths": "Nyligen Ack-vägar (tryck för att använda):", + "chat_pathHistoryFull": "Historisk sökväg är full. Ta bort poster för att lägga till nya.", "chat_hopSingular": "hoppa", "chat_hopPlural": "hoppar", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -557,20 +557,20 @@ } } }, - "chat_successes": "framgångar", - "chat_removePath": "Ta bort sökväg", - "chat_noPathHistoryYet": "Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.", + "chat_successes": "framgÃ¥ngar", + "chat_removePath": "Ta bort sökväg", + "chat_noPathHistoryYet": "Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spÃ¥r.", "chat_pathActions": "Stigar:", - "chat_setCustomPath": "Ange anpassad sökväg", - "chat_setCustomPathSubtitle": "Ange ruttväg manuellt", - "chat_clearPath": "Rensa Vägen", - "chat_clearPathSubtitle": "Tvinga fram omstart vid nästa sändning", - "chat_pathCleared": "Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.", - "chat_floodModeSubtitle": "Använd routningsomkopplaren i appraden", - "chat_floodModeEnabled": "Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.", - "chat_fullPath": "Fullständig sökväg", - "chat_pathDetailsNotAvailable": "Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.", - "chat_pathSetHops": "Sökväg inställd: {hopCount} {hopCount, plural, =1{hopp} other{hoppar}} - {status}", + "chat_setCustomPath": "Ange anpassad sökväg", + "chat_setCustomPathSubtitle": "Ange ruttväg manuellt", + "chat_clearPath": "Rensa Vägen", + "chat_clearPathSubtitle": "Tvinga fram omstart vid nästa sändning", + "chat_pathCleared": "Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.", + "chat_floodModeSubtitle": "Använd routningsomkopplaren i appraden", + "chat_floodModeEnabled": "Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.", + "chat_fullPath": "Fullständig sökväg", + "chat_pathDetailsNotAvailable": "Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.", + "chat_pathSetHops": "Sökväg inställd: {hopCount} {hopCount, plural, =1{hopp} other{hoppar}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,14 +581,14 @@ } } }, - "chat_pathSavedLocally": "Sparat lokalt. Anslut för att synkronisera.", - "chat_pathDeviceConfirmed": "Enheten bekräftad.", - "chat_pathDeviceNotConfirmed": "Enheten har inte bekräftats ännu.", + "chat_pathSavedLocally": "Sparat lokalt. Anslut för att synkronisera.", + "chat_pathDeviceConfirmed": "Enheten bekräftad.", + "chat_pathDeviceNotConfirmed": "Enheten har inte bekräftats ännu.", "chat_type": "Skriv", - "chat_path": "Sökväg", - "chat_publicKey": "Allmänt nyckel", - "chat_compressOutgoingMessages": "Kryptera utgående meddelanden", - "chat_floodForced": "Översvämning (tvingad)", + "chat_path": "Sökväg", + "chat_publicKey": "Allmänt nyckel", + "chat_compressOutgoingMessages": "Kryptera utgÃ¥ende meddelanden", + "chat_floodForced": "Översvämning (tvingad)", "chat_directForced": "Direkt (tvingad)", "chat_hopsForced": "{count} hopp (tvingat)", "@chat_hopsForced": { @@ -598,10 +598,10 @@ } } }, - "chat_floodAuto": "Översvämning (auto)", + "chat_floodAuto": "Översvämning (auto)", "chat_direct": "Direkt", "chat_poiShared": "Delad POI", - "chat_unread": "Olästa: {count}", + "chat_unread": "Olästa: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Öppna länk?", - "chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?", - "chat_open": "Öppna", - "chat_couldNotOpenLink": "Kunde inte öppna länken: {url}", + "chat_openLink": "Öppna länk?", + "chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?", + "chat_open": "Öppna", + "chat_couldNotOpenLink": "Kunde inte öppna länken: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,10 +620,10 @@ } } }, - "chat_invalidLink": "Ogiltigt länkformat", + "chat_invalidLink": "Ogiltigt länkformat", "map_title": "Nodkarta", "map_noNodesWithLocation": "Inga noder med platsinformation", - "map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan", + "map_nodesNeedGps": "Noder mÃ¥ste dela sina GPS-koordinater\nför att visas pÃ¥ kartan", "map_nodesCount": "Noder: {count}", "@map_nodesCount": { "placeholders": { @@ -641,26 +641,26 @@ } }, "map_chat": "Chat", - "map_repeater": "Återuppspelare", + "map_repeater": "Ã…teruppspelare", "map_room": "Rum", "map_sensor": "Sensor", - "map_pinDm": "Lås (DM)", - "map_pinPrivate": "Lås (Privat)", - "map_pinPublic": "Anslå (Offentligt)", + "map_pinDm": "LÃ¥s (DM)", + "map_pinPrivate": "LÃ¥s (Privat)", + "map_pinPublic": "AnslÃ¥ (Offentligt)", "map_lastSeen": "Senast sedd", - "map_disconnectConfirm": "Är du säker på att du vill koppla från enheten?", - "map_from": "Från", - "map_source": "Källa", + "map_disconnectConfirm": "Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?", + "map_from": "FrÃ¥n", + "map_source": "Källa", "map_flags": "Flaggor", - "map_shareMarkerHere": "Dela markeringen här", - "map_pinLabel": "Fästetikett", + "map_shareMarkerHere": "Dela markeringen här", + "map_pinLabel": "Fästetikett", "map_label": "Etikett", "map_pointOfInterest": "Plats av intresse", "map_sendToContact": "Skicka till kontakt", "map_sendToChannel": "Skicka till kanal", - "map_noChannelsAvailable": "Inga kanaler tillgängliga", + "map_noChannelsAvailable": "Inga kanaler tillgängliga", "map_publicLocationShare": "Dela offentlig plats", - "map_publicLocationShareConfirm": "Du håller på att dela en plats i {channelLabel}. Denna kanal är offentlig och alla med PSK kan se den.", + "map_publicLocationShareConfirm": "Du hÃ¥ller pÃ¥ att dela en plats i {channelLabel}. Denna kanal är offentlig och alla med PSK kan se den.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,7 +668,7 @@ } } }, - "map_connectToShareMarkers": "Anslut till en enhet för att dela markörer", + "map_connectToShareMarkers": "Anslut till en enhet för att dela markörer", "map_filterNodes": "Filtrera noder", "map_nodeTypes": "Nodtyper", "map_chatNodes": "Chatnoder", @@ -676,18 +676,18 @@ "map_otherNodes": "Andra noder", "map_keyPrefix": "Nyckelprefix", "map_filterByKeyPrefix": "Filtrera efter nyckelprefix", - "map_publicKeyPrefix": "Allmänt nyckelprästegenskap", - "map_markers": "Markörer", - "map_showSharedMarkers": "Visa delade markörer", + "map_publicKeyPrefix": "Allmänt nyckelprästegenskap", + "map_markers": "Markörer", + "map_showSharedMarkers": "Visa delade markörer", "map_lastSeenTime": "Senaste Visats Tid", "map_sharedPin": "Delad PIN", - "map_joinRoom": "Gå med i rum", + "map_joinRoom": "GÃ¥ med i rum", "map_manageRepeater": "Hantera Upprepare", "mapCache_title": "Offline Kartcache", - "mapCache_selectAreaFirst": "Välj ett område att cachera först", - "mapCache_noTilesToDownload": "Inga kuber att ladda ner för detta område", + "mapCache_selectAreaFirst": "Välj ett omrÃ¥de att cachera först", + "mapCache_noTilesToDownload": "Inga kuber att ladda ner för detta omrÃ¥de", "mapCache_downloadTilesTitle": "Ladda ner klick", - "mapCache_downloadTilesPrompt": "Ladda ner {count} kuber för offlineanvändning?", + "mapCache_downloadTilesPrompt": "Ladda ner {count} kuber för offlineanvändning?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -719,9 +719,9 @@ "mapCache_clearOfflineCachePrompt": "Ta bort alla cachemapplaner?", "mapCache_offlineCacheCleared": "Offline-cache rensad", "mapCache_noAreaSelected": "Ingen area markerad", - "mapCache_cacheArea": "Cacheområde", - "mapCache_useCurrentView": "Använd Aktuell Visning", - "mapCache_zoomRange": "Zoombegränsning", + "mapCache_cacheArea": "CacheomrÃ¥de", + "mapCache_useCurrentView": "Använd Aktuell Visning", + "mapCache_zoomRange": "Zoombegränsning", "mapCache_estimatedTiles": "Uppskattat antal klick: {count}", "@mapCache_estimatedTiles": { "placeholders": { @@ -799,27 +799,27 @@ "time_days": "dagar", "time_week": "vecka", "time_weeks": "veckor", - "time_month": "månad", - "time_months": "månader", + "time_month": "mÃ¥nad", + "time_months": "mÃ¥nader", "time_minutes": "minuter", "time_allTime": "Alla tider", - "dialog_disconnect": "Koppla från", - "dialog_disconnectConfirm": "Är du säker på att du vill koppla från enheten?", - "login_repeaterLogin": "Återuppta Inloggning", + "dialog_disconnect": "Koppla frÃ¥n", + "dialog_disconnectConfirm": "Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?", + "login_repeaterLogin": "Ã…teruppta Inloggning", "login_roomLogin": "Rum Inloggning", - "login_password": "Lösenord", - "login_enterPassword": "Ange lösenord", - "login_savePassword": "Spara lösenord", - "login_savePasswordSubtitle": "Lösenord kommer att lagras säkert på enheten.", - "login_repeaterDescription": "Ange återuppspelarens lösenord för att komma åt inställningar och status.", - "login_roomDescription": "Ange rummets lösenord för att komma åt inställningar och status.", + "login_password": "Lösenord", + "login_enterPassword": "Ange lösenord", + "login_savePassword": "Spara lösenord", + "login_savePasswordSubtitle": "Lösenord kommer att lagras säkert pÃ¥ enheten.", + "login_repeaterDescription": "Ange Ã¥teruppspelarens lösenord för att komma Ã¥t inställningar och status.", + "login_roomDescription": "Ange rummets lösenord för att komma Ã¥t inställningar och status.", "login_routing": "Ruttning", - "login_routingMode": "Ruttläge", - "login_autoUseSavedPath": "Automatisk (använd sparad sökväg)", - "login_forceFloodMode": "Tvinga Översvämningsläge", - "login_managePaths": "Hantera Sökvägar", + "login_routingMode": "Ruttläge", + "login_autoUseSavedPath": "Automatisk (använd sparad sökväg)", + "login_forceFloodMode": "Tvinga Översvämningsläge", + "login_managePaths": "Hantera Sökvägar", "login_login": "Logga in", - "login_attempt": "Försök {current}/{max}", + "login_attempt": "Försök {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.", + "login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller sÃ¥ gÃ¥r det inte att nÃ¥ repeatern.", "common_reload": "Ladda om", "common_clear": "Rensa", - "path_currentPath": "Nuvarande sökväg: {path}", + "path_currentPath": "Nuvarande sökväg: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Använda {count} {count, plural, =1{hop} other{hops}} sökväg", + "path_usingHopsPath": "Använda {count} {count, plural, =1{hop} other{hops}} sökväg", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,15 +857,15 @@ } } }, - "path_enterCustomPath": "Ange anpassad sökväg", - "path_currentPathLabel": "Nuvarande sökväg", - "path_hexPrefixInstructions": "Ange 2-tecknets hex-prefett för varje hopp, åtskilda med komma.", - "path_hexPrefixExample": "Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)", + "path_enterCustomPath": "Ange anpassad sökväg", + "path_currentPathLabel": "Nuvarande sökväg", + "path_hexPrefixInstructions": "Ange 2-tecknets hex-prefett för varje hopp, Ã¥tskilda med komma.", + "path_hexPrefixExample": "Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)", "path_labelHexPrefixes": "Hexprefixer", - "path_helperMaxHops": "Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)", - "path_selectFromContacts": "Välj istället från kontakter:", - "path_noRepeatersFound": "Inga återuppspelare eller rumsservrar hittades.", - "path_customPathsRequire": "Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.", + "path_helperMaxHops": "Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)", + "path_selectFromContacts": "Välj istället frÃ¥n kontakter:", + "path_noRepeatersFound": "Inga Ã¥teruppspelare eller rumsservrar hittades.", + "path_customPathsRequire": "Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.", "path_invalidHexPrefixes": "Ogiltiga hex-prefikser: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Sökvägen är för lång. Max 64 hopp tillåtna.", - "path_setPath": "Ange Sökväg", - "repeater_management": "Återuppspelarens Hantering", + "path_tooLong": "Sökvägen är för lÃ¥ng. Max 64 hopp tillÃ¥tna.", + "path_setPath": "Ange Sökväg", + "repeater_management": "Ã…teruppspelarens Hantering", "repeater_managementTools": "Administrationsverktyg", "repeater_status": "Status", - "repeater_statusSubtitle": "Visa återspolningsstatus, statistik och grannar", + "repeater_statusSubtitle": "Visa Ã¥terspolningsstatus, statistik och grannar", "repeater_telemetry": "Telemetry", - "repeater_telemetrySubtitle": "Visa telemetri för sensorer och systemstatistik", + "repeater_telemetrySubtitle": "Visa telemetri för sensorer och systemstatistik", "repeater_cli": "CLI", "repeater_cliSubtitle": "Skicka kommandon till repetitorn", - "repeater_settings": "Inställningar", - "repeater_settingsSubtitle": "Konfigurera återspolarparametrar", - "repeater_statusTitle": "Återspelsstatus", - "repeater_routingMode": "Ruttläge", - "repeater_autoUseSavedPath": "Automatisk (använd sparad sökväg)", - "repeater_forceFloodMode": "Tvinga Översvämningsläge", + "repeater_settings": "Inställningar", + "repeater_settingsSubtitle": "Konfigurera Ã¥terspolarparametrar", + "repeater_statusTitle": "Ã…terspelsstatus", + "repeater_routingMode": "Ruttläge", + "repeater_autoUseSavedPath": "Automatisk (använd sparad sökväg)", + "repeater_forceFloodMode": "Tvinga Översvämningsläge", "repeater_pathManagement": "Stigarhantering", "repeater_refresh": "Uppdatera", - "repeater_statusRequestTimeout": "Statusförfrågan gick inte att hämta.", - "repeater_errorLoadingStatus": "Fel vid inläsning av status: {error}", + "repeater_statusRequestTimeout": "StatusförfrÃ¥gan gick inte att hämta.", + "repeater_errorLoadingStatus": "Fel vid inläsning av status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -904,13 +904,13 @@ "repeater_systemInformation": "Systeminformation", "repeater_battery": "Batteri", "repeater_clockAtLogin": "Klocka (vid inloggning)", - "repeater_uptime": "Tillgänglighet", - "repeater_queueLength": "Köans längd", - "repeater_debugFlags": "Felsökningsflaggor", + "repeater_uptime": "Tillgänglighet", + "repeater_queueLength": "Köans längd", + "repeater_debugFlags": "Felsökningsflaggor", "repeater_radioStatistics": "Radiostatistik", "repeater_lastRssi": "Senaste RSSI", "repeater_lastSnr": "Sista SNR", - "repeater_noiseFloor": "Ljudnivå", + "repeater_noiseFloor": "LjudnivÃ¥", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", "repeater_packetStatistics": "Paketstatistik", @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", + "repeater_packetTxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", + "repeater_packetRxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Översvämning: {flood}, Direkt: {direct}", + "repeater_duplicatesFloodDirect": "Översvämning: {flood}, Direkt: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,15 +981,15 @@ } } }, - "repeater_settingsTitle": "Återuppspelarens Inställningar", - "repeater_basicSettings": "Grundinställningar", + "repeater_settingsTitle": "Ã…teruppspelarens Inställningar", + "repeater_basicSettings": "Grundinställningar", "repeater_repeaterName": "Upprepare Namn", - "repeater_repeaterNameHelper": "Visa namn för denna återupprepare", - "repeater_adminPassword": "Adminlösenord", - "repeater_adminPasswordHelper": "Fullständig åtkomstlösenord", - "repeater_guestPassword": "Gästlösenhet", - "repeater_guestPasswordHelper": "Läs-skyddspassord", - "repeater_radioSettings": "Radioinställningar", + "repeater_repeaterNameHelper": "Visa namn för denna Ã¥terupprepare", + "repeater_adminPassword": "Adminlösenord", + "repeater_adminPasswordHelper": "Fullständig Ã¥tkomstlösenord", + "repeater_guestPassword": "Gästlösenhet", + "repeater_guestPasswordHelper": "Läs-skyddspassord", + "repeater_radioSettings": "Radioinställningar", "repeater_frequencyMhz": "Frekvens (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Effekt", @@ -997,19 +997,19 @@ "repeater_bandwidth": "Bandbredd", "repeater_spreadingFactor": "Spreadingfaktor", "repeater_codingRate": "Kodningsgrad", - "repeater_locationSettings": "Platsinställningar", + "repeater_locationSettings": "Platsinställningar", "repeater_latitude": "Latitud", "repeater_latitudeHelper": "Decimalgrader (t.ex. 37.7749)", - "repeater_longitude": "Längdgrad", + "repeater_longitude": "Längdgrad", "repeater_longitudeHelper": "Decimalgrader (t.ex. -122.4194)", "repeater_features": "Funktioner", - "repeater_packetForwarding": "Paketväxling", - "repeater_packetForwardingSubtitle": "Aktivera återuppspelaren för att vidarebefordra paket", - "repeater_guestAccess": "Gäståtkomst", - "repeater_guestAccessSubtitle": "Tillåt läsbehörigheter för gäster.", - "repeater_privacyMode": "Privatläge", - "repeater_privacyModeSubtitle": "Dölj namn/plats i annonser", - "repeater_advertisementSettings": "Annonsinställningar", + "repeater_packetForwarding": "Paketväxling", + "repeater_packetForwardingSubtitle": "Aktivera Ã¥teruppspelaren för att vidarebefordra paket", + "repeater_guestAccess": "GästÃ¥tkomst", + "repeater_guestAccessSubtitle": "TillÃ¥t läsbehörigheter för gäster.", + "repeater_privacyMode": "Privatläge", + "repeater_privacyModeSubtitle": "Dölj namn/plats i annonser", + "repeater_advertisementSettings": "Annonsinställningar", "repeater_localAdvertInterval": "Lokalt Annonsintervall", "repeater_localAdvertIntervalMinutes": "{minutes} minuter", "@repeater_localAdvertIntervalMinutes": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Översvämnadsannonsens tidsintervall", + "repeater_floodAdvertInterval": "Översvämnadsannonsens tidsintervall", "repeater_floodAdvertIntervalHours": "{hours} timmar", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1029,17 +1029,17 @@ } }, "repeater_encryptedAdvertInterval": "Krypterad Annonsintervall", - "repeater_dangerZone": "Faraområde", - "repeater_rebootRepeater": "Starta Återuppspelaren", + "repeater_dangerZone": "FaraomrÃ¥de", + "repeater_rebootRepeater": "Starta Ã…teruppspelaren", "repeater_rebootRepeaterSubtitle": "Starta om repeternheten", - "repeater_rebootRepeaterConfirm": "Är du säker på att du vill starta om denna repeater?", + "repeater_rebootRepeaterConfirm": "Är du säker pÃ¥ att du vill starta om denna repeater?", "repeater_regenerateIdentityKey": "Generera Identitetsknyckel", "repeater_regenerateIdentityKeySubtitle": "Generera ny publik/privat nyckelpar", - "repeater_regenerateIdentityKeyConfirm": "Detta kommer att generera en ny identitet för återspelaren. Fortsätta?", + "repeater_regenerateIdentityKeyConfirm": "Detta kommer att generera en ny identitet för Ã¥terspelaren. Fortsätta?", "repeater_eraseFileSystem": "Radera Filsystem", - "repeater_eraseFileSystemSubtitle": "Formatera återspelsfilsystemet", - "repeater_eraseFileSystemConfirm": "VARNING: Detta kommer att radera all data på repeatern. Detta kan inte ångras!", - "repeater_eraseSerialOnly": "Rensa är endast tillgängligt via seriell konsol.", + "repeater_eraseFileSystemSubtitle": "Formatera Ã¥terspelsfilsystemet", + "repeater_eraseFileSystemConfirm": "VARNING: Detta kommer att radera all data pÃ¥ repeatern. Detta kan inte Ã¥ngras!", + "repeater_eraseSerialOnly": "Rensa är endast tillgängligt via seriell konsol.", "repeater_commandSent": "Kommandot skickades: {command}", "@repeater_commandSent": { "placeholders": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "Bekräfta", - "repeater_settingsSaved": "Inställningarna sparades framgångsrikt.", - "repeater_errorSavingSettings": "Fel vid sparande av inställningar: {error}", + "repeater_confirm": "Bekräfta", + "repeater_settingsSaved": "Inställningarna sparades framgÃ¥ngsrikt.", + "repeater_errorSavingSettings": "Fel vid sparande av inställningar: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,14 +1066,14 @@ } } }, - "repeater_refreshBasicSettings": "Återställ Grundläggande Inställningar", - "repeater_refreshRadioSettings": "Återställ Radiosinställningar", - "repeater_refreshTxPower": "Återställ TX-effekt", - "repeater_refreshLocationSettings": "Uppdatera Lokationsinställningar", - "repeater_refreshPacketForwarding": "Återställ Paketväxling", - "repeater_refreshGuestAccess": "Återställ Gäståtkomst", - "repeater_refreshPrivacyMode": "Återställ Sekretessläge", - "repeater_refreshAdvertisementSettings": "Återställ Annonsinställningar", + "repeater_refreshBasicSettings": "Ã…terställ Grundläggande Inställningar", + "repeater_refreshRadioSettings": "Ã…terställ Radiosinställningar", + "repeater_refreshTxPower": "Ã…terställ TX-effekt", + "repeater_refreshLocationSettings": "Uppdatera Lokationsinställningar", + "repeater_refreshPacketForwarding": "Ã…terställ Paketväxling", + "repeater_refreshGuestAccess": "Ã…terställ GästÃ¥tkomst", + "repeater_refreshPrivacyMode": "Ã…terställ Sekretessläge", + "repeater_refreshAdvertisementSettings": "Ã…terställ Annonsinställningar", "repeater_refreshed": "{label} har uppdaterats", "@repeater_refreshed": { "placeholders": { @@ -1090,17 +1090,17 @@ } } }, - "repeater_cliTitle": "Återuppspelaren CLI", - "repeater_debugNextCommand": "Felsök Nästa Kommando", - "repeater_commandHelp": "Hjälp", + "repeater_cliTitle": "Ã…teruppspelaren CLI", + "repeater_debugNextCommand": "Felsök Nästa Kommando", + "repeater_commandHelp": "Hjälp", "repeater_clearHistory": "Rensa Historik", - "repeater_noCommandsSent": "Inga kommandon skickats ännu", - "repeater_typeCommandOrUseQuick": "Skriv en kommando nedan eller använd snabba kommandon", + "repeater_noCommandsSent": "Inga kommandon skickats ännu", + "repeater_typeCommandOrUseQuick": "Skriv en kommando nedan eller använd snabba kommandon", "repeater_enterCommandHint": "Ange kommando...", "repeater_previousCommand": "Tidigare kommando", - "repeater_nextCommand": "Nästa kommando", - "repeater_enterCommandFirst": "Ange en kommando först", - "repeater_cliCommandFrameTitle": "Kommandofönster", + "repeater_nextCommand": "Nästa kommando", + "repeater_enterCommandFirst": "Ange en kommando först", + "repeater_cliCommandFrameTitle": "Kommandofönster", "repeater_cliCommandError": "Fel: {error}", "@repeater_cliCommandError": { "placeholders": { @@ -1109,80 +1109,80 @@ } } }, - "repeater_cliQuickGetName": "Hämta namn", - "repeater_cliQuickGetRadio": "Få Radio", - "repeater_cliQuickGetTx": "Hämta TX", + "repeater_cliQuickGetName": "Hämta namn", + "repeater_cliQuickGetRadio": "FÃ¥ Radio", + "repeater_cliQuickGetTx": "Hämta TX", "repeater_cliQuickNeighbors": "Grannar", "repeater_cliQuickVersion": "Version", "repeater_cliQuickAdvertise": "Annonsera", "repeater_cliQuickClock": "Klocka", "repeater_cliHelpAdvert": "Skickar ett annonspaket", - "repeater_cliHelpReboot": "Startar om enheten. (notera, du får kanske 'Timeout' vilket är normalt)", + "repeater_cliHelpReboot": "Startar om enheten. (notera, du fÃ¥r kanske 'Timeout' vilket är normalt)", "repeater_cliHelpClock": "Visar aktuell tid per enhetens klocka.", - "repeater_cliHelpPassword": "Ställer in ett nytt administratörslösenord för enheten.", + "repeater_cliHelpPassword": "Ställer in ett nytt administratörslösenord för enheten.", "repeater_cliHelpVersion": "Visar enhetsversion och firmwarebyggnadsdatum.", - "repeater_cliHelpClearStats": "Återställer olika statistikräknare till noll.", - "repeater_cliHelpSetAf": "Ställer in lufttidsfaktor.", - "repeater_cliHelpSetTx": "Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)", - "repeater_cliHelpSetRepeat": "Aktiverar eller inaktiverar återuppspelarens roll för denna nod.", - "repeater_cliHelpSetAllowReadOnly": "(Rumserver) Om 'på', så tillåts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).", - "repeater_cliHelpSetFloodMax": "Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).", - "repeater_cliHelpSetIntThresh": "Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den på 0 för att inaktivera detektion av kanalinterferens.", - "repeater_cliHelpSetAgcResetInterval": "Ställer in intervallet för att återställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.", + "repeater_cliHelpClearStats": "Ã…terställer olika statistikräknare till noll.", + "repeater_cliHelpSetAf": "Ställer in lufttidsfaktor.", + "repeater_cliHelpSetTx": "Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)", + "repeater_cliHelpSetRepeat": "Aktiverar eller inaktiverar Ã¥teruppspelarens roll för denna nod.", + "repeater_cliHelpSetAllowReadOnly": "(Rumserver) Om 'pÃ¥', sÃ¥ tillÃ¥ts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).", + "repeater_cliHelpSetFloodMax": "Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).", + "repeater_cliHelpSetIntThresh": "Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den pÃ¥ 0 för att inaktivera detektion av kanalinterferens.", + "repeater_cliHelpSetAgcResetInterval": "Ställer in intervallet för att Ã¥terställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.", "repeater_cliHelpSetMultiAcks": "Aktiverar eller inaktiverar funktionen 'dubbla ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.", - "repeater_cliHelpSetFloodAdvertInterval": "Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in på 0 för att inaktivera.", - "repeater_cliHelpSetGuestPassword": "Ställer in/uppdaterar gästlösenordet. (för återvändare kan gästloggar skicka \"Get Stats\"-förfrågan)", - "repeater_cliHelpSetName": "Ställer in annonstexterna namn.", - "repeater_cliHelpSetLat": "Ställer in annonskartans latitud. (decimalgrader)", - "repeater_cliHelpSetLon": "Ställer in annonskartans longitud (decimalgrader).", - "repeater_cliHelpSetRadio": "Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.", - "repeater_cliHelpSetRxDelay": "Ställer (experimentell) basvärde (måste vara > 1 för effekt) för att applicera en liten fördröjning på mottagna paket, baserat på signalstyrka/poäng. Ställ in på 0 för att inaktivera.", - "repeater_cliHelpSetTxDelay": "Ställer in en faktor som multipliceras med tid på luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).", - "repeater_cliHelpSetDirectTxDelay": "Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.", + "repeater_cliHelpSetAdvertInterval": "Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.", + "repeater_cliHelpSetFloodAdvertInterval": "Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in pÃ¥ 0 för att inaktivera.", + "repeater_cliHelpSetGuestPassword": "Ställer in/uppdaterar gästlösenordet. (för Ã¥tervändare kan gästloggar skicka \"Get Stats\"-förfrÃ¥gan)", + "repeater_cliHelpSetName": "Ställer in annonstexterna namn.", + "repeater_cliHelpSetLat": "Ställer in annonskartans latitud. (decimalgrader)", + "repeater_cliHelpSetLon": "Ställer in annonskartans longitud (decimalgrader).", + "repeater_cliHelpSetRadio": "Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.", + "repeater_cliHelpSetRxDelay": "Ställer (experimentell) basvärde (mÃ¥ste vara > 1 för effekt) för att applicera en liten fördröjning pÃ¥ mottagna paket, baserat pÃ¥ signalstyrka/poäng. Ställ in pÃ¥ 0 för att inaktivera.", + "repeater_cliHelpSetTxDelay": "Ställer in en faktor som multipliceras med tid pÃ¥ luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).", + "repeater_cliHelpSetDirectTxDelay": "Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.", "repeater_cliHelpSetBridgeEnabled": "Aktivera/Inaktivera brygga.", - "repeater_cliHelpSetBridgeDelay": "Ställ in fördröjning innan paket åter sänder.", - "repeater_cliHelpSetBridgeSource": "Välj om bron ska återända mottagna paket eller sända paket.", - "repeater_cliHelpSetBridgeBaud": "Ställ baudgränsen för rs232-bryggarna.", - "repeater_cliHelpSetBridgeSecret": "Ställ bro-hemlighet för espnow-broar.", - "repeater_cliHelpSetAdcMultiplier": "Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd på utvalda kort).", - "repeater_cliHelpTempRadio": "Ställer temporära radioparametrar för det angivna antalet minuter, vilket återgår till de ursprungliga radioparametrarna efteråt. (sparar inte i inställningar).", - "repeater_cliHelpSetPerm": "Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. Tillståndsbiten varierar per firmware-roll, men de låga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).", - "repeater_cliHelpGetBridgeType": "Får brotyperna ingen, rs232, espnow", + "repeater_cliHelpSetBridgeDelay": "Ställ in fördröjning innan paket Ã¥ter sänder.", + "repeater_cliHelpSetBridgeSource": "Välj om bron ska Ã¥terända mottagna paket eller sända paket.", + "repeater_cliHelpSetBridgeBaud": "Ställ baudgränsen för rs232-bryggarna.", + "repeater_cliHelpSetBridgeSecret": "Ställ bro-hemlighet för espnow-broar.", + "repeater_cliHelpSetAdcMultiplier": "Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd pÃ¥ utvalda kort).", + "repeater_cliHelpTempRadio": "Ställer temporära radioparametrar för det angivna antalet minuter, vilket Ã¥tergÃ¥r till de ursprungliga radioparametrarna efterÃ¥t. (sparar inte i inställningar).", + "repeater_cliHelpSetPerm": "Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. TillstÃ¥ndsbiten varierar per firmware-roll, men de lÃ¥ga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).", + "repeater_cliHelpGetBridgeType": "FÃ¥r brotyperna ingen, rs232, espnow", "repeater_cliHelpLogStart": "Starta paketloggning till filsystem.", "repeater_cliHelpLogStop": "Stoppar paketloggning till filsystem.", - "repeater_cliHelpLogErase": "Raderar pakets loggar från filsystemet.", - "repeater_cliHelpNeighbors": "Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-g撮-4", - "repeater_cliHelpNeighborRemove": "Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) från grannlistan.", - "repeater_cliHelpRegion": "(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.", - "repeater_cliHelpRegionLoad": "MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.", - "repeater_cliHelpRegionGet": "Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) 'F'\"", - "repeater_cliHelpRegionPut": "Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.", - "repeater_cliHelpRegionRemove": "Tar bort en regionsdefinition med det angivna namnet. (måste matcha exakt och inte ha några barnregioner)", - "repeater_cliHelpRegionAllowf": "Ställer 'Flöde'-behörighet för det angivna området. ('' för det globala/gamla scopet)", - "repeater_cliHelpRegionDenyf": "Tar bort 'F'lood-behörigheten för det angivna området. (OBS: rekommenderas inte att använda detta i detta skede på den globala/gamla omfattningen!!).", - "repeater_cliHelpRegionHome": "Svarar med den aktuella 'hem'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).", - "repeater_cliHelpRegionHomeSet": "Ställer in 'hemregionen'.", + "repeater_cliHelpLogErase": "Raderar pakets loggar frÃ¥n filsystemet.", + "repeater_cliHelpNeighbors": "Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-gæ’®-4", + "repeater_cliHelpNeighborRemove": "Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) frÃ¥n grannlistan.", + "repeater_cliHelpRegion": "(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.", + "repeater_cliHelpRegionLoad": "MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.", + "repeater_cliHelpRegionGet": "Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) 'F'\"", + "repeater_cliHelpRegionPut": "Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.", + "repeater_cliHelpRegionRemove": "Tar bort en regionsdefinition med det angivna namnet. (mÃ¥ste matcha exakt och inte ha nÃ¥gra barnregioner)", + "repeater_cliHelpRegionAllowf": "Ställer 'Flöde'-behörighet för det angivna omrÃ¥det. ('' för det globala/gamla scopet)", + "repeater_cliHelpRegionDenyf": "Tar bort 'F'lood-behörigheten för det angivna omrÃ¥det. (OBS: rekommenderas inte att använda detta i detta skede pÃ¥ den globala/gamla omfattningen!!).", + "repeater_cliHelpRegionHome": "Svarar med den aktuella 'hem'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).", + "repeater_cliHelpRegionHomeSet": "Ställer in 'hemregionen'.", "repeater_cliHelpRegionSave": "Sparar regionlistan/kartan till lagring.", - "repeater_cliHelpGps": "Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"på\", status, fix, antal satelliter.", - "repeater_cliHelpGpsOnOff": "Aktiverar/inaktiverar GPS-strömsättningen.", - "repeater_cliHelpGpsSync": "Synkroniserar nätverks tid med GPS-klockan.", - "repeater_cliHelpGpsSetLoc": "Ställer nodens position till GPS-koordinater och sparar inställningar.", - "repeater_cliHelpGpsAdvert": "Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (från SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar", - "repeater_cliHelpGpsAdvertSet": "Ställer in annonsplatskonfiguration.", - "repeater_commandsListTitle": "Inställningslista", - "repeater_commandsListNote": "OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.", - "repeater_general": "Allmänt", - "repeater_settingsCategory": "Inställningar", + "repeater_cliHelpGps": "Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"pÃ¥\", status, fix, antal satelliter.", + "repeater_cliHelpGpsOnOff": "Aktiverar/inaktiverar GPS-strömsättningen.", + "repeater_cliHelpGpsSync": "Synkroniserar nätverks tid med GPS-klockan.", + "repeater_cliHelpGpsSetLoc": "Ställer nodens position till GPS-koordinater och sparar inställningar.", + "repeater_cliHelpGpsAdvert": "Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (frÃ¥n SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar", + "repeater_cliHelpGpsAdvertSet": "Ställer in annonsplatskonfiguration.", + "repeater_commandsListTitle": "Inställningslista", + "repeater_commandsListNote": "OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.", + "repeater_general": "Allmänt", + "repeater_settingsCategory": "Inställningar", "repeater_bridge": "Bro", "repeater_logging": "Logga", - "repeater_neighborsRepeaterOnly": "Grannar (Endast återspelare)", - "repeater_regionManagementRepeaterOnly": "Regionhantering (endast återuppspelare)", - "repeater_regionNote": "Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.", + "repeater_neighborsRepeaterOnly": "Grannar (Endast Ã¥terspelare)", + "repeater_regionManagementRepeaterOnly": "Regionhantering (endast Ã¥teruppspelare)", + "repeater_regionNote": "Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.", "repeater_gpsManagement": "GPS Hantering", - "repeater_gpsNote": "GPS-kommando har introducerats för att hantera platsrelaterade ämnen.", + "repeater_gpsNote": "GPS-kommando har introducerats för att hantera platsrelaterade ämnen.", "telemetry_receivedData": "Mottagen Telemetridata", - "telemetry_requestTimeout": "Telemetryförfrågan gick ut.", + "telemetry_requestTimeout": "TelemetryförfrÃ¥gan gick ut.", "telemetry_errorLoading": "Fel vid laddning av telemetri: {error}", "@telemetry_errorLoading": { "placeholders": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Inga telemetridata tillgängliga.", + "telemetry_noData": "Inga telemetridata tillgängliga.", "telemetry_channelTitle": "Kanal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1201,7 +1201,7 @@ } }, "telemetry_batteryLabel": "Batteri", - "telemetry_voltageLabel": "Spänning", + "telemetry_voltageLabel": "Spänning", "telemetry_mcuTemperatureLabel": "MCU Temperatur", "telemetry_temperatureLabel": "Temperatur", "telemetry_currentLabel": "Aktuell", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Paketväg", + "channelPath_title": "Paketväg", "channelPath_viewMap": "Visa karta", - "channelPath_otherObservedPaths": "Övriga observerade stigar", - "channelPath_repeaterHops": "Återupptagningssteg", - "channelPath_noHopDetails": "Detaljer för denna paket är inte angivna.", + "channelPath_otherObservedPaths": "Övriga observerade stigar", + "channelPath_repeaterHops": "Ã…terupptagningssteg", + "channelPath_noHopDetails": "Detaljer för denna paket är inte angivna.", "channelPath_messageDetails": "Meddelandets detaljer", - "channelPath_senderLabel": "Avsändare", + "channelPath_senderLabel": "Avsändare", "channelPath_timeLabel": "Tid", "channelPath_repeatsLabel": "Upprepa", - "channelPath_pathLabel": "Sökväg {index}", + "channelPath_pathLabel": "Sökväg {index}", "channelPath_observedLabel": "Observerat", - "channelPath_observedPathTitle": "Observerad bana {index} • {hops}", + "channelPath_observedPathTitle": "Observerad bana {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1288,8 +1288,8 @@ } } }, - "channelPath_unknownPath": "Okänt", - "channelPath_floodPath": "Översvämning", + "channelPath_unknownPath": "Okänt", + "channelPath_floodPath": "Översvämning", "channelPath_directPath": "Direkt", "channelPath_observedZeroOf": "0 av {total} hopp", "@channelPath_observedZeroOf": { @@ -1310,9 +1310,9 @@ } } }, - "channelPath_mapTitle": "Sökvägskarta", - "channelPath_noRepeaterLocations": "Inga återupprepningsplatser finns tillgängliga för denna väg.", - "channelPath_primaryPath": "Sökväg {index} (Primär)", + "channelPath_mapTitle": "Sökvägskarta", + "channelPath_noRepeaterLocations": "Inga Ã¥terupprepningsplatser finns tillgängliga för denna väg.", + "channelPath_primaryPath": "Sökväg {index} (Primär)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1327,9 +1327,9 @@ } } }, - "channelPath_pathLabelTitle": "Sökväg", - "channelPath_observedPathHeader": "Observerad Sökväg", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Sökväg", + "channelPath_observedPathHeader": "Observerad Sökväg", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,19 +1340,19 @@ } } }, - "channelPath_noHopDetailsAvailable": "Inga hoppdetaljer finns tillgängliga för detta paket.", - "channelPath_unknownRepeater": "Okänt Upprepare", + "channelPath_noHopDetailsAvailable": "Inga hoppdetaljer finns tillgängliga för detta paket.", + "channelPath_unknownRepeater": "Okänt Upprepare", "listFilter_tooltip": "Filtrera och sortera", "listFilter_sortBy": "Sortera efter", "listFilter_latestMessages": "Senaste meddelanden", - "listFilter_heardRecently": "Hörts nyligen", + "listFilter_heardRecently": "Hörts nyligen", "listFilter_az": "A-Z", "listFilter_filters": "Filteralternativ", "listFilter_all": "Alla", - "listFilter_users": "Användare", + "listFilter_users": "Användare", "listFilter_repeaters": "Upprepare", "listFilter_roomServers": "Rumservrar", - "listFilter_unreadOnly": "Endast oinlästa", + "listFilter_unreadOnly": "Endast oinlästa", "listFilter_newGroup": "Ny grupp", "@neighbors_errorLoading": { "placeholders": { @@ -1364,18 +1364,18 @@ "repeater_neighbors": "Grannar", "repeater_neighborsSubtitle": "Visa noll hoppgrannar.", "neighbors_receivedData": "Mottagna grannars data", - "neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.", - "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", + "neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.", + "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", "neighbors_repeatersNeighbors": "Upprepar grannar", - "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", + "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", "channels_createPrivateChannel": "Skapa en privat kanal", - "channels_joinPrivateChannel": "Gå med i en Privat Kanal", + "channels_joinPrivateChannel": "GÃ¥ med i en Privat Kanal", "channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.", "channels_createPrivateChannelDesc": "Skyddat med en hemlig nyckel.", - "channels_joinPublicChannel": "Gå med i den Offentliga Kanalen", - "channels_joinPublicChannelDesc": "Vem som helst kan gå med i denna kanal.", - "channels_joinHashtagChannel": "Gå med i en Hashtagkanal", - "channels_joinHashtagChannelDesc": "Väldigt enkelt att gå med i hashtag-kanaler.", + "channels_joinPublicChannel": "GÃ¥ med i den Offentliga Kanalen", + "channels_joinPublicChannelDesc": "Vem som helst kan gÃ¥ med i denna kanal.", + "channels_joinHashtagChannel": "GÃ¥ med i en Hashtagkanal", + "channels_joinHashtagChannelDesc": "Väldigt enkelt att gÃ¥ med i hashtag-kanaler.", "channels_scanQrCode": "Skanna en QR-kod", "channels_scanQrCodeComingSoon": "Kommer snart", "channels_enterHashtag": "Ange hashtag", @@ -1394,12 +1394,12 @@ } } }, - "neighbors_heardAgo": "Hördes: {time} sedan", - "neighbors_unknownContact": "Okänd {pubkey}", + "neighbors_heardAgo": "Hördes: {time} sedan", + "neighbors_unknownContact": "Okänd {pubkey}", "settings_locationGPSEnable": "Aktivera GPS", - "settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.", - "settings_locationIntervalSec": "Interval för GPS (Sekunder)", - "settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.", + "settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.", + "settings_locationIntervalSec": "Interval för GPS (Sekunder)", + "settings_locationIntervalInvalid": "Intervalet mÃ¥ste vara minst 60 sekunder och mindre än 86400 sekunder.", "contacts_manageRoom": "Hantera Rumserver", "room_management": "Rumserverhantering", "@community_joinConfirmation": { @@ -1462,32 +1462,32 @@ "community_createDesc": "Skapa en ny gemenskap och dela via QR-kod.", "common_ok": "Okej", "community_title": "Gemenskap", - "community_join": "Gå med", - "community_joinTitle": "Gå med i gemenskapen", - "community_joinConfirmation": "Vill du gå med i communityn \"{name}\"?", + "community_join": "GÃ¥ med", + "community_joinTitle": "GÃ¥ med i gemenskapen", + "community_joinConfirmation": "Vill du gÃ¥ med i communityn \"{name}\"?", "community_scanQr": "Skanna Gemenskapens QR", "community_scanInstructions": "Rikta kameran mot en QR-kod i communityn", "community_showQr": "Visa QR-kod", - "community_publicChannel": "Föreningens Offentliga", + "community_publicChannel": "Föreningens Offentliga", "community_name": "Gemenskapens namn", "community_enterName": "Ange communities namn", "community_created": "Community \"{name}\" har skapats", "community_joined": "Medlem i communityn \"{name}\"", "community_qrTitle": "Dela Gemenskap", - "community_qrInstructions": "Skanna denna QR-kod för att gå med i \"{name}\"", - "community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nås av medlemmar i communityn", + "community_qrInstructions": "Skanna denna QR-kod för att gÃ¥ med i \"{name}\"", + "community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nÃ¥s av medlemmar i communityn", "community_hashtagChannel": "Community Hashtag", "community_invalidQrCode": "Ogiltig community QR-kod", - "community_alreadyMember": "Är redan medlem", - "community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".", - "community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)", - "community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community", - "community_noCommunities": "Inga gemenskaper har anslutats ännu", - "community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igång", + "community_alreadyMember": "Är redan medlem", + "community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".", + "community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)", + "community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community", + "community_noCommunities": "Inga gemenskaper har anslutats ännu", + "community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igÃ¥ng", "community_manageCommunities": "Hantera Gemenskaper", - "community_delete": "Lämna Gemenskap", - "community_deleteConfirm": "Lämna \"{name}\"?", - "community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.", + "community_delete": "Lämna Gemenskap", + "community_deleteConfirm": "Lämna \"{name}\"?", + "community_deleteChannelsWarning": "Detta kommer ocksÃ¥ att radera {count} kanal/kanaler och deras meddelanden.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Lämnade community \"{name}\"", - "community_addHashtagChannel": "Lägg till Gemenskapens Hashtag", - "community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community", - "community_selectCommunity": "Välj Gemenskap", + "community_deleted": "Lämnade community \"{name}\"", + "community_addHashtagChannel": "Lägg till Gemenskapens Hashtag", + "community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community", + "community_selectCommunity": "Välj Gemenskap", "community_regularHashtag": "Vanlig Hash Tag", - "community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)", - "community_communityHashtagDesc": "Endast för medlemmar", - "community_forCommunity": "För {name}", + "community_regularHashtagDesc": "Offentlig hashtag (alla kan gÃ¥ med)", + "community_communityHashtagDesc": "Endast för medlemmar", + "community_forCommunity": "För {name}", "community_communityHashtag": "Community Hashtag", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1533,11 +1533,11 @@ } }, "community_regenerate": "Regenerera", - "community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.", - "community_secretRegenerated": "Lösenord återskapad för \"{name}\"", + "community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar mÃ¥ste scanna den nya QR-koden för att fortsätta kommunicera.", + "community_secretRegenerated": "Lösenord Ã¥terskapad för \"{name}\"", "community_regenerateSecret": "Regenerera hemlig kod", - "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"", - "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"", + "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"", + "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"", "community_updateSecret": "Uppdatera hemlighet", "@contacts_pathTraceTo": { "placeholders": { @@ -1547,28 +1547,28 @@ } }, "pathTrace_you": "Du", - "pathTrace_failed": "Sökvägsföljning misslyckades.", - "pathTrace_notAvailable": "Path trace ej tillgänglig.", + "pathTrace_failed": "Sökvägsföljning misslyckades.", + "pathTrace_notAvailable": "Path trace ej tillgänglig.", "pathTrace_refreshTooltip": "Uppdatera Path Trace", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", - "contacts_repeaterPathTrace": "Vägspårning till repeater", + "contacts_repeaterPathTrace": "VägspÃ¥rning till repeater", "contacts_repeaterPing": "Ping-repeater", - "contacts_roomPathTrace": "Vägspårning till rumserver", + "contacts_roomPathTrace": "VägspÃ¥rning till rumserver", "contacts_roomPing": "Ping rumsserver", - "contacts_chatTraceRoute": "Spåra rutt", - "contacts_pathTraceTo": "Spåra rutt till {name}", - "contacts_clipboardEmpty": "Urklipp är tomt.", + "contacts_chatTraceRoute": "SpÃ¥ra rutt", + "contacts_pathTraceTo": "SpÃ¥ra rutt till {name}", + "contacts_clipboardEmpty": "Urklipp är tomt.", "appSettings_languageRu": "Ryska", "contacts_contactImportFailed": "Kontakt kunde inte importeras.", "contacts_zeroHopAdvert": "Reklam med nollhopp", - "contacts_floodAdvert": "Översvämningsannons", + "contacts_floodAdvert": "Översvämningsannons", "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", "appSettings_languageUk": "Ukrainska", - "appSettings_enableMessageTracing": "Aktivera meddelandespårning", - "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", - "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", + "appSettings_enableMessageTracing": "Aktivera meddelandespÃ¥rning", + "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", + "contacts_addContactFromClipboard": "Lägg till kontakt frÃ¥n urklipp", "contacts_contactImported": "Kontakt har importerats.", "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", "contacts_contactAdvertCopied": "Annons kopierad till Urklipp.", @@ -1580,47 +1580,47 @@ "notification_messagesCount": "{count} {count, plural, =1{meddelande} other{meddelanden}}", "notification_channelMessagesCount": "{count} {count, plural, =1{kanalmeddelande} other{kanalmeddelanden}}", "notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}", - "notification_newTypeDiscovered": "Ny {contactType} upptäckt", + "notification_newTypeDiscovered": "Ny {contactType} upptäckt", "notification_receivedNewMessage": "Nytt meddelande mottaget", "settings_gpxExportAll": "Exportera alla kontakter till GPX", "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", - "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", + "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgÃ¥ng", "settings_gpxExportNoContacts": "Inga kontakter att exportera.", - "settings_gpxExportNotAvailable": "Stöds inte på din enhet/operativsystem", + "settings_gpxExportNotAvailable": "Stöds inte pÃ¥ din enhet/operativsystem", "settings_gpxExportRepeatersRoom": "Repeater- och rumsserverplatser", "settings_gpxExportRepeaters": "Exportera repeater / rumsservrar till GPX", "settings_gpxExportAllSubtitle": "Exporterar alla kontakter med en plats till GPX-fil.", - "settings_gpxExportContacts": "Exportera följeslagare till GPX", - "settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.", - "settings_gpxExportChat": "Medhjälparplatser", - "settings_gpxExportError": "Det uppstod ett fel när data exporterades.", + "settings_gpxExportContacts": "Exportera följeslagare till GPX", + "settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.", + "settings_gpxExportChat": "Medhjälparplatser", + "settings_gpxExportError": "Det uppstod ett fel när data exporterades.", "settings_gpxExportAllContacts": "Alla kontakters platser", "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", - "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open", + "settings_gpxExportShareText": "Kartdata exporterad frÃ¥n meshcore-open", "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!", - "pathTrace_clearTooltip": "Rensa väg", - "map_pathTraceCancelled": "Sökvägsspårning avbruten.", - "map_runTrace": "Kör spårsökning", - "map_tapToAdd": "Tryck på noder för att lägga till dem i banan.", + "pathTrace_clearTooltip": "Rensa väg", + "map_pathTraceCancelled": "SökvägsspÃ¥rning avbruten.", + "map_runTrace": "Kör spÃ¥rsökning", + "map_tapToAdd": "Tryck pÃ¥ noder för att lägga till dem i banan.", "map_removeLast": "Ta bort sista", "scanner_enableBluetooth": "Aktivera Bluetooth", - "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", - "scanner_chromeRequired": "Chrome-webbläsare krävs", - "scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.", - "scanner_bluetoothOff": "Bluetooth är avstängt", + "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", + "scanner_chromeRequired": "Chrome-webbläsare krävs", + "scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.", + "scanner_bluetoothOff": "Bluetooth är avstängt", "snrIndicator_lastSeen": "Senast sedd", - "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", - "chat_ShowAllPaths": "Visa alla vägar", - "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", - "settings_clientRepeat": "Upprepa utan elnät", - "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.", - "settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)", + "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", + "chat_ShowAllPaths": "Visa alla vägar", + "settings_clientRepeatSubtitle": "LÃ¥t enheten repetera nätpaket för andra användare.", + "settings_clientRepeat": "Upprepa utan elnät", + "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.", + "settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Enheter", "appSettings_unitsMetric": "Metriskt (m/km)", "appSettings_unitsImperial": "Imperialt (ft / mi)", "map_lineOfSight": "Synlinje", "map_losScreenTitle": "Synlinje", - "losSelectStartEnd": "Välj start- och slutnoder för LOS.", + "losSelectStartEnd": "Välj start- och slutnoder för LOS.", "losRunFailed": "Synlinjekontroll misslyckades: {error}", "@losRunFailed": { "placeholders": { @@ -1630,11 +1630,11 @@ } }, "losClearAllPoints": "Rensa alla punkter", - "losRunToViewElevationProfile": "Kör LOS för att se höjdprofil", + "losRunToViewElevationProfile": "Kör LOS för att se höjdprofil", "losMenuTitle": "LOS-menyn", - "losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter", + "losMenuSubtitle": "Tryck pÃ¥ noder eller tryck länge pÃ¥ kartan för anpassade punkter", "losShowDisplayNodes": "Visa displaynoder", - "losCustomPoints": "Anpassade poäng", + "losCustomPoints": "Anpassade poäng", "losCustomPointLabel": "Anpassad {index}", "@losCustomPointLabel": { "placeholders": { @@ -1667,8 +1667,8 @@ } } }, - "losRun": "Kör LOS", - "losNoElevationData": "Inga höjddata", + "losRun": "Kör LOS", + "losNoElevationData": "Inga höjddata", "losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { @@ -1705,7 +1705,7 @@ }, "losStatusChecking": "LOS: kollar...", "losStatusNoData": "LOS: inga data", - "losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd", + "losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.", - "losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.", - "losRenameCustomPoint": "Byt namn på anpassad punkt", + "losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.", + "losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.", + "losRenameCustomPoint": "Byt namn pÃ¥ anpassad punkt", "losPointName": "Punktnamn", "losShowPanelTooltip": "Visa LOS-panelen", - "losHidePanelTooltip": "Dölj LOS-panelen", - "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", + "losHidePanelTooltip": "Dölj LOS-panelen", + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Radiohorisont", "losLegendLosBeam": "Siktlinje", - "losLegendTerrain": "Terräng", + "losLegendTerrain": "Terräng", "losFrequencyLabel": "Frekvens", - "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", - "losFrequencyDialogTitle": "Beräkning av radiohorisonten", - "losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", + "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", + "losFrequencyDialogTitle": "Beräkning av radiohorisonten", + "losFrequencyDialogDescription": "Med start frÃ¥n k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,8 +1753,8 @@ } } }, - "listFilter_removeFromFavorites": "Ta bort från favoriter", - "listFilter_addToFavorites": "Lägg till i favoriter", + "listFilter_removeFromFavorites": "Ta bort frÃ¥n favoriter", + "listFilter_addToFavorites": "Lägg till i favoriter", "listFilter_favorites": "Favoriter", "@contacts_searchFavorites": { "placeholders": { @@ -1796,19 +1796,17 @@ } } }, - "contacts_unread": "Oläst", - "contacts_searchContactsNoNumber": "Sök kontakter...", - "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", - "contacts_searchFavorites": "Sök {number}{str} Favoriter...", - "contacts_searchUsers": "Sök {number}{str} användare...", - "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", + "contacts_unread": "Oläst", + "contacts_searchContactsNoNumber": "Sök kontakter...", + "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", + "contacts_searchFavorites": "Sök {number}{str} Favoriter...", + "contacts_searchUsers": "Sök {number}{str} användare...", + "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceSubtitle": "Välj hur du vill komma åt din MeshCore-enhet.", - "connectionChoiceTitle": "Välj din anslutningsmetod", "usbScreenTitle": "Anslut via USB", - "usbScreenNote": "USB-seriell kommunikation är aktiv på kompatibla Android-enheter och datorplattformar.", - "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", - "usbScreenStatus": "Välj en USB-enhet", + "usbScreenNote": "USB-seriell kommunikation är aktiv pÃ¥ kompatibla Android-enheter och datorplattformar.", + "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", + "usbScreenStatus": "Välj en USB-enhet", "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 5b0bde5..cc02c86 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", +{ + "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,34 +9,34 @@ }, "@@locale": "uk", "appTitle": "MeshCore Open", - "nav_contacts": "Контакти", - "nav_channels": "Канали", - "nav_map": "Карта", - "common_cancel": "Скасувати", - "common_connect": "Підключити", - "common_unknownDevice": "Невідомий пристрій", - "common_save": "Зберегти", - "common_delete": "Видалити", - "common_close": "Закрити", - "common_edit": "Редагувати", - "common_add": "Додати", - "common_settings": "Налаштування", - "common_disconnect": "Відключити", - "common_connected": "Підключено", - "common_disconnected": "Відключено", - "common_create": "Створити", - "common_continue": "Продовжити", - "common_share": "Поділитися", - "common_copy": "Копіювати", - "common_retry": "Повторити", - "common_hide": "Приховати", - "common_remove": "Прибрати", - "common_enable": "Увімкнути", - "common_disable": "Вимкнути", - "common_reboot": "Перезавантажити", - "common_loading": "Завантаження...", - "common_notAvailable": "—", - "common_voltageValue": "{volts} В", + "nav_contacts": "Контакти", + "nav_channels": "Канали", + "nav_map": "Карта", + "common_cancel": "Скасувати", + "common_connect": "Підключити", + "common_unknownDevice": "Невідомий пристрій", + "common_save": "Зберегти", + "common_delete": "Видалити", + "common_close": "Закрити", + "common_edit": "Редагувати", + "common_add": "Додати", + "common_settings": "Налаштування", + "common_disconnect": "Відключити", + "common_connected": "Підключено", + "common_disconnected": "Відключено", + "common_create": "Створити", + "common_continue": "Продовжити", + "common_share": "Поділитися", + "common_copy": "Копіювати", + "common_retry": "Повторити", + "common_hide": "Приховати", + "common_remove": "Прибрати", + "common_enable": "Увімкнути", + "common_disable": "Вимкнути", + "common_reboot": "Перезавантажити", + "common_loading": "Завантаження...", + "common_notAvailable": "—", + "common_voltageValue": "{volts} Ð’", "@common_voltageValue": { "placeholders": { "volts": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Пошук пристроїв...", - "scanner_connecting": "Підключення...", - "scanner_disconnecting": "Відключення...", - "scanner_notConnected": "Не підключено", - "scanner_connectedTo": "Підключено до {deviceName}", + "scanner_scanning": "Пошук пристроїв...", + "scanner_connecting": "Підключення...", + "scanner_disconnecting": "Відключення...", + "scanner_notConnected": "Не підключено", + "scanner_connectedTo": "Підключено до {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,9 +65,9 @@ } } }, - "scanner_searchingDevices": "Пошук пристроїв MeshCore...", - "scanner_tapToScan": "Натисніть «Сканувати», щоб знайти пристрої MeshCore", - "scanner_connectionFailed": "Помилка підключення: {error}", + "scanner_searchingDevices": "Пошук пристроїв MeshCore...", + "scanner_tapToScan": "Натисніть «Сканувати», щоб знайти пристрої MeshCore", + "scanner_connectionFailed": "Помилка підключення: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,52 +75,52 @@ } } }, - "scanner_stop": "Стоп", - "scanner_scan": "Сканувати", - "device_quickSwitch": "Швидке перемикання", + "scanner_stop": "Стоп", + "scanner_scan": "Сканувати", + "device_quickSwitch": "Швидке перемикання", "device_meshcore": "MeshCore", - "settings_title": "Налаштування", - "settings_deviceInfo": "Інформація про пристрій", - "settings_appSettings": "Налаштування програми", - "settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти", - "settings_nodeSettings": "Налаштування вузла", - "settings_nodeName": "Ім'я вузла", - "settings_nodeNameNotSet": "Не встановлено", - "settings_nodeNameHint": "Введіть ім'я вузла", - "settings_nodeNameUpdated": "Ім'я оновлено", - "settings_radioSettings": "Налаштування радіо", - "settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення", - "settings_radioSettingsUpdated": "Налаштування радіо оновлено", - "settings_location": "Розташування", - "settings_locationSubtitle": "GPS координати", - "settings_locationUpdated": "Розташування оновлено", - "settings_locationBothRequired": "Введіть широту та довготу.", - "settings_locationInvalid": "Некоректна широта або довгота.", - "settings_latitude": "Широта", - "settings_longitude": "Довгота", - "settings_privacyMode": "Режим приватності", - "settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", - "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.", - "settings_privacyModeEnabled": "Режим приватності увімкнено", - "settings_privacyModeDisabled": "Режим приватності вимкнено", - "settings_actions": "Дії", - "settings_sendAdvertisement": "Оголосити себе", - "settings_sendAdvertisementSubtitle": "Транслювати присутність зараз", - "settings_advertisementSent": "Оголошення надіслано", - "settings_syncTime": "Синхронізація часу", - "settings_syncTimeSubtitle": "Встановити час пристрою відповідно до часу телефону.", - "settings_timeSynchronized": "Час синхронізовано", - "settings_refreshContacts": "Оновити контакти", - "settings_refreshContactsSubtitle": "Перезавантажити список контактів з пристрою", - "settings_rebootDevice": "Перезавантажити пристрій", - "settings_rebootDeviceSubtitle": "Перезавантажити пристрій MeshCore", - "settings_rebootDeviceConfirm": "Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.", - "settings_debug": "Налагодження", - "settings_bleDebugLog": "Журнал налагодження BLE", - "settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані", - "settings_appDebugLog": "Журнал налагодження програми", - "settings_appDebugLogSubtitle": "Повідомлення налагодження програми", - "settings_about": "Про програму", + "settings_title": "Налаштування", + "settings_deviceInfo": "Інформація про пристрій", + "settings_appSettings": "Налаштування програми", + "settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти", + "settings_nodeSettings": "Налаштування вузла", + "settings_nodeName": "Ім'я вузла", + "settings_nodeNameNotSet": "Не встановлено", + "settings_nodeNameHint": "Введіть ім'я вузла", + "settings_nodeNameUpdated": "Ім'я оновлено", + "settings_radioSettings": "Налаштування радіо", + "settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення", + "settings_radioSettingsUpdated": "Налаштування радіо оновлено", + "settings_location": "Розташування", + "settings_locationSubtitle": "GPS координати", + "settings_locationUpdated": "Розташування оновлено", + "settings_locationBothRequired": "Введіть широту та довготу.", + "settings_locationInvalid": "Некоректна широта або довгота.", + "settings_latitude": "Широта", + "settings_longitude": "Довгота", + "settings_privacyMode": "Режим приватності", + "settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", + "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.", + "settings_privacyModeEnabled": "Режим приватності увімкнено", + "settings_privacyModeDisabled": "Режим приватності вимкнено", + "settings_actions": "Дії", + "settings_sendAdvertisement": "Оголосити себе", + "settings_sendAdvertisementSubtitle": "Транслювати присутність зараз", + "settings_advertisementSent": "Оголошення надіслано", + "settings_syncTime": "Синхронізація часу", + "settings_syncTimeSubtitle": "Встановити час пристрою відповідно до часу телефону.", + "settings_timeSynchronized": "Час синхронізовано", + "settings_refreshContacts": "Оновити контакти", + "settings_refreshContactsSubtitle": "Перезавантажити список контактів з пристрою", + "settings_rebootDevice": "Перезавантажити пристрій", + "settings_rebootDeviceSubtitle": "Перезавантажити пристрій MeshCore", + "settings_rebootDeviceConfirm": "Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.", + "settings_debug": "Налагодження", + "settings_bleDebugLog": "Журнал налагодження BLE", + "settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані", + "settings_appDebugLog": "Журнал налагодження програми", + "settings_appDebugLogSubtitle": "Повідомлення налагодження програми", + "settings_about": "Про програму", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -129,26 +129,26 @@ } } }, - "settings_aboutLegalese": "Проєкт MeshCore Open Source 2026", - "settings_aboutDescription": "Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.", - "settings_infoName": "Ім'я", + "settings_aboutLegalese": "Проєкт MeshCore Open Source 2026", + "settings_aboutDescription": "Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.", + "settings_infoName": "Ім'я", "settings_infoId": "ID", - "settings_infoStatus": "Статус", - "settings_infoBattery": "Батарея", - "settings_infoPublicKey": "Відкритий ключ", - "settings_infoContactsCount": "Кількість контактів", - "settings_infoChannelCount": "Кількість каналів", - "settings_presets": "Попередні налаштування", - "settings_frequency": "Частота (МГц)", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батарея", + "settings_infoPublicKey": "Відкритий ключ", + "settings_infoContactsCount": "Кількість контактів", + "settings_infoChannelCount": "Кількість каналів", + "settings_presets": "Попередні налаштування", + "settings_frequency": "Частота (МГц)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)", - "settings_bandwidth": "Смуга пропускання", - "settings_spreadingFactor": "Коефіцієнт розширення", - "settings_codingRate": "Швидкість кодування", - "settings_txPower": "Потужність TX (дБм)", + "settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)", + "settings_bandwidth": "Смуга пропускання", + "settings_spreadingFactor": "Коефіцієнт розширення", + "settings_codingRate": "Швидкість кодування", + "settings_txPower": "Потужність TX (дБм)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)", - "settings_error": "Помилка: {message}", + "settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)", + "settings_error": "Помилка: {message}", "@settings_error": { "placeholders": { "message": { @@ -156,52 +156,52 @@ } } }, - "appSettings_title": "Налаштування програми", - "appSettings_appearance": "Вигляд", - "appSettings_theme": "Тема", - "appSettings_themeSystem": "Системна", - "appSettings_themeLight": "Світла", - "appSettings_themeDark": "Темна", - "appSettings_language": "Мова", - "appSettings_languageSystem": "Як у системі", + "appSettings_title": "Налаштування програми", + "appSettings_appearance": "Вигляд", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Системна", + "appSettings_themeLight": "Світла", + "appSettings_themeDark": "Темна", + "appSettings_language": "Мова", + "appSettings_languageSystem": "Як у системі", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_languageUk": "Українська", - "appSettings_notifications": "Сповіщення", - "appSettings_enableNotifications": "Увімкнути сповіщення", - "appSettings_enableNotificationsSubtitle": "Отримувати сповіщення про повідомлення та оголошення", - "appSettings_notificationPermissionDenied": "У доступі до сповіщень відмовлено", - "appSettings_notificationsEnabled": "Сповіщення увімкнено", - "appSettings_notificationsDisabled": "Сповіщення вимкнено", - "appSettings_messageNotifications": "Сповіщення про повідомлення", - "appSettings_messageNotificationsSubtitle": "Показувати сповіщення при отриманні нових повідомлень", - "appSettings_channelMessageNotifications": "Сповіщення каналів", - "appSettings_channelMessageNotificationsSubtitle": "Показувати сповіщення при отриманні повідомлень каналу", - "appSettings_advertisementNotifications": "Сповіщення про оголошення", - "appSettings_advertisementNotificationsSubtitle": "Показувати сповіщення при виявленні нових вузлів", - "appSettings_messaging": "Обмін повідомленнями", - "appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб", - "appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання", - "appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.", - "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.", - "appSettings_autoRouteRotation": "Авторотація маршруту", - "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)", - "appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена", - "appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена", - "appSettings_battery": "Батарея", - "appSettings_batteryChemistry": "Хімія батареї", - "appSettings_batteryChemistryPerDevice": "Встановити для пристрою ({deviceName})", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_languageUk": "Українська", + "appSettings_notifications": "Сповіщення", + "appSettings_enableNotifications": "Увімкнути сповіщення", + "appSettings_enableNotificationsSubtitle": "Отримувати сповіщення про повідомлення та оголошення", + "appSettings_notificationPermissionDenied": "У доступі до сповіщень відмовлено", + "appSettings_notificationsEnabled": "Сповіщення увімкнено", + "appSettings_notificationsDisabled": "Сповіщення вимкнено", + "appSettings_messageNotifications": "Сповіщення про повідомлення", + "appSettings_messageNotificationsSubtitle": "Показувати сповіщення при отриманні нових повідомлень", + "appSettings_channelMessageNotifications": "Сповіщення каналів", + "appSettings_channelMessageNotificationsSubtitle": "Показувати сповіщення при отриманні повідомлень каналу", + "appSettings_advertisementNotifications": "Сповіщення про оголошення", + "appSettings_advertisementNotificationsSubtitle": "Показувати сповіщення при виявленні нових вузлів", + "appSettings_messaging": "Обмін повідомленнями", + "appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб", + "appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання", + "appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.", + "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.", + "appSettings_autoRouteRotation": "Авторотація маршруту", + "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)", + "appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена", + "appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена", + "appSettings_battery": "Батарея", + "appSettings_batteryChemistry": "Хімія батареї", + "appSettings_batteryChemistryPerDevice": "Встановити для пристрою ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -209,20 +209,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Підключіть пристрій, щоб вибрати", - "appSettings_batteryNmc": "18650 NMC (3.0-4.2В)", - "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65В)", - "appSettings_batteryLipo": "LiPo (3.0-4.2В)", - "appSettings_mapDisplay": "Відображення карти", - "appSettings_showRepeaters": "Показувати ретранслятори", - "appSettings_showRepeatersSubtitle": "Відображати вузли-ретранслятори на карті", - "appSettings_showChatNodes": "Показувати вузли чату", - "appSettings_showChatNodesSubtitle": "Відображати вузли чату на карті", - "appSettings_showOtherNodes": "Показувати інші вузли", - "appSettings_showOtherNodesSubtitle": "Відображати інші типи вузлів на карті", - "appSettings_timeFilter": "Фільтр часу", - "appSettings_timeFilterShowAll": "Показати всі вузли", - "appSettings_timeFilterShowLast": "Показати вузли за останні {hours} год", + "appSettings_batteryChemistryConnectFirst": "Підключіть пристрій, щоб вибрати", + "appSettings_batteryNmc": "18650 NMC (3.0-4.2Ð’)", + "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65Ð’)", + "appSettings_batteryLipo": "LiPo (3.0-4.2Ð’)", + "appSettings_mapDisplay": "Відображення карти", + "appSettings_showRepeaters": "Показувати ретранслятори", + "appSettings_showRepeatersSubtitle": "Відображати вузли-ретранслятори на карті", + "appSettings_showChatNodes": "Показувати вузли чату", + "appSettings_showChatNodesSubtitle": "Відображати вузли чату на карті", + "appSettings_showOtherNodes": "Показувати інші вузли", + "appSettings_showOtherNodesSubtitle": "Відображати інші типи вузлів на карті", + "appSettings_timeFilter": "Фільтр часу", + "appSettings_timeFilterShowAll": "Показати всі вузли", + "appSettings_timeFilterShowLast": "Показати вузли за останні {hours} год", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,16 +230,16 @@ } } }, - "appSettings_mapTimeFilter": "Фільтр часу карти", - "appSettings_showNodesDiscoveredWithin": "Показувати вузли, виявлені за:", - "appSettings_allTime": "Весь час", - "appSettings_lastHour": "Останню годину", - "appSettings_last6Hours": "Останні 6 годин", - "appSettings_last24Hours": "Останні 24 години", - "appSettings_lastWeek": "Минулий тиждень", - "appSettings_offlineMapCache": "Офлайн-кеш карти", - "appSettings_noAreaSelected": "Область не вибрано", - "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", + "appSettings_mapTimeFilter": "Фільтр часу карти", + "appSettings_showNodesDiscoveredWithin": "Показувати вузли, виявлені за:", + "appSettings_allTime": "Весь час", + "appSettings_lastHour": "Останню годину", + "appSettings_last6Hours": "Останні 6 годин", + "appSettings_last24Hours": "Останні 24 години", + "appSettings_lastWeek": "Минулий тиждень", + "appSettings_offlineMapCache": "Офлайн-кеш карти", + "appSettings_noAreaSelected": "Область не вибрано", + "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,19 +250,19 @@ } } }, - "appSettings_debugCard": "Налагодження", - "appSettings_appDebugLogging": "Логування налагодження програми", - "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.", - "appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено", - "appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.", - "contacts_title": "Контакти", - "contacts_noContacts": "Контактів не знайдено.", - "contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.", - "contacts_searchContacts": "Пошук контактів...", - "contacts_noUnreadContacts": "Немає непрочитаних контактів", - "contacts_noContactsFound": "Контактів або груп не знайдено.", - "contacts_deleteContact": "Видалити контакт", - "contacts_removeConfirm": "Видалити {contactName} з контактів?", + "appSettings_debugCard": "Налагодження", + "appSettings_appDebugLogging": "Логування налагодження програми", + "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.", + "appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено", + "appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.", + "contacts_title": "Контакти", + "contacts_noContacts": "Контактів не знайдено.", + "contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.", + "contacts_searchContacts": "Пошук контактів...", + "contacts_noUnreadContacts": "Немає непрочитаних контактів", + "contacts_noContactsFound": "Контактів або груп не знайдено.", + "contacts_deleteContact": "Видалити контакт", + "contacts_removeConfirm": "Видалити {contactName} з контактів?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -270,12 +270,12 @@ } } }, - "contacts_manageRepeater": "Керувати ретранслятором", - "contacts_roomLogin": "Вхід у кімнату", - "contacts_openChat": "Відкрити чат", - "contacts_editGroup": "Редагувати групу", - "contacts_deleteGroup": "Видалити групу", - "contacts_deleteGroupConfirm": "Видалити {groupName}?", + "contacts_manageRepeater": "Керувати ретранслятором", + "contacts_roomLogin": "Вхід у кімнату", + "contacts_openChat": "Відкрити чат", + "contacts_editGroup": "Редагувати групу", + "contacts_deleteGroup": "Видалити групу", + "contacts_deleteGroupConfirm": "Видалити {groupName}?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -283,10 +283,10 @@ } } }, - "contacts_newGroup": "Нова група", - "contacts_groupName": "Назва групи", - "contacts_groupNameRequired": "Назва групи обов'язкова.", - "contacts_groupAlreadyExists": "Група «{name}» вже існує.", + "contacts_newGroup": "Нова група", + "contacts_groupName": "Назва групи", + "contacts_groupNameRequired": "Назва групи обов'язкова.", + "contacts_groupAlreadyExists": "Група «{name}» вже існує.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,11 +294,11 @@ } } }, - "contacts_filterContacts": "Фільтрувати контакти...", - "contacts_noContactsMatchFilter": "Жоден контакт не відповідає фільтру.", - "contacts_noMembers": "Немає учасників", - "contacts_lastSeenNow": "В мережі", - "contacts_lastSeenMinsAgo": "В мережі {minutes} хв. тому", + "contacts_filterContacts": "Фільтрувати контакти...", + "contacts_noContactsMatchFilter": "Жоден контакт не відповідає фільтру.", + "contacts_noMembers": "Немає учасників", + "contacts_lastSeenNow": "Ð’ мережі", + "contacts_lastSeenMinsAgo": "Ð’ мережі {minutes} хв. тому", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -306,8 +306,8 @@ } } }, - "contacts_lastSeenHourAgo": "В мережі 1 годину тому", - "contacts_lastSeenHoursAgo": "В мережі {hours} год. тому", + "contacts_lastSeenHourAgo": "Ð’ мережі 1 годину тому", + "contacts_lastSeenHoursAgo": "Ð’ мережі {hours} год. тому", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -315,8 +315,8 @@ } } }, - "contacts_lastSeenDayAgo": "В мережі 1 день тому", - "contacts_lastSeenDaysAgo": "В мережі {days} дн. тому", + "contacts_lastSeenDayAgo": "Ð’ мережі 1 день тому", + "contacts_lastSeenDaysAgo": "Ð’ мережі {days} дн. тому", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -324,12 +324,12 @@ } } }, - "channels_title": "Канали", - "channels_noChannelsConfigured": "Канали не налаштовані", - "channels_addPublicChannel": "Додати публічний канал", - "channels_searchChannels": "Пошук каналів...", - "channels_noChannelsFound": "Каналів не знайдено", - "channels_channelIndex": "Канал {index}", + "channels_title": "Канали", + "channels_noChannelsConfigured": "Канали не налаштовані", + "channels_addPublicChannel": "Додати публічний канал", + "channels_searchChannels": "Пошук каналів...", + "channels_noChannelsFound": "Каналів не знайдено", + "channels_channelIndex": "Канал {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -337,16 +337,16 @@ } } }, - "channels_hashtagChannel": "Канал з хештегом", - "channels_public": "Публічний", - "channels_private": "Приватний", - "channels_publicChannel": "Публічний канал", - "channels_privateChannel": "Приватний канал", - "channels_editChannel": "Редагувати канал", - "channels_muteChannel": "Вимкнути сповіщення каналу", - "channels_unmuteChannel": "Увімкнути сповіщення каналу", - "channels_deleteChannel": "Видалити канал", - "channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.", + "channels_hashtagChannel": "Канал з хештегом", + "channels_public": "Публічний", + "channels_private": "Приватний", + "channels_publicChannel": "Публічний канал", + "channels_privateChannel": "Приватний канал", + "channels_editChannel": "Редагувати канал", + "channels_muteChannel": "Вимкнути сповіщення каналу", + "channels_unmuteChannel": "Увімкнути сповіщення каналу", + "channels_deleteChannel": "Видалити канал", + "channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -354,7 +354,7 @@ } } }, - "channels_channelDeleted": "Канал «{name}» видалено", + "channels_channelDeleted": "Канал «{name}» видалено", "@channels_channelDeleted": { "placeholders": { "name": { @@ -362,16 +362,16 @@ } } }, - "channels_addChannel": "Додати канал", - "channels_channelIndexLabel": "Індекс каналу", - "channels_channelName": "Назва каналу", - "channels_usePublicChannel": "Використовувати публічний канал", - "channels_standardPublicPsk": "Стандартний публічний PSK", + "channels_addChannel": "Додати канал", + "channels_channelIndexLabel": "Індекс каналу", + "channels_channelName": "Назва каналу", + "channels_usePublicChannel": "Використовувати публічний канал", + "channels_standardPublicPsk": "Стандартний публічний PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Згенерувати випадковий ключ PSK", - "channels_enterChannelName": "Будь ласка, введіть назву каналу", - "channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.", - "channels_channelAdded": "Канал «{name}» додано", + "channels_generateRandomPsk": "Згенерувати випадковий ключ PSK", + "channels_enterChannelName": "Будь ласка, введіть назву каналу", + "channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.", + "channels_channelAdded": "Канал «{name}» додано", "@channels_channelAdded": { "placeholders": { "name": { @@ -379,7 +379,7 @@ } } }, - "channels_editChannelTitle": "Редагувати канал {index}", + "channels_editChannelTitle": "Редагувати канал {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -387,8 +387,8 @@ } } }, - "channels_smazCompression": "Стиснення SMAZ", - "channels_channelUpdated": "Канал «{name}» оновлено", + "channels_smazCompression": "Стиснення SMAZ", + "channels_channelUpdated": "Канал «{name}» оновлено", "@channels_channelUpdated": { "placeholders": { "name": { @@ -396,16 +396,16 @@ } } }, - "channels_publicChannelAdded": "Публічний канал додано", - "channels_sortBy": "Сортувати за", - "channels_sortManual": "Вручну", - "channels_sortAZ": "А-Я", - "channels_sortLatestMessages": "Останні повідомлення", - "channels_sortUnread": "Непрочитані", - "chat_noMessages": "Поки немає повідомлень.", - "chat_sendMessageToStart": "Надішліть повідомлення, щоб почати", - "chat_originalMessageNotFound": "Оригінальне повідомлення не знайдено", - "chat_replyingTo": "Відповідь {name}", + "channels_publicChannelAdded": "Публічний канал додано", + "channels_sortBy": "Сортувати за", + "channels_sortManual": "Вручну", + "channels_sortAZ": "А-Я", + "channels_sortLatestMessages": "Останні повідомлення", + "channels_sortUnread": "Непрочитані", + "chat_noMessages": "Поки немає повідомлень.", + "chat_sendMessageToStart": "Надішліть повідомлення, щоб почати", + "chat_originalMessageNotFound": "Оригінальне повідомлення не знайдено", + "chat_replyingTo": "Відповідь {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -413,7 +413,7 @@ } } }, - "chat_replyTo": "Відповісти {name}", + "chat_replyTo": "Відповісти {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,8 +421,8 @@ } } }, - "chat_location": "Розташування", - "chat_sendMessageTo": "Надіслати повідомлення {contactName}", + "chat_location": "Розташування", + "chat_sendMessageTo": "Надіслати повідомлення {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -430,8 +430,8 @@ } } }, - "chat_typeMessage": "Введіть повідомлення...", - "chat_messageTooLong": "Повідомлення занадто довге (макс. {maxBytes} байт).", + "chat_typeMessage": "Введіть повідомлення...", + "chat_messageTooLong": "Повідомлення занадто довге (макс. {maxBytes} байт).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -439,10 +439,10 @@ } } }, - "chat_messageCopied": "Повідомлення скопійовано", - "chat_messageDeleted": "Повідомлення видалено", - "chat_retryingMessage": "Спроба відновлення.", - "chat_retryCount": "Повторна спроба {current}/{max}", + "chat_messageCopied": "Повідомлення скопійовано", + "chat_messageDeleted": "Повідомлення видалено", + "chat_retryingMessage": "Спроба відновлення.", + "chat_retryCount": "Повторна спроба {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -453,33 +453,33 @@ } } }, - "chat_sendGif": "Надіслати GIF", - "chat_reply": "Відповісти", - "chat_addReaction": "Додати реакцію", - "chat_me": "Я", - "emojiCategorySmileys": "Емодзі", - "emojiCategoryGestures": "Жести", - "emojiCategoryHearts": "Серця", - "emojiCategoryObjects": "Об'єкти", - "gifPicker_title": "Вибрати GIF", - "gifPicker_searchHint": "Пошук GIF...", - "gifPicker_poweredBy": "На базі GIPHY", - "gifPicker_noGifsFound": "GIF не знайдено", - "gifPicker_failedLoad": "Не вдалося завантажити GIF-файли", - "gifPicker_failedSearch": "Пошук GIF не вдався", - "gifPicker_noInternet": "Немає інтернет-з'єднання", - "debugLog_appTitle": "Журнал налагодження програми", - "debugLog_bleTitle": "Журнал налагодження BLE", - "debugLog_copyLog": "Копіювати журнал", - "debugLog_clearLog": "Очистити журнал", - "debugLog_copied": "Журнал налагодження скопійовано", - "debugLog_bleCopied": "Журнал BLE скопійовано", - "debugLog_noEntries": "Поки що немає записів журналу налагодження.", - "debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях", - "debugLog_frames": "Кадри", - "debugLog_rawLogRx": "Необроблений лог - RX", - "debugLog_noBleActivity": "Поки що немає активності BLE.", - "debugFrame_length": "Довжина кадру: {count} байт", + "chat_sendGif": "Надіслати GIF", + "chat_reply": "Відповісти", + "chat_addReaction": "Додати реакцію", + "chat_me": "Я", + "emojiCategorySmileys": "Емодзі", + "emojiCategoryGestures": "Жести", + "emojiCategoryHearts": "Серця", + "emojiCategoryObjects": "Об'єкти", + "gifPicker_title": "Вибрати GIF", + "gifPicker_searchHint": "Пошук GIF...", + "gifPicker_poweredBy": "На базі GIPHY", + "gifPicker_noGifsFound": "GIF не знайдено", + "gifPicker_failedLoad": "Не вдалося завантажити GIF-файли", + "gifPicker_failedSearch": "Пошук GIF не вдався", + "gifPicker_noInternet": "Немає інтернет-з'єднання", + "debugLog_appTitle": "Журнал налагодження програми", + "debugLog_bleTitle": "Журнал налагодження BLE", + "debugLog_copyLog": "Копіювати журнал", + "debugLog_clearLog": "Очистити журнал", + "debugLog_copied": "Журнал налагодження скопійовано", + "debugLog_bleCopied": "Журнал BLE скопійовано", + "debugLog_noEntries": "Поки що немає записів журналу налагодження.", + "debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях", + "debugLog_frames": "Кадри", + "debugLog_rawLogRx": "Необроблений лог - RX", + "debugLog_noBleActivity": "Поки що немає активності BLE.", + "debugFrame_length": "Довжина кадру: {count} байт", "@debugFrame_length": { "placeholders": { "count": { @@ -487,7 +487,7 @@ } } }, - "debugFrame_command": "Команда: 0x{value}", + "debugFrame_command": "Команда: 0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -495,8 +495,8 @@ } } }, - "debugFrame_textMessageHeader": "Повідомлення:", - "debugFrame_destinationPubKey": "- PubKey призначення: {pubKey}", + "debugFrame_textMessageHeader": "Повідомлення:", + "debugFrame_destinationPubKey": "- PubKey призначення: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -504,7 +504,7 @@ } } }, - "debugFrame_timestamp": "- Мітка часу: {timestamp}", + "debugFrame_timestamp": "- Мітка часу: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -512,7 +512,7 @@ } } }, - "debugFrame_flags": "- Прапорці: 0x{value}", + "debugFrame_flags": "- Прапорці: 0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -520,7 +520,7 @@ } } }, - "debugFrame_textType": "- Тип тексту: {type} ({label})", + "debugFrame_textType": "- Тип тексту: {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -532,8 +532,8 @@ } }, "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Звичайний", - "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_textTypePlain": "Звичайний", + "debugFrame_text": "- Текст: \"{text}\"", "@debugFrame_text": { "placeholders": { "text": { @@ -541,16 +541,16 @@ } } }, - "debugFrame_hexDump": "Дамп Hex:", - "chat_pathManagement": "Керування шляхами", - "chat_routingMode": "Режим маршрутизації", - "chat_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "chat_forceFloodMode": "Примусово на всю мережу", - "chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):", - "chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.", - "chat_hopSingular": "Стрибок", - "chat_hopPlural": "стрибків", - "chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}", + "debugFrame_hexDump": "Дамп Hex:", + "chat_pathManagement": "Керування шляхами", + "chat_routingMode": "Режим маршрутизації", + "chat_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "chat_forceFloodMode": "Примусово на всю мережу", + "chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):", + "chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.", + "chat_hopSingular": "Стрибок", + "chat_hopPlural": "стрибків", + "chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -558,20 +558,20 @@ } } }, - "chat_successes": "Успішно", - "chat_removePath": "Видалити шлях", - "chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.", - "chat_pathActions": "Дії зі шляхом:", - "chat_setCustomPath": "Встановити власний шлях", - "chat_setCustomPathSubtitle": "Вказати шлях маршрутизації вручну", - "chat_clearPath": "Очистити шлях", - "chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні", - "chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.", - "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми", - "chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", - "chat_fullPath": "Повний шлях", - "chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.", - "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}", + "chat_successes": "Успішно", + "chat_removePath": "Видалити шлях", + "chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.", + "chat_pathActions": "Дії зі шляхом:", + "chat_setCustomPath": "Встановити власний шлях", + "chat_setCustomPathSubtitle": "Вказати шлях маршрутизації вручну", + "chat_clearPath": "Очистити шлях", + "chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні", + "chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.", + "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми", + "chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", + "chat_fullPath": "Повний шлях", + "chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.", + "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -582,16 +582,16 @@ } } }, - "chat_pathSavedLocally": "Збережено локально. Підключіться для синхронізації.", - "chat_pathDeviceConfirmed": "Пристрій підтверджено.", - "chat_pathDeviceNotConfirmed": "Пристрій ще не підтверджено.", - "chat_type": "Ввід", - "chat_path": "Шлях", - "chat_publicKey": "Відкритий ключ", - "chat_compressOutgoingMessages": "Стискати вихідні повідомлення", - "chat_floodForced": "На всю мережу (примусово)", - "chat_directForced": "Прямий (примусово)", - "chat_hopsForced": "{count} стрибків (примусово)", + "chat_pathSavedLocally": "Збережено локально. Підключіться для синхронізації.", + "chat_pathDeviceConfirmed": "Пристрій підтверджено.", + "chat_pathDeviceNotConfirmed": "Пристрій ще не підтверджено.", + "chat_type": "Ввід", + "chat_path": "Шлях", + "chat_publicKey": "Відкритий ключ", + "chat_compressOutgoingMessages": "Стискати вихідні повідомлення", + "chat_floodForced": "На всю мережу (примусово)", + "chat_directForced": "Прямий (примусово)", + "chat_hopsForced": "{count} стрибків (примусово)", "@chat_hopsForced": { "placeholders": { "count": { @@ -599,10 +599,10 @@ } } }, - "chat_floodAuto": "На всю мережу (авто)", - "chat_direct": "Прямий", - "chat_poiShared": "Точкою інтересу поділилися", - "chat_unread": "Непрочитано: {count}", + "chat_floodAuto": "На всю мережу (авто)", + "chat_direct": "Прямий", + "chat_poiShared": "Точкою інтересу поділилися", + "chat_unread": "Непрочитано: {count}", "@chat_unread": { "placeholders": { "count": { @@ -610,10 +610,10 @@ } } }, - "chat_openLink": "Відкрити посилання?", - "chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?", - "chat_open": "Відкрити", - "chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}", + "chat_openLink": "Відкрити посилання?", + "chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?", + "chat_open": "Відкрити", + "chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -621,11 +621,11 @@ } } }, - "chat_invalidLink": "Невірний формат посилання", - "map_title": "Карта вузлів", - "map_noNodesWithLocation": "Немає вузлів з даними про розташування", - "map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.", - "map_nodesCount": "Вузли: {count}", + "chat_invalidLink": "Невірний формат посилання", + "map_title": "Карта вузлів", + "map_noNodesWithLocation": "Немає вузлів з даними про розташування", + "map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.", + "map_nodesCount": "Вузли: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -633,7 +633,7 @@ } } }, - "map_pinsCount": "Мітки: {count}", + "map_pinsCount": "Мітки: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -641,27 +641,27 @@ } } }, - "map_chat": "Чат", - "map_repeater": "Ретранслятор", - "map_room": "Кімната", - "map_sensor": "Сенсор", - "map_pinDm": "Ключ (DM)", - "map_pinPrivate": "Замок (Приватний)", - "map_pinPublic": "Ключ (Публічний)", - "map_lastSeen": "Останній раз бачили", - "map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", - "map_from": "Від", - "map_source": "Джерело", - "map_flags": "Прапорці", - "map_shareMarkerHere": "Поділитися маркером тут", - "map_pinLabel": "Мітка піна", - "map_label": "Мітка", - "map_pointOfInterest": "Точка інтересу", - "map_sendToContact": "Надіслати контакту", - "map_sendToChannel": "Надіслати в канал", - "map_noChannelsAvailable": "Немає доступних каналів", - "map_publicLocationShare": "Поділитися в публічному місці", - "map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.", + "map_chat": "Чат", + "map_repeater": "Ретранслятор", + "map_room": "Кімната", + "map_sensor": "Сенсор", + "map_pinDm": "Ключ (DM)", + "map_pinPrivate": "Замок (Приватний)", + "map_pinPublic": "Ключ (Публічний)", + "map_lastSeen": "Останній раз бачили", + "map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "map_from": "Від", + "map_source": "Джерело", + "map_flags": "Прапорці", + "map_shareMarkerHere": "Поділитися маркером тут", + "map_pinLabel": "Мітка піна", + "map_label": "Мітка", + "map_pointOfInterest": "Точка інтересу", + "map_sendToContact": "Надіслати контакту", + "map_sendToChannel": "Надіслати в канал", + "map_noChannelsAvailable": "Немає доступних каналів", + "map_publicLocationShare": "Поділитися в публічному місці", + "map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, Ñ– кожен, хто має ключ PSK, може це побачити.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -669,26 +669,26 @@ } } }, - "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами", - "map_filterNodes": "Фільтрувати вузли", - "map_nodeTypes": "Типи вузлів", - "map_chatNodes": "Вузли чату", - "map_repeaters": "Ретранслятори", - "map_otherNodes": "Інші вузли", - "map_keyPrefix": "Префікс ключа", - "map_filterByKeyPrefix": "Фільтрувати за префіксом ключа", - "map_publicKeyPrefix": "Префікс відкритого ключа", - "map_markers": "Маркери", - "map_showSharedMarkers": "Показувати спільні маркери", - "map_lastSeenTime": "Час останньої активності", - "map_sharedPin": "Спільний пін", - "map_joinRoom": "Приєднатися до кімнати", - "map_manageRepeater": "Керувати ретранслятором", - "mapCache_title": "Офлайн-кеш карти", - "mapCache_selectAreaFirst": "Спершу виберіть область для кешування", - "mapCache_noTilesToDownload": "Немає плиток для завантаження в цій області.", - "mapCache_downloadTilesTitle": "Завантажити плитки", - "mapCache_downloadTilesPrompt": "Завантажити {count} плиток для використання офлайн?", + "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами", + "map_filterNodes": "Фільтрувати вузли", + "map_nodeTypes": "Типи вузлів", + "map_chatNodes": "Вузли чату", + "map_repeaters": "Ретранслятори", + "map_otherNodes": "Інші вузли", + "map_keyPrefix": "Префікс ключа", + "map_filterByKeyPrefix": "Фільтрувати за префіксом ключа", + "map_publicKeyPrefix": "Префікс відкритого ключа", + "map_markers": "Маркери", + "map_showSharedMarkers": "Показувати спільні маркери", + "map_lastSeenTime": "Час останньої активності", + "map_sharedPin": "Спільний пін", + "map_joinRoom": "Приєднатися до кімнати", + "map_manageRepeater": "Керувати ретранслятором", + "mapCache_title": "Офлайн-кеш карти", + "mapCache_selectAreaFirst": "Спершу виберіть область для кешування", + "mapCache_noTilesToDownload": "Немає плиток для завантаження в цій області.", + "mapCache_downloadTilesTitle": "Завантажити плитки", + "mapCache_downloadTilesPrompt": "Завантажити {count} плиток для використання офлайн?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -696,8 +696,8 @@ } } }, - "mapCache_downloadAction": "Завантажити", - "mapCache_cachedTiles": "Закешовано {count} плиток", + "mapCache_downloadAction": "Завантажити", + "mapCache_cachedTiles": "Закешовано {count} плиток", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -705,7 +705,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Плитки в кеші ({downloaded}) ({failed} помилок)", + "mapCache_cachedTilesWithFailed": "Плитки в кеші ({downloaded}) ({failed} помилок)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -716,14 +716,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Очистити офлайн-кеш", - "mapCache_clearOfflineCachePrompt": "Видалити всі закешовані плитки карти?", - "mapCache_offlineCacheCleared": "Офлайн-кеш очищено.", - "mapCache_noAreaSelected": "Область не вибрано", - "mapCache_cacheArea": "Область кешування", - "mapCache_useCurrentView": "Використати поточний вигляд", - "mapCache_zoomRange": "Діапазон масштабування", - "mapCache_estimatedTiles": "Оцінка плиток: {count}", + "mapCache_clearOfflineCacheTitle": "Очистити офлайн-кеш", + "mapCache_clearOfflineCachePrompt": "Видалити всі закешовані плитки карти?", + "mapCache_offlineCacheCleared": "Офлайн-кеш очищено.", + "mapCache_noAreaSelected": "Область не вибрано", + "mapCache_cacheArea": "Область кешування", + "mapCache_useCurrentView": "Використати поточний вигляд", + "mapCache_zoomRange": "Діапазон масштабування", + "mapCache_estimatedTiles": "Оцінка плиток: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -731,7 +731,7 @@ } } }, - "mapCache_downloadedTiles": "Завантажено {completed} / {total}", + "mapCache_downloadedTiles": "Завантажено {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -742,9 +742,9 @@ } } }, - "mapCache_downloadTilesButton": "Завантажити плитки", - "mapCache_clearCacheButton": "Очистити кеш", - "mapCache_failedDownloads": "Невдалі завантаження: {count}", + "mapCache_downloadTilesButton": "Завантажити плитки", + "mapCache_clearCacheButton": "Очистити кеш", + "mapCache_failedDownloads": "Невдалі завантаження: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -752,7 +752,7 @@ } } }, - "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", + "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -769,8 +769,8 @@ } } }, - "time_justNow": "Тільки що", - "time_minutesAgo": "{minutes} хв. тому", + "time_justNow": "Тільки що", + "time_minutesAgo": "{minutes} хв. тому", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -778,7 +778,7 @@ } } }, - "time_hoursAgo": "{hours} год. тому", + "time_hoursAgo": "{hours} год. тому", "@time_hoursAgo": { "placeholders": { "hours": { @@ -786,7 +786,7 @@ } } }, - "time_daysAgo": "{days} дн. тому", + "time_daysAgo": "{days} дн. тому", "@time_daysAgo": { "placeholders": { "days": { @@ -794,33 +794,33 @@ } } }, - "time_hour": "година", - "time_hours": "годин", - "time_day": "день", - "time_days": "днів", - "time_week": "тиждень", - "time_weeks": "тижнів", - "time_month": "місяць", - "time_months": "місяців", - "time_minutes": "хвилин", - "time_allTime": "Весь час", - "dialog_disconnect": "Відключити", - "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", - "login_repeaterLogin": "Вхід у ретранслятор", - "login_roomLogin": "Вхід у кімнату", - "login_password": "Пароль", - "login_enterPassword": "Введіть пароль", - "login_savePassword": "Зберегти пароль", - "login_savePasswordSubtitle": "Пароль буде надійно збережено на цьому пристрої.", - "login_repeaterDescription": "Введіть пароль ретранслятора для доступу до налаштувань та статусу.", - "login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.", - "login_routing": "Маршрутизація", - "login_routingMode": "Режим маршрутизації", - "login_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "login_forceFloodMode": "Примусово на всю мережу", - "login_managePaths": "Керувати шляхами", - "login_login": "Вхід", - "login_attempt": "Спроба {current}/{max}", + "time_hour": "година", + "time_hours": "годин", + "time_day": "день", + "time_days": "днів", + "time_week": "тиждень", + "time_weeks": "тижнів", + "time_month": "місяць", + "time_months": "місяців", + "time_minutes": "хвилин", + "time_allTime": "Весь час", + "dialog_disconnect": "Відключити", + "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "login_repeaterLogin": "Вхід у ретранслятор", + "login_roomLogin": "Вхід у кімнату", + "login_password": "Пароль", + "login_enterPassword": "Введіть пароль", + "login_savePassword": "Зберегти пароль", + "login_savePasswordSubtitle": "Пароль буде надійно збережено на цьому пристрої.", + "login_repeaterDescription": "Введіть пароль ретранслятора для доступу до налаштувань та статусу.", + "login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.", + "login_routing": "Маршрутизація", + "login_routingMode": "Режим маршрутизації", + "login_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "login_forceFloodMode": "Примусово на всю мережу", + "login_managePaths": "Керувати шляхами", + "login_login": "Вхід", + "login_attempt": "Спроба {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -831,7 +831,7 @@ } } }, - "login_failed": "Вхід не вдався: {error}", + "login_failed": "Вхід не вдався: {error}", "@login_failed": { "placeholders": { "error": { @@ -839,10 +839,10 @@ } } }, - "login_failedMessage": "Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.", - "common_reload": "Перезавантажити", - "common_clear": "Очистити", - "path_currentPath": "Поточний шлях: {path}", + "login_failedMessage": "Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.", + "common_reload": "Перезавантажити", + "common_clear": "Очистити", + "path_currentPath": "Поточний шлях: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -850,7 +850,7 @@ } } }, - "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}", + "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}", "@path_usingHopsPath": { "placeholders": { "count": { @@ -858,16 +858,16 @@ } } }, - "path_enterCustomPath": "Ввести власний шлях", - "path_currentPathLabel": "Поточний шлях", - "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.", - "path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).", - "path_labelHexPrefixes": "Hex-префікси", - "path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)", - "path_selectFromContacts": "Вибрати з контактів:", - "path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.", - "path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.", - "path_invalidHexPrefixes": "Некоректні hex-префікси: {prefixes}", + "path_enterCustomPath": "Ввести власний шлях", + "path_currentPathLabel": "Поточний шлях", + "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.", + "path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).", + "path_labelHexPrefixes": "Hex-префікси", + "path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)", + "path_selectFromContacts": "Вибрати з контактів:", + "path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.", + "path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.", + "path_invalidHexPrefixes": "Некоректні hex-префікси: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -875,26 +875,26 @@ } } }, - "path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.", - "path_setPath": "Встановити шлях", - "repeater_management": "Керування ретранслятором", - "repeater_managementTools": "Інструменти керування", - "repeater_status": "Статус", - "repeater_statusSubtitle": "Показати статус, статистику та сусідів ретранслятора", - "repeater_telemetry": "Телеметрія", - "repeater_telemetrySubtitle": "Показати телеметрію сенсорів та статистику системи", + "path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.", + "path_setPath": "Встановити шлях", + "repeater_management": "Керування ретранслятором", + "repeater_managementTools": "Інструменти керування", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Показати статус, статистику та сусідів ретранслятора", + "repeater_telemetry": "Телеметрія", + "repeater_telemetrySubtitle": "Показати телеметрію сенсорів та статистику системи", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Надіслати команди ретранслятору", - "repeater_settings": "Налаштування", - "repeater_settingsSubtitle": "Налаштувати параметри ретранслятора", - "repeater_statusTitle": "Статус ретранслятора", - "repeater_routingMode": "Режим маршрутизації", - "repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "repeater_forceFloodMode": "Примусово на всю мережу", - "repeater_pathManagement": "Керування шляхами", - "repeater_refresh": "Оновити", - "repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.", - "repeater_errorLoadingStatus": "Помилка завантаження статусу: {error}", + "repeater_cliSubtitle": "Надіслати команди ретранслятору", + "repeater_settings": "Налаштування", + "repeater_settingsSubtitle": "Налаштувати параметри ретранслятора", + "repeater_statusTitle": "Статус ретранслятора", + "repeater_routingMode": "Режим маршрутизації", + "repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "repeater_forceFloodMode": "Примусово на всю мережу", + "repeater_pathManagement": "Керування шляхами", + "repeater_refresh": "Оновити", + "repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.", + "repeater_errorLoadingStatus": "Помилка завантаження статусу: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -902,23 +902,23 @@ } } }, - "repeater_systemInformation": "Системна інформація", - "repeater_battery": "Батарея", - "repeater_clockAtLogin": "Годинник (при вході)", - "repeater_uptime": "Час роботи", - "repeater_queueLength": "Довжина черги", - "repeater_debugFlags": "Прапорці налагодження", - "repeater_radioStatistics": "Статистика радіо", - "repeater_lastRssi": "Останній RSSI", - "repeater_lastSnr": "Останній SNR", - "repeater_noiseFloor": "Рівень шуму", - "repeater_txAirtime": "Ефірний час TX", - "repeater_rxAirtime": "Ефірний час RX", - "repeater_packetStatistics": "Статистика пакетів", - "repeater_sent": "Надіслано", - "repeater_received": "Отримано", - "repeater_duplicates": "Дублікати", - "repeater_daysHoursMinsSecs": "{days} дн. {hours} год {minutes} хв {seconds} с", + "repeater_systemInformation": "Системна інформація", + "repeater_battery": "Батарея", + "repeater_clockAtLogin": "Годинник (при вході)", + "repeater_uptime": "Час роботи", + "repeater_queueLength": "Довжина черги", + "repeater_debugFlags": "Прапорці налагодження", + "repeater_radioStatistics": "Статистика радіо", + "repeater_lastRssi": "Останній RSSI", + "repeater_lastSnr": "Останній SNR", + "repeater_noiseFloor": "Рівень шуму", + "repeater_txAirtime": "Ефірний час TX", + "repeater_rxAirtime": "Ефірний час RX", + "repeater_packetStatistics": "Статистика пакетів", + "repeater_sent": "Надіслано", + "repeater_received": "Отримано", + "repeater_duplicates": "Дублікати", + "repeater_daysHoursMinsSecs": "{days} дн. {hours} год {minutes} хв {seconds} с", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -935,7 +935,7 @@ } } }, - "repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -949,7 +949,7 @@ } } }, - "repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -963,7 +963,7 @@ } } }, - "repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}", + "repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -974,7 +974,7 @@ } } }, - "repeater_duplicatesTotal": "Всього: {total}", + "repeater_duplicatesTotal": "Всього: {total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -982,37 +982,37 @@ } } }, - "repeater_settingsTitle": "Налаштування ретранслятора", - "repeater_basicSettings": "Основні налаштування", - "repeater_repeaterName": "Ім'я ретранслятора", - "repeater_repeaterNameHelper": "Показати ім'я цього ретранслятора", - "repeater_adminPassword": "Пароль адміністратора", - "repeater_adminPasswordHelper": "Пароль повного доступу", - "repeater_guestPassword": "Гостьовий пароль", - "repeater_guestPasswordHelper": "Доступ лише для читання з паролем", - "repeater_radioSettings": "Налаштування радіо", - "repeater_frequencyMhz": "Частота (МГц)", - "repeater_frequencyHelper": "300-2500 МГц", - "repeater_txPower": "Потужність TX", - "repeater_txPowerHelper": "1-30 дБм", - "repeater_bandwidth": "Смуга пропускання", - "repeater_spreadingFactor": "Коефіцієнт розширення", - "repeater_codingRate": "Швидкість кодування", - "repeater_locationSettings": "Налаштування розташування", - "repeater_latitude": "Широта", - "repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)", - "repeater_longitude": "Довгота", - "repeater_longitudeHelper": "Десяткові градуси (наприклад, -122.4194)", - "repeater_features": "Функції", - "repeater_packetForwarding": "Пересилання пакетів", - "repeater_packetForwardingSubtitle": "Дозволити ретранслятору пересилати пакети", - "repeater_guestAccess": "Гостьовий доступ", - "repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання", - "repeater_privacyMode": "Режим приватності", - "repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", - "repeater_advertisementSettings": "Налаштування оголошень", - "repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)", - "repeater_localAdvertIntervalMinutes": "{minutes} хвилин", + "repeater_settingsTitle": "Налаштування ретранслятора", + "repeater_basicSettings": "Основні налаштування", + "repeater_repeaterName": "Ім'я ретранслятора", + "repeater_repeaterNameHelper": "Показати ім'я цього ретранслятора", + "repeater_adminPassword": "Пароль адміністратора", + "repeater_adminPasswordHelper": "Пароль повного доступу", + "repeater_guestPassword": "Гостьовий пароль", + "repeater_guestPasswordHelper": "Доступ лише для читання з паролем", + "repeater_radioSettings": "Налаштування радіо", + "repeater_frequencyMhz": "Частота (МГц)", + "repeater_frequencyHelper": "300-2500 МГц", + "repeater_txPower": "Потужність TX", + "repeater_txPowerHelper": "1-30 дБм", + "repeater_bandwidth": "Смуга пропускання", + "repeater_spreadingFactor": "Коефіцієнт розширення", + "repeater_codingRate": "Швидкість кодування", + "repeater_locationSettings": "Налаштування розташування", + "repeater_latitude": "Широта", + "repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)", + "repeater_longitude": "Довгота", + "repeater_longitudeHelper": "Десяткові градуси (наприклад, -122.4194)", + "repeater_features": "Функції", + "repeater_packetForwarding": "Пересилання пакетів", + "repeater_packetForwardingSubtitle": "Дозволити ретранслятору пересилати пакети", + "repeater_guestAccess": "Гостьовий доступ", + "repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання", + "repeater_privacyMode": "Режим приватності", + "repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", + "repeater_advertisementSettings": "Налаштування оголошень", + "repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)", + "repeater_localAdvertIntervalMinutes": "{minutes} хвилин", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1020,8 +1020,8 @@ } } }, - "repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)", - "repeater_floodAdvertIntervalHours": "{hours} годин", + "repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)", + "repeater_floodAdvertIntervalHours": "{hours} годин", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1029,19 +1029,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Інтервал зашифрованих оголошень", - "repeater_dangerZone": "Небезпечна зона", - "repeater_rebootRepeater": "Перезавантажити ретранслятор", - "repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора", - "repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?", - "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності", - "repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)", - "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?", - "repeater_eraseFileSystem": "Очистити файлову систему", - "repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора", - "repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!", - "repeater_eraseSerialOnly": "Очищення доступне лише через послідовну консоль.", - "repeater_commandSent": "Команда надіслана: {command}", + "repeater_encryptedAdvertInterval": "Інтервал зашифрованих оголошень", + "repeater_dangerZone": "Небезпечна зона", + "repeater_rebootRepeater": "Перезавантажити ретранслятор", + "repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора", + "repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?", + "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності", + "repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)", + "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?", + "repeater_eraseFileSystem": "Очистити файлову систему", + "repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора", + "repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!", + "repeater_eraseSerialOnly": "Очищення доступне лише через послідовну консоль.", + "repeater_commandSent": "Команда надіслана: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1049,7 +1049,7 @@ } } }, - "repeater_errorSendingCommand": "Помилка надсилання команди: {error}", + "repeater_errorSendingCommand": "Помилка надсилання команди: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1057,9 +1057,9 @@ } } }, - "repeater_confirm": "Підтвердити", - "repeater_settingsSaved": "Налаштування успішно збережено.", - "repeater_errorSavingSettings": "Помилка збереження налаштувань: {error}", + "repeater_confirm": "Підтвердити", + "repeater_settingsSaved": "Налаштування успішно збережено.", + "repeater_errorSavingSettings": "Помилка збереження налаштувань: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1067,15 +1067,15 @@ } } }, - "repeater_refreshBasicSettings": "Оновити основні налаштування", - "repeater_refreshRadioSettings": "Оновити налаштування радіо", - "repeater_refreshTxPower": "Оновити потужність TX", - "repeater_refreshLocationSettings": "Оновити налаштування розташування", - "repeater_refreshPacketForwarding": "Оновити пересилання пакетів", - "repeater_refreshGuestAccess": "Оновити гостьовий доступ", - "repeater_refreshPrivacyMode": "Оновити режим приватності", - "repeater_refreshAdvertisementSettings": "Оновити налаштування оголошень", - "repeater_refreshed": "{label} оновлено", + "repeater_refreshBasicSettings": "Оновити основні налаштування", + "repeater_refreshRadioSettings": "Оновити налаштування радіо", + "repeater_refreshTxPower": "Оновити потужність TX", + "repeater_refreshLocationSettings": "Оновити налаштування розташування", + "repeater_refreshPacketForwarding": "Оновити пересилання пакетів", + "repeater_refreshGuestAccess": "Оновити гостьовий доступ", + "repeater_refreshPrivacyMode": "Оновити режим приватності", + "repeater_refreshAdvertisementSettings": "Оновити налаштування оголошень", + "repeater_refreshed": "{label} оновлено", "@repeater_refreshed": { "placeholders": { "label": { @@ -1083,7 +1083,7 @@ } } }, - "repeater_errorRefreshing": "Помилка оновлення {label}", + "repeater_errorRefreshing": "Помилка оновлення {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,18 +1091,18 @@ } } }, - "repeater_cliTitle": "Ретранслятор CLI", - "repeater_debugNextCommand": "Налагодити наступну команду", - "repeater_commandHelp": "Довідка", - "repeater_clearHistory": "Очистити історію", - "repeater_noCommandsSent": "Команди ще не надсилалися.", - "repeater_typeCommandOrUseQuick": "Введіть команду нижче або використовуйте швидкі команди", - "repeater_enterCommandHint": "Введіть команду...", - "repeater_previousCommand": "Попередня команда", - "repeater_nextCommand": "Наступна команда", - "repeater_enterCommandFirst": "Спершу введіть команду", - "repeater_cliCommandFrameTitle": "Фрейм команди CLI", - "repeater_cliCommandError": "Помилка: {error}", + "repeater_cliTitle": "Ретранслятор CLI", + "repeater_debugNextCommand": "Налагодити наступну команду", + "repeater_commandHelp": "Довідка", + "repeater_clearHistory": "Очистити історію", + "repeater_noCommandsSent": "Команди ще не надсилалися.", + "repeater_typeCommandOrUseQuick": "Введіть команду нижче або використовуйте швидкі команди", + "repeater_enterCommandHint": "Введіть команду...", + "repeater_previousCommand": "Попередня команда", + "repeater_nextCommand": "Наступна команда", + "repeater_enterCommandFirst": "Спершу введіть команду", + "repeater_cliCommandFrameTitle": "Фрейм команди CLI", + "repeater_cliCommandError": "Помилка: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1110,81 +1110,81 @@ } } }, - "repeater_cliQuickGetName": "Отримати ім'я", - "repeater_cliQuickGetRadio": "Отримати Радіо", - "repeater_cliQuickGetTx": "Отримати TX", - "repeater_cliQuickNeighbors": "Сусіди", - "repeater_cliQuickVersion": "Версія", - "repeater_cliQuickAdvertise": "Оголосити", - "repeater_cliQuickClock": "Годинник", - "repeater_cliHelpAdvert": "Надсилає пакет оголошення", - "repeater_cliHelpReboot": "Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що є нормальним)", - "repeater_cliHelpClock": "Відображає поточний час за годинником кожного пристрою.", - "repeater_cliHelpPassword": "Встановлює новий пароль адміністратора для пристрою.", - "repeater_cliHelpVersion": "Відображає версію пристрою та дату збірки прошивки.", - "repeater_cliHelpClearStats": "Скидає різні лічильники статистики до нуля.", - "repeater_cliHelpSetAf": "Встановлює коефіцієнт ефірного часу.", - "repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).", - "repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.", - "repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)", - "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).", - "repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.", - "repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.", - "repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.", - "repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.", - "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.", - "repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)", - "repeater_cliHelpSetName": "Встановлює ім'я для оголошення.", - "repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)", - "repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)", - "repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.", - "repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.", - "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", - "repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.", - "repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.", - "repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.", - "repeater_cliHelpSetBridgeSource": "Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.", - "repeater_cliHelpSetBridgeBaud": "Встановити швидкість послідовного зв'язку для мостів Rs232.", - "repeater_cliHelpSetBridgeSecret": "Встановити секрет мосту для мостів espnow.", - "repeater_cliHelpSetAdcMultiplier": "Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).", - "repeater_cliHelpTempRadio": "Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).", - "repeater_cliHelpSetPerm": "Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний і його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).", - "repeater_cliHelpGetBridgeType": "Отримати тип мосту: немає, rs232, espnow", - "repeater_cliHelpLogStart": "Починає запис пакетів у файлову систему.", - "repeater_cliHelpLogStop": "Зупиняє запис пакетів у файлову систему.", - "repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.", - "repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4", - "repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.", - "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).", - "repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.", - "repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»", - "repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.", - "repeater_cliHelpRegionRemove": "Видаляє визначення регіону з заданою назвою.", - "repeater_cliHelpRegionAllowf": "Встановлює дозвіл «Flood» для заданого регіону. ('' для глобальної/успадкованої області)", - "repeater_cliHelpRegionDenyf": "Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )", - "repeater_cliHelpRegionHome": "Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)", - "repeater_cliHelpRegionHomeSet": "Встановлює «домашній» регіон.", - "repeater_cliHelpRegionSave": "Зберігає список/карту регіонів у сховищі.", - "repeater_cliHelpGps": "Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.", - "repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.", - "repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.", - "repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.", - "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях", - "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.", - "repeater_commandsListTitle": "Список команд", - "repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...", - "repeater_general": "Загальні", - "repeater_settingsCategory": "Налаштування", - "repeater_bridge": "Міст", - "repeater_logging": "Логування", - "repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)", - "repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)", - "repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.", - "repeater_gpsManagement": "Керування GPS", - "repeater_gpsNote": "Команда GPS була введена для керування питаннями, пов'язаними з локацією.", - "telemetry_receivedData": "Дані телеметрії отримано", - "telemetry_requestTimeout": "Час запиту телеметрії вичерпано.", - "telemetry_errorLoading": "Помилка завантаження телеметрії: {error}", + "repeater_cliQuickGetName": "Отримати ім'я", + "repeater_cliQuickGetRadio": "Отримати Радіо", + "repeater_cliQuickGetTx": "Отримати TX", + "repeater_cliQuickNeighbors": "Сусіди", + "repeater_cliQuickVersion": "Версія", + "repeater_cliQuickAdvertise": "Оголосити", + "repeater_cliQuickClock": "Годинник", + "repeater_cliHelpAdvert": "Надсилає пакет оголошення", + "repeater_cliHelpReboot": "Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що Ñ” нормальним)", + "repeater_cliHelpClock": "Відображає поточний час за годинником кожного пристрою.", + "repeater_cliHelpPassword": "Встановлює новий пароль адміністратора для пристрою.", + "repeater_cliHelpVersion": "Відображає версію пристрою та дату збірки прошивки.", + "repeater_cliHelpClearStats": "Скидає різні лічильники статистики до нуля.", + "repeater_cliHelpSetAf": "Встановлює коефіцієнт ефірного часу.", + "repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).", + "repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.", + "repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)", + "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).", + "repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.", + "repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.", + "repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.", + "repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.", + "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.", + "repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)", + "repeater_cliHelpSetName": "Встановлює ім'я для оголошення.", + "repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)", + "repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)", + "repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає Ñ—Ñ… у налаштуваннях. Потребує команди «перезавантаження» для застосування.", + "repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.", + "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", + "repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.", + "repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.", + "repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.", + "repeater_cliHelpSetBridgeSource": "Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.", + "repeater_cliHelpSetBridgeBaud": "Встановити швидкість послідовного зв'язку для мостів Rs232.", + "repeater_cliHelpSetBridgeSecret": "Встановити секрет мосту для мостів espnow.", + "repeater_cliHelpSetAdcMultiplier": "Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).", + "repeater_cliHelpTempRadio": "Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).", + "repeater_cliHelpSetPerm": "Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний Ñ– його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).", + "repeater_cliHelpGetBridgeType": "Отримати тип мосту: немає, rs232, espnow", + "repeater_cliHelpLogStart": "Починає запис пакетів у файлову систему.", + "repeater_cliHelpLogStop": "Зупиняє запис пакетів у файлову систему.", + "repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.", + "repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4", + "repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.", + "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).", + "repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.", + "repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»", + "repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.", + "repeater_cliHelpRegionRemove": "Видаляє визначення регіону з заданою назвою.", + "repeater_cliHelpRegionAllowf": "Встановлює дозвіл «Flood» для заданого регіону. ('' для глобальної/успадкованої області)", + "repeater_cliHelpRegionDenyf": "Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )", + "repeater_cliHelpRegionHome": "Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)", + "repeater_cliHelpRegionHomeSet": "Встановлює «домашній» регіон.", + "repeater_cliHelpRegionSave": "Зберігає список/карту регіонів у сховищі.", + "repeater_cliHelpGps": "Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.", + "repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.", + "repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.", + "repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS Ñ– зберігає в налаштуваннях.", + "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях", + "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.", + "repeater_commandsListTitle": "Список команд", + "repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...", + "repeater_general": "Загальні", + "repeater_settingsCategory": "Налаштування", + "repeater_bridge": "Міст", + "repeater_logging": "Логування", + "repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)", + "repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)", + "repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.", + "repeater_gpsManagement": "Керування GPS", + "repeater_gpsNote": "Команда GPS була введена для керування питаннями, пов'язаними з локацією.", + "telemetry_receivedData": "Дані телеметрії отримано", + "telemetry_requestTimeout": "Час запиту телеметрії вичерпано.", + "telemetry_errorLoading": "Помилка завантаження телеметрії: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1192,8 +1192,8 @@ } } }, - "telemetry_noData": "Дані телеметрії недоступні.", - "telemetry_channelTitle": "Канал {channel}", + "telemetry_noData": "Дані телеметрії недоступні.", + "telemetry_channelTitle": "Канал {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1201,12 +1201,12 @@ } } }, - "telemetry_batteryLabel": "Батарея", - "telemetry_voltageLabel": "Напруга", - "telemetry_mcuTemperatureLabel": "Температура MCU", - "telemetry_temperatureLabel": "Температура", - "telemetry_currentLabel": "Поточний струм", - "telemetry_batteryValue": "{percent}% / {volts}В", + "telemetry_batteryLabel": "Батарея", + "telemetry_voltageLabel": "Напруга", + "telemetry_mcuTemperatureLabel": "Температура MCU", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Поточний струм", + "telemetry_batteryValue": "{percent}% / {volts}Ð’", "@telemetry_batteryValue": { "placeholders": { "percent": { @@ -1217,7 +1217,7 @@ } } }, - "telemetry_voltageValue": "{volts}В", + "telemetry_voltageValue": "{volts}Ð’", "@telemetry_voltageValue": { "placeholders": { "volts": { @@ -1225,7 +1225,7 @@ } } }, - "telemetry_currentValue": "{amps}А", + "telemetry_currentValue": "{amps}А", "@telemetry_currentValue": { "placeholders": { "amps": { @@ -1233,7 +1233,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1244,18 +1244,18 @@ } } }, - "channelPath_title": "Шлях пакету", - "channelPath_viewMap": "Показати карту", - "channelPath_otherObservedPaths": "Інші спостережувані шляхи", - "channelPath_repeaterHops": "Стрибки ретранслятора", - "channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.", - "channelPath_messageDetails": "Деталі повідомлення", - "channelPath_senderLabel": "Відправник", - "channelPath_timeLabel": "Час", - "channelPath_repeatsLabel": "Повторення", - "channelPath_pathLabel": "Шлях {index}", - "channelPath_observedLabel": "Спостережено", - "channelPath_observedPathTitle": "Спостережуваний шлях {index} • {hops}", + "channelPath_title": "Шлях пакету", + "channelPath_viewMap": "Показати карту", + "channelPath_otherObservedPaths": "Інші спостережувані шляхи", + "channelPath_repeaterHops": "Стрибки ретранслятора", + "channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.", + "channelPath_messageDetails": "Деталі повідомлення", + "channelPath_senderLabel": "Відправник", + "channelPath_timeLabel": "Час", + "channelPath_repeatsLabel": "Повторення", + "channelPath_pathLabel": "Шлях {index}", + "channelPath_observedLabel": "Спостережено", + "channelPath_observedPathTitle": "Спостережуваний шлях {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1266,7 +1266,7 @@ } } }, - "channelPath_noLocationData": "Немає даних про розташування", + "channelPath_noLocationData": "Немає даних про розташування", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,10 +1289,10 @@ } } }, - "channelPath_unknownPath": "Невідомий", - "channelPath_floodPath": "На всю мережу", - "channelPath_directPath": "Прямий", - "channelPath_observedZeroOf": "0 з {total} стрибків", + "channelPath_unknownPath": "Невідомий", + "channelPath_floodPath": "На всю мережу", + "channelPath_directPath": "Прямий", + "channelPath_observedZeroOf": "0 з {total} стрибків", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1300,7 +1300,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} з {total} стрибків", + "channelPath_observedSomeOf": "{observed} з {total} стрибків", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1311,9 +1311,9 @@ } } }, - "channelPath_mapTitle": "Карта шляху", - "channelPath_noRepeaterLocations": "Позиції ретрансляторів недоступні для цього шляху.", - "channelPath_primaryPath": "Шлях {index} (Основний)", + "channelPath_mapTitle": "Карта шляху", + "channelPath_noRepeaterLocations": "Позиції ретрансляторів недоступні для цього шляху.", + "channelPath_primaryPath": "Шлях {index} (Основний)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1328,9 +1328,9 @@ } } }, - "channelPath_pathLabelTitle": "Шлях", - "channelPath_observedPathHeader": "Спостережуваний шлях", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Шлях", + "channelPath_observedPathHeader": "Спостережуваний шлях", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1341,20 +1341,20 @@ } } }, - "channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.", - "channelPath_unknownRepeater": "Невідомий ретранслятор", - "listFilter_tooltip": "Фільтр та сортування", - "listFilter_sortBy": "Сортувати за", - "listFilter_latestMessages": "Останні повідомлення", - "listFilter_heardRecently": "Нещодавно чули", - "listFilter_az": "А-Я", - "listFilter_filters": "Фільтри", - "listFilter_all": "Все", - "listFilter_users": "Користувачі", - "listFilter_repeaters": "Ретранслятори", - "listFilter_roomServers": "Сервери кімнат", - "listFilter_unreadOnly": "Тільки непрочитані повідомлення", - "listFilter_newGroup": "Нова група", + "channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.", + "channelPath_unknownRepeater": "Невідомий ретранслятор", + "listFilter_tooltip": "Фільтр та сортування", + "listFilter_sortBy": "Сортувати за", + "listFilter_latestMessages": "Останні повідомлення", + "listFilter_heardRecently": "Нещодавно чули", + "listFilter_az": "А-Я", + "listFilter_filters": "Фільтри", + "listFilter_all": "Все", + "listFilter_users": "Користувачі", + "listFilter_repeaters": "Ретранслятори", + "listFilter_roomServers": "Сервери кімнат", + "listFilter_unreadOnly": "Тільки непрочитані повідомлення", + "listFilter_newGroup": "Нова група", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1362,25 +1362,25 @@ } } }, - "repeater_neighbors": "Сусіди", - "repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", - "neighbors_receivedData": "Дані сусідів отримано", - "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", - "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", - "neighbors_repeatersNeighbors": "Ретранслятори-сусіди", - "neighbors_noData": "Дані про сусідів недоступні.", - "channels_createPrivateChannelDesc": "Захищено секретним ключем.", - "channels_joinPrivateChannel": "Приєднатися до приватного каналу", - "channels_createPrivateChannel": "Створити приватний канал", - "channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.", - "channels_joinPublicChannel": "Приєднатися до публічного каналу", - "channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.", - "channels_joinHashtagChannel": "Приєднатися до каналу з хештегом", - "channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.", - "channels_scanQrCode": "Сканувати QR-код", - "channels_scanQrCodeComingSoon": "Скоро буде", - "channels_enterHashtag": "Введіть хештег", - "channels_hashtagHint": "напр. #команда", + "repeater_neighbors": "Сусіди", + "repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", + "neighbors_receivedData": "Дані сусідів отримано", + "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", + "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", + "neighbors_repeatersNeighbors": "Ретранслятори-сусіди", + "neighbors_noData": "Дані про сусідів недоступні.", + "channels_createPrivateChannelDesc": "Захищено секретним ключем.", + "channels_joinPrivateChannel": "Приєднатися до приватного каналу", + "channels_createPrivateChannel": "Створити приватний канал", + "channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.", + "channels_joinPublicChannel": "Приєднатися до публічного каналу", + "channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.", + "channels_joinHashtagChannel": "Приєднатися до каналу з хештегом", + "channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.", + "channels_scanQrCode": "Сканувати QR-код", + "channels_scanQrCodeComingSoon": "Скоро буде", + "channels_enterHashtag": "Введіть хештег", + "channels_hashtagHint": "напр. #команда", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1395,14 +1395,14 @@ } } }, - "neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}", - "neighbors_heardAgo": "Почуто: {time} тому", - "settings_locationGPSEnable": "Увімкнути GPS", - "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.", - "settings_locationIntervalSec": "Інтервал для GPS (Секунди)", - "settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.", - "contacts_manageRoom": "Керувати сервером кімнати", - "room_management": "Адміністрування сервера кімнати", + "neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}", + "neighbors_heardAgo": "Почуто: {time} тому", + "settings_locationGPSEnable": "Увімкнути GPS", + "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.", + "settings_locationIntervalSec": "Інтервал для GPS (Секунди)", + "settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд Ñ– менше 86400 секунд.", + "contacts_manageRoom": "Керувати сервером кімнати", + "room_management": "Адміністрування сервера кімнати", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1459,36 +1459,36 @@ } } }, - "common_ok": "ОК", - "community_title": "Спільнота", - "community_create": "Створити спільноту", - "community_createDesc": "Створити нову спільноту та поділитися через QR-код.", - "community_join": "Приєднатися", - "community_joinTitle": "Приєднатися до спільноти", - "community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?", - "community_scanQr": "Сканувати QR спільноти", - "community_scanInstructions": "Наведіть камеру на QR-код спільноти.", - "community_showQr": "Показати QR-код", - "community_publicChannel": "Публічна спільнота", - "community_hashtagChannel": "Хештег спільноти", - "community_name": "Назва спільноти", - "community_enterName": "Введіть назву спільноти", - "community_created": "Спільноту «{name}» створено", - "community_joined": "Приєднався до спільноти «{name}»", - "community_qrTitle": "Поділитися спільнотою", - "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}", - "community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти", - "community_invalidQrCode": "Недійсний QR-код спільноти", - "community_alreadyMember": "Вже учасник", - "community_alreadyMemberMessage": "Ви вже є учасником «{name}».", - "community_addPublicChannel": "Додати публічний канал спільноти", - "community_addPublicChannelHint": "Автоматично додати публічний канал для цієї спільноти", - "community_noCommunities": "Поки не приєднано до жодної групи.", - "community_scanOrCreate": "Відскануйте QR-код або створіть спільноту, щоб почати", - "community_manageCommunities": "Керувати спільнотами", - "community_delete": "Покинути спільноту", - "community_deleteConfirm": "Покинути «{name}»?", - "community_deleteChannelsWarning": "Це також видалить {count} {count, plural, =1{канал} few{канали} many{каналів} other{каналів}} та їх повідомлення.", + "common_ok": "ОК", + "community_title": "Спільнота", + "community_create": "Створити спільноту", + "community_createDesc": "Створити нову спільноту та поділитися через QR-код.", + "community_join": "Приєднатися", + "community_joinTitle": "Приєднатися до спільноти", + "community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?", + "community_scanQr": "Сканувати QR спільноти", + "community_scanInstructions": "Наведіть камеру на QR-код спільноти.", + "community_showQr": "Показати QR-код", + "community_publicChannel": "Публічна спільнота", + "community_hashtagChannel": "Хештег спільноти", + "community_name": "Назва спільноти", + "community_enterName": "Введіть назву спільноти", + "community_created": "Спільноту «{name}» створено", + "community_joined": "Приєднався до спільноти «{name}»", + "community_qrTitle": "Поділитися спільнотою", + "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}", + "community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти", + "community_invalidQrCode": "Недійсний QR-код спільноти", + "community_alreadyMember": "Вже учасник", + "community_alreadyMemberMessage": "Ви вже Ñ” учасником «{name}».", + "community_addPublicChannel": "Додати публічний канал спільноти", + "community_addPublicChannelHint": "Автоматично додати публічний канал для цієї спільноти", + "community_noCommunities": "Поки не приєднано до жодної групи.", + "community_scanOrCreate": "Відскануйте QR-код або створіть спільноту, щоб почати", + "community_manageCommunities": "Керувати спільнотами", + "community_delete": "Покинути спільноту", + "community_deleteConfirm": "Покинути «{name}»?", + "community_deleteChannelsWarning": "Це також видалить {count} {count, plural, =1{канал} few{канали} many{каналів} other{каналів}} та Ñ—Ñ… повідомлення.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1496,15 +1496,15 @@ } } }, - "community_deleted": "Спільноту «{name}» покинуто", - "community_addHashtagChannel": "Додати хештег спільноти", - "community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти", - "community_selectCommunity": "Вибрати спільноту", - "community_regularHashtag": "Звичайний хештег", - "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)", - "community_communityHashtag": "Хештег спільноти", - "community_communityHashtagDesc": "Ексклюзивно для членів спільноти", - "community_forCommunity": "Для {name}", + "community_deleted": "Спільноту «{name}» покинуто", + "community_addHashtagChannel": "Додати хештег спільноти", + "community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти", + "community_selectCommunity": "Вибрати спільноту", + "community_regularHashtag": "Звичайний хештег", + "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)", + "community_communityHashtag": "Хештег спільноти", + "community_communityHashtagDesc": "Ексклюзивно для членів спільноти", + "community_forCommunity": "Для {name}", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1533,13 +1533,13 @@ } } }, - "community_regenerateSecret": "Перегенерувати секрет", - "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.", - "community_regenerate": "Перегенерувати", - "community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", - "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", - "community_updateSecret": "Оновити секрет", - "community_secretUpdated": "Зміну секрету для «{name}» оновлено", + "community_regenerateSecret": "Перегенерувати секрет", + "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.", + "community_regenerate": "Перегенерувати", + "community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", + "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", + "community_updateSecret": "Оновити секрет", + "community_secretUpdated": "Зміну секрету для «{name}» оновлено", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1547,81 +1547,81 @@ } } }, - "pathTrace_you": "Ви", - "pathTrace_failed": "Відстеження шляху не вдалося.", - "pathTrace_notAvailable": "Трасування шляху недоступне.", - "pathTrace_refreshTooltip": "Оновити Path Trace", - "contacts_pathTrace": "Трасування шляхів", - "contacts_ping": "Пінгувати", - "contacts_repeaterPathTrace": "Трасування шляху до повторювача", - "contacts_repeaterPing": "Пінгувати повторювач", - "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", - "contacts_roomPing": "Пінг сервера кімнати", - "contacts_chatTraceRoute": "Трасування шляху", - "contacts_pathTraceTo": "Відстежити маршрут до {name}", - "contacts_invalidAdvertFormat": "Недійсні контактні дані", - "contacts_contactImported": "Контакт було імпортовано.", - "contacts_contactImportFailed": "Контакт не вдалося імпортувати", - "contacts_zeroHopAdvert": "Реклама без перехоплення", - "contacts_floodAdvert": "Залив реклами", - "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", - "contacts_clipboardEmpty": "Буфер обміну порожній", - "appSettings_languageRu": "Російська", - "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", - "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", - "contacts_ShareContact": "Копіювати контакт у буфер обміну", - "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", - "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", - "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", - "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", - "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", - "notification_activityTitle": "Активність MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}", - "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", - "notification_newTypeDiscovered": "Виявлено новий {contactType}", - "notification_receivedNewMessage": "Отримано нове повідомлення", - "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", - "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", - "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", - "settings_gpxExportNoContacts": "Немає контактів для експорту.", - "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", - "settings_gpxExportError": "Сталася помилка під час експорту.", - "settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", - "settings_gpxExportAll": "Експортувати всі контакти до GPX", - "settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", - "settings_gpxExportContacts": "Експортувати супутників до GPX", - "settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", - "settings_gpxExportChat": "Місця супутників", - "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", - "settings_gpxExportAllContacts": "Усі місця контактів", - "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", - "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", - "map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху", - "map_runTrace": "Виконати трасування шляху", - "pathTrace_clearTooltip": "Очистити шлях", - "map_removeLast": "Видалити останній", - "map_pathTraceCancelled": "Відмінується трасування шляху", - "scanner_enableBluetooth": "Увімкніть Bluetooth", - "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", - "scanner_chromeRequired": "Потрібен браузер Chrome", - "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", - "scanner_bluetoothOff": "Bluetooth вимкнено", - "snrIndicator_lastSeen": "Останній раз бачили", - "snrIndicator_nearByRepeaters": "Ближні ретранслятори", - "chat_ShowAllPaths": "Показати всі шляхи", - "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", - "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", - "settings_clientRepeat": "Автономна система", - "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "одиниці", - "appSettings_unitsMetric": "Метричний (м / км)", - "appSettings_unitsImperial": "Імперська (ft / mi)", - "map_lineOfSight": "Пряма видимість", - "map_losScreenTitle": "Пряма видимість", - "losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.", - "losRunFailed": "Помилка перевірки прямої видимості: {error}", + "pathTrace_you": "Ви", + "pathTrace_failed": "Відстеження шляху не вдалося.", + "pathTrace_notAvailable": "Трасування шляху недоступне.", + "pathTrace_refreshTooltip": "Оновити Path Trace", + "contacts_pathTrace": "Трасування шляхів", + "contacts_ping": "Пінгувати", + "contacts_repeaterPathTrace": "Трасування шляху до повторювача", + "contacts_repeaterPing": "Пінгувати повторювач", + "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", + "contacts_roomPing": "Пінг сервера кімнати", + "contacts_chatTraceRoute": "Трасування шляху", + "contacts_pathTraceTo": "Відстежити маршрут до {name}", + "contacts_invalidAdvertFormat": "Недійсні контактні дані", + "contacts_contactImported": "Контакт було імпортовано.", + "contacts_contactImportFailed": "Контакт не вдалося імпортувати", + "contacts_zeroHopAdvert": "Реклама без перехоплення", + "contacts_floodAdvert": "Залив реклами", + "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", + "contacts_clipboardEmpty": "Буфер обміну порожній", + "appSettings_languageRu": "Російська", + "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", + "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", + "contacts_ShareContact": "Копіювати контакт у буфер обміну", + "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", + "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", + "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", + "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", + "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "notification_activityTitle": "Активність MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}", + "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", + "notification_newTypeDiscovered": "Виявлено новий {contactType}", + "notification_receivedNewMessage": "Отримано нове повідомлення", + "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", + "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", + "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", + "settings_gpxExportNoContacts": "Немає контактів для експорту.", + "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", + "settings_gpxExportError": "Сталася помилка під час експорту.", + "settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", + "settings_gpxExportAll": "Експортувати всі контакти до GPX", + "settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", + "settings_gpxExportContacts": "Експортувати супутників до GPX", + "settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", + "settings_gpxExportChat": "Місця супутників", + "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", + "settings_gpxExportAllContacts": "Усі місця контактів", + "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", + "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", + "map_tapToAdd": "Натисніть на вузли, щоб додати Ñ—Ñ… до шляху", + "map_runTrace": "Виконати трасування шляху", + "pathTrace_clearTooltip": "Очистити шлях", + "map_removeLast": "Видалити останній", + "map_pathTraceCancelled": "Відмінується трасування шляху", + "scanner_enableBluetooth": "Увімкніть Bluetooth", + "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", + "scanner_chromeRequired": "Потрібен браузер Chrome", + "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", + "scanner_bluetoothOff": "Bluetooth вимкнено", + "snrIndicator_lastSeen": "Останній раз бачили", + "snrIndicator_nearByRepeaters": "Ближні ретранслятори", + "chat_ShowAllPaths": "Показати всі шляхи", + "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", + "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", + "settings_clientRepeat": "Автономна система", + "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "одиниці", + "appSettings_unitsMetric": "Метричний (м / км)", + "appSettings_unitsImperial": "Імперська (ft / mi)", + "map_lineOfSight": "Пряма видимість", + "map_losScreenTitle": "Пряма видимість", + "losSelectStartEnd": "Виберіть початковий Ñ– кінцевий вузли для LOS.", + "losRunFailed": "Помилка перевірки прямої видимості: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,13 +1629,13 @@ } } }, - "losClearAllPoints": "Очистити всі пункти", - "losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти", - "losMenuTitle": "Меню LOS", - "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", - "losShowDisplayNodes": "Показати вузли відображення", - "losCustomPoints": "Користувальницькі точки", - "losCustomPointLabel": "Спеціальний {index}", + "losClearAllPoints": "Очистити всі пункти", + "losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти", + "losMenuTitle": "Меню LOS", + "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", + "losShowDisplayNodes": "Показати вузли відображення", + "losCustomPoints": "Користувальницькі точки", + "losCustomPointLabel": "Спеціальний {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1643,9 +1643,9 @@ } } }, - "losPointA": "Точка А", - "losPointB": "Точка Б", - "losAntennaA": "Антена A: {value} {unit}", + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Антена B: {value} {unit}", + "losAntennaB": "Антена B: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1667,9 +1667,9 @@ } } }, - "losRun": "Запустіть LOS", - "losNoElevationData": "Немає даних про висоту", - "losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}", + "losRun": "Запустіть LOS", + "losNoElevationData": "Немає даних про висоту", + "losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1703,9 +1703,9 @@ } } }, - "losStatusChecking": "LOS: перевірка...", - "losStatusNoData": "LOS: немає даних", - "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо", + "losStatusChecking": "LOS: перевірка...", + "losStatusNoData": "LOS: немає даних", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", - "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", - "losRenameCustomPoint": "Перейменуйте спеціальну точку", - "losPointName": "Назва точки", - "losShowPanelTooltip": "Показати панель LOS", - "losHidePanelTooltip": "Приховати панель LOS", - "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Радіогоризонт", - "losLegendLosBeam": "Лінія прямої видимості", - "losLegendTerrain": "Рельєф", - "losFrequencyLabel": "Частота", - "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", - "losFrequencyDialogTitle": "Розрахунок радіогоризонту", - "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", + "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", + "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", + "losRenameCustomPoint": "Перейменуйте спеціальну точку", + "losPointName": "Назва точки", + "losShowPanelTooltip": "Показати панель LOS", + "losHidePanelTooltip": "Приховати панель LOS", + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радіогоризонт", + "losLegendLosBeam": "Лінія прямої видимості", + "losLegendTerrain": "Рельєф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", + "losFrequencyDialogTitle": "Розрахунок радіогоризонту", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_removeFromFavorites": "Видалити зі списку улюблених", - "listFilter_addToFavorites": "Додати до улюблених", - "listFilter_favorites": "Улюблені", + "listFilter_removeFromFavorites": "Видалити зі списку улюблених", + "listFilter_addToFavorites": "Додати до улюблених", + "listFilter_favorites": "Улюблені", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1796,19 +1796,17 @@ } } }, - "contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...", - "contacts_searchUsers": "Пошук {number}{str} користувачів...", - "contacts_searchFavorites": "Пошук {number}{str} улюблених...", - "contacts_searchContactsNoNumber": "Пошук контактів...", - "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", - "contacts_unread": "Непрочитане", - "connectionChoiceSubtitle": "Виберіть, яким способом ви бажаєте отримати доступ до вашого пристрою MeshCore.", + "contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...", + "contacts_searchUsers": "Пошук {number}{str} користувачів...", + "contacts_searchFavorites": "Пошук {number}{str} улюблених...", + "contacts_searchContactsNoNumber": "Пошук контактів...", + "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", + "contacts_unread": "Непрочитане", "connectionChoiceUsbLabel": "USB", - "connectionChoiceTitle": "Виберіть спосіб зв'язку", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", - "usbScreenTitle": "Підключити через USB", - "usbScreenStatus": "Виберіть пристрій USB", - "usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.", - "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте." + "usbScreenSubtitle": "Виберіть виявлене серійне пристрій Ñ– підключіть його безпосередньо до вашого вузла MeshCore.", + "usbScreenTitle": "Підключити через USB", + "usbScreenStatus": "Виберіть пристрій USB", + "usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.", + "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один Ñ– перезавантажте." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c35cbaa..c3ea415 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", +{ + "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,34 +9,34 @@ }, "@@locale": "zh", "appTitle": "MeshCore Open", - "nav_contacts": "联系人", - "nav_channels": "频道", - "nav_map": "地图", - "common_cancel": "取消", - "common_ok": "确定", - "common_connect": "连接", - "common_unknownDevice": "未知设备", - "common_save": "保存", - "common_delete": "删除", - "common_close": "关闭", - "common_edit": "编辑", - "common_add": "添加", - "common_settings": "设置", - "common_disconnect": "断开", - "common_connected": "已连接", - "common_disconnected": "已断开", - "common_create": "创建", - "common_continue": "继续", - "common_share": "分享", - "common_copy": "复制", - "common_retry": "重试", - "common_hide": "隐藏", - "common_remove": "移除", - "common_enable": "启用", - "common_disable": "禁用", - "common_reboot": "重启", - "common_loading": "正在加载...", - "common_notAvailable": "—", + "nav_contacts": "联系人", + "nav_channels": "频道", + "nav_map": "地图", + "common_cancel": "取消", + "common_ok": "确定", + "common_connect": "连接", + "common_unknownDevice": "未知设备", + "common_save": "保存", + "common_delete": "删除", + "common_close": "关闭", + "common_edit": "编辑", + "common_add": "添加", + "common_settings": "设置", + "common_disconnect": "æ–­å¼€", + "common_connected": "已连接", + "common_disconnected": "已断开", + "common_create": "创建", + "common_continue": "ç»§ç»­", + "common_share": "分享", + "common_copy": "复制", + "common_retry": "重试", + "common_hide": "隐藏", + "common_remove": "移除", + "common_enable": "启用", + "common_disable": "禁用", + "common_reboot": "重启", + "common_loading": "正在加载...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,12 +53,12 @@ } } }, - "scanner_title": "连接设备", - "scanner_scanning": "正在搜索设备...", - "scanner_connecting": "正在连接...", - "scanner_disconnecting": "断开连接...", - "scanner_notConnected": "未连接", - "scanner_connectedTo": "已连接到 {deviceName}", + "scanner_title": "连接设备", + "scanner_scanning": "正在搜索设备...", + "scanner_connecting": "正在连接...", + "scanner_disconnecting": "断开连接...", + "scanner_notConnected": "未连接", + "scanner_connectedTo": "已连接到 {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -66,9 +66,9 @@ } } }, - "scanner_searchingDevices": "正在搜索 MeshCore 设备...", - "scanner_tapToScan": "点击“扫描”按钮以查找 MeshCore 设备。", - "scanner_connectionFailed": "连接失败:{error}", + "scanner_searchingDevices": "正在搜索 MeshCore 设备...", + "scanner_tapToScan": "点击“扫描”按钮以查找 MeshCore 设备。", + "scanner_connectionFailed": "连接失败:{error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -76,56 +76,56 @@ } } }, - "scanner_stop": "停止", - "scanner_scan": "扫描", - "device_quickSwitch": "快速切换", + "scanner_stop": "停止", + "scanner_scan": "扫描", + "device_quickSwitch": "快速切换", "device_meshcore": "MeshCore", - "settings_title": "设置", - "settings_deviceInfo": "设备信息", - "settings_appSettings": "应用设置", - "settings_appSettingsSubtitle": "通知、消息和地图偏好", - "settings_nodeSettings": "节点设置", - "settings_nodeName": "节点名称", - "settings_nodeNameNotSet": "未设置", - "settings_nodeNameHint": "请输入节点名称", - "settings_nodeNameUpdated": "节点名称已更新", - "settings_radioSettings": "无线电设置", - "settings_radioSettingsSubtitle": "频率、功率、扩频因子", - "settings_radioSettingsUpdated": "无线电设置已更新", - "settings_location": "位置", - "settings_locationSubtitle": "GPS 坐标", - "settings_locationUpdated": "位置和 GPS 设置已更新", - "settings_locationBothRequired": "请输入经度和纬度", - "settings_locationInvalid": "无效的经度和纬度", - "settings_locationGPSEnable": "启用 GPS", - "settings_locationGPSEnableSubtitle": "启用 GPS 以自动更新位置。", - "settings_locationIntervalSec": "GPS 间隔(秒)", - "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", - "settings_latitude": "纬度", - "settings_longitude": "经度", - "settings_privacyMode": "隐私模式", - "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", - "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", - "settings_privacyModeEnabled": "隐私模式已启用", - "settings_privacyModeDisabled": "隐私模式已关闭", - "settings_actions": "操作", - "settings_sendAdvertisement": "发送广播", - "settings_sendAdvertisementSubtitle": "立即发送广播", - "settings_advertisementSent": "已发送广播", - "settings_syncTime": "同步时间", - "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", - "settings_timeSynchronized": "时间已同步", - "settings_refreshContacts": "刷新联系人", - "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", - "settings_rebootDevice": "重启设备", - "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", - "settings_rebootDeviceConfirm": "确定要重启设备吗?这将断开与设备的连接。", - "settings_debug": "调试", - "settings_bleDebugLog": "BLE 调试日志", - "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", - "settings_appDebugLog": "应用调试日志", - "settings_appDebugLogSubtitle": "应用调试消息", - "settings_about": "关于", + "settings_title": "设置", + "settings_deviceInfo": "设备信息", + "settings_appSettings": "应用设置", + "settings_appSettingsSubtitle": "通知、消息和地图偏好", + "settings_nodeSettings": "节点设置", + "settings_nodeName": "节点名称", + "settings_nodeNameNotSet": "未设置", + "settings_nodeNameHint": "请输入节点名称", + "settings_nodeNameUpdated": "节点名称已更新", + "settings_radioSettings": "无线电设置", + "settings_radioSettingsSubtitle": "频率、功率、扩频因子", + "settings_radioSettingsUpdated": "无线电设置已更新", + "settings_location": "位置", + "settings_locationSubtitle": "GPS 坐标", + "settings_locationUpdated": "位置和 GPS 设置已更新", + "settings_locationBothRequired": "请输入经度和纬度", + "settings_locationInvalid": "无效的经度和纬度", + "settings_locationGPSEnable": "启用 GPS", + "settings_locationGPSEnableSubtitle": "启用 GPS 以自动更新位置。", + "settings_locationIntervalSec": "GPS 间隔(秒)", + "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", + "settings_latitude": "纬度", + "settings_longitude": "经度", + "settings_privacyMode": "隐私模式", + "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", + "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", + "settings_privacyModeEnabled": "隐私模式已启用", + "settings_privacyModeDisabled": "隐私模式已关闭", + "settings_actions": "操作", + "settings_sendAdvertisement": "发送广播", + "settings_sendAdvertisementSubtitle": "立即发送广播", + "settings_advertisementSent": "已发送广播", + "settings_syncTime": "同步时间", + "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", + "settings_timeSynchronized": "时间已同步", + "settings_refreshContacts": "刷新联系人", + "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", + "settings_rebootDevice": "重启设备", + "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", + "settings_rebootDeviceConfirm": "确定要重启设备吗?这将断开与设备的连接。", + "settings_debug": "调试", + "settings_bleDebugLog": "BLE 调试日志", + "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", + "settings_appDebugLog": "应用调试日志", + "settings_appDebugLogSubtitle": "应用调试消息", + "settings_about": "关于", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -134,31 +134,31 @@ } } }, - "settings_aboutLegalese": "2026 MeshCore 开源项目", - "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", - "settings_infoName": "名称", + "settings_aboutLegalese": "2026 MeshCore 开源项目", + "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", + "settings_infoName": "名称", "settings_infoId": "MAC ID", - "settings_infoStatus": "状态", - "settings_infoBattery": "电池", - "settings_infoPublicKey": "公钥", - "settings_infoContactsCount": "联系人数量", - "settings_infoChannelCount": "频道数量", - "settings_presets": "预设", + "settings_infoStatus": "状态", + "settings_infoBattery": "电池", + "settings_infoPublicKey": "公钥", + "settings_infoContactsCount": "联系人数量", + "settings_infoChannelCount": "频道数量", + "settings_presets": "预设", "settings_preset915Mhz": "915 MHz", "settings_preset868Mhz": "868 MHz", "settings_preset433Mhz": "433 MHz", - "settings_frequency": "频率 (MHz)", + "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "无效频率范围(300-2500 MHz)", - "settings_bandwidth": "带宽", - "settings_spreadingFactor": "扩频因子", - "settings_codingRate": "编码速率", - "settings_txPower": "TX 功率 (dBm)", + "settings_frequencyInvalid": "无效频率范围(300-2500 MHz)", + "settings_bandwidth": "带宽", + "settings_spreadingFactor": "扩频因子", + "settings_codingRate": "编码速率", + "settings_txPower": "TX 功率 (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", - "settings_longRange": "远距离", - "settings_fastSpeed": "高速", - "settings_error": "错误:{message}", + "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", + "settings_longRange": "远距离", + "settings_fastSpeed": "高速", + "settings_error": "错误:{message}", "@settings_error": { "placeholders": { "message": { @@ -166,55 +166,55 @@ } } }, - "appSettings_title": "应用设置", - "appSettings_appearance": "外观", - "appSettings_theme": "主题", - "appSettings_themeSystem": "跟随系统", - "appSettings_themeLight": "浅色", - "appSettings_themeDark": "深色", - "appSettings_language": "语言", - "appSettings_languageSystem": "跟随系统", - "appSettings_languageEn": "英语", - "appSettings_languageFr": "法语", - "appSettings_languageEs": "西班牙语", - "appSettings_languageDe": "德语", - "appSettings_languagePl": "波兰语", - "appSettings_languageSl": "斯洛文尼亚语", - "appSettings_languagePt": "葡萄牙语", - "appSettings_languageIt": "意大利语", - "appSettings_languageZh": "中文", - "appSettings_languageSv": "瑞典语", - "appSettings_languageNl": "荷兰语", - "appSettings_languageSk": "斯洛伐克语", - "appSettings_languageBg": "保加利亚语", - "appSettings_languageRu": "俄语", - "appSettings_languageUk": "乌克兰语", - "appSettings_enableMessageTracing": "启用消息追踪", - "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", - "appSettings_notifications": "通知", - "appSettings_enableNotifications": "启用通知", - "appSettings_enableNotificationsSubtitle": "接收消息和广播的通知", - "appSettings_notificationPermissionDenied": "权限被拒绝", - "appSettings_notificationsEnabled": "通知已启用", - "appSettings_notificationsDisabled": "通知已关闭", - "appSettings_messageNotifications": "消息通知", - "appSettings_messageNotificationsSubtitle": "收到新消息时显示通知", - "appSettings_channelMessageNotifications": "频道消息通知", - "appSettings_channelMessageNotificationsSubtitle": "收到频道消息时显示通知", - "appSettings_advertisementNotifications": "广播通知", - "appSettings_advertisementNotificationsSubtitle": "发现新节点时显示通知", - "appSettings_messaging": "消息", - "appSettings_clearPathOnMaxRetry": "达到最大重试次数时清除路径", - "appSettings_clearPathOnMaxRetrySubtitle": "在5次发送失败后重置联系路径。", - "appSettings_pathsWillBeCleared": "5次失败后将重新路由", - "appSettings_pathsWillNotBeCleared": "路径不会自动清除", - "appSettings_autoRouteRotation": "自动路径轮换", - "appSettings_autoRouteRotationSubtitle": "在最佳路径和泛洪模式之间切换", - "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", - "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", - "appSettings_battery": "电池", - "appSettings_batteryChemistry": "电池类型", - "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", + "appSettings_title": "应用设置", + "appSettings_appearance": "外观", + "appSettings_theme": "主题", + "appSettings_themeSystem": "跟随系统", + "appSettings_themeLight": "浅色", + "appSettings_themeDark": "深色", + "appSettings_language": "语言", + "appSettings_languageSystem": "跟随系统", + "appSettings_languageEn": "英语", + "appSettings_languageFr": "法语", + "appSettings_languageEs": "西班牙语", + "appSettings_languageDe": "德语", + "appSettings_languagePl": "波兰语", + "appSettings_languageSl": "斯洛文尼亚语", + "appSettings_languagePt": "葡萄牙语", + "appSettings_languageIt": "意大利语", + "appSettings_languageZh": "中文", + "appSettings_languageSv": "瑞典语", + "appSettings_languageNl": "荷兰语", + "appSettings_languageSk": "斯洛伐克语", + "appSettings_languageBg": "保加利亚语", + "appSettings_languageRu": "俄语", + "appSettings_languageUk": "乌克兰语", + "appSettings_enableMessageTracing": "启用消息追踪", + "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", + "appSettings_notifications": "通知", + "appSettings_enableNotifications": "启用通知", + "appSettings_enableNotificationsSubtitle": "接收消息和广播的通知", + "appSettings_notificationPermissionDenied": "权限被拒绝", + "appSettings_notificationsEnabled": "通知已启用", + "appSettings_notificationsDisabled": "通知已关闭", + "appSettings_messageNotifications": "消息通知", + "appSettings_messageNotificationsSubtitle": "收到新消息时显示通知", + "appSettings_channelMessageNotifications": "频道消息通知", + "appSettings_channelMessageNotificationsSubtitle": "收到频道消息时显示通知", + "appSettings_advertisementNotifications": "广播通知", + "appSettings_advertisementNotificationsSubtitle": "发现新节点时显示通知", + "appSettings_messaging": "消息", + "appSettings_clearPathOnMaxRetry": "达到最大重试次数时清除路径", + "appSettings_clearPathOnMaxRetrySubtitle": "在5次发送失败后重置联系路径。", + "appSettings_pathsWillBeCleared": "5次失败后将重新路由", + "appSettings_pathsWillNotBeCleared": "路径不会自动清除", + "appSettings_autoRouteRotation": "自动路径轮换", + "appSettings_autoRouteRotationSubtitle": "在最佳路径和泛洪模式之间切换", + "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", + "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", + "appSettings_battery": "电池", + "appSettings_batteryChemistry": "电池类型", + "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -222,20 +222,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "请先连接设备", - "appSettings_batteryNmc": "18650 NMC 电池 (3.0-4.2V)", - "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", - "appSettings_batteryLipo": "锂聚合物电池 (3.0-4.2V)", - "appSettings_mapDisplay": "地图显示", - "appSettings_showRepeaters": "显示转发节点", - "appSettings_showRepeatersSubtitle": "在地图上显示转发节点", - "appSettings_showChatNodes": "显示聊天节点", - "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", - "appSettings_showOtherNodes": "显示其他节点", - "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", - "appSettings_timeFilter": "时间过滤器", - "appSettings_timeFilterShowAll": "显示所有节点", - "appSettings_timeFilterShowLast": "显示过去 {hours} 小时内的节点", + "appSettings_batteryChemistryConnectFirst": "请先连接设备", + "appSettings_batteryNmc": "18650 NMC 电池 (3.0-4.2V)", + "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", + "appSettings_batteryLipo": "锂聚合物电池 (3.0-4.2V)", + "appSettings_mapDisplay": "地图显示", + "appSettings_showRepeaters": "显示转发节点", + "appSettings_showRepeatersSubtitle": "在地图上显示转发节点", + "appSettings_showChatNodes": "显示聊天节点", + "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", + "appSettings_showOtherNodes": "显示其他节点", + "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", + "appSettings_timeFilter": "时间过滤器", + "appSettings_timeFilterShowAll": "显示所有节点", + "appSettings_timeFilterShowLast": "显示过去 {hours} 小时内的节点", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -243,16 +243,16 @@ } } }, - "appSettings_mapTimeFilter": "地图时间筛选", - "appSettings_showNodesDiscoveredWithin": "显示在此时间段内发现的节点:", - "appSettings_allTime": "所有时间", - "appSettings_lastHour": "过去一小时", - "appSettings_last6Hours": "过去6小时", - "appSettings_last24Hours": "过去24小时", - "appSettings_lastWeek": "上周", - "appSettings_offlineMapCache": "离线地图缓存", - "appSettings_noAreaSelected": "未选择任何区域", - "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", + "appSettings_mapTimeFilter": "地图时间筛选", + "appSettings_showNodesDiscoveredWithin": "显示在此时间段内发现的节点:", + "appSettings_allTime": "所有时间", + "appSettings_lastHour": "过去一小时", + "appSettings_last6Hours": "过去6小时", + "appSettings_last24Hours": "过去24小时", + "appSettings_lastWeek": "上周", + "appSettings_offlineMapCache": "离线地图缓存", + "appSettings_noAreaSelected": "未选择任何区域", + "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -263,19 +263,19 @@ } } }, - "appSettings_debugCard": "调试", - "appSettings_appDebugLogging": "应用调试日志", - "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以进行故障排除。", - "appSettings_appDebugLoggingEnabled": "调试日志已启用", - "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", - "contacts_title": "联系人", - "contacts_noContacts": "暂无联系人", - "contacts_contactsWillAppear": "当设备发送广播时,联系人将显示。", - "contacts_searchContacts": "搜索联系人...", - "contacts_noUnreadContacts": "没有未读内容", - "contacts_noContactsFound": "未找到任何联系人或群聊", - "contacts_deleteContact": "删除联系人", - "contacts_removeConfirm": "从联系人中移除 {contactName}?", + "appSettings_debugCard": "调试", + "appSettings_appDebugLogging": "应用调试日志", + "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以进行故障排除。", + "appSettings_appDebugLoggingEnabled": "调试日志已启用", + "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", + "contacts_title": "联系人", + "contacts_noContacts": "暂无联系人", + "contacts_contactsWillAppear": "当设备发送广播时,联系人将显示。", + "contacts_searchContacts": "搜索联系人...", + "contacts_noUnreadContacts": "没有未读内容", + "contacts_noContactsFound": "未找到任何联系人或群聊", + "contacts_deleteContact": "删除联系人", + "contacts_removeConfirm": "从联系人中移除 {contactName}?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -283,13 +283,13 @@ } } }, - "contacts_manageRepeater": "管理转发节点", - "contacts_manageRoom": "管理房间服务器", - "contacts_roomLogin": "服务器登录", - "contacts_openChat": "打开聊天", - "contacts_editGroup": "编辑群聊", - "contacts_deleteGroup": "删除群聊", - "contacts_deleteGroupConfirm": "删除群聊 \"{groupName}\"?", + "contacts_manageRepeater": "管理转发节点", + "contacts_manageRoom": "管理房间服务器", + "contacts_roomLogin": "服务器登录", + "contacts_openChat": "打开聊天", + "contacts_editGroup": "编辑群聊", + "contacts_deleteGroup": "删除群聊", + "contacts_deleteGroupConfirm": "删除群聊 \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -297,10 +297,10 @@ } } }, - "contacts_newGroup": "新建群聊", - "contacts_groupName": "群聊名称", - "contacts_groupNameRequired": "请输入群聊名称", - "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", + "contacts_newGroup": "新建群聊", + "contacts_groupName": "群聊名称", + "contacts_groupNameRequired": "请输入群聊名称", + "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -308,11 +308,11 @@ } } }, - "contacts_filterContacts": "筛选联系人...", - "contacts_noContactsMatchFilter": "没有符合条件的联系人", - "contacts_noMembers": "暂无成员", - "contacts_lastSeenNow": "刚刚", - "contacts_lastSeenMinsAgo": "最后在线 {minutes} 分钟前", + "contacts_filterContacts": "筛选联系人...", + "contacts_noContactsMatchFilter": "没有符合条件的联系人", + "contacts_noMembers": "暂无成员", + "contacts_lastSeenNow": "刚刚", + "contacts_lastSeenMinsAgo": "最后在线 {minutes} 分钟前", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -320,8 +320,8 @@ } } }, - "contacts_lastSeenHourAgo": "最后在线 1小时前", - "contacts_lastSeenHoursAgo": "最后在线 {hours} 小时前", + "contacts_lastSeenHourAgo": "最后在线 1小时前", + "contacts_lastSeenHoursAgo": "最后在线 {hours} 小时前", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -329,8 +329,8 @@ } } }, - "contacts_lastSeenDayAgo": "最后在线 1天前", - "contacts_lastSeenDaysAgo": "最后在线 {days} 天前", + "contacts_lastSeenDayAgo": "最后在线 1天前", + "contacts_lastSeenDaysAgo": "最后在线 {days} 天前", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -338,12 +338,12 @@ } } }, - "channels_title": "频道", - "channels_noChannelsConfigured": "未配置任何频道", - "channels_addPublicChannel": "添加公共频道", - "channels_searchChannels": "搜索频道...", - "channels_noChannelsFound": "未找到任何频道", - "channels_channelIndex": "频道 {index}", + "channels_title": "频道", + "channels_noChannelsConfigured": "未配置任何频道", + "channels_addPublicChannel": "添加公共频道", + "channels_searchChannels": "搜索频道...", + "channels_noChannelsFound": "未找到任何频道", + "channels_channelIndex": "频道 {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -351,16 +351,16 @@ } } }, - "channels_hashtagChannel": "标签频道", - "channels_public": "公共", - "channels_private": "私有", - "channels_publicChannel": "公共频道", - "channels_privateChannel": "私有频道", - "channels_editChannel": "编辑频道", - "channels_muteChannel": "静音频道", - "channels_unmuteChannel": "取消静音频道", - "channels_deleteChannel": "删除频道", - "channels_deleteChannelConfirm": "删除频道 \"{name}\"?此操作不可撤销。", + "channels_hashtagChannel": "标签频道", + "channels_public": "公共", + "channels_private": "私有", + "channels_publicChannel": "公共频道", + "channels_privateChannel": "私有频道", + "channels_editChannel": "编辑频道", + "channels_muteChannel": "静音频道", + "channels_unmuteChannel": "取消静音频道", + "channels_deleteChannel": "删除频道", + "channels_deleteChannelConfirm": "删除频道 \"{name}\"?此操作不可撤销。", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -368,7 +368,7 @@ } } }, - "channels_channelDeleted": "已删除频道 \"{name}\"", + "channels_channelDeleted": "已删除频道 \"{name}\"", "@channels_channelDeleted": { "placeholders": { "name": { @@ -376,16 +376,16 @@ } } }, - "channels_addChannel": "添加频道", - "channels_channelIndexLabel": "频道索引", - "channels_channelName": "频道名称", - "channels_usePublicChannel": "使用公共频道", - "channels_standardPublicPsk": "标准公共 PSK", - "channels_pskHex": "PSK (十六进制)", - "channels_generateRandomPsk": "生成随机 PSK", - "channels_enterChannelName": "请输入频道名称", - "channels_pskMustBe32Hex": "PSK 必须为 32 个十六进制字符", - "channels_channelAdded": "已添加频道 \"{name}\"", + "channels_addChannel": "添加频道", + "channels_channelIndexLabel": "频道索引", + "channels_channelName": "频道名称", + "channels_usePublicChannel": "使用公共频道", + "channels_standardPublicPsk": "标准公共 PSK", + "channels_pskHex": "PSK (十六进制)", + "channels_generateRandomPsk": "生成随机 PSK", + "channels_enterChannelName": "请输入频道名称", + "channels_pskMustBe32Hex": "PSK 必须为 32 个十六进制字符", + "channels_channelAdded": "已添加频道 \"{name}\"", "@channels_channelAdded": { "placeholders": { "name": { @@ -393,7 +393,7 @@ } } }, - "channels_editChannelTitle": "编辑频道 {index}", + "channels_editChannelTitle": "编辑频道 {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -401,8 +401,8 @@ } } }, - "channels_smazCompression": "SMAZ 压缩", - "channels_channelUpdated": "频道 \"{name}\" 已更新", + "channels_smazCompression": "SMAZ 压缩", + "channels_channelUpdated": "频道 \"{name}\" 已更新", "@channels_channelUpdated": { "placeholders": { "name": { @@ -410,28 +410,28 @@ } } }, - "channels_publicChannelAdded": "已添加公共频道", - "channels_sortBy": "排序方式", - "channels_sortManual": "手动", + "channels_publicChannelAdded": "已添加公共频道", + "channels_sortBy": "排序方式", + "channels_sortManual": "手动", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "最新消息", - "channels_sortUnread": "未读", - "channels_createPrivateChannel": "创建私有频道", - "channels_createPrivateChannelDesc": "使用密钥保护。", - "channels_joinPrivateChannel": "加入私有频道", - "channels_joinPrivateChannelDesc": "手动输入密钥。", - "channels_joinPublicChannel": "加入公共频道", - "channels_joinPublicChannelDesc": "任何人都可以加入。", - "channels_joinHashtagChannel": "加入标签频道", - "channels_joinHashtagChannelDesc": "任何人都可以加入标签频道。", - "channels_scanQrCode": "扫描二维码", - "channels_scanQrCodeComingSoon": "即将推出", - "channels_enterHashtag": "输入标签", - "channels_hashtagHint": "例如:#团队", - "chat_noMessages": "暂无消息", - "chat_sendMessageToStart": "发送消息开始对话", - "chat_originalMessageNotFound": "找不到原始消息", - "chat_replyingTo": "正在回复 {name}", + "channels_sortLatestMessages": "最新消息", + "channels_sortUnread": "未读", + "channels_createPrivateChannel": "创建私有频道", + "channels_createPrivateChannelDesc": "使用密钥保护。", + "channels_joinPrivateChannel": "加入私有频道", + "channels_joinPrivateChannelDesc": "手动输入密钥。", + "channels_joinPublicChannel": "加入公共频道", + "channels_joinPublicChannelDesc": "任何人都可以加入。", + "channels_joinHashtagChannel": "加入标签频道", + "channels_joinHashtagChannelDesc": "任何人都可以加入标签频道。", + "channels_scanQrCode": "扫描二维码", + "channels_scanQrCodeComingSoon": "即将推出", + "channels_enterHashtag": "输入标签", + "channels_hashtagHint": "例如:#团队", + "chat_noMessages": "暂无消息", + "chat_sendMessageToStart": "发送消息开始对话", + "chat_originalMessageNotFound": "找不到原始消息", + "chat_replyingTo": "正在回复 {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -439,7 +439,7 @@ } } }, - "chat_replyTo": "回复 {name}", + "chat_replyTo": "回复 {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -447,8 +447,8 @@ } } }, - "chat_location": "位置", - "chat_sendMessageTo": "发送消息给 {contactName}", + "chat_location": "位置", + "chat_sendMessageTo": "发送消息给 {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -456,8 +456,8 @@ } } }, - "chat_typeMessage": "输入消息...", - "chat_messageTooLong": "消息过长(最多 {maxBytes} 字节)", + "chat_typeMessage": "输入消息...", + "chat_messageTooLong": "消息过长(最多 {maxBytes} 字节)", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -465,10 +465,10 @@ } } }, - "chat_messageCopied": "消息已复制", - "chat_messageDeleted": "消息已删除", - "chat_retryingMessage": "正在重试消息", - "chat_retryCount": "重试 {current}/{max}", + "chat_messageCopied": "消息已复制", + "chat_messageDeleted": "消息已删除", + "chat_retryingMessage": "正在重试消息", + "chat_retryCount": "重试 {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -479,33 +479,33 @@ } } }, - "chat_sendGif": "发送 GIF", - "chat_reply": "回复", - "chat_addReaction": "添加表情", - "chat_me": "我", - "emojiCategorySmileys": "表情", - "emojiCategoryGestures": "手势", - "emojiCategoryHearts": "爱心", - "emojiCategoryObjects": "物品", - "gifPicker_title": "选择 GIF", - "gifPicker_searchHint": "搜索 GIF...", - "gifPicker_poweredBy": "由 GIPHY 提供", - "gifPicker_noGifsFound": "未找到 GIF", - "gifPicker_failedLoad": "加载 GIF 失败", - "gifPicker_failedSearch": "搜索 GIF 失败", - "gifPicker_noInternet": "无网络连接", - "debugLog_appTitle": "应用调试日志", - "debugLog_bleTitle": "BLE 调试日志", - "debugLog_copyLog": "复制日志", - "debugLog_clearLog": "清除日志", - "debugLog_copied": "调试日志已复制", - "debugLog_bleCopied": "BLE 日志已复制", - "debugLog_noEntries": "暂无调试日志", - "debugLog_enableInSettings": "请在设置中启用应用调试日志。", - "debugLog_frames": "帧", - "debugLog_rawLogRx": "原始日志 RX", - "debugLog_noBleActivity": "暂无 BLE 活动", - "debugFrame_length": "帧长度:{count} 字节", + "chat_sendGif": "发送 GIF", + "chat_reply": "回复", + "chat_addReaction": "添加表情", + "chat_me": "我", + "emojiCategorySmileys": "表情", + "emojiCategoryGestures": "手势", + "emojiCategoryHearts": "爱心", + "emojiCategoryObjects": "物品", + "gifPicker_title": "选择 GIF", + "gifPicker_searchHint": "搜索 GIF...", + "gifPicker_poweredBy": "ç”± GIPHY 提供", + "gifPicker_noGifsFound": "未找到 GIF", + "gifPicker_failedLoad": "加载 GIF 失败", + "gifPicker_failedSearch": "搜索 GIF 失败", + "gifPicker_noInternet": "无网络连接", + "debugLog_appTitle": "应用调试日志", + "debugLog_bleTitle": "BLE 调试日志", + "debugLog_copyLog": "复制日志", + "debugLog_clearLog": "清除日志", + "debugLog_copied": "调试日志已复制", + "debugLog_bleCopied": "BLE 日志已复制", + "debugLog_noEntries": "暂无调试日志", + "debugLog_enableInSettings": "请在设置中启用应用调试日志。", + "debugLog_frames": "帧", + "debugLog_rawLogRx": "原始日志 RX", + "debugLog_noBleActivity": "暂无 BLE 活动", + "debugFrame_length": "帧长度:{count} 字节", "@debugFrame_length": { "placeholders": { "count": { @@ -513,7 +513,7 @@ } } }, - "debugFrame_command": "命令:0x{value}", + "debugFrame_command": "命令:0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -521,8 +521,8 @@ } } }, - "debugFrame_textMessageHeader": "文本消息:", - "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", + "debugFrame_textMessageHeader": "文本消息:", + "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -530,7 +530,7 @@ } } }, - "debugFrame_timestamp": "- 时间戳:{timestamp}", + "debugFrame_timestamp": "- 时间戳:{timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -538,7 +538,7 @@ } } }, - "debugFrame_flags": "- 标志:0x{value}", + "debugFrame_flags": "- 标志:0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -546,7 +546,7 @@ } } }, - "debugFrame_textType": "- 文本类型:{type} ({label})", + "debugFrame_textType": "- 文本类型:{type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -557,9 +557,9 @@ } } }, - "debugFrame_textTypeCli": "命令行", - "debugFrame_textTypePlain": "纯文本", - "debugFrame_text": "- 文本:“{text}”", + "debugFrame_textTypeCli": "命令行", + "debugFrame_textTypePlain": "纯文本", + "debugFrame_text": "- 文本:“{text}”", "@debugFrame_text": { "placeholders": { "text": { @@ -567,16 +567,16 @@ } } }, - "debugFrame_hexDump": "十六进制数据:", - "chat_pathManagement": "路径管理", - "chat_routingMode": "路由模式", - "chat_autoUseSavedPath": "自动(使用保存的路径)", - "chat_forceFloodMode": "强制泛洪模式", - "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", - "chat_pathHistoryFull": "路径历史已满,请删除后再添加。", - "chat_hopSingular": "跳", - "chat_hopPlural": "跳", - "chat_hopsCount": "{count} 跳", + "debugFrame_hexDump": "十六进制数据:", + "chat_pathManagement": "路径管理", + "chat_routingMode": "路由模式", + "chat_autoUseSavedPath": "自动(使用保存的路径)", + "chat_forceFloodMode": "强制泛洪模式", + "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", + "chat_pathHistoryFull": "路径历史已满,请删除后再添加。", + "chat_hopSingular": "è·³", + "chat_hopPlural": "è·³", + "chat_hopsCount": "{count} è·³", "@chat_hopsCount": { "placeholders": { "count": { @@ -584,20 +584,20 @@ } } }, - "chat_successes": "成功", - "chat_removePath": "移除路径", - "chat_noPathHistoryYet": "暂无路径历史。\n发送消息以探索路径。", - "chat_pathActions": "路径操作:", - "chat_setCustomPath": "设置自定义路径", - "chat_setCustomPathSubtitle": "手动指定路由路径", - "chat_clearPath": "清除路径", - "chat_clearPathSubtitle": "清除当前路径,下次发送将重新尝试。", - "chat_pathCleared": "路径已清除。下一条消息将重新路由。", - "chat_floodModeSubtitle": "在应用栏中切换路由模式。", - "chat_floodModeEnabled": "泛洪模式已启用。可通过应用栏的路由图标切换。", - "chat_fullPath": "完整路径", - "chat_pathDetailsNotAvailable": "路径信息暂不可用,请尝试发送消息刷新。", - "chat_pathSetHops": "路径设置:{hopCount} 跳 - {status}", + "chat_successes": "成功", + "chat_removePath": "移除路径", + "chat_noPathHistoryYet": "暂无路径历史。\n发送消息以探索路径。", + "chat_pathActions": "路径操作:", + "chat_setCustomPath": "设置自定义路径", + "chat_setCustomPathSubtitle": "手动指定路由路径", + "chat_clearPath": "清除路径", + "chat_clearPathSubtitle": "清除当前路径,下次发送将重新尝试。", + "chat_pathCleared": "路径已清除。下一条消息将重新路由。", + "chat_floodModeSubtitle": "在应用栏中切换路由模式。", + "chat_floodModeEnabled": "泛洪模式已启用。可通过应用栏的路由图标切换。", + "chat_fullPath": "完整路径", + "chat_pathDetailsNotAvailable": "路径信息暂不可用,请尝试发送消息刷新。", + "chat_pathSetHops": "路径设置:{hopCount} è·³ - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -608,16 +608,16 @@ } } }, - "chat_pathSavedLocally": "已本地保存,连接设备后可同步。", - "chat_pathDeviceConfirmed": "设备已确认。", - "chat_pathDeviceNotConfirmed": "设备尚未确认。", - "chat_type": "类型", - "chat_path": "路径", - "chat_publicKey": "公钥", - "chat_compressOutgoingMessages": "压缩发送的消息", - "chat_floodForced": "泛洪(强制)", - "chat_directForced": "直连(强制)", - "chat_hopsForced": "{count} 跳(强制)", + "chat_pathSavedLocally": "已本地保存,连接设备后可同步。", + "chat_pathDeviceConfirmed": "设备已确认。", + "chat_pathDeviceNotConfirmed": "设备尚未确认。", + "chat_type": "类型", + "chat_path": "路径", + "chat_publicKey": "公钥", + "chat_compressOutgoingMessages": "压缩发送的消息", + "chat_floodForced": "泛洪(强制)", + "chat_directForced": "直连(强制)", + "chat_hopsForced": "{count} 跳(强制)", "@chat_hopsForced": { "placeholders": { "count": { @@ -625,10 +625,10 @@ } } }, - "chat_floodAuto": "自动泛洪", - "chat_direct": "直连", - "chat_poiShared": "共享位置", - "chat_unread": "未读:{count}", + "chat_floodAuto": "自动泛洪", + "chat_direct": "直连", + "chat_poiShared": "共享位置", + "chat_unread": "未读:{count}", "@chat_unread": { "placeholders": { "count": { @@ -636,10 +636,10 @@ } } }, - "chat_openLink": "打开链接?", - "chat_openLinkConfirmation": "是否使用浏览器打开此链接?", - "chat_open": "打开", - "chat_couldNotOpenLink": "无法打开链接:{url}", + "chat_openLink": "打开链接?", + "chat_openLinkConfirmation": "是否使用浏览器打开此链接?", + "chat_open": "打开", + "chat_couldNotOpenLink": "无法打开链接:{url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -647,11 +647,11 @@ } } }, - "chat_invalidLink": "无效的链接格式", - "map_title": "节点地图", - "map_noNodesWithLocation": "没有包含位置信息的节点", - "map_nodesNeedGps": "节点需要共享 GPS 坐标才能在地图上显示", - "map_nodesCount": "节点:{count}", + "chat_invalidLink": "无效的链接格式", + "map_title": "节点地图", + "map_noNodesWithLocation": "没有包含位置信息的节点", + "map_nodesNeedGps": "节点需要共享 GPS 坐标才能在地图上显示", + "map_nodesCount": "节点:{count}", "@map_nodesCount": { "placeholders": { "count": { @@ -659,7 +659,7 @@ } } }, - "map_pinsCount": "标记:{count}", + "map_pinsCount": "标记:{count}", "@map_pinsCount": { "placeholders": { "count": { @@ -667,27 +667,27 @@ } } }, - "map_chat": "聊天", - "map_repeater": "转发节点", - "map_room": "房间", - "map_sensor": "传感器", - "map_pinDm": "标记(私信)", - "map_pinPrivate": "私有", - "map_pinPublic": "公共", - "map_lastSeen": "最后在线", - "map_disconnectConfirm": "确定要断开与此设备的连接吗?", - "map_from": "来自", - "map_source": "来源", - "map_flags": "标志", - "map_shareMarkerHere": "在此分享标记", - "map_pinLabel": "标签", - "map_label": "标签", - "map_pointOfInterest": "兴趣点", - "map_sendToContact": "发送给联系人", - "map_sendToChannel": "发送到频道", - "map_noChannelsAvailable": "没有可用的频道", - "map_publicLocationShare": "公共位置共享", - "map_publicLocationShareConfirm": "您即将在 {channelLabel} 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。", + "map_chat": "聊天", + "map_repeater": "转发节点", + "map_room": "房间", + "map_sensor": "传感器", + "map_pinDm": "标记(私信)", + "map_pinPrivate": "私有", + "map_pinPublic": "公共", + "map_lastSeen": "最后在线", + "map_disconnectConfirm": "确定要断开与此设备的连接吗?", + "map_from": "来自", + "map_source": "来源", + "map_flags": "标志", + "map_shareMarkerHere": "在此分享标记", + "map_pinLabel": "标签", + "map_label": "标签", + "map_pointOfInterest": "兴趣点", + "map_sendToContact": "发送给联系人", + "map_sendToChannel": "发送到频道", + "map_noChannelsAvailable": "没有可用的频道", + "map_publicLocationShare": "公共位置共享", + "map_publicLocationShareConfirm": "您即将在 {channelLabel} 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -695,26 +695,26 @@ } } }, - "map_connectToShareMarkers": "连接设备以共享标记", - "map_filterNodes": "过滤节点", - "map_nodeTypes": "节点类型", - "map_chatNodes": "聊天节点", - "map_repeaters": "转发节点", - "map_otherNodes": "其他节点", - "map_keyPrefix": "关键字前缀", - "map_filterByKeyPrefix": "按关键字前缀筛选", - "map_publicKeyPrefix": "关键字前缀", - "map_markers": "标记", - "map_showSharedMarkers": "显示共享标记", - "map_lastSeenTime": "最后在线时间", - "map_sharedPin": "共享标记", - "map_joinRoom": "加入房间", - "map_manageRepeater": "管理转发节点", - "mapCache_title": "离线地图缓存", - "mapCache_selectAreaFirst": "请先选择要缓存的区域", - "mapCache_noTilesToDownload": "此区域没有可下载的瓦片", - "mapCache_downloadTilesTitle": "下载瓦片", - "mapCache_downloadTilesPrompt": "这需要下载 {count} 个瓦片", + "map_connectToShareMarkers": "连接设备以共享标记", + "map_filterNodes": "过滤节点", + "map_nodeTypes": "节点类型", + "map_chatNodes": "聊天节点", + "map_repeaters": "转发节点", + "map_otherNodes": "其他节点", + "map_keyPrefix": "关键字前缀", + "map_filterByKeyPrefix": "按关键字前缀筛选", + "map_publicKeyPrefix": "关键字前缀", + "map_markers": "标记", + "map_showSharedMarkers": "显示共享标记", + "map_lastSeenTime": "最后在线时间", + "map_sharedPin": "共享标记", + "map_joinRoom": "加入房间", + "map_manageRepeater": "管理转发节点", + "mapCache_title": "离线地图缓存", + "mapCache_selectAreaFirst": "请先选择要缓存的区域", + "mapCache_noTilesToDownload": "此区域没有可下载的瓦片", + "mapCache_downloadTilesTitle": "下载瓦片", + "mapCache_downloadTilesPrompt": "这需要下载 {count} 个瓦片", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -722,8 +722,8 @@ } } }, - "mapCache_downloadAction": "下载", - "mapCache_cachedTiles": "已缓存 {count} 个瓦片", + "mapCache_downloadAction": "下载", + "mapCache_cachedTiles": "已缓存 {count} 个瓦片", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -731,7 +731,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片({failed} 个失败)", + "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片({failed} 个失败)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -742,14 +742,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "清除离线缓存", - "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", - "mapCache_offlineCacheCleared": "离线缓存已清除", - "mapCache_noAreaSelected": "未选择区域", - "mapCache_cacheArea": "缓存区域", - "mapCache_useCurrentView": "使用当前视图", - "mapCache_zoomRange": "缩放范围", - "mapCache_estimatedTiles": "估计瓦片数:{count}", + "mapCache_clearOfflineCacheTitle": "清除离线缓存", + "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", + "mapCache_offlineCacheCleared": "离线缓存已清除", + "mapCache_noAreaSelected": "未选择区域", + "mapCache_cacheArea": "缓存区域", + "mapCache_useCurrentView": "使用当前视图", + "mapCache_zoomRange": "缩放范围", + "mapCache_estimatedTiles": "估计瓦片数:{count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -757,7 +757,7 @@ } } }, - "mapCache_downloadedTiles": "已下载 {completed}/{total}", + "mapCache_downloadedTiles": "已下载 {completed}/{total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -768,9 +768,9 @@ } } }, - "mapCache_downloadTilesButton": "下载瓦片", - "mapCache_clearCacheButton": "清除缓存", - "mapCache_failedDownloads": "下载失败:{count}", + "mapCache_downloadTilesButton": "下载瓦片", + "mapCache_clearCacheButton": "清除缓存", + "mapCache_failedDownloads": "下载失败:{count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -778,7 +778,7 @@ } } }, - "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", + "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -795,8 +795,8 @@ } } }, - "time_justNow": "刚才", - "time_minutesAgo": "{minutes}分钟前", + "time_justNow": "刚才", + "time_minutesAgo": "{minutes}分钟前", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -804,7 +804,7 @@ } } }, - "time_hoursAgo": "{hours}小时前", + "time_hoursAgo": "{hours}小时前", "@time_hoursAgo": { "placeholders": { "hours": { @@ -812,7 +812,7 @@ } } }, - "time_daysAgo": "{days}天前", + "time_daysAgo": "{days}天前", "@time_daysAgo": { "placeholders": { "days": { @@ -820,33 +820,33 @@ } } }, - "time_hour": "小时", - "time_hours": "小时", - "time_day": "天", - "time_days": "天", - "time_week": "周", - "time_weeks": "周", - "time_month": "月", - "time_months": "月", - "time_minutes": "分钟", - "time_allTime": "所有时间", - "dialog_disconnect": "断开", - "dialog_disconnectConfirm": "确定要断开与此设备的连接吗?", - "login_repeaterLogin": "转发节点登录", - "login_roomLogin": "房间服务器登录", - "login_password": "密码", - "login_enterPassword": "请输入密码", - "login_savePassword": "保存密码", - "login_savePasswordSubtitle": "密码将安全地存储在此设备上", - "login_repeaterDescription": "输入转发节点密码以访问设置和状态。", - "login_roomDescription": "输入房间服务器密码以访问设置和状态。", - "login_routing": "路由", - "login_routingMode": "路由模式", - "login_autoUseSavedPath": "自动(使用保存的路径)", - "login_forceFloodMode": "强制泛洪模式", - "login_managePaths": "管理路径", - "login_login": "登录", - "login_attempt": "尝试 {current}/{max}", + "time_hour": "小时", + "time_hours": "小时", + "time_day": "天", + "time_days": "天", + "time_week": "周", + "time_weeks": "周", + "time_month": "月", + "time_months": "月", + "time_minutes": "分钟", + "time_allTime": "所有时间", + "dialog_disconnect": "æ–­å¼€", + "dialog_disconnectConfirm": "确定要断开与此设备的连接吗?", + "login_repeaterLogin": "转发节点登录", + "login_roomLogin": "房间服务器登录", + "login_password": "密码", + "login_enterPassword": "请输入密码", + "login_savePassword": "保存密码", + "login_savePasswordSubtitle": "密码将安全地存储在此设备上", + "login_repeaterDescription": "输入转发节点密码以访问设置和状态。", + "login_roomDescription": "输入房间服务器密码以访问设置和状态。", + "login_routing": "路由", + "login_routingMode": "路由模式", + "login_autoUseSavedPath": "自动(使用保存的路径)", + "login_forceFloodMode": "强制泛洪模式", + "login_managePaths": "管理路径", + "login_login": "登录", + "login_attempt": "尝试 {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -857,7 +857,7 @@ } } }, - "login_failed": "登录失败:{error}", + "login_failed": "登录失败:{error}", "@login_failed": { "placeholders": { "error": { @@ -865,10 +865,10 @@ } } }, - "login_failedMessage": "登录失败。可能是密码错误或无法连接到服务器。", - "common_reload": "重新加载", - "common_clear": "清除", - "path_currentPath": "当前路径:{path}", + "login_failedMessage": "登录失败。可能是密码错误或无法连接到服务器。", + "common_reload": "重新加载", + "common_clear": "清除", + "path_currentPath": "当前路径:{path}", "@path_currentPath": { "placeholders": { "path": { @@ -876,7 +876,7 @@ } } }, - "path_usingHopsPath": "使用 {count} 跳路径", + "path_usingHopsPath": "使用 {count} 跳路径", "@path_usingHopsPath": { "placeholders": { "count": { @@ -884,16 +884,16 @@ } } }, - "path_enterCustomPath": "输入自定义路径", - "path_currentPathLabel": "当前路径", - "path_hexPrefixInstructions": "请输入每个中继节点的2字符十六进制前缀,用逗号分隔。", - "path_hexPrefixExample": "例如:A1, F2, 3C(每个节点使用其公钥的第一字节)", - "path_labelHexPrefixes": "路径(十六进制前缀)", - "path_helperMaxHops": "最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。", - "path_selectFromContacts": "或从联系人列表中选择:", - "path_noRepeatersFound": "未找到任何转发节点或房间服务器。", - "path_customPathsRequire": "自定义路径需要中间节点转发消息。", - "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", + "path_enterCustomPath": "输入自定义路径", + "path_currentPathLabel": "当前路径", + "path_hexPrefixInstructions": "请输入每个中继节点的2字符十六进制前缀,用逗号分隔。", + "path_hexPrefixExample": "例如:A1, F2, 3C(每个节点使用其公钥的第一字节)", + "path_labelHexPrefixes": "路径(十六进制前缀)", + "path_helperMaxHops": "最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。", + "path_selectFromContacts": "或从联系人列表中选择:", + "path_noRepeatersFound": "未找到任何转发节点或房间服务器。", + "path_customPathsRequire": "自定义路径需要中间节点转发消息。", + "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -901,29 +901,29 @@ } } }, - "path_tooLong": "路径过长,最多允许 64 跳。", - "path_setPath": "设置路径", - "repeater_management": "转发节点管理", - "room_management": "房间服务器管理", - "repeater_managementTools": "管理工具", - "repeater_status": "状态", - "repeater_statusSubtitle": "查看转发节点状态、统计和邻居", - "repeater_telemetry": "遥测", - "repeater_telemetrySubtitle": "查看传感器和系统状态数据", - "repeater_cli": "命令行", - "repeater_cliSubtitle": "向转发节点发送命令", - "repeater_neighbors": "邻居", - "repeater_neighborsSubtitle": "查看邻居节点(零跳)", - "repeater_settings": "设置", - "repeater_settingsSubtitle": "配置转发节点参数", - "repeater_statusTitle": "转发节点状态", - "repeater_routingMode": "路由模式", - "repeater_autoUseSavedPath": "自动(使用保存的路径)", - "repeater_forceFloodMode": "强制泛洪模式", - "repeater_pathManagement": "路径管理", - "repeater_refresh": "刷新", - "repeater_statusRequestTimeout": "状态请求超时", - "repeater_errorLoadingStatus": "加载状态时出错:{error}", + "path_tooLong": "路径过长,最多允许 64 跳。", + "path_setPath": "设置路径", + "repeater_management": "转发节点管理", + "room_management": "房间服务器管理", + "repeater_managementTools": "管理工具", + "repeater_status": "状态", + "repeater_statusSubtitle": "查看转发节点状态、统计和邻居", + "repeater_telemetry": "遥测", + "repeater_telemetrySubtitle": "查看传感器和系统状态数据", + "repeater_cli": "命令行", + "repeater_cliSubtitle": "向转发节点发送命令", + "repeater_neighbors": "邻居", + "repeater_neighborsSubtitle": "查看邻居节点(零跳)", + "repeater_settings": "设置", + "repeater_settingsSubtitle": "配置转发节点参数", + "repeater_statusTitle": "转发节点状态", + "repeater_routingMode": "路由模式", + "repeater_autoUseSavedPath": "自动(使用保存的路径)", + "repeater_forceFloodMode": "强制泛洪模式", + "repeater_pathManagement": "路径管理", + "repeater_refresh": "刷新", + "repeater_statusRequestTimeout": "状态请求超时", + "repeater_errorLoadingStatus": "加载状态时出错:{error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -931,23 +931,23 @@ } } }, - "repeater_systemInformation": "系统信息", - "repeater_battery": "电池", - "repeater_clockAtLogin": "登录时的时钟", - "repeater_uptime": "运行时间", - "repeater_queueLength": "队列长度", - "repeater_debugFlags": "调试标志", - "repeater_radioStatistics": "无线电统计", - "repeater_lastRssi": "上次 RSSI", - "repeater_lastSnr": "上次 SNR", - "repeater_noiseFloor": "底噪", - "repeater_txAirtime": "发送空中时间", - "repeater_rxAirtime": "接收空中时间", - "repeater_packetStatistics": "数据包统计", - "repeater_sent": "发送", - "repeater_received": "接收", - "repeater_duplicates": "重复", - "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}秒", + "repeater_systemInformation": "系统信息", + "repeater_battery": "电池", + "repeater_clockAtLogin": "登录时的时钟", + "repeater_uptime": "运行时间", + "repeater_queueLength": "队列长度", + "repeater_debugFlags": "调试标志", + "repeater_radioStatistics": "无线电统计", + "repeater_lastRssi": "上次 RSSI", + "repeater_lastSnr": "上次 SNR", + "repeater_noiseFloor": "底噪", + "repeater_txAirtime": "发送空中时间", + "repeater_rxAirtime": "接收空中时间", + "repeater_packetStatistics": "数据包统计", + "repeater_sent": "发送", + "repeater_received": "接收", + "repeater_duplicates": "重复", + "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}ç§’", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -964,7 +964,7 @@ } } }, - "repeater_packetTxTotal": "总计:{total},泛洪:{flood},直连:{direct}", + "repeater_packetTxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -978,7 +978,7 @@ } } }, - "repeater_packetRxTotal": "总计:{total},泛洪:{flood},直连:{direct}", + "repeater_packetRxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -992,7 +992,7 @@ } } }, - "repeater_duplicatesFloodDirect": "泛洪:{flood},直连:{direct}", + "repeater_duplicatesFloodDirect": "泛洪:{flood},直连:{direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -1003,7 +1003,7 @@ } } }, - "repeater_duplicatesTotal": "总计:{total}", + "repeater_duplicatesTotal": "总计:{total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -1011,37 +1011,37 @@ } } }, - "repeater_settingsTitle": "转发节点设置", - "repeater_basicSettings": "基本设置", - "repeater_repeaterName": "转发节点名称", - "repeater_repeaterNameHelper": "此转发节点的显示名称", - "repeater_adminPassword": "管理员密码", - "repeater_adminPasswordHelper": "完整访问密码", - "repeater_guestPassword": "访客密码", - "repeater_guestPasswordHelper": "只读访问密码", - "repeater_radioSettings": "无线电设置", - "repeater_frequencyMhz": "频率 (MHz)", + "repeater_settingsTitle": "转发节点设置", + "repeater_basicSettings": "基本设置", + "repeater_repeaterName": "转发节点名称", + "repeater_repeaterNameHelper": "此转发节点的显示名称", + "repeater_adminPassword": "管理员密码", + "repeater_adminPasswordHelper": "完整访问密码", + "repeater_guestPassword": "访客密码", + "repeater_guestPasswordHelper": "只读访问密码", + "repeater_radioSettings": "无线电设置", + "repeater_frequencyMhz": "频率 (MHz)", "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX 功率", + "repeater_txPower": "TX 功率", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "带宽", - "repeater_spreadingFactor": "扩频因子", - "repeater_codingRate": "编码速率", - "repeater_locationSettings": "位置设置", - "repeater_latitude": "纬度", - "repeater_latitudeHelper": "十进制,例如 37.7749", - "repeater_longitude": "经度", - "repeater_longitudeHelper": "十进制,例如 -122.4194", - "repeater_features": "功能", - "repeater_packetForwarding": "数据包转发", - "repeater_packetForwardingSubtitle": "启用转发节点转发数据包", - "repeater_guestAccess": "访客访问", - "repeater_guestAccessSubtitle": "允许访客只读权限", - "repeater_privacyMode": "隐私模式", - "repeater_privacyModeSubtitle": "在广播中隐藏姓名/位置", - "repeater_advertisementSettings": "广播设置", - "repeater_localAdvertInterval": "本地广播间隔", - "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", + "repeater_bandwidth": "带宽", + "repeater_spreadingFactor": "扩频因子", + "repeater_codingRate": "编码速率", + "repeater_locationSettings": "位置设置", + "repeater_latitude": "纬度", + "repeater_latitudeHelper": "十进制,例如 37.7749", + "repeater_longitude": "经度", + "repeater_longitudeHelper": "十进制,例如 -122.4194", + "repeater_features": "功能", + "repeater_packetForwarding": "数据包转发", + "repeater_packetForwardingSubtitle": "启用转发节点转发数据包", + "repeater_guestAccess": "访客访问", + "repeater_guestAccessSubtitle": "允许访客只读权限", + "repeater_privacyMode": "隐私模式", + "repeater_privacyModeSubtitle": "在广播中隐藏姓名/位置", + "repeater_advertisementSettings": "广播设置", + "repeater_localAdvertInterval": "本地广播间隔", + "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1049,8 +1049,8 @@ } } }, - "repeater_floodAdvertInterval": "泛洪广播间隔", - "repeater_floodAdvertIntervalHours": "{hours} 小时", + "repeater_floodAdvertInterval": "泛洪广播间隔", + "repeater_floodAdvertIntervalHours": "{hours} 小时", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1058,19 +1058,19 @@ } } }, - "repeater_encryptedAdvertInterval": "加密广播间隔", - "repeater_dangerZone": "危险设置", - "repeater_rebootRepeater": "重启转发节点", - "repeater_rebootRepeaterSubtitle": "重启转发节点设备", - "repeater_rebootRepeaterConfirm": "确定要重启此转发节点吗?", - "repeater_regenerateIdentityKey": "重新生成身份密钥", - "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", - "repeater_regenerateIdentityKeyConfirm": "这将为转发节点生成新身份,继续吗?", - "repeater_eraseFileSystem": "擦除文件系统", - "repeater_eraseFileSystemSubtitle": "格式化转发节点文件系统", - "repeater_eraseFileSystemConfirm": "警告:此操作将清除转发节点上的所有数据,且无法恢复!", - "repeater_eraseSerialOnly": "擦除功能仅可通过串行控制台使用。", - "repeater_commandSent": "命令已发送:{command}", + "repeater_encryptedAdvertInterval": "加密广播间隔", + "repeater_dangerZone": "危险设置", + "repeater_rebootRepeater": "重启转发节点", + "repeater_rebootRepeaterSubtitle": "重启转发节点设备", + "repeater_rebootRepeaterConfirm": "确定要重启此转发节点吗?", + "repeater_regenerateIdentityKey": "重新生成身份密钥", + "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", + "repeater_regenerateIdentityKeyConfirm": "这将为转发节点生成新身份,继续吗?", + "repeater_eraseFileSystem": "擦除文件系统", + "repeater_eraseFileSystemSubtitle": "格式化转发节点文件系统", + "repeater_eraseFileSystemConfirm": "警告:此操作将清除转发节点上的所有数据,且无法恢复!", + "repeater_eraseSerialOnly": "擦除功能仅可通过串行控制台使用。", + "repeater_commandSent": "命令已发送:{command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1078,7 +1078,7 @@ } } }, - "repeater_errorSendingCommand": "发送命令时出错:{error}", + "repeater_errorSendingCommand": "发送命令时出错:{error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1086,9 +1086,9 @@ } } }, - "repeater_confirm": "确认", - "repeater_settingsSaved": "设置保存成功", - "repeater_errorSavingSettings": "保存设置时出错:{error}", + "repeater_confirm": "确认", + "repeater_settingsSaved": "设置保存成功", + "repeater_errorSavingSettings": "保存设置时出错:{error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1096,15 +1096,15 @@ } } }, - "repeater_refreshBasicSettings": "刷新基本设置", - "repeater_refreshRadioSettings": "刷新无线电设置", - "repeater_refreshTxPower": "刷新 TX 功率", - "repeater_refreshLocationSettings": "刷新位置设置", - "repeater_refreshPacketForwarding": "刷新包转发", - "repeater_refreshGuestAccess": "刷新访客权限", - "repeater_refreshPrivacyMode": "刷新隐私模式", - "repeater_refreshAdvertisementSettings": "刷新广播设置", - "repeater_refreshed": "{label} 已刷新", + "repeater_refreshBasicSettings": "刷新基本设置", + "repeater_refreshRadioSettings": "刷新无线电设置", + "repeater_refreshTxPower": "刷新 TX 功率", + "repeater_refreshLocationSettings": "刷新位置设置", + "repeater_refreshPacketForwarding": "刷新包转发", + "repeater_refreshGuestAccess": "刷新访客权限", + "repeater_refreshPrivacyMode": "刷新隐私模式", + "repeater_refreshAdvertisementSettings": "刷新广播设置", + "repeater_refreshed": "{label} 已刷新", "@repeater_refreshed": { "placeholders": { "label": { @@ -1112,7 +1112,7 @@ } } }, - "repeater_errorRefreshing": "刷新 {label} 时出错", + "repeater_errorRefreshing": "刷新 {label} 时出错", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1120,18 +1120,18 @@ } } }, - "repeater_cliTitle": "转发节点命令行", - "repeater_debugNextCommand": "调试下一条命令", - "repeater_commandHelp": "帮助", - "repeater_clearHistory": "清除历史", - "repeater_noCommandsSent": "尚未发送命令", - "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", - "repeater_enterCommandHint": "输入命令...", - "repeater_previousCommand": "上一条命令", - "repeater_nextCommand": "下一条命令", - "repeater_enterCommandFirst": "请先输入命令", - "repeater_cliCommandFrameTitle": "CLI 命令帧", - "repeater_cliCommandError": "错误:{error}", + "repeater_cliTitle": "转发节点命令行", + "repeater_debugNextCommand": "调试下一条命令", + "repeater_commandHelp": "帮助", + "repeater_clearHistory": "清除历史", + "repeater_noCommandsSent": "尚未发送命令", + "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", + "repeater_enterCommandHint": "输入命令...", + "repeater_previousCommand": "上一条命令", + "repeater_nextCommand": "下一条命令", + "repeater_enterCommandFirst": "请先输入命令", + "repeater_cliCommandFrameTitle": "CLI 命令帧", + "repeater_cliCommandError": "错误:{error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1139,81 +1139,81 @@ } } }, - "repeater_cliQuickGetName": "获取名称", - "repeater_cliQuickGetRadio": "获取无线电设置", - "repeater_cliQuickGetTx": "获取 TX", - "repeater_cliQuickNeighbors": "邻居", - "repeater_cliQuickVersion": "版本", - "repeater_cliQuickAdvertise": "发送广播", - "repeater_cliQuickClock": "时钟", - "repeater_cliHelpAdvert": "发送广播包", - "repeater_cliHelpReboot": "重启设备。(注意:可能会收到超时错误,属于正常现象)", - "repeater_cliHelpClock": "显示设备当前时间", - "repeater_cliHelpPassword": "设置新的管理员密码", - "repeater_cliHelpVersion": "显示设备版本和固件构建日期", - "repeater_cliHelpClearStats": "重置各种统计数据", - "repeater_cliHelpSetAf": "设置时间因子", - "repeater_cliHelpSetTx": "设置 LoRa 发射功率 (dBm)(重启生效)", - "repeater_cliHelpSetRepeat": "启用或禁用此节点的转发功能", - "repeater_cliHelpSetAllowReadOnly": "(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)", - "repeater_cliHelpSetFloodMax": "设置最大传入数据包跳数(≥该值则不转发)", - "repeater_cliHelpSetIntThresh": "设置干扰阈值 (dB),默认14,设为0禁用", - "repeater_cliHelpSetAgcResetInterval": "设置 AGC 重置间隔(秒),设为0禁用", - "repeater_cliHelpSetMultiAcks": "启用或禁用“多重确认”功能", - "repeater_cliHelpSetAdvertInterval": "设置本地广播间隔(分钟),设为0禁用", - "repeater_cliHelpSetFloodAdvertInterval": "设置泛洪广播间隔(小时),设为0禁用", - "repeater_cliHelpSetGuestPassword": "设置/更新访客密码", - "repeater_cliHelpSetName": "设置广播名称", - "repeater_cliHelpSetLat": "设置广播纬度(十进制)", - "repeater_cliHelpSetLon": "设置广播经度(十进制)", - "repeater_cliHelpSetRadio": "完全重设无线电参数并保存,需重启生效", - "repeater_cliHelpSetRxDelay": "(实验性)设置接收延迟基数,设为0禁用", - "repeater_cliHelpSetTxDelay": "通过因子和随机时隙延迟泛洪数据包转发,降低冲突", - "repeater_cliHelpSetDirectTxDelay": "同 txdelay,用于直连模式数据包", - "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接", - "repeater_cliHelpSetBridgeDelay": "设置桥接转发延迟", - "repeater_cliHelpSetBridgeSource": "选择桥接器转发接收或发送的数据包", - "repeater_cliHelpSetBridgeBaud": "设置 RS232 桥接串口波特率", - "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥接密钥", - "repeater_cliHelpSetAdcMultiplier": "设置电池电压校正系数(特定板支持)", - "repeater_cliHelpTempRadio": "临时设置无线电参数指定分钟,之后恢复(不保存)", - "repeater_cliHelpSetPerm": "修改 ACL,权限位:0访客、1只读、2读写、3管理员", - "repeater_cliHelpGetBridgeType": "支持桥接模式:RS232、ESPNOW", - "repeater_cliHelpLogStart": "开始记录数据包到文件系统", - "repeater_cliHelpLogStop": "停止记录数据包", - "repeater_cliHelpLogErase": "删除所有记录的数据包", - "repeater_cliHelpNeighbors": "显示零跳广播收到的其他转发节点列表", - "repeater_cliHelpNeighborRemove": "从邻居列表删除第一个匹配项(通过公钥前缀)", - "repeater_cliHelpRegion": "(仅串口)列出所有定义区域及当前泛洪权限", - "repeater_cliHelpRegionLoad": "特殊多命令调用,以空行结束", - "repeater_cliHelpRegionGet": "搜索指定前缀的区域", - "repeater_cliHelpRegionPut": "添加或更新区域定义", - "repeater_cliHelpRegionRemove": "删除指定区域定义", - "repeater_cliHelpRegionAllowf": "为区域设置“泛洪”权限", - "repeater_cliHelpRegionDenyf": "移除区域的“泛洪”权限", - "repeater_cliHelpRegionHome": "返回当前“主区域”(预留)", - "repeater_cliHelpRegionHomeSet": "设置“主”区域", - "repeater_cliHelpRegionSave": "保存区域列表到存储", - "repeater_cliHelpGps": "显示 GPS 状态", - "repeater_cliHelpGpsOnOff": "切换 GPS 电源", - "repeater_cliHelpGpsSync": "将节点时间与 GPS 同步", - "repeater_cliHelpGpsSetLoc": "将节点坐标设为 GPS 坐标并保存", - "repeater_cliHelpGpsAdvert": "设置位置广播配置:none/share/prefs", - "repeater_cliHelpGpsAdvertSet": "设置广播位置配置", - "repeater_commandsListTitle": "命令列表", - "repeater_commandsListNote": "注意:多数 set 命令也有对应的 get 命令", - "repeater_general": "通用", - "repeater_settingsCategory": "设置", - "repeater_bridge": "桥接", - "repeater_logging": "日志", - "repeater_neighborsRepeaterOnly": "邻居(仅转发节点)", - "repeater_regionManagementRepeaterOnly": "区域管理(仅转发节点)", - "repeater_regionNote": "区域命令用于管理区域定义和权限", - "repeater_gpsManagement": "GPS 管理", - "repeater_gpsNote": "GPS 命令用于位置相关任务", - "telemetry_receivedData": "接收到的遥测数据", - "telemetry_requestTimeout": "遥测请求超时", - "telemetry_errorLoading": "加载遥测数据时出错:{error}", + "repeater_cliQuickGetName": "获取名称", + "repeater_cliQuickGetRadio": "获取无线电设置", + "repeater_cliQuickGetTx": "获取 TX", + "repeater_cliQuickNeighbors": "邻居", + "repeater_cliQuickVersion": "版本", + "repeater_cliQuickAdvertise": "发送广播", + "repeater_cliQuickClock": "æ—¶é’Ÿ", + "repeater_cliHelpAdvert": "发送广播包", + "repeater_cliHelpReboot": "重启设备。(注意:可能会收到超时错误,属于正常现象)", + "repeater_cliHelpClock": "显示设备当前时间", + "repeater_cliHelpPassword": "设置新的管理员密码", + "repeater_cliHelpVersion": "显示设备版本和固件构建日期", + "repeater_cliHelpClearStats": "重置各种统计数据", + "repeater_cliHelpSetAf": "设置时间因子", + "repeater_cliHelpSetTx": "设置 LoRa 发射功率 (dBm)(重启生效)", + "repeater_cliHelpSetRepeat": "启用或禁用此节点的转发功能", + "repeater_cliHelpSetAllowReadOnly": "(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)", + "repeater_cliHelpSetFloodMax": "设置最大传入数据包跳数(≥该值则不转发)", + "repeater_cliHelpSetIntThresh": "设置干扰阈值 (dB),默认14,设为0禁用", + "repeater_cliHelpSetAgcResetInterval": "设置 AGC 重置间隔(秒),设为0禁用", + "repeater_cliHelpSetMultiAcks": "启用或禁用“多重确认”功能", + "repeater_cliHelpSetAdvertInterval": "设置本地广播间隔(分钟),设为0禁用", + "repeater_cliHelpSetFloodAdvertInterval": "设置泛洪广播间隔(小时),设为0禁用", + "repeater_cliHelpSetGuestPassword": "设置/更新访客密码", + "repeater_cliHelpSetName": "设置广播名称", + "repeater_cliHelpSetLat": "设置广播纬度(十进制)", + "repeater_cliHelpSetLon": "设置广播经度(十进制)", + "repeater_cliHelpSetRadio": "完全重设无线电参数并保存,需重启生效", + "repeater_cliHelpSetRxDelay": "(实验性)设置接收延迟基数,设为0禁用", + "repeater_cliHelpSetTxDelay": "通过因子和随机时隙延迟泛洪数据包转发,降低冲突", + "repeater_cliHelpSetDirectTxDelay": "同 txdelay,用于直连模式数据包", + "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接", + "repeater_cliHelpSetBridgeDelay": "设置桥接转发延迟", + "repeater_cliHelpSetBridgeSource": "选择桥接器转发接收或发送的数据包", + "repeater_cliHelpSetBridgeBaud": "设置 RS232 桥接串口波特率", + "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥接密钥", + "repeater_cliHelpSetAdcMultiplier": "设置电池电压校正系数(特定板支持)", + "repeater_cliHelpTempRadio": "临时设置无线电参数指定分钟,之后恢复(不保存)", + "repeater_cliHelpSetPerm": "修改 ACL,权限位:0访客、1只读、2读写、3管理员", + "repeater_cliHelpGetBridgeType": "支持桥接模式:RS232、ESPNOW", + "repeater_cliHelpLogStart": "开始记录数据包到文件系统", + "repeater_cliHelpLogStop": "停止记录数据包", + "repeater_cliHelpLogErase": "删除所有记录的数据包", + "repeater_cliHelpNeighbors": "显示零跳广播收到的其他转发节点列表", + "repeater_cliHelpNeighborRemove": "从邻居列表删除第一个匹配项(通过公钥前缀)", + "repeater_cliHelpRegion": "(仅串口)列出所有定义区域及当前泛洪权限", + "repeater_cliHelpRegionLoad": "特殊多命令调用,以空行结束", + "repeater_cliHelpRegionGet": "搜索指定前缀的区域", + "repeater_cliHelpRegionPut": "添加或更新区域定义", + "repeater_cliHelpRegionRemove": "删除指定区域定义", + "repeater_cliHelpRegionAllowf": "为区域设置“泛洪”权限", + "repeater_cliHelpRegionDenyf": "移除区域的“泛洪”权限", + "repeater_cliHelpRegionHome": "返回当前“主区域”(预留)", + "repeater_cliHelpRegionHomeSet": "设置“主”区域", + "repeater_cliHelpRegionSave": "保存区域列表到存储", + "repeater_cliHelpGps": "显示 GPS 状态", + "repeater_cliHelpGpsOnOff": "切换 GPS 电源", + "repeater_cliHelpGpsSync": "将节点时间与 GPS 同步", + "repeater_cliHelpGpsSetLoc": "将节点坐标设为 GPS 坐标并保存", + "repeater_cliHelpGpsAdvert": "设置位置广播配置:none/share/prefs", + "repeater_cliHelpGpsAdvertSet": "设置广播位置配置", + "repeater_commandsListTitle": "命令列表", + "repeater_commandsListNote": "注意:多数 set 命令也有对应的 get 命令", + "repeater_general": "通用", + "repeater_settingsCategory": "设置", + "repeater_bridge": "桥接", + "repeater_logging": "日志", + "repeater_neighborsRepeaterOnly": "邻居(仅转发节点)", + "repeater_regionManagementRepeaterOnly": "区域管理(仅转发节点)", + "repeater_regionNote": "区域命令用于管理区域定义和权限", + "repeater_gpsManagement": "GPS 管理", + "repeater_gpsNote": "GPS 命令用于位置相关任务", + "telemetry_receivedData": "接收到的遥测数据", + "telemetry_requestTimeout": "遥测请求超时", + "telemetry_errorLoading": "加载遥测数据时出错:{error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1221,8 +1221,8 @@ } } }, - "telemetry_noData": "暂无遥测数据", - "telemetry_channelTitle": "频道 {channel}", + "telemetry_noData": "暂无遥测数据", + "telemetry_channelTitle": "频道 {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1230,11 +1230,11 @@ } } }, - "telemetry_batteryLabel": "电池", - "telemetry_voltageLabel": "电压", - "telemetry_mcuTemperatureLabel": "MCU 温度", - "telemetry_temperatureLabel": "温度", - "telemetry_currentLabel": "电流", + "telemetry_batteryLabel": "电池", + "telemetry_voltageLabel": "电压", + "telemetry_mcuTemperatureLabel": "MCU 温度", + "telemetry_temperatureLabel": "温度", + "telemetry_currentLabel": "电流", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1262,7 +1262,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1273,9 +1273,9 @@ } } }, - "neighbors_receivedData": "已接收邻居信息", - "neighbors_requestTimedOut": "邻居请求超时", - "neighbors_errorLoading": "加载邻居时出错:{error}", + "neighbors_receivedData": "已接收邻居信息", + "neighbors_requestTimedOut": "邻居请求超时", + "neighbors_errorLoading": "加载邻居时出错:{error}", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1283,9 +1283,9 @@ } } }, - "neighbors_repeatersNeighbors": "转发节点的邻居", - "neighbors_noData": "暂无邻居信息", - "neighbors_unknownContact": "未知 {pubkey}", + "neighbors_repeatersNeighbors": "转发节点的邻居", + "neighbors_noData": "暂无邻居信息", + "neighbors_unknownContact": "未知 {pubkey}", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1293,7 +1293,7 @@ } } }, - "neighbors_heardAgo": "听到:{time}前", + "neighbors_heardAgo": "听到:{time}前", "@neighbors_heardAgo": { "placeholders": { "time": { @@ -1301,18 +1301,18 @@ } } }, - "channelPath_title": "数据包路径", - "channelPath_viewMap": "查看地图", - "channelPath_otherObservedPaths": "其他观察到的路径", - "channelPath_repeaterHops": "转发节点跳数", - "channelPath_noHopDetails": "此数据包未提供详细信息", - "channelPath_messageDetails": "消息详情", - "channelPath_senderLabel": "发送者", - "channelPath_timeLabel": "时间", - "channelPath_repeatsLabel": "重复", - "channelPath_pathLabel": "路径 {index}", - "channelPath_observedLabel": "观察到的", - "channelPath_observedPathTitle": "观察到的路径 {index} • {hops}", + "channelPath_title": "数据包路径", + "channelPath_viewMap": "查看地图", + "channelPath_otherObservedPaths": "其他观察到的路径", + "channelPath_repeaterHops": "转发节点跳数", + "channelPath_noHopDetails": "此数据包未提供详细信息", + "channelPath_messageDetails": "消息详情", + "channelPath_senderLabel": "发送者", + "channelPath_timeLabel": "æ—¶é—´", + "channelPath_repeatsLabel": "重复", + "channelPath_pathLabel": "路径 {index}", + "channelPath_observedLabel": "观察到的", + "channelPath_observedPathTitle": "观察到的路径 {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1323,7 +1323,7 @@ } } }, - "channelPath_noLocationData": "无位置信息", + "channelPath_noLocationData": "无位置信息", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1346,10 +1346,10 @@ } } }, - "channelPath_unknownPath": "未知", - "channelPath_floodPath": "泛洪", - "channelPath_directPath": "直连", - "channelPath_observedZeroOf": "0 / {total} 跳", + "channelPath_unknownPath": "未知", + "channelPath_floodPath": "泛洪", + "channelPath_directPath": "直连", + "channelPath_observedZeroOf": "0 / {total} è·³", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1357,7 +1357,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} / {total} 跳", + "channelPath_observedSomeOf": "{observed} / {total} è·³", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1368,9 +1368,9 @@ } } }, - "channelPath_mapTitle": "路径地图", - "channelPath_noRepeaterLocations": "此路径上没有可用的转发节点位置信息", - "channelPath_primaryPath": "路径 {index}(主要)", + "channelPath_mapTitle": "路径地图", + "channelPath_noRepeaterLocations": "此路径上没有可用的转发节点位置信息", + "channelPath_primaryPath": "路径 {index}(主要)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1385,9 +1385,9 @@ } } }, - "channelPath_pathLabelTitle": "路径", - "channelPath_observedPathHeader": "观察到的路径", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "路径", + "channelPath_observedPathHeader": "观察到的路径", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1398,14 +1398,14 @@ } } }, - "channelPath_noHopDetailsAvailable": "此数据包暂无详细信息", - "channelPath_unknownRepeater": "未知转发节点", - "community_title": "社区", - "community_create": "创建社区", - "community_createDesc": "创建新社区并通过二维码分享。", - "community_join": "加入", - "community_joinTitle": "加入社区", - "community_joinConfirmation": "是否加入社区 \"{name}\"?", + "channelPath_noHopDetailsAvailable": "此数据包暂无详细信息", + "channelPath_unknownRepeater": "未知转发节点", + "community_title": "社区", + "community_create": "创建社区", + "community_createDesc": "创建新社区并通过二维码分享。", + "community_join": "加入", + "community_joinTitle": "加入社区", + "community_joinConfirmation": "是否加入社区 \"{name}\"?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1413,14 +1413,14 @@ } } }, - "community_scanQr": "扫描社区二维码", - "community_scanInstructions": "将摄像头对准社区的二维码", - "community_showQr": "显示二维码", - "community_publicChannel": "社区公共频道", - "community_hashtagChannel": "社区标签频道", - "community_name": "社区名称", - "community_enterName": "请输入社区名称", - "community_created": "社区 \"{name}\" 已创建", + "community_scanQr": "扫描社区二维码", + "community_scanInstructions": "将摄像头对准社区的二维码", + "community_showQr": "显示二维码", + "community_publicChannel": "社区公共频道", + "community_hashtagChannel": "社区标签频道", + "community_name": "社区名称", + "community_enterName": "请输入社区名称", + "community_created": "社区 \"{name}\" 已创建", "@community_created": { "placeholders": { "name": { @@ -1428,7 +1428,7 @@ } } }, - "community_joined": "已加入社区 \"{name}\"", + "community_joined": "已加入社区 \"{name}\"", "@community_joined": { "placeholders": { "name": { @@ -1436,8 +1436,8 @@ } } }, - "community_qrTitle": "分享社区", - "community_qrInstructions": "扫描此二维码加入 \"{name}\"", + "community_qrTitle": "分享社区", + "community_qrInstructions": "扫描此二维码加入 \"{name}\"", "@community_qrInstructions": { "placeholders": { "name": { @@ -1445,10 +1445,10 @@ } } }, - "community_hashtagPrivacyHint": "仅社区成员可加入社区标签频道。", - "community_invalidQrCode": "无效的社区二维码", - "community_alreadyMember": "已是成员", - "community_alreadyMemberMessage": "您已是 \"{name}\" 的成员。", + "community_hashtagPrivacyHint": "仅社区成员可加入社区标签频道。", + "community_invalidQrCode": "无效的社区二维码", + "community_alreadyMember": "已是成员", + "community_alreadyMemberMessage": "您已是 \"{name}\" 的成员。", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1456,13 +1456,13 @@ } } }, - "community_addPublicChannel": "添加公共频道", - "community_addPublicChannelHint": "自动添加此社区的公共频道", - "community_noCommunities": "尚未加入任何社区。", - "community_scanOrCreate": "扫描二维码或创建社区以开始。", - "community_manageCommunities": "管理社区", - "community_delete": "退出社区", - "community_deleteConfirm": "是否退出 \"{name}\"?", + "community_addPublicChannel": "添加公共频道", + "community_addPublicChannelHint": "自动添加此社区的公共频道", + "community_noCommunities": "尚未加入任何社区。", + "community_scanOrCreate": "扫描二维码或创建社区以开始。", + "community_manageCommunities": "管理社区", + "community_delete": "退出社区", + "community_deleteConfirm": "是否退出 \"{name}\"?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1470,7 +1470,7 @@ } } }, - "community_deleteChannelsWarning": "这将同时删除 {count} 个频道及其所有消息。", + "community_deleteChannelsWarning": "这将同时删除 {count} 个频道及其所有消息。", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1478,7 +1478,7 @@ } } }, - "community_deleted": "已退出社区 \"{name}\"", + "community_deleted": "已退出社区 \"{name}\"", "@community_deleted": { "placeholders": { "name": { @@ -1486,8 +1486,8 @@ } } }, - "community_regenerateSecret": "重新生成密钥", - "community_regenerateSecretConfirm": "是否为 \"{name}\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。", + "community_regenerateSecret": "重新生成密钥", + "community_regenerateSecretConfirm": "是否为 \"{name}\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1495,8 +1495,8 @@ } } }, - "community_regenerate": "重新生成", - "community_secretRegenerated": "已为 \"{name}\" 重新生成密钥", + "community_regenerate": "重新生成", + "community_secretRegenerated": "已为 \"{name}\" 重新生成密钥", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1504,8 +1504,8 @@ } } }, - "community_updateSecret": "更新密钥", - "community_secretUpdated": "“{name}”的密钥已更新", + "community_updateSecret": "更新密钥", + "community_secretUpdated": "“{name}”的密钥已更新", "@community_secretUpdated": { "placeholders": { "name": { @@ -1513,7 +1513,7 @@ } } }, - "community_scanToUpdateSecret": "扫描新二维码以更新 \"{name}\" 的密钥", + "community_scanToUpdateSecret": "扫描新二维码以更新 \"{name}\" 的密钥", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1521,14 +1521,14 @@ } } }, - "community_addHashtagChannel": "添加标签频道", - "community_addHashtagChannelDesc": "为此社区创建标签频道", - "community_selectCommunity": "选择社区", - "community_regularHashtag": "普通标签", - "community_regularHashtagDesc": "公共标签频道(任何人都可参与)", - "community_communityHashtag": "社区标签", - "community_communityHashtagDesc": "仅限社区成员", - "community_forCommunity": "为 {name}", + "community_addHashtagChannel": "添加标签频道", + "community_addHashtagChannelDesc": "为此社区创建标签频道", + "community_selectCommunity": "选择社区", + "community_regularHashtag": "普通标签", + "community_regularHashtagDesc": "公共标签频道(任何人都可参与)", + "community_communityHashtag": "社区标签", + "community_communityHashtagDesc": "仅限社区成员", + "community_forCommunity": "为 {name}", "@community_forCommunity": { "placeholders": { "name": { @@ -1536,30 +1536,30 @@ } } }, - "listFilter_tooltip": "筛选与排序", - "listFilter_sortBy": "排序方式", - "listFilter_latestMessages": "最新消息", - "listFilter_heardRecently": "最近听到", + "listFilter_tooltip": "筛选与排序", + "listFilter_sortBy": "排序方式", + "listFilter_latestMessages": "最新消息", + "listFilter_heardRecently": "最近听到", "listFilter_az": "A-Z", - "listFilter_filters": "筛选", - "listFilter_all": "全部", - "listFilter_users": "用户", - "listFilter_repeaters": "转发节点", - "listFilter_roomServers": "房间服务器", - "listFilter_unreadOnly": "仅显示未读", - "listFilter_newGroup": "新建群聊", - "pathTrace_you": "我自己", - "pathTrace_failed": "路径追踪失败。", - "pathTrace_notAvailable": "无法获取路径信息。", - "pathTrace_refreshTooltip": "刷新路径追踪", - "contacts_pathTrace": "路径追踪", + "listFilter_filters": "筛选", + "listFilter_all": "全部", + "listFilter_users": "用户", + "listFilter_repeaters": "转发节点", + "listFilter_roomServers": "房间服务器", + "listFilter_unreadOnly": "仅显示未读", + "listFilter_newGroup": "新建群聊", + "pathTrace_you": "我自己", + "pathTrace_failed": "路径追踪失败。", + "pathTrace_notAvailable": "无法获取路径信息。", + "pathTrace_refreshTooltip": "刷新路径追踪", + "contacts_pathTrace": "路径追踪", "contacts_ping": "Ping", - "contacts_repeaterPathTrace": "Trace 转发节点", - "contacts_repeaterPing": "Ping 转发节点", - "contacts_roomPathTrace": "Trace 房间服务器", - "contacts_roomPing": "Ping 房间服务器", - "contacts_chatTraceRoute": "路由追踪", - "contacts_pathTraceTo": "追踪至 {name} 的路径", + "contacts_repeaterPathTrace": "Trace 转发节点", + "contacts_repeaterPing": "Ping 转发节点", + "contacts_roomPathTrace": "Trace 房间服务器", + "contacts_roomPing": "Ping 房间服务器", + "contacts_chatTraceRoute": "路由追踪", + "contacts_pathTraceTo": "追踪至 {name} 的路径", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1567,66 +1567,66 @@ } } }, - "contacts_clipboardEmpty": "剪贴板为空", - "contacts_invalidAdvertFormat": "无效的联系人信息格式", - "contacts_contactImported": "联系人已导入", - "contacts_contactImportFailed": "导入联系人失败。", - "contacts_zeroHopAdvert": "发送零跳广播", - "contacts_floodAdvert": "发送泛洪广播", - "contacts_copyAdvertToClipboard": "复制广播到剪贴板", - "contacts_addContactFromClipboard": "从剪贴板添加联系人", - "contacts_ShareContact": "复制联系人信息到剪贴板", - "contacts_ShareContactZeroHop": "通过广播分享联系人", - "contacts_zeroHopContactAdvertSent": "零跳广播已发送", - "contacts_zeroHopContactAdvertFailed": "发送联系人广播失败。", - "contacts_contactAdvertCopied": "广播已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "复制广播到剪贴板失败。", - "notification_activityTitle": "MeshCore 活动", - "notification_messagesCount": "{count} 条消息", - "notification_channelMessagesCount": "{count} 条频道消息", - "notification_newNodesCount": "{count} 个新节点", - "notification_newTypeDiscovered": "发现新 {contactType}", - "notification_receivedNewMessage": "收到新消息", - "settings_gpxExportRepeaters": "导出转发节点/房间服务器到 GPX", - "settings_gpxExportRepeatersSubtitle": "导出带位置的转发节点/房间服务器到 GPX 文件", - "settings_gpxExportContactsSubtitle": "导出带位置的伙伴到 GPX 文件", - "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", - "settings_gpxExportSuccess": "GPX 文件导出成功", - "settings_gpxExportError": "导出时出错", - "settings_gpxExportRepeatersRoom": "转发节点与房间服务器位置", - "settings_gpxExportChat": "伙伴位置", - "settings_gpxExportAll": "导出所有联系人到 GPX", - "settings_gpxExportContacts": "导出伙伴到 GPX", - "settings_gpxExportAllSubtitle": "导出所有带位置的联系人到 GPX 文件", - "settings_gpxExportAllContacts": "所有联系人位置", - "settings_gpxExportNoContacts": "没有可导出的联系人", - "settings_gpxExportShareText": "来自 MeshCore Open 的地图数据导出", - "settings_gpxExportShareSubject": "MeshCore Open GPX 地图数据导出", - "pathTrace_someHopsNoLocation": "某些跳缺少位置信息!", - "map_tapToAdd": "点击节点以添加到路径", - "pathTrace_clearTooltip": "清除路径", - "map_pathTraceCancelled": "路径追踪已取消", - "map_removeLast": "移除最后一个", - "map_runTrace": "运行路径追踪", - "scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备", - "scanner_chromeRequired": "需要 Chrome 浏览器", - "scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。", - "scanner_bluetoothOff": "蓝牙已关闭", - "scanner_enableBluetooth": "启用蓝牙", - "snrIndicator_lastSeen": "最近访问", - "snrIndicator_nearByRepeaters": "附近的重复器", - "chat_ShowAllPaths": "显示所有路径", - "settings_clientRepeat": "离网重复", - "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", - "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。", - "settings_aboutOpenMeteoAttribution": "LOS 高程数据:Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "单位", - "appSettings_unitsMetric": "公制(米/公里)", - "appSettings_unitsImperial": "英制 (ft / mi)", - "map_lineOfSight": "视线", - "map_losScreenTitle": "视线", - "losSelectStartEnd": "选择 LOS 的起始节点和结束节点。", - "losRunFailed": "视线检查失败:{error}", + "contacts_clipboardEmpty": "剪贴板为空", + "contacts_invalidAdvertFormat": "无效的联系人信息格式", + "contacts_contactImported": "联系人已导入", + "contacts_contactImportFailed": "导入联系人失败。", + "contacts_zeroHopAdvert": "发送零跳广播", + "contacts_floodAdvert": "发送泛洪广播", + "contacts_copyAdvertToClipboard": "复制广播到剪贴板", + "contacts_addContactFromClipboard": "从剪贴板添加联系人", + "contacts_ShareContact": "复制联系人信息到剪贴板", + "contacts_ShareContactZeroHop": "通过广播分享联系人", + "contacts_zeroHopContactAdvertSent": "零跳广播已发送", + "contacts_zeroHopContactAdvertFailed": "发送联系人广播失败。", + "contacts_contactAdvertCopied": "广播已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "复制广播到剪贴板失败。", + "notification_activityTitle": "MeshCore 活动", + "notification_messagesCount": "{count} 条消息", + "notification_channelMessagesCount": "{count} 条频道消息", + "notification_newNodesCount": "{count} 个新节点", + "notification_newTypeDiscovered": "发现新 {contactType}", + "notification_receivedNewMessage": "收到新消息", + "settings_gpxExportRepeaters": "导出转发节点/房间服务器到 GPX", + "settings_gpxExportRepeatersSubtitle": "导出带位置的转发节点/房间服务器到 GPX 文件", + "settings_gpxExportContactsSubtitle": "导出带位置的伙伴到 GPX 文件", + "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", + "settings_gpxExportSuccess": "GPX 文件导出成功", + "settings_gpxExportError": "导出时出错", + "settings_gpxExportRepeatersRoom": "转发节点与房间服务器位置", + "settings_gpxExportChat": "伙伴位置", + "settings_gpxExportAll": "导出所有联系人到 GPX", + "settings_gpxExportContacts": "导出伙伴到 GPX", + "settings_gpxExportAllSubtitle": "导出所有带位置的联系人到 GPX 文件", + "settings_gpxExportAllContacts": "所有联系人位置", + "settings_gpxExportNoContacts": "没有可导出的联系人", + "settings_gpxExportShareText": "来自 MeshCore Open 的地图数据导出", + "settings_gpxExportShareSubject": "MeshCore Open GPX 地图数据导出", + "pathTrace_someHopsNoLocation": "某些跳缺少位置信息!", + "map_tapToAdd": "点击节点以添加到路径", + "pathTrace_clearTooltip": "清除路径", + "map_pathTraceCancelled": "路径追踪已取消", + "map_removeLast": "移除最后一个", + "map_runTrace": "运行路径追踪", + "scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备", + "scanner_chromeRequired": "需要 Chrome 浏览器", + "scanner_chromeRequiredMessage": "æ­¤ Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。", + "scanner_bluetoothOff": "蓝牙已关闭", + "scanner_enableBluetooth": "启用蓝牙", + "snrIndicator_lastSeen": "最近访问", + "snrIndicator_nearByRepeaters": "附近的重复器", + "chat_ShowAllPaths": "显示所有路径", + "settings_clientRepeat": "离网重复", + "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", + "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。", + "settings_aboutOpenMeteoAttribution": "LOS 高程数据:Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "单位", + "appSettings_unitsMetric": "公制(米/公里)", + "appSettings_unitsImperial": "英制 (ft / mi)", + "map_lineOfSight": "视线", + "map_losScreenTitle": "视线", + "losSelectStartEnd": "选择 LOS 的起始节点和结束节点。", + "losRunFailed": "视线检查失败:{error}", "@losRunFailed": { "placeholders": { "error": { @@ -1634,13 +1634,13 @@ } } }, - "losClearAllPoints": "清除所有点", - "losRunToViewElevationProfile": "运行 LOS 查看高程剖面", - "losMenuTitle": "服务水平菜单", - "losMenuSubtitle": "点击节点或长按地图以获取自定义点", - "losShowDisplayNodes": "显示显示节点", - "losCustomPoints": "自定义积分", - "losCustomPointLabel": "自定义 {index}", + "losClearAllPoints": "清除所有点", + "losRunToViewElevationProfile": "运行 LOS 查看高程剖面", + "losMenuTitle": "服务水平菜单", + "losMenuSubtitle": "点击节点或长按地图以获取自定义点", + "losShowDisplayNodes": "显示显示节点", + "losCustomPoints": "自定义积分", + "losCustomPointLabel": "自定义 {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1648,9 +1648,9 @@ } } }, - "losPointA": "A点", - "losPointB": "B点", - "losAntennaA": "天线 A: {value} {unit}", + "losPointA": "A点", + "losPointB": "B点", + "losAntennaA": "天线 A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1661,7 +1661,7 @@ } } }, - "losAntennaB": "天线 B:{value} {unit}", + "losAntennaB": "天线 B:{value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1672,9 +1672,9 @@ } } }, - "losRun": "运行视距", - "losNoElevationData": "无海拔数据", - "losProfileClear": "{distance} {distanceUnit},清除 LOS,最小间隙 {clearance} {heightUnit}", + "losRun": "运行视距", + "losNoElevationData": "无海拔数据", + "losProfileClear": "{distance} {distanceUnit},清除 LOS,最小间隙 {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1691,7 +1691,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit},被 {obstruction} {heightUnit} 阻止", + "losProfileBlocked": "{distance} {distanceUnit},被 {obstruction} {heightUnit} 阻止", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1708,9 +1708,9 @@ } } }, - "losStatusChecking": "洛斯:正在检查...", - "losStatusNoData": "LOS:无数据", - "losStatusSummary": "LOS:{clear}/{total} 清除,{blocked} 阻塞,{unknown} 未知", + "losStatusChecking": "洛斯:正在检查...", + "losStatusNoData": "LOS:无数据", + "losStatusSummary": "LOS:{clear}/{total} 清除,{blocked} 阻塞,{unknown} 未知", "@losStatusSummary": { "placeholders": { "clear": { @@ -1727,20 +1727,20 @@ } } }, - "losErrorElevationUnavailable": "一个或多个样本的海拔数据不可用。", - "losErrorInvalidInput": "用于 LOS 计算的点/高程数据无效。", - "losRenameCustomPoint": "重命名自定义点", - "losPointName": "点名称", - "losShowPanelTooltip": "显示 LOS 面板", - "losHidePanelTooltip": "隐藏 LOS 面板", - "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "无线电地平线", - "losLegendLosBeam": "视距波束", - "losLegendTerrain": "地形", - "losFrequencyLabel": "频率", - "losFrequencyInfoTooltip": "查看计算详情", - "losFrequencyDialogTitle": "无线电地平线计算", - "losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", + "losErrorElevationUnavailable": "一个或多个样本的海拔数据不可用。", + "losErrorInvalidInput": "用于 LOS 计算的点/高程数据无效。", + "losRenameCustomPoint": "重命名自定义点", + "losPointName": "点名称", + "losShowPanelTooltip": "显示 LOS 面板", + "losHidePanelTooltip": "隐藏 LOS 面板", + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "无线电地平线", + "losLegendLosBeam": "视距波束", + "losLegendTerrain": "地形", + "losFrequencyLabel": "频率", + "losFrequencyInfoTooltip": "查看计算详情", + "losFrequencyDialogTitle": "无线电地平线计算", + "losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1758,9 +1758,9 @@ } } }, - "listFilter_favorites": "收藏", - "listFilter_addToFavorites": "添加到收藏", - "listFilter_removeFromFavorites": "从收藏中移除", + "listFilter_favorites": "收藏", + "listFilter_addToFavorites": "添加到收藏", + "listFilter_removeFromFavorites": "从收藏中移除", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1801,19 +1801,17 @@ } } }, - "contacts_searchUsers": "搜索 {number}{str} 位用户...", - "contacts_unread": "未读", - "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", - "contacts_searchContactsNoNumber": "搜索联系人...", - "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", - "contacts_searchFavorites": "搜索 {number}{str} 收藏...", - "connectionChoiceSubtitle": "请选择您希望如何访问 MeshCore 设备的选项。", - "connectionChoiceBluetoothLabel": "蓝牙", - "connectionChoiceTitle": "选择您的连接方式", + "contacts_searchUsers": "搜索 {number}{str} 位用户...", + "contacts_unread": "未读", + "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", + "contacts_searchContactsNoNumber": "搜索联系人...", + "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", + "contacts_searchFavorites": "搜索 {number}{str} 收藏...", + "connectionChoiceBluetoothLabel": "蓝牙", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "通过USB连接", - "usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。", - "usbScreenStatus": "选择一个 USB 设备", - "usbScreenNote": "在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。", - "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。" + "usbScreenTitle": "通过USB连接", + "usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。", + "usbScreenStatus": "选择一个 USB 设备", + "usbScreenNote": "在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。", + "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。" } diff --git a/lib/main.dart b/lib/main.dart index dd503fd..9e53e21 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,7 @@ import 'screens/chrome_required_screen.dart'; import 'utils/platform_info.dart'; import 'connector/meshcore_connector.dart'; -import 'screens/connection_choice_screen.dart'; +import 'screens/scanner_screen.dart'; import 'services/storage_service.dart'; import 'services/message_retry_service.dart'; import 'services/path_history_service.dart'; @@ -192,7 +192,7 @@ class MeshCoreApp extends StatelessWidget { }, home: (PlatformInfo.isWeb && !PlatformInfo.isChrome) ? const ChromeRequiredScreen() - : const ConnectionChoiceScreen(), + : const ScannerScreen(), ); }, ), diff --git a/lib/screens/connection_choice_screen.dart b/lib/screens/connection_choice_screen.dart deleted file mode 100644 index 16634c6..0000000 --- a/lib/screens/connection_choice_screen.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:math' as math; - -import 'package:flutter/material.dart'; - -import '../l10n/l10n.dart'; -import '../utils/platform_info.dart'; -import 'scanner_screen.dart'; -import 'usb_screen.dart'; - -/// Entry point that lets the user choose between USB or Bluetooth. -class ConnectionChoiceScreen extends StatelessWidget { - const ConnectionChoiceScreen({super.key}); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - final theme = Theme.of(context); - final usbSupported = PlatformInfo.supportsUsbSerial; - return Scaffold( - appBar: AppBar( - title: FittedBox( - fit: BoxFit.scaleDown, - child: Text(l10n.appTitle, textAlign: TextAlign.center), - ), - centerTitle: true, - automaticallyImplyLeading: false, - ), - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), - child: LayoutBuilder( - builder: (context, constraints) { - final availableHeight = constraints.maxHeight.isFinite - ? constraints.maxHeight - : 600.0; - final gap = math.max( - 8.0, - math.min(20.0, availableHeight * 0.035), - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - flex: 3, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - l10n.connectionChoiceTitle, - textAlign: TextAlign.center, - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - ), - ), - SizedBox(height: math.max(4.0, gap * 0.5)), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - l10n.connectionChoiceSubtitle, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ), - ], - ), - ), - ), - SizedBox(height: gap), - Expanded( - flex: 4, - child: _ConnectionMethodButton( - icon: Icons.usb, - label: l10n.connectionChoiceUsbLabel, - color: theme.colorScheme.primaryContainer, - iconColor: theme.colorScheme.onPrimaryContainer, - onPressed: usbSupported - ? () { - debugPrint( - 'ConnectionChoiceScreen: USB selected, opening UsbScreen', - ); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const UsbScreen(), - ), - ); - } - : null, - ), - ), - SizedBox(height: gap), - Expanded( - flex: 4, - child: _ConnectionMethodButton( - icon: Icons.bluetooth, - label: l10n.connectionChoiceBluetoothLabel, - color: theme.colorScheme.surfaceContainerHighest, - iconColor: theme.colorScheme.onSurfaceVariant, - onPressed: () { - debugPrint( - 'ConnectionChoiceScreen: Bluetooth selected, opening ScannerScreen', - ); - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const ScannerScreen(), - ), - ); - }, - ), - ), - ], - ); - }, - ), - ), - ), - ); - } -} - -class _ConnectionMethodButton extends StatelessWidget { - const _ConnectionMethodButton({ - required this.icon, - required this.label, - required this.onPressed, - required this.color, - required this.iconColor, - }); - - final IconData icon; - final String label; - final VoidCallback? onPressed; - final Color color; - final Color iconColor; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: color, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)), - minimumSize: const Size.fromHeight(0), - ), - onPressed: onPressed, - child: LayoutBuilder( - builder: (context, constraints) { - final availableHeight = constraints.maxHeight.isFinite - ? constraints.maxHeight - : 200.0; - final availableWidth = constraints.maxWidth.isFinite - ? constraints.maxWidth - : 320.0; - final isCompact = availableHeight < 72.0 || availableWidth < 180.0; - final useTightVertical = !isCompact && availableHeight < 120.0; - final baseGap = isCompact - ? 8.0 - : (useTightVertical - ? math.max(4.0, math.min(8.0, availableHeight * 0.06)) - : 12.0); - final labelStyle = - (isCompact - ? theme.textTheme.titleMedium - : (useTightVertical - ? theme.textTheme.titleMedium - : theme.textTheme.titleLarge)) - ?.copyWith(fontWeight: FontWeight.w600); - final verticalIconSize = useTightVertical - ? math.max(32.0, math.min(48.0, availableHeight * 0.42)) - : 60.0; - final content = isCompact - ? Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: 24.0, color: iconColor), - SizedBox(width: baseGap), - Flexible( - child: Text( - label, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: labelStyle, - ), - ), - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, size: verticalIconSize, color: iconColor), - SizedBox(height: baseGap), - Text( - label, - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.visible, - style: labelStyle, - ), - ], - ); - - return Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: math.max(0, availableWidth - 12), - maxHeight: math.max(0, availableHeight - 12), - ), - child: content, - ), - ), - ); - }, - ), - ); - } -} diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 69ddf6c..54074a9 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -9,6 +9,7 @@ import '../l10n/l10n.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; import 'contacts_screen.dart'; +import 'usb_screen.dart'; /// Screen for scanning and connecting to MeshCore devices class ScannerScreen extends StatefulWidget { @@ -114,40 +115,67 @@ class _ScannerScreenState extends State { }, ), ), - floatingActionButton: Consumer( + bottomNavigationBar: Consumer( builder: (context, connector, child) { final isScanning = connector.state == MeshCoreConnectionState.scanning; final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; + final usbSupported = PlatformInfo.supportsUsbSerial; - return FloatingActionButton.extended( - onPressed: isBluetoothOff - ? null - : () { - if (isScanning) { - connector.stopScan(); - } else { - unawaited( - connector.startScan().catchError((e) { - debugPrint("Scanner screen startScan error: $e"); - }), + return SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (usbSupported) + FloatingActionButton.extended( + onPressed: () { + debugPrint( + 'ScannerScreen: USB selected, opening UsbScreen', ); - } - }, - icon: isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Icon(Icons.bluetooth_searching), - label: Text( - isScanning - ? context.l10n.scanner_stop - : context.l10n.scanner_scan, + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const UsbScreen()), + ); + }, + heroTag: 'scanner_usb_action', + icon: const Icon(Icons.usb), + label: Text(context.l10n.connectionChoiceUsbLabel), + ), + if (usbSupported) const SizedBox(width: 12), + FloatingActionButton.extended( + heroTag: 'scanner_ble_action', + onPressed: isBluetoothOff + ? null + : () { + if (isScanning) { + connector.stopScan(); + } else { + unawaited( + connector.startScan().catchError((e) { + debugPrint( + "Scanner screen startScan error: $e", + ); + }), + ); + } + }, + icon: isScanning + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.bluetooth_searching), + label: Text( + isScanning + ? context.l10n.scanner_stop + : context.l10n.scanner_scan, + ), + ), + ], ), ); }, diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 03d9446..c703a64 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -5,10 +5,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../connector/meshcore_connector_usb.dart'; import '../l10n/l10n.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'contacts_screen.dart'; +import 'scanner_screen.dart'; class UsbScreen extends StatefulWidget { const UsbScreen({super.key}); @@ -28,6 +30,7 @@ class _UsbScreenState extends State { String? _errorText; Timer? _hotPlugTimer; late final MeshCoreConnector _connector; + late final MeshCoreConnectorUsb _usbConnector; late final VoidCallback _connectionListener; /// Whether the current platform supports dynamic hot-plug polling. @@ -40,12 +43,13 @@ class _UsbScreenState extends State { void initState() { super.initState(); _connector = context.read(); + _usbConnector = MeshCoreConnectorUsb(_connector); _connectionListener = () { if (!mounted) return; - final activeUsbPortDisplayLabel = _connector.activeUsbPortDisplayLabel; + final activeUsbPortDisplayLabel = _usbConnector.activeUsbPortDisplayLabel; final shouldUpdateDisplayLabel = activeUsbPortDisplayLabel != _connectedPortDisplayLabel; - if (_connector.state == MeshCoreConnectionState.disconnected) { + if (_usbConnector.state == MeshCoreConnectionState.disconnected) { _navigatedToContacts = false; setState(() { _isConnecting = false; @@ -56,8 +60,8 @@ class _UsbScreenState extends State { _connectedPortDisplayLabel = activeUsbPortDisplayLabel; }); } - if (_connector.state == MeshCoreConnectionState.connected && - _connector.isUsbTransportConnected && + if (_usbConnector.state == MeshCoreConnectionState.connected && + _usbConnector.isUsbTransportConnected && !_navigatedToContacts) { _navigatedToContacts = true; Navigator.of(context).pushReplacement( @@ -65,14 +69,14 @@ class _UsbScreenState extends State { ); } }; - _connector.addListener(_connectionListener); + _usbConnector.addListener(_connectionListener); _startHotPlugTimer(); } @override void didChangeDependencies() { super.didChangeDependencies(); - _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); if (!_didScheduleInitialLoad) { _didScheduleInitialLoad = true; unawaited(_loadPorts()); @@ -83,12 +87,12 @@ class _UsbScreenState extends State { void dispose() { _hotPlugTimer?.cancel(); _hotPlugTimer = null; - _connector.removeListener(_connectionListener); + _usbConnector.removeListener(_connectionListener); if (!_navigatedToContacts && - _connector.activeTransport == MeshCoreTransportType.usb && - _connector.state != MeshCoreConnectionState.disconnected) { + _usbConnector.activeTransport == MeshCoreTransportType.usb && + _usbConnector.state != MeshCoreConnectionState.disconnected) { WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(_connector.disconnect(manual: true)); + unawaited(_usbConnector.disconnect(manual: true)); }); } super.dispose(); @@ -113,6 +117,23 @@ class _UsbScreenState extends State { style: theme.textTheme.titleLarge, ), centerTitle: true, + actions: [ + if (PlatformInfo.isWeb || + PlatformInfo.isAndroid || + PlatformInfo.isIOS) + TextButton.icon( + onPressed: () { + debugPrint( + 'UsbScreen: Bluetooth selected, opening ScannerScreen', + ); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const ScannerScreen()), + ); + }, + icon: const Icon(Icons.bluetooth), + label: Text(l10n.connectionChoiceBluetoothLabel), + ), + ], ), body: SafeArea( child: LayoutBuilder( @@ -376,7 +397,8 @@ class _UsbScreenState extends State { final isSelected = port == _selectedPort; final displayName = _friendlyPortName(port); final rawName = normalizeUsbPortName(port); - final showRawName = rawName != displayName; + final showRawName = + rawName != displayName && !rawName.startsWith('web:'); return Material( color: isSelected ? theme.colorScheme.primaryContainer @@ -433,7 +455,7 @@ class _UsbScreenState extends State { Future _loadPorts() async { if (!mounted) return; - _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); setState(() { _isLoadingPorts = true; @@ -441,7 +463,7 @@ class _UsbScreenState extends State { }); try { - final ports = await _connector.listUsbPorts(); + final ports = await _usbConnector.listPorts(); if (!mounted) return; setState(() { _ports @@ -470,8 +492,8 @@ class _UsbScreenState extends State { if (selectedPort == null || selectedPort.isEmpty) { return; } - _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); - if (_connector.state != MeshCoreConnectionState.disconnected) { + _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); + if (_usbConnector.state != MeshCoreConnectionState.disconnected) { setState(() { _isConnecting = false; _errorText = null; @@ -486,7 +508,7 @@ class _UsbScreenState extends State { }); try { - await _connector.connectUsb(portName: rawPortName); + await _usbConnector.connect(portName: rawPortName); } catch (error, stackTrace) { debugPrint( 'UsbScreen: connect failed for $rawPortName: $error\n$stackTrace', diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 204f312..ac73673 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -280,6 +280,11 @@ class UsbSerialService { Future disconnect() async { if (_status == UsbSerialStatus.disconnected) return; + final portLabel = _connectedPortLabel ?? _connectedPortKey; + _debugLogService?.info( + 'USB disconnect starting port=${portLabel ?? 'unknown'}', + tag: 'USB Serial', + ); _status = UsbSerialStatus.disconnecting; _connectedPortKey = null; _connectedPortLabel = null; @@ -319,6 +324,10 @@ class UsbSerialService { _dataSubscription = null; } _status = UsbSerialStatus.disconnected; + _debugLogService?.info( + 'USB disconnect complete port=${portLabel ?? 'unknown'}', + tag: 'USB Serial', + ); } void setRequestPortLabel(String label) { diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 974928e..df5893b 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -16,6 +16,10 @@ class UsbSerialService { '2886:1667': 'Seeed Wio Tracker L1', }; static final Map _deviceNamesByPortKey = {}; + static final Map _baseLabelsByPortKey = {}; + static final Map _authorizedPortsByKey = + {}; + static int _nextAuthorizedPortId = 1; final StreamController _frameController = StreamController.broadcast(); @@ -51,11 +55,12 @@ class UsbSerialService { return const []; } + _resetPortCache(); final ports = await _getAuthorizedPorts(); if (ports.isEmpty) { - return [_requestPortLabel]; + return [_requestPortListEntry]; } - return ports.map(_displayLabelForPort).toList(growable: false); + return ports.map(_listEntryForPort).toList(growable: false); } Future connect({ @@ -75,8 +80,12 @@ class UsbSerialService { try { final requestedPortName = normalizeUsbPortName(portName); + final selectedPortKey = requestedPortName.startsWith('web:port:') + ? requestedPortName + : null; + _port = _authorizedPortsByKey[requestedPortName]; final authorizedPorts = await _getAuthorizedPorts(); - _port = _selectPort(authorizedPorts, requestedPortName); + _port ??= _selectPort(authorizedPorts, requestedPortName); _port ??= await _requestPort(); if (_port == null) { @@ -84,8 +93,11 @@ class UsbSerialService { } await _openPort(_port!, baudRate); - _connectedPortKey = _portKeyFor(_port!); - _connectedPortName = _buildDisplayLabel(_connectedPortKey!); + _connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey); + _connectedPortName = _displayLabelForPort( + _port!, + portKey: _connectedPortKey, + ); _writer = _getWriter(_port!); _reader = _getReader(_port!); _status = UsbSerialStatus.connected; @@ -122,6 +134,11 @@ class UsbSerialService { Future disconnect() async { if (_status == UsbSerialStatus.disconnected) return; + final portLabel = _connectedPortName ?? _connectedPortKey; + _debugLogService?.info( + 'USB disconnect starting port=${portLabel ?? 'unknown'}', + tag: 'USB Serial', + ); _status = UsbSerialStatus.disconnecting; final reader = _reader; final writer = _writer; @@ -156,6 +173,10 @@ class UsbSerialService { } _status = UsbSerialStatus.disconnected; + _debugLogService?.info( + 'USB disconnect complete port=${portLabel ?? 'unknown'}', + tag: 'USB Serial', + ); } void updateConnectedLabel(String label) { @@ -210,9 +231,12 @@ class UsbSerialService { if (ports.isEmpty) { return null; } - if (requestedPortName.isEmpty || requestedPortName == _requestPortLabel) { + if (requestedPortName.isEmpty || requestedPortName == _requestPortKey) { return ports.first; } + if (requestedPortName.startsWith('web:port:')) { + return null; + } for (final port in ports) { final description = _describePort(port); if (description == requestedPortName) { @@ -368,10 +392,29 @@ class UsbSerialService { } String _describePort(JSObject port) { + final info = _portInfo(port); + if (info == null) { + return _requestPortLabel; + } + + final vendorId = info.usbVendorId; + final productId = info.usbProductId; + final hasVendor = vendorId != null; + final hasProduct = productId != null; + + return describeWebUsbPort( + vendorId: hasVendor ? vendorId : null, + productId: hasProduct ? productId : null, + requestPortLabel: _requestPortLabel, + knownUsbNames: _knownUsbNames, + ); + } + + _WebPortInfo? _portInfo(JSObject port) { try { final info = port.callMethod('getInfo'.toJS); if (info == null) { - return _requestPortLabel; + return null; } final infoObject = info as JSObject; @@ -381,32 +424,52 @@ class UsbSerialService { final productId = infoObject .getProperty('usbProductId'.toJS) ?.dartify(); - final hasVendor = vendorId is num; - final hasProduct = productId is num; - - return describeWebUsbPort( - vendorId: hasVendor ? vendorId.toInt() : null, - productId: hasProduct ? productId.toInt() : null, - requestPortLabel: _requestPortLabel, - knownUsbNames: _knownUsbNames, + return _WebPortInfo( + usbVendorId: vendorId is num ? vendorId.toInt() : null, + usbProductId: productId is num ? productId.toInt() : null, ); } catch (_) { - return _requestPortLabel; + return null; } } - String _portKeyFor(JSObject port) => _describePort(port); + String _portKeyFor(JSObject port) { + return _cachePort(port); + } - String _displayLabelForPort(JSObject port) => - _buildDisplayLabel(_portKeyFor(port)); + String _cachePort(JSObject port, {String? preferredKey}) { + final portKey = preferredKey ?? 'web:port:${_nextAuthorizedPortId++}'; + _baseLabelsByPortKey[portKey] = _describePort(port); + _authorizedPortsByKey[portKey] = port; + return portKey; + } + + String _displayLabelForPort(JSObject port, {String? portKey}) => + _buildDisplayLabel(portKey ?? _portKeyFor(port)); String _buildDisplayLabel(String portKey) { return buildUsbDisplayLabel( - basePortLabel: portKey, + basePortLabel: _baseLabelsByPortKey[portKey] ?? portKey, deviceName: _deviceNamesByPortKey[portKey], ); } + String _listEntryForPort(JSObject port) { + final portKey = _portKeyFor(port); + return '$portKey - ${_displayLabelForPort(port, portKey: portKey)}'; + } + + String get _requestPortKey => 'web:request'; + + String get _requestPortListEntry => '$_requestPortKey - $_requestPortLabel'; + + void _resetPortCache() { + _authorizedPortsByKey.clear(); + _baseLabelsByPortKey.clear(); + _deviceNamesByPortKey.clear(); + _nextAuthorizedPortId = 1; + } + void _releaseLock(JSObject resource) { try { resource.callMethod('releaseLock'.toJS); @@ -462,3 +525,10 @@ class UsbSerialService { } enum UsbSerialStatus { disconnected, connecting, connected, disconnecting } + +final class _WebPortInfo { + const _WebPortInfo({required this.usbVendorId, required this.usbProductId}); + + final int? usbVendorId; + final int? usbProductId; +} diff --git a/lib/utils/dialog_utils.dart b/lib/utils/dialog_utils.dart index 510eca7..8a6ad9b 100644 --- a/lib/utils/dialog_utils.dart +++ b/lib/utils/dialog_utils.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import 'app_logger.dart'; /// Shows a confirmation dialog before disconnecting from the device. /// Returns true if user confirmed and disconnect completed, false otherwise. @@ -28,6 +29,7 @@ Future showDisconnectDialog( ); if (confirmed == true) { + appLogger.info('Disconnect confirmed from popup', tag: 'Connection'); await connector.disconnect(); return true; } diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 317dc92..115281c 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -4,7 +4,7 @@ import 'package:provider/provider.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/l10n/app_localizations.dart'; -import 'package:meshcore_open/screens/connection_choice_screen.dart'; +import 'package:meshcore_open/screens/scanner_screen.dart'; import 'package:meshcore_open/screens/usb_screen.dart'; import 'package:meshcore_open/utils/platform_info.dart'; @@ -131,7 +131,7 @@ void main() { }, ); - testWidgets('ConnectionChoiceScreen USB button reflects platform support', ( + testWidgets('ScannerScreen USB action reflects platform support', ( tester, ) async { final connector = _FakeMeshCoreConnector(); @@ -139,19 +139,15 @@ void main() { await tester.pumpWidget( _buildTestApp( connector: connector, - child: const ConnectionChoiceScreen(), + child: const ScannerScreen(), ), ); await tester.pumpAndSettle(); - final usbButton = tester.widget( - find.widgetWithText(ElevatedButton, 'USB'), - ); - if (PlatformInfo.supportsUsbSerial) { - expect(usbButton.onPressed, isNotNull); + expect(find.widgetWithText(FloatingActionButton, 'USB'), findsOneWidget); } else { - expect(usbButton.onPressed, isNull); + expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing); } }); } From 7cb84dbf6fc3c9e73f2a44641967f7de7e373086 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:52:23 -0500 Subject: [PATCH 257/421] Dart Format --- lib/screens/scanner_screen.dart | 4 +--- test/screens/usb_flow_test.dart | 5 +---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 54074a9..f7966e9 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -164,9 +164,7 @@ class _ScannerScreenState extends State { ? const SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - ), + child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.bluetooth_searching), label: Text( diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 115281c..0cf57c5 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -137,10 +137,7 @@ void main() { final connector = _FakeMeshCoreConnector(); await tester.pumpWidget( - _buildTestApp( - connector: connector, - child: const ScannerScreen(), - ), + _buildTestApp(connector: connector, child: const ScannerScreen()), ); await tester.pumpAndSettle(); From f9b6299620e1098ee67d5d12998b9c1050105e9d Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 3 Mar 2026 10:25:03 -0800 Subject: [PATCH 258/421] gitmodule cleanup --- .gitmodules | 3 --- pubspec.yaml | 9 +++++---- vendor/flserial | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) delete mode 160000 vendor/flserial diff --git a/.gitmodules b/.gitmodules index a47ab4d..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "vendor/flserial"] - path = vendor/flserial - url = git@github.com:MeshEnvy/flserial.git diff --git a/pubspec.yaml b/pubspec.yaml index d727919..8252178 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,10 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 flutter_blue_plus: ^2.1.0 - flserial: ^0.3.5 + flserial: + git: + url: https://github.com/MeshEnvy/flserial.git + ref: main provider: ^6.1.5+1 shared_preferences: ^2.2.2 uuid: ^4.3.3 @@ -131,9 +134,7 @@ flutter_launcher_icons: # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package -dependency_overrides: - flserial: - path: vendor/flserial + build_pipe: workflows: diff --git a/vendor/flserial b/vendor/flserial deleted file mode 160000 index 4821631..0000000 --- a/vendor/flserial +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 48216310061efc8d5d217cc18014fc2cb501646e From 5b4535d5dccdfc928b9541dd0b651b7e44f3fda1 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:45:58 -0500 Subject: [PATCH 259/421] update flserial dependency reference from main to master --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 8252178..3656e1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: flserial: git: url: https://github.com/MeshEnvy/flserial.git - ref: main + ref: master provider: ^6.1.5+1 shared_preferences: ^2.2.2 uuid: ^4.3.3 @@ -147,4 +147,4 @@ build_pipe: build_command: flutter build web --release --pwa-strategy=none # Strongly recommended: disables the default service worker which often causes more cache headaches add_version_query_param: true - # This is the key flag! It appends ?v= to bootstrap/JS files \ No newline at end of file + # This is the key flag! It appends ?v= to bootstrap/JS files From 38d40ca0a46036becf113ab089e6bb9ce48ccbf8 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:04:22 -0500 Subject: [PATCH 260/421] Enhance USB error handling and improve user feedback - Updated the _friendlyErrorMessage method in UsbScreen to provide more user-friendly error messages based on specific PlatformException codes. - Added localized error messages for various USB-related errors, improving clarity for users. - Modified the UsbSerialService to rethrow exceptions instead of throwing StateError, allowing for better error propagation. - Updated the usb_flow_test to reflect changes in the USB display label behavior, ensuring the test accurately describes the functionality. --- lib/l10n/app_bg.arb | 1710 ++++++++-------- lib/l10n/app_de.arb | 568 +++--- lib/l10n/app_en.arb | 14 +- lib/l10n/app_es.arb | 702 +++---- lib/l10n/app_fr.arb | 1034 +++++----- lib/l10n/app_it.arb | 212 +- lib/l10n/app_localizations.dart | 72 + lib/l10n/app_localizations_bg.dart | 1934 +++++++++--------- lib/l10n/app_localizations_de.dart | 598 +++--- lib/l10n/app_localizations_en.dart | 41 + lib/l10n/app_localizations_es.dart | 731 +++---- lib/l10n/app_localizations_fr.dart | 1067 +++++----- lib/l10n/app_localizations_it.dart | 235 ++- lib/l10n/app_localizations_nl.dart | 111 +- lib/l10n/app_localizations_pl.dart | 1264 ++++++------ lib/l10n/app_localizations_pt.dart | 827 ++++---- lib/l10n/app_localizations_ru.dart | 1991 +++++++++---------- lib/l10n/app_localizations_sk.dart | 1529 +++++++------- lib/l10n/app_localizations_sl.dart | 834 ++++---- lib/l10n/app_localizations_sv.dart | 948 ++++----- lib/l10n/app_localizations_uk.dart | 1980 +++++++++--------- lib/l10n/app_localizations_zh.dart | 1879 +++++++++-------- lib/l10n/app_nl.arb | 90 +- lib/l10n/app_pl.arb | 1202 +++++------ lib/l10n/app_pt.arb | 790 ++++---- lib/l10n/app_ru.arb | 1750 ++++++++-------- lib/l10n/app_sk.arb | 1468 +++++++------- lib/l10n/app_sl.arb | 786 ++++---- lib/l10n/app_sv.arb | 914 ++++----- lib/l10n/app_uk.arb | 1736 ++++++++-------- lib/l10n/app_zh.arb | 1744 ++++++++-------- lib/screens/usb_screen.dart | 49 +- lib/services/usb_serial_service_native.dart | 2 +- test/screens/usb_flow_test.dart | 4 +- 34 files changed, 15499 insertions(+), 15317 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 1ac7acc..f71dfdd 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", +{ + "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,33 +9,33 @@ }, "@@locale": "bg", "appTitle": "MeshCore Open", - "nav_contacts": "Контакти", - "nav_channels": "Канали", - "nav_map": "Карта", - "common_cancel": "Отказ", - "common_connect": "Свържи се", - "common_unknownDevice": "Неизвестно устройство", - "common_save": "Запази", - "common_delete": "Изтрий", - "common_close": "Затвори", - "common_edit": "Редактирай", - "common_add": "Добави", - "common_settings": "Настройки", - "common_disconnect": "Прекъсни", - "common_connected": "Свързано", - "common_disconnected": "Откъснато", - "common_create": "Създай", - "common_continue": "Продължи", - "common_share": "Сподели", - "common_copy": "Копирай", - "common_retry": "Опитай отново", - "common_hide": "Скриване", - "common_remove": "Изтрий", - "common_enable": "Активирай", - "common_disable": "Деактивирай", - "common_reboot": "Рестартирай", - "common_loading": "Зареждане...", - "common_notAvailable": "—", + "nav_contacts": "Контакти", + "nav_channels": "Канали", + "nav_map": "Карта", + "common_cancel": "Отказ", + "common_connect": "Свържи се", + "common_unknownDevice": "Неизвестно устройство", + "common_save": "Запази", + "common_delete": "Изтрий", + "common_close": "Затвори", + "common_edit": "Редактирай", + "common_add": "Добави", + "common_settings": "Настройки", + "common_disconnect": "Прекъсни", + "common_connected": "Свързано", + "common_disconnected": "Откъснато", + "common_create": "Създай", + "common_continue": "Продължи", + "common_share": "Сподели", + "common_copy": "Копирай", + "common_retry": "Опитай отново", + "common_hide": "Скриване", + "common_remove": "Изтрий", + "common_enable": "Активирай", + "common_disable": "Деактивирай", + "common_reboot": "Рестартирай", + "common_loading": "Зареждане...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Сканиране за устройства...", - "scanner_connecting": "Свързвам се...", - "scanner_disconnecting": "Изключване...", - "scanner_notConnected": "Не е свързан", - "scanner_connectedTo": "Свързано с {deviceName}", + "scanner_scanning": "Сканиране за устройства...", + "scanner_connecting": "Свързвам се...", + "scanner_disconnecting": "Изключване...", + "scanner_notConnected": "Не е свързан", + "scanner_connectedTo": "Свързано с {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,9 +65,9 @@ } } }, - "scanner_searchingDevices": "Търсене на устройства MeshCore...", - "scanner_tapToScan": "Натиснете Сканиране, за да намерите устройства MeshCore.", - "scanner_connectionFailed": "Връзката не успя: {error}", + "scanner_searchingDevices": "Търсене на устройства MeshCore...", + "scanner_tapToScan": "Натиснете Сканиране, за да намерите устройства MeshCore.", + "scanner_connectionFailed": "Връзката не успя: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,52 +75,52 @@ } } }, - "scanner_stop": "Спрете", - "scanner_scan": "Сканирай", - "device_quickSwitch": "Бързо превключване", + "scanner_stop": "Спрете", + "scanner_scan": "Сканирай", + "device_quickSwitch": "Бързо превключване", "device_meshcore": "MeshCore", - "settings_title": "Настройки", - "settings_deviceInfo": "Информация за устройството", - "settings_appSettings": "Настройки на приложението", - "settings_appSettingsSubtitle": "Уведомления, съобщения и предпочитания за карта", - "settings_nodeSettings": "Настройки на възела", - "settings_nodeName": "Име на възела", - "settings_nodeNameNotSet": "Не е зададено", - "settings_nodeNameHint": "Въведете име на възел", - "settings_nodeNameUpdated": "Името е актуализирано", - "settings_radioSettings": "Настройки на радиопредавателя", - "settings_radioSettingsSubtitle": "Честота, мощност, разпространяващ фактор", - "settings_radioSettingsUpdated": "Радио настройките са актуализирани", - "settings_location": "Местоположение", - "settings_locationSubtitle": "Координати на GPS", - "settings_locationUpdated": "Местоположението е актуализирано", - "settings_locationBothRequired": "Въведете както географска ширина, така и географска дължина.", - "settings_locationInvalid": "Невалидна ширина или дължина.", - "settings_latitude": "Широчина", - "settings_longitude": "Дължина", - "settings_privacyMode": "Режим на поверителност", - "settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", - "settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.", - "settings_privacyModeEnabled": "Режим на поверителност е активиран", - "settings_privacyModeDisabled": "Режим на поверителност е деактивиран", - "settings_actions": "Действия", - "settings_sendAdvertisement": "Изпрати Реклама", - "settings_sendAdvertisementSubtitle": "Сега присъствие в ефир", - "settings_advertisementSent": "Реклама изпратена", - "settings_syncTime": "Време за синхронизация", - "settings_syncTimeSubtitle": "Задайте часовника на устройството да отговаря на времето на телефона.", - "settings_timeSynchronized": "Синхронизирано във времето", - "settings_refreshContacts": "Презареди контакти", - "settings_refreshContactsSubtitle": "Презареди списъка с контакти от устройството", - "settings_rebootDevice": "Рестартирайте устройството", - "settings_rebootDeviceSubtitle": "Рестартирайте устройството MeshCore", - "settings_rebootDeviceConfirm": "Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.", - "settings_debug": "Отстрани", - "settings_bleDebugLog": "Лог за отстраняване на грешки на BLE", - "settings_bleDebugLogSubtitle": "Команди, отговори и сурови данни BLE", - "settings_appDebugLog": "Лог на отстраняване на грешки на приложението", - "settings_appDebugLogSubtitle": "Съобщения за отстраняване на грешки на приложението", - "settings_about": "За нас", + "settings_title": "Настройки", + "settings_deviceInfo": "Информация за устройството", + "settings_appSettings": "Настройки на приложението", + "settings_appSettingsSubtitle": "Уведомления, съобщения и предпочитания за карта", + "settings_nodeSettings": "Настройки на възела", + "settings_nodeName": "Име на възела", + "settings_nodeNameNotSet": "Не е зададено", + "settings_nodeNameHint": "Въведете име на възел", + "settings_nodeNameUpdated": "Името е актуализирано", + "settings_radioSettings": "Настройки на радиопредавателя", + "settings_radioSettingsSubtitle": "Честота, мощност, разпространяващ фактор", + "settings_radioSettingsUpdated": "Радио настройките са актуализирани", + "settings_location": "Местоположение", + "settings_locationSubtitle": "Координати на GPS", + "settings_locationUpdated": "Местоположението е актуализирано", + "settings_locationBothRequired": "Въведете както географска ширина, така и географска дължина.", + "settings_locationInvalid": "Невалидна ширина или дължина.", + "settings_latitude": "Широчина", + "settings_longitude": "Дължина", + "settings_privacyMode": "Режим на поверителност", + "settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", + "settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.", + "settings_privacyModeEnabled": "Режим на поверителност е активиран", + "settings_privacyModeDisabled": "Режим на поверителност е деактивиран", + "settings_actions": "Действия", + "settings_sendAdvertisement": "Изпрати Реклама", + "settings_sendAdvertisementSubtitle": "Сега присъствие в ефир", + "settings_advertisementSent": "Реклама изпратена", + "settings_syncTime": "Време за синхронизация", + "settings_syncTimeSubtitle": "Задайте часовника на устройството да отговаря на времето на телефона.", + "settings_timeSynchronized": "Синхронизирано във времето", + "settings_refreshContacts": "Презареди контакти", + "settings_refreshContactsSubtitle": "Презареди списъка с контакти от устройството", + "settings_rebootDevice": "Рестартирайте устройството", + "settings_rebootDeviceSubtitle": "Рестартирайте устройството MeshCore", + "settings_rebootDeviceConfirm": "Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.", + "settings_debug": "Отстрани", + "settings_bleDebugLog": "Лог за отстраняване на грешки на BLE", + "settings_bleDebugLogSubtitle": "Команди, отговори и сурови данни BLE", + "settings_appDebugLog": "Лог на отстраняване на грешки на приложението", + "settings_appDebugLogSubtitle": "Съобщения за отстраняване на грешки на приложението", + "settings_about": "За нас", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -129,26 +129,26 @@ } } }, - "settings_aboutLegalese": "Проект MeshCore с отворен код 2024 г.", - "settings_aboutDescription": "Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.", - "settings_infoName": "Име", - "settings_infoId": "ИД", - "settings_infoStatus": "Статус", - "settings_infoBattery": "Батерия", - "settings_infoPublicKey": "Общ публичен ключ", - "settings_infoContactsCount": "Брой контакти", - "settings_infoChannelCount": "Брой канали", - "settings_presets": "Предварителни настройки", - "settings_frequency": "Честота (MHz)", + "settings_aboutLegalese": "Проект MeshCore с отворен код 2024 г.", + "settings_aboutDescription": "Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.", + "settings_infoName": "Име", + "settings_infoId": "ИД", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батерия", + "settings_infoPublicKey": "Общ публичен ключ", + "settings_infoContactsCount": "Брой контакти", + "settings_infoChannelCount": "Брой канали", + "settings_presets": "Предварителни настройки", + "settings_frequency": "Честота (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)", - "settings_bandwidth": "Ширина на честотния спектър", - "settings_spreadingFactor": "Фактор на разпространение", - "settings_codingRate": "Такса за кодиране", - "settings_txPower": "TX Мощност (dBm)", + "settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)", + "settings_bandwidth": "Ширина на честотния спектър", + "settings_spreadingFactor": "Фактор на разпространение", + "settings_codingRate": "Такса за кодиране", + "settings_txPower": "TX Мощност (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)", - "settings_error": "Грешка: {message}", + "settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)", + "settings_error": "Грешка: {message}", "@settings_error": { "placeholders": { "message": { @@ -156,51 +156,51 @@ } } }, - "appSettings_title": "Настройки на приложението", - "appSettings_appearance": "Външен вид", - "appSettings_theme": "Тема", - "appSettings_themeSystem": "Система по подразбиране", - "appSettings_themeLight": "Ярка", - "appSettings_themeDark": "Тъмно", - "appSettings_language": "Език", - "appSettings_languageSystem": "Система по подразбиране", + "appSettings_title": "Настройки на приложението", + "appSettings_appearance": "Външен вид", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Система по подразбиране", + "appSettings_themeLight": "Ярка", + "appSettings_themeDark": "Тъмно", + "appSettings_language": "Език", + "appSettings_languageSystem": "Система по подразбиране", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_notifications": "Уведомления", - "appSettings_enableNotifications": "Активирай Известия", - "appSettings_enableNotificationsSubtitle": "Получете известия за съобщения и реклами", - "appSettings_notificationPermissionDenied": "Отказвано е разрешение за известия", - "appSettings_notificationsEnabled": "Уведомителни са активирани", - "appSettings_notificationsDisabled": "Известия са изключени", - "appSettings_messageNotifications": "Уведомления", - "appSettings_messageNotificationsSubtitle": "Покажи известие при получаване на нови съобщения", - "appSettings_channelMessageNotifications": "Уведомления за съобщения от канал", - "appSettings_channelMessageNotificationsSubtitle": "Покажи известие при получаване на съобщения от канали", - "appSettings_advertisementNotifications": "Уведомления за реклами", - "appSettings_advertisementNotificationsSubtitle": "Покажи известие, когато бъдат открити нови възли.", - "appSettings_messaging": "Съобщения", - "appSettings_clearPathOnMaxRetry": "Изчисти Път на Макс Опит", - "appSettings_clearPathOnMaxRetrySubtitle": "Възстанови контактния път след 5 неуспешни опита за изпращане", - "appSettings_pathsWillBeCleared": "Пътищата ще бъдат почистени след 5 неуспешни опита.", - "appSettings_pathsWillNotBeCleared": "Пътищата няма да бъдат автоматично изчистени.", - "appSettings_autoRouteRotation": "Автоматично маршрутизиране на завъртания", - "appSettings_autoRouteRotationSubtitle": "Превключете между най-добрите пътища и режим на наводняване", - "appSettings_autoRouteRotationEnabled": "Автоматично маршрутизиране вкл.", - "appSettings_autoRouteRotationDisabled": "Автоматично маршрутизирането е деактивирано", - "appSettings_battery": "Батерия", - "appSettings_batteryChemistry": "Химия на батерията", - "appSettings_batteryChemistryPerDevice": "Зададено за устройство ({deviceName})", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_notifications": "Уведомления", + "appSettings_enableNotifications": "Активирай Известия", + "appSettings_enableNotificationsSubtitle": "Получете известия за съобщения и реклами", + "appSettings_notificationPermissionDenied": "Отказвано е разрешение за известия", + "appSettings_notificationsEnabled": "Уведомителни са активирани", + "appSettings_notificationsDisabled": "Известия са изключени", + "appSettings_messageNotifications": "Уведомления", + "appSettings_messageNotificationsSubtitle": "Покажи известие при получаване на нови съобщения", + "appSettings_channelMessageNotifications": "Уведомления за съобщения от канал", + "appSettings_channelMessageNotificationsSubtitle": "Покажи известие при получаване на съобщения от канали", + "appSettings_advertisementNotifications": "Уведомления за реклами", + "appSettings_advertisementNotificationsSubtitle": "Покажи известие, когато бъдат открити нови възли.", + "appSettings_messaging": "Съобщения", + "appSettings_clearPathOnMaxRetry": "Изчисти Път на Макс Опит", + "appSettings_clearPathOnMaxRetrySubtitle": "Възстанови контактния път след 5 неуспешни опита за изпращане", + "appSettings_pathsWillBeCleared": "Пътищата ще бъдат почистени след 5 неуспешни опита.", + "appSettings_pathsWillNotBeCleared": "Пътищата няма да бъдат автоматично изчистени.", + "appSettings_autoRouteRotation": "Автоматично маршрутизиране на завъртания", + "appSettings_autoRouteRotationSubtitle": "Превключете между най-добрите пътища и режим на наводняване", + "appSettings_autoRouteRotationEnabled": "Автоматично маршрутизиране вкл.", + "appSettings_autoRouteRotationDisabled": "Автоматично маршрутизирането е деактивирано", + "appSettings_battery": "Батерия", + "appSettings_batteryChemistry": "Химия на батерията", + "appSettings_batteryChemistryPerDevice": "Зададено за устройство ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Свържете се с устройство, за да изберете.", + "appSettings_batteryChemistryConnectFirst": "Свържете се с устройство, за да изберете.", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", - "appSettings_batteryLifepo4": "Литиево желязо фосфат (2.6-3.65V)", - "appSettings_batteryLipo": "Литиев полимер (3.0-4.2V)", - "appSettings_mapDisplay": "Карта за показване", - "appSettings_showRepeaters": "Показване на повторители", - "appSettings_showRepeatersSubtitle": "Показване на възпроизвеждащи се възли на картата", - "appSettings_showChatNodes": "Покажи Възли на Чат", - "appSettings_showChatNodesSubtitle": "Показване на чат възли на картата", - "appSettings_showOtherNodes": "Покажи други възли", - "appSettings_showOtherNodesSubtitle": "Покажи други типове възли на картата", - "appSettings_timeFilter": "Филтриране по време", - "appSettings_timeFilterShowAll": "Покажи всички възли", - "appSettings_timeFilterShowLast": "Покажи възли от последните {hours} часа", + "appSettings_batteryLifepo4": "Литиево желязо фосфат (2.6-3.65V)", + "appSettings_batteryLipo": "Литиев полимер (3.0-4.2V)", + "appSettings_mapDisplay": "Карта за показване", + "appSettings_showRepeaters": "Показване на повторители", + "appSettings_showRepeatersSubtitle": "Показване на възпроизвеждащи се възли на картата", + "appSettings_showChatNodes": "Покажи Възли на Чат", + "appSettings_showChatNodesSubtitle": "Показване на чат възли на картата", + "appSettings_showOtherNodes": "Покажи други възли", + "appSettings_showOtherNodesSubtitle": "Покажи други типове възли на картата", + "appSettings_timeFilter": "Филтриране по време", + "appSettings_timeFilterShowAll": "Покажи всички възли", + "appSettings_timeFilterShowLast": "Покажи възли от последните {hours} часа", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -229,16 +229,16 @@ } } }, - "appSettings_mapTimeFilter": "Филтри за време на картата", - "appSettings_showNodesDiscoveredWithin": "Покажи възлите, открити в:", - "appSettings_allTime": "Всичко време", - "appSettings_lastHour": "Последната минута", - "appSettings_last6Hours": "Последни 6 часа", - "appSettings_last24Hours": "Последно 24 часа", - "appSettings_lastWeek": "Миналата седмица", - "appSettings_offlineMapCache": "Кеш на офлайн карти", - "appSettings_noAreaSelected": "Няма избрана област", - "appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})", + "appSettings_mapTimeFilter": "Филтри за време на картата", + "appSettings_showNodesDiscoveredWithin": "Покажи възлите, открити в:", + "appSettings_allTime": "Всичко време", + "appSettings_lastHour": "Последната минута", + "appSettings_last6Hours": "Последни 6 часа", + "appSettings_last24Hours": "Последно 24 часа", + "appSettings_lastWeek": "Миналата седмица", + "appSettings_offlineMapCache": "Кеш на офлайн карти", + "appSettings_noAreaSelected": "Няма избрана област", + "appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,19 +249,19 @@ } } }, - "appSettings_debugCard": "Отстрани", - "appSettings_appDebugLogging": "Логване за отстраняване на грешки на приложението", - "appSettings_appDebugLoggingSubtitle": "Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.", - "appSettings_appDebugLoggingEnabled": "Режимът за отстраняване на грешки в приложението е активиран.", - "appSettings_appDebugLoggingDisabled": "Логването за отстраняване на грешки в приложението е изключено.", - "contacts_title": "Контакти", - "contacts_noContacts": "Няма контакти към момента.", - "contacts_contactsWillAppear": "Контактите ще се появят, когато устройствата рекламират.", - "contacts_searchContacts": "Търсене на контакти...", - "contacts_noUnreadContacts": "Няма непрочетени контакти", - "contacts_noContactsFound": "Няма намерени контакти или групи.", - "contacts_deleteContact": "Изтрий Контакт", - "contacts_removeConfirm": "Изтрий {contactName} от контактите?", + "appSettings_debugCard": "Отстрани", + "appSettings_appDebugLogging": "Логване за отстраняване на грешки на приложението", + "appSettings_appDebugLoggingSubtitle": "Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.", + "appSettings_appDebugLoggingEnabled": "Режимът за отстраняване на грешки в приложението е активиран.", + "appSettings_appDebugLoggingDisabled": "Логването за отстраняване на грешки в приложението е изключено.", + "contacts_title": "Контакти", + "contacts_noContacts": "Няма контакти към момента.", + "contacts_contactsWillAppear": "Контактите ще се появят, когато устройствата рекламират.", + "contacts_searchContacts": "Търсене на контакти...", + "contacts_noUnreadContacts": "Няма непрочетени контакти", + "contacts_noContactsFound": "Няма намерени контакти или групи.", + "contacts_deleteContact": "Изтрий Контакт", + "contacts_removeConfirm": "Изтрий {contactName} от контактите?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -269,12 +269,12 @@ } } }, - "contacts_manageRepeater": "Управление на Повтарящ се Елемент", - "contacts_roomLogin": "Вход в стаята", - "contacts_openChat": "Отвори чат", - "contacts_editGroup": "Редактирай Група", - "contacts_deleteGroup": "Изтрий група", - "contacts_deleteGroupConfirm": "Премахнете \"{groupName}\"?", + "contacts_manageRepeater": "Управление на Повтарящ се Елемент", + "contacts_roomLogin": "Вход в стаята", + "contacts_openChat": "Отвори чат", + "contacts_editGroup": "Редактирай Група", + "contacts_deleteGroup": "Изтрий група", + "contacts_deleteGroupConfirm": "Премахнете \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -282,10 +282,10 @@ } } }, - "contacts_newGroup": "Нова група", - "contacts_groupName": "Група", - "contacts_groupNameRequired": "Името на групата е задължително.", - "contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.", + "contacts_newGroup": "Нова група", + "contacts_groupName": "Група", + "contacts_groupNameRequired": "Името на групата е задължително.", + "contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -293,11 +293,11 @@ } } }, - "contacts_filterContacts": "Филтрирайте контактите...", - "contacts_noContactsMatchFilter": "Няма съвпадения с вашия филтър.", - "contacts_noMembers": "Няма членове", - "contacts_lastSeenNow": "Последно видяно сега", - "contacts_lastSeenMinsAgo": "Последна активност {minutes} минути преди", + "contacts_filterContacts": "Филтрирайте контактите...", + "contacts_noContactsMatchFilter": "Няма съвпадения с вашия филтър.", + "contacts_noMembers": "Няма членове", + "contacts_lastSeenNow": "Последно видяно сега", + "contacts_lastSeenMinsAgo": "Последна активност {minutes} минути преди", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Последно видяно преди час", - "contacts_lastSeenHoursAgo": "Последно видян {hours} часа преди.", + "contacts_lastSeenHourAgo": "Последно видяно преди час", + "contacts_lastSeenHoursAgo": "Последно видян {hours} часа преди.", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Последно видяно преди 1 ден", - "contacts_lastSeenDaysAgo": "Последно видян {days} дни преди.", + "contacts_lastSeenDayAgo": "Последно видяно преди 1 ден", + "contacts_lastSeenDaysAgo": "Последно видян {days} дни преди.", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -323,12 +323,12 @@ } } }, - "channels_title": "Канали", - "channels_noChannelsConfigured": "Няма конфигурирани канали", - "channels_addPublicChannel": "Добави публичен канал", - "channels_searchChannels": "Търсене на канали...", - "channels_noChannelsFound": "Няма намерени канали", - "channels_channelIndex": "Канал {index}", + "channels_title": "Канали", + "channels_noChannelsConfigured": "Няма конфигурирани канали", + "channels_addPublicChannel": "Добави публичен канал", + "channels_searchChannels": "Търсене на канали...", + "channels_noChannelsFound": "Няма намерени канали", + "channels_channelIndex": "Канал {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -336,16 +336,16 @@ } } }, - "channels_hashtagChannel": "Канал с хаштаг", - "channels_public": "Публично", - "channels_private": "Личен", - "channels_publicChannel": "Публичен канал", - "channels_privateChannel": "Частен канал", - "channels_editChannel": "Редактирай канал", - "channels_muteChannel": "Заглуши канала", - "channels_unmuteChannel": "Включи известията на канала", - "channels_deleteChannel": "Изтрий канала", - "channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.", + "channels_hashtagChannel": "Канал с хаштаг", + "channels_public": "Публично", + "channels_private": "Личен", + "channels_publicChannel": "Публичен канал", + "channels_privateChannel": "Частен канал", + "channels_editChannel": "Редактирай канал", + "channels_muteChannel": "Заглуши канала", + "channels_unmuteChannel": "Включи известията на канала", + "channels_deleteChannel": "Изтрий канала", + "channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Каналът \"{name}\" е изтрит", + "channels_channelDeleted": "Каналът \"{name}\" е изтрит", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Добави Канал", - "channels_channelIndexLabel": "Индекс на канал", - "channels_channelName": "Име на канала", - "channels_usePublicChannel": "Използвайте публичен канал", - "channels_standardPublicPsk": "Стандартен публичен PSK", + "channels_addChannel": "Добави Канал", + "channels_channelIndexLabel": "Индекс на канал", + "channels_channelName": "Име на канала", + "channels_usePublicChannel": "Използвайте публичен канал", + "channels_standardPublicPsk": "Стандартен публичен PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Генерирай случайна PSK", - "channels_enterChannelName": "Моля, въведете име на канал.", - "channels_pskMustBe32Hex": "PSK трябва да бъде 32 шестнаредни знака.", - "channels_channelAdded": "Каналът \"{name}\" е добавен", + "channels_generateRandomPsk": "Генерирай случайна PSK", + "channels_enterChannelName": "Моля, въведете име на канал.", + "channels_pskMustBe32Hex": "PSK трябва да бъде 32 шестнаредни знака.", + "channels_channelAdded": "Каналът \"{name}\" е добавен", "@channels_channelAdded": { "placeholders": { "name": { @@ -378,7 +378,7 @@ } } }, - "channels_editChannelTitle": "Редактирай Канал {index}", + "channels_editChannelTitle": "Редактирай Канал {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -386,8 +386,8 @@ } } }, - "channels_smazCompression": "Компресия SMAZ", - "channels_channelUpdated": "Каналът \"{name}\" е актуализиран", + "channels_smazCompression": "Компресия SMAZ", + "channels_channelUpdated": "Каналът \"{name}\" е актуализиран", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,16 +395,16 @@ } } }, - "channels_publicChannelAdded": "Публичен канал добавен", - "channels_sortBy": "Сортирай по", - "channels_sortManual": "Ръчно", + "channels_publicChannelAdded": "Публичен канал добавен", + "channels_sortBy": "Сортирай по", + "channels_sortManual": "Ръчно", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Последни съобщения", - "channels_sortUnread": "Непрочетено", - "chat_noMessages": "Няма съобщения.", - "chat_sendMessageToStart": "Изпрати съобщение, за да започнеш.", - "chat_originalMessageNotFound": "Съобщението не е намерено", - "chat_replyingTo": "Отговарям на {name}", + "channels_sortLatestMessages": "Последни съобщения", + "channels_sortUnread": "Непрочетено", + "chat_noMessages": "Няма съобщения.", + "chat_sendMessageToStart": "Изпрати съобщение, за да започнеш.", + "chat_originalMessageNotFound": "Съобщението не е намерено", + "chat_replyingTo": "Отговарям на {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "Отговори на {name}", + "chat_replyTo": "Отговори на {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -420,8 +420,8 @@ } } }, - "chat_location": "Местоположение", - "chat_sendMessageTo": "Изпрати съобщение на {contactName}", + "chat_location": "Местоположение", + "chat_sendMessageTo": "Изпрати съобщение на {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "Въведете съобщение...", - "chat_messageTooLong": "Съобщението е твърде дълго (макс {maxBytes} байта).", + "chat_typeMessage": "Въведете съобщение...", + "chat_messageTooLong": "Съобщението е твърде дълго (макс {maxBytes} байта).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,10 +438,10 @@ } } }, - "chat_messageCopied": "Съобщението е копирано", - "chat_messageDeleted": "Съобщението е изтрито", - "chat_retryingMessage": "Опитваме се отново.", - "chat_retryCount": "Опитай отново {current}/{max}", + "chat_messageCopied": "Съобщението е копирано", + "chat_messageDeleted": "Съобщението е изтрито", + "chat_retryingMessage": "Опитваме се отново.", + "chat_retryCount": "Опитай отново {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -452,33 +452,33 @@ } } }, - "chat_sendGif": "Изпрати GIF", - "chat_reply": "Отговори", - "chat_addReaction": "Добави Реакция", - "chat_me": "Аз", - "emojiCategorySmileys": "Емотикони", - "emojiCategoryGestures": "Жестове", - "emojiCategoryHearts": "Сърца", - "emojiCategoryObjects": "Обекти", - "gifPicker_title": "Изберете GIF", - "gifPicker_searchHint": "Търсене на GIF-ове...", - "gifPicker_poweredBy": "Задвижвано от GIPHY", - "gifPicker_noGifsFound": "Няма намерени GIF файлове.", - "gifPicker_failedLoad": "Не можа да се заредят GIF файловете", - "gifPicker_failedSearch": "Неуспешно търсене на GIF-ове", - "gifPicker_noInternet": "Няма интернет връзка", - "debugLog_appTitle": "Лог на отстраняване на грешки на приложението", - "debugLog_bleTitle": "Лог за отстраняване на грешки на BLE", - "debugLog_copyLog": "Копирай лог", - "debugLog_clearLog": "Изчисти логовете", - "debugLog_copied": "Копирано лого за отстраняване на грешки", - "debugLog_bleCopied": "Копиран лог от BLE", - "debugLog_noEntries": "Все още няма дебъг логове.", - "debugLog_enableInSettings": "Активирайте отстраняване на грешки в настройките на приложението", - "debugLog_frames": "Рамки", + "chat_sendGif": "Изпрати GIF", + "chat_reply": "Отговори", + "chat_addReaction": "Добави Реакция", + "chat_me": "Аз", + "emojiCategorySmileys": "Емотикони", + "emojiCategoryGestures": "Жестове", + "emojiCategoryHearts": "Сърца", + "emojiCategoryObjects": "Обекти", + "gifPicker_title": "Изберете GIF", + "gifPicker_searchHint": "Търсене на GIF-ове...", + "gifPicker_poweredBy": "Задвижвано от GIPHY", + "gifPicker_noGifsFound": "Няма намерени GIF файлове.", + "gifPicker_failedLoad": "Не можа да се заредят GIF файловете", + "gifPicker_failedSearch": "Неуспешно търсене на GIF-ове", + "gifPicker_noInternet": "Няма интернет връзка", + "debugLog_appTitle": "Лог на отстраняване на грешки на приложението", + "debugLog_bleTitle": "Лог за отстраняване на грешки на BLE", + "debugLog_copyLog": "Копирай лог", + "debugLog_clearLog": "Изчисти логовете", + "debugLog_copied": "Копирано лого за отстраняване на грешки", + "debugLog_bleCopied": "Копиран лог от BLE", + "debugLog_noEntries": "Все още няма дебъг логове.", + "debugLog_enableInSettings": "Активирайте отстраняване на грешки в настройките на приложението", + "debugLog_frames": "Рамки", "debugLog_rawLogRx": "Raw Log-RX", - "debugLog_noBleActivity": "Няма BLE активност към момента.", - "debugFrame_length": "Дължина на кадъра: {count} байта", + "debugLog_noBleActivity": "Няма BLE активност към момента.", + "debugFrame_length": "Дължина на кадъра: {count} байта", "@debugFrame_length": { "placeholders": { "count": { @@ -486,7 +486,7 @@ } } }, - "debugFrame_command": "Команда: 0x{value}", + "debugFrame_command": "Команда: 0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -494,8 +494,8 @@ } } }, - "debugFrame_textMessageHeader": "Съобщение:", - "debugFrame_destinationPubKey": "- Дестинация Публичен Ключ: {pubKey}", + "debugFrame_textMessageHeader": "Съобщение:", + "debugFrame_destinationPubKey": "- Дестинация Публичен Ключ: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- Време: {timestamp}", + "debugFrame_timestamp": "- Време: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -511,7 +511,7 @@ } } }, - "debugFrame_flags": "- Флагове: 0x{value}", + "debugFrame_flags": "- Флагове: 0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -519,7 +519,7 @@ } } }, - "debugFrame_textType": "- Тип текст: {type} ({label})", + "debugFrame_textType": "- Тип текст: {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -531,8 +531,8 @@ } }, "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Просто", - "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_textTypePlain": "Просто", + "debugFrame_text": "- Текст: \"{text}\"", "@debugFrame_text": { "placeholders": { "text": { @@ -540,15 +540,15 @@ } } }, - "debugFrame_hexDump": "Хексадесетичен Dump:", - "chat_pathManagement": "Управление на пътища", - "chat_routingMode": "Режим на маршрутизиране", - "chat_autoUseSavedPath": "Автоматично (използвай запазения път)", - "chat_forceFloodMode": "Принуди режим на наводняване", - "chat_recentAckPaths": "Неотдавни ACK пътища (докоснете, за да използвате):", - "chat_pathHistoryFull": "Историята на пътя е пълна. Премахнете записи, за да добавите нови.", - "chat_hopSingular": "скочи", - "chat_hopPlural": "скоци", + "debugFrame_hexDump": "Хексадесетичен Dump:", + "chat_pathManagement": "Управление на пътища", + "chat_routingMode": "Режим на маршрутизиране", + "chat_autoUseSavedPath": "Автоматично (използвай запазения път)", + "chat_forceFloodMode": "Принуди режим на наводняване", + "chat_recentAckPaths": "Неотдавни ACK пътища (докоснете, за да използвате):", + "chat_pathHistoryFull": "Историята на пътя е пълна. Премахнете записи, за да добавите нови.", + "chat_hopSingular": "скочи", + "chat_hopPlural": "скоци", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { @@ -557,20 +557,20 @@ } } }, - "chat_successes": "Успехи", - "chat_removePath": "Премахни пътя", - "chat_noPathHistoryYet": "Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.", - "chat_pathActions": "Действия по пътя:", - "chat_setCustomPath": "Задайте персонализиран път", - "chat_setCustomPathSubtitle": "Ръчно укажете маршрутен път", - "chat_clearPath": "Почисти Път", - "chat_clearPathSubtitle": "Принуди преоткриване при следващо изпращане", - "chat_pathCleared": "Пътят е почистен. Следващото съобщение ще открие маршрута отново.", - "chat_floodModeSubtitle": "Използвайте превключвателя за маршрутизиране в лентата на приложението.", - "chat_floodModeEnabled": "Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.", - "chat_fullPath": "Пълен път", - "chat_pathDetailsNotAvailable": "Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.", - "chat_pathSetHops": "Пътят е зададен: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_successes": "Успехи", + "chat_removePath": "Премахни пътя", + "chat_noPathHistoryYet": "Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.", + "chat_pathActions": "Действия по пътя:", + "chat_setCustomPath": "Задайте персонализиран път", + "chat_setCustomPathSubtitle": "Ръчно укажете маршрутен път", + "chat_clearPath": "Почисти Път", + "chat_clearPathSubtitle": "Принуди преоткриване при следващо изпращане", + "chat_pathCleared": "Пътят е почистен. Следващото съобщение ще открие маршрута отново.", + "chat_floodModeSubtitle": "Използвайте превключвателя за маршрутизиране в лентата на приложението.", + "chat_floodModeEnabled": "Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.", + "chat_fullPath": "Пълен път", + "chat_pathDetailsNotAvailable": "Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.", + "chat_pathSetHops": "Пътят е зададен: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Запазено локално. Свържете се за синхронизиране.", - "chat_pathDeviceConfirmed": "Устройство потвърдено.", - "chat_pathDeviceNotConfirmed": "Устройството все още не е потвърдено.", - "chat_type": "Въведете", - "chat_path": "Пътекино", - "chat_publicKey": "Публичен ключ", - "chat_compressOutgoingMessages": "Компресиране на изходящи съобщения", - "chat_floodForced": "Потоп (принуден)", - "chat_directForced": "Директно (принудително)", - "chat_hopsForced": "{count} скока (принудително)", + "chat_pathSavedLocally": "Запазено локално. Свържете се за синхронизиране.", + "chat_pathDeviceConfirmed": "Устройство потвърдено.", + "chat_pathDeviceNotConfirmed": "Устройството все още не е потвърдено.", + "chat_type": "Въведете", + "chat_path": "Пътекино", + "chat_publicKey": "Публичен ключ", + "chat_compressOutgoingMessages": "Компресиране на изходящи съобщения", + "chat_floodForced": "Потоп (принуден)", + "chat_directForced": "Директно (принудително)", + "chat_hopsForced": "{count} скока (принудително)", "@chat_hopsForced": { "placeholders": { "count": { @@ -598,10 +598,10 @@ } } }, - "chat_floodAuto": "Потоп (автоматично)", - "chat_direct": "Директно", - "chat_poiShared": "Споделено място от интерес", - "chat_unread": "Непрочетени: {count}", + "chat_floodAuto": "Потоп (автоматично)", + "chat_direct": "Директно", + "chat_poiShared": "Споделено място от интерес", + "chat_unread": "Непрочетени: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Отваряне на връзката?", - "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?", - "chat_open": "Отвори", - "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}", + "chat_openLink": "Отваряне на връзката?", + "chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?", + "chat_open": "Отвори", + "chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,11 +620,11 @@ } } }, - "chat_invalidLink": "Невалиден формат на връзката", - "map_title": "Карта на възлите", - "map_noNodesWithLocation": "Няма възли с данни за местоположение.", - "map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.", - "map_nodesCount": "Нодове: {count}", + "chat_invalidLink": "Невалиден формат на връзката", + "map_title": "Карта на възлите", + "map_noNodesWithLocation": "Няма възли с данни за местоположение.", + "map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.", + "map_nodesCount": "Нодове: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -632,7 +632,7 @@ } } }, - "map_pinsCount": "Ключове: {count}", + "map_pinsCount": "Ключове: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -640,27 +640,27 @@ } } }, - "map_chat": "Чат", - "map_repeater": "Повтарящ се", - "map_room": "Стая", - "map_sensor": "Датчик", - "map_pinDm": "Задържане (DM)", - "map_pinPrivate": "Задържане (Приватно)", - "map_pinPublic": "Публичен ключ", - "map_lastSeen": "Последна видяна", - "map_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", - "map_from": "От", - "map_source": "Източник", - "map_flags": "Флаг", - "map_shareMarkerHere": "Споделете маркер тук", - "map_pinLabel": "Етикетиране на пин", - "map_label": "Етикет", - "map_pointOfInterest": "Точка на интерес", - "map_sendToContact": "Изпрати на контакт", - "map_sendToChannel": "Изпрати в канала", - "map_noChannelsAvailable": "Няма налични канали", - "map_publicLocationShare": "Споделяне на публично място", - "map_publicLocationShareConfirm": "Ще споделите местоположение в {channelLabel}. Този канал е публичен и всеки с PSK може да го види.", + "map_chat": "Чат", + "map_repeater": "Повтарящ се", + "map_room": "Стая", + "map_sensor": "Датчик", + "map_pinDm": "Задържане (DM)", + "map_pinPrivate": "Задържане (Приватно)", + "map_pinPublic": "Публичен ключ", + "map_lastSeen": "Последна видяна", + "map_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", + "map_from": "От", + "map_source": "Източник", + "map_flags": "Флаг", + "map_shareMarkerHere": "Споделете маркер тук", + "map_pinLabel": "Етикетиране на пин", + "map_label": "Етикет", + "map_pointOfInterest": "Точка на интерес", + "map_sendToContact": "Изпрати на контакт", + "map_sendToChannel": "Изпрати в канала", + "map_noChannelsAvailable": "Няма налични канали", + "map_publicLocationShare": "Споделяне на публично място", + "map_publicLocationShareConfirm": "Ще споделите местоположение в {channelLabel}. Този канал е публичен и всеки с PSK може да го види.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Свържете се с устройство, за да споделите маркери.", - "map_filterNodes": "Филтрирайте възли", - "map_nodeTypes": "Типове възли", - "map_chatNodes": "Възли на чата", - "map_repeaters": "Повторители", - "map_otherNodes": "Други възли", - "map_keyPrefix": "Префикс на ключа", - "map_filterByKeyPrefix": "Филтрирайте по префикс на ключ", - "map_publicKeyPrefix": "Префикс на публичен ключ", - "map_markers": "Маркери", - "map_showSharedMarkers": "Покажи споделени маркери", - "map_lastSeenTime": "Последна видяна дата", - "map_sharedPin": "Споделено копие", - "map_joinRoom": "Присъедини се към стаята", - "map_manageRepeater": "Управление на Повтарящ се Елемент", - "mapCache_title": "Кеш на офлайн карти", - "mapCache_selectAreaFirst": "Изберете област за кеширане първа", - "mapCache_noTilesToDownload": "Няма плочки за изтегляне за тази област.", - "mapCache_downloadTilesTitle": "Изтегли плочки", - "mapCache_downloadTilesPrompt": "Изтегли {count} плочки за офлайн употреба?", + "map_connectToShareMarkers": "Свържете се с устройство, за да споделите маркери.", + "map_filterNodes": "Филтрирайте възли", + "map_nodeTypes": "Типове възли", + "map_chatNodes": "Възли на чата", + "map_repeaters": "Повторители", + "map_otherNodes": "Други възли", + "map_keyPrefix": "Префикс на ключа", + "map_filterByKeyPrefix": "Филтрирайте по префикс на ключ", + "map_publicKeyPrefix": "Префикс на публичен ключ", + "map_markers": "Маркери", + "map_showSharedMarkers": "Покажи споделени маркери", + "map_lastSeenTime": "Последна видяна дата", + "map_sharedPin": "Споделено копие", + "map_joinRoom": "Присъедини се към стаята", + "map_manageRepeater": "Управление на Повтарящ се Елемент", + "mapCache_title": "Кеш на офлайн карти", + "mapCache_selectAreaFirst": "Изберете област за кеширане първа", + "mapCache_noTilesToDownload": "Няма плочки за изтегляне за тази област.", + "mapCache_downloadTilesTitle": "Изтегли плочки", + "mapCache_downloadTilesPrompt": "Изтегли {count} плочки за офлайн употреба?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,8 +695,8 @@ } } }, - "mapCache_downloadAction": "Изтегли", - "mapCache_cachedTiles": "Кеширани {count} плочки", + "mapCache_downloadAction": "Изтегли", + "mapCache_cachedTiles": "Кеширани {count} плочки", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Запазени {downloaded} плочки ({failed} неуспешни)", + "mapCache_cachedTilesWithFailed": "Запазени {downloaded} плочки ({failed} неуспешни)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Изчисти офлайн кеша", - "mapCache_clearOfflineCachePrompt": "Премахнете всички кеширани плочки на картата?", - "mapCache_offlineCacheCleared": "Кешът на устройството е изчистен.", - "mapCache_noAreaSelected": "Няма избрана област", - "mapCache_cacheArea": "Област с кеш", - "mapCache_useCurrentView": "Използвайте текущия изглед", - "mapCache_zoomRange": "Обхват на увеличението", - "mapCache_estimatedTiles": "Очаквани плочки: {count}", + "mapCache_clearOfflineCacheTitle": "Изчисти офлайн кеша", + "mapCache_clearOfflineCachePrompt": "Премахнете всички кеширани плочки на картата?", + "mapCache_offlineCacheCleared": "Кешът на устройството е изчистен.", + "mapCache_noAreaSelected": "Няма избрана област", + "mapCache_cacheArea": "Област с кеш", + "mapCache_useCurrentView": "Използвайте текущия изглед", + "mapCache_zoomRange": "Обхват на увеличението", + "mapCache_estimatedTiles": "Очаквани плочки: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Изтеглено {completed} / {total}", + "mapCache_downloadedTiles": "Изтеглено {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "Изтегли Плочки", - "mapCache_clearCacheButton": "Изчисти кеша", - "mapCache_failedDownloads": "Неуспешни изтегляния: {count}", + "mapCache_downloadTilesButton": "Изтегли Плочки", + "mapCache_clearCacheButton": "Изчисти кеша", + "mapCache_failedDownloads": "Неуспешни изтегляния: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -751,7 +751,7 @@ } } }, - "mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}", + "mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -768,8 +768,8 @@ } } }, - "time_justNow": "Сега", - "time_minutesAgo": "{minutes} минути преди", + "time_justNow": "Сега", + "time_minutesAgo": "{minutes} минути преди", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -777,7 +777,7 @@ } } }, - "time_hoursAgo": "{hours} часа преди", + "time_hoursAgo": "{hours} часа преди", "@time_hoursAgo": { "placeholders": { "hours": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} дни преди", + "time_daysAgo": "{days} дни преди", "@time_daysAgo": { "placeholders": { "days": { @@ -793,33 +793,33 @@ } } }, - "time_hour": "час", - "time_hours": "часове", - "time_day": "ден", - "time_days": "дни", - "time_week": "седмица", - "time_weeks": "секти", - "time_month": "месец", - "time_months": "месеци", - "time_minutes": "минути", - "time_allTime": "Всичко време", - "dialog_disconnect": "Прекъсни", - "dialog_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", - "login_repeaterLogin": "Повторител Вход", - "login_roomLogin": "Вход в стаята", - "login_password": "Парола", - "login_enterPassword": "Въведете парола", - "login_savePassword": "Запази парола", - "login_savePasswordSubtitle": "Паролата ще бъде съхранена сигурно на това устройство.", - "login_repeaterDescription": "Въведете паролата на репитера, за да получите достъп до настройките и статуса.", - "login_roomDescription": "Въведете паролата на стаята, за да получите достъп до настройките и статуса.", - "login_routing": "Маршрутизиране", - "login_routingMode": "Режим на маршрутизиране", - "login_autoUseSavedPath": "Автоматично (използвай запазения път)", - "login_forceFloodMode": "Принуди режим на наводняване", - "login_managePaths": "Управление на пътища", - "login_login": "Вход", - "login_attempt": "Опитвате {current}/{max}", + "time_hour": "час", + "time_hours": "часове", + "time_day": "ден", + "time_days": "дни", + "time_week": "седмица", + "time_weeks": "секти", + "time_month": "месец", + "time_months": "месеци", + "time_minutes": "минути", + "time_allTime": "Всичко време", + "dialog_disconnect": "Прекъсни", + "dialog_disconnectConfirm": "Сигурни ли сте, че искате да се откъснете от това устройство?", + "login_repeaterLogin": "Повторител Вход", + "login_roomLogin": "Вход в стаята", + "login_password": "Парола", + "login_enterPassword": "Въведете парола", + "login_savePassword": "Запази парола", + "login_savePasswordSubtitle": "Паролата ще бъде съхранена сигурно на това устройство.", + "login_repeaterDescription": "Въведете паролата на репитера, за да получите достъп до настройките и статуса.", + "login_roomDescription": "Въведете паролата на стаята, за да получите достъп до настройките и статуса.", + "login_routing": "Маршрутизиране", + "login_routingMode": "Режим на маршрутизиране", + "login_autoUseSavedPath": "Автоматично (използвай запазения път)", + "login_forceFloodMode": "Принуди режим на наводняване", + "login_managePaths": "Управление на пътища", + "login_login": "Вход", + "login_attempt": "Опитвате {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Входът не беше успешен: {error}", + "login_failed": "Входът не беше успешен: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.", - "common_reload": "Презареди", - "common_clear": "Изчисти", - "path_currentPath": "Текущ път: {path}", + "login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.", + "common_reload": "Презареди", + "common_clear": "Изчисти", + "path_currentPath": "Текущ път: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Използване на {count} {count, plural, =1{hop} other{hops}} път", + "path_usingHopsPath": "Използване на {count} {count, plural, =1{hop} other{hops}} път", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Въведете персонализиран път", - "path_currentPathLabel": "Текущ път", - "path_hexPrefixInstructions": "Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.", - "path_hexPrefixExample": "A1,F2,3C (всяка нода използва първия байт от публичния си ключ)", - "path_labelHexPrefixes": "Пътеки (шестнадесетични префикси)", - "path_helperMaxHops": "Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).", - "path_selectFromContacts": "Изберете от контакти:", - "path_noRepeatersFound": "Няма намерени репетитори или сървъри на стаи.", - "path_customPathsRequire": "Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.", - "path_invalidHexPrefixes": "Невалидни шестнадесетични префикси: {prefixes}", + "path_enterCustomPath": "Въведете персонализиран път", + "path_currentPathLabel": "Текущ път", + "path_hexPrefixInstructions": "Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.", + "path_hexPrefixExample": "A1,F2,3C (всяка нода използва първия байт от публичния си ключ)", + "path_labelHexPrefixes": "Пътеки (шестнадесетични префикси)", + "path_helperMaxHops": "Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).", + "path_selectFromContacts": "Изберете от контакти:", + "path_noRepeatersFound": "Няма намерени репетитори или сървъри на стаи.", + "path_customPathsRequire": "Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.", + "path_invalidHexPrefixes": "Невалидни шестнадесетични префикси: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Пътят е твърде дълъг. Максимум 64 скока са разрешени.", - "path_setPath": "Задайте път", - "repeater_management": "Управление на повторители", - "repeater_managementTools": "Инструменти за управление", - "repeater_status": "Статус", - "repeater_statusSubtitle": "Прегледайте статуса, статистиката и съседните устройства.", - "repeater_telemetry": "Телеметрия", - "repeater_telemetrySubtitle": "Прегледайте телеметрията на сензорите и системните статистики", + "path_tooLong": "Пътят е твърде дълъг. Максимум 64 скока са разрешени.", + "path_setPath": "Задайте път", + "repeater_management": "Управление на повторители", + "repeater_managementTools": "Инструменти за управление", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Прегледайте статуса, статистиката и съседните устройства.", + "repeater_telemetry": "Телеметрия", + "repeater_telemetrySubtitle": "Прегледайте телеметрията на сензорите и системните статистики", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Изпрати команди към ретранслатора", - "repeater_settings": "Настройки", - "repeater_settingsSubtitle": "Конфигурирайте параметрите на репитера", - "repeater_statusTitle": "Статус на повтарянето", - "repeater_routingMode": "Режим на маршрутизиране", - "repeater_autoUseSavedPath": "Автоматично (използвай запазения път)", - "repeater_forceFloodMode": "Принуди режим на наводняване", - "repeater_pathManagement": "Управление на пътища", - "repeater_refresh": "Презареди", - "repeater_statusRequestTimeout": "Заявката за статус премина прекалено дълго.", - "repeater_errorLoadingStatus": "Грешка при зареждане на статуса: {error}", + "repeater_cliSubtitle": "Изпрати команди към ретранслатора", + "repeater_settings": "Настройки", + "repeater_settingsSubtitle": "Конфигурирайте параметрите на репитера", + "repeater_statusTitle": "Статус на повтарянето", + "repeater_routingMode": "Режим на маршрутизиране", + "repeater_autoUseSavedPath": "Автоматично (използвай запазения път)", + "repeater_forceFloodMode": "Принуди режим на наводняване", + "repeater_pathManagement": "Управление на пътища", + "repeater_refresh": "Презареди", + "repeater_statusRequestTimeout": "Заявката за статус премина прекалено дълго.", + "repeater_errorLoadingStatus": "Грешка при зареждане на статуса: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -901,23 +901,23 @@ } } }, - "repeater_systemInformation": "Информация за системата", - "repeater_battery": "Батерия", - "repeater_clockAtLogin": "Часовник (при влизане)", - "repeater_uptime": "Наличност", - "repeater_queueLength": "Дължина на опашката", - "repeater_debugFlags": "Контролни точки за отстраняване на грешки", - "repeater_radioStatistics": "Статистика на радиостанциите", - "repeater_lastRssi": "Последна RSSI", - "repeater_lastSnr": "Последна SNR", - "repeater_noiseFloor": "Ниво на шум", + "repeater_systemInformation": "Информация за системата", + "repeater_battery": "Батерия", + "repeater_clockAtLogin": "Часовник (при влизане)", + "repeater_uptime": "Наличност", + "repeater_queueLength": "Дължина на опашката", + "repeater_debugFlags": "Контролни точки за отстраняване на грешки", + "repeater_radioStatistics": "Статистика на радиостанциите", + "repeater_lastRssi": "Последна RSSI", + "repeater_lastSnr": "Последна SNR", + "repeater_noiseFloor": "Ниво на шум", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Статистика на пакетите", - "repeater_sent": "Изпратено", - "repeater_received": "Получено", - "repeater_duplicates": "Дубликати", - "repeater_daysHoursMinsSecs": "{days} дни {hours}ч {minutes}м {seconds}с", + "repeater_packetStatistics": "Статистика на пакетите", + "repeater_sent": "Изпратено", + "repeater_received": "Получено", + "repeater_duplicates": "Дубликати", + "repeater_daysHoursMinsSecs": "{days} дни {hours}ч {minutes}м {seconds}с", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", + "repeater_packetTxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", + "repeater_packetRxTotal": "Общо: {total}, Наводнение: {flood}, Директно: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Поливане: {flood}, Директен: {direct}", + "repeater_duplicatesFloodDirect": "Поливане: {flood}, Директен: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -973,7 +973,7 @@ } } }, - "repeater_duplicatesTotal": "Общо: {total}", + "repeater_duplicatesTotal": "Общо: {total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -981,37 +981,37 @@ } } }, - "repeater_settingsTitle": "Настройки на повтарящия се елемент", - "repeater_basicSettings": "Основни настройки", - "repeater_repeaterName": "Име на повтарящ се елемент", - "repeater_repeaterNameHelper": "Показване на името на този репитер", - "repeater_adminPassword": "Парола на администратора", - "repeater_adminPasswordHelper": "Пълен достъпен парола", - "repeater_guestPassword": "Парола на гост", - "repeater_guestPasswordHelper": "Достъп с ограничен достъп", - "repeater_radioSettings": "Настройки на радиостанцията", - "repeater_frequencyMhz": "Честота (MHz)", + "repeater_settingsTitle": "Настройки на повтарящия се елемент", + "repeater_basicSettings": "Основни настройки", + "repeater_repeaterName": "Име на повтарящ се елемент", + "repeater_repeaterNameHelper": "Показване на името на този репитер", + "repeater_adminPassword": "Парола на администратора", + "repeater_adminPasswordHelper": "Пълен достъпен парола", + "repeater_guestPassword": "Парола на гост", + "repeater_guestPasswordHelper": "Достъп с ограничен достъп", + "repeater_radioSettings": "Настройки на радиостанцията", + "repeater_frequencyMhz": "Честота (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Ширина на честотния спектър", - "repeater_spreadingFactor": "Фактор на разпространение", - "repeater_codingRate": "Такса за кодиране", - "repeater_locationSettings": "Настройки на местоположението", - "repeater_latitude": "Широчина", - "repeater_latitudeHelper": "Десетични градуси (напр. 37.7749)", - "repeater_longitude": "Дължина", - "repeater_longitudeHelper": "Градуси с десетични знаци (напр. -122.4194)", - "repeater_features": "Характеристики", - "repeater_packetForwarding": "Пренасочване на пакети", - "repeater_packetForwardingSubtitle": "Активирайте репитера, за да препращате пакети.", - "repeater_guestAccess": "Достъп за Гост", - "repeater_guestAccessSubtitle": "Разрешете самочетене за гости", - "repeater_privacyMode": "Режим на поверителност", - "repeater_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", - "repeater_advertisementSettings": "Настройки на рекламите", - "repeater_localAdvertInterval": "Местен Рекламен Интервал", - "repeater_localAdvertIntervalMinutes": "{minutes} минути", + "repeater_bandwidth": "Ширина на честотния спектър", + "repeater_spreadingFactor": "Фактор на разпространение", + "repeater_codingRate": "Такса за кодиране", + "repeater_locationSettings": "Настройки на местоположението", + "repeater_latitude": "Широчина", + "repeater_latitudeHelper": "Десетични градуси (напр. 37.7749)", + "repeater_longitude": "Дължина", + "repeater_longitudeHelper": "Градуси с десетични знаци (напр. -122.4194)", + "repeater_features": "Характеристики", + "repeater_packetForwarding": "Пренасочване на пакети", + "repeater_packetForwardingSubtitle": "Активирайте репитера, за да препращате пакети.", + "repeater_guestAccess": "Достъп за Гост", + "repeater_guestAccessSubtitle": "Разрешете самочетене за гости", + "repeater_privacyMode": "Режим на поверителност", + "repeater_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", + "repeater_advertisementSettings": "Настройки на рекламите", + "repeater_localAdvertInterval": "Местен Рекламен Интервал", + "repeater_localAdvertIntervalMinutes": "{minutes} минути", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1019,8 +1019,8 @@ } } }, - "repeater_floodAdvertInterval": "Интервал на рекламата за наводнения", - "repeater_floodAdvertIntervalHours": "{hours} часа", + "repeater_floodAdvertInterval": "Интервал на рекламата за наводнения", + "repeater_floodAdvertIntervalHours": "{hours} часа", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Криптиран Рекламен Интервал", - "repeater_dangerZone": "Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно", - "repeater_rebootRepeater": "БеРестартирай Репитер", - "repeater_rebootRepeaterSubtitle": "Рестартирайте ретранслатора.", - "repeater_rebootRepeaterConfirm": "Сигурни ли сте, че искате да рестартирате този репитер?", - "repeater_regenerateIdentityKey": "Генериране на Ключ за Идентичност", - "repeater_regenerateIdentityKeySubtitle": "Генериране на нова двойка публичен/частен ключ", - "repeater_regenerateIdentityKeyConfirm": "БеТова ще генерира нова идентичност за репитера. Продължете?", - "repeater_eraseFileSystem": "Изтрий Файлова Система", - "repeater_eraseFileSystemSubtitle": "Форматирайте файла на репитера", - "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!", - "repeater_eraseSerialOnly": "Изтриването е достъпно само през серийния терминал.", - "repeater_commandSent": "Командата е изпратена: {command}", + "repeater_encryptedAdvertInterval": "Криптиран Рекламен Интервал", + "repeater_dangerZone": "Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно", + "repeater_rebootRepeater": "БеРестартирай Репитер", + "repeater_rebootRepeaterSubtitle": "Рестартирайте ретранслатора.", + "repeater_rebootRepeaterConfirm": "Сигурни ли сте, че искате да рестартирате този репитер?", + "repeater_regenerateIdentityKey": "Генериране на Ключ за Идентичност", + "repeater_regenerateIdentityKeySubtitle": "Генериране на нова двойка публичен/частен ключ", + "repeater_regenerateIdentityKeyConfirm": "БеТова ще генерира нова идентичност за репитера. Продължете?", + "repeater_eraseFileSystem": "Изтрий Файлова Система", + "repeater_eraseFileSystemSubtitle": "Форматирайте файла на репитера", + "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!", + "repeater_eraseSerialOnly": "Изтриването е достъпно само през серийния терминал.", + "repeater_commandSent": "Командата е изпратена: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Грешка при изпращане на командата: {error}", + "repeater_errorSendingCommand": "Грешка при изпращане на командата: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "БеПотвърди", - "repeater_settingsSaved": "Настройките са запазени успешно.", - "repeater_errorSavingSettings": "Грешка при запазване на настройките: {error}", + "repeater_confirm": "БеПотвърди", + "repeater_settingsSaved": "Настройките са запазени успешно.", + "repeater_errorSavingSettings": "Грешка при запазване на настройките: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "Обнови Основни Настройки", - "repeater_refreshRadioSettings": "Обнови настройките на радиопредавателите", - "repeater_refreshTxPower": "Обнови TX захранване", - "repeater_refreshLocationSettings": "Обнови настройките на местоположението", - "repeater_refreshPacketForwarding": "Обнови пакетно пренасочване", - "repeater_refreshGuestAccess": "Обнови достъп за гости", - "repeater_refreshPrivacyMode": "Обнови Режим на поверителност", - "repeater_refreshAdvertisementSettings": "Обнови Настройки на Рекламата", - "repeater_refreshed": "{label} е обновено", + "repeater_refreshBasicSettings": "Обнови Основни Настройки", + "repeater_refreshRadioSettings": "Обнови настройките на радиопредавателите", + "repeater_refreshTxPower": "Обнови TX захранване", + "repeater_refreshLocationSettings": "Обнови настройките на местоположението", + "repeater_refreshPacketForwarding": "Обнови пакетно пренасочване", + "repeater_refreshGuestAccess": "Обнови достъп за гости", + "repeater_refreshPrivacyMode": "Обнови Режим на поверителност", + "repeater_refreshAdvertisementSettings": "Обнови Настройки на Рекламата", + "repeater_refreshed": "{label} е обновено", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Грешка при обновяване на {label}", + "repeater_errorRefreshing": "Грешка при обновяване на {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1090,18 +1090,18 @@ } } }, - "repeater_cliTitle": "Повторител CLI", - "repeater_debugNextCommand": "Поправи Следваща Команда", - "repeater_commandHelp": "Помощ", - "repeater_clearHistory": "Изчисти История", - "repeater_noCommandsSent": "Няма изпратени команди засега.", - "repeater_typeCommandOrUseQuick": "Въведете команда по-долу или използвайте бързи команди", - "repeater_enterCommandHint": "Въведете команда...", - "repeater_previousCommand": "Предходна команда", - "repeater_nextCommand": "Следваща команда", - "repeater_enterCommandFirst": "Въведете първо команда.", - "repeater_cliCommandFrameTitle": "Рамка за команда CLI", - "repeater_cliCommandError": "Грешка: {error}", + "repeater_cliTitle": "Повторител CLI", + "repeater_debugNextCommand": "Поправи Следваща Команда", + "repeater_commandHelp": "Помощ", + "repeater_clearHistory": "Изчисти История", + "repeater_noCommandsSent": "Няма изпратени команди засега.", + "repeater_typeCommandOrUseQuick": "Въведете команда по-долу или използвайте бързи команди", + "repeater_enterCommandHint": "Въведете команда...", + "repeater_previousCommand": "Предходна команда", + "repeater_nextCommand": "Следваща команда", + "repeater_enterCommandFirst": "Въведете първо команда.", + "repeater_cliCommandFrameTitle": "Рамка за команда CLI", + "repeater_cliCommandError": "Грешка: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1109,81 +1109,81 @@ } } }, - "repeater_cliQuickGetName": "Получи име", - "repeater_cliQuickGetRadio": "Получи радио", - "repeater_cliQuickGetTx": "Получи TX", - "repeater_cliQuickNeighbors": "Съседи", - "repeater_cliQuickVersion": "Версия", - "repeater_cliQuickAdvertise": "Рекламирай", - "repeater_cliQuickClock": "Часовник", - "repeater_cliHelpAdvert": "Изпраща рекламен пакет", - "repeater_cliHelpReboot": "Рестартира устройството. (Забележка, може да получите 'Timeout', което е нормално)", - "repeater_cliHelpClock": "Показва текущото време според часовника на всяко устройство.", - "repeater_cliHelpPassword": "Задава се нова администраторска парола за устройството.", - "repeater_cliHelpVersion": "Показва версията на устройството и датата на компилация на фърмуера.", - "repeater_cliHelpClearStats": "Рестартира различни статистики броячи до нула.", - "repeater_cliHelpSetAf": "Задава времето на фактора.", - "repeater_cliHelpSetTx": "Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).", - "repeater_cliHelpSetRepeat": "Активира или деактивира ролята на репитера за този възел.", - "repeater_cliHelpSetAllowReadOnly": "(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).", - "repeater_cliHelpSetFloodMax": "Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).", - "repeater_cliHelpSetIntThresh": "Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.", - "repeater_cliHelpSetAgcResetInterval": "Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetMultiAcks": "Активира или деактивира функцията 'двойни ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetFloodAdvertInterval": "Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetGuestPassword": "Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")", - "repeater_cliHelpSetName": "Задава име на обявата.", - "repeater_cliHelpSetLat": "Задава географска ширина на картата с реклами (в десетими градуси).", - "repeater_cliHelpSetLon": "Задава обхвата на дължина на картата на рекламата. (десетими градуса)", - "repeater_cliHelpSetRadio": "Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.", - "repeater_cliHelpSetRxDelay": "Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.", - "repeater_cliHelpSetTxDelay": "Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).", - "repeater_cliHelpSetDirectTxDelay": "Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.", - "repeater_cliHelpSetBridgeEnabled": "Активиране/Деактивиране на мост.", - "repeater_cliHelpSetBridgeDelay": "Задайте забавяне преди преизпращане на пакети.", - "repeater_cliHelpSetBridgeSource": "Изберете дали мостът ще предава препратени пакети или получени пакети.", - "repeater_cliHelpSetBridgeBaud": "Задайте скоростта на предаване за RS232 мостовете.", - "repeater_cliHelpSetBridgeSecret": "Задайте тайна за мостовете на EspNow.", - "repeater_cliHelpSetAdcMultiplier": "Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).", - "repeater_cliHelpTempRadio": "Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).", - "repeater_cliHelpSetPerm": "Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).", - "repeater_cliHelpGetBridgeType": "Получава тип мост none, rs232, espnow", - "repeater_cliHelpLogStart": "Започва записване на пакети във файловата система.", - "repeater_cliHelpLogStop": "Спира записването на пакети във файловата система.", - "repeater_cliHelpLogErase": "Изтрива логовете от пакета от файловата система.", - "repeater_cliHelpNeighbors": "Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.", - "repeater_cliHelpRegion": "(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.", - "repeater_cliHelpRegionLoad": "Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.", - "repeater_cliHelpRegionGet": "Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) 'F'\"", - "repeater_cliHelpRegionPut": "Добавя или актуализира дефиниция на регион с дадено име.", - "repeater_cliHelpRegionRemove": "Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)", - "repeater_cliHelpRegionAllowf": "Задава 'Потоп' разрешение за посочената област. ('' за глобалния/стария обхват)", - "repeater_cliHelpRegionDenyf": "Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )", - "repeater_cliHelpRegionHome": "Отговаря с текущия 'home' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).", - "repeater_cliHelpRegionHomeSet": "Задава 'домашно' региона.", - "repeater_cliHelpRegionSave": "Запазва списъка/картата с региони в съхранение.", - "repeater_cliHelpGps": "Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.", - "repeater_cliHelpGpsOnOff": "Включва/Изключва GPS захранването.", - "repeater_cliHelpGpsSync": "Синхронизира времето на възела с GPS часовника.", - "repeater_cliHelpGpsSetLoc": "Задава координатите на нодата по GPS и запазва предпочитанията.", - "repeater_cliHelpGpsAdvert": "Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията", - "repeater_cliHelpGpsAdvertSet": "Задава конфигурация на обявите за местоположение.", - "repeater_commandsListTitle": "Списък с команди", - "repeater_commandsListNote": "ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".", - "repeater_general": "Общо", - "repeater_settingsCategory": "Настройки", - "repeater_bridge": "Мост", - "repeater_logging": "Логване", - "repeater_neighborsRepeaterOnly": "Съседи (Само за повтаряне)", - "repeater_regionManagementRepeaterOnly": "Управление на региони (Само за повтарящ се канал)", - "repeater_regionNote": "Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.", - "repeater_gpsManagement": "Управление на GPS", - "repeater_gpsNote": "GPS командата е въведена, за да управлява теми, свързани с местоположението.", - "telemetry_receivedData": "Получени телеметрични данни", - "telemetry_requestTimeout": "Заявката за телеметрия е прекъсната.", - "telemetry_errorLoading": "Грешка при зареждане на телеметрията: {error}", + "repeater_cliQuickGetName": "Получи име", + "repeater_cliQuickGetRadio": "Получи радио", + "repeater_cliQuickGetTx": "Получи TX", + "repeater_cliQuickNeighbors": "Съседи", + "repeater_cliQuickVersion": "Версия", + "repeater_cliQuickAdvertise": "Рекламирай", + "repeater_cliQuickClock": "Часовник", + "repeater_cliHelpAdvert": "Изпраща рекламен пакет", + "repeater_cliHelpReboot": "Рестартира устройството. (Забележка, може да получите 'Timeout', което е нормално)", + "repeater_cliHelpClock": "Показва текущото време според часовника на всяко устройство.", + "repeater_cliHelpPassword": "Задава се нова администраторска парола за устройството.", + "repeater_cliHelpVersion": "Показва версията на устройството и датата на компилация на фърмуера.", + "repeater_cliHelpClearStats": "Рестартира различни статистики броячи до нула.", + "repeater_cliHelpSetAf": "Задава времето на фактора.", + "repeater_cliHelpSetTx": "Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).", + "repeater_cliHelpSetRepeat": "Активира или деактивира ролята на репитера за този възел.", + "repeater_cliHelpSetAllowReadOnly": "(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).", + "repeater_cliHelpSetFloodMax": "Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).", + "repeater_cliHelpSetIntThresh": "Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.", + "repeater_cliHelpSetAgcResetInterval": "Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetMultiAcks": "Активира или деактивира функцията 'двойни ACKs'.", + "repeater_cliHelpSetAdvertInterval": "Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetFloodAdvertInterval": "Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetGuestPassword": "Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")", + "repeater_cliHelpSetName": "Задава име на обявата.", + "repeater_cliHelpSetLat": "Задава географска ширина на картата с реклами (в десетими градуси).", + "repeater_cliHelpSetLon": "Задава обхвата на дължина на картата на рекламата. (десетими градуса)", + "repeater_cliHelpSetRadio": "Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.", + "repeater_cliHelpSetRxDelay": "Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.", + "repeater_cliHelpSetTxDelay": "Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).", + "repeater_cliHelpSetDirectTxDelay": "Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.", + "repeater_cliHelpSetBridgeEnabled": "Активиране/Деактивиране на мост.", + "repeater_cliHelpSetBridgeDelay": "Задайте забавяне преди преизпращане на пакети.", + "repeater_cliHelpSetBridgeSource": "Изберете дали мостът ще предава препратени пакети или получени пакети.", + "repeater_cliHelpSetBridgeBaud": "Задайте скоростта на предаване за RS232 мостовете.", + "repeater_cliHelpSetBridgeSecret": "Задайте тайна за мостовете на EspNow.", + "repeater_cliHelpSetAdcMultiplier": "Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).", + "repeater_cliHelpTempRadio": "Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).", + "repeater_cliHelpSetPerm": "Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).", + "repeater_cliHelpGetBridgeType": "Получава тип мост none, rs232, espnow", + "repeater_cliHelpLogStart": "Започва записване на пакети във файловата система.", + "repeater_cliHelpLogStop": "Спира записването на пакети във файловата система.", + "repeater_cliHelpLogErase": "Изтрива логовете от пакета от файловата система.", + "repeater_cliHelpNeighbors": "Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.", + "repeater_cliHelpRegion": "(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.", + "repeater_cliHelpRegionLoad": "Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.", + "repeater_cliHelpRegionGet": "Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) 'F'\"", + "repeater_cliHelpRegionPut": "Добавя или актуализира дефиниция на регион с дадено име.", + "repeater_cliHelpRegionRemove": "Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)", + "repeater_cliHelpRegionAllowf": "Задава 'Потоп' разрешение за посочената област. ('' за глобалния/стария обхват)", + "repeater_cliHelpRegionDenyf": "Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )", + "repeater_cliHelpRegionHome": "Отговаря с текущия 'home' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).", + "repeater_cliHelpRegionHomeSet": "Задава 'домашно' региона.", + "repeater_cliHelpRegionSave": "Запазва списъка/картата с региони в съхранение.", + "repeater_cliHelpGps": "Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.", + "repeater_cliHelpGpsOnOff": "Включва/Изключва GPS захранването.", + "repeater_cliHelpGpsSync": "Синхронизира времето на възела с GPS часовника.", + "repeater_cliHelpGpsSetLoc": "Задава координатите на нодата по GPS и запазва предпочитанията.", + "repeater_cliHelpGpsAdvert": "Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията", + "repeater_cliHelpGpsAdvertSet": "Задава конфигурация на обявите за местоположение.", + "repeater_commandsListTitle": "Списък с команди", + "repeater_commandsListNote": "ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".", + "repeater_general": "Общо", + "repeater_settingsCategory": "Настройки", + "repeater_bridge": "Мост", + "repeater_logging": "Логване", + "repeater_neighborsRepeaterOnly": "Съседи (Само за повтаряне)", + "repeater_regionManagementRepeaterOnly": "Управление на региони (Само за повтарящ се канал)", + "repeater_regionNote": "Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.", + "repeater_gpsManagement": "Управление на GPS", + "repeater_gpsNote": "GPS командата е въведена, за да управлява теми, свързани с местоположението.", + "telemetry_receivedData": "Получени телеметрични данни", + "telemetry_requestTimeout": "Заявката за телеметрия е прекъсната.", + "telemetry_errorLoading": "Грешка при зареждане на телеметрията: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,8 +1191,8 @@ } } }, - "telemetry_noData": "Няма налични данни за телеметрията.", - "telemetry_channelTitle": "Канал {channel}", + "telemetry_noData": "Няма налични данни за телеметрията.", + "telemetry_channelTitle": "Канал {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1200,11 +1200,11 @@ } } }, - "telemetry_batteryLabel": "Батерия", - "telemetry_voltageLabel": "Напрежение", - "telemetry_mcuTemperatureLabel": "Температура на MCU", - "telemetry_temperatureLabel": "Температура", - "telemetry_currentLabel": "Текущо", + "telemetry_batteryLabel": "Батерия", + "telemetry_voltageLabel": "Напрежение", + "telemetry_mcuTemperatureLabel": "Температура на MCU", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Текущо", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Пътеки пъзел", - "channelPath_viewMap": "Преглед на картата", - "channelPath_otherObservedPaths": "Други Наблюдавани Пътища", - "channelPath_repeaterHops": "Повтарящи се скокове", - "channelPath_noHopDetails": "Детайлите за пакета не са предоставени.", - "channelPath_messageDetails": "Подробности на съобщението", - "channelPath_senderLabel": "Изпращач", - "channelPath_timeLabel": "Време", - "channelPath_repeatsLabel": "Повтаря", - "channelPath_pathLabel": "Път {index}", - "channelPath_observedLabel": "Наблюдавано", - "channelPath_observedPathTitle": "Наблюдаван път {index} • {hops}", + "channelPath_title": "Пътеки пъзел", + "channelPath_viewMap": "Преглед на картата", + "channelPath_otherObservedPaths": "Други Наблюдавани Пътища", + "channelPath_repeaterHops": "Повтарящи се скокове", + "channelPath_noHopDetails": "Детайлите за пакета не са предоставени.", + "channelPath_messageDetails": "Подробности на съобщението", + "channelPath_senderLabel": "Изпращач", + "channelPath_timeLabel": "Време", + "channelPath_repeatsLabel": "Повтаря", + "channelPath_pathLabel": "Път {index}", + "channelPath_observedLabel": "Наблюдавано", + "channelPath_observedPathTitle": "Наблюдаван път {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Няма данни за местоположение.", + "channelPath_noLocationData": "Няма данни за местоположение.", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1288,10 +1288,10 @@ } } }, - "channelPath_unknownPath": "Неизвестно", - "channelPath_floodPath": "Поливане", - "channelPath_directPath": "Директно", - "channelPath_observedZeroOf": "0 от {total} скокове", + "channelPath_unknownPath": "Неизвестно", + "channelPath_floodPath": "Поливане", + "channelPath_directPath": "Директно", + "channelPath_observedZeroOf": "0 от {total} скокове", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1299,7 +1299,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} от {total} скокове", + "channelPath_observedSomeOf": "{observed} от {total} скокове", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1310,9 +1310,9 @@ } } }, - "channelPath_mapTitle": "Карта на пътя", - "channelPath_noRepeaterLocations": "Няма налични местоположения на повторителите за този път.", - "channelPath_primaryPath": "Път {index} (Основен)", + "channelPath_mapTitle": "Карта на пътя", + "channelPath_noRepeaterLocations": "Няма налични местоположения на повторителите за този път.", + "channelPath_primaryPath": "Път {index} (Основен)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1327,9 +1327,9 @@ } } }, - "channelPath_pathLabelTitle": "Пътекино", - "channelPath_observedPathHeader": "Наблюдаван път", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Пътекино", + "channelPath_observedPathHeader": "Наблюдаван път", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,20 +1340,20 @@ } } }, - "channelPath_noHopDetailsAvailable": "Няма налични детайли за този пакет.", - "channelPath_unknownRepeater": "Неизвестен повторител", - "listFilter_tooltip": "Филтрирайте и сортирайте", - "listFilter_sortBy": "Сортирай по", - "listFilter_latestMessages": "Последни съобщения", - "listFilter_heardRecently": "Слушано е наскоро", + "channelPath_noHopDetailsAvailable": "Няма налични детайли за този пакет.", + "channelPath_unknownRepeater": "Неизвестен повторител", + "listFilter_tooltip": "Филтрирайте и сортирайте", + "listFilter_sortBy": "Сортирай по", + "listFilter_latestMessages": "Последни съобщения", + "listFilter_heardRecently": "Слушано е наскоро", "listFilter_az": "A-Z", - "listFilter_filters": "Филтри", - "listFilter_all": "Всички", - "listFilter_users": "Потребители", - "listFilter_repeaters": "Повторители", - "listFilter_roomServers": "Сървъри на стая", - "listFilter_unreadOnly": "Само непрочетените", - "listFilter_newGroup": "Нова група", + "listFilter_filters": "Филтри", + "listFilter_all": "Всички", + "listFilter_users": "Потребители", + "listFilter_repeaters": "Повторители", + "listFilter_roomServers": "Сървъри на стая", + "listFilter_unreadOnly": "Само непрочетените", + "listFilter_newGroup": "Нова група", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1361,25 +1361,25 @@ } } }, - "repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.", - "repeater_neighbors": "Съседи", - "neighbors_receivedData": "Получени данни за съседи", - "neighbors_requestTimedOut": "Съседите поискат изтичане на време.", - "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", - "neighbors_repeatersNeighbors": "Повторители Съседи", - "neighbors_noData": "Няма налични данни за съседи.", - "channels_createPrivateChannel": "Създай Частен Канал", - "channels_joinPrivateChannel": "Присъедини се към Частен Канал", - "channels_createPrivateChannelDesc": "Защитено с таен ключ.", - "channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.", - "channels_joinPublicChannel": "Присъединете се към Публичния канал", - "channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.", - "channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал", - "channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.", - "channels_scanQrCode": "Сканирайте QR код", - "channels_scanQrCodeComingSoon": "Ще излезе скоро", - "channels_enterHashtag": "Въведете хаштаг", - "channels_hashtagHint": "напр. #отбор", + "repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.", + "repeater_neighbors": "Съседи", + "neighbors_receivedData": "Получени данни за съседи", + "neighbors_requestTimedOut": "Съседите поискат изтичане на време.", + "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", + "neighbors_repeatersNeighbors": "Повторители Съседи", + "neighbors_noData": "Няма налични данни за съседи.", + "channels_createPrivateChannel": "Създай Частен Канал", + "channels_joinPrivateChannel": "Присъедини се към Частен Канал", + "channels_createPrivateChannelDesc": "Защитено с таен ключ.", + "channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.", + "channels_joinPublicChannel": "Присъединете се към Публичния канал", + "channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.", + "channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал", + "channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.", + "channels_scanQrCode": "Сканирайте QR код", + "channels_scanQrCodeComingSoon": "Ще излезе скоро", + "channels_enterHashtag": "Въведете хаштаг", + "channels_hashtagHint": "напр. #отбор", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_heardAgo": "Слушано преди {time}.", - "neighbors_unknownContact": "Неизвестна {pubkey}", - "settings_locationIntervalSec": "Интервал за GPS (Секунди)", - "settings_locationGPSEnable": "Активиране на GPS", - "settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.", - "settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.", - "room_management": "Управление на сървъра за стая", - "contacts_manageRoom": "Управление на сървър за стая", + "neighbors_heardAgo": "Слушано преди {time}.", + "neighbors_unknownContact": "Неизвестна {pubkey}", + "settings_locationIntervalSec": "Интервал за GPS (Секунди)", + "settings_locationGPSEnable": "Активиране на GPS", + "settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.", + "settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.", + "room_management": "Управление на сървъра за стая", + "contacts_manageRoom": "Управление на сървър за стая", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1458,36 +1458,36 @@ } } }, - "community_title": "Общност", - "common_ok": "Добре", - "community_createDesc": "Създайте нова общност и я споделете чрез QR код.", - "community_create": "Създай общност", - "community_joinTitle": "Присъедини се към общността", - "community_joinConfirmation": "Искате ли да се присъедините към общността \"{name}\"?", - "community_scanQr": "Сканирайте QR кода на общността", - "community_scanInstructions": "Насочете камерата към QR код на общността", - "community_showQr": "Покажи QR код", - "community_publicChannel": "Обществено общност", - "community_hashtagChannel": "Хаштаг на общността", - "community_name": "Име на общността", - "community_enterName": "Въведете име на общността", - "community_created": "Общността \"{name}\" е създадена", - "community_joined": "Присъединено общност \"{name}\"", - "community_qrTitle": "Споделяне в общността", - "community_join": "Присъедини се", - "community_qrInstructions": "Сканирайте този QR код, за да се присъедините към {name}.", - "community_hashtagPrivacyHint": "Хаштаг каналите на общността са достъпни само за членове на общността", - "community_invalidQrCode": "Невалиден QR код на общността", - "community_alreadyMember": "Вече съм член", - "community_alreadyMemberMessage": "Вие вече сте член на \"{name}\".", - "community_addPublicChannel": "Добави публичен общностен канал", - "community_addPublicChannelHint": "Автоматично добавете публичния канал за тази общност.", - "community_noCommunities": "Няма присъединени общности още.", - "community_scanOrCreate": "Сканирайте QR код или създайте общност, за да започнете.", - "community_manageCommunities": "Управление на общности", - "community_delete": "Напусни общността", - "community_deleteConfirm": "Напускате \"{name}\"?", - "community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.", + "community_title": "Общност", + "common_ok": "Добре", + "community_createDesc": "Създайте нова общност и я споделете чрез QR код.", + "community_create": "Създай общност", + "community_joinTitle": "Присъедини се към общността", + "community_joinConfirmation": "Искате ли да се присъедините към общността \"{name}\"?", + "community_scanQr": "Сканирайте QR кода на общността", + "community_scanInstructions": "Насочете камерата към QR код на общността", + "community_showQr": "Покажи QR код", + "community_publicChannel": "Обществено общност", + "community_hashtagChannel": "Хаштаг на общността", + "community_name": "Име на общността", + "community_enterName": "Въведете име на общността", + "community_created": "Общността \"{name}\" е създадена", + "community_joined": "Присъединено общност \"{name}\"", + "community_qrTitle": "Споделяне в общността", + "community_join": "Присъедини се", + "community_qrInstructions": "Сканирайте този QR код, за да се присъедините към {name}.", + "community_hashtagPrivacyHint": "Хаштаг каналите на общността са достъпни само за членове на общността", + "community_invalidQrCode": "Невалиден QR код на общността", + "community_alreadyMember": "Вече съм член", + "community_alreadyMemberMessage": "Вие вече сте член на \"{name}\".", + "community_addPublicChannel": "Добави публичен общностен канал", + "community_addPublicChannelHint": "Автоматично добавете публичния канал за тази общност.", + "community_noCommunities": "Няма присъединени общности още.", + "community_scanOrCreate": "Сканирайте QR код или създайте общност, за да започнете.", + "community_manageCommunities": "Управление на общности", + "community_delete": "Напусни общността", + "community_deleteConfirm": "Напускате \"{name}\"?", + "community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,15 +1495,15 @@ } } }, - "community_deleted": "Остави общността \"{name}\"", - "community_addHashtagChannel": "Добави общностен хаштаг", - "community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност", - "community_selectCommunity": "Изберете общност", - "community_regularHashtag": "Обикновен хаштаг", - "community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)", - "community_communityHashtag": "Общностен хаштаг", - "community_communityHashtagDesc": "Само за членове на общността", - "community_forCommunity": "За {name}", + "community_deleted": "Остави общността \"{name}\"", + "community_addHashtagChannel": "Добави общностен хаштаг", + "community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност", + "community_selectCommunity": "Изберете общност", + "community_regularHashtag": "Обикновен хаштаг", + "community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)", + "community_communityHashtag": "Общностен хаштаг", + "community_communityHashtagDesc": "Само за членове на общността", + "community_forCommunity": "За {name}", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1532,13 +1532,13 @@ } } }, - "community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.", - "community_secretRegenerated": "Секретно презареждане за \"{name}\"", - "community_regenerateSecret": "Регенерейрай секрет", - "community_regenerate": "Регенерация", - "community_updateSecret": "Актуализирай тайна", - "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"", - "community_secretUpdated": "Секретно обновено за \"{name}\"", + "community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.", + "community_secretRegenerated": "Секретно презареждане за \"{name}\"", + "community_regenerateSecret": "Регенерейрай секрет", + "community_regenerate": "Регенерация", + "community_updateSecret": "Актуализирай тайна", + "community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"", + "community_secretUpdated": "Секретно обновено за \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1546,82 +1546,82 @@ } } }, - "pathTrace_you": "Вие", - "pathTrace_notAvailable": "Пътека за проследяване не е достъпна.", - "contacts_pathTrace": "Пътен проследяване", - "pathTrace_refreshTooltip": "Обнови Path Trace.", - "pathTrace_failed": "Пътят за проследяване не успя.", - "contacts_repeaterPing": "Пингване на повторителя", - "contacts_repeaterPathTrace": "Трасировка до повторител", - "contacts_ping": "Пинг", - "contacts_chatTraceRoute": "Трасиране на път", - "contacts_roomPathTrace": "Трасиране на път до съ", - "contacts_roomPing": "Ping на сървъра на стаята", - "contacts_pathTraceTo": "Проследи маршрут към {name}", - "appSettings_languageUk": "Украински", - "contacts_clipboardEmpty": "Клипборда е празна.", - "contacts_invalidAdvertFormat": "Невалидни данни за контакт", - "appSettings_languageRu": "Руски", - "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", - "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", - "contacts_contactImported": "Контактът е импортиран.", - "contacts_zeroHopAdvert": "Реклама без скок", - "contacts_contactImportFailed": "Контактът не е успешно импортиран.", - "contacts_floodAdvert": "Потопна реклама", - "contacts_addContactFromClipboard": "Добави контакт от клипборда", - "contacts_copyAdvertToClipboard": "Копирай обявата в клипборда", - "contacts_ShareContact": "Копирай контакт в клипборда", - "contacts_ShareContactZeroHop": "Сподели контакт чрез обява", - "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", - "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", - "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", - "notification_activityTitle": "Активност на MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}", - "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", - "notification_newTypeDiscovered": "Открит нов {contactType}", - "notification_receivedNewMessage": "Получено ново съобщение", - "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", - "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", - "settings_gpxExportAll": "Експортирай всички контакти в GPX", - "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", - "settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX", - "settings_gpxExportContacts": "Експортирай спътници към GPX", - "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", - "settings_gpxExportNoContacts": "Няма контакти за изlexport.", - "settings_gpxExportChat": "Местоположения на спътници", - "settings_gpxExportError": "Възникна грешка при изнасяне.", - "settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята", - "settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС", - "settings_gpxExportAllContacts": "Местоположения на всички контакти", - "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", - "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!", - "map_pathTraceCancelled": "Отменен е следването на пътя.", - "pathTrace_clearTooltip": "Изчисти пътя", - "map_removeLast": "Премахни Последно", - "map_runTrace": "Изпълни Път на Следване", - "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", - "scanner_bluetoothOff": "Bluetooth е изключен.", - "scanner_enableBluetooth": "Активирайте Bluetooth", - "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", - "scanner_chromeRequired": "Изисква се браузър Chrome", - "scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.", - "snrIndicator_lastSeen": "Последно видян", - "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", - "chat_ShowAllPaths": "Покажи всички пътища", - "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", - "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", - "settings_clientRepeat": "Без електричество – повторение", - "settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "единици", - "appSettings_unitsMetric": "Метрика (m / km)", - "appSettings_unitsImperial": "Имперска (ft / mi)", - "map_lineOfSight": "Линия на видимост", - "map_losScreenTitle": "Линия на видимост", - "losSelectStartEnd": "Изберете начални и крайни възли за LOS.", - "losRunFailed": "Проверката на пряката видимост е неуспешна: {error}", + "pathTrace_you": "Вие", + "pathTrace_notAvailable": "Пътека за проследяване не е достъпна.", + "contacts_pathTrace": "Пътен проследяване", + "pathTrace_refreshTooltip": "Обнови Path Trace.", + "pathTrace_failed": "Пътят за проследяване не успя.", + "contacts_repeaterPing": "Пингване на повторителя", + "contacts_repeaterPathTrace": "Трасировка до повторител", + "contacts_ping": "Пинг", + "contacts_chatTraceRoute": "Трасиране на път", + "contacts_roomPathTrace": "Трасиране на път до съ", + "contacts_roomPing": "Ping на сървъра на стаята", + "contacts_pathTraceTo": "Проследи маршрут към {name}", + "appSettings_languageUk": "Украински", + "contacts_clipboardEmpty": "Клипборда е празна.", + "contacts_invalidAdvertFormat": "Невалидни данни за контакт", + "appSettings_languageRu": "Руски", + "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", + "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", + "contacts_contactImported": "Контактът е импортиран.", + "contacts_zeroHopAdvert": "Реклама без скок", + "contacts_contactImportFailed": "Контактът не е успешно импортиран.", + "contacts_floodAdvert": "Потопна реклама", + "contacts_addContactFromClipboard": "Добави контакт от клипборда", + "contacts_copyAdvertToClipboard": "Копирай обявата в клипборда", + "contacts_ShareContact": "Копирай контакт в клипборда", + "contacts_ShareContactZeroHop": "Сподели контакт чрез обява", + "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", + "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", + "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", + "notification_activityTitle": "Активност на MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}", + "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", + "notification_newTypeDiscovered": "Открит нов {contactType}", + "notification_receivedNewMessage": "Получено ново съобщение", + "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", + "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", + "settings_gpxExportAll": "Експортирай всички контакти в GPX", + "settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.", + "settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX", + "settings_gpxExportContacts": "Експортирай спътници към GPX", + "settings_gpxExportSuccess": "Успешно изlexport на файл GPX.", + "settings_gpxExportNoContacts": "Няма контакти за изlexport.", + "settings_gpxExportChat": "Местоположения на спътници", + "settings_gpxExportError": "Възникна грешка при изнасяне.", + "settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята", + "settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС", + "settings_gpxExportAllContacts": "Местоположения на всички контакти", + "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", + "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!", + "map_pathTraceCancelled": "Отменен е следването на пътя.", + "pathTrace_clearTooltip": "Изчисти пътя", + "map_removeLast": "Премахни Последно", + "map_runTrace": "Изпълни Път на Следване", + "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", + "scanner_bluetoothOff": "Bluetooth е изключен.", + "scanner_enableBluetooth": "Активирайте Bluetooth", + "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "scanner_chromeRequired": "Изисква се браузър Chrome", + "scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.", + "snrIndicator_lastSeen": "Последно видян", + "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", + "chat_ShowAllPaths": "Покажи всички пътища", + "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", + "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", + "settings_clientRepeat": "Без електричество – повторение", + "settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "единици", + "appSettings_unitsMetric": "Метрика (m / km)", + "appSettings_unitsImperial": "Имперска (ft / mi)", + "map_lineOfSight": "Линия на видимост", + "map_losScreenTitle": "Линия на видимост", + "losSelectStartEnd": "Изберете начални и крайни възли за LOS.", + "losRunFailed": "Проверката на пряката видимост е неуспешна: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,13 +1629,13 @@ } } }, - "losClearAllPoints": "Изчистете всички точки", - "losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина", - "losMenuTitle": "LOS меню", - "losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки", - "losShowDisplayNodes": "Показване на възли на дисплея", - "losCustomPoints": "Персонализирани точки", - "losCustomPointLabel": "Персонализирано {index}", + "losClearAllPoints": "Изчистете всички точки", + "losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина", + "losMenuTitle": "LOS меню", + "losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки", + "losShowDisplayNodes": "Показване на възли на дисплея", + "losCustomPoints": "Персонализирани точки", + "losCustomPointLabel": "Персонализирано {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1643,9 +1643,9 @@ } } }, - "losPointA": "Точка А", - "losPointB": "Точка Б", - "losAntennaA": "Антена A: {value} {unit}", + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Антена B: {value} {unit}", + "losAntennaB": "Антена B: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1667,9 +1667,9 @@ } } }, - "losRun": "Стартирайте LOS", - "losNoElevationData": "Няма данни за надморска височина", - "losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}", + "losRun": "Стартирайте LOS", + "losNoElevationData": "Няма данни за надморска височина", + "losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1703,9 +1703,9 @@ } } }, - "losStatusChecking": "LOS: проверка...", - "losStatusNoData": "LOS: няма данни", - "losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно", + "losStatusChecking": "LOS: проверка...", + "losStatusNoData": "LOS: няма данни", + "losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.", - "losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.", - "losRenameCustomPoint": "Преименувайте персонализирана точка", - "losPointName": "Име на точката", - "losShowPanelTooltip": "Показване на LOS панел", - "losHidePanelTooltip": "Скриване на LOS панела", - "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Радиохоризонт", - "losLegendLosBeam": "Линия на видимост", - "losLegendTerrain": "Терен", - "losFrequencyLabel": "Честота", - "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", - "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", - "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", + "losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.", + "losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.", + "losRenameCustomPoint": "Преименувайте персонализирана точка", + "losPointName": "Име на точката", + "losShowPanelTooltip": "Показване на LOS панел", + "losHidePanelTooltip": "Скриване на LOS панела", + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиохоризонт", + "losLegendLosBeam": "Линия на видимост", + "losLegendTerrain": "Терен", + "losFrequencyLabel": "Честота", + "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", + "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_removeFromFavorites": "Премахване от списъка с любими", - "listFilter_addToFavorites": "Добави към любими", - "listFilter_favorites": "Любими", + "listFilter_removeFromFavorites": "Премахване от списъка с любими", + "listFilter_addToFavorites": "Добави към любими", + "listFilter_favorites": "Любими", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1796,17 +1796,29 @@ } } }, - "contacts_searchFavorites": "Търсене на {number}{str} любими...", - "contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...", - "contacts_unread": "Непрочетено", - "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", - "contacts_searchContactsNoNumber": "Търси контакти...", - "contacts_searchUsers": "Търсене на {number}{str} потребители...", - "connectionChoiceUsbLabel": "USB", + "contacts_searchFavorites": "Търсене на {number}{str} любими...", + "contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...", + "contacts_unread": "Непрочетено", + "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", + "contacts_searchContactsNoNumber": "Търси контакти...", + "contacts_searchUsers": "Търсене на {number}{str} потребители...", + "usbScreenSubtitle": "Изберете открит сериен уред и се свържете директно към вашия MeshCore възел.", + "usbScreenStatus": "Изберете USB устройство", + "usbScreenTitle": "Свързване чрез USB", + "usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.", + "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.", + "usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.", + "usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.", + "usbErrorInvalidPort": "Изберете валитно USB устройство.", + "usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.", + "usbErrorNotConnected": "Няма свързано USB устройство.", + "usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.", + "usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.", + "usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.", + "usbErrorAlreadyActive": "USB връзката вече е активирана.", + "usbErrorNoDeviceSelected": "Няма избран USB устройство.", + "usbErrorPortClosed": "USB връзката не е активна.", + "usbErrorConnectTimedOut": "Изчаква се, но устройството не отговаря в рамките на зададения време.", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.", - "usbScreenStatus": "Изберете USB устройство", - "usbScreenTitle": "Свързване чрез USB", - "usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.", - "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново." + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0b4bfff..3588ab7 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", +{ + "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -10,16 +10,16 @@ "@@locale": "de", "appTitle": "MeshCore Open", "nav_contacts": "Kontakte", - "nav_channels": "Kanäle", + "nav_channels": "Kanäle", "nav_map": "Karte", "common_cancel": "Abbrechen", "common_connect": "Verbinden", - "common_unknownDevice": "Unbekanntes Gerät", + "common_unknownDevice": "Unbekanntes Gerät", "common_save": "Speichern", - "common_delete": "Löschen", - "common_close": "Schließen", + "common_delete": "Löschen", + "common_close": "Schließen", "common_edit": "Bearbeiten", - "common_add": "Hinzufügen", + "common_add": "Hinzufügen", "common_settings": "Einstellungen", "common_disconnect": "Trennen", "common_connected": "Verbunden", @@ -30,12 +30,12 @@ "common_copy": "Kopieren", "common_retry": "Versuchen", "common_hide": "Ausblenden", - "common_remove": "Löschen", + "common_remove": "Löschen", "common_enable": "Aktivieren", "common_disable": "Deaktivieren", "common_reboot": "Neustart", "common_loading": "Laden...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,7 +53,7 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Scannen nach Geräten...", + "scanner_scanning": "Scannen nach Geräten...", "scanner_connecting": "Verbunden...", "scanner_disconnecting": "Trenne...", "scanner_notConnected": "Nicht verbunden", @@ -65,8 +65,8 @@ } } }, - "scanner_searchingDevices": "Suche nach MeshCore-Geräten...", - "scanner_tapToScan": "Tippen Sie auf Scan, um MeshCore-Geräte zu finden.", + "scanner_searchingDevices": "Suche nach MeshCore-Geräten...", + "scanner_tapToScan": "Tippen Sie auf Scan, um MeshCore-Geräte zu finden.", "scanner_connectionFailed": "Verbindungsfehler: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -80,7 +80,7 @@ "device_quickSwitch": "Schnelles Umschalten", "device_meshcore": "MeshCore", "settings_title": "Einstellungen", - "settings_deviceInfo": "Geräteinformationen", + "settings_deviceInfo": "Geräteinformationen", "settings_appSettings": "App-Einstellungen", "settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung", "settings_nodeSettings": "Knoten-Einstellungen", @@ -94,33 +94,33 @@ "settings_location": "Ort", "settings_locationSubtitle": "GPS-Koordinaten", "settings_locationUpdated": "Ort aktualisiert", - "settings_locationBothRequired": "Bitte geben Sie sowohl Breite als auch Längengrad ein.", - "settings_locationInvalid": "Ungültige Breiten- oder Längengrade.", + "settings_locationBothRequired": "Bitte geben Sie sowohl Breite als auch Längengrad ein.", + "settings_locationInvalid": "Ungültige Breiten- oder Längengrade.", "settings_latitude": "Breitengrad", - "settings_longitude": "Längengrad", - "settings_privacyMode": "Privatsphäreeinstellung", - "settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", - "settings_privacyModeToggle": "Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.", + "settings_longitude": "Längengrad", + "settings_privacyMode": "Privatsphäreeinstellung", + "settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", + "settings_privacyModeToggle": "Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.", "settings_privacyModeEnabled": "Datenschutzmodus aktiviert", "settings_privacyModeDisabled": "Datenschutzmodus deaktiviert", "settings_actions": "Aktionen", - "settings_sendAdvertisement": "Sende Ankündigung", - "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", - "settings_advertisementSent": "Ankündigung gesendet", + "settings_sendAdvertisement": "Sende Ankündigung", + "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", + "settings_advertisementSent": "Ankündigung gesendet", "settings_syncTime": "Zeitsynchronisierung", - "settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein", + "settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein", "settings_timeSynchronized": "Zeit synchronisiert", "settings_refreshContacts": "Kontakte aktualisieren", - "settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden", - "settings_rebootDevice": "Gerät neu starten", - "settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten", - "settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.", + "settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden", + "settings_rebootDevice": "Gerät neu starten", + "settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten", + "settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.", "settings_debug": "Fehlerbehebung", "settings_bleDebugLog": "BLE-Debug-Protokoll", "settings_bleDebugLogSubtitle": "BLE-Befehle, Antworten und Rohdaten", "settings_appDebugLog": "App-Debug-Protokoll", "settings_appDebugLogSubtitle": "Anwendung Debug-Nachrichten", - "settings_about": "Über", + "settings_about": "Über", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "MeshCore Open Source Projekt 2026", - "settings_aboutDescription": "Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.", + "settings_aboutDescription": "Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.", "settings_infoName": "Name", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Akku", - "settings_infoPublicKey": "Öffentlicher Schlüssel", + "settings_infoPublicKey": "Öffentlicher Schlüssel", "settings_infoContactsCount": "Anzahl Kontakte", - "settings_infoChannelCount": "Anzahl Kanäle", + "settings_infoChannelCount": "Anzahl Kanäle", "settings_presets": "Voreinstellungen", "settings_frequency": "Frequenz (MHz)", "settings_frequencyHelper": "300,00 - 2.500,00", - "settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)", + "settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)", "settings_bandwidth": "Bandbreite", "settings_spreadingFactor": "Verteilungsfaktor", "settings_codingRate": "Kodierungsrate", "settings_txPower": "TX-Leistung (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)", + "settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)", "settings_error": "Fehler: {message}", "@settings_error": { "placeholders": { @@ -165,21 +165,21 @@ "appSettings_language": "Sprache", "appSettings_languageSystem": "Systemstandard", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Benachrichtigungen", "appSettings_enableNotifications": "Benachrichtigungen aktivieren", - "appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Ankündigungen", + "appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Ankündigungen", "appSettings_notificationPermissionDenied": "Erlaubnis zur Benachrichtigung verweigert", "appSettings_notificationsEnabled": "Benachrichtigungen aktiviert", "appSettings_notificationsDisabled": "Benachrichtigungen deaktiviert", @@ -187,20 +187,20 @@ "appSettings_messageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfang neuer Direktnachrichten", "appSettings_channelMessageNotifications": "Kanalnachrichten Benachrichtigungen", "appSettings_channelMessageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfangen von Kanalnachrichten", - "appSettings_advertisementNotifications": "Ankündigungsbenachrichtigungen", + "appSettings_advertisementNotifications": "Ankündigungsbenachrichtigungen", "appSettings_advertisementNotificationsSubtitle": "Zeige Benachrichtigung, wenn neue Knoten entdeckt werden.", "appSettings_messaging": "Nachrichten", - "appSettings_clearPathOnMaxRetry": "Lösche Pfade bei Max Wiederholungsversuchen", - "appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen", - "appSettings_pathsWillBeCleared": "Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.", - "appSettings_pathsWillNotBeCleared": "Die Pfade werden nicht automatisch gelöscht.", + "appSettings_clearPathOnMaxRetry": "Lösche Pfade bei Max Wiederholungsversuchen", + "appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen", + "appSettings_pathsWillBeCleared": "Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.", + "appSettings_pathsWillNotBeCleared": "Die Pfade werden nicht automatisch gelöscht.", "appSettings_autoRouteRotation": "Automatische Routenrotation", "appSettings_autoRouteRotationSubtitle": "Wechseln zwischen den besten Pfaden und dem Fluten", "appSettings_autoRouteRotationEnabled": "Automatische Routenrotation aktiviert", "appSettings_autoRouteRotationDisabled": "Automatische Routenrotation deaktiviert", "appSettings_battery": "Akku", "appSettings_batteryChemistry": "Batteriechemie", - "appSettings_batteryChemistryPerDevice": "Konfiguriert pro Gerät ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Konfiguriert pro Gerät ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,10 +208,10 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Verbinde ein Gerät, um zu wählen", - "appSettings_batteryNmc": "18650 NMC (3,0–4,2 V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", - "appSettings_batteryLipo": "LiPo (3,0–4,2V)", + "appSettings_batteryChemistryConnectFirst": "Verbinde ein Gerät, um zu wählen", + "appSettings_batteryNmc": "18650 NMC (3,0–4,2 V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", + "appSettings_batteryLipo": "LiPo (3,0–4,2V)", "appSettings_mapDisplay": "Kartendarstellung", "appSettings_showRepeaters": "Zeige Repeater", "appSettings_showRepeatersSubtitle": "Zeige Repeater-Knoten auf der Karte an", @@ -237,8 +237,8 @@ "appSettings_last24Hours": "Letzte 24 Stunden", "appSettings_lastWeek": "Letzte Woche", "appSettings_offlineMapCache": "Offline-Karten-Cache", - "appSettings_noAreaSelected": "Kein Bereich ausgewählt", - "appSettings_areaSelectedZoom": "Ausgewählte Fläche (Zoom {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Kein Bereich ausgewählt", + "appSettings_areaSelectedZoom": "Ausgewählte Fläche (Zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -256,11 +256,11 @@ "appSettings_appDebugLoggingDisabled": "App-Debug-Protokollierung deaktiviert", "contacts_title": "Kontakte", "contacts_noContacts": "Noch keine Kontakte vorhanden.", - "contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.", + "contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.", "contacts_searchContacts": "Suche Kontakte...", "contacts_noUnreadContacts": "Keine ungesehene Kontakte", "contacts_noContactsFound": "Keine Kontakte oder Gruppen gefunden.", - "contacts_deleteContact": "Lösche den Kontakt", + "contacts_deleteContact": "Lösche den Kontakt", "contacts_removeConfirm": "{contactName} aus den Kontakten entfernen?", "@contacts_removeConfirm": { "placeholders": { @@ -271,10 +271,10 @@ }, "contacts_manageRepeater": "Repeater verwalten", "contacts_roomLogin": "Raum-Login", - "contacts_openChat": "Öffne Chat", + "contacts_openChat": "Öffne Chat", "contacts_editGroup": "Gruppe bearbeiten", - "contacts_deleteGroup": "Löschen Gruppe", - "contacts_deleteGroupConfirm": "Löschen von \"{groupName}\"?", + "contacts_deleteGroup": "Löschen Gruppe", + "contacts_deleteGroupConfirm": "Löschen von \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -323,11 +323,11 @@ } } }, - "channels_title": "Kanäle", - "channels_noChannelsConfigured": "Keine Kanäle konfiguriert", - "channels_addPublicChannel": "Öffentlichen Kanal hinzufügen", - "channels_searchChannels": "Suche Kanäle...", - "channels_noChannelsFound": "Keine Kanäle gefunden", + "channels_title": "Kanäle", + "channels_noChannelsConfigured": "Keine Kanäle konfiguriert", + "channels_addPublicChannel": "Öffentlichen Kanal hinzufügen", + "channels_searchChannels": "Suche Kanäle...", + "channels_noChannelsFound": "Keine Kanäle gefunden", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { "placeholders": { @@ -337,15 +337,15 @@ } }, "channels_hashtagChannel": "Hashtag-Kanal", - "channels_public": "Öffentlich", + "channels_public": "Öffentlich", "channels_private": "Privat", - "channels_publicChannel": "Öffentlicher Kanal", + "channels_publicChannel": "Öffentlicher Kanal", "channels_privateChannel": "Privater Kanal", "channels_editChannel": "Kanal bearbeiten", "channels_muteChannel": "Kanal stummschalten", "channels_unmuteChannel": "Kanal Stummschaltung aufheben", - "channels_deleteChannel": "Lösche den Kanal", - "channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.", + "channels_deleteChannel": "Lösche den Kanal", + "channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Kanal \"{name}\" gelöscht", + "channels_channelDeleted": "Kanal \"{name}\" gelöscht", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Kanal hinzufügen", + "channels_addChannel": "Kanal hinzufügen", "channels_channelIndexLabel": "Kanalindex", "channels_channelName": "Kanalname", - "channels_usePublicChannel": "Verwende öffentlichen Kanal", - "channels_standardPublicPsk": "Öffentliche Standard PSK", + "channels_usePublicChannel": "Verwende öffentlichen Kanal", + "channels_standardPublicPsk": "Öffentliche Standard PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Zufällige PSK generieren", + "channels_generateRandomPsk": "Zufällige PSK generieren", "channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.", "channels_pskMustBe32Hex": "Die PSK muss 32 hexadezimale Zeichen haben.", - "channels_channelAdded": "Kanal \"{name}\" hinzugefügt", + "channels_channelAdded": "Kanal \"{name}\" hinzugefügt", "@channels_channelAdded": { "placeholders": { "name": { @@ -395,7 +395,7 @@ } } }, - "channels_publicChannelAdded": "Öffentlicher Kanal hinzugefügt", + "channels_publicChannelAdded": "Öffentlicher Kanal hinzugefügt", "channels_sortBy": "Sortiere nach", "channels_sortManual": "Manuell", "channels_sortAZ": "A bis Z", @@ -439,7 +439,7 @@ } }, "chat_messageCopied": "Nachricht kopiert", - "chat_messageDeleted": "Nachricht gelöscht", + "chat_messageDeleted": "Nachricht gelöscht", "chat_retryingMessage": "Versuche es erneut.", "chat_retryCount": "Versuche {current}/{max}", "@chat_retryCount": { @@ -454,13 +454,13 @@ }, "chat_sendGif": "GIF senden", "chat_reply": "Beantworten", - "chat_addReaction": "Reaktion hinzufügen", + "chat_addReaction": "Reaktion hinzufügen", "chat_me": "Ich", "emojiCategorySmileys": "Emoticons", "emojiCategoryGestures": "Gesten", "emojiCategoryHearts": "Herz", "emojiCategoryObjects": "Objekte", - "gifPicker_title": "Wähle ein GIF", + "gifPicker_title": "Wähle ein GIF", "gifPicker_searchHint": "Suche nach GIFs...", "gifPicker_poweredBy": "Bereitgestellt von GIPHY", "gifPicker_noGifsFound": "Keine GIFs gefunden", @@ -470,15 +470,15 @@ "debugLog_appTitle": "App-Debug-Protokoll", "debugLog_bleTitle": "BLE-Debug-Protokoll", "debugLog_copyLog": "Kopieren des Protokolls", - "debugLog_clearLog": "Protokoll löschen", + "debugLog_clearLog": "Protokoll löschen", "debugLog_copied": "Debug-Protokoll kopiert", "debugLog_bleCopied": "BLE-Protokoll kopiert", - "debugLog_noEntries": "No Debug-Protokolle noch verfügbar", + "debugLog_noEntries": "No Debug-Protokolle noch verfügbar", "debugLog_enableInSettings": "Aktivieren Sie das App-Debug-Logging in den Einstellungen", "debugLog_frames": "Rahmen", "debugLog_rawLogRx": "Roh-Log-RX", - "debugLog_noBleActivity": "Bisher keine BLE-Aktivität", - "debugFrame_length": "Rahmenlänge: {count} Bytes", + "debugLog_noBleActivity": "Bisher keine BLE-Aktivität", + "debugFrame_length": "Rahmenlänge: {count} Bytes", "@debugFrame_length": { "placeholders": { "count": { @@ -495,7 +495,7 @@ } }, "debugFrame_textMessageHeader": "Textnachrichten Frame:", - "debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}", + "debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -546,10 +546,10 @@ "chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)", "chat_forceFloodMode": "Flut-Modus erzwingen", "chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):", - "chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.", + "chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.", "chat_hopSingular": "Sprung", - "chat_hopPlural": "Sprünge", - "chat_hopsCount": "{count} {count, plural, =1{Sprung} other{Sprünge}}", + "chat_hopPlural": "Sprünge", + "chat_hopsCount": "{count} {count, plural, =1{Sprung} other{Sprünge}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -563,13 +563,13 @@ "chat_pathActions": "Pfadaktionen:", "chat_setCustomPath": "Lege benutzerdefinierten Pfad fest", "chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen", - "chat_clearPath": "Pfad zurücksetzen", - "chat_clearPathSubtitle": "Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.", - "chat_pathCleared": "Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.", + "chat_clearPath": "Pfad zurücksetzen", + "chat_clearPathSubtitle": "Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.", + "chat_pathCleared": "Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.", "chat_floodModeSubtitle": "Verwende den Routingschalter in der App-Leiste", "chat_floodModeEnabled": "Flutmodus aktiviert.", - "chat_fullPath": "Vollständiger Pfad", - "chat_pathDetailsNotAvailable": "Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.", + "chat_fullPath": "Vollständiger Pfad", + "chat_pathDetailsNotAvailable": "Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.", "chat_pathSetHops": "Pfad gesetzt: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -582,15 +582,15 @@ } }, "chat_pathSavedLocally": "Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.", - "chat_pathDeviceConfirmed": "Gerät bestätigt.", - "chat_pathDeviceNotConfirmed": "Gerät noch nicht bestätigt.", + "chat_pathDeviceConfirmed": "Gerät bestätigt.", + "chat_pathDeviceNotConfirmed": "Gerät noch nicht bestätigt.", "chat_type": "Gebe ein", "chat_path": "Pfad", - "chat_publicKey": "Öffentlicher Schlüssel", + "chat_publicKey": "Öffentlicher Schlüssel", "chat_compressOutgoingMessages": "Komprimieren ausgehender Nachrichten", "chat_floodForced": "Geflutet (erzwungen)", "chat_directForced": "Direkt (erzwungen)", - "chat_hopsForced": "{count} Sprünge (erzwungen)", + "chat_hopsForced": "{count} Sprünge (erzwungen)", "@chat_hopsForced": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Link öffnen?", - "chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?", - "chat_open": "Öffnen", - "chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}", + "chat_openLink": "Link öffnen?", + "chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?", + "chat_open": "Öffnen", + "chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,10 +620,10 @@ } } }, - "chat_invalidLink": "Ungültiges Link-Format", + "chat_invalidLink": "Ungültiges Link-Format", "map_title": "Karte", "map_noNodesWithLocation": "Keine Knoten mit Standortdaten", - "map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.", + "map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.", "map_nodesCount": "Knoten: {count}", "@map_nodesCount": { "placeholders": { @@ -648,7 +648,7 @@ "map_pinPrivate": "Pin (Channel)", "map_pinPublic": "Pin (Public)", "map_lastSeen": "Letzte Sichtung", - "map_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", + "map_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", "map_from": "Von", "map_source": "Quelle", "map_flags": "Flags", @@ -658,9 +658,9 @@ "map_pointOfInterest": "Punkt von Interesse", "map_sendToContact": "Senden an Kontakt", "map_sendToChannel": "Senden an Kanal", - "map_noChannelsAvailable": "Keine Kanäle verfügbar", - "map_publicLocationShare": "Öffentliche Standortfreigabe", - "map_publicLocationShareConfirm": "Sie werden kurz darauf einen Ort in {channelLabel} teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.", + "map_noChannelsAvailable": "Keine Kanäle verfügbar", + "map_publicLocationShare": "Öffentliche Standortfreigabe", + "map_publicLocationShareConfirm": "Sie werden kurz darauf einen Ort in {channelLabel} teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,15 +668,15 @@ } } }, - "map_connectToShareMarkers": "Verbinde ein Gerät, um Marker zu teilen", + "map_connectToShareMarkers": "Verbinde ein Gerät, um Marker zu teilen", "map_filterNodes": "Knotenfilter", "map_nodeTypes": "Knotentypen", "map_chatNodes": "Chat-Knoten", "map_repeaters": "Repeater", "map_otherNodes": "Andere Knoten", - "map_keyPrefix": "Schlüsselpräfix", - "map_filterByKeyPrefix": "Filter nach Schlüsselpräfix", - "map_publicKeyPrefix": "Schlüsselpräfix", + "map_keyPrefix": "Schlüsselpräfix", + "map_filterByKeyPrefix": "Filter nach Schlüsselpräfix", + "map_publicKeyPrefix": "Schlüsselpräfix", "map_markers": "Marker", "map_showSharedMarkers": "Zeige gemeinsam genutzte Marker", "map_lastSeenTime": "Letzte Sichtung", @@ -684,10 +684,10 @@ "map_joinRoom": "Beitreten Sie dem Raum", "map_manageRepeater": "Repeater verwalten", "mapCache_title": "Offline-Karten-Cache", - "mapCache_selectAreaFirst": "Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.", - "mapCache_noTilesToDownload": "Keine Kacheln für diese Region zum Herunterladen verfügbar.", + "mapCache_selectAreaFirst": "Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.", + "mapCache_noTilesToDownload": "Keine Kacheln für diese Region zum Herunterladen verfügbar.", "mapCache_downloadTilesTitle": "Herunterladen von Kacheln", - "mapCache_downloadTilesPrompt": "Laden {count} Kacheln für den Offline-Bereich herunter?", + "mapCache_downloadTilesPrompt": "Laden {count} Kacheln für den Offline-Bereich herunter?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -717,12 +717,12 @@ }, "mapCache_clearOfflineCacheTitle": "Leere Offline-Cache", "mapCache_clearOfflineCachePrompt": "Alle zwischengespeicherten Kartenraster entfernen?", - "mapCache_offlineCacheCleared": "Offline-Cache gelöscht", - "mapCache_noAreaSelected": "Kein Bereich ausgewählt", + "mapCache_offlineCacheCleared": "Offline-Cache gelöscht", + "mapCache_noAreaSelected": "Kein Bereich ausgewählt", "mapCache_cacheArea": "Zwischenspeicherbereich", "mapCache_useCurrentView": "Aktuelle Ansicht verwenden", "mapCache_zoomRange": "Zoom Bereich", - "mapCache_estimatedTiles": "Geschätzte Kacheln: {count}", + "mapCache_estimatedTiles": "Geschätzte Kacheln: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -804,13 +804,13 @@ "time_minutes": "Minuten", "time_allTime": "Ganzer Zeitraum", "dialog_disconnect": "Trennen", - "dialog_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", + "dialog_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?", "login_repeaterLogin": "Beim Repeater anmelden", "login_roomLogin": "Raum-Login", "login_password": "Passwort", "login_enterPassword": "Passwort eingeben", "login_savePassword": "Passwort speichern", - "login_savePasswordSubtitle": "Das Passwort wird auf diesem Gerät sicher gespeichert.", + "login_savePasswordSubtitle": "Das Passwort wird auf diesem Gerät sicher gespeichert.", "login_repeaterDescription": "Geben Sie das Repeater-Passwort ein, um auf Einstellungen und Status zuzugreifen.", "login_roomDescription": "Geben Sie das Raumkennwort ein, um auf die Einstellungen und den Status zuzugreifen.", "login_routing": "Routen", @@ -840,7 +840,7 @@ }, "login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.", "common_reload": "Neu laden", - "common_clear": "Löschen", + "common_clear": "Löschen", "path_currentPath": "Aktiver Pfad: {path}", "@path_currentPath": { "placeholders": { @@ -859,14 +859,14 @@ }, "path_enterCustomPath": "Gebe Pfad ein", "path_currentPathLabel": "Aktueller Pfad", - "path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.", - "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)", - "path_labelHexPrefixes": "Pfad (Hex-Präfixe)", - "path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)", - "path_selectFromContacts": "Oder wähle aus Kontakten aus:", + "path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.", + "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)", + "path_labelHexPrefixes": "Pfad (Hex-Präfixe)", + "path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)", + "path_selectFromContacts": "Oder wähle aus Kontakten aus:", "path_noRepeatersFound": "Keine Repeater oder Raumserver gefunden.", - "path_customPathsRequire": "Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.", - "path_invalidHexPrefixes": "Ungültige Hexadezimal-Präfixe: {prefixes}", + "path_customPathsRequire": "Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.", + "path_invalidHexPrefixes": "Ungültige Hexadezimal-Präfixe: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -904,8 +904,8 @@ "repeater_systemInformation": "Systeminformation", "repeater_battery": "Akku", "repeater_clockAtLogin": "Uhr (bei Anmeldung)", - "repeater_uptime": "Verfügbarkeit", - "repeater_queueLength": "Warteschlangenlänge", + "repeater_uptime": "Verfügbarkeit", + "repeater_queueLength": "Warteschlangenlänge", "repeater_debugFlags": "Fehlerbehebungsoptionen", "repeater_radioStatistics": "Funk-Statistik", "repeater_lastRssi": "Letzter RSSI", @@ -984,11 +984,11 @@ "repeater_settingsTitle": "Repeater Einstellungen", "repeater_basicSettings": "Grundlegende Einstellungen", "repeater_repeaterName": "Repeater Name", - "repeater_repeaterNameHelper": "Anzeigename für diesen Repeater", + "repeater_repeaterNameHelper": "Anzeigename für diesen Repeater", "repeater_adminPassword": "Admin-Passwort", "repeater_adminPasswordHelper": "Vollzugriffspasswort", "repeater_guestPassword": "Gast-Passwort", - "repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort", + "repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort", "repeater_radioSettings": "Funk Einstellungen", "repeater_frequencyMhz": "Frequenz (MHz)", "repeater_frequencyHelper": "300-2500 MHz", @@ -1000,17 +1000,17 @@ "repeater_locationSettings": "Standort Einstellungen", "repeater_latitude": "Breitengrad", "repeater_latitudeHelper": "Dezimalgrad (z.B. 37,7749)", - "repeater_longitude": "Längengrad", + "repeater_longitude": "Längengrad", "repeater_longitudeHelper": "Dezimalgrad (z.B. -122,4194)", "repeater_features": "Funktionen", "repeater_packetForwarding": "Paketweiterleitung", "repeater_packetForwardingSubtitle": "Aktivieren Sie den Repeater, um Pakete weiterzuleiten.", "repeater_guestAccess": "Gastzugriff", - "repeater_guestAccessSubtitle": "Gast-Zugriff mit beschränkten Rechten zulassen", - "repeater_privacyMode": "Privatsphäreeinstellung", - "repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", - "repeater_advertisementSettings": "Ankündigungseinstellungen", - "repeater_localAdvertInterval": "Intervall der lokalen Ankündigungen", + "repeater_guestAccessSubtitle": "Gast-Zugriff mit beschränkten Rechten zulassen", + "repeater_privacyMode": "Privatsphäreeinstellung", + "repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen", + "repeater_advertisementSettings": "Ankündigungseinstellungen", + "repeater_localAdvertInterval": "Intervall der lokalen Ankündigungen", "repeater_localAdvertIntervalMinutes": "{minutes} Minuten", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervall der gefluteten Ankündigungen", + "repeater_floodAdvertInterval": "Intervall der gefluteten Ankündigungen", "repeater_floodAdvertIntervalHours": "{hours} Stunden", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,18 +1028,18 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung", + "repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung", "repeater_dangerZone": "Gefahrenzone", "repeater_rebootRepeater": "Neustart Repeater", - "repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.", - "repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?", - "repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung", - "repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren", - "repeater_regenerateIdentityKeyConfirm": "Dies generiert eine neue Identität für den Repeater. Fortfahren?", - "repeater_eraseFileSystem": "Dateisystem löschen", + "repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.", + "repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?", + "repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung", + "repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren", + "repeater_regenerateIdentityKeyConfirm": "Dies generiert eine neue Identität für den Repeater. Fortfahren?", + "repeater_eraseFileSystem": "Dateisystem löschen", "repeater_eraseFileSystemSubtitle": "Formatiere die Repeater-Dateisystemdatei", - "repeater_eraseFileSystemConfirm": "WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!", - "repeater_eraseSerialOnly": "Löschen ist nur über die serielle Konsole möglich.", + "repeater_eraseFileSystemConfirm": "WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!", + "repeater_eraseSerialOnly": "Löschen ist nur über die serielle Konsole möglich.", "repeater_commandSent": "Befehl gesendet: {command}", "@repeater_commandSent": { "placeholders": { @@ -1056,7 +1056,7 @@ } } }, - "repeater_confirm": "Bestätigen", + "repeater_confirm": "Bestätigen", "repeater_settingsSaved": "Einstellungen erfolgreich gespeichert", "repeater_errorSavingSettings": "Fehler beim Speichern der Einstellungen: {error}", "@repeater_errorSavingSettings": { @@ -1073,7 +1073,7 @@ "repeater_refreshPacketForwarding": "Aktualisieren Paketweiterleitung", "repeater_refreshGuestAccess": "Aktualisieren Sie den Gastzugriff", "repeater_refreshPrivacyMode": "Wiederherstellen des Datenschutzzustands", - "repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Ankündigungseinstellungen", + "repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Ankündigungseinstellungen", "repeater_refreshed": "{label} wurde aktualisiert", "@repeater_refreshed": { "placeholders": { @@ -1091,14 +1091,14 @@ } }, "repeater_cliTitle": "Repeater CLI", - "repeater_debugNextCommand": "Fehlersuche des nächsten Befehls", + "repeater_debugNextCommand": "Fehlersuche des nächsten Befehls", "repeater_commandHelp": "Hilfe", - "repeater_clearHistory": "Löschen der Historie", + "repeater_clearHistory": "Löschen der Historie", "repeater_noCommandsSent": "Noch keine Befehle gesendet.", "repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle", "repeater_enterCommandHint": "Geben Sie den Befehl ein...", "repeater_previousCommand": "Vorhergehende Aktion", - "repeater_nextCommand": "Nächste Aktion", + "repeater_nextCommand": "Nächste Aktion", "repeater_enterCommandFirst": "Geben Sie zuerst einen Befehl ein", "repeater_cliCommandFrameTitle": "CLI-Befehlsfenster", "repeater_cliCommandError": "Fehler: {error}", @@ -1114,73 +1114,73 @@ "repeater_cliQuickGetTx": "Erhalte TX", "repeater_cliQuickNeighbors": "Nachbarn", "repeater_cliQuickVersion": "Version", - "repeater_cliQuickAdvertise": "Ankündigungen", + "repeater_cliQuickAdvertise": "Ankündigungen", "repeater_cliQuickClock": "Uhr", - "repeater_cliHelpAdvert": "Sendet eine Ankündigung", - "repeater_cliHelpReboot": "Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer 'Timeout'-Situation kommt, was normal ist.)", - "repeater_cliHelpClock": "Zeigt die aktuelle Uhrzeit pro Gerät an.", - "repeater_cliHelpPassword": "Legt ein neues Administrator-Passwort für das Gerät fest.", - "repeater_cliHelpVersion": "Zeigt die Geräteversion und das Datum des Firmware-Builds an.", - "repeater_cliHelpClearStats": "Setzt verschiedene Statistikberechnungen auf Null zurück.", + "repeater_cliHelpAdvert": "Sendet eine Ankündigung", + "repeater_cliHelpReboot": "Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer 'Timeout'-Situation kommt, was normal ist.)", + "repeater_cliHelpClock": "Zeigt die aktuelle Uhrzeit pro Gerät an.", + "repeater_cliHelpPassword": "Legt ein neues Administrator-Passwort für das Gerät fest.", + "repeater_cliHelpVersion": "Zeigt die Geräteversion und das Datum des Firmware-Builds an.", + "repeater_cliHelpClearStats": "Setzt verschiedene Statistikberechnungen auf Null zurück.", "repeater_cliHelpSetAf": "Legt den Luftzeitfaktor fest.", - "repeater_cliHelpSetTx": "Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)", - "repeater_cliHelpSetRepeat": "Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.", - "repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).", - "repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)", + "repeater_cliHelpSetTx": "Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)", + "repeater_cliHelpSetRepeat": "Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.", + "repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).", + "repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)", "repeater_cliHelpSetIntThresh": "Legt den Interferenzeniveau (in dB) fest. Der Standardwert ist 14. Auf 0 setzen, um die Erkennung von Kanalinterferenzen zu deaktivieren.", - "repeater_cliHelpSetAgcResetInterval": "Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetAgcResetInterval": "Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.", "repeater_cliHelpSetMultiAcks": "Aktiviert oder deaktiviert die Funktion 'Doppel-ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.", - "repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.", - "repeater_cliHelpSetGuestPassword": "Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)", + "repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.", + "repeater_cliHelpSetGuestPassword": "Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)", "repeater_cliHelpSetName": "Legt den Anzeigenamen fest.", - "repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)", - "repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)", - "repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.", - "repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", - "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", - "repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.", - "repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.", - "repeater_cliHelpSetBridgeDelay": "Setze Verzögerung vor erneuter Übertragung von Paketen.", - "repeater_cliHelpSetBridgeSource": "Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.", - "repeater_cliHelpSetBridgeBaud": "Setze die serielle Link-Baudrate für RS232-Brücken.", - "repeater_cliHelpSetBridgeSecret": "Richte das Brückenpassword ein.", - "repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).", - "repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).", - "repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)", - "repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.", + "repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)", + "repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)", + "repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.", + "repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", + "repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.", + "repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.", + "repeater_cliHelpSetBridgeDelay": "Setze Verzögerung vor erneuter Übertragung von Paketen.", + "repeater_cliHelpSetBridgeSource": "Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.", + "repeater_cliHelpSetBridgeBaud": "Setze die serielle Link-Baudrate für RS232-Brücken.", + "repeater_cliHelpSetBridgeSecret": "Richte das Brückenpassword ein.", + "repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).", + "repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).", + "repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)", + "repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.", "repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.", "repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.", - "repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.", - "repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.", + "repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.", + "repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.", "repeater_cliHelpRegion": "Listet alle definierten Regionen auf.", - "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.", - "repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".", - "repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.", - "repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)", - "repeater_cliHelpRegionAllowf": "Legt die 'Flut'-Berechtigung für die angegebene Region fest. ('' für den globalen/legacy-Bereich)", - "repeater_cliHelpRegionDenyf": "Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)", - "repeater_cliHelpRegionHome": "Antwortet mit der aktuellen 'Home'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)", + "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.", + "repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".", + "repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.", + "repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)", + "repeater_cliHelpRegionAllowf": "Legt die 'Flut'-Berechtigung für die angegebene Region fest. ('' für den globalen/legacy-Bereich)", + "repeater_cliHelpRegionDenyf": "Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)", + "repeater_cliHelpRegionHome": "Antwortet mit der aktuellen 'Home'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)", "repeater_cliHelpRegionHomeSet": "Legt die 'Home'-Region fest.", "repeater_cliHelpRegionSave": "Speichert die Regionenliste/Karte in den Speicher.", "repeater_cliHelpGps": "Zeigt GPS-Status an. Wenn GPS deaktiviert ist, antwortet es nur mit \"aus\", wenn es eingeschaltet ist, antwortet es mit \"an\", \"Status\", \"Fix\" und Satellitenanzahl.", "repeater_cliHelpGpsOnOff": "Schaltet die GPS-Leistung ein/aus.", "repeater_cliHelpGpsSync": "Synchronisiert die Knotenzeit mit der GPS-Uhr.", - "repeater_cliHelpGpsSetLoc": "Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.", - "repeater_cliHelpGpsAdvert": "Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen", + "repeater_cliHelpGpsSetLoc": "Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.", + "repeater_cliHelpGpsAdvert": "Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen", "repeater_cliHelpGpsAdvertSet": "Legt die Standort-Anzeigekonfiguration fest.", "repeater_commandsListTitle": "Befehlsliste", - "repeater_commandsListNote": "ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.", + "repeater_commandsListNote": "ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.", "repeater_general": "Allgemein", "repeater_settingsCategory": "Einstellungen", - "repeater_bridge": "Brücke", + "repeater_bridge": "Brücke", "repeater_logging": "Protokollierung", "repeater_neighborsRepeaterOnly": "Nachbarn (nur Repeater)", "repeater_regionManagementRepeaterOnly": "Regionenverwaltung (nur Repeater)", - "repeater_regionNote": "Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.", + "repeater_regionNote": "Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.", "repeater_gpsManagement": "GPS-Verwaltung", - "repeater_gpsNote": "Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.", + "repeater_gpsNote": "Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.", "telemetry_receivedData": "Empfangene Telemetriedaten", "telemetry_requestTimeout": "Telemetry-Anfrage hat zu lange gedauert.", "telemetry_errorLoading": "Fehler beim Laden der Telemetrie: {error}", @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Keine Telemetriedaten verfügbar.", + "telemetry_noData": "Keine Telemetriedaten verfügbar.", "telemetry_channelTitle": "Kanal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1246,15 +1246,15 @@ "channelPath_title": "Paketpfad", "channelPath_viewMap": "Karte anzeigen", "channelPath_otherObservedPaths": "Sonstige beobachtete Pfade", - "channelPath_repeaterHops": "Repeater-Sprünge", - "channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.", + "channelPath_repeaterHops": "Repeater-Sprünge", + "channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.", "channelPath_messageDetails": "Nachrichtendetails", "channelPath_senderLabel": "Sender", "channelPath_timeLabel": "Zeit", "channelPath_repeatsLabel": "Wiederholungen", "channelPath_pathLabel": "Pfad {index}", "channelPath_observedLabel": "Beobachtet", - "channelPath_observedPathTitle": "Beobachteter Pfad {index} • {hops}", + "channelPath_observedPathTitle": "Beobachteter Pfad {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1291,7 +1291,7 @@ "channelPath_unknownPath": "Unbekannt", "channelPath_floodPath": "Geflutet", "channelPath_directPath": "Direkt", - "channelPath_observedZeroOf": "0 von {total} Sprüngen", + "channelPath_observedZeroOf": "0 von {total} Sprüngen", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1299,7 +1299,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} von {total} Sprüngen", + "channelPath_observedSomeOf": "{observed} von {total} Sprüngen", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1311,8 +1311,8 @@ } }, "channelPath_mapTitle": "Pfadkarte", - "channelPath_noRepeaterLocations": "Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.", - "channelPath_primaryPath": "Pfad {index} (Primär)", + "channelPath_noRepeaterLocations": "Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.", + "channelPath_primaryPath": "Pfad {index} (Primär)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Pfad", "channelPath_observedPathHeader": "Beobachteter Pfad", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,17 +1340,17 @@ } } }, - "channelPath_noHopDetailsAvailable": "Keine Informationen zu dieser Paketroute verfügbar.", + "channelPath_noHopDetailsAvailable": "Keine Informationen zu dieser Paketroute verfügbar.", "channelPath_unknownRepeater": "Unbekannter Repeater", "listFilter_tooltip": "Filteren und sortieren", "listFilter_sortBy": "Sortiere nach", "listFilter_latestMessages": "Letzte Nachrichten", - "listFilter_heardRecently": "Kürzlich gehört", + "listFilter_heardRecently": "Kürzlich gehört", "listFilter_az": "A-Z", "listFilter_filters": "Filtere", "listFilter_all": "Alle", "listFilter_favorites": "Favoriten", - "listFilter_addToFavorites": "Zu Favoriten hinzufügen", + "listFilter_addToFavorites": "Zu Favoriten hinzufügen", "listFilter_removeFromFavorites": "Aus Favoriten entfernen", "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", @@ -1370,17 +1370,17 @@ "neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", "neighbors_repeatersNeighbors": "Nachbarn", - "neighbors_noData": "Keine Nachbarsdaten verfügbar.", + "neighbors_noData": "Keine Nachbarsdaten verfügbar.", "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", - "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", + "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", "channels_createPrivateChannel": "Erstelle einen privaten Kanal", - "channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.", - "channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei", + "channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.", + "channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei", "channels_joinPublicChannelDesc": "Jeder kann diesem Kanal beitreten.", "channels_joinHashtagChannel": "Treten Sie einem Hashtag-Kanal bei", - "channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.", + "channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.", "channels_scanQrCode": "Scannen Sie einen QR-Code", - "channels_scanQrCodeComingSoon": "Bald verfügbar", + "channels_scanQrCodeComingSoon": "Bald verfügbar", "channels_enterHashtag": "Gib Hashtag ein", "channels_hashtagHint": "z.B. #team", "@neighbors_unknownContact": { @@ -1397,11 +1397,11 @@ } } }, - "neighbors_heardAgo": "Gehört vor: {time}", + "neighbors_heardAgo": "Gehört vor: {time}", "neighbors_unknownContact": "Unbekannt {pubkey}", "settings_locationGPSEnable": "GPS aktivieren", "settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.", - "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", + "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", "settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.", "contacts_manageRoom": "Raum-Server verwalten", "room_management": "Raum-Server-Verwaltung", @@ -1463,34 +1463,34 @@ }, "common_ok": "OK", "community_create": "Erstelle Community", - "community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.", + "community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.", "community_join": "Beitreten", "community_joinTitle": "Tritt der Community bei", - "community_joinConfirmation": "Möchten Sie sich der Community \"{name}\" anschließen?", + "community_joinConfirmation": "Möchten Sie sich der Community \"{name}\" anschließen?", "community_scanQr": "Scannen Sie die Community QR-Code", "community_scanInstructions": "Richten Sie die Kamera auf einen Community-QR-Code.", "community_showQr": "Zeige QR-Code", - "community_publicChannel": "Community Öffentlich", + "community_publicChannel": "Community Öffentlich", "community_enterName": "Bitte Community-Name eingeben", "community_title": "Community", "community_created": "Community \"{name}\" wurde erstellt", "community_joined": "Community \"{name}\" beigetreten", "community_qrTitle": "Teile Community", - "community_qrInstructions": "Scannen Sie diesen QR-Code, um sich \"{name}\" anzuschließen.", - "community_hashtagPrivacyHint": "Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden", + "community_qrInstructions": "Scannen Sie diesen QR-Code, um sich \"{name}\" anzuschließen.", + "community_hashtagPrivacyHint": "Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden", "community_hashtagChannel": "Community Hashtag", "community_name": "Community Name", - "community_invalidQrCode": "Ungültiger Community-QR-Code", + "community_invalidQrCode": "Ungültiger Community-QR-Code", "community_alreadyMember": "Bereits registriert", "community_alreadyMemberMessage": "Sie sind bereits Mitglied von \"{name}\".", - "community_addPublicChannel": "Füge einen öffentlichen Community-Kanal hinzu", - "community_addPublicChannelHint": "Automatisch den öffentlichen Kanal für diese Community hinzufügen", + "community_addPublicChannel": "Füge einen öffentlichen Community-Kanal hinzu", + "community_addPublicChannelHint": "Automatisch den öffentlichen Kanal für diese Community hinzufügen", "community_noCommunities": "Noch keiner Community beigetreten", "community_scanOrCreate": "Scannen Sie einen QR-Code oder eine Community erstellen, um loszulegen.", "community_manageCommunities": "Verwalten von Communities", "community_delete": "Verlasse Community", "community_deleteConfirm": "\"{name}\" verlassen?", - "community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.", + "community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1499,13 +1499,13 @@ } }, "community_deleted": "Community \"{name}\" verlassen", - "community_addHashtagChannel": "Füge einen Community-Hashtag hinzu", - "community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu", - "community_selectCommunity": "Wählen Sie eine Community", - "community_regularHashtag": "Regulärer Hashtag", - "community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)", - "community_communityHashtagDesc": "Nur für Mitglieder der Community", - "community_forCommunity": "Für {name}", + "community_addHashtagChannel": "Füge einen Community-Hashtag hinzu", + "community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu", + "community_selectCommunity": "Wählen Sie eine Community", + "community_regularHashtag": "Regulärer Hashtag", + "community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)", + "community_communityHashtagDesc": "Nur für Mitglieder der Community", + "community_forCommunity": "Für {name}", "community_communityHashtag": "Community Hashtag", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1536,12 +1536,12 @@ } }, "community_regenerate": "Neu generieren", - "community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich", - "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.", - "community_regenerateSecret": "Neugenerierung des Schlüssels", - "community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert", - "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", - "community_updateSecret": "Aktualisieren Sie den Schlüssel", + "community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich", + "community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.", + "community_regenerateSecret": "Neugenerierung des Schlüssels", + "community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert", + "community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.", + "community_updateSecret": "Aktualisieren Sie den Schlüssel", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1552,7 +1552,7 @@ "pathTrace_refreshTooltip": "Path Trace aktualisieren.", "pathTrace_you": "Du", "pathTrace_failed": "Pfadverfolgung fehlgeschlagen.", - "pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.", + "pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.", "contacts_pathTrace": "Pfadverfolgung", "contacts_ping": "Pingen", "contacts_repeaterPathTrace": "Pfadverfolgung zum Repeater", @@ -1562,24 +1562,24 @@ "contacts_pathTraceTo": "Route nach {name} verfolgen", "contacts_chatTraceRoute": "Pfadverfolgungsroute", "appSettings_languageRu": "Russisch", - "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", + "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", "appSettings_languageUk": "Ukrainisch", "appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren", - "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", + "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", - "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", - "contacts_floodAdvert": "Flut-Ankündigung", - "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", - "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", - "contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren", + "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", + "contacts_floodAdvert": "Flut-Ankündigung", + "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", + "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", + "contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren", "contacts_ShareContact": "Kontakt in die Zwischenablage kopieren", "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", - "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", + "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", - "notification_activityTitle": "MeshCore Aktivität", + "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", + "notification_activityTitle": "MeshCore Aktivität", "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", "@notification_messagesCount": { "placeholders": { @@ -1623,36 +1623,36 @@ "settings_gpxExportChat": "Kontaktstandorte", "settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.", "settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.", - "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", + "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", "settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.", "settings_gpxExportAllContacts": "Alle Kontaktstandorte", "settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren", "settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert", "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!", "map_removeLast": "Letztes Entfernen", - "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", - "map_runTrace": "Pfadverlauf ausführen", - "pathTrace_clearTooltip": "Pfad löschen", + "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", + "map_runTrace": "Pfadverlauf ausführen", + "pathTrace_clearTooltip": "Pfad löschen", "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", - "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", + "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", "scanner_chromeRequired": "Chrome Browser erforderlich", - "scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.", + "scanner_chromeRequiredMessage": "Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", "scanner_enableBluetooth": "Bluetooth aktivieren", "snrIndicator_lastSeen": "Zuletzt gesehen", - "snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater", + "snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater", "chat_ShowAllPaths": "Alle Pfade anzeigen", "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", - "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.", - "settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)", + "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.", + "settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Einheiten", "appSettings_unitsMetric": "Metrisch (m/km)", "appSettings_unitsImperial": "Imperial (ft/mi)", "map_lineOfSight": "Sichtlinie", "map_losScreenTitle": "Sichtlinie", - "losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.", - "losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}", + "losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.", + "losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1660,10 +1660,10 @@ } } }, - "losClearAllPoints": "Löschen Sie alle Punkte", - "losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen", - "losMenuTitle": "LOS-Menü", - "losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen", + "losClearAllPoints": "Löschen Sie alle Punkte", + "losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen", + "losMenuTitle": "LOS-Menü", + "losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen", "losShowDisplayNodes": "Anzeigeknoten anzeigen", "losCustomPoints": "Benutzerdefinierte Punkte", "losCustomPointLabel": "Benutzerdefiniert {index}", @@ -1698,8 +1698,8 @@ } } }, - "losRun": "Führen Sie LOS aus", - "losNoElevationData": "Keine Höhendaten", + "losRun": "Führen Sie LOS aus", + "losNoElevationData": "Keine Höhendaten", "losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { @@ -1734,7 +1734,7 @@ } } }, - "losStatusChecking": "LOS: Überprüfen...", + "losStatusChecking": "LOS: Überprüfen...", "losStatusNoData": "LOS: keine Daten", "losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt", "@losStatusSummary": { @@ -1753,20 +1753,20 @@ } } }, - "losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.", - "losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.", + "losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.", + "losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.", "losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um", "losPointName": "Punktname", "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", - "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Funkhorizont", "losLegendLosBeam": "Sichtlinie", - "losLegendTerrain": "Gelände", + "losLegendTerrain": "Gelände", "losFrequencyLabel": "Frequenz", "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", - "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1830,11 +1830,23 @@ "contacts_searchFavorites": "Suche {number}{str} Favoriten...", "contacts_searchUsers": "Suche {number}{str} Benutzer...", "contacts_searchRoomServers": "Suche {number}{str} Raumserver...", - "connectionChoiceUsbLabel": "USB", + "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", + "usbScreenTitle": "Über USB verbinden", + "usbScreenNote": "USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", + "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", + "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.", + "usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.", + "usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.", + "usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.", + "usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.", + "usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.", + "usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.", + "usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.", + "usbErrorUnsupported": "Die Unterstützung für USB-Seriellschnittstellen ist auf dieser Plattform nicht vorhanden.", + "usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.", + "usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.", + "usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.", + "usbErrorConnectTimedOut": "Die Wartezeit ist abgelaufen, da keine Antwort vom Gerät empfangen wurde.", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", - "usbScreenNote": "USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", - "usbScreenTitle": "Über USB verbinden", - "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", - "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie." + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 58b04aa..cbb0d0d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,4 @@ -{ +{ "@@locale": "en", "appTitle": "MeshCore Open", "nav_contacts": "Contacts", @@ -53,6 +53,18 @@ "usbScreenStatus": "Select a USB device", "usbScreenNote": "USB serial is active on supported Android devices and desktop platforms.", "usbScreenEmptyState": "No USB devices found. Plug one in and refresh.", + "usbErrorPermissionDenied": "USB permission was denied.", + "usbErrorDeviceMissing": "The selected USB device is no longer available.", + "usbErrorInvalidPort": "Select a valid USB device.", + "usbErrorBusy": "Another USB connection request is already in progress.", + "usbErrorNotConnected": "No USB device is connected.", + "usbErrorOpenFailed": "Failed to open the selected USB device.", + "usbErrorConnectFailed": "Failed to connect to the selected USB device.", + "usbErrorUnsupported": "USB serial is not supported on this platform.", + "usbErrorAlreadyActive": "A USB connection is already active.", + "usbErrorNoDeviceSelected": "No USB device was selected.", + "usbErrorPortClosed": "The USB connection is not open.", + "usbErrorConnectTimedOut": "Timed out waiting for the device to respond.", "scanner_scanning": "Scanning for devices...", "scanner_connecting": "Connecting...", "scanner_disconnecting": "Disconnecting...", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 8a11672..b279dd6 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -19,8 +19,8 @@ "common_delete": "Eliminar", "common_close": "Cerrar", "common_edit": "Editar", - "common_add": "Añadir", - "common_settings": "Configuración", + "common_add": "Añadir", + "common_settings": "Configuración", "common_disconnect": "Desconectar", "common_connected": "Conectado", "common_disconnected": "Desconectado", @@ -35,7 +35,7 @@ "common_disable": "Desactivar", "common_reboot": "Reiniciar", "common_loading": "Cargando...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -56,7 +56,7 @@ "scanner_scanning": "Escaneando dispositivos...", "scanner_connecting": "Conectando...", "scanner_disconnecting": "Desconectando...", - "scanner_notConnected": "No está conectado", + "scanner_notConnected": "No está conectado", "scanner_connectedTo": "Conectado a {deviceName}", "@scanner_connectedTo": { "placeholders": { @@ -67,7 +67,7 @@ }, "scanner_searchingDevices": "Buscando dispositivos MeshCore...", "scanner_tapToScan": "Toca Escanear para encontrar dispositivos MeshCore", - "scanner_connectionFailed": "Error de conexión: {error}", + "scanner_connectionFailed": "Error de conexión: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -77,49 +77,49 @@ }, "scanner_stop": "Detener", "scanner_scan": "Escanea", - "device_quickSwitch": "Cambiar rápidamente", + "device_quickSwitch": "Cambiar rápidamente", "device_meshcore": "MeshCore", - "settings_title": "Configuración", - "settings_deviceInfo": "Información del dispositivo", - "settings_appSettings": "Configuración de la App", + "settings_title": "Configuración", + "settings_deviceInfo": "Información del dispositivo", + "settings_appSettings": "Configuración de la App", "settings_appSettingsSubtitle": "Notificaciones, mensajes y preferencias de mapa", - "settings_nodeSettings": "Configuración del Nodo", + "settings_nodeSettings": "Configuración del Nodo", "settings_nodeName": "Nombre del nodo", - "settings_nodeNameNotSet": "No está configurado", + "settings_nodeNameNotSet": "No está configurado", "settings_nodeNameHint": "Introducir nombre de nodo", "settings_nodeNameUpdated": "Nombre actualizado", - "settings_radioSettings": "Configuración de Radio", - "settings_radioSettingsSubtitle": "Frecuencia, potencia, factor de dispersión", + "settings_radioSettings": "Configuración de Radio", + "settings_radioSettingsSubtitle": "Frecuencia, potencia, factor de dispersión", "settings_radioSettingsUpdated": "Ajustes de radio actualizados", - "settings_location": "Ubicación", + "settings_location": "Ubicación", "settings_locationSubtitle": "Coordenadas GPS", - "settings_locationUpdated": "Ubicación actualizada", + "settings_locationUpdated": "Ubicación actualizada", "settings_locationBothRequired": "Introduzca tanto la latitud como la longitud.", - "settings_locationInvalid": "Latitud o longitud inválidos.", + "settings_locationInvalid": "Latitud o longitud inválidos.", "settings_latitude": "Latitud", "settings_longitude": "Longitud", "settings_privacyMode": "Modo Privacidad", - "settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", - "settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.", + "settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", + "settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.", "settings_privacyModeEnabled": "Modo de privacidad activado", "settings_privacyModeDisabled": "Modo de privacidad desactivado", "settings_actions": "Acciones", "settings_sendAdvertisement": "Enviar Anuncio", - "settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora", + "settings_sendAdvertisementSubtitle": "Presencia de transmisión ahora", "settings_advertisementSent": "Anuncio enviado", - "settings_syncTime": "Tiempo de Sincronización", - "settings_syncTimeSubtitle": "Establecer la hora del dispositivo al tiempo del teléfono", + "settings_syncTime": "Tiempo de Sincronización", + "settings_syncTimeSubtitle": "Establecer la hora del dispositivo al tiempo del teléfono", "settings_timeSynchronized": "Sincronizado en el tiempo", "settings_refreshContacts": "Actualizar Contactos", "settings_refreshContactsSubtitle": "Recargar lista de contactos del dispositivo", "settings_rebootDevice": "Reiniciar Dispositivo", "settings_rebootDeviceSubtitle": "Reiniciar el dispositivo MeshCore", - "settings_rebootDeviceConfirm": "¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.", + "settings_rebootDeviceConfirm": "¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.", "settings_debug": "Depurar", - "settings_bleDebugLog": "Registro de Depuración BLE", + "settings_bleDebugLog": "Registro de Depuración BLE", "settings_bleDebugLogSubtitle": "Comandos, respuestas y datos brutos de BLE", - "settings_appDebugLog": "Registro de Depuración de la App", - "settings_appDebugLogSubtitle": "Mensajes de depuración de la aplicación", + "settings_appDebugLog": "Registro de Depuración de la App", + "settings_appDebugLogSubtitle": "Mensajes de depuración de la aplicación", "settings_about": "Acerca de", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "2026 Proyecto Open Source MeshCore", - "settings_aboutDescription": "Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.", + "settings_aboutDescription": "Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.", "settings_infoName": "Nombre", "settings_infoId": "ID", "settings_infoStatus": "Estado", - "settings_infoBattery": "Batería", - "settings_infoPublicKey": "Clave Pública", - "settings_infoContactsCount": "Número de contactos", - "settings_infoChannelCount": "Número de canales", + "settings_infoBattery": "Batería", + "settings_infoPublicKey": "Clave Pública", + "settings_infoContactsCount": "Número de contactos", + "settings_infoChannelCount": "Número de canales", "settings_presets": "Preajustes", "settings_frequency": "Frecuencia (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", - "settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)", + "settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)", "settings_bandwidth": "Ancho de banda", - "settings_spreadingFactor": "Factor de propagación", - "settings_codingRate": "Tasa de Programación", + "settings_spreadingFactor": "Factor de propagación", + "settings_codingRate": "Tasa de Programación", "settings_txPower": "TX Potencia (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)", + "settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)", "settings_error": "Error: {message}", "@settings_error": { "placeholders": { @@ -156,7 +156,7 @@ } } }, - "appSettings_title": "Configuración de la App", + "appSettings_title": "Configuración de la App", "appSettings_appearance": "Apariencia", "appSettings_theme": "Tema", "appSettings_themeSystem": "Valor predeterminado del sistema", @@ -165,42 +165,42 @@ "appSettings_language": "Idioma", "appSettings_languageSystem": "Predeterminado del sistema", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notificaciones", "appSettings_enableNotifications": "Habilitar Notificaciones", "appSettings_enableNotificationsSubtitle": "Recibir notificaciones para mensajes y anuncios", - "appSettings_notificationPermissionDenied": "Permiso de notificación denegado", + "appSettings_notificationPermissionDenied": "Permiso de notificación denegado", "appSettings_notificationsEnabled": "Notificaciones activadas", "appSettings_notificationsDisabled": "Notificaciones desactivadas", "appSettings_messageNotifications": "Notificaciones de Mensaje", - "appSettings_messageNotificationsSubtitle": "Mostrar notificación al recibir nuevos mensajes", + "appSettings_messageNotificationsSubtitle": "Mostrar notificación al recibir nuevos mensajes", "appSettings_channelMessageNotifications": "Notificaciones de Mensajes del Canal", - "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificación al recibir mensajes del canal", + "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificación al recibir mensajes del canal", "appSettings_advertisementNotifications": "Notificaciones de Anuncios", - "appSettings_advertisementNotificationsSubtitle": "Mostrar notificación cuando se descubren nuevos nodos", - "appSettings_messaging": "Mensajería", + "appSettings_advertisementNotificationsSubtitle": "Mostrar notificación cuando se descubren nuevos nodos", + "appSettings_messaging": "Mensajería", "appSettings_clearPathOnMaxRetry": "Borrar Camino en Max Reintentos", - "appSettings_clearPathOnMaxRetrySubtitle": "Restablecer la ruta de contacto después de 5 intentos de envío fallidos", - "appSettings_pathsWillBeCleared": "Los caminos se limpiarán después de 5 intentos fallidos.", - "appSettings_pathsWillNotBeCleared": "Las rutas no se eliminarán automáticamente.", - "appSettings_autoRouteRotation": "Rotación de Ruta Automática", - "appSettings_autoRouteRotationSubtitle": "Alternar entre las mejores rutas y el modo inundación", - "appSettings_autoRouteRotationEnabled": "Rotación de ruta automática habilitada", - "appSettings_autoRouteRotationDisabled": "Rotación de ruta automática desactivada", - "appSettings_battery": "Batería", - "appSettings_batteryChemistry": "Química de la batería", - "appSettings_batteryChemistryPerDevice": "Configuración por dispositivo ({deviceName})", + "appSettings_clearPathOnMaxRetrySubtitle": "Restablecer la ruta de contacto después de 5 intentos de envío fallidos", + "appSettings_pathsWillBeCleared": "Los caminos se limpiarán después de 5 intentos fallidos.", + "appSettings_pathsWillNotBeCleared": "Las rutas no se eliminarán automáticamente.", + "appSettings_autoRouteRotation": "Rotación de Ruta Automática", + "appSettings_autoRouteRotationSubtitle": "Alternar entre las mejores rutas y el modo inundación", + "appSettings_autoRouteRotationEnabled": "Rotación de ruta automática habilitada", + "appSettings_autoRouteRotationDisabled": "Rotación de ruta automática desactivada", + "appSettings_battery": "Batería", + "appSettings_batteryChemistry": "Química de la batería", + "appSettings_batteryChemistryPerDevice": "Configuración por dispositivo ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,11 +208,11 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Conéctate a un dispositivo para elegir", + "appSettings_batteryChemistryConnectFirst": "Conéctate a un dispositivo para elegir", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)", "appSettings_batteryLipo": "LiPo (3.0-4.2V)", - "appSettings_mapDisplay": "Visualización del Mapa", + "appSettings_mapDisplay": "Visualización del Mapa", "appSettings_showRepeaters": "Mostrar Repetidores", "appSettings_showRepeatersSubtitle": "Mostrar nodos de repetidor en el mapa", "appSettings_showChatNodes": "Mostrar Nodos de Chat", @@ -221,7 +221,7 @@ "appSettings_showOtherNodesSubtitle": "Mostrar otros tipos de nodo en el mapa", "appSettings_timeFilter": "Filtro de Tiempo", "appSettings_timeFilterShowAll": "Mostrar todos los nodos", - "appSettings_timeFilterShowLast": "Mostrar nodos de las últimas {hours} horas", + "appSettings_timeFilterShowLast": "Mostrar nodos de las últimas {hours} horas", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -232,13 +232,13 @@ "appSettings_mapTimeFilter": "Filtro de Tiempo del Mapa", "appSettings_showNodesDiscoveredWithin": "Mostrar nodos descubiertos dentro de:", "appSettings_allTime": "Todo el tiempo", - "appSettings_lastHour": "Última hora", - "appSettings_last6Hours": "Últimas 6 horas", - "appSettings_last24Hours": "Últimas 24 horas", + "appSettings_lastHour": "Última hora", + "appSettings_last6Hours": "Últimas 6 horas", + "appSettings_last24Hours": "Últimas 24 horas", "appSettings_lastWeek": "La semana pasada", - "appSettings_offlineMapCache": "Caché de Mapa Offline", - "appSettings_noAreaSelected": "No se ha seleccionado ningún área", - "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", + "appSettings_offlineMapCache": "Caché de Mapa Offline", + "appSettings_noAreaSelected": "No se ha seleccionado ningún área", + "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,13 +250,13 @@ } }, "appSettings_debugCard": "Depurar", - "appSettings_appDebugLogging": "Registro de Depuración de la App", - "appSettings_appDebugLoggingSubtitle": "Registrar mensajes de depuración de la app de registro para solucionar problemas", - "appSettings_appDebugLoggingEnabled": "Registro de depuración de la aplicación habilitado", - "appSettings_appDebugLoggingDisabled": "El registro de depuración de la aplicación está desactivado", + "appSettings_appDebugLogging": "Registro de Depuración de la App", + "appSettings_appDebugLoggingSubtitle": "Registrar mensajes de depuración de la app de registro para solucionar problemas", + "appSettings_appDebugLoggingEnabled": "Registro de depuración de la aplicación habilitado", + "appSettings_appDebugLoggingDisabled": "El registro de depuración de la aplicación está desactivado", "contacts_title": "Contactos", - "contacts_noContacts": "Aún no hay contactos.", - "contacts_contactsWillAppear": "Los contactos aparecerán cuando los dispositivos anuncien.", + "contacts_noContacts": "Aún no hay contactos.", + "contacts_contactsWillAppear": "Los contactos aparecerán cuando los dispositivos anuncien.", "contacts_searchContacts": "Buscar contactos...", "contacts_noUnreadContacts": "No contactos sin leer", "contacts_noContactsFound": "No se encontraron contactos ni grupos.", @@ -296,8 +296,8 @@ "contacts_filterContacts": "Filtrar contactos...", "contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro", "contacts_noMembers": "No miembros", - "contacts_lastSeenNow": "Última vez que se vio ahora", - "contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.", + "contacts_lastSeenNow": "Última vez que se vio ahora", + "contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora", - "contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.", + "contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora", + "contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día", - "contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.", + "contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día", + "contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -325,7 +325,7 @@ }, "channels_title": "Canales", "channels_noChannelsConfigured": "No se han configurado canales", - "channels_addPublicChannel": "Añadir Canal Público", + "channels_addPublicChannel": "Añadir Canal Público", "channels_searchChannels": "Buscar canales...", "channels_noChannelsFound": "No se encontraron canales", "channels_channelIndex": "Canal {index}", @@ -337,9 +337,9 @@ } }, "channels_hashtagChannel": "Canal con hashtag", - "channels_public": "Público", + "channels_public": "Público", "channels_private": "Privado", - "channels_publicChannel": "Canal público", + "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", "channels_muteChannel": "Silenciar canal", @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Añadir Canal", - "channels_channelIndexLabel": "Índice de Canal", + "channels_addChannel": "Añadir Canal", + "channels_channelIndexLabel": "Índice de Canal", "channels_channelName": "Nombre del canal", - "channels_usePublicChannel": "Usar Canal Público", - "channels_standardPublicPsk": "PSK estándar público", + "channels_usePublicChannel": "Usar Canal Público", + "channels_standardPublicPsk": "PSK estándar público", "channels_pskHex": "PSK (Hex)", "channels_generateRandomPsk": "Generar PSK aleatorio", "channels_enterChannelName": "Por favor, introduce un nombre de canal", "channels_pskMustBe32Hex": "PSK debe ser de 32 caracteres hexadecimales.", - "channels_channelAdded": "Canal \"{name}\" añadido", + "channels_channelAdded": "Canal \"{name}\" añadido", "@channels_channelAdded": { "placeholders": { "name": { @@ -386,7 +386,7 @@ } } }, - "channels_smazCompression": "Compresión SMAZ", + "channels_smazCompression": "Compresión SMAZ", "channels_channelUpdated": "Canal \"{name}\" actualizado", "@channels_channelUpdated": { "placeholders": { @@ -395,13 +395,13 @@ } } }, - "channels_publicChannelAdded": "Canal público añadido", + "channels_publicChannelAdded": "Canal público añadido", "channels_sortBy": "Ordenar por", "channels_sortManual": "Manual", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Últimos mensajes", + "channels_sortLatestMessages": "Últimos mensajes", "channels_sortUnread": "Sin leer", - "chat_noMessages": "Aún no hay mensajes", + "chat_noMessages": "Aún no hay mensajes", "chat_sendMessageToStart": "Enviar un mensaje para comenzar", "chat_originalMessageNotFound": "Mensaje original no encontrado", "chat_replyingTo": "Responder a {name}", @@ -420,7 +420,7 @@ } } }, - "chat_location": "Ubicación", + "chat_location": "Ubicación", "chat_sendMessageTo": "Enviar un mensaje a {contactName}", "@chat_sendMessageTo": { "placeholders": { @@ -430,7 +430,7 @@ } }, "chat_typeMessage": "Escribe un mensaje...", - "chat_messageTooLong": "Mensaje demasiado largo (máximo {maxBytes} bytes).", + "chat_messageTooLong": "Mensaje demasiado largo (máximo {maxBytes} bytes).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -440,7 +440,7 @@ }, "chat_messageCopied": "Mensaje copiado", "chat_messageDeleted": "Mensaje borrado", - "chat_retryingMessage": "Reintentando…", + "chat_retryingMessage": "Reintentando…", "chat_retryCount": "Reintentar {current}/{max}", "@chat_retryCount": { "placeholders": { @@ -454,7 +454,7 @@ }, "chat_sendGif": "Enviar GIF", "chat_reply": "Responder", - "chat_addReaction": "Añadir Reacción", + "chat_addReaction": "Añadir Reacción", "chat_me": "Yo", "emojiCategorySmileys": "Emoticones", "emojiCategoryGestures": "Gestos", @@ -466,18 +466,18 @@ "gifPicker_noGifsFound": "No se encontraron GIFs", "gifPicker_failedLoad": "No se pudo cargar los GIFs", "gifPicker_failedSearch": "No se encontraron GIFs", - "gifPicker_noInternet": "No hay conexión a internet", - "debugLog_appTitle": "Registro de Depuración de la App", - "debugLog_bleTitle": "Registro de Depuración BLE", + "gifPicker_noInternet": "No hay conexión a internet", + "debugLog_appTitle": "Registro de Depuración de la App", + "debugLog_bleTitle": "Registro de Depuración BLE", "debugLog_copyLog": "Copiar registro", "debugLog_clearLog": "Borrar registro", - "debugLog_copied": "Registro de depuración copiado", + "debugLog_copied": "Registro de depuración copiado", "debugLog_bleCopied": "Registro BLE copiado", - "debugLog_noEntries": "Aún no hay registros de depuración.", - "debugLog_enableInSettings": "Habilitar el registro de depuración de la aplicación en la configuración", + "debugLog_noEntries": "Aún no hay registros de depuración.", + "debugLog_enableInSettings": "Habilitar el registro de depuración de la aplicación en la configuración", "debugLog_frames": "Marcos", "debugLog_rawLogRx": "Registro Crudo-RX", - "debugLog_noBleActivity": "Aún no hay actividad BLE", + "debugLog_noBleActivity": "Aún no hay actividad BLE", "debugFrame_length": "Longitud del Marco: {count} bytes", "@debugFrame_length": { "placeholders": { @@ -541,12 +541,12 @@ } }, "debugFrame_hexDump": "Mapeo Hexadecimal:", - "chat_pathManagement": "Gestión de Rutas", + "chat_pathManagement": "Gestión de Rutas", "chat_routingMode": "Modo de enrutamiento", "chat_autoUseSavedPath": "Auto (usar la ruta guardada)", - "chat_forceFloodMode": "Modo Inundación Forzado", + "chat_forceFloodMode": "Modo Inundación Forzado", "chat_recentAckPaths": "Rutas de ACK Recientes (tocar para usar):", - "chat_pathHistoryFull": "El historial de rutas está completo. Eliminar entradas para añadir nuevas.", + "chat_pathHistoryFull": "El historial de rutas está completo. Eliminar entradas para añadir nuevas.", "chat_hopSingular": "salta", "chat_hopPlural": "salta", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -557,19 +557,19 @@ } } }, - "chat_successes": "Éxitos", + "chat_successes": "Éxitos", "chat_removePath": "Eliminar ruta", - "chat_noPathHistoryYet": "Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.", + "chat_noPathHistoryYet": "Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.", "chat_pathActions": "Acciones de Ruta:", "chat_setCustomPath": "Establecer Ruta Personalizada", "chat_setCustomPathSubtitle": "Especificar manualmente la ruta de enrutamiento", "chat_clearPath": "Limpiar Ruta", - "chat_clearPathSubtitle": "Forzar redescubrimiento en el próximo envío", - "chat_pathCleared": "Ruta eliminada. El siguiente mensaje redescubrirá la ruta.", + "chat_clearPathSubtitle": "Forzar redescubrimiento en el próximo envío", + "chat_pathCleared": "Ruta eliminada. El siguiente mensaje redescubrirá la ruta.", "chat_floodModeSubtitle": "Utilizar el interruptor de enrutamiento en la barra de herramientas", - "chat_floodModeEnabled": "El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.", + "chat_floodModeEnabled": "El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.", "chat_fullPath": "Ruta completa", - "chat_pathDetailsNotAvailable": "Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.", + "chat_pathDetailsNotAvailable": "Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.", "chat_pathSetHops": "Ruta establecida: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -581,14 +581,14 @@ } } }, - "chat_pathSavedLocally": "Guardado localmente. Conéctate para sincronizar.", + "chat_pathSavedLocally": "Guardado localmente. Conéctate para sincronizar.", "chat_pathDeviceConfirmed": "Dispositivo confirmado.", - "chat_pathDeviceNotConfirmed": "Dispositivo aún no confirmado.", + "chat_pathDeviceNotConfirmed": "Dispositivo aún no confirmado.", "chat_type": "Escribe", "chat_path": "Ruta", - "chat_publicKey": "Clave Pública", + "chat_publicKey": "Clave Pública", "chat_compressOutgoingMessages": "Comprimir mensajes salientes", - "chat_floodForced": "Inundación (forzada)", + "chat_floodForced": "Inundación (forzada)", "chat_directForced": "Directo (forzado)", "chat_hopsForced": "{count} saltos (forzados)", "@chat_hopsForced": { @@ -598,9 +598,9 @@ } } }, - "chat_floodAuto": "Inundación (automática)", + "chat_floodAuto": "Inundación (automática)", "chat_direct": "Guardar", - "chat_poiShared": "Punto de Interés Compartido", + "chat_poiShared": "Punto de Interés Compartido", "chat_unread": "Sin leer: {count}", "@chat_unread": { "placeholders": { @@ -609,8 +609,8 @@ } } }, - "chat_openLink": "¿Abrir enlace?", - "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?", + "chat_openLink": "¿Abrir enlace?", + "chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?", "chat_open": "Abrir", "chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}", "@chat_couldNotOpenLink": { @@ -620,9 +620,9 @@ } } }, - "chat_invalidLink": "Formato de enlace no válido", + "chat_invalidLink": "Formato de enlace no válido", "map_title": "Mapa de Nodos", - "map_noNodesWithLocation": "No hay nodos con datos de ubicación", + "map_noNodesWithLocation": "No hay nodos con datos de ubicación", "map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa", "map_nodesCount": "Nodos: {count}", "@map_nodesCount": { @@ -642,25 +642,25 @@ }, "map_chat": "Chat", "map_repeater": "Repetidor", - "map_room": "Habitación", + "map_room": "Habitación", "map_sensor": "Sensor", "map_pinDm": "Pin (DM)", "map_pinPrivate": "Bloqueo (Privado)", - "map_pinPublic": "Clave (Pública)", - "map_lastSeen": "Última vez que se vio", - "map_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", + "map_pinPublic": "Clave (Pública)", + "map_lastSeen": "Última vez que se vio", + "map_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", "map_from": "De", "map_source": "Fuente", "map_flags": "Banderas", - "map_shareMarkerHere": "Compartir marcador aquí", + "map_shareMarkerHere": "Compartir marcador aquí", "map_pinLabel": "Etiqueta de marcador", "map_label": "Etiqueta", - "map_pointOfInterest": "Punto de interés", + "map_pointOfInterest": "Punto de interés", "map_sendToContact": "Enviar a contacto", "map_sendToChannel": "Enviar a canal", "map_noChannelsAvailable": "No hay canales disponibles", - "map_publicLocationShare": "Compartir ubicación pública", - "map_publicLocationShareConfirm": "Estás a punto de compartir una ubicación en {channelLabel}. Este canal es público y cualquiera con la PSK puede verlo.", + "map_publicLocationShare": "Compartir ubicación pública", + "map_publicLocationShareConfirm": "Estás a punto de compartir una ubicación en {channelLabel}. Este canal es público y cualquiera con la PSK puede verlo.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,7 +668,7 @@ } } }, - "map_connectToShareMarkers": "Conéctate a un dispositivo para compartir marcadores", + "map_connectToShareMarkers": "Conéctate a un dispositivo para compartir marcadores", "map_filterNodes": "Filtrar Nodos", "map_nodeTypes": "Tipos de nodo", "map_chatNodes": "Nodos de Chat", @@ -676,18 +676,18 @@ "map_otherNodes": "Otros Nodos", "map_keyPrefix": "Prefijo de clave", "map_filterByKeyPrefix": "Filtrar por prefijo clave", - "map_publicKeyPrefix": "Prefijo de clave pública", + "map_publicKeyPrefix": "Prefijo de clave pública", "map_markers": "Marcadores", "map_showSharedMarkers": "Mostrar marcadores compartidos", - "map_lastSeenTime": "Última vez que se vio", + "map_lastSeenTime": "Última vez que se vio", "map_sharedPin": "Pin compartido", - "map_joinRoom": "Únete a la sala", + "map_joinRoom": "Únete a la sala", "map_manageRepeater": "Gestionar Repetidor", - "mapCache_title": "Caché de Mapa Offline", - "mapCache_selectAreaFirst": "Seleccionar un área para cachear primero", - "mapCache_noTilesToDownload": "No hay azulejos para descargar para este área.", + "mapCache_title": "Caché de Mapa Offline", + "mapCache_selectAreaFirst": "Seleccionar un área para cachear primero", + "mapCache_noTilesToDownload": "No hay azulejos para descargar para este área.", "mapCache_downloadTilesTitle": "Descargar ficheros", - "mapCache_downloadTilesPrompt": "Descargar {count} ficheros para usar sin conexión?", + "mapCache_downloadTilesPrompt": "Descargar {count} ficheros para usar sin conexión?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -715,11 +715,11 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Borrar caché offline", - "mapCache_clearOfflineCachePrompt": "Eliminar todas las baldosas en caché del mapa?", - "mapCache_offlineCacheCleared": "Almacén en caché sin conexión eliminado", - "mapCache_noAreaSelected": "No se ha seleccionado ningún área", - "mapCache_cacheArea": "Área de Caché", + "mapCache_clearOfflineCacheTitle": "Borrar caché offline", + "mapCache_clearOfflineCachePrompt": "Eliminar todas las baldosas en caché del mapa?", + "mapCache_offlineCacheCleared": "Almacén en caché sin conexión eliminado", + "mapCache_noAreaSelected": "No se ha seleccionado ningún área", + "mapCache_cacheArea": "Área de Caché", "mapCache_useCurrentView": "Usar Vista Actual", "mapCache_zoomRange": "Rango de Zoom", "mapCache_estimatedTiles": "Tiles estimados: {count}", @@ -742,7 +742,7 @@ } }, "mapCache_downloadTilesButton": "Descargar Mosaicos", - "mapCache_clearCacheButton": "Borrar Caché", + "mapCache_clearCacheButton": "Borrar Caché", "mapCache_failedDownloads": "Descargas fallidas: {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} días hace", + "time_daysAgo": "{days} días hace", "@time_daysAgo": { "placeholders": { "days": { @@ -795,8 +795,8 @@ }, "time_hour": "hora", "time_hours": "horas", - "time_day": "día", - "time_days": "días", + "time_day": "día", + "time_days": "días", "time_week": "semana", "time_weeks": "semanas", "time_month": "mes", @@ -804,21 +804,21 @@ "time_minutes": "minutos", "time_allTime": "Todas las veces", "dialog_disconnect": "Desconectar", - "dialog_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", - "login_repeaterLogin": "Iniciar sesión en el Repetidor", + "dialog_disconnectConfirm": "¿Está seguro de que desea desconectarse de este dispositivo?", + "login_repeaterLogin": "Iniciar sesión en el Repetidor", "login_roomLogin": "Inicio de Sala", - "login_password": "Contraseña", - "login_enterPassword": "Introducir contraseña", - "login_savePassword": "Guardar contraseña", - "login_savePasswordSubtitle": "La contraseña se almacenará de forma segura en este dispositivo.", - "login_repeaterDescription": "Ingrese la contraseña del repetidor para acceder a la configuración y el estado.", - "login_roomDescription": "Ingrese la contraseña de la sala para acceder a la configuración y el estado.", + "login_password": "Contraseña", + "login_enterPassword": "Introducir contraseña", + "login_savePassword": "Guardar contraseña", + "login_savePasswordSubtitle": "La contraseña se almacenará de forma segura en este dispositivo.", + "login_repeaterDescription": "Ingrese la contraseña del repetidor para acceder a la configuración y el estado.", + "login_roomDescription": "Ingrese la contraseña de la sala para acceder a la configuración y el estado.", "login_routing": "Enrutamiento", "login_routingMode": "Modo de enrutamiento", "login_autoUseSavedPath": "Auto (usar la ruta guardada)", - "login_forceFloodMode": "Activar Modo Inundación Forzada", + "login_forceFloodMode": "Activar Modo Inundación Forzada", "login_managePaths": "Gestionar Rutas", - "login_login": "Iniciar sesión", + "login_login": "Iniciar sesión", "login_attempt": "Intentar {current}/{max}", "@login_attempt": { "placeholders": { @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", + "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", "common_reload": "Recargar", "common_clear": "Borrar", "path_currentPath": "Ruta actual: {path}", @@ -860,13 +860,13 @@ "path_enterCustomPath": "Introducir Ruta Personalizada", "path_currentPathLabel": "Ruta actual", "path_hexPrefixInstructions": "Introduzca los prefijos hexadecimales de 2 caracteres para cada salto, separados por comas.", - "path_hexPrefixExample": "Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).", + "path_hexPrefixExample": "Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).", "path_labelHexPrefixes": "Prefijos hexadecimales", - "path_helperMaxHops": "Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).", + "path_helperMaxHops": "Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).", "path_selectFromContacts": "O seleccionar de contactos:", "path_noRepeatersFound": "No se encontraron repetidores ni servidores de sala.", "path_customPathsRequire": "Las rutas personalizadas requieren saltos intermedios que pueden transmitir mensajes.", - "path_invalidHexPrefixes": "Prefijos hexadecimales inválidos: {prefixes}", + "path_invalidHexPrefixes": "Prefijos hexadecimales inválidos: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,25 +874,25 @@ } } }, - "path_tooLong": "La ruta es demasiado larga. Se permiten un máximo de 64 saltos.", + "path_tooLong": "La ruta es demasiado larga. Se permiten un máximo de 64 saltos.", "path_setPath": "Establecer Ruta", - "repeater_management": "Gestión de Repetidores", - "repeater_managementTools": "Herramientas de Gestión", + "repeater_management": "Gestión de Repetidores", + "repeater_managementTools": "Herramientas de Gestión", "repeater_status": "Estado", - "repeater_statusSubtitle": "Ver el estado, las estadísticas y los vecinos del repetidor", + "repeater_statusSubtitle": "Ver el estado, las estadísticas y los vecinos del repetidor", "repeater_telemetry": "Telemetry", - "repeater_telemetrySubtitle": "Ver la telemetría de los sensores y las estadísticas del sistema", + "repeater_telemetrySubtitle": "Ver la telemetría de los sensores y las estadísticas del sistema", "repeater_cli": "CLI", "repeater_cliSubtitle": "Enviar comandos al repetidor", - "repeater_settings": "Configuración", - "repeater_settingsSubtitle": "Configurar parámetros del repetidor", + "repeater_settings": "Configuración", + "repeater_settingsSubtitle": "Configurar parámetros del repetidor", "repeater_statusTitle": "Estado del Repetidor", "repeater_routingMode": "Modo de enrutamiento", "repeater_autoUseSavedPath": "Auto (usar la ruta guardada)", - "repeater_forceFloodMode": "Modo Inundación Forzado", - "repeater_pathManagement": "Gestión de rutas", + "repeater_forceFloodMode": "Modo Inundación Forzado", + "repeater_pathManagement": "Gestión de rutas", "repeater_refresh": "Actualizar", - "repeater_statusRequestTimeout": "Solicitud de estado caducó.", + "repeater_statusRequestTimeout": "Solicitud de estado caducó.", "repeater_errorLoadingStatus": "Error al cargar el estado: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -901,23 +901,23 @@ } } }, - "repeater_systemInformation": "Información del sistema", - "repeater_battery": "Batería", - "repeater_clockAtLogin": "Reloj (al inicio de sesión)", + "repeater_systemInformation": "Información del sistema", + "repeater_battery": "Batería", + "repeater_clockAtLogin": "Reloj (al inicio de sesión)", "repeater_uptime": "Tiempo de actividad", "repeater_queueLength": "Longitud de la cola", - "repeater_debugFlags": "Marcadores de Depuración", - "repeater_radioStatistics": "Estadísticas de Radio", - "repeater_lastRssi": "Último RSSI", - "repeater_lastSnr": "Último SNR", + "repeater_debugFlags": "Marcadores de Depuración", + "repeater_radioStatistics": "Estadísticas de Radio", + "repeater_lastRssi": "Último RSSI", + "repeater_lastSnr": "Último SNR", "repeater_noiseFloor": "Nivel de Ruido", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Estadísticas del Paquete", + "repeater_packetStatistics": "Estadísticas del Paquete", "repeater_sent": "Enviado", "repeater_received": "Recibido", "repeater_duplicates": "Duplicados", - "repeater_daysHoursMinsSecs": "{days} días {hours}h {minutes}m {seconds}s", + "repeater_daysHoursMinsSecs": "{days} días {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", + "repeater_packetTxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", + "repeater_packetRxTotal": "Total: {total}, Inundación: {flood}, Directo: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Inundación: {flood}, Directo: {direct}", + "repeater_duplicatesFloodDirect": "Inundación: {flood}, Directo: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,35 +981,35 @@ } } }, - "repeater_settingsTitle": "Configuración del Repetidor", - "repeater_basicSettings": "Configuración Básica", + "repeater_settingsTitle": "Configuración del Repetidor", + "repeater_basicSettings": "Configuración Básica", "repeater_repeaterName": "Nombre del Repetidor", "repeater_repeaterNameHelper": "Mostrar nombre para este repetidor", - "repeater_adminPassword": "Contraseña de Administrador", - "repeater_adminPasswordHelper": "Contraseña de acceso completo", - "repeater_guestPassword": "Contraseña de invitado", - "repeater_guestPasswordHelper": "Acceso de solo lectura con contraseña", - "repeater_radioSettings": "Configuración de Radio", + "repeater_adminPassword": "Contraseña de Administrador", + "repeater_adminPasswordHelper": "Contraseña de acceso completo", + "repeater_guestPassword": "Contraseña de invitado", + "repeater_guestPasswordHelper": "Acceso de solo lectura con contraseña", + "repeater_radioSettings": "Configuración de Radio", "repeater_frequencyMhz": "Frecuencia (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Potencia", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Ancho de banda", - "repeater_spreadingFactor": "Factor de propagación", - "repeater_codingRate": "Tasa de Programación", - "repeater_locationSettings": "Configuración de Ubicación", + "repeater_spreadingFactor": "Factor de propagación", + "repeater_codingRate": "Tasa de Programación", + "repeater_locationSettings": "Configuración de Ubicación", "repeater_latitude": "Latitud", "repeater_latitudeHelper": "Grados decimales (por ejemplo, 37.7749)", "repeater_longitude": "Longitud", "repeater_longitudeHelper": "Grados decimales (por ejemplo, -122.4194)", - "repeater_features": "Características", + "repeater_features": "Características", "repeater_packetForwarding": "Enrutamiento de Paquetes", "repeater_packetForwardingSubtitle": "Habilitar el repetidor para reenviar paquetes", "repeater_guestAccess": "Acceso de Invitados", "repeater_guestAccessSubtitle": "Permitir acceso de invitado en solo lectura", "repeater_privacyMode": "Modo Privacidad", - "repeater_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", - "repeater_advertisementSettings": "Configuración de Anuncios", + "repeater_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", + "repeater_advertisementSettings": "Configuración de Anuncios", "repeater_localAdvertInterval": "Intervalo de Anuncio Local", "repeater_localAdvertIntervalMinutes": "{minutes} minutos", "@repeater_localAdvertIntervalMinutes": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervalo de Anuncio de Inundación", + "repeater_floodAdvertInterval": "Intervalo de Anuncio de Inundación", "repeater_floodAdvertIntervalHours": "{hours} horas", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1032,14 +1032,14 @@ "repeater_dangerZone": "Zona de Peligro", "repeater_rebootRepeater": "Reiniciar Repetidor", "repeater_rebootRepeaterSubtitle": "Reiniciar el dispositivo repetidor", - "repeater_rebootRepeaterConfirm": "¿Está seguro de que desea reiniciar este repetidor?", + "repeater_rebootRepeaterConfirm": "¿Está seguro de que desea reiniciar este repetidor?", "repeater_regenerateIdentityKey": "Regenerar Clave de Identidad", - "repeater_regenerateIdentityKeySubtitle": "Generar nueva pareja de clave pública/privada", - "repeater_regenerateIdentityKeyConfirm": "Esto generará una nueva identidad para el repetidor. Continuar?", + "repeater_regenerateIdentityKeySubtitle": "Generar nueva pareja de clave pública/privada", + "repeater_regenerateIdentityKeyConfirm": "Esto generará una nueva identidad para el repetidor. Continuar?", "repeater_eraseFileSystem": "Borrar Sistema de Archivos", "repeater_eraseFileSystemSubtitle": "Formatear el sistema de archivos del repetidor", - "repeater_eraseFileSystemConfirm": "ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!", - "repeater_eraseSerialOnly": "Borrar solo está disponible a través de la consola serial.", + "repeater_eraseFileSystemConfirm": "ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!", + "repeater_eraseSerialOnly": "Borrar solo está disponible a través de la consola serial.", "repeater_commandSent": "Comando enviado: {command}", "@repeater_commandSent": { "placeholders": { @@ -1058,7 +1058,7 @@ }, "repeater_confirm": "Confirmar", "repeater_settingsSaved": "Guardado de ajustes exitoso", - "repeater_errorSavingSettings": "Error al guardar la configuración: {error}", + "repeater_errorSavingSettings": "Error al guardar la configuración: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,14 +1066,14 @@ } } }, - "repeater_refreshBasicSettings": "Actualizar Configuración Básica", + "repeater_refreshBasicSettings": "Actualizar Configuración Básica", "repeater_refreshRadioSettings": "Actualizar Ajustes de Radio", - "repeater_refreshTxPower": "Actualizar TX de energía", - "repeater_refreshLocationSettings": "Actualizar Configuración de Ubicación", + "repeater_refreshTxPower": "Actualizar TX de energía", + "repeater_refreshLocationSettings": "Actualizar Configuración de Ubicación", "repeater_refreshPacketForwarding": "Actualizar Enrutamiento de Paquetes", "repeater_refreshGuestAccess": "Actualizar Acceso Invitados", "repeater_refreshPrivacyMode": "Actualizar Modo Privacidad", - "repeater_refreshAdvertisementSettings": "Actualizar Configuración de Anuncios", + "repeater_refreshAdvertisementSettings": "Actualizar Configuración de Anuncios", "repeater_refreshed": "{label} actualizado", "@repeater_refreshed": { "placeholders": { @@ -1091,11 +1091,11 @@ } }, "repeater_cliTitle": "Repetidor CLI", - "repeater_debugNextCommand": "Siguiente Comando de Depuración", + "repeater_debugNextCommand": "Siguiente Comando de Depuración", "repeater_commandHelp": "Ayuda", "repeater_clearHistory": "Borrar historial", - "repeater_noCommandsSent": "Aún no se han enviado comandos.", - "repeater_typeCommandOrUseQuick": "Escriba un comando a continuación o use comandos rápidos", + "repeater_noCommandsSent": "Aún no se han enviado comandos.", + "repeater_typeCommandOrUseQuick": "Escriba un comando a continuación o use comandos rápidos", "repeater_enterCommandHint": "Escribir comando...", "repeater_previousCommand": "Comando anterior", "repeater_nextCommand": "Siguiente comando", @@ -1113,77 +1113,77 @@ "repeater_cliQuickGetRadio": "Obtener Radio", "repeater_cliQuickGetTx": "Obtener TX", "repeater_cliQuickNeighbors": "Vecinos", - "repeater_cliQuickVersion": "Versión", + "repeater_cliQuickVersion": "Versión", "repeater_cliQuickAdvertise": "Anunciar", "repeater_cliQuickClock": "Reloj", - "repeater_cliHelpAdvert": "Envía un paquete de publicidad", + "repeater_cliHelpAdvert": "Envía un paquete de publicidad", "repeater_cliHelpReboot": "Reinicia el dispositivo. (ten en cuenta, es normal que aparezca 'Timeout')", - "repeater_cliHelpClock": "Muestra la hora actual según el reloj del dispositivo.", - "repeater_cliHelpPassword": "Establece una nueva contraseña de administrador para el dispositivo.", - "repeater_cliHelpVersion": "Muestra la versión del dispositivo y la fecha de compilación del firmware.", - "repeater_cliHelpClearStats": "Reinicia varios contadores de estadísticas a cero.", + "repeater_cliHelpClock": "Muestra la hora actual según el reloj del dispositivo.", + "repeater_cliHelpPassword": "Establece una nueva contraseña de administrador para el dispositivo.", + "repeater_cliHelpVersion": "Muestra la versión del dispositivo y la fecha de compilación del firmware.", + "repeater_cliHelpClearStats": "Reinicia varios contadores de estadísticas a cero.", "repeater_cliHelpSetAf": "Establece el factor de tiempo de aire.", - "repeater_cliHelpSetTx": "Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).", + "repeater_cliHelpSetTx": "Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).", "repeater_cliHelpSetRepeat": "Habilita o deshabilita el rol del repetidor para este nodo.", - "repeater_cliHelpSetAllowReadOnly": "(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).", - "repeater_cliHelpSetFloodMax": "Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).", - "repeater_cliHelpSetIntThresh": "Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.", - "repeater_cliHelpSetAgcResetInterval": "Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.", - "repeater_cliHelpSetMultiAcks": "Habilita o deshabilita la función de 'ACKs dobles'.", + "repeater_cliHelpSetAllowReadOnly": "(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).", + "repeater_cliHelpSetFloodMax": "Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).", + "repeater_cliHelpSetIntThresh": "Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.", + "repeater_cliHelpSetAgcResetInterval": "Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.", + "repeater_cliHelpSetMultiAcks": "Habilita o deshabilita la función de 'ACKs dobles'.", "repeater_cliHelpSetAdvertInterval": "Establece el intervalo del temporizador en minutos para enviar un paquete de anuncio local (sin salto). Establecer en 0 para desactivarlo.", "repeater_cliHelpSetFloodAdvertInterval": "Establece el intervalo del temporizador en horas para enviar un paquete de anuncio masivo. Establecer en 0 para desactivarlo.", - "repeater_cliHelpSetGuestPassword": "Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")", + "repeater_cliHelpSetGuestPassword": "Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")", "repeater_cliHelpSetName": "Establece el nombre del anuncio.", "repeater_cliHelpSetLat": "Establece la latitud del mapa de publicidad. (grados decimales)", "repeater_cliHelpSetLon": "Establece la longitud del mapa de la publicidad. (grados decimales)", - "repeater_cliHelpSetRadio": "Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.", - "repeater_cliHelpSetRxDelay": "Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.", - "repeater_cliHelpSetTxDelay": "Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).", + "repeater_cliHelpSetRadio": "Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.", + "repeater_cliHelpSetRxDelay": "Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.", + "repeater_cliHelpSetTxDelay": "Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).", "repeater_cliHelpSetDirectTxDelay": "Igual que txdelay, pero para aplicar un retraso aleatorio a la transferencia de paquetes en modo directo.", "repeater_cliHelpSetBridgeEnabled": "Habilitar/Deshabilitar puente.", "repeater_cliHelpSetBridgeDelay": "Establecer retraso antes de retransmitir paquetes.", - "repeater_cliHelpSetBridgeSource": "Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.", + "repeater_cliHelpSetBridgeSource": "Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.", "repeater_cliHelpSetBridgeBaud": "Establecer la velocidad de baudios del enlace serial para los puentes rs232.", "repeater_cliHelpSetBridgeSecret": "Establecer secreto de puente para puentes espnow.", - "repeater_cliHelpSetAdcMultiplier": "Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).", - "repeater_cliHelpTempRadio": "Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).", - "repeater_cliHelpSetPerm": "Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).", + "repeater_cliHelpSetAdcMultiplier": "Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).", + "repeater_cliHelpTempRadio": "Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).", + "repeater_cliHelpSetPerm": "Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).", "repeater_cliHelpGetBridgeType": "Obtiene tipo de puente ninguno, rs232, espnow", "repeater_cliHelpLogStart": "Inicia el registro de paquetes en el sistema de archivos.", "repeater_cliHelpLogStop": "Detener el registro de paquetes al sistema de archivos.", "repeater_cliHelpLogErase": "Elimina los registros del paquete del sistema de archivos.", - "repeater_cliHelpNeighbors": "Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4", + "repeater_cliHelpNeighbors": "Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4", "repeater_cliHelpNeighborRemove": "Elimina la primera entrada coincidente (por prefijo de pubkey (hex)) de la lista de vecinos.", - "repeater_cliHelpRegion": "(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.", - "repeater_cliHelpRegionLoad": "NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.", - "repeater_cliHelpRegionGet": "Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) 'F'\"", - "repeater_cliHelpRegionPut": "Agrega o actualiza una definición de región con el nombre dado.", - "repeater_cliHelpRegionRemove": "Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)", - "repeater_cliHelpRegionAllowf": "Establece el permiso de 'F'lujo para la región dada. ('' para el ámbito global/legado)", - "repeater_cliHelpRegionDenyf": "Elimina el permiso de 'F'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)", - "repeater_cliHelpRegionHome": "Responde con la región 'home' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).", - "repeater_cliHelpRegionHomeSet": "Establece la región 'hogar'.", + "repeater_cliHelpRegion": "(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.", + "repeater_cliHelpRegionLoad": "NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.", + "repeater_cliHelpRegionGet": "Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) 'F'\"", + "repeater_cliHelpRegionPut": "Agrega o actualiza una definición de región con el nombre dado.", + "repeater_cliHelpRegionRemove": "Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)", + "repeater_cliHelpRegionAllowf": "Establece el permiso de 'F'lujo para la región dada. ('' para el ámbito global/legado)", + "repeater_cliHelpRegionDenyf": "Elimina el permiso de 'F'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)", + "repeater_cliHelpRegionHome": "Responde con la región 'home' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).", + "repeater_cliHelpRegionHomeSet": "Establece la región 'hogar'.", "repeater_cliHelpRegionSave": "Persiste la lista/mapa de regiones al almacenamiento.", - "repeater_cliHelpGps": "Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.", + "repeater_cliHelpGps": "Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.", "repeater_cliHelpGpsOnOff": "Activa o desactiva el modo GPS.", "repeater_cliHelpGpsSync": "Sincroniza la hora del nodo con el reloj GPS.", - "repeater_cliHelpGpsSetLoc": "Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.", - "repeater_cliHelpGpsAdvert": "Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias", - "repeater_cliHelpGpsAdvertSet": "Configura la configuración de la publicidad de la ubicación.", + "repeater_cliHelpGpsSetLoc": "Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.", + "repeater_cliHelpGpsAdvert": "Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias", + "repeater_cliHelpGpsAdvertSet": "Configura la configuración de la publicidad de la ubicación.", "repeater_commandsListTitle": "Lista de comandos", - "repeater_commandsListNote": "NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".", + "repeater_commandsListNote": "NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".", "repeater_general": "General", - "repeater_settingsCategory": "Configuración", + "repeater_settingsCategory": "Configuración", "repeater_bridge": "Puente", "repeater_logging": "Registrando", "repeater_neighborsRepeaterOnly": "Vecinos (solo repetidor)", - "repeater_regionManagementRepeaterOnly": "Gestión de Regiones (solo Repetidor)", - "repeater_regionNote": "Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.", - "repeater_gpsManagement": "Gestión de GPS", - "repeater_gpsNote": "Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.", - "telemetry_receivedData": "Datos de Telemetría Recibidos", - "telemetry_requestTimeout": "Solicitud de telemetría ha expirado.", - "telemetry_errorLoading": "Error al cargar la telemetría: {error}", + "repeater_regionManagementRepeaterOnly": "Gestión de Regiones (solo Repetidor)", + "repeater_regionNote": "Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.", + "repeater_gpsManagement": "Gestión de GPS", + "repeater_gpsNote": "Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.", + "telemetry_receivedData": "Datos de Telemetría Recibidos", + "telemetry_requestTimeout": "Solicitud de telemetría ha expirado.", + "telemetry_errorLoading": "Error al cargar la telemetría: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "No hay datos de telemetría disponibles.", + "telemetry_noData": "No hay datos de telemetría disponibles.", "telemetry_channelTitle": "Canal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1200,7 +1200,7 @@ } } }, - "telemetry_batteryLabel": "Batería", + "telemetry_batteryLabel": "Batería", "telemetry_voltageLabel": "Voltaje", "telemetry_mcuTemperatureLabel": "Temperatura del MCU", "telemetry_temperatureLabel": "Temperatura", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1247,14 +1247,14 @@ "channelPath_viewMap": "Ver mapa", "channelPath_otherObservedPaths": "Otros caminos observados", "channelPath_repeaterHops": "Saltos del Repetidor", - "channelPath_noHopDetails": "Los detalles del paquete no están disponibles.", + "channelPath_noHopDetails": "Los detalles del paquete no están disponibles.", "channelPath_messageDetails": "Detalles del mensaje", "channelPath_senderLabel": "Remitente", "channelPath_timeLabel": "Tiempo", "channelPath_repeatsLabel": "Repetir", "channelPath_pathLabel": "Ruta {index}", "channelPath_observedLabel": "Observado", - "channelPath_observedPathTitle": "Ruta observada {index} • {hops}", + "channelPath_observedPathTitle": "Ruta observada {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "No datos de ubicación", + "channelPath_noLocationData": "No datos de ubicación", "channelPath_timeWithDate": "{day}/{month} a las {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,7 +1289,7 @@ } }, "channelPath_unknownPath": "Desconocido", - "channelPath_floodPath": "Inundación", + "channelPath_floodPath": "Inundación", "channelPath_directPath": "Guardar", "channelPath_observedZeroOf": "0 de {total} saltos", "@channelPath_observedZeroOf": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Ruta", "channelPath_observedPathHeader": "Ruta Observada", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1344,7 +1344,7 @@ "channelPath_unknownRepeater": "Repetidor Desconocido", "listFilter_tooltip": "Filtrar y ordenar", "listFilter_sortBy": "Ordenar por", - "listFilter_latestMessages": "Últimos mensajes", + "listFilter_latestMessages": "Últimos mensajes", "listFilter_heardRecently": "Escuchado recientemente", "listFilter_az": "A-Z", "listFilter_filters": "Filtros", @@ -1368,16 +1368,16 @@ "neighbors_errorLoading": "Error al cargar vecinos: {error}", "neighbors_repeatersNeighbors": "Repetidores Vecinos", "neighbors_noData": "No hay datos de vecinos disponibles.", - "channels_joinPrivateChannel": "Únete a un Canal Privado", + "channels_joinPrivateChannel": "Únete a un Canal Privado", "channels_createPrivateChannel": "Crear un Canal Privado", "channels_createPrivateChannelDesc": "Cifrado con una clave secreta.", "channels_joinPrivateChannelDesc": "Introducir manualmente una clave secreta.", - "channels_joinPublicChannel": "Únete al Canal Público", + "channels_joinPublicChannel": "Únete al Canal Público", "channels_joinPublicChannelDesc": "Cualquiera puede unirse a este canal.", - "channels_joinHashtagChannel": "Únete a un Canal con Hashtag", + "channels_joinHashtagChannel": "Únete a un Canal con Hashtag", "channels_joinHashtagChannelDesc": "Cualquiera puede unirse a los canales de hashtag.", - "channels_scanQrCode": "Escanear un Código QR", - "channels_scanQrCodeComingSoon": "Próximamente", + "channels_scanQrCode": "Escanear un Código QR", + "channels_scanQrCodeComingSoon": "Próximamente", "channels_enterHashtag": "Introducir hashtag", "channels_hashtagHint": "ej. #equipo", "@neighbors_unknownContact": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_unknownContact": "Clave pública desconocida {pubkey}", - "neighbors_heardAgo": "Escuchado: {time} hace atrás", + "neighbors_unknownContact": "Clave pública desconocida {pubkey}", + "neighbors_heardAgo": "Escuchado: {time} hace atrás", "settings_locationGPSEnable": "Habilitar GPS", - "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", + "settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.", "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", "settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.", - "contacts_manageRoom": "Gestionar Servidor de Habitación", - "room_management": "Administración del Servidor de Habitación", + "contacts_manageRoom": "Gestionar Servidor de Habitación", + "room_management": "Administración del Servidor de Habitación", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1459,35 +1459,35 @@ } }, "community_create": "Crear Comunidad", - "community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.", + "community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.", "community_title": "Comunidad", - "community_join": "Únete", - "community_joinTitle": "Únete a la comunidad", - "community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?", - "community_scanQr": "Escanear Código QR de la Comunidad", - "community_scanInstructions": "Apunte la cámara a un código QR de la comunidad", - "community_showQr": "Mostrar Código QR", - "community_publicChannel": "Comunidad Pública", + "community_join": "Únete", + "community_joinTitle": "Únete a la comunidad", + "community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?", + "community_scanQr": "Escanear Código QR de la Comunidad", + "community_scanInstructions": "Apunte la cámara a un código QR de la comunidad", + "community_showQr": "Mostrar Código QR", + "community_publicChannel": "Comunidad Pública", "community_hashtagChannel": "Hashtag de la Comunidad", "community_name": "Nombre de la comunidad", "common_ok": "De acuerdo", "community_enterName": "Introducir nombre de comunidad", "community_created": "Comunidad \"{name}\" creada", - "community_joined": "Se unió a la comunidad \"{name}\"", + "community_joined": "Se unió a la comunidad \"{name}\"", "community_qrTitle": "Compartir Comunidad", - "community_qrInstructions": "Escanear este código QR para unirte a {name}", + "community_qrInstructions": "Escanear este código QR para unirte a {name}", "community_hashtagPrivacyHint": "Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad", - "community_invalidQrCode": "Código QR de comunidad no válido", + "community_invalidQrCode": "Código QR de comunidad no válido", "community_alreadyMember": "Ya eres Miembro", "community_alreadyMemberMessage": "Ya eres miembro de \"{name}\".", - "community_addPublicChannel": "Añadir Canal Público de la Comunidad", - "community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.", - "community_noCommunities": "Aún no se han unido comunidades.", - "community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar", + "community_addPublicChannel": "Añadir Canal Público de la Comunidad", + "community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.", + "community_noCommunities": "Aún no se han unido comunidades.", + "community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar", "community_manageCommunities": "Gestionar Comunidades", "community_delete": "Salir de la Comunidad", - "community_deleteConfirm": "¿Salir de \"{name}\"?", - "community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.", + "community_deleteConfirm": "¿Salir de \"{name}\"?", + "community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1496,11 +1496,11 @@ } }, "community_deleted": "Has salido de la comunidad \"{name}\"", - "community_addHashtagChannel": "Añadir Hashtag de la Comunidad", - "community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad", + "community_addHashtagChannel": "Añadir Hashtag de la Comunidad", + "community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad", "community_selectCommunity": "Seleccionar Comunidad", "community_regularHashtag": "Etiqueta de Hashtag Regular", - "community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)", + "community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)", "community_communityHashtag": "Hashtag de la Comunidad", "community_communityHashtagDesc": "Exclusivo para miembros de la comunidad", "community_forCommunity": "Para {name}", @@ -1532,13 +1532,13 @@ } } }, - "community_regenerateSecret": "Regenerar Contraseña Secreta", - "community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.", - "community_secretRegenerated": "Código secreto regenerado para \"{name}\"", + "community_regenerateSecret": "Regenerar Contraseña Secreta", + "community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.", + "community_secretRegenerated": "Código secreto regenerado para \"{name}\"", "community_regenerate": "Regenerar", "community_secretUpdated": "Confidencialidad actualizada para \"{name}\"", - "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"", - "community_updateSecret": "Actualizar Contraseña", + "community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"", + "community_updateSecret": "Actualizar Contraseña", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1546,34 +1546,34 @@ } } }, - "pathTrace_you": "Tú", - "pathTrace_failed": "El trazado de ruta falló.", + "pathTrace_you": "Tú", + "pathTrace_failed": "El trazado de ruta falló.", "pathTrace_refreshTooltip": "Actualizar Path Trace", "contacts_pathTrace": "Rastreo de caminos", "contacts_repeaterPathTrace": "Rastrear ruta al repetidor", "contacts_repeaterPing": "Pingar repetidor", "contacts_ping": "Ping", - "pathTrace_notAvailable": "El trazado de ruta no está disponible.", + "pathTrace_notAvailable": "El trazado de ruta no está disponible.", "contacts_roomPing": "Pingar servidor de sala", - "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", + "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", "contacts_pathTraceTo": "Rastrear ruta a {name}", "contacts_chatTraceRoute": "Ruta de trazado", "appSettings_languageUk": "Ucraniano", - "contacts_clipboardEmpty": "El portapapeles está vacío.", + "contacts_clipboardEmpty": "El portapapeles está vacío.", "appSettings_languageRu": "Ruso", "appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes", "appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes", - "contacts_invalidAdvertFormat": "Datos de contacto no válidos", - "contacts_floodAdvert": "Anuncio de inundación", + "contacts_invalidAdvertFormat": "Datos de contacto no válidos", + "contacts_floodAdvert": "Anuncio de inundación", "contacts_contactImported": "El contacto ha sido importado.", - "contacts_contactImportFailed": "Contacto no se importó correctamente.", + "contacts_contactImportFailed": "Contacto no se importó correctamente.", "contacts_zeroHopAdvert": "Anuncio de Zero Hop", "contacts_ShareContactZeroHop": "Compartir contacto por anuncio", "contacts_ShareContact": "Copiar contacto al Portapapeles", "contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles", "contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles", "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", - "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", + "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", "notification_activityTitle": "Actividad de MeshCore", @@ -1610,46 +1610,46 @@ } }, "notification_receivedNewMessage": "Nuevo mensaje recibido", - "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", + "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", - "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", + "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", "settings_gpxExportNoContacts": "No hay contactos para exportar.", "settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO", "settings_gpxExportError": "Hubo un error al exportar.", - "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.", - "settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.", + "settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.", "settings_gpxExportAll": "Exportar todos los contactos a GPX", - "settings_gpxExportContacts": "Exportar compañeros a GPX", - "settings_gpxExportChat": "Ubicaciones de compañero", + "settings_gpxExportContacts": "Exportar compañeros a GPX", + "settings_gpxExportChat": "Ubicaciones de compañero", "settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala", "settings_gpxExportAllContacts": "Todas las ubicaciones de contactos", "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", - "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación", + "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", + "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación", "pathTrace_clearTooltip": "Borrar ruta", "map_runTrace": "Ejecutar Rastreo de Ruta", "map_tapToAdd": "Pulse en los nodos para agregarlos al camino.", - "map_removeLast": "Eliminar último", + "map_removeLast": "Eliminar último", "map_pathTraceCancelled": "Rastreo de ruta cancelado.", "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", "scanner_chromeRequired": "Navegador Chrome requerido", - "scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.", - "scanner_bluetoothOff": "Bluetooth está desactivado.", + "scanner_chromeRequiredMessage": "Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.", + "scanner_bluetoothOff": "Bluetooth está desactivado.", "scanner_enableBluetooth": "Habilitar Bluetooth", "snrIndicator_nearByRepeaters": "Repetidores cercanos", - "snrIndicator_lastSeen": "Visto por última vez", + "snrIndicator_lastSeen": "Visto por última vez", "chat_ShowAllPaths": "Mostrar todos los caminos", - "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", - "settings_clientRepeat": "Repetir sin conexión", + "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", + "settings_clientRepeat": "Repetir sin conexión", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.", - "settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)", + "settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Unidades", - "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsMetric": "Métrico (m/km)", "appSettings_unitsImperial": "Imperial (pies/millas)", - "map_lineOfSight": "Línea de visión", - "map_losScreenTitle": "Línea de visión", + "map_lineOfSight": "Línea de visión", + "map_losScreenTitle": "Línea de visión", "losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.", - "losRunFailed": "Error en la comprobación de la línea de visión: {error}", + "losRunFailed": "Error en la comprobación de la línea de visión: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1658,10 +1658,10 @@ } }, "losClearAllPoints": "Borrar todos los puntos", - "losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación", - "losMenuTitle": "Menú LOS", + "losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación", + "losMenuTitle": "Menú LOS", "losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados", - "losShowDisplayNodes": "Mostrar nodos de visualización", + "losShowDisplayNodes": "Mostrar nodos de visualización", "losCustomPoints": "Puntos personalizados", "losCustomPointLabel": "Personalizado {index}", "@losCustomPointLabel": { @@ -1696,8 +1696,8 @@ } }, "losRun": "Ejecutar LOS", - "losNoElevationData": "Sin datos de elevación", - "losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}", + "losNoElevationData": "Sin datos de elevación", + "losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1750,20 +1750,20 @@ } } }, - "losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.", - "losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.", + "losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.", + "losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.", "losRenameCustomPoint": "Cambiar el nombre del punto personalizado", "losPointName": "Nombre del punto", "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", - "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Horizonte radioeléctrico", - "losLegendLosBeam": "Línea de visión", + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte radioeléctrico", + "losLegendLosBeam": "Línea de visión", "losLegendTerrain": "Terreno", "losFrequencyLabel": "Frecuencia", - "losFrequencyInfoTooltip": "Ver detalles del cálculo", - "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", - "losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", + "losFrequencyInfoTooltip": "Ver detalles del cálculo", + "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", + "losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1783,7 +1783,7 @@ }, "listFilter_favorites": "Favoritos", "listFilter_removeFromFavorites": "Eliminar de las favoritas", - "listFilter_addToFavorites": "Añadir a favoritos", + "listFilter_addToFavorites": "Añadir a favoritos", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1825,16 +1825,28 @@ } }, "contacts_searchContactsNoNumber": "Buscar contactos...", - "contacts_unread": "No leído", + "contacts_unread": "No leído", "contacts_searchFavorites": "Buscar {number}{str} Favoritos...", "contacts_searchUsers": "Buscar {number}{str} Usuarios...", "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala...", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB", - "usbScreenStatus": "Seleccione un dispositivo USB", - "usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.", "usbScreenTitle": "Conecte mediante USB", - "usbScreenSubtitle": "Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.", - "usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a cargar." + "usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.", + "usbScreenStatus": "Seleccione un dispositivo USB", + "usbScreenSubtitle": "Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.", + "usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a cargar.", + "usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.", + "usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.", + "usbErrorInvalidPort": "Seleccione un dispositivo USB válido.", + "usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.", + "usbErrorNotConnected": "No hay ningún dispositivo USB conectado.", + "usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.", + "usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.", + "usbErrorUnsupported": "La comunicación serial mediante USB no está soportada en esta plataforma.", + "usbErrorAlreadyActive": "La conexión USB ya está activa.", + "usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.", + "usbErrorPortClosed": "La conexión USB no está activa.", + "usbErrorConnectTimedOut": "Se ha producido un error debido a la espera prolongada para recibir una respuesta del dispositivo.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 901f4e6..7265974 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", +{ + "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -20,22 +20,22 @@ "common_close": "Fermer", "common_edit": "Modifier", "common_add": "Ajouter", - "common_settings": "Paramètres", - "common_disconnect": "Déconnecter", - "common_connected": "Connecté", - "common_disconnected": "Déconnecté", - "common_create": "Créer", + "common_settings": "Paramètres", + "common_disconnect": "Déconnecter", + "common_connected": "Connecté", + "common_disconnected": "Déconnecté", + "common_create": "Créer", "common_continue": "Continuer", "common_share": "Partager", "common_copy": "Copier", - "common_retry": "Réessayer", + "common_retry": "Réessayer", "common_hide": "Masquer", "common_remove": "Supprimer", "common_enable": "Activer", - "common_disable": "Désactiver", - "common_reboot": "Redémarrer", + "common_disable": "Désactiver", + "common_reboot": "Redémarrer", "common_loading": "Chargement...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Recherche de périphériques...", + "scanner_scanning": "Recherche de périphériques...", "scanner_connecting": "Connexion en cours...", - "scanner_disconnecting": "Déconnexion...", - "scanner_notConnected": "Non connecté", - "scanner_connectedTo": "Connecté à {deviceName}", + "scanner_disconnecting": "Déconnexion...", + "scanner_notConnected": "Non connecté", + "scanner_connectedTo": "Connecté à {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -67,7 +67,7 @@ }, "scanner_searchingDevices": "Recherche des appareils MeshCore...", "scanner_tapToScan": "Appuyez sur Scanner pour trouver les appareils MeshCore", - "scanner_connectionFailed": "Échec de la connexion : {error}", + "scanner_connectionFailed": "Échec de la connexion : {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,52 +75,52 @@ } } }, - "scanner_stop": "Arrêter", + "scanner_stop": "Arrêter", "scanner_scan": "Scanner", "device_quickSwitch": "Basculement rapide", "device_meshcore": "MeshCore", - "settings_title": "Paramètres", - "settings_deviceInfo": "Informations du périphérique", - "settings_appSettings": "Paramètres de l'application", - "settings_appSettingsSubtitle": "Notifications, messagerie et préférences de carte", - "settings_nodeSettings": "Paramètres du nÅ“ud", - "settings_nodeName": "Nom du nÅ“ud", - "settings_nodeNameNotSet": "Non défini", - "settings_nodeNameHint": "Entrer le nom du nÅ“ud", - "settings_nodeNameUpdated": "Nom mis à jour", - "settings_radioSettings": "Paramètres Radio", - "settings_radioSettingsSubtitle": "Fréquence, puissance, facteur d'espacement", - "settings_radioSettingsUpdated": "Paramètres radio mis à jour", + "settings_title": "Paramètres", + "settings_deviceInfo": "Informations du périphérique", + "settings_appSettings": "Paramètres de l'application", + "settings_appSettingsSubtitle": "Notifications, messagerie et préférences de carte", + "settings_nodeSettings": "Paramètres du nœud", + "settings_nodeName": "Nom du nœud", + "settings_nodeNameNotSet": "Non défini", + "settings_nodeNameHint": "Entrer le nom du nœud", + "settings_nodeNameUpdated": "Nom mis à jour", + "settings_radioSettings": "Paramètres Radio", + "settings_radioSettingsSubtitle": "Fréquence, puissance, facteur d'espacement", + "settings_radioSettingsUpdated": "Paramètres radio mis à jour", "settings_location": "Emplacement", - "settings_locationSubtitle": "Coordonnées GPS", - "settings_locationUpdated": "Emplacement mis à jour", + "settings_locationSubtitle": "Coordonnées GPS", + "settings_locationUpdated": "Emplacement mis à jour", "settings_locationBothRequired": "Entrez la latitude et la longitude.", "settings_locationInvalid": "Latitude ou longitude invalide.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", - "settings_privacyMode": "Mode de confidentialité", + "settings_privacyMode": "Mode de confidentialité", "settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces", - "settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", - "settings_privacyModeEnabled": "Mode de confidentialité activé", - "settings_privacyModeDisabled": "Mode de confidentialité désactivé", + "settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", + "settings_privacyModeEnabled": "Mode de confidentialité activé", + "settings_privacyModeDisabled": "Mode de confidentialité désactivé", "settings_actions": "Actions", "settings_sendAdvertisement": "S'annoncer", - "settings_sendAdvertisementSubtitle": "Présence diffusée maintenant", - "settings_advertisementSent": "Annonce envoyée", + "settings_sendAdvertisementSubtitle": "Présence diffusée maintenant", + "settings_advertisementSent": "Annonce envoyée", "settings_syncTime": "Temps de synchronisation", - "settings_syncTimeSubtitle": "Définir l'heure de l'appareil sur l'heure du téléphone.", + "settings_syncTimeSubtitle": "Définir l'heure de l'appareil sur l'heure du téléphone.", "settings_timeSynchronized": "Synchronisation temporelle", - "settings_refreshContacts": "Rafraîchir les Contacts", + "settings_refreshContacts": "Rafraîchir les Contacts", "settings_refreshContactsSubtitle": "Recharger la liste des contacts depuis l'appareil", - "settings_rebootDevice": "Redémarrer l'appareil", - "settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore", - "settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.", - "settings_debug": "Déboguer", - "settings_bleDebugLog": "Journal de débogage BLE", - "settings_bleDebugLogSubtitle": "Commandes BLE, réponses et données brutes", - "settings_appDebugLog": "Journal de débogage de l'application", - "settings_appDebugLogSubtitle": "Messages de débogage de l'application", - "settings_about": "À propos", + "settings_rebootDevice": "Redémarrer l'appareil", + "settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore", + "settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.", + "settings_debug": "Déboguer", + "settings_bleDebugLog": "Journal de débogage BLE", + "settings_bleDebugLogSubtitle": "Commandes BLE, réponses et données brutes", + "settings_appDebugLog": "Journal de débogage de l'application", + "settings_appDebugLogSubtitle": "Messages de débogage de l'application", + "settings_about": "À propos", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -130,20 +130,20 @@ } }, "settings_aboutLegalese": "Projet MeshCore Open Source 2026", - "settings_aboutDescription": "Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.", + "settings_aboutDescription": "Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.", "settings_infoName": "Nom", "settings_infoId": "ID", - "settings_infoStatus": "État", + "settings_infoStatus": "État", "settings_infoBattery": "Batterie", - "settings_infoPublicKey": "Clé Publique", + "settings_infoPublicKey": "Clé Publique", "settings_infoContactsCount": "Nombre de contacts", "settings_infoChannelCount": "Nombre de canaux", - "settings_presets": "Préréglages", - "settings_frequency": "Fréquence (MHz)", + "settings_presets": "Préréglages", + "settings_frequency": "Fréquence (MHz)", "settings_frequencyHelper": "300,0 - 2 500,0", - "settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)", + "settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)", "settings_bandwidth": "Bande passante", - "settings_spreadingFactor": "Facteur de répartition", + "settings_spreadingFactor": "Facteur de répartition", "settings_codingRate": "Taux de codage", "settings_txPower": "TX Puissance (dBm)", "settings_txPowerHelper": "0 - 22", @@ -156,51 +156,51 @@ } } }, - "appSettings_title": "Paramètres de l'application", + "appSettings_title": "Paramètres de l'application", "appSettings_appearance": "Apparence", - "appSettings_theme": "Thème", - "appSettings_themeSystem": "Défaut système", - "appSettings_themeLight": "Lumière", + "appSettings_theme": "Thème", + "appSettings_themeSystem": "Défaut système", + "appSettings_themeLight": "Lumière", "appSettings_themeDark": "Sombre", "appSettings_language": "Langue", - "appSettings_languageSystem": "Par défaut du système", + "appSettings_languageSystem": "Par défaut du système", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notifications", "appSettings_enableNotifications": "Activer les Notifications", "appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les annonces", - "appSettings_notificationPermissionDenied": "Permission de notification refusée", - "appSettings_notificationsEnabled": "Notifications activées", - "appSettings_notificationsDisabled": "Notifications désactivées", + "appSettings_notificationPermissionDenied": "Permission de notification refusée", + "appSettings_notificationsEnabled": "Notifications activées", + "appSettings_notificationsDisabled": "Notifications désactivées", "appSettings_messageNotifications": "Notifications de Messages", - "appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages", + "appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages", "appSettings_channelMessageNotifications": "Notifications des Messages de Canal", - "appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal", + "appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal", "appSettings_advertisementNotifications": "Notifications d'annonces", - "appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nÅ“uds", + "appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nœuds", "appSettings_messaging": "Messagerie", "appSettings_clearPathOnMaxRetry": "Effacer le chemin sur Max Retry", - "appSettings_clearPathOnMaxRetrySubtitle": "Réinitialiser le chemin de contact après 5 tentatives d'envoi infructueuses", - "appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.", - "appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.", - "appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique", - "appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)", - "appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée", - "appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée", + "appSettings_clearPathOnMaxRetrySubtitle": "Réinitialiser le chemin de contact après 5 tentatives d'envoi infructueuses", + "appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.", + "appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.", + "appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique", + "appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)", + "appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée", + "appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée", "appSettings_battery": "Batterie", "appSettings_batteryChemistry": "Chimie de la batterie", - "appSettings_batteryChemistryPerDevice": "Définir par appareil ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Définir par appareil ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -213,15 +213,15 @@ "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Affichage de la carte", - "appSettings_showRepeaters": "Afficher les répéteurs", - "appSettings_showRepeatersSubtitle": "Afficher les nÅ“uds répéteurs sur la carte", - "appSettings_showChatNodes": "Afficher les nÅ“uds de discussion", - "appSettings_showChatNodesSubtitle": "Afficher les nÅ“uds de chat sur la carte", - "appSettings_showOtherNodes": "Afficher d'autres nÅ“uds", - "appSettings_showOtherNodesSubtitle": "Afficher d'autres types de nÅ“uds sur la carte", + "appSettings_showRepeaters": "Afficher les répéteurs", + "appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte", + "appSettings_showChatNodes": "Afficher les nœuds de discussion", + "appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte", + "appSettings_showOtherNodes": "Afficher d'autres nœuds", + "appSettings_showOtherNodesSubtitle": "Afficher d'autres types de nœuds sur la carte", "appSettings_timeFilter": "Filtre du temps", - "appSettings_timeFilterShowAll": "Afficher tous les nÅ“uds", - "appSettings_timeFilterShowLast": "Afficher les nÅ“uds des {hours} dernières heures", + "appSettings_timeFilterShowAll": "Afficher tous les nœuds", + "appSettings_timeFilterShowLast": "Afficher les nœuds des {hours} dernières heures", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,15 +230,15 @@ } }, "appSettings_mapTimeFilter": "Filtre du Temps de la Carte", - "appSettings_showNodesDiscoveredWithin": "Afficher les nÅ“uds découverts dans :", + "appSettings_showNodesDiscoveredWithin": "Afficher les nœuds découverts dans :", "appSettings_allTime": "Tout le temps", - "appSettings_lastHour": "Dernière heure", - "appSettings_last6Hours": "Dernières 6 heures", - "appSettings_last24Hours": "Dernières 24 heures", - "appSettings_lastWeek": "La semaine dernière", + "appSettings_lastHour": "Dernière heure", + "appSettings_last6Hours": "Dernières 6 heures", + "appSettings_last24Hours": "Dernières 24 heures", + "appSettings_lastWeek": "La semaine dernière", "appSettings_offlineMapCache": "Cache de Carte Hors Ligne", - "appSettings_noAreaSelected": "Aucune zone sélectionnée", - "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Aucune zone sélectionnée", + "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,17 +249,17 @@ } } }, - "appSettings_debugCard": "Déboguer", - "appSettings_appDebugLogging": "Journalisation de débogage de l'application", - "appSettings_appDebugLoggingSubtitle": "Enregistrez les messages de débogage de l'application Log pour le dépannage.", - "appSettings_appDebugLoggingEnabled": "Journalisation de débogage de l'application activée", - "appSettings_appDebugLoggingDisabled": "Le débogage de l'application est désactivé.", + "appSettings_debugCard": "Déboguer", + "appSettings_appDebugLogging": "Journalisation de débogage de l'application", + "appSettings_appDebugLoggingSubtitle": "Enregistrez les messages de débogage de l'application Log pour le dépannage.", + "appSettings_appDebugLoggingEnabled": "Journalisation de débogage de l'application activée", + "appSettings_appDebugLoggingDisabled": "Le débogage de l'application est désactivé.", "contacts_title": "Contacts", - "contacts_noContacts": "Aucun contact trouvé.", - "contacts_contactsWillAppear": "Les contacts apparaîtront lorsque les appareils font leur annonce.", + "contacts_noContacts": "Aucun contact trouvé.", + "contacts_contactsWillAppear": "Les contacts apparaîtront lorsque les appareils font leur annonce.", "contacts_searchContacts": "Rechercher des contacts...", "contacts_noUnreadContacts": "Aucun contact non lu", - "contacts_noContactsFound": "Aucun contact ou groupe trouvé.", + "contacts_noContactsFound": "Aucun contact ou groupe trouvé.", "contacts_deleteContact": "Supprimer le contact", "contacts_removeConfirm": "Supprimer {contactName} des contacts ?", "@contacts_removeConfirm": { @@ -269,7 +269,7 @@ } } }, - "contacts_manageRepeater": "Gérer le répéteur", + "contacts_manageRepeater": "Gérer le répéteur", "contacts_roomLogin": "Connexion Room Server", "contacts_openChat": "Ouverture du Chat", "contacts_editGroup": "Modifier le groupe", @@ -285,7 +285,7 @@ "contacts_newGroup": "Nouveau Groupe", "contacts_groupName": "Nom du groupe", "contacts_groupNameRequired": "Le nom du groupe est obligatoire.", - "contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.", + "contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,7 +294,7 @@ } }, "contacts_filterContacts": "Filtrer les contacts...", - "contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.", + "contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.", "contacts_noMembers": "Aucun membre", "contacts_lastSeenNow": "Vu maintenant", "contacts_lastSeenMinsAgo": "Vu il y a {minutes} minutes", @@ -324,10 +324,10 @@ } }, "channels_title": "Canaux", - "channels_noChannelsConfigured": "Aucun canal configuré", + "channels_noChannelsConfigured": "Aucun canal configuré", "channels_addPublicChannel": "Ajouter un canal public", "channels_searchChannels": "Rechercher des canaux...", - "channels_noChannelsFound": "Aucun canal trouvé", + "channels_noChannelsFound": "Aucun canal trouvé", "channels_channelIndex": "Canal {index}", "@channels_channelIndex": { "placeholders": { @@ -338,14 +338,14 @@ }, "channels_hashtagChannel": "Canal avec hashtag", "channels_public": "Public", - "channels_private": "Privé", + "channels_private": "Privé", "channels_publicChannel": "Canal public", - "channels_privateChannel": "Canal privé", + "channels_privateChannel": "Canal privé", "channels_editChannel": "Modifier le canal", - "channels_muteChannel": "Désactiver les notifications du canal", - "channels_unmuteChannel": "Réactiver les notifications du canal", + "channels_muteChannel": "Désactiver les notifications du canal", + "channels_unmuteChannel": "Réactiver les notifications du canal", "channels_deleteChannel": "Supprimer le canal", - "channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.", + "channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Le canal \"{name}\" a été supprimé", + "channels_channelDeleted": "Le canal \"{name}\" a été supprimé", "@channels_channelDeleted": { "placeholders": { "name": { @@ -367,10 +367,10 @@ "channels_usePublicChannel": "Utiliser le canal public", "channels_standardPublicPsk": "PSK public standard", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Générer une clé de modulation PSK aléatoire", + "channels_generateRandomPsk": "Générer une clé de modulation PSK aléatoire", "channels_enterChannelName": "Veuillez entrer un nom de canal", - "channels_pskMustBe32Hex": "Le PKS doit être composé de 32 caractères hexadécimaux.", - "channels_channelAdded": "Le canal \"{name}\" a été ajouté", + "channels_pskMustBe32Hex": "Le PKS doit être composé de 32 caractères hexadécimaux.", + "channels_channelAdded": "Le canal \"{name}\" a été ajouté", "@channels_channelAdded": { "placeholders": { "name": { @@ -387,7 +387,7 @@ } }, "channels_smazCompression": "Compression SMAZ", - "channels_channelUpdated": "Le canal \"{name}\" a été mis à jour", + "channels_channelUpdated": "Le canal \"{name}\" a été mis à jour", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,16 +395,16 @@ } } }, - "channels_publicChannelAdded": "Le canal public a été ajouté", + "channels_publicChannelAdded": "Le canal public a été ajouté", "channels_sortBy": "Trier par", "channels_sortManual": "Manuel", - "channels_sortAZ": "A à Z", + "channels_sortAZ": "A à Z", "channels_sortLatestMessages": "Derniers messages", "channels_sortUnread": "Non lu", "chat_noMessages": "Aucun message pour le moment.", "chat_sendMessageToStart": "Envoyer un message pour commencer", - "chat_originalMessageNotFound": "Message d'origine non trouvé", - "chat_replyingTo": "Répondre à {name}", + "chat_originalMessageNotFound": "Message d'origine non trouvé", + "chat_replyingTo": "Répondre à {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "Répondre à {name}", + "chat_replyTo": "Répondre à {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,7 +421,7 @@ } }, "chat_location": "Emplacement", - "chat_sendMessageTo": "Envoyer un message à {contactName}", + "chat_sendMessageTo": "Envoyer un message à {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -438,9 +438,9 @@ } } }, - "chat_messageCopied": "Message copié", - "chat_messageDeleted": "Message supprimé", - "chat_retryingMessage": "Tentative de récupération.", + "chat_messageCopied": "Message copié", + "chat_messageDeleted": "Message supprimé", + "chat_retryingMessage": "Tentative de récupération.", "chat_retryCount": "Essai {current}/{max}", "@chat_retryCount": { "placeholders": { @@ -453,32 +453,32 @@ } }, "chat_sendGif": "Envoyer GIF", - "chat_reply": "Répondre", - "chat_addReaction": "Ajouter une Réaction", + "chat_reply": "Répondre", + "chat_addReaction": "Ajouter une Réaction", "chat_me": "Moi", - "emojiCategorySmileys": "Émojis", + "emojiCategorySmileys": "Émojis", "emojiCategoryGestures": "Gestes", - "emojiCategoryHearts": "CÅ“urs", + "emojiCategoryHearts": "Cœurs", "emojiCategoryObjects": "Objets", "gifPicker_title": "Choisir un GIF", "gifPicker_searchHint": "Rechercher des GIF...", - "gifPicker_poweredBy": "Propulsé par GIPHY", - "gifPicker_noGifsFound": "Aucun GIF trouvé", + "gifPicker_poweredBy": "Propulsé par GIPHY", + "gifPicker_noGifsFound": "Aucun GIF trouvé", "gifPicker_failedLoad": "Impossible de charger les GIFs", - "gifPicker_failedSearch": "Recherche de GIFs échouée", + "gifPicker_failedSearch": "Recherche de GIFs échouée", "gifPicker_noInternet": "Aucune connexion internet", - "debugLog_appTitle": "Journal de débogage de l'application", - "debugLog_bleTitle": "Journal de débogage BLE", + "debugLog_appTitle": "Journal de débogage de l'application", + "debugLog_bleTitle": "Journal de débogage BLE", "debugLog_copyLog": "Copier le journal", "debugLog_clearLog": "Effacer le journal", - "debugLog_copied": "Journal de débogage copié", - "debugLog_bleCopied": "Journal BLE copié", - "debugLog_noEntries": "Aucun journal de débogage pour le moment.", - "debugLog_enableInSettings": "Activer le débogage de l'application dans les paramètres", + "debugLog_copied": "Journal de débogage copié", + "debugLog_bleCopied": "Journal BLE copié", + "debugLog_noEntries": "Aucun journal de débogage pour le moment.", + "debugLog_enableInSettings": "Activer le débogage de l'application dans les paramètres", "debugLog_frames": "Cadres", "debugLog_rawLogRx": "Enregistrement brut - RX", - "debugLog_noBleActivity": "Pas d'activité BLE enregistrée pour le moment.", - "debugFrame_length": "Longueur du cadre : {count} octets", + "debugLog_noBleActivity": "Pas d'activité BLE enregistrée pour le moment.", + "debugFrame_length": "Longueur du cadre : {count} octets", "@debugFrame_length": { "placeholders": { "count": { @@ -519,7 +519,7 @@ } } }, - "debugFrame_textType": "- Type de texte : {type} ({label})", + "debugFrame_textType": "- Type de texte : {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -540,13 +540,13 @@ } } }, - "debugFrame_hexDump": "Vidéo de Dump Hexadécimal :", + "debugFrame_hexDump": "Vidéo de Dump Hexadécimal :", "chat_pathManagement": "Gestion des chemins", "chat_routingMode": "Mode de routage", - "chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", - "chat_forceFloodMode": "Mode tout le réseau forcé", - "chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :", - "chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.", + "chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", + "chat_forceFloodMode": "Mode tout le réseau forcé", + "chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :", + "chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.", "chat_hopSingular": "saut", "chat_hopPlural": "sauts", "chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}", @@ -557,20 +557,20 @@ } } }, - "chat_successes": "Succès", + "chat_successes": "Succès", "chat_removePath": "Supprimer le chemin", - "chat_noPathHistoryYet": "Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.", - "chat_pathActions": "Actions du chemin :", - "chat_setCustomPath": "Définir un chemin personnalisé", - "chat_setCustomPathSubtitle": "Spécifier manuellement le chemin de routage", + "chat_noPathHistoryYet": "Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.", + "chat_pathActions": "Actions du chemin :", + "chat_setCustomPath": "Définir un chemin personnalisé", + "chat_setCustomPathSubtitle": "Spécifier manuellement le chemin de routage", "chat_clearPath": "Effacer le chemin", - "chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi", - "chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.", + "chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi", + "chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.", "chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application", - "chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.", + "chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.", "chat_fullPath": "Chemin complet", - "chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.", - "chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.", + "chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Sauvegardé localement. Connectez-vous pour synchroniser.", - "chat_pathDeviceConfirmed": "Appareil confirmé.", - "chat_pathDeviceNotConfirmed": "L'appareil n'a pas encore été confirmé.", + "chat_pathSavedLocally": "Sauvegardé localement. Connectez-vous pour synchroniser.", + "chat_pathDeviceConfirmed": "Appareil confirmé.", + "chat_pathDeviceNotConfirmed": "L'appareil n'a pas encore été confirmé.", "chat_type": "Saisir", "chat_path": "Chemin", - "chat_publicKey": "Clé Publique", + "chat_publicKey": "Clé Publique", "chat_compressOutgoingMessages": "Compresser les messages sortants", - "chat_floodForced": "Tout le réseau (forcée)", - "chat_directForced": "Direct (forcé)", - "chat_hopsForced": "{count} sauts (forcés)", + "chat_floodForced": "Tout le réseau (forcée)", + "chat_directForced": "Direct (forcé)", + "chat_hopsForced": "{count} sauts (forcés)", "@chat_hopsForced": { "placeholders": { "count": { @@ -598,9 +598,9 @@ } } }, - "chat_floodAuto": "Tout le réseau (auto)", + "chat_floodAuto": "Tout le réseau (auto)", "chat_direct": "Afficher", - "chat_poiShared": "Point d'intérêt Partagé", + "chat_poiShared": "Point d'intérêt Partagé", "chat_unread": "Non lu : {count}", "@chat_unread": { "placeholders": { @@ -621,10 +621,10 @@ } }, "chat_invalidLink": "Format de lien invalide", - "map_title": "Carte des nÅ“uds", - "map_noNodesWithLocation": "Aucun nÅ“ud avec des données de localisation", - "map_nodesNeedGps": "Les nÅ“uds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.", - "map_nodesCount": "NÅ“uds : {count}", + "map_title": "Carte des nœuds", + "map_noNodesWithLocation": "Aucun nœud avec des données de localisation", + "map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.", + "map_nodesCount": "Nœuds : {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -641,26 +641,26 @@ } }, "map_chat": "Chat", - "map_repeater": "Répéteur", + "map_repeater": "Répéteur", "map_room": "Salle", "map_sensor": "Capteur", - "map_pinDm": "Clé (DM)", - "map_pinPrivate": "Verrouiller (Privé)", - "map_pinPublic": "Clé (Public)", - "map_lastSeen": "Dernière fois vu", - "map_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", - "map_from": "À partir de", + "map_pinDm": "Clé (DM)", + "map_pinPrivate": "Verrouiller (Privé)", + "map_pinPublic": "Clé (Public)", + "map_lastSeen": "Dernière fois vu", + "map_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", + "map_from": "À partir de", "map_source": "Source", "map_flags": "Drapeaux", "map_shareMarkerHere": "Partager le marqueur ici", - "map_pinLabel": "Étiquete de repin", - "map_label": "Étiquette", - "map_pointOfInterest": "Point d'intérêt", + "map_pinLabel": "Étiquete de repin", + "map_label": "Étiquette", + "map_pointOfInterest": "Point d'intérêt", "map_sendToContact": "Envoyer au contact", "map_sendToChannel": "Envoyer sur le canal", "map_noChannelsAvailable": "Aucun canal disponible", "map_publicLocationShare": "Partager dans un lieu public", - "map_publicLocationShareConfirm": "Vous êtes sur le point de partager un emplacement dans {channelLabel}. Ce canal est public et toute personne disposant de la clé PSK peut le voir.", + "map_publicLocationShareConfirm": "Vous êtes sur le point de partager un emplacement dans {channelLabel}. Ce canal est public et toute personne disposant de la clé PSK peut le voir.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Connectez-vous à un appareil pour partager des marqueurs", - "map_filterNodes": "Filtrer les nÅ“uds", - "map_nodeTypes": "Types de nÅ“uds", - "map_chatNodes": "NÅ“uds de Chat", - "map_repeaters": "Répéteurs", - "map_otherNodes": "Autres nÅ“uds", - "map_keyPrefix": "Préfixe clé", - "map_filterByKeyPrefix": "Filtrer par préfixe de clé", - "map_publicKeyPrefix": "Préfixe de clé publique", + "map_connectToShareMarkers": "Connectez-vous à un appareil pour partager des marqueurs", + "map_filterNodes": "Filtrer les nœuds", + "map_nodeTypes": "Types de nœuds", + "map_chatNodes": "Nœuds de Chat", + "map_repeaters": "Répéteurs", + "map_otherNodes": "Autres nœuds", + "map_keyPrefix": "Préfixe clé", + "map_filterByKeyPrefix": "Filtrer par préfixe de clé", + "map_publicKeyPrefix": "Préfixe de clé publique", "map_markers": "Marqueurs", - "map_showSharedMarkers": "Afficher les marqueurs partagés", - "map_lastSeenTime": "Dernière fois vu", - "map_sharedPin": "Clé partagée", + "map_showSharedMarkers": "Afficher les marqueurs partagés", + "map_lastSeenTime": "Dernière fois vu", + "map_sharedPin": "Clé partagée", "map_joinRoom": "Rejoindre la salle", - "map_manageRepeater": "Gérer le répéteur", + "map_manageRepeater": "Gérer le répéteur", "mapCache_title": "Cache de Carte Hors Ligne", - "mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier", - "mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.", - "mapCache_downloadTilesTitle": "Télécharger les tuiles", - "mapCache_downloadTilesPrompt": "Télécharger {count} tuiles pour un usage hors ligne ?", + "mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier", + "mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.", + "mapCache_downloadTilesTitle": "Télécharger les tuiles", + "mapCache_downloadTilesPrompt": "Télécharger {count} tuiles pour un usage hors ligne ?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,7 +695,7 @@ } } }, - "mapCache_downloadAction": "Télécharger", + "mapCache_downloadAction": "Télécharger", "mapCache_cachedTiles": "Cachez {count} tuiles", "@mapCache_cachedTiles": { "placeholders": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)", + "mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -716,9 +716,9 @@ } }, "mapCache_clearOfflineCacheTitle": "Vider le cache hors ligne", - "mapCache_clearOfflineCachePrompt": "Supprimer toutes les tuiles de carte mises en cache ?", - "mapCache_offlineCacheCleared": "Le cache hors ligne a été effacé.", - "mapCache_noAreaSelected": "Aucune zone sélectionnée", + "mapCache_clearOfflineCachePrompt": "Supprimer toutes les tuiles de carte mises en cache ?", + "mapCache_offlineCacheCleared": "Le cache hors ligne a été effacé.", + "mapCache_noAreaSelected": "Aucune zone sélectionnée", "mapCache_cacheArea": "Zone de cache", "mapCache_useCurrentView": "Utiliser la Vue Actuelle", "mapCache_zoomRange": "Plage de zoom", @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Téléchargé {completed} / {total}", + "mapCache_downloadedTiles": "Téléchargé {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "Télécharger les tuiles", + "mapCache_downloadTilesButton": "Télécharger les tuiles", "mapCache_clearCacheButton": "Vider le Cache", - "mapCache_failedDownloads": "Téléchargements échoués : {count}", + "mapCache_failedDownloads": "Téléchargements échoués : {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -803,21 +803,21 @@ "time_months": "mois", "time_minutes": "minutes", "time_allTime": "Tout le temps", - "dialog_disconnect": "Déconnecter", - "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", - "login_repeaterLogin": "Connexion au répéteur", + "dialog_disconnect": "Déconnecter", + "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", + "login_repeaterLogin": "Connexion au répéteur", "login_roomLogin": "Connexion Room Server", "login_password": "Mot de passe", "login_enterPassword": "Entrez votre mot de passe", "login_savePassword": "Sauvegarder le mot de passe", - "login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.", - "login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.", - "login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.", + "login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.", + "login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.", + "login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.", "login_routing": "Redirection", "login_routingMode": "Mode de routage", - "login_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", - "login_forceFloodMode": "Mode tout le réseau forcé", - "login_managePaths": "Gérer les chemins", + "login_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", + "login_forceFloodMode": "Mode tout le réseau forcé", + "login_managePaths": "Gérer les chemins", "login_login": "Connexion", "login_attempt": "Essayer {current}/{max}", "@login_attempt": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Connexion échouée : {error}", + "login_failed": "Connexion échouée : {error}", "@login_failed": { "placeholders": { "error": { @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.", + "login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.", "common_reload": "Recharger", "common_clear": "Effacer", "path_currentPath": "Chemin actuel : {path}", @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Entrer un chemin personnalisé", + "path_enterCustomPath": "Entrer un chemin personnalisé", "path_currentPathLabel": "Chemin actuel", - "path_hexPrefixInstructions": "Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.", - "path_hexPrefixExample": "Exemple : A1,F2,3C (chaque nÅ“ud utilise le premier octet de sa clé publique).", - "path_labelHexPrefixes": "Préfixes hexadécimaux", - "path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)", - "path_selectFromContacts": "Sélectionner à partir des contacts :", - "path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.", - "path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.", - "path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}", + "path_hexPrefixInstructions": "Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.", + "path_hexPrefixExample": "Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).", + "path_labelHexPrefixes": "Préfixes hexadécimaux", + "path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)", + "path_selectFromContacts": "Sélectionner à partir des contacts :", + "path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.", + "path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.", + "path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,25 +874,25 @@ } } }, - "path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.", - "path_setPath": "Définir le chemin", - "repeater_management": "Gestion des répéteurs", + "path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.", + "path_setPath": "Définir le chemin", + "repeater_management": "Gestion des répéteurs", "repeater_managementTools": "Outils de Gestion", - "repeater_status": "État", - "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur", - "repeater_telemetry": "Télémetrie", - "repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système", + "repeater_status": "État", + "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur", + "repeater_telemetry": "Télémetrie", + "repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Envoyer des commandes au répéteur", - "repeater_settings": "Paramètres", - "repeater_settingsSubtitle": "Configurer les paramètres du répéteur", - "repeater_statusTitle": "État du répéteur", + "repeater_cliSubtitle": "Envoyer des commandes au répéteur", + "repeater_settings": "Paramètres", + "repeater_settingsSubtitle": "Configurer les paramètres du répéteur", + "repeater_statusTitle": "État du répéteur", "repeater_routingMode": "Mode de routage", - "repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", - "repeater_forceFloodMode": "Mode tout le réseau forcé", + "repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", + "repeater_forceFloodMode": "Mode tout le réseau forcé", "repeater_pathManagement": "Gestion des chemins", - "repeater_refresh": "Rafraîchir", - "repeater_statusRequestTimeout": "Demande de statut délai dépassé.", + "repeater_refresh": "Rafraîchir", + "repeater_statusRequestTimeout": "Demande de statut délai dépassé.", "repeater_errorLoadingStatus": "Erreur lors du chargement du statut : {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -901,12 +901,12 @@ } } }, - "repeater_systemInformation": "Informations Système", + "repeater_systemInformation": "Informations Système", "repeater_battery": "Batterie", - "repeater_clockAtLogin": "Horloge (au démarrage)", - "repeater_uptime": "Disponibilité", + "repeater_clockAtLogin": "Horloge (au démarrage)", + "repeater_uptime": "Disponibilité", "repeater_queueLength": "Longueur de la file d'attente", - "repeater_debugFlags": "Marqueurs de débogage", + "repeater_debugFlags": "Marqueurs de débogage", "repeater_radioStatistics": "Statistiques Radio", "repeater_lastRssi": "Dernier RSSI", "repeater_lastSnr": "Dernier SNR", @@ -914,8 +914,8 @@ "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", "repeater_packetStatistics": "Statistiques des paquets", - "repeater_sent": "Envoyé", - "repeater_received": "Reçu", + "repeater_sent": "Envoyé", + "repeater_received": "Reçu", "repeater_duplicates": "Doublons", "repeater_daysHoursMinsSecs": "{days} jours {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", + "repeater_packetTxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", + "repeater_packetRxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Tout le réseau : {flood}, Direct : {direct}", + "repeater_duplicatesFloodDirect": "Tout le réseau : {flood}, Direct : {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,35 +981,35 @@ } } }, - "repeater_settingsTitle": "Paramètres du répéteur", - "repeater_basicSettings": "Paramètres de base", - "repeater_repeaterName": "Nom du répéteur", - "repeater_repeaterNameHelper": "Afficher le nom de ce répéteur", + "repeater_settingsTitle": "Paramètres du répéteur", + "repeater_basicSettings": "Paramètres de base", + "repeater_repeaterName": "Nom du répéteur", + "repeater_repeaterNameHelper": "Afficher le nom de ce répéteur", "repeater_adminPassword": "Mot de passe Administrateur", - "repeater_adminPasswordHelper": "Mot de passe d'accès complet", - "repeater_guestPassword": "Mot de passe invité", - "repeater_guestPasswordHelper": "Accès en lecture seule avec mot de passe", - "repeater_radioSettings": "Paramètres Radio", - "repeater_frequencyMhz": "Fréquence (MHz)", + "repeater_adminPasswordHelper": "Mot de passe d'accès complet", + "repeater_guestPassword": "Mot de passe invité", + "repeater_guestPasswordHelper": "Accès en lecture seule avec mot de passe", + "repeater_radioSettings": "Paramètres Radio", + "repeater_frequencyMhz": "Fréquence (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Puissance", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Bande passante", - "repeater_spreadingFactor": "Facteur de répartition", + "repeater_spreadingFactor": "Facteur de répartition", "repeater_codingRate": "Taux de codage", - "repeater_locationSettings": "Paramètres de localisation", + "repeater_locationSettings": "Paramètres de localisation", "repeater_latitude": "Latitude", - "repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)", + "repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)", "repeater_longitude": "Longitude", - "repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)", - "repeater_features": "Fonctionnalités", + "repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)", + "repeater_features": "Fonctionnalités", "repeater_packetForwarding": "Transfert de paquets", - "repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets", - "repeater_guestAccess": "Accès Invité", - "repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule", - "repeater_privacyMode": "Mode de confidentialité", + "repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets", + "repeater_guestAccess": "Accès Invité", + "repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule", + "repeater_privacyMode": "Mode de confidentialité", "repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces", - "repeater_advertisementSettings": "Paramètres d'annonces", + "repeater_advertisementSettings": "Paramètres d'annonces", "repeater_localAdvertInterval": "Intervalle des annonces Locale (0 saut)", "repeater_localAdvertIntervalMinutes": "{minutes} minutes", "@repeater_localAdvertIntervalMinutes": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervalle des annonces à tout le réseau (flood)", + "repeater_floodAdvertInterval": "Intervalle des annonces à tout le réseau (flood)", "repeater_floodAdvertIntervalHours": "{hours} heures", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées", + "repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées", "repeater_dangerZone": "Zone dangereuse", - "repeater_rebootRepeater": "Redémarrer Répéteur", - "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur", - "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?", - "repeater_regenerateIdentityKey": "Ré générer la clé d'identité", - "repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée", - "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?", - "repeater_eraseFileSystem": "Supprimer le système de fichiers", - "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur", - "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !", - "repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.", - "repeater_commandSent": "Commande envoyée : {command}", + "repeater_rebootRepeater": "Redémarrer Répéteur", + "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur", + "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?", + "repeater_regenerateIdentityKey": "Ré générer la clé d'identité", + "repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée", + "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?", + "repeater_eraseFileSystem": "Supprimer le système de fichiers", + "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur", + "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !", + "repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.", + "repeater_commandSent": "Commande envoyée : {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1057,8 +1057,8 @@ } }, "repeater_confirm": "Confirmer", - "repeater_settingsSaved": "Les paramètres ont été enregistrés avec succès.", - "repeater_errorSavingSettings": "Erreur lors de la sauvegarde des paramètres : {error}", + "repeater_settingsSaved": "Les paramètres ont été enregistrés avec succès.", + "repeater_errorSavingSettings": "Erreur lors de la sauvegarde des paramètres : {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "Rafraîchir les paramètres de base", - "repeater_refreshRadioSettings": "Rafraîchir les paramètres Radio", - "repeater_refreshTxPower": "Rafraîchir la tension TX", - "repeater_refreshLocationSettings": "Rafraîchir les paramètres de localisation", - "repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets", - "repeater_refreshGuestAccess": "Rafraîchir l'accès invité", - "repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité", - "repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres des annonces", - "repeater_refreshed": "{label} rafraîchi", + "repeater_refreshBasicSettings": "Rafraîchir les paramètres de base", + "repeater_refreshRadioSettings": "Rafraîchir les paramètres Radio", + "repeater_refreshTxPower": "Rafraîchir la tension TX", + "repeater_refreshLocationSettings": "Rafraîchir les paramètres de localisation", + "repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets", + "repeater_refreshGuestAccess": "Rafraîchir l'accès invité", + "repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité", + "repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres des annonces", + "repeater_refreshed": "{label} rafraîchi", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Erreur lors du rafraîchissement de {label}", + "repeater_errorRefreshing": "Erreur lors du rafraîchissement de {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1090,14 +1090,14 @@ } } }, - "repeater_cliTitle": "Répéteur CLI", - "repeater_debugNextCommand": "Déboguer Prochaine Commande", + "repeater_cliTitle": "Répéteur CLI", + "repeater_debugNextCommand": "Déboguer Prochaine Commande", "repeater_commandHelp": "Aide", "repeater_clearHistory": "Effacer l'historique", - "repeater_noCommandsSent": "Aucune commande n'a encore été envoyée.", + "repeater_noCommandsSent": "Aucune commande n'a encore été envoyée.", "repeater_typeCommandOrUseQuick": "Saisissez une commande ci-dessous ou utilisez les commandes rapides", "repeater_enterCommandHint": "Entrer la commande...", - "repeater_previousCommand": "Commande précédente", + "repeater_previousCommand": "Commande précédente", "repeater_nextCommand": "Prochaine commande", "repeater_enterCommandFirst": "Entrez d'abord une commande", "repeater_cliCommandFrameTitle": "Frame de commande CLI", @@ -1117,73 +1117,73 @@ "repeater_cliQuickAdvertise": "Publier", "repeater_cliQuickClock": "Horloge", "repeater_cliHelpAdvert": "Envoie un paquet d'annonce", - "repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)", + "repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)", "repeater_cliHelpClock": "Affiche l'heure actuelle par l'horloge de chaque appareil.", - "repeater_cliHelpPassword": "Définit un nouveau mot de passe administrateur pour l'appareil.", - "repeater_cliHelpVersion": "Affiche la version du périphérique et la date de construction du micrologiciel.", - "repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.", - "repeater_cliHelpSetAf": "Définit le facteur de temps d'air.", - "repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).", - "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nÅ“ud.", - "repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", - "repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).", - "repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.", - "repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.", - "repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».", - "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.", - "repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.", - "repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")", - "repeater_cliHelpSetName": "Définit le nom de l'annonce.", - "repeater_cliHelpSetLat": "Définit la latitude de la carte des annonces. (degrés décimaux)", - "repeater_cliHelpSetLon": "Définit la longitude de la carte de l'annonce. (degrés décimaux)", - "repeater_cliHelpSetRadio": "Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.", - "repeater_cliHelpSetRxDelay": "Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.", - "repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).", - "repeater_cliHelpSetDirectTxDelay": "Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.", - "repeater_cliHelpSetBridgeEnabled": "Activer/Désactiver le pont.", - "repeater_cliHelpSetBridgeDelay": "Définir le délai avant de renvoyer les paquets.", - "repeater_cliHelpSetBridgeSource": "Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.", - "repeater_cliHelpSetBridgeBaud": "Définir la vitesse de communication série pour les ponts Rs232.", - "repeater_cliHelpSetBridgeSecret": "Définir le secret du pont pour les ponts espnow.", - "repeater_cliHelpSetAdcMultiplier": "Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).", - "repeater_cliHelpTempRadio": "Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d'origine. (ne sauvegarde pas dans les préférences).", - "repeater_cliHelpSetPerm": "Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).", + "repeater_cliHelpPassword": "Définit un nouveau mot de passe administrateur pour l'appareil.", + "repeater_cliHelpVersion": "Affiche la version du périphérique et la date de construction du micrologiciel.", + "repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.", + "repeater_cliHelpSetAf": "Définit le facteur de temps d'air.", + "repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).", + "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.", + "repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", + "repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).", + "repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.", + "repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.", + "repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».", + "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.", + "repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.", + "repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")", + "repeater_cliHelpSetName": "Définit le nom de l'annonce.", + "repeater_cliHelpSetLat": "Définit la latitude de la carte des annonces. (degrés décimaux)", + "repeater_cliHelpSetLon": "Définit la longitude de la carte de l'annonce. (degrés décimaux)", + "repeater_cliHelpSetRadio": "Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.", + "repeater_cliHelpSetRxDelay": "Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.", + "repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).", + "repeater_cliHelpSetDirectTxDelay": "Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.", + "repeater_cliHelpSetBridgeEnabled": "Activer/Désactiver le pont.", + "repeater_cliHelpSetBridgeDelay": "Définir le délai avant de renvoyer les paquets.", + "repeater_cliHelpSetBridgeSource": "Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.", + "repeater_cliHelpSetBridgeBaud": "Définir la vitesse de communication série pour les ponts Rs232.", + "repeater_cliHelpSetBridgeSecret": "Définir le secret du pont pour les ponts espnow.", + "repeater_cliHelpSetAdcMultiplier": "Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).", + "repeater_cliHelpTempRadio": "Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d'origine. (ne sauvegarde pas dans les préférences).", + "repeater_cliHelpSetPerm": "Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).", "repeater_cliHelpGetBridgeType": "Obtenir le type de pont : aucun, rs232, espnow", - "repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.", - "repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.", - "repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.", - "repeater_cliHelpNeighbors": "Affiche une liste d'autres nÅ“uds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", - "repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.", - "repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).", - "repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.", - "repeater_cliHelpRegionGet": "Recherche la région avec le préfixe de nom donné (ou \"\" pour l'étendue globale). Répond avec \"-> nom-de-région (nom-parent) 'F'\"", - "repeater_cliHelpRegionPut": "Ajoute ou met à jour une définition de région avec le nom donné.", - "repeater_cliHelpRegionRemove": "Supprime une définition de région avec le nom donné.", - "repeater_cliHelpRegionAllowf": "Définit les autorisations de \"Flot\" pour la région donnée. ('' pour la portée globale/héritée)", - "repeater_cliHelpRegionDenyf": "Supprime l'autorisation 'F'lood' pour la région donnée. (NOTE : à ce stade, il n'est pas conseillé de l'utiliser sur l'étendue globale/héritée !! )", - "repeater_cliHelpRegionHome": "Répond avec la région 'maison' actuelle. (Note appliquée nulle part pour l'instant, réservée à une utilisation future)", - "repeater_cliHelpRegionHomeSet": "Définit la région 'maison'.", - "repeater_cliHelpRegionSave": "Conserve la liste/la carte des régions dans le stockage.", - "repeater_cliHelpGps": "Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.", - "repeater_cliHelpGpsOnOff": "Activer/désactiver le GPS.", - "repeater_cliHelpGpsSync": "Synchronise l'heure du nÅ“ud avec l'horloge GPS.", - "repeater_cliHelpGpsSetLoc": "Définit la position du nÅ“ud aux coordonnées GPS et enregistre les préférences.", - "repeater_cliHelpGpsAdvert": "Donne la configuration de l'annonce de la localisation du nÅ“ud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences", - "repeater_cliHelpGpsAdvertSet": "Définit la configuration de l'annonce de localisation.", + "repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.", + "repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.", + "repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.", + "repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", + "repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.", + "repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).", + "repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.", + "repeater_cliHelpRegionGet": "Recherche la région avec le préfixe de nom donné (ou \"\" pour l'étendue globale). Répond avec \"-> nom-de-région (nom-parent) 'F'\"", + "repeater_cliHelpRegionPut": "Ajoute ou met à jour une définition de région avec le nom donné.", + "repeater_cliHelpRegionRemove": "Supprime une définition de région avec le nom donné.", + "repeater_cliHelpRegionAllowf": "Définit les autorisations de \"Flot\" pour la région donnée. ('' pour la portée globale/héritée)", + "repeater_cliHelpRegionDenyf": "Supprime l'autorisation 'F'lood' pour la région donnée. (NOTE : à ce stade, il n'est pas conseillé de l'utiliser sur l'étendue globale/héritée !! )", + "repeater_cliHelpRegionHome": "Répond avec la région 'maison' actuelle. (Note appliquée nulle part pour l'instant, réservée à une utilisation future)", + "repeater_cliHelpRegionHomeSet": "Définit la région 'maison'.", + "repeater_cliHelpRegionSave": "Conserve la liste/la carte des régions dans le stockage.", + "repeater_cliHelpGps": "Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.", + "repeater_cliHelpGpsOnOff": "Activer/désactiver le GPS.", + "repeater_cliHelpGpsSync": "Synchronise l'heure du nœud avec l'horloge GPS.", + "repeater_cliHelpGpsSetLoc": "Définit la position du nœud aux coordonnées GPS et enregistre les préférences.", + "repeater_cliHelpGpsAdvert": "Donne la configuration de l'annonce de la localisation du nœud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences", + "repeater_cliHelpGpsAdvertSet": "Définit la configuration de l'annonce de localisation.", "repeater_commandsListTitle": "Liste des commandes", - "repeater_commandsListNote": "NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...", - "repeater_general": "Général", - "repeater_settingsCategory": "Paramètres", + "repeater_commandsListNote": "NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...", + "repeater_general": "Général", + "repeater_settingsCategory": "Paramètres", "repeater_bridge": "Pont", "repeater_logging": "Journalisation", - "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)", - "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)", - "repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.", + "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)", + "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)", + "repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.", "repeater_gpsManagement": "Gestion GPS", - "repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.", - "telemetry_receivedData": "Données de télémétrie reçues", - "telemetry_requestTimeout": "Demande de télémétrie expirée.", - "telemetry_errorLoading": "Erreur lors du chargement de la télémétrie : {error}", + "repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.", + "telemetry_receivedData": "Données de télémétrie reçues", + "telemetry_requestTimeout": "Demande de télémétrie expirée.", + "telemetry_errorLoading": "Erreur lors du chargement de la télémétrie : {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Aucune donnée de télémétrie disponible.", + "telemetry_noData": "Aucune donnée de télémétrie disponible.", "telemetry_channelTitle": "Canal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1202,8 +1202,8 @@ }, "telemetry_batteryLabel": "Batterie", "telemetry_voltageLabel": "Tension", - "telemetry_mcuTemperatureLabel": "Température du MCU", - "telemetry_temperatureLabel": "Température", + "telemetry_mcuTemperatureLabel": "Température du MCU", + "telemetry_temperatureLabel": "Température", "telemetry_currentLabel": "Actuellement", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1245,16 +1245,16 @@ }, "channelPath_title": "Chemin de paquet", "channelPath_viewMap": "Afficher la carte", - "channelPath_otherObservedPaths": "Autres chemins observés", - "channelPath_repeaterHops": "Sauts du répéteur", - "channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.", - "channelPath_messageDetails": "Détails du message", - "channelPath_senderLabel": "Expéditeur", + "channelPath_otherObservedPaths": "Autres chemins observés", + "channelPath_repeaterHops": "Sauts du répéteur", + "channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.", + "channelPath_messageDetails": "Détails du message", + "channelPath_senderLabel": "Expéditeur", "channelPath_timeLabel": "Temps", - "channelPath_repeatsLabel": "Répétitions", + "channelPath_repeatsLabel": "Répétitions", "channelPath_pathLabel": "Chemin {index}", - "channelPath_observedLabel": "Observé", - "channelPath_observedPathTitle": "Chemin observé {index} • {hops}", + "channelPath_observedLabel": "Observé", + "channelPath_observedPathTitle": "Chemin observé {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Aucune donnée de localisation", + "channelPath_noLocationData": "Aucune donnée de localisation", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,7 +1289,7 @@ } }, "channelPath_unknownPath": "Inconnu", - "channelPath_floodPath": "Tout le réseau", + "channelPath_floodPath": "Tout le réseau", "channelPath_directPath": "Afficher", "channelPath_observedZeroOf": "0 de {total} sauts", "@channelPath_observedZeroOf": { @@ -1311,7 +1311,7 @@ } }, "channelPath_mapTitle": "Carte du chemin", - "channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.", + "channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.", "channelPath_primaryPath": "Chemin {index} (Principal)", "@channelPath_primaryPath": { "placeholders": { @@ -1328,8 +1328,8 @@ } }, "channelPath_pathLabelTitle": "Chemin", - "channelPath_observedPathHeader": "Chemin observé", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_observedPathHeader": "Chemin observé", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,17 +1340,17 @@ } } }, - "channelPath_noHopDetailsAvailable": "Aucun détail de saut disponible pour ce paquet.", - "channelPath_unknownRepeater": "Répéteur Inconnu", + "channelPath_noHopDetailsAvailable": "Aucun détail de saut disponible pour ce paquet.", + "channelPath_unknownRepeater": "Répéteur Inconnu", "listFilter_tooltip": "Filtrer et trier", "listFilter_sortBy": "Trier par", "listFilter_latestMessages": "Derniers messages", - "listFilter_heardRecently": "Écoute récemment", - "listFilter_az": "A à Z", + "listFilter_heardRecently": "Écoute récemment", + "listFilter_az": "A à Z", "listFilter_filters": "Filtres", "listFilter_all": "Tout", "listFilter_users": "Utilisateurs", - "listFilter_repeaters": "Répéteurs", + "listFilter_repeaters": "Répéteurs", "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Messages non lus seulement", "listFilter_newGroup": "Nouveau groupe", @@ -1363,21 +1363,21 @@ }, "repeater_neighbors": "Voisins", "repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.", - "neighbors_receivedData": "Données des voisins reçues", - "neighbors_requestTimedOut": "Les voisins demandent un délai.", + "neighbors_receivedData": "Données des voisins reçues", + "neighbors_requestTimedOut": "Les voisins demandent un délai.", "neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}", - "neighbors_repeatersNeighbors": "Répéteurs Voisins", - "neighbors_noData": "Aucune donnée concernant les voisins disponible.", - "channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.", - "channels_joinPrivateChannel": "Rejoindre un Canal Privé", - "channels_createPrivateChannel": "Créer un Canal Privé", - "channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.", + "neighbors_repeatersNeighbors": "Répéteurs Voisins", + "neighbors_noData": "Aucune donnée concernant les voisins disponible.", + "channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.", + "channels_joinPrivateChannel": "Rejoindre un Canal Privé", + "channels_createPrivateChannel": "Créer un Canal Privé", + "channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.", "channels_joinPublicChannel": "Rejoindre le canal public", "channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.", "channels_joinHashtagChannel": "Rejoindre un Canal Hashtag", "channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.", "channels_scanQrCode": "Scanner un code QR", - "channels_scanQrCodeComingSoon": "Bientôt disponible", + "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", "channels_hashtagHint": "ex. #equipe", "@neighbors_unknownContact": { @@ -1394,13 +1394,13 @@ } } }, - "neighbors_unknownContact": "Clé publique inconnue {pubkey}", - "neighbors_heardAgo": "Écouté : {time} auparavant", + "neighbors_unknownContact": "Clé publique inconnue {pubkey}", + "neighbors_heardAgo": "Écouté : {time} auparavant", "settings_locationGPSEnable": "Activer le GPS", - "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS", - "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", - "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", - "contacts_manageRoom": "Gérer le Room Server", + "settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS", + "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", + "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", + "contacts_manageRoom": "Gérer le Room Server", "room_management": "Administrattion Room Server", "@community_joinConfirmation": { "placeholders": { @@ -1459,35 +1459,35 @@ } }, "common_ok": "OK", - "community_title": "Communauté", - "community_create": "Créer une Communauté", - "community_createDesc": "Créer une nouvelle communauté et la partager via QR code.", + "community_title": "Communauté", + "community_create": "Créer une Communauté", + "community_createDesc": "Créer une nouvelle communauté et la partager via QR code.", "community_join": "Rejoindre", - "community_joinTitle": "Rejoindre la communauté", - "community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?", - "community_scanQr": "Scanner la communauté QR", + "community_joinTitle": "Rejoindre la communauté", + "community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?", + "community_scanQr": "Scanner la communauté QR", "community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.", "community_showQr": "Afficher le QR Code", - "community_publicChannel": "Communauté Publique", - "community_hashtagChannel": "Hashtag Communauté", - "community_name": "Nom de la communauté", - "community_enterName": "Entrez le nom de la communauté", - "community_created": "Communauté \"{name}\" créée", - "community_joined": "Rejoint la communauté \"{name}\"", - "community_qrTitle": "Partager Communauté", + "community_publicChannel": "Communauté Publique", + "community_hashtagChannel": "Hashtag Communauté", + "community_name": "Nom de la communauté", + "community_enterName": "Entrez le nom de la communauté", + "community_created": "Communauté \"{name}\" créée", + "community_joined": "Rejoint la communauté \"{name}\"", + "community_qrTitle": "Partager Communauté", "community_qrInstructions": "Scanner ce QR code pour rejoindre {name}", - "community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté", - "community_invalidQrCode": "Code QR de communauté non valide", - "community_alreadyMember": "Déjà membre", - "community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".", - "community_addPublicChannel": "Ajouter un Canal Public de la Communauté", - "community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté", - "community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.", - "community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer", - "community_manageCommunities": "Gérer les Communautés", - "community_delete": "Quitter la communauté", + "community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté", + "community_invalidQrCode": "Code QR de communauté non valide", + "community_alreadyMember": "Déjà membre", + "community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".", + "community_addPublicChannel": "Ajouter un Canal Public de la Communauté", + "community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté", + "community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.", + "community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer", + "community_manageCommunities": "Gérer les Communautés", + "community_delete": "Quitter la communauté", "community_deleteConfirm": "Quitter \"{name}\" ?", - "community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.", + "community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Communauté \"{name}\" quittée", - "community_addHashtagChannel": "Ajouter un Hashtag Communauté", - "community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté", - "community_selectCommunity": "Sélectionner Communauté", - "community_regularHashtag": "Hashtag régulier", + "community_deleted": "Communauté \"{name}\" quittée", + "community_addHashtagChannel": "Ajouter un Hashtag Communauté", + "community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté", + "community_selectCommunity": "Sélectionner Communauté", + "community_regularHashtag": "Hashtag régulier", "community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)", - "community_communityHashtag": "Hashtag de la communauté", - "community_communityHashtagDesc": "Exclusif aux membres de la communauté", + "community_communityHashtag": "Hashtag de la communauté", + "community_communityHashtagDesc": "Exclusif aux membres de la communauté", "community_forCommunity": "Pour {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1532,13 +1532,13 @@ } } }, - "community_regenerateSecret": "Régénérer le secret", - "community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.", - "community_regenerate": "Régénérer", - "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"", - "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"", - "community_updateSecret": "Mettre à jour le secret", - "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"", + "community_regenerateSecret": "Régénérer le secret", + "community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.", + "community_regenerate": "Régénérer", + "community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"", + "community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"", + "community_updateSecret": "Mettre à jour le secret", + "community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1548,80 +1548,80 @@ }, "pathTrace_you": "Vous", "pathTrace_refreshTooltip": "Actualiser Path Trace", - "pathTrace_failed": "Traçage du chemin échoué.", - "pathTrace_notAvailable": "Tracé de chemin non disponible.", - "contacts_pathTrace": "Traçage de chemin", - "contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur", - "contacts_repeaterPing": "Pinguer le répéteur", - "contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle", + "pathTrace_failed": "Traçage du chemin échoué.", + "pathTrace_notAvailable": "Tracé de chemin non disponible.", + "contacts_pathTrace": "Traçage de chemin", + "contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur", + "contacts_repeaterPing": "Pinguer le répéteur", + "contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle", "contacts_chatTraceRoute": "Tracer le chemin", - "contacts_pathTraceTo": "Tracer l'itinéraire vers {name}", + "contacts_pathTraceTo": "Tracer l'itinéraire vers {name}", "contacts_ping": "Ping", "contacts_roomPing": "Pinguer le serveur de la salle", - "contacts_invalidAdvertFormat": "Données de contact non valides", + "contacts_invalidAdvertFormat": "Données de contact non valides", "appSettings_languageUk": "Ukrainien", "appSettings_languageRu": "Russe", - "appSettings_enableMessageTracing": "Activer le traçage des messages", - "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", + "appSettings_enableMessageTracing": "Activer le traçage des messages", + "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", "contacts_clipboardEmpty": "Le presse-papiers est vide.", - "contacts_contactImported": "Le contact a été importé.", - "contacts_floodAdvert": "Annonce à tout le réseau", - "contacts_contactImportFailed": "Échec de l'importation du contact.", + "contacts_contactImported": "Le contact a été importé.", + "contacts_floodAdvert": "Annonce à tout le réseau", + "contacts_contactImportFailed": "Échec de l'importation du contact.", "contacts_zeroHopAdvert": "Annonce Zero saut", "contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers", "contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers", "contacts_ShareContact": "Copier le contact dans le presse-papiers", "contacts_ShareContactZeroHop": "Partager un contact par annonce", - "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", - "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", + "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", + "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", - "notification_activityTitle": "Activité MeshCore", + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", + "notification_activityTitle": "Activité MeshCore", "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", "notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}", - "notification_newNodesCount": "{count} {count, plural, =1{nouveau nÅ“ud} other{nouveaux nÅ“uds}}", - "notification_newTypeDiscovered": "Nouveau {contactType} découvert", - "notification_receivedNewMessage": "Nouveau message reçu", - "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", - "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", - "settings_gpxExportNoContacts": "Aucun contact à exporter.", - "settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation", + "notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}", + "notification_newTypeDiscovered": "Nouveau {contactType} découvert", + "notification_receivedNewMessage": "Nouveau message reçu", + "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", + "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", + "settings_gpxExportNoContacts": "Aucun contact à exporter.", + "settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation", "settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.", - "settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle", + "settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle", "settings_gpxExportContacts": "Exporter les compagnons au format GPX", "settings_gpxExportAll": "Exporter tous les contacts au format GPX", "settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.", "settings_gpxExportContactsSubtitle": "Exporte les compagnons avec un emplacement vers un fichier GPX.", "settings_gpxExportChat": "Emplacements des compagnons", - "settings_gpxExportSuccess": "Fichier GPX exporté avec succès.", + "settings_gpxExportSuccess": "Fichier GPX exporté avec succès.", "settings_gpxExportAllContacts": "Tous les emplacements des contacts", - "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", + "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !", - "map_tapToAdd": "Appuyez sur les nÅ“uds pour les ajouter au chemin.", + "map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.", "pathTrace_clearTooltip": "Effacer le chemin", - "map_pathTraceCancelled": "Traçage de chemin annulé", + "map_pathTraceCancelled": "Traçage de chemin annulé", "map_removeLast": "Supprimer le dernier", - "map_runTrace": "Exécuter la traçage de chemin", + "map_runTrace": "Exécuter la traçage de chemin", "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", "scanner_chromeRequired": "Navigateur Chrome requis", - "scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.", - "scanner_bluetoothOff": "Le Bluetooth est désactivé.", + "scanner_chromeRequiredMessage": "Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.", + "scanner_bluetoothOff": "Le Bluetooth est désactivé.", "scanner_enableBluetooth": "Activer le Bluetooth", - "snrIndicator_lastSeen": "Dernière fois vu", - "snrIndicator_nearByRepeaters": "Répéteurs à proximité", + "snrIndicator_lastSeen": "Dernière fois vu", + "snrIndicator_nearByRepeaters": "Répéteurs à proximité", "chat_ShowAllPaths": "Afficher tous les chemins", - "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", - "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", - "settings_clientRepeat": "Répétition hors réseau", - "settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "Unités", - "appSettings_unitsMetric": "Métrique (m/km)", - "appSettings_unitsImperial": "Impérial (ft / mi)", + "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", + "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", + "settings_clientRepeat": "Répétition hors réseau", + "settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unités", + "appSettings_unitsMetric": "Métrique (m/km)", + "appSettings_unitsImperial": "Impérial (ft / mi)", "map_lineOfSight": "Ligne de vue", "map_losScreenTitle": "Ligne de vue", - "losSelectStartEnd": "Sélectionnez les nÅ“uds de début et de fin pour LOS.", - "losRunFailed": "Échec de la vérification de la ligne de vue : {error}", + "losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.", + "losRunFailed": "Échec de la vérification de la ligne de vue : {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1630,12 +1630,12 @@ } }, "losClearAllPoints": "Effacer tous les points", - "losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude", + "losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Appuyez sur les nÅ“uds ou appuyez longuement sur la carte pour des points personnalisés", - "losShowDisplayNodes": "Afficher les nÅ“uds d'affichage", - "losCustomPoints": "Points personnalisés", - "losCustomPointLabel": "Personnalisé {index}", + "losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés", + "losShowDisplayNodes": "Afficher les nœuds d'affichage", + "losCustomPoints": "Points personnalisés", + "losCustomPointLabel": "Personnalisé {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1645,7 +1645,7 @@ }, "losPointA": "Point A", "losPointB": "Point B", - "losAntennaA": "Antenne A : {value} {unit}", + "losAntennaA": "Antenne A : {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Antenne B : {value} {unit}", + "losAntennaB": "Antenne B : {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1667,8 +1667,8 @@ } } }, - "losRun": "Exécuter la LOS", - "losNoElevationData": "Aucune donnée d'altitude", + "losRun": "Exécuter la LOS", + "losNoElevationData": "Aucune donnée d'altitude", "losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1703,9 +1703,9 @@ } } }, - "losStatusChecking": "LOS : vérification...", - "losStatusNoData": "LOS : aucune donnée", - "losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu", + "losStatusChecking": "LOS : vérification...", + "losStatusNoData": "LOS : aucune donnée", + "losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.", - "losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.", - "losRenameCustomPoint": "Renommer le point personnalisé", + "losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.", + "losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.", + "losRenameCustomPoint": "Renommer le point personnalisé", "losPointName": "Nom du point", "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", - "losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)", + "losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Horizon radio", - "losLegendLosBeam": "Ligne de visée", + "losLegendLosBeam": "Ligne de visée", "losLegendTerrain": "Terrain", - "losFrequencyLabel": "Fréquence", - "losFrequencyInfoTooltip": "Voir les détails du calcul", - "losFrequencyDialogTitle": "Calcul de l’horizon radio", - "losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", + "losFrequencyLabel": "Fréquence", + "losFrequencyInfoTooltip": "Voir les détails du calcul", + "losFrequencyDialogTitle": "Calcul de l’horizon radio", + "losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_addToFavorites": "Ajouter à mes favoris", + "listFilter_addToFavorites": "Ajouter à mes favoris", "listFilter_removeFromFavorites": "Supprimer des favoris", - "listFilter_favorites": "Préférences", + "listFilter_favorites": "Préférences", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1800,13 +1800,25 @@ "contacts_searchFavorites": "Rechercher {number}{str} Favoris...", "contacts_searchUsers": "Rechercher {number}{str} utilisateurs...", "contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...", - "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", + "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", "contacts_searchContactsNoNumber": "Rechercher des contacts...", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB", - "usbScreenStatus": "Sélectionnez un périphérique USB", - "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nÅ“ud MeshCore.", - "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.", + "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.", + "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.", "usbScreenTitle": "Connectez via USB", - "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page." + "usbScreenStatus": "Sélectionnez un périphérique USB", + "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.", + "usbErrorPermissionDenied": "L'accès via USB a été refusé.", + "usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.", + "usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.", + "usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.", + "usbErrorNotConnected": "Aucun appareil USB n'est connecté.", + "usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.", + "usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.", + "usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.", + "usbErrorAlreadyActive": "Une connexion USB est déjà établie.", + "usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.", + "usbErrorPortClosed": "La connexion USB n'est pas établie.", + "usbErrorConnectTimedOut": "Attente avec délai, en attendant une réponse de l'appareil.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c77f1b9..7ce77ea 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -35,7 +35,7 @@ "common_disable": "Disattivare", "common_reboot": "Riavvia", "common_loading": "Caricamento...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -98,11 +98,11 @@ "settings_locationInvalid": "Latitudine o longitudine non valida.", "settings_latitude": "Latitudine", "settings_longitude": "Longitudine", - "settings_privacyMode": "Modalità Privacy", + "settings_privacyMode": "Modalità Privacy", "settings_privacyModeSubtitle": "Nascondere nome/luogo negli annunci", - "settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.", - "settings_privacyModeEnabled": "Modalità privacy abilitata", - "settings_privacyModeDisabled": "Modalità privacy disabilitata", + "settings_privacyModeToggle": "Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.", + "settings_privacyModeEnabled": "Modalità privacy abilitata", + "settings_privacyModeDisabled": "Modalità privacy disabilitata", "settings_actions": "Azioni", "settings_sendAdvertisement": "Invia Annuncio", "settings_sendAdvertisementSubtitle": "Presenza trasmessa ora", @@ -165,18 +165,18 @@ "appSettings_language": "Lingua", "appSettings_languageSystem": "Predefinito di sistema", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notifiche", "appSettings_enableNotifications": "Abilita Notifiche", "appSettings_enableNotificationsSubtitle": "Ricevi notifiche per messaggi e annunci", @@ -195,7 +195,7 @@ "appSettings_pathsWillBeCleared": "I percorsi verranno puliti dopo 5 tentativi falliti.", "appSettings_pathsWillNotBeCleared": "I percorsi non verranno eliminati automaticamente.", "appSettings_autoRouteRotation": "Rotazione Percorso Automatico", - "appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione", + "appSettings_autoRouteRotationSubtitle": "Alterna tra i percorsi migliori e la modalità alluvione", "appSettings_autoRouteRotationEnabled": "Rotazione percorso automatico abilitata", "appSettings_autoRouteRotationDisabled": "Rotazione del percorso automatico disabilitata", "appSettings_battery": "Batteria", @@ -284,8 +284,8 @@ }, "contacts_newGroup": "Nuovo Gruppo", "contacts_groupName": "Nome gruppo", - "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", - "contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.", + "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", + "contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -345,7 +345,7 @@ "channels_muteChannel": "Silenzia canale", "channels_unmuteChannel": "Attiva notifiche canale", "channels_deleteChannel": "Elimina canale", - "channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.", + "channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -477,7 +477,7 @@ "debugLog_enableInSettings": "Abilita il logging di debug dell'app nelle impostazioni", "debugLog_frames": "Frame", "debugLog_rawLogRx": "Log Raw-RX", - "debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.", + "debugLog_noBleActivity": "Nessuna attività BLE rilevata ancora.", "debugFrame_length": "Lunghezza del Frame: {count} byte", "@debugFrame_length": { "placeholders": { @@ -542,11 +542,11 @@ }, "debugFrame_hexDump": "Dumpa Esadecimale:", "chat_pathManagement": "Gestione Percorsi", - "chat_routingMode": "Modalità di routing", + "chat_routingMode": "Modalità di routing", "chat_autoUseSavedPath": "Utilizza il percorso salvato", - "chat_forceFloodMode": "Modalità Inondamento Forzato", + "chat_forceFloodMode": "Modalità Inondamento Forzato", "chat_recentAckPaths": "Percorsi ACK Recenti (tocca per usare):", - "chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.", + "chat_pathHistoryFull": "La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.", "chat_hopSingular": "salta", "chat_hopPlural": "salta", "chat_hopsCount": "{count} {count, plural, =1{salto} other{salti}}", @@ -559,15 +559,15 @@ }, "chat_successes": "successi", "chat_removePath": "Rimuovi percorso", - "chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.", + "chat_noPathHistoryYet": "Non c'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.", "chat_pathActions": "Azioni Percorso:", "chat_setCustomPath": "Imposta Percorso Personalizzato", "chat_setCustomPathSubtitle": "Specifica manualmente il percorso di routing", "chat_clearPath": "Cancella Percorso", "chat_clearPathSubtitle": "Riprova la scoperta alla prossima invio", - "chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.", + "chat_pathCleared": "Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.", "chat_floodModeSubtitle": "Utilizza l'interruttore di routing nella barra delle applicazioni", - "chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.", + "chat_floodModeEnabled": "Modalità alluvione abilitata. Disattivala tramite l'icona di routing nella barra in alto.", "chat_fullPath": "Percorso Completo", "chat_pathDetailsNotAvailable": "I dettagli del percorso non sono ancora disponibili. Prova a inviare un messaggio per ricaricare.", "chat_pathSetHops": "Percorso impostato: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", @@ -660,7 +660,7 @@ "map_sendToChannel": "Invia al canale", "map_noChannelsAvailable": "Nessun canale disponibile", "map_publicLocationShare": "Condividi in una posizione pubblica", - "map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.", + "map_publicLocationShareConfirm": "Stai per condividere una posizione in {channelLabel}. Questo canale è pubblico e chiunque abbia la PSK può vederlo.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -810,13 +810,13 @@ "login_password": "Password", "login_enterPassword": "Inserisci password", "login_savePassword": "Salva password", - "login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.", + "login_savePasswordSubtitle": "La password verrà memorizzata in modo sicuro su questo dispositivo.", "login_repeaterDescription": "Inserisci la password del ripetitore per accedere alle impostazioni e allo stato.", "login_roomDescription": "Inserisci la password della stanza per accedere alle impostazioni e allo stato.", "login_routing": "Instradamento", - "login_routingMode": "Modalità di routing", + "login_routingMode": "Modalità di routing", "login_autoUseSavedPath": "Utilizza il percorso salvato", - "login_forceFloodMode": "Modalità Inondamento Forzato", + "login_forceFloodMode": "Modalità Inondamento Forzato", "login_managePaths": "Gestisci Percorsi", "login_login": "Accedi", "login_attempt": "Prova {current}/{max}", @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", + "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", "common_reload": "Ricaricare", "common_clear": "Cancella", "path_currentPath": "Percorso corrente: {path}", @@ -862,7 +862,7 @@ "path_hexPrefixInstructions": "Inserire i prefissi esadecimali a 2 caratteri per ogni salto, separati da virgole.", "path_hexPrefixExample": "Esempio: A1,F2,3C (ogni nodo utilizza il primo byte della sua chiave pubblica)", "path_labelHexPrefixes": "Prefisso esadecimale (percorso)", - "path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)", + "path_helperMaxHops": "Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)", "path_selectFromContacts": "Seleziona da contatti:", "path_noRepeatersFound": "Non sono stati trovati ripetitori o server di stanza.", "path_customPathsRequire": "I percorsi personalizzati richiedono salti intermedi che possono inoltrare messaggi.", @@ -874,7 +874,7 @@ } } }, - "path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.", + "path_tooLong": "Il percorso è troppo lungo. Massimo 64 salti consentiti.", "path_setPath": "Imposta Percorso", "repeater_management": "Gestione Ripetitori", "repeater_managementTools": "Strumenti di Gestione", @@ -887,9 +887,9 @@ "repeater_settings": "Impostazioni", "repeater_settingsSubtitle": "Configura i parametri del ripetitore", "repeater_statusTitle": "Stato del Ripetitore", - "repeater_routingMode": "Modalità di routing", + "repeater_routingMode": "Modalità di routing", "repeater_autoUseSavedPath": "Percorso salvato automatico", - "repeater_forceFloodMode": "Modalità Inondamento Forzato", + "repeater_forceFloodMode": "Modalità Inondamento Forzato", "repeater_pathManagement": "Gestione dei percorsi", "repeater_refresh": "Aggiorna", "repeater_statusRequestTimeout": "Richiesta stato scaduta.", @@ -904,7 +904,7 @@ "repeater_systemInformation": "Informazioni di sistema", "repeater_battery": "Batteria", "repeater_clockAtLogin": "Orologio (all'accesso)", - "repeater_uptime": "Disponibilità", + "repeater_uptime": "Disponibilità", "repeater_queueLength": "Lunghezza della coda", "repeater_debugFlags": "Impostazioni Debug", "repeater_radioStatistics": "Statistiche Radio", @@ -1007,10 +1007,10 @@ "repeater_packetForwardingSubtitle": "Abilita il ripetitore per inoltrare i pacchetti", "repeater_guestAccess": "Accesso Ospite", "repeater_guestAccessSubtitle": "Consenti l'accesso ospite in sola lettura", - "repeater_privacyMode": "Modalità Privacy", + "repeater_privacyMode": "Modalità Privacy", "repeater_privacyModeSubtitle": "Nascondere nome/luogo negli annunci", "repeater_advertisementSettings": "Impostazioni Annuncio", - "repeater_localAdvertInterval": "Intervallo Pubblicità Locale", + "repeater_localAdvertInterval": "Intervallo Pubblicità Locale", "repeater_localAdvertIntervalMinutes": "{minutes} minuti", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione", + "repeater_floodAdvertInterval": "Intervallo Pubblicità Inondazione", "repeater_floodAdvertIntervalHours": "{hours} ore", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1033,13 +1033,13 @@ "repeater_rebootRepeater": "Riavvia Ripetitore", "repeater_rebootRepeaterSubtitle": "Riavvia il dispositivo ripetitore", "repeater_rebootRepeaterConfirm": "Sei sicuro di voler riavviare questo ripetitore?", - "repeater_regenerateIdentityKey": "Rigenera Chiave Identità", + "repeater_regenerateIdentityKey": "Rigenera Chiave Identità", "repeater_regenerateIdentityKeySubtitle": "Genera una nuova coppia di chiavi pubblica/privata", - "repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?", + "repeater_regenerateIdentityKeyConfirm": "Questo genererà una nuova identità per il ripetitore. Procedere?", "repeater_eraseFileSystem": "Elimina File System", "repeater_eraseFileSystemSubtitle": "Formatta il file system del ripetitore", - "repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!", - "repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.", + "repeater_eraseFileSystemConfirm": "ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!", + "repeater_eraseSerialOnly": "Elimina è disponibile solo tramite console seriale.", "repeater_commandSent": "Comando inviato: {command}", "@repeater_commandSent": { "placeholders": { @@ -1072,7 +1072,7 @@ "repeater_refreshLocationSettings": "Aggiorna le Impostazioni della Posizione", "repeater_refreshPacketForwarding": "Aggiorna il inoltro pacchetti", "repeater_refreshGuestAccess": "Aggiorna Accesso Ospite", - "repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy", + "repeater_refreshPrivacyMode": "Aggiorna Modalità Privacy", "repeater_refreshAdvertisementSettings": "Aggiorna le Impostazioni dell'Annuncio", "repeater_refreshed": "{label} aggiornato", "@repeater_refreshed": { @@ -1117,7 +1117,7 @@ "repeater_cliQuickAdvertise": "Pubblicare", "repeater_cliQuickClock": "Orologio", "repeater_cliHelpAdvert": "Invia un pacchetto pubblicitario", - "repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)", + "repeater_cliHelpReboot": "Riavvia il dispositivo. (nota, potresti ottenere 'Timeout' che è normale)", "repeater_cliHelpClock": "Mostra l'ora corrente per l'orologio di ciascun dispositivo.", "repeater_cliHelpPassword": "Imposta una nuova password di amministratore per il dispositivo.", "repeater_cliHelpVersion": "Mostra la versione del dispositivo e la data di costruzione del firmware.", @@ -1125,12 +1125,12 @@ "repeater_cliHelpSetAf": "Imposta il fattore di tempo di trasmissione.", "repeater_cliHelpSetTx": "Imposta la potenza di trasmissione LoRa in dBm (riavvia per applicare).", "repeater_cliHelpSetRepeat": "Abilita o disabilita il ruolo del ripetitore per questo nodo.", - "repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).", + "repeater_cliHelpSetAllowReadOnly": "(Server della stanza) Se 'on', allora l'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).", "repeater_cliHelpSetFloodMax": "Imposta il numero massimo di salti per i pacchetti di inondazione in entrata (se >= max, il pacchetto non viene inoltrato)", - "repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.", + "repeater_cliHelpSetIntThresh": "Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.", "repeater_cliHelpSetAgcResetInterval": "Imposta l'intervallo per resettare il controllore Automatico del Guadagno. Imposta su 0 per disabilitare.", "repeater_cliHelpSetMultiAcks": "Abilita o disabilita la funzione 'double ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.", + "repeater_cliHelpSetAdvertInterval": "Imposta l'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.", "repeater_cliHelpSetFloodAdvertInterval": "Imposta l'intervallo del timer in ore per inviare un pacchetto pubblicitario di massa. Imposta su 0 per disabilitare.", "repeater_cliHelpSetGuestPassword": "Imposta/aggiorna la password dell'ospite. (per ripetitori, gli accessi degli ospiti possono inviare la richiesta \"Get Stats\")", "repeater_cliHelpSetName": "Imposta il nome dell'annuncio.", @@ -1138,33 +1138,33 @@ "repeater_cliHelpSetLon": "Imposta la longitudine della mappa pubblicitaria. (gradi decimali)", "repeater_cliHelpSetRadio": "Imposta completamente nuovi parametri radio e li salva nelle preferenze. Richiede un comando \"reboot\" per l'applicazione.", "repeater_cliHelpSetRxDelay": "Impostazioni (experimental) base (deve essere > 1 per l'effetto) per applicare un leggero ritardo ai pacchetti ricevuti, in base alla forza del segnale/punteggio. Imposta a 0 per disabilitare.", - "repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).", - "repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.", + "repeater_cliHelpSetTxDelay": "Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).", + "repeater_cliHelpSetDirectTxDelay": "Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.", "repeater_cliHelpSetBridgeEnabled": "Abilita/Disabilita ponte.", "repeater_cliHelpSetBridgeDelay": "Imposta il ritardo prima di ritrasmettere i pacchetti.", - "repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.", - "repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.", + "repeater_cliHelpSetBridgeSource": "Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.", + "repeater_cliHelpSetBridgeBaud": "Imposta la velocità di trasmissione per i ponti rs232.", "repeater_cliHelpSetBridgeSecret": "Imposta il segreto per i ponti espnow.", "repeater_cliHelpSetAdcMultiplier": "Imposta un fattore personalizzato per regolare la tensione della batteria riportata (supportato solo su schede selezionate).", "repeater_cliHelpTempRadio": "Imposta parametri radio temporanei per il numero specificato di minuti, per poi tornare ai parametri radio originali. (non salva nelle preferenze).", - "repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)", + "repeater_cliHelpSetPerm": "Modifica l'ACL. Rimuove l'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell'ACL. Aggiorna l'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)", "repeater_cliHelpGetBridgeType": "Ottiene tipo ponte nessuno, rs232, espnow", "repeater_cliHelpLogStart": "Avvia registrazione pacchetti nel file system.", "repeater_cliHelpLogStop": "Interrompi la registrazione dei pacchetti al file system.", "repeater_cliHelpLogErase": "Elimina i log del pacchetto dal file system.", - "repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4", + "repeater_cliHelpNeighbors": "Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4", "repeater_cliHelpNeighborRemove": "Rimuove la prima corrispondenza in base al prefisso (esadecimale) della pubkey, dalla lista dei vicini.", "repeater_cliHelpRegion": "(solo serie) Elenca tutte le regioni definite e le autorizzazioni di allagamento correnti.", - "repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.", + "repeater_cliHelpRegionLoad": "NOTA: questo è un'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.", "repeater_cliHelpRegionGet": "Cerca la regione con il prefisso del nome dato (o \"\" per l'ambito globale). Risponde con \"-> nome-regione (nome-genitore) 'F'\"", "repeater_cliHelpRegionPut": "Aggiunge o aggiorna una definizione di regione con il nome specificato.", "repeater_cliHelpRegionRemove": "Rimuove una definizione di regione con il dato nome. (deve corrispondere esattamente e non avere regioni figlio)", "repeater_cliHelpRegionAllowf": "Imposta il permesso di 'F'lood per la regione specificata. ('' per lo scope globale/legacy)", - "repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).", + "repeater_cliHelpRegionDenyf": "Rimuove il permesso 'F'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).", "repeater_cliHelpRegionHome": "Risposte con la regione 'home' corrente. (Nota applicata finora, riservata per il futuro)", "repeater_cliHelpRegionHomeSet": "Imposta la regione 'home'.", "repeater_cliHelpRegionSave": "Persiste l'elenco/mappa delle regioni all'archiviazione.", - "repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.", + "repeater_cliHelpGps": "Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.", "repeater_cliHelpGpsOnOff": "Attiva/disattiva l'alimentazione del GPS.", "repeater_cliHelpGpsSync": "Sincronizza l'orario del nodo con l'orologio GPS.", "repeater_cliHelpGpsSetLoc": "Imposta la posizione del nodo alle coordinate GPS e salva le preferenze.", @@ -1180,7 +1180,7 @@ "repeater_regionManagementRepeaterOnly": "Gestione Regione (solo Ripetitore)", "repeater_regionNote": "Sono state introdotte le comandi di regione per gestire le definizioni e le autorizzazioni delle regioni.", "repeater_gpsManagement": "Gestione GPS", - "repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.", + "repeater_gpsNote": "è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.", "telemetry_receivedData": "Dati Telemetria Ricevuti", "telemetry_requestTimeout": "Richiesta di telemetria scaduta.", "telemetry_errorLoading": "Errore nel caricamento della telemetria: {error}", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1254,7 +1254,7 @@ "channelPath_repeatsLabel": "Ripeti", "channelPath_pathLabel": "Percorso {index}", "channelPath_observedLabel": "Osservato", - "channelPath_observedPathTitle": "Percorso osservato {index} • {hops}", + "channelPath_observedPathTitle": "Percorso osservato {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Percorso", "channelPath_observedPathHeader": "Percorso Osservato", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1373,11 +1373,11 @@ "channels_joinPrivateChannel": "Unisciti a un Canale Privato", "channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.", "channels_joinPublicChannel": "Unisciti al Canale Pubblico", - "channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.", + "channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.", "channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag", - "channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.", + "channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.", "channels_scanQrCode": "Scansiona un codice QR", - "channels_scanQrCodeComingSoon": "Arriverà presto", + "channels_scanQrCodeComingSoon": "Arriverà presto", "channels_enterHashtag": "Inserisci hashtag", "channels_hashtagHint": "es. #team", "@neighbors_unknownContact": { @@ -1459,35 +1459,35 @@ } }, "common_ok": "OK", - "community_title": "Comunità", - "community_create": "Crea Comunità", - "community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.", + "community_title": "Comunità", + "community_create": "Crea Comunità", + "community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.", "community_join": "Unisciti", "community_joinTitle": "Unisciti alla Community", "community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?", "community_scanQr": "Scansiona il QR Code della Community", - "community_scanInstructions": "Punta la fotocamera su un codice QR della comunità", + "community_scanInstructions": "Punta la fotocamera su un codice QR della comunità", "community_showQr": "Mostra il codice QR", - "community_publicChannel": "Comunità Pubblica", - "community_hashtagChannel": "Hashtag della Comunità", - "community_name": "Nome della Comunità", - "community_enterName": "Inserisci il nome della comunità", - "community_created": "Comunità \"{name}\" creata", - "community_joined": "Unito alla comunità \"{name}\"", - "community_qrTitle": "Condividi Comunità", + "community_publicChannel": "Comunità Pubblica", + "community_hashtagChannel": "Hashtag della Comunità", + "community_name": "Nome della Comunità", + "community_enterName": "Inserisci il nome della comunità", + "community_created": "Comunità \"{name}\" creata", + "community_joined": "Unito alla comunità \"{name}\"", + "community_qrTitle": "Condividi Comunità", "community_qrInstructions": "Scansiona questo codice QR per unirti a {name}", "community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community", "community_invalidQrCode": "Codice QR della community non valido", - "community_alreadyMember": "Già membro", - "community_alreadyMemberMessage": "Sei già un membro di \"{name}\".", - "community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità", + "community_alreadyMember": "Già membro", + "community_alreadyMemberMessage": "Sei già un membro di \"{name}\".", + "community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità", "community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community", "community_noCommunities": "Nessun gruppo aggiunto finora", "community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.", - "community_manageCommunities": "Gestisci Comunità", - "community_delete": "Lascia la Comunità", + "community_manageCommunities": "Gestisci Comunità", + "community_delete": "Lascia la Comunità", "community_deleteConfirm": "Uscire da \"{name}\"?", - "community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.", + "community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Hai lasciato la comunità \"{name}\"", + "community_deleted": "Hai lasciato la comunità \"{name}\"", "community_addHashtagChannel": "Aggiungi Hashtag della Community", "community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community", - "community_selectCommunity": "Seleziona Comunità", + "community_selectCommunity": "Seleziona Comunità", "community_regularHashtag": "Hashtag regolare", - "community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)", - "community_communityHashtag": "Hashtag della Comunità", - "community_communityHashtagDesc": "Visibile solo ai membri della comunità", + "community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)", + "community_communityHashtag": "Hashtag della Comunità", + "community_communityHashtagDesc": "Visibile solo ai membri della comunità", "community_forCommunity": "Per {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1567,16 +1567,16 @@ "contacts_floodAdvert": "Annuncio alluvionale", "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", "contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard", - "contacts_clipboardEmpty": "La clipboard è vuota.", + "contacts_clipboardEmpty": "La clipboard è vuota.", "contacts_ShareContact": "Copia contatto negli Appunti", - "contacts_contactImported": "Il contatto è stato importato.", + "contacts_contactImported": "Il contatto è stato importato.", "contacts_contactImportFailed": "Contatto non importato con successo.", "contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.", "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", - "notification_activityTitle": "Attività MeshCore", + "notification_activityTitle": "Attività MeshCore", "notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}", "notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}", "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", @@ -1587,7 +1587,7 @@ "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", "settings_gpxExportNoContacts": "Nessun contatto da esportare.", "settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo", - "settings_gpxExportError": "Si è verificato un errore durante l'esportazione.", + "settings_gpxExportError": "Si è verificato un errore durante l'esportazione.", "settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.", "settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.", "settings_gpxExportAll": "Esporta tutti i contatti in GPX", @@ -1597,13 +1597,13 @@ "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", - "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", + "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", "map_removeLast": "Rimuovi ultimo", "map_pathTraceCancelled": "Tracciamento del percorso annullato.", "pathTrace_clearTooltip": "Pulisci percorso", "map_runTrace": "Esegui Path Trace", "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", - "scanner_bluetoothOff": "Il Bluetooth è disattivato.", + "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", "scanner_chromeRequired": "Browser Chrome richiesto", "scanner_chromeRequiredMessage": "Questa applicazione web richiede Google Chrome o un browser basato su Chromium per il supporto Bluetooth.", @@ -1612,10 +1612,10 @@ "snrIndicator_lastSeen": "Ultimo accesso", "chat_ShowAllPaths": "Mostra tutti i percorsi", "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", - "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", + "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.", "settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "Unità", + "appSettings_unitsTitle": "Unità", "appSettings_unitsMetric": "Metrico (m/km)", "appSettings_unitsImperial": "Imperiale (ft / mi)", "map_lineOfSight": "Linea di vista", @@ -1631,7 +1631,7 @@ }, "losClearAllPoints": "Cancella tutti i punti", "losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico", - "losMenuTitle": "Menù LOS", + "losMenuTitle": "Menù LOS", "losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati", "losShowDisplayNodes": "Mostra i nodi di visualizzazione", "losCustomPoints": "Punti personalizzati", @@ -1722,7 +1722,7 @@ } } }, - "losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.", + "losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.", "losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.", "losRenameCustomPoint": "Rinomina punto personalizzato", "losPointName": "Nome del punto", @@ -1734,7 +1734,7 @@ "losLegendTerrain": "Terreno", "losFrequencyLabel": "Frequenza", "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", - "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", + "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", @@ -1802,11 +1802,23 @@ "contacts_unread": "Non letti", "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", "contacts_searchRoomServers": "Cerca {number}{str} server Room...", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB", - "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", - "usbScreenStatus": "Seleziona un dispositivo USB", + "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", "usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.", + "usbScreenStatus": "Seleziona un dispositivo USB", "usbScreenTitle": "Connessione tramite USB", - "usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e riavviare." + "usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e riavviare.", + "usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.", + "usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.", + "usbErrorInvalidPort": "Seleziona un dispositivo USB valido.", + "usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.", + "usbErrorNotConnected": "Non è collegato alcun dispositivo USB.", + "usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.", + "usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.", + "usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.", + "usbErrorAlreadyActive": "La connessione USB è già attiva.", + "usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.", + "usbErrorPortClosed": "La connessione USB non è attiva.", + "usbErrorConnectTimedOut": "Attesa superata, in attesa di una risposta dal dispositivo.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2540c33..aeec38b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -358,6 +358,78 @@ abstract class AppLocalizations { /// **'No USB devices found. Plug one in and refresh.'** String get usbScreenEmptyState; + /// No description provided for @usbErrorPermissionDenied. + /// + /// In en, this message translates to: + /// **'USB permission was denied.'** + String get usbErrorPermissionDenied; + + /// No description provided for @usbErrorDeviceMissing. + /// + /// In en, this message translates to: + /// **'The selected USB device is no longer available.'** + String get usbErrorDeviceMissing; + + /// No description provided for @usbErrorInvalidPort. + /// + /// In en, this message translates to: + /// **'Select a valid USB device.'** + String get usbErrorInvalidPort; + + /// No description provided for @usbErrorBusy. + /// + /// In en, this message translates to: + /// **'Another USB connection request is already in progress.'** + String get usbErrorBusy; + + /// No description provided for @usbErrorNotConnected. + /// + /// In en, this message translates to: + /// **'No USB device is connected.'** + String get usbErrorNotConnected; + + /// No description provided for @usbErrorOpenFailed. + /// + /// In en, this message translates to: + /// **'Failed to open the selected USB device.'** + String get usbErrorOpenFailed; + + /// No description provided for @usbErrorConnectFailed. + /// + /// In en, this message translates to: + /// **'Failed to connect to the selected USB device.'** + String get usbErrorConnectFailed; + + /// No description provided for @usbErrorUnsupported. + /// + /// In en, this message translates to: + /// **'USB serial is not supported on this platform.'** + String get usbErrorUnsupported; + + /// No description provided for @usbErrorAlreadyActive. + /// + /// In en, this message translates to: + /// **'A USB connection is already active.'** + String get usbErrorAlreadyActive; + + /// No description provided for @usbErrorNoDeviceSelected. + /// + /// In en, this message translates to: + /// **'No USB device was selected.'** + String get usbErrorNoDeviceSelected; + + /// No description provided for @usbErrorPortClosed. + /// + /// In en, this message translates to: + /// **'The USB connection is not open.'** + String get usbErrorPortClosed; + + /// No description provided for @usbErrorConnectTimedOut. + /// + /// In en, this message translates to: + /// **'Timed out waiting for the device to respond.'** + String get usbErrorConnectTimedOut; + /// No description provided for @scanner_scanning. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 042ae8b..39f827b 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -12,89 +12,88 @@ class AppLocalizationsBg extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Контакти'; + String get nav_contacts => 'Контакти'; @override - String get nav_channels => 'Канали'; + String get nav_channels => 'Канали'; @override - String get nav_map => 'Карта'; + String get nav_map => 'Карта'; @override - String get common_cancel => 'Отказ'; + String get common_cancel => 'Отказ'; @override - String get common_ok => 'Добре'; + String get common_ok => 'Добре'; @override - String get common_connect => 'Свържи се'; + String get common_connect => 'Свържи се'; @override - String get common_unknownDevice => - 'Неизвестно устройство'; + String get common_unknownDevice => 'Неизвестно устройство'; @override - String get common_save => 'Запази'; + String get common_save => 'Запази'; @override - String get common_delete => 'Изтрий'; + String get common_delete => 'Изтрий'; @override - String get common_close => 'Затвори'; + String get common_close => 'Затвори'; @override - String get common_edit => 'Редактирай'; + String get common_edit => 'Редактирай'; @override - String get common_add => 'Добави'; + String get common_add => 'Добави'; @override - String get common_settings => 'Настройки'; + String get common_settings => 'Настройки'; @override - String get common_disconnect => 'Прекъсни'; + String get common_disconnect => 'Прекъсни'; @override - String get common_connected => 'Свързано'; + String get common_connected => 'Свързано'; @override - String get common_disconnected => 'Откъснато'; + String get common_disconnected => 'Откъснато'; @override - String get common_create => 'Създай'; + String get common_create => 'Създай'; @override - String get common_continue => 'Продължи'; + String get common_continue => 'Продължи'; @override - String get common_share => 'Сподели'; + String get common_share => 'Сподели'; @override - String get common_copy => 'Копирай'; + String get common_copy => 'Копирай'; @override - String get common_retry => 'Опитай отново'; + String get common_retry => 'Опитай отново'; @override - String get common_hide => 'Скриване'; + String get common_hide => 'Скриване'; @override - String get common_remove => 'Изтрий'; + String get common_remove => 'Изтрий'; @override - String get common_enable => 'Активирай'; + String get common_enable => 'Активирай'; @override - String get common_disable => 'Деактивирай'; + String get common_disable => 'Деактивирай'; @override - String get common_reboot => 'Рестартирай'; + String get common_reboot => 'Рестартирай'; @override - String get common_loading => 'Зареждане...'; + String get common_loading => 'Зареждане...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -116,250 +115,277 @@ class AppLocalizationsBg extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Свързване чрез USB'; + String get usbScreenTitle => 'Свързване чрез USB'; @override String get usbScreenSubtitle => - 'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.'; + 'Изберете открит сериен уред и се свържете директно към вашия MeshCore възел.'; @override - String get usbScreenStatus => 'Изберете USB устройство'; + String get usbScreenStatus => 'Изберете USB устройство'; @override String get usbScreenNote => - 'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.'; + 'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.'; @override String get usbScreenEmptyState => - 'Няма открити USB устройства. Включете едно и опитайте отново.'; + 'Няма открити USB устройства. Включете едно и опитайте отново.'; @override - String get scanner_scanning => - 'Сканиране за устройства...'; + String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.'; @override - String get scanner_connecting => 'Свързвам се...'; + String get usbErrorDeviceMissing => + 'Избраното USB устройство вече не е налично.'; @override - String get scanner_disconnecting => 'Изключване...'; + String get usbErrorInvalidPort => 'Изберете валитно USB устройство.'; @override - String get scanner_notConnected => 'Не е свързан'; + String get usbErrorBusy => + 'Друг мол за свързване през USB вече е в процес на изпълнение.'; + + @override + String get usbErrorNotConnected => 'Няма свързано USB устройство.'; + + @override + String get usbErrorOpenFailed => + 'Не успях да отворя избраното USB устройство.'; + + @override + String get usbErrorConnectFailed => + 'Не успях да се свържа с избраното USB устройство.'; + + @override + String get usbErrorUnsupported => + 'USB серийната комуникация не се поддържа на тази платформа.'; + + @override + String get usbErrorAlreadyActive => 'USB връзката вече е активирана.'; + + @override + String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.'; + + @override + String get usbErrorPortClosed => 'USB връзката не е активна.'; + + @override + String get usbErrorConnectTimedOut => + 'Изчаква се, но устройството не отговаря в рамките на зададения време.'; + + @override + String get scanner_scanning => 'Сканиране за устройства...'; + + @override + String get scanner_connecting => 'Свързвам се...'; + + @override + String get scanner_disconnecting => 'Изключване...'; + + @override + String get scanner_notConnected => 'Не е свързан'; @override String scanner_connectedTo(String deviceName) { - return 'Свързано с $deviceName'; + return 'Свързано с $deviceName'; } @override - String get scanner_searchingDevices => - 'Търсене на устройства MeshCore...'; + String get scanner_searchingDevices => 'Търсене на устройства MeshCore...'; @override String get scanner_tapToScan => - 'Натиснете Сканиране, за да намерите устройства MeshCore.'; + 'Натиснете Сканиране, за да намерите устройства MeshCore.'; @override String scanner_connectionFailed(String error) { - return 'Връзката не успя: $error'; + return 'Връзката не успя: $error'; } @override - String get scanner_stop => 'Спрете'; + String get scanner_stop => 'Спрете'; @override - String get scanner_scan => 'Сканирай'; + String get scanner_scan => 'Сканирай'; @override - String get scanner_bluetoothOff => 'Bluetooth е изключен.'; + String get scanner_bluetoothOff => 'Bluetooth е изключен.'; @override String get scanner_bluetoothOffMessage => - 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; + 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; @override - String get scanner_chromeRequired => - 'Изисква се браузър Chrome'; + String get scanner_chromeRequired => 'Изисква се браузър Chrome'; @override String get scanner_chromeRequiredMessage => - 'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.'; + 'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.'; @override - String get scanner_enableBluetooth => 'Активирайте Bluetooth'; + String get scanner_enableBluetooth => 'Активирайте Bluetooth'; @override - String get device_quickSwitch => 'Бързо превключване'; + String get device_quickSwitch => 'Бързо превключване'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Настройки'; + String get settings_title => 'Настройки'; @override - String get settings_deviceInfo => - 'Информация за устройството'; + String get settings_deviceInfo => 'Информация за устройството'; @override - String get settings_appSettings => - 'Настройки на приложението'; + String get settings_appSettings => 'Настройки на приложението'; @override String get settings_appSettingsSubtitle => - 'Уведомления, съобщения и предпочитания за карта'; + 'Уведомления, съобщения и предпочитания за карта'; @override - String get settings_nodeSettings => 'Настройки на възела'; + String get settings_nodeSettings => 'Настройки на възела'; @override - String get settings_nodeName => 'Име на възела'; + String get settings_nodeName => 'Име на възела'; @override - String get settings_nodeNameNotSet => 'Не е зададено'; + String get settings_nodeNameNotSet => 'Не е зададено'; @override - String get settings_nodeNameHint => 'Въведете име на възел'; + String get settings_nodeNameHint => 'Въведете име на възел'; @override - String get settings_nodeNameUpdated => - 'Името е актуализирано'; + String get settings_nodeNameUpdated => 'Името е актуализирано'; @override - String get settings_radioSettings => - 'Настройки на радиопредавателя'; + String get settings_radioSettings => 'Настройки на радиопредавателя'; @override String get settings_radioSettingsSubtitle => - 'Честота, мощност, разпространяващ фактор'; + 'Честота, мощност, разпространяващ фактор'; @override String get settings_radioSettingsUpdated => - 'Радио настройките са актуализирани'; + 'Радио настройките са актуализирани'; @override - String get settings_location => 'Местоположение'; + String get settings_location => 'Местоположение'; @override - String get settings_locationSubtitle => 'Координати на GPS'; + String get settings_locationSubtitle => 'Координати на GPS'; @override - String get settings_locationUpdated => - 'Местоположението е актуализирано'; + String get settings_locationUpdated => 'Местоположението е актуализирано'; @override String get settings_locationBothRequired => - 'Въведете както географска ширина, така и географска дължина.'; + 'Въведете както географска ширина, така и географска дължина.'; @override - String get settings_locationInvalid => - 'Невалидна ширина или дължина.'; + String get settings_locationInvalid => 'Невалидна ширина или дължина.'; @override - String get settings_locationGPSEnable => 'Активиране на GPS'; + String get settings_locationGPSEnable => 'Активиране на GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Активирайте автоматичното актуализиране на местоположението чрез GPS.'; + 'Активирайте автоматичното актуализиране на местоположението чрез GPS.'; @override - String get settings_locationIntervalSec => - 'Интервал за GPS (Секунди)'; + String get settings_locationIntervalSec => 'Интервал за GPS (Секунди)'; @override String get settings_locationIntervalInvalid => - 'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.'; + 'Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.'; @override - String get settings_latitude => 'Широчина'; + String get settings_latitude => 'Широчина'; @override - String get settings_longitude => 'Дължина'; + String get settings_longitude => 'Дължина'; @override - String get settings_privacyMode => - 'Режим на поверителност'; + String get settings_privacyMode => 'Режим на поверителност'; @override String get settings_privacyModeSubtitle => - 'Скриване на име/местоположение в рекламите'; + 'Скриване на име/местоположение в рекламите'; @override String get settings_privacyModeToggle => - 'Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.'; + 'Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.'; @override String get settings_privacyModeEnabled => - 'Режим на поверителност е активиран'; + 'Режим на поверителност е активиран'; @override String get settings_privacyModeDisabled => - 'Режим на поверителност е деактивиран'; + 'Режим на поверителност е деактивиран'; @override - String get settings_actions => 'Действия'; + String get settings_actions => 'Действия'; @override - String get settings_sendAdvertisement => 'Изпрати Реклама'; + String get settings_sendAdvertisement => 'Изпрати Реклама'; @override - String get settings_sendAdvertisementSubtitle => - 'Сега присъствие в ефир'; + String get settings_sendAdvertisementSubtitle => 'Сега присъствие в ефир'; @override - String get settings_advertisementSent => 'Реклама изпратена'; + String get settings_advertisementSent => 'Реклама изпратена'; @override - String get settings_syncTime => 'Време за синхронизация'; + String get settings_syncTime => 'Време за синхронизация'; @override String get settings_syncTimeSubtitle => - 'Задайте часовника на устройството да отговаря на времето на телефона.'; + 'Задайте часовника на устройството да отговаря на времето на телефона.'; @override - String get settings_timeSynchronized => - 'Синхронизирано във времето'; + String get settings_timeSynchronized => 'Синхронизирано във времето'; @override - String get settings_refreshContacts => 'Презареди контакти'; + String get settings_refreshContacts => 'Презареди контакти'; @override String get settings_refreshContactsSubtitle => - 'Презареди списъка с контакти от устройството'; + 'Презареди списъка с контакти от устройството'; @override - String get settings_rebootDevice => - 'Рестартирайте устройството'; + String get settings_rebootDevice => 'Рестартирайте устройството'; @override String get settings_rebootDeviceSubtitle => - 'Рестартирайте устройството MeshCore'; + 'Рестартирайте устройството MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.'; + 'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.'; @override - String get settings_debug => 'Отстрани'; + String get settings_debug => 'Отстрани'; @override - String get settings_bleDebugLog => - 'Лог за отстраняване на грешки на BLE'; + String get settings_bleDebugLog => 'Лог за отстраняване на грешки на BLE'; @override String get settings_bleDebugLogSubtitle => - 'Команди, отговори и сурови данни BLE'; + 'Команди, отговори и сурови данни BLE'; @override String get settings_appDebugLog => - 'Лог на отстраняване на грешки на приложението'; + 'Лог на отстраняване на грешки на приложението'; @override String get settings_appDebugLogSubtitle => - 'Съобщения за отстраняване на грешки на приложението'; + 'Съобщения за отстраняване на грешки на приложението'; @override - String get settings_about => 'За нас'; + String get settings_about => 'За нас'; @override String settings_aboutVersion(String version) { @@ -367,125 +393,115 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get settings_aboutLegalese => - 'Проект MeshCore с отворен код 2024 г.'; + String get settings_aboutLegalese => 'Проект MeshCore с отворен код 2024 г.'; @override String get settings_aboutDescription => - 'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.'; + 'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.'; @override String get settings_aboutOpenMeteoAttribution => - 'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)'; + 'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'Име'; + String get settings_infoName => 'Име'; @override - String get settings_infoId => 'ИД'; + String get settings_infoId => 'ИД'; @override - String get settings_infoStatus => 'Статус'; + String get settings_infoStatus => 'Статус'; @override - String get settings_infoBattery => 'Батерия'; + String get settings_infoBattery => 'Батерия'; @override - String get settings_infoPublicKey => 'Общ публичен ключ'; + String get settings_infoPublicKey => 'Общ публичен ключ'; @override - String get settings_infoContactsCount => 'Брой контакти'; + String get settings_infoContactsCount => 'Брой контакти'; @override - String get settings_infoChannelCount => 'Брой канали'; + String get settings_infoChannelCount => 'Брой канали'; @override - String get settings_presets => - 'Предварителни настройки'; + String get settings_presets => 'Предварителни настройки'; @override - String get settings_frequency => 'Честота (MHz)'; + String get settings_frequency => 'Честота (MHz)'; @override String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => - 'Невалидна честота (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Невалидна честота (300-2500 MHz)'; @override - String get settings_bandwidth => - 'Ширина на честотния спектър'; + String get settings_bandwidth => 'Ширина на честотния спектър'; @override - String get settings_spreadingFactor => - 'Фактор на разпространение'; + String get settings_spreadingFactor => 'Фактор на разпространение'; @override - String get settings_codingRate => 'Такса за кодиране'; + String get settings_codingRate => 'Такса за кодиране'; @override - String get settings_txPower => 'TX Мощност (dBm)'; + String get settings_txPower => 'TX Мощност (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => - 'Невалидна мощност на TX (0-22 dBm)'; + String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)'; @override - String get settings_clientRepeat => - 'Без електричество – повторение'; + String get settings_clientRepeat => 'Без електричество – повторение'; @override String get settings_clientRepeatSubtitle => - 'Позволете на това устройство да предава пакети към мрежата за други устройства.'; + 'Позволете на това устройство да предава пакети към мрежата за други устройства.'; @override String get settings_clientRepeatFreqWarning => - 'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.'; + 'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.'; @override String settings_error(String message) { - return 'Грешка: $message'; + return 'Грешка: $message'; } @override - String get appSettings_title => - 'Настройки на приложението'; + String get appSettings_title => 'Настройки на приложението'; @override - String get appSettings_appearance => 'Външен вид'; + String get appSettings_appearance => 'Външен вид'; @override - String get appSettings_theme => 'Тема'; + String get appSettings_theme => 'Тема'; @override - String get appSettings_themeSystem => - 'Система по подразбиране'; + String get appSettings_themeSystem => 'Система по подразбиране'; @override - String get appSettings_themeLight => 'Ярка'; + String get appSettings_themeLight => 'Ярка'; @override - String get appSettings_themeDark => 'Тъмно'; + String get appSettings_themeDark => 'Тъмно'; @override - String get appSettings_language => 'Език'; + String get appSettings_language => 'Език'; @override - String get appSettings_languageSystem => - 'Система по подразбиране'; + String get appSettings_languageSystem => 'Система по подразбиране'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -494,16 +510,16 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -512,765 +528,716 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Руски'; + String get appSettings_languageRu => 'Руски'; @override - String get appSettings_languageUk => 'Украински'; + String get appSettings_languageUk => 'Украински'; @override String get appSettings_enableMessageTracing => - 'Разрешаване на проследяване на съобщения'; + 'Разрешаване на проследяване на съобщения'; @override String get appSettings_enableMessageTracingSubtitle => - 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; + 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; @override - String get appSettings_notifications => 'Уведомления'; + String get appSettings_notifications => 'Уведомления'; @override - String get appSettings_enableNotifications => - 'Активирай Известия'; + String get appSettings_enableNotifications => 'Активирай Известия'; @override String get appSettings_enableNotificationsSubtitle => - 'Получете известия за съобщения и реклами'; + 'Получете известия за съобщения и реклами'; @override String get appSettings_notificationPermissionDenied => - 'Отказвано е разрешение за известия'; + 'Отказвано е разрешение за известия'; @override - String get appSettings_notificationsEnabled => - 'Уведомителни са активирани'; + String get appSettings_notificationsEnabled => 'Уведомителни са активирани'; @override - String get appSettings_notificationsDisabled => - 'Известия са изключени'; + String get appSettings_notificationsDisabled => 'Известия са изключени'; @override - String get appSettings_messageNotifications => 'Уведомления'; + String get appSettings_messageNotifications => 'Уведомления'; @override String get appSettings_messageNotificationsSubtitle => - 'Покажи известие при получаване на нови съобщения'; + 'Покажи известие при получаване на нови съобщения'; @override String get appSettings_channelMessageNotifications => - 'Уведомления за съобщения от канал'; + 'Уведомления за съобщения от канал'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Покажи известие при получаване на съобщения от канали'; + 'Покажи известие при получаване на съобщения от канали'; @override - String get appSettings_advertisementNotifications => - 'Уведомления за реклами'; + String get appSettings_advertisementNotifications => 'Уведомления за реклами'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Покажи известие, когато бъдат открити нови възли.'; + 'Покажи известие, когато бъдат открити нови възли.'; @override - String get appSettings_messaging => 'Съобщения'; + String get appSettings_messaging => 'Съобщения'; @override - String get appSettings_clearPathOnMaxRetry => - 'Изчисти Път на Макс Опит'; + String get appSettings_clearPathOnMaxRetry => 'Изчисти Път на Макс Опит'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Възстанови контактния път след 5 неуспешни опита за изпращане'; + 'Възстанови контактния път след 5 неуспешни опита за изпращане'; @override String get appSettings_pathsWillBeCleared => - 'Пътищата ще бъдат почистени след 5 неуспешни опита.'; + 'Пътищата ще бъдат почистени след 5 неуспешни опита.'; @override String get appSettings_pathsWillNotBeCleared => - 'Пътищата няма да бъдат автоматично изчистени.'; + 'Пътищата няма да бъдат автоматично изчистени.'; @override String get appSettings_autoRouteRotation => - 'Автоматично маршрутизиране на завъртания'; + 'Автоматично маршрутизиране на завъртания'; @override String get appSettings_autoRouteRotationSubtitle => - 'Превключете между най-добрите пътища и режим на наводняване'; + 'Превключете между най-добрите пътища и режим на наводняване'; @override String get appSettings_autoRouteRotationEnabled => - 'Автоматично маршрутизиране вкл.'; + 'Автоматично маршрутизиране вкл.'; @override String get appSettings_autoRouteRotationDisabled => - 'Автоматично маршрутизирането е деактивирано'; + 'Автоматично маршрутизирането е деактивирано'; @override - String get appSettings_battery => 'Батерия'; + String get appSettings_battery => 'Батерия'; @override - String get appSettings_batteryChemistry => - 'Химия на батерията'; + String get appSettings_batteryChemistry => 'Химия на батерията'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Зададено за устройство ($deviceName)'; + return 'Зададено за устройство ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Свържете се с устройство, за да изберете.'; + 'Свържете се с устройство, за да изберете.'; @override String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; @override - String get appSettings_batteryLifepo4 => - 'Литиево желязо фосфат (2.6-3.65V)'; + String get appSettings_batteryLifepo4 => 'Литиево желязо фосфат (2.6-3.65V)'; @override - String get appSettings_batteryLipo => - 'Литиев полимер (3.0-4.2V)'; + String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)'; @override - String get appSettings_mapDisplay => 'Карта за показване'; + String get appSettings_mapDisplay => 'Карта за показване'; @override - String get appSettings_showRepeaters => - 'Показване на повторители'; + String get appSettings_showRepeaters => 'Показване на повторители'; @override String get appSettings_showRepeatersSubtitle => - 'Показване на възпроизвеждащи се възли на картата'; + 'Показване на възпроизвеждащи се възли на картата'; @override - String get appSettings_showChatNodes => 'Покажи Възли на Чат'; + String get appSettings_showChatNodes => 'Покажи Възли на Чат'; @override String get appSettings_showChatNodesSubtitle => - 'Показване на чат възли на картата'; + 'Показване на чат възли на картата'; @override - String get appSettings_showOtherNodes => 'Покажи други възли'; + String get appSettings_showOtherNodes => 'Покажи други възли'; @override String get appSettings_showOtherNodesSubtitle => - 'Покажи други типове възли на картата'; + 'Покажи други типове възли на картата'; @override - String get appSettings_timeFilter => 'Филтриране по време'; + String get appSettings_timeFilter => 'Филтриране по време'; @override - String get appSettings_timeFilterShowAll => - 'Покажи всички възли'; + String get appSettings_timeFilterShowAll => 'Покажи всички възли'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Покажи възли от последните $hours часа'; + return 'Покажи възли от последните $hours часа'; } @override - String get appSettings_mapTimeFilter => - 'Филтри за време на картата'; + String get appSettings_mapTimeFilter => 'Филтри за време на картата'; @override String get appSettings_showNodesDiscoveredWithin => - 'Покажи възлите, открити в:'; + 'Покажи възлите, открити в:'; @override - String get appSettings_allTime => 'Всичко време'; + String get appSettings_allTime => 'Всичко време'; @override - String get appSettings_lastHour => 'Последната минута'; + String get appSettings_lastHour => 'Последната минута'; @override - String get appSettings_last6Hours => 'Последни 6 часа'; + String get appSettings_last6Hours => 'Последни 6 часа'; @override - String get appSettings_last24Hours => 'Последно 24 часа'; + String get appSettings_last24Hours => 'Последно 24 часа'; @override - String get appSettings_lastWeek => 'Миналата седмица'; + String get appSettings_lastWeek => 'Миналата седмица'; @override - String get appSettings_offlineMapCache => - 'Кеш на офлайн карти'; + String get appSettings_offlineMapCache => 'Кеш на офлайн карти'; @override - String get appSettings_unitsTitle => 'единици'; + String get appSettings_unitsTitle => 'единици'; @override - String get appSettings_unitsMetric => 'Метрика (m / km)'; + String get appSettings_unitsMetric => 'Метрика (m / km)'; @override - String get appSettings_unitsImperial => 'Имперска (ft / mi)'; + String get appSettings_unitsImperial => 'Имперска (ft / mi)'; @override - String get appSettings_noAreaSelected => - 'Няма избрана област'; + String get appSettings_noAreaSelected => 'Няма избрана област'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Избрана е област (мащаб $minZoom-$maxZoom)'; + return 'Избрана е област (мащаб $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Отстрани'; + String get appSettings_debugCard => 'Отстрани'; @override String get appSettings_appDebugLogging => - 'Логване за отстраняване на грешки на приложението'; + 'Логване за отстраняване на грешки на приложението'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.'; + 'Записване на съобщения за отстраняване на грешки от приложението за отстраняване на грешки.'; @override String get appSettings_appDebugLoggingEnabled => - 'Режимът за отстраняване на грешки в приложението е активиран.'; + 'Режимът за отстраняване на грешки в приложението е активиран.'; @override String get appSettings_appDebugLoggingDisabled => - 'Логването за отстраняване на грешки в приложението е изключено.'; + 'Логването за отстраняване на грешки в приложението е изключено.'; @override - String get contacts_title => 'Контакти'; + String get contacts_title => 'Контакти'; @override - String get contacts_noContacts => - 'Няма контакти към момента.'; + String get contacts_noContacts => 'Няма контакти към момента.'; @override String get contacts_contactsWillAppear => - 'Контактите ще се появят, когато устройствата рекламират.'; + 'Контактите ще се появят, когато устройствата рекламират.'; @override - String get contacts_unread => 'Непрочетено'; + String get contacts_unread => 'Непрочетено'; @override - String get contacts_searchContactsNoNumber => - 'Търси контакти...'; + String get contacts_searchContactsNoNumber => 'Търси контакти...'; @override String contacts_searchContacts(int number, String str) { - return 'Търсене на контакти...'; + return 'Търсене на контакти...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Търсене на $number$str любими...'; + return 'Търсене на $number$str любими...'; } @override String contacts_searchUsers(int number, String str) { - return 'Търсене на $number$str потребители...'; + return 'Търсене на $number$str потребители...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Търсене на $number$str повтарящи се...'; + return 'Търсене на $number$str повтарящи се...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Търсене на $number$str сървъри в стаята...'; + return 'Търсене на $number$str сървъри в стаята...'; } @override - String get contacts_noUnreadContacts => - 'Няма непрочетени контакти'; + String get contacts_noUnreadContacts => 'Няма непрочетени контакти'; @override - String get contacts_noContactsFound => - 'Няма намерени контакти или групи.'; + String get contacts_noContactsFound => 'Няма намерени контакти или групи.'; @override - String get contacts_deleteContact => 'Изтрий Контакт'; + String get contacts_deleteContact => 'Изтрий Контакт'; @override String contacts_removeConfirm(String contactName) { - return 'Изтрий $contactName от контактите?'; + return 'Изтрий $contactName от контактите?'; } @override - String get contacts_manageRepeater => - 'Управление на Повтарящ се Елемент'; + String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент'; @override - String get contacts_manageRoom => - 'Управление на сървър за стая'; + String get contacts_manageRoom => 'Управление на сървър за стая'; @override - String get contacts_roomLogin => 'Вход в стаята'; + String get contacts_roomLogin => 'Вход в стаята'; @override - String get contacts_openChat => 'Отвори чат'; + String get contacts_openChat => 'Отвори чат'; @override - String get contacts_editGroup => 'Редактирай Група'; + String get contacts_editGroup => 'Редактирай Група'; @override - String get contacts_deleteGroup => 'Изтрий група'; + String get contacts_deleteGroup => 'Изтрий група'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Премахнете \"$groupName\"?'; + return 'Премахнете \"$groupName\"?'; } @override - String get contacts_newGroup => 'Нова група'; + String get contacts_newGroup => 'Нова група'; @override - String get contacts_groupName => 'Група'; + String get contacts_groupName => 'Група'; @override - String get contacts_groupNameRequired => - 'Името на групата е задължително.'; + String get contacts_groupNameRequired => 'Името на групата е задължително.'; @override String contacts_groupAlreadyExists(String name) { - return 'Групата \"$name\" вече съществува.'; + return 'Групата \"$name\" вече съществува.'; } @override - String get contacts_filterContacts => - 'Филтрирайте контактите...'; + String get contacts_filterContacts => 'Филтрирайте контактите...'; @override String get contacts_noContactsMatchFilter => - 'Няма съвпадения с вашия филтър.'; + 'Няма съвпадения с вашия филтър.'; @override - String get contacts_noMembers => 'Няма членове'; + String get contacts_noMembers => 'Няма членове'; @override - String get contacts_lastSeenNow => 'Последно видяно сега'; + String get contacts_lastSeenNow => 'Последно видяно сега'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Последна активност $minutes минути преди'; + return 'Последна активност $minutes минути преди'; } @override - String get contacts_lastSeenHourAgo => - 'Последно видяно преди час'; + String get contacts_lastSeenHourAgo => 'Последно видяно преди час'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Последно видян $hours часа преди.'; + return 'Последно видян $hours часа преди.'; } @override - String get contacts_lastSeenDayAgo => - 'Последно видяно преди 1 ден'; + String get contacts_lastSeenDayAgo => 'Последно видяно преди 1 ден'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Последно видян $days дни преди.'; + return 'Последно видян $days дни преди.'; } @override - String get channels_title => 'Канали'; + String get channels_title => 'Канали'; @override - String get channels_noChannelsConfigured => - 'Няма конфигурирани канали'; + String get channels_noChannelsConfigured => 'Няма конфигурирани канали'; @override - String get channels_addPublicChannel => - 'Добави публичен канал'; + String get channels_addPublicChannel => 'Добави публичен канал'; @override - String get channels_searchChannels => 'Търсене на канали...'; + String get channels_searchChannels => 'Търсене на канали...'; @override - String get channels_noChannelsFound => - 'Няма намерени канали'; + String get channels_noChannelsFound => 'Няма намерени канали'; @override String channels_channelIndex(int index) { - return 'Канал $index'; + return 'Канал $index'; } @override - String get channels_hashtagChannel => 'Канал с хаштаг'; + String get channels_hashtagChannel => 'Канал с хаштаг'; @override - String get channels_public => 'Публично'; + String get channels_public => 'Публично'; @override - String get channels_private => 'Личен'; + String get channels_private => 'Личен'; @override - String get channels_publicChannel => 'Публичен канал'; + String get channels_publicChannel => 'Публичен канал'; @override - String get channels_privateChannel => 'Частен канал'; + String get channels_privateChannel => 'Частен канал'; @override - String get channels_editChannel => 'Редактирай канал'; + String get channels_editChannel => 'Редактирай канал'; @override - String get channels_muteChannel => 'Заглуши канала'; + String get channels_muteChannel => 'Заглуши канала'; @override - String get channels_unmuteChannel => - 'Включи известията на канала'; + String get channels_unmuteChannel => 'Включи известията на канала'; @override - String get channels_deleteChannel => 'Изтрий канала'; + String get channels_deleteChannel => 'Изтрий канала'; @override String channels_deleteChannelConfirm(String name) { - return 'Изтрий \"$name\"? Това не може да бъде отменено.'; + return 'Изтрий \"$name\"? Това не може да бъде отменено.'; } @override String channels_channelDeleteFailed(String name) { - return 'Неуспешно изтриване на канала \"$name\"'; + return 'Неуспешно изтриване на канала \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Каналът \"$name\" е изтрит'; + return 'Каналът \"$name\" е изтрит'; } @override - String get channels_addChannel => 'Добави Канал'; + String get channels_addChannel => 'Добави Канал'; @override - String get channels_channelIndexLabel => 'Индекс на канал'; + String get channels_channelIndexLabel => 'Индекс на канал'; @override - String get channels_channelName => 'Име на канала'; + String get channels_channelName => 'Име на канала'; @override - String get channels_usePublicChannel => - 'Използвайте публичен канал'; + String get channels_usePublicChannel => 'Използвайте публичен канал'; @override - String get channels_standardPublicPsk => - 'Стандартен публичен PSK'; + String get channels_standardPublicPsk => 'Стандартен публичен PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => - 'Генерирай случайна PSK'; + String get channels_generateRandomPsk => 'Генерирай случайна PSK'; @override - String get channels_enterChannelName => - 'Моля, въведете име на канал.'; + String get channels_enterChannelName => 'Моля, въведете име на канал.'; @override String get channels_pskMustBe32Hex => - 'PSK трябва да бъде 32 шестнаредни знака.'; + 'PSK трябва да бъде 32 шестнаредни знака.'; @override String channels_channelAdded(String name) { - return 'Каналът \"$name\" е добавен'; + return 'Каналът \"$name\" е добавен'; } @override String channels_editChannelTitle(int index) { - return 'Редактирай Канал $index'; + return 'Редактирай Канал $index'; } @override - String get channels_smazCompression => 'Компресия SMAZ'; + String get channels_smazCompression => 'Компресия SMAZ'; @override String channels_channelUpdated(String name) { - return 'Каналът \"$name\" е актуализиран'; + return 'Каналът \"$name\" е актуализиран'; } @override - String get channels_publicChannelAdded => - 'Публичен канал добавен'; + String get channels_publicChannelAdded => 'Публичен канал добавен'; @override - String get channels_sortBy => 'Сортирай по'; + String get channels_sortBy => 'Сортирай по'; @override - String get channels_sortManual => 'Ръчно'; + String get channels_sortManual => 'Ръчно'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => - 'Последни съобщения'; + String get channels_sortLatestMessages => 'Последни съобщения'; @override - String get channels_sortUnread => 'Непрочетено'; + String get channels_sortUnread => 'Непрочетено'; @override - String get channels_createPrivateChannel => - 'Създай Частен Канал'; + String get channels_createPrivateChannel => 'Създай Частен Канал'; @override - String get channels_createPrivateChannelDesc => - 'Защитено с таен ключ.'; + String get channels_createPrivateChannelDesc => 'Защитено с таен ключ.'; @override - String get channels_joinPrivateChannel => - 'Присъедини се към Частен Канал'; + String get channels_joinPrivateChannel => 'Присъедини се към Частен Канал'; @override - String get channels_joinPrivateChannelDesc => - 'Ръчно въведете таен ключ.'; + String get channels_joinPrivateChannelDesc => 'Ръчно въведете таен ключ.'; @override String get channels_joinPublicChannel => - 'Присъединете се към Публичния канал'; + 'Присъединете се към Публичния канал'; @override String get channels_joinPublicChannelDesc => - 'Всеки може да се присъедини към този канал.'; + 'Всеки може да се присъедини към този канал.'; @override - String get channels_joinHashtagChannel => - 'Присъедини се към Хаштаг Канал'; + String get channels_joinHashtagChannel => 'Присъедини се към Хаштаг Канал'; @override String get channels_joinHashtagChannelDesc => - 'Всеки може да се присъедини към хаштаговите канали.'; + 'Всеки може да се присъедини към хаштаговите канали.'; @override - String get channels_scanQrCode => 'Сканирайте QR код'; + String get channels_scanQrCode => 'Сканирайте QR код'; @override - String get channels_scanQrCodeComingSoon => 'Ще излезе скоро'; + String get channels_scanQrCodeComingSoon => 'Ще излезе скоро'; @override - String get channels_enterHashtag => 'Въведете хаштаг'; + String get channels_enterHashtag => 'Въведете хаштаг'; @override - String get channels_hashtagHint => 'напр. #отбор'; + String get channels_hashtagHint => 'напр. #отбор'; @override - String get chat_noMessages => 'Няма съобщения.'; + String get chat_noMessages => 'Няма съобщения.'; @override - String get chat_sendMessageToStart => - 'Изпрати съобщение, за да започнеш.'; + String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.'; @override - String get chat_originalMessageNotFound => - 'Съобщението не е намерено'; + String get chat_originalMessageNotFound => 'Съобщението не е намерено'; @override String chat_replyingTo(String name) { - return 'Отговарям на $name'; + return 'Отговарям на $name'; } @override String chat_replyTo(String name) { - return 'Отговори на $name'; + return 'Отговори на $name'; } @override - String get chat_location => 'Местоположение'; + String get chat_location => 'Местоположение'; @override String chat_sendMessageTo(String contactName) { - return 'Изпрати съобщение на $contactName'; + return 'Изпрати съобщение на $contactName'; } @override - String get chat_typeMessage => 'Въведете съобщение...'; + String get chat_typeMessage => 'Въведете съобщение...'; @override String chat_messageTooLong(int maxBytes) { - return 'Съобщението е твърде дълго (макс $maxBytes байта).'; + return 'Съобщението е твърде дълго (макс $maxBytes байта).'; } @override - String get chat_messageCopied => 'Съобщението е копирано'; + String get chat_messageCopied => 'Съобщението е копирано'; @override - String get chat_messageDeleted => 'Съобщението е изтрито'; + String get chat_messageDeleted => 'Съобщението е изтрито'; @override - String get chat_retryingMessage => 'Опитваме се отново.'; + String get chat_retryingMessage => 'Опитваме се отново.'; @override String chat_retryCount(int current, int max) { - return 'Опитай отново $current/$max'; + return 'Опитай отново $current/$max'; } @override - String get chat_sendGif => 'Изпрати GIF'; + String get chat_sendGif => 'Изпрати GIF'; @override - String get chat_reply => 'Отговори'; + String get chat_reply => 'Отговори'; @override - String get chat_addReaction => 'Добави Реакция'; + String get chat_addReaction => 'Добави Реакция'; @override - String get chat_me => 'Аз'; + String get chat_me => 'Аз'; @override - String get emojiCategorySmileys => 'Емотикони'; + String get emojiCategorySmileys => 'Емотикони'; @override - String get emojiCategoryGestures => 'Жестове'; + String get emojiCategoryGestures => 'Жестове'; @override - String get emojiCategoryHearts => 'Сърца'; + String get emojiCategoryHearts => 'Сърца'; @override - String get emojiCategoryObjects => 'Обекти'; + String get emojiCategoryObjects => 'Обекти'; @override - String get gifPicker_title => 'Изберете GIF'; + String get gifPicker_title => 'Изберете GIF'; @override - String get gifPicker_searchHint => 'Търсене на GIF-ове...'; + String get gifPicker_searchHint => 'Търсене на GIF-ове...'; @override - String get gifPicker_poweredBy => 'Задвижвано от GIPHY'; + String get gifPicker_poweredBy => 'Задвижвано от GIPHY'; @override - String get gifPicker_noGifsFound => - 'Няма намерени GIF файлове.'; + String get gifPicker_noGifsFound => 'Няма намерени GIF файлове.'; @override - String get gifPicker_failedLoad => - 'Не можа да се заредят GIF файловете'; + String get gifPicker_failedLoad => 'Не можа да се заредят GIF файловете'; @override - String get gifPicker_failedSearch => - 'Неуспешно търсене на GIF-ове'; + String get gifPicker_failedSearch => 'Неуспешно търсене на GIF-ове'; @override - String get gifPicker_noInternet => 'Няма интернет връзка'; + String get gifPicker_noInternet => 'Няма интернет връзка'; @override String get debugLog_appTitle => - 'Лог на отстраняване на грешки на приложението'; + 'Лог на отстраняване на грешки на приложението'; @override - String get debugLog_bleTitle => - 'Лог за отстраняване на грешки на BLE'; + String get debugLog_bleTitle => 'Лог за отстраняване на грешки на BLE'; @override - String get debugLog_copyLog => 'Копирай лог'; + String get debugLog_copyLog => 'Копирай лог'; @override - String get debugLog_clearLog => 'Изчисти логовете'; + String get debugLog_clearLog => 'Изчисти логовете'; @override - String get debugLog_copied => - 'Копирано лого за отстраняване на грешки'; + String get debugLog_copied => 'Копирано лого за отстраняване на грешки'; @override - String get debugLog_bleCopied => 'Копиран лог от BLE'; + String get debugLog_bleCopied => 'Копиран лог от BLE'; @override - String get debugLog_noEntries => - 'Все още няма дебъг логове.'; + String get debugLog_noEntries => 'Все още няма дебъг логове.'; @override String get debugLog_enableInSettings => - 'Активирайте отстраняване на грешки в настройките на приложението'; + 'Активирайте отстраняване на грешки в настройките на приложението'; @override - String get debugLog_frames => 'Рамки'; + String get debugLog_frames => 'Рамки'; @override String get debugLog_rawLogRx => 'Raw Log-RX'; @override - String get debugLog_noBleActivity => - 'Няма BLE активност към момента.'; + String get debugLog_noBleActivity => 'Няма BLE активност към момента.'; @override String debugFrame_length(int count) { - return 'Дължина на кадъра: $count байта'; + return 'Дължина на кадъра: $count байта'; } @override String debugFrame_command(String value) { - return 'Команда: 0x$value'; + return 'Команда: 0x$value'; } @override - String get debugFrame_textMessageHeader => 'Съобщение:'; + String get debugFrame_textMessageHeader => 'Съобщение:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Дестинация Публичен Ключ: $pubKey'; + return '- Дестинация Публичен Ключ: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Време: $timestamp'; + return '- Време: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Флагове: 0x$value'; + return '- Флагове: 0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- Тип текст: $type ($label)'; + return '- Тип текст: $type ($label)'; } @override String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Просто'; + String get debugFrame_textTypePlain => 'Просто'; @override String debugFrame_text(String text) { - return '- Текст: \"$text\"'; + return '- Текст: \"$text\"'; } @override - String get debugFrame_hexDump => 'Хексадесетичен Dump:'; + String get debugFrame_hexDump => 'Хексадесетичен Dump:'; @override - String get chat_pathManagement => 'Управление на пътища'; + String get chat_pathManagement => 'Управление на пътища'; @override - String get chat_ShowAllPaths => 'Покажи всички пътища'; + String get chat_ShowAllPaths => 'Покажи всички пътища'; @override - String get chat_routingMode => 'Режим на маршрутизиране'; + String get chat_routingMode => 'Режим на маршрутизиране'; @override - String get chat_autoUseSavedPath => - 'Автоматично (използвай запазения път)'; + String get chat_autoUseSavedPath => 'Автоматично (използвай запазения път)'; @override - String get chat_forceFloodMode => - 'Принуди режим на наводняване'; + String get chat_forceFloodMode => 'Принуди режим на наводняване'; @override String get chat_recentAckPaths => - 'Неотдавни ACK пътища (докоснете, за да използвате):'; + 'Неотдавни ACK пътища (докоснете, за да използвате):'; @override String get chat_pathHistoryFull => - 'Историята на пътя е пълна. Премахнете записи, за да добавите нови.'; + 'Историята на пътя е пълна. Премахнете записи, за да добавите нови.'; @override - String get chat_hopSingular => 'скочи'; + String get chat_hopSingular => 'скочи'; @override - String get chat_hopPlural => 'скоци'; + String get chat_hopPlural => 'скоци'; @override String chat_hopsCount(int count) { @@ -1284,51 +1251,49 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get chat_successes => 'Успехи'; + String get chat_successes => 'Успехи'; @override - String get chat_removePath => 'Премахни пътя'; + String get chat_removePath => 'Премахни пътя'; @override String get chat_noPathHistoryYet => - 'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.'; + 'Няма история на пътищата още.\nИзпратете съобщение, за да откриете пътища.'; @override - String get chat_pathActions => 'Действия по пътя:'; + String get chat_pathActions => 'Действия по пътя:'; @override - String get chat_setCustomPath => - 'Задайте персонализиран път'; + String get chat_setCustomPath => 'Задайте персонализиран път'; @override - String get chat_setCustomPathSubtitle => - 'Ръчно укажете маршрутен път'; + String get chat_setCustomPathSubtitle => 'Ръчно укажете маршрутен път'; @override - String get chat_clearPath => 'Почисти Път'; + String get chat_clearPath => 'Почисти Път'; @override String get chat_clearPathSubtitle => - 'Принуди преоткриване при следващо изпращане'; + 'Принуди преоткриване при следващо изпращане'; @override String get chat_pathCleared => - 'Пътят е почистен. Следващото съобщение ще открие маршрута отново.'; + 'Пътят е почистен. Следващото съобщение ще открие маршрута отново.'; @override String get chat_floodModeSubtitle => - 'Използвайте превключвателя за маршрутизиране в лентата на приложението.'; + 'Използвайте превключвателя за маршрутизиране в лентата на приложението.'; @override String get chat_floodModeEnabled => - 'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.'; + 'Режим на наводнение е активиран. Включете го отново чрез иконката за маршрутизиране в лентата на приложението.'; @override - String get chat_fullPath => 'Пълен път'; + String get chat_fullPath => 'Пълен път'; @override String get chat_pathDetailsNotAvailable => - 'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.'; + 'Детайлите за пътя все още не са налични. Опитайте да изпратите съобщение, за да освежите.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1338,313 +1303,300 @@ class AppLocalizationsBg extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Пътят е зададен: $hopCount $_temp0 - $status'; + return 'Пътят е зададен: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Запазено локално. Свържете се за синхронизиране.'; + 'Запазено локално. Свържете се за синхронизиране.'; @override - String get chat_pathDeviceConfirmed => - 'Устройство потвърдено.'; + String get chat_pathDeviceConfirmed => 'Устройство потвърдено.'; @override String get chat_pathDeviceNotConfirmed => - 'Устройството все още не е потвърдено.'; + 'Устройството все още не е потвърдено.'; @override - String get chat_type => 'Въведете'; + String get chat_type => 'Въведете'; @override - String get chat_path => 'Пътекино'; + String get chat_path => 'Пътекино'; @override - String get chat_publicKey => 'Публичен ключ'; + String get chat_publicKey => 'Публичен ключ'; @override String get chat_compressOutgoingMessages => - 'Компресиране на изходящи съобщения'; + 'Компресиране на изходящи съобщения'; @override - String get chat_floodForced => 'Потоп (принуден)'; + String get chat_floodForced => 'Потоп (принуден)'; @override - String get chat_directForced => 'Директно (принудително)'; + String get chat_directForced => 'Директно (принудително)'; @override String chat_hopsForced(int count) { - return '$count скока (принудително)'; + return '$count скока (принудително)'; } @override - String get chat_floodAuto => 'Потоп (автоматично)'; + String get chat_floodAuto => 'Потоп (автоматично)'; @override - String get chat_direct => 'Директно'; + String get chat_direct => 'Директно'; @override - String get chat_poiShared => - 'Споделено място от интерес'; + String get chat_poiShared => 'Споделено място от интерес'; @override String chat_unread(int count) { - return 'Непрочетени: $count'; + return 'Непрочетени: $count'; } @override - String get chat_openLink => 'Отваряне на връзката?'; + String get chat_openLink => 'Отваряне на връзката?'; @override String get chat_openLinkConfirmation => - 'Искате ли да отворите тази връзка в браузъра си?'; + 'Искате ли да отворите тази връзка в браузъра си?'; @override - String get chat_open => 'Отвори'; + String get chat_open => 'Отвори'; @override String chat_couldNotOpenLink(String url) { - return 'Не можа да се отвори връзката: $url'; + return 'Не можа да се отвори връзката: $url'; } @override - String get chat_invalidLink => - 'Невалиден формат на връзката'; + String get chat_invalidLink => 'Невалиден формат на връзката'; @override - String get map_title => 'Карта на възлите'; + String get map_title => 'Карта на възлите'; @override - String get map_lineOfSight => 'Линия на видимост'; + String get map_lineOfSight => 'Линия на видимост'; @override - String get map_losScreenTitle => 'Линия на видимост'; + String get map_losScreenTitle => 'Линия на видимост'; @override - String get map_noNodesWithLocation => - 'Няма възли с данни за местоположение.'; + String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.'; @override String get map_nodesNeedGps => - 'Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.'; + 'Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.'; @override String map_nodesCount(int count) { - return 'Нодове: $count'; + return 'Нодове: $count'; } @override String map_pinsCount(int count) { - return 'Ключове: $count'; + return 'Ключове: $count'; } @override - String get map_chat => 'Чат'; + String get map_chat => 'Чат'; @override - String get map_repeater => 'Повтарящ се'; + String get map_repeater => 'Повтарящ се'; @override - String get map_room => 'Стая'; + String get map_room => 'Стая'; @override - String get map_sensor => 'Датчик'; + String get map_sensor => 'Датчик'; @override - String get map_pinDm => 'Задържане (DM)'; + String get map_pinDm => 'Задържане (DM)'; @override - String get map_pinPrivate => 'Задържане (Приватно)'; + String get map_pinPrivate => 'Задържане (Приватно)'; @override - String get map_pinPublic => 'Публичен ключ'; + String get map_pinPublic => 'Публичен ключ'; @override - String get map_lastSeen => 'Последна видяна'; + String get map_lastSeen => 'Последна видяна'; @override String get map_disconnectConfirm => - 'Сигурни ли сте, че искате да се откъснете от това устройство?'; + 'Сигурни ли сте, че искате да се откъснете от това устройство?'; @override - String get map_from => 'От'; + String get map_from => 'От'; @override - String get map_source => 'Източник'; + String get map_source => 'Източник'; @override - String get map_flags => 'Флаг'; + String get map_flags => 'Флаг'; @override - String get map_shareMarkerHere => 'Споделете маркер тук'; + String get map_shareMarkerHere => 'Споделете маркер тук'; @override - String get map_pinLabel => 'Етикетиране на пин'; + String get map_pinLabel => 'Етикетиране на пин'; @override - String get map_label => 'Етикет'; + String get map_label => 'Етикет'; @override - String get map_pointOfInterest => 'Точка на интерес'; + String get map_pointOfInterest => 'Точка на интерес'; @override - String get map_sendToContact => 'Изпрати на контакт'; + String get map_sendToContact => 'Изпрати на контакт'; @override - String get map_sendToChannel => 'Изпрати в канала'; + String get map_sendToChannel => 'Изпрати в канала'; @override - String get map_noChannelsAvailable => 'Няма налични канали'; + String get map_noChannelsAvailable => 'Няма налични канали'; @override - String get map_publicLocationShare => - 'Споделяне на публично място'; + String get map_publicLocationShare => 'Споделяне на публично място'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Ще споделите местоположение в $channelLabel. Този канал е публичен и всеки с PSK може да го види.'; + return 'Ще споделите местоположение в $channelLabel. Този канал е публичен и всеки с PSK може да го види.'; } @override String get map_connectToShareMarkers => - 'Свържете се с устройство, за да споделите маркери.'; + 'Свържете се с устройство, за да споделите маркери.'; @override - String get map_filterNodes => 'Филтрирайте възли'; + String get map_filterNodes => 'Филтрирайте възли'; @override - String get map_nodeTypes => 'Типове възли'; + String get map_nodeTypes => 'Типове възли'; @override - String get map_chatNodes => 'Възли на чата'; + String get map_chatNodes => 'Възли на чата'; @override - String get map_repeaters => 'Повторители'; + String get map_repeaters => 'Повторители'; @override - String get map_otherNodes => 'Други възли'; + String get map_otherNodes => 'Други възли'; @override - String get map_keyPrefix => 'Префикс на ключа'; + String get map_keyPrefix => 'Префикс на ключа'; @override - String get map_filterByKeyPrefix => - 'Филтрирайте по префикс на ключ'; + String get map_filterByKeyPrefix => 'Филтрирайте по префикс на ключ'; @override - String get map_publicKeyPrefix => - 'Префикс на публичен ключ'; + String get map_publicKeyPrefix => 'Префикс на публичен ключ'; @override - String get map_markers => 'Маркери'; + String get map_markers => 'Маркери'; @override - String get map_showSharedMarkers => - 'Покажи споделени маркери'; + String get map_showSharedMarkers => 'Покажи споделени маркери'; @override - String get map_lastSeenTime => 'Последна видяна дата'; + String get map_lastSeenTime => 'Последна видяна дата'; @override - String get map_sharedPin => 'Споделено копие'; + String get map_sharedPin => 'Споделено копие'; @override - String get map_joinRoom => 'Присъедини се към стаята'; + String get map_joinRoom => 'Присъедини се към стаята'; @override - String get map_manageRepeater => - 'Управление на Повтарящ се Елемент'; + String get map_manageRepeater => 'Управление на Повтарящ се Елемент'; @override String get map_tapToAdd => - 'Натиснете върху възлите, за да ги добавите към пътя.'; + 'Натиснете върху възлите, за да ги добавите към пътя.'; @override - String get map_runTrace => 'Изпълни Път на Следване'; + String get map_runTrace => 'Изпълни Път на Следване'; @override - String get map_removeLast => 'Премахни Последно'; + String get map_removeLast => 'Премахни Последно'; @override - String get map_pathTraceCancelled => - 'Отменен е следването на пътя.'; + String get map_pathTraceCancelled => 'Отменен е следването на пътя.'; @override - String get mapCache_title => 'Кеш на офлайн карти'; + String get mapCache_title => 'Кеш на офлайн карти'; @override - String get mapCache_selectAreaFirst => - 'Изберете област за кеширане първа'; + String get mapCache_selectAreaFirst => 'Изберете област за кеширане първа'; @override String get mapCache_noTilesToDownload => - 'Няма плочки за изтегляне за тази област.'; + 'Няма плочки за изтегляне за тази област.'; @override - String get mapCache_downloadTilesTitle => 'Изтегли плочки'; + String get mapCache_downloadTilesTitle => 'Изтегли плочки'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Изтегли $count плочки за офлайн употреба?'; + return 'Изтегли $count плочки за офлайн употреба?'; } @override - String get mapCache_downloadAction => 'Изтегли'; + String get mapCache_downloadAction => 'Изтегли'; @override String mapCache_cachedTiles(int count) { - return 'Кеширани $count плочки'; + return 'Кеширани $count плочки'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Запазени $downloaded плочки ($failed неуспешни)'; + return 'Запазени $downloaded плочки ($failed неуспешни)'; } @override - String get mapCache_clearOfflineCacheTitle => - 'Изчисти офлайн кеша'; + String get mapCache_clearOfflineCacheTitle => 'Изчисти офлайн кеша'; @override String get mapCache_clearOfflineCachePrompt => - 'Премахнете всички кеширани плочки на картата?'; + 'Премахнете всички кеширани плочки на картата?'; @override String get mapCache_offlineCacheCleared => - 'Кешът на устройството е изчистен.'; + 'Кешът на устройството е изчистен.'; @override - String get mapCache_noAreaSelected => 'Няма избрана област'; + String get mapCache_noAreaSelected => 'Няма избрана област'; @override - String get mapCache_cacheArea => 'Област с кеш'; + String get mapCache_cacheArea => 'Област с кеш'; @override - String get mapCache_useCurrentView => - 'Използвайте текущия изглед'; + String get mapCache_useCurrentView => 'Използвайте текущия изглед'; @override - String get mapCache_zoomRange => 'Обхват на увеличението'; + String get mapCache_zoomRange => 'Обхват на увеличението'; @override String mapCache_estimatedTiles(int count) { - return 'Очаквани плочки: $count'; + return 'Очаквани плочки: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Изтеглено $completed / $total'; + return 'Изтеглено $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Изтегли Плочки'; + String get mapCache_downloadTilesButton => 'Изтегли Плочки'; @override - String get mapCache_clearCacheButton => 'Изчисти кеша'; + String get mapCache_clearCacheButton => 'Изчисти кеша'; @override String mapCache_failedDownloads(int count) { - return 'Неуспешни изтегляния: $count'; + return 'Неуспешни изтегляния: $count'; } @override @@ -1654,135 +1606,132 @@ class AppLocalizationsBg extends AppLocalizations { String east, String west, ) { - return 'Север $north, Юг $south, Изток $east, Запад $west'; + return 'Север $north, Юг $south, Изток $east, Запад $west'; } @override - String get time_justNow => 'Сега'; + String get time_justNow => 'Сега'; @override String time_minutesAgo(int minutes) { - return '$minutes минути преди'; + return '$minutes минути преди'; } @override String time_hoursAgo(int hours) { - return '$hours часа преди'; + return '$hours часа преди'; } @override String time_daysAgo(int days) { - return '$days дни преди'; + return '$days дни преди'; } @override - String get time_hour => 'час'; + String get time_hour => 'час'; @override - String get time_hours => 'часове'; + String get time_hours => 'часове'; @override - String get time_day => 'ден'; + String get time_day => 'ден'; @override - String get time_days => 'дни'; + String get time_days => 'дни'; @override - String get time_week => 'седмица'; + String get time_week => 'седмица'; @override - String get time_weeks => 'секти'; + String get time_weeks => 'секти'; @override - String get time_month => 'месец'; + String get time_month => 'месец'; @override - String get time_months => 'месеци'; + String get time_months => 'месеци'; @override - String get time_minutes => 'минути'; + String get time_minutes => 'минути'; @override - String get time_allTime => 'Всичко време'; + String get time_allTime => 'Всичко време'; @override - String get dialog_disconnect => 'Прекъсни'; + String get dialog_disconnect => 'Прекъсни'; @override String get dialog_disconnectConfirm => - 'Сигурни ли сте, че искате да се откъснете от това устройство?'; + 'Сигурни ли сте, че искате да се откъснете от това устройство?'; @override - String get login_repeaterLogin => 'Повторител Вход'; + String get login_repeaterLogin => 'Повторител Вход'; @override - String get login_roomLogin => 'Вход в стаята'; + String get login_roomLogin => 'Вход в стаята'; @override - String get login_password => 'Парола'; + String get login_password => 'Парола'; @override - String get login_enterPassword => 'Въведете парола'; + String get login_enterPassword => 'Въведете парола'; @override - String get login_savePassword => 'Запази парола'; + String get login_savePassword => 'Запази парола'; @override String get login_savePasswordSubtitle => - 'Паролата ще бъде съхранена сигурно на това устройство.'; + 'Паролата ще бъде съхранена сигурно на това устройство.'; @override String get login_repeaterDescription => - 'Въведете паролата на репитера, за да получите достъп до настройките и статуса.'; + 'Въведете паролата на репитера, за да получите достъп до настройките и статуса.'; @override String get login_roomDescription => - 'Въведете паролата на стаята, за да получите достъп до настройките и статуса.'; + 'Въведете паролата на стаята, за да получите достъп до настройките и статуса.'; @override - String get login_routing => 'Маршрутизиране'; + String get login_routing => 'Маршрутизиране'; @override - String get login_routingMode => - 'Режим на маршрутизиране'; + String get login_routingMode => 'Режим на маршрутизиране'; @override - String get login_autoUseSavedPath => - 'Автоматично (използвай запазения път)'; + String get login_autoUseSavedPath => 'Автоматично (използвай запазения път)'; @override - String get login_forceFloodMode => - 'Принуди режим на наводняване'; + String get login_forceFloodMode => 'Принуди режим на наводняване'; @override - String get login_managePaths => 'Управление на пътища'; + String get login_managePaths => 'Управление на пътища'; @override - String get login_login => 'Вход'; + String get login_login => 'Вход'; @override String login_attempt(int current, int max) { - return 'Опитвате $current/$max'; + return 'Опитвате $current/$max'; } @override String login_failed(String error) { - return 'Входът не беше успешен: $error'; + return 'Входът не беше успешен: $error'; } @override String get login_failedMessage => - 'Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.'; + 'Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.'; @override - String get common_reload => 'Презареди'; + String get common_reload => 'Презареди'; @override - String get common_clear => 'Изчисти'; + String get common_clear => 'Изчисти'; @override String path_currentPath(String path) { - return 'Текущ път: $path'; + return 'Текущ път: $path'; } @override @@ -1793,167 +1742,153 @@ class AppLocalizationsBg extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Използване на $count $_temp0 път'; + return 'Използване на $count $_temp0 път'; } @override - String get path_enterCustomPath => - 'Въведете персонализиран път'; + String get path_enterCustomPath => 'Въведете персонализиран път'; @override - String get path_currentPathLabel => 'Текущ път'; + String get path_currentPathLabel => 'Текущ път'; @override String get path_hexPrefixInstructions => - 'Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.'; + 'Въведете 2-символни шестнадесетични префикси за всеки хоп, разделени с кама.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (всяка нода използва първия байт от публичния си ключ)'; + 'A1,F2,3C (всяка нода използва първия байт от публичния си ключ)'; @override - String get path_labelHexPrefixes => - 'Пътеки (шестнадесетични префикси)'; + String get path_labelHexPrefixes => 'Пътеки (шестнадесетични префикси)'; @override String get path_helperMaxHops => - 'Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).'; + 'Максимум 64 скока. Всеки префикс е 2 шестнадесетични знака (1 байт).'; @override - String get path_selectFromContacts => - 'Изберете от контакти:'; + String get path_selectFromContacts => 'Изберете от контакти:'; @override String get path_noRepeatersFound => - 'Няма намерени репетитори или сървъри на стаи.'; + 'Няма намерени репетитори или сървъри на стаи.'; @override String get path_customPathsRequire => - 'Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.'; + 'Персонализираните пътища изискват междинни скокове, които могат да препращат съобщения.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Невалидни шестнадесетични префикси: $prefixes'; + return 'Невалидни шестнадесетични префикси: $prefixes'; } @override String get path_tooLong => - 'Пътят е твърде дълъг. Максимум 64 скока са разрешени.'; + 'Пътят е твърде дълъг. Максимум 64 скока са разрешени.'; @override - String get path_setPath => 'Задайте път'; + String get path_setPath => 'Задайте път'; @override - String get repeater_management => - 'Управление на повторители'; + String get repeater_management => 'Управление на повторители'; @override - String get room_management => - 'Управление на сървъра за стая'; + String get room_management => 'Управление на сървъра за стая'; @override - String get repeater_managementTools => - 'Инструменти за управление'; + String get repeater_managementTools => 'Инструменти за управление'; @override - String get repeater_status => 'Статус'; + String get repeater_status => 'Статус'; @override String get repeater_statusSubtitle => - 'Прегледайте статуса, статистиката и съседните устройства.'; + 'Прегледайте статуса, статистиката и съседните устройства.'; @override - String get repeater_telemetry => 'Телеметрия'; + String get repeater_telemetry => 'Телеметрия'; @override String get repeater_telemetrySubtitle => - 'Прегледайте телеметрията на сензорите и системните статистики'; + 'Прегледайте телеметрията на сензорите и системните статистики'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => - 'Изпрати команди към ретранслатора'; + String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора'; @override - String get repeater_neighbors => 'Съседи'; + String get repeater_neighbors => 'Съседи'; @override String get repeater_neighborsSubtitle => - 'Преглед на съседни възли с нулев скок.'; + 'Преглед на съседни възли с нулев скок.'; @override - String get repeater_settings => 'Настройки'; + String get repeater_settings => 'Настройки'; @override String get repeater_settingsSubtitle => - 'Конфигурирайте параметрите на репитера'; + 'Конфигурирайте параметрите на репитера'; @override - String get repeater_statusTitle => 'Статус на повтарянето'; + String get repeater_statusTitle => 'Статус на повтарянето'; @override - String get repeater_routingMode => - 'Режим на маршрутизиране'; + String get repeater_routingMode => 'Режим на маршрутизиране'; @override String get repeater_autoUseSavedPath => - 'Автоматично (използвай запазения път)'; + 'Автоматично (използвай запазения път)'; @override - String get repeater_forceFloodMode => - 'Принуди режим на наводняване'; + String get repeater_forceFloodMode => 'Принуди режим на наводняване'; @override - String get repeater_pathManagement => - 'Управление на пътища'; + String get repeater_pathManagement => 'Управление на пътища'; @override - String get repeater_refresh => 'Презареди'; + String get repeater_refresh => 'Презареди'; @override String get repeater_statusRequestTimeout => - 'Заявката за статус премина прекалено дълго.'; + 'Заявката за статус премина прекалено дълго.'; @override String repeater_errorLoadingStatus(String error) { - return 'Грешка при зареждане на статуса: $error'; + return 'Грешка при зареждане на статуса: $error'; } @override - String get repeater_systemInformation => - 'Информация за системата'; + String get repeater_systemInformation => 'Информация за системата'; @override - String get repeater_battery => 'Батерия'; + String get repeater_battery => 'Батерия'; @override - String get repeater_clockAtLogin => - 'Часовник (при влизане)'; + String get repeater_clockAtLogin => 'Часовник (при влизане)'; @override - String get repeater_uptime => 'Наличност'; + String get repeater_uptime => 'Наличност'; @override - String get repeater_queueLength => 'Дължина на опашката'; + String get repeater_queueLength => 'Дължина на опашката'; @override - String get repeater_debugFlags => - 'Контролни точки за отстраняване на грешки'; + String get repeater_debugFlags => 'Контролни точки за отстраняване на грешки'; @override - String get repeater_radioStatistics => - 'Статистика на радиостанциите'; + String get repeater_radioStatistics => 'Статистика на радиостанциите'; @override - String get repeater_lastRssi => 'Последна RSSI'; + String get repeater_lastRssi => 'Последна RSSI'; @override - String get repeater_lastSnr => 'Последна SNR'; + String get repeater_lastSnr => 'Последна SNR'; @override - String get repeater_noiseFloor => 'Ниво на шум'; + String get repeater_noiseFloor => 'Ниво на шум'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1962,17 +1897,16 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => - 'Статистика на пакетите'; + String get repeater_packetStatistics => 'Статистика на пакетите'; @override - String get repeater_sent => 'Изпратено'; + String get repeater_sent => 'Изпратено'; @override - String get repeater_received => 'Получено'; + String get repeater_received => 'Получено'; @override - String get repeater_duplicates => 'Дубликати'; + String get repeater_duplicates => 'Дубликати'; @override String repeater_daysHoursMinsSecs( @@ -1981,65 +1915,59 @@ class AppLocalizationsBg extends AppLocalizations { int minutes, int seconds, ) { - return '$days дни $hoursч $minutesм $secondsс'; + return '$days дни $hoursч $minutesм $secondsс'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Общо: $total, Наводнение: $flood, Директно: $direct'; + return 'Общо: $total, Наводнение: $flood, Директно: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Общо: $total, Наводнение: $flood, Директно: $direct'; + return 'Общо: $total, Наводнение: $flood, Директно: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Поливане: $flood, Директен: $direct'; + return 'Поливане: $flood, Директен: $direct'; } @override String repeater_duplicatesTotal(int total) { - return 'Общо: $total'; + return 'Общо: $total'; } @override - String get repeater_settingsTitle => - 'Настройки на повтарящия се елемент'; + String get repeater_settingsTitle => 'Настройки на повтарящия се елемент'; @override - String get repeater_basicSettings => 'Основни настройки'; + String get repeater_basicSettings => 'Основни настройки'; @override - String get repeater_repeaterName => - 'Име на повтарящ се елемент'; + String get repeater_repeaterName => 'Име на повтарящ се елемент'; @override String get repeater_repeaterNameHelper => - 'Показване на името на този репитер'; + 'Показване на името на този репитер'; @override - String get repeater_adminPassword => - 'Парола на администратора'; + String get repeater_adminPassword => 'Парола на администратора'; @override - String get repeater_adminPasswordHelper => - 'Пълен достъпен парола'; + String get repeater_adminPasswordHelper => 'Пълен достъпен парола'; @override - String get repeater_guestPassword => 'Парола на гост'; + String get repeater_guestPassword => 'Парола на гост'; @override - String get repeater_guestPasswordHelper => - 'Достъп с ограничен достъп'; + String get repeater_guestPasswordHelper => 'Достъп с ограничен достъп'; @override - String get repeater_radioSettings => - 'Настройки на радиостанцията'; + String get repeater_radioSettings => 'Настройки на радиостанцията'; @override - String get repeater_frequencyMhz => 'Честота (MHz)'; + String get repeater_frequencyMhz => 'Честота (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -2051,547 +1979,516 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => - 'Ширина на честотния спектър'; + String get repeater_bandwidth => 'Ширина на честотния спектър'; @override - String get repeater_spreadingFactor => - 'Фактор на разпространение'; + String get repeater_spreadingFactor => 'Фактор на разпространение'; @override - String get repeater_codingRate => 'Такса за кодиране'; + String get repeater_codingRate => 'Такса за кодиране'; @override - String get repeater_locationSettings => - 'Настройки на местоположението'; + String get repeater_locationSettings => 'Настройки на местоположението'; @override - String get repeater_latitude => 'Широчина'; + String get repeater_latitude => 'Широчина'; @override - String get repeater_latitudeHelper => - 'Десетични градуси (напр. 37.7749)'; + String get repeater_latitudeHelper => 'Десетични градуси (напр. 37.7749)'; @override - String get repeater_longitude => 'Дължина'; + String get repeater_longitude => 'Дължина'; @override String get repeater_longitudeHelper => - 'Градуси с десетични знаци (напр. -122.4194)'; + 'Градуси с десетични знаци (напр. -122.4194)'; @override - String get repeater_features => 'Характеристики'; + String get repeater_features => 'Характеристики'; @override - String get repeater_packetForwarding => - 'Пренасочване на пакети'; + String get repeater_packetForwarding => 'Пренасочване на пакети'; @override String get repeater_packetForwardingSubtitle => - 'Активирайте репитера, за да препращате пакети.'; + 'Активирайте репитера, за да препращате пакети.'; @override - String get repeater_guestAccess => 'Достъп за Гост'; + String get repeater_guestAccess => 'Достъп за Гост'; @override - String get repeater_guestAccessSubtitle => - 'Разрешете самочетене за гости'; + String get repeater_guestAccessSubtitle => 'Разрешете самочетене за гости'; @override - String get repeater_privacyMode => - 'Режим на поверителност'; + String get repeater_privacyMode => 'Режим на поверителност'; @override String get repeater_privacyModeSubtitle => - 'Скриване на име/местоположение в рекламите'; + 'Скриване на име/местоположение в рекламите'; @override - String get repeater_advertisementSettings => - 'Настройки на рекламите'; + String get repeater_advertisementSettings => 'Настройки на рекламите'; @override - String get repeater_localAdvertInterval => - 'Местен Рекламен Интервал'; + String get repeater_localAdvertInterval => 'Местен Рекламен Интервал'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes минути'; + return '$minutes минути'; } @override String get repeater_floodAdvertInterval => - 'Интервал на рекламата за наводнения'; + 'Интервал на рекламата за наводнения'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours часа'; + return '$hours часа'; } @override - String get repeater_encryptedAdvertInterval => - 'Криптиран Рекламен Интервал'; + String get repeater_encryptedAdvertInterval => 'Криптиран Рекламен Интервал'; @override String get repeater_dangerZone => - 'Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно'; + 'Опасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно Безопасно'; @override - String get repeater_rebootRepeater => - 'БеРестартирай Репитер'; + String get repeater_rebootRepeater => 'БеРестартирай Репитер'; @override - String get repeater_rebootRepeaterSubtitle => - 'Рестартирайте ретранслатора.'; + String get repeater_rebootRepeaterSubtitle => 'Рестартирайте ретранслатора.'; @override String get repeater_rebootRepeaterConfirm => - 'Сигурни ли сте, че искате да рестартирате този репитер?'; + 'Сигурни ли сте, че искате да рестартирате този репитер?'; @override String get repeater_regenerateIdentityKey => - 'Генериране на Ключ за Идентичност'; + 'Генериране на Ключ за Идентичност'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Генериране на нова двойка публичен/частен ключ'; + 'Генериране на нова двойка публичен/частен ключ'; @override String get repeater_regenerateIdentityKeyConfirm => - 'БеТова ще генерира нова идентичност за репитера. Продължете?'; + 'БеТова ще генерира нова идентичност за репитера. Продължете?'; @override - String get repeater_eraseFileSystem => - 'Изтрий Файлова Система'; + String get repeater_eraseFileSystem => 'Изтрий Файлова Система'; @override String get repeater_eraseFileSystemSubtitle => - 'Форматирайте файла на репитера'; + 'Форматирайте файла на репитера'; @override String get repeater_eraseFileSystemConfirm => - 'ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!'; + 'ВНИМАНИЕ: Това ще изтрие всички данни от репетитора. Това не може да бъде отменено!'; @override String get repeater_eraseSerialOnly => - 'Изтриването е достъпно само през серийния терминал.'; + 'Изтриването е достъпно само през серийния терминал.'; @override String repeater_commandSent(String command) { - return 'Командата е изпратена: $command'; + return 'Командата е изпратена: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Грешка при изпращане на командата: $error'; + return 'Грешка при изпращане на командата: $error'; } @override - String get repeater_confirm => 'БеПотвърди'; + String get repeater_confirm => 'БеПотвърди'; @override - String get repeater_settingsSaved => - 'Настройките са запазени успешно.'; + String get repeater_settingsSaved => 'Настройките са запазени успешно.'; @override String repeater_errorSavingSettings(String error) { - return 'Грешка при запазване на настройките: $error'; + return 'Грешка при запазване на настройките: $error'; } @override - String get repeater_refreshBasicSettings => - 'Обнови Основни Настройки'; + String get repeater_refreshBasicSettings => 'Обнови Основни Настройки'; @override String get repeater_refreshRadioSettings => - 'Обнови настройките на радиопредавателите'; + 'Обнови настройките на радиопредавателите'; @override - String get repeater_refreshTxPower => 'Обнови TX захранване'; + String get repeater_refreshTxPower => 'Обнови TX захранване'; @override String get repeater_refreshLocationSettings => - 'Обнови настройките на местоположението'; + 'Обнови настройките на местоположението'; @override - String get repeater_refreshPacketForwarding => - 'Обнови пакетно пренасочване'; + String get repeater_refreshPacketForwarding => 'Обнови пакетно пренасочване'; @override - String get repeater_refreshGuestAccess => - 'Обнови достъп за гости'; + String get repeater_refreshGuestAccess => 'Обнови достъп за гости'; @override - String get repeater_refreshPrivacyMode => - 'Обнови Режим на поверителност'; + String get repeater_refreshPrivacyMode => 'Обнови Режим на поверителност'; @override String get repeater_refreshAdvertisementSettings => - 'Обнови Настройки на Рекламата'; + 'Обнови Настройки на Рекламата'; @override String repeater_refreshed(String label) { - return '$label е обновено'; + return '$label е обновено'; } @override String repeater_errorRefreshing(String label) { - return 'Грешка при обновяване на $label'; + return 'Грешка при обновяване на $label'; } @override - String get repeater_cliTitle => 'Повторител CLI'; + String get repeater_cliTitle => 'Повторител CLI'; @override - String get repeater_debugNextCommand => - 'Поправи Следваща Команда'; + String get repeater_debugNextCommand => 'Поправи Следваща Команда'; @override - String get repeater_commandHelp => 'Помощ'; + String get repeater_commandHelp => 'Помощ'; @override - String get repeater_clearHistory => 'Изчисти История'; + String get repeater_clearHistory => 'Изчисти История'; @override - String get repeater_noCommandsSent => - 'Няма изпратени команди засега.'; + String get repeater_noCommandsSent => 'Няма изпратени команди засега.'; @override String get repeater_typeCommandOrUseQuick => - 'Въведете команда по-долу или използвайте бързи команди'; + 'Въведете команда по-долу или използвайте бързи команди'; @override - String get repeater_enterCommandHint => 'Въведете команда...'; + String get repeater_enterCommandHint => 'Въведете команда...'; @override - String get repeater_previousCommand => 'Предходна команда'; + String get repeater_previousCommand => 'Предходна команда'; @override - String get repeater_nextCommand => 'Следваща команда'; + String get repeater_nextCommand => 'Следваща команда'; @override - String get repeater_enterCommandFirst => - 'Въведете първо команда.'; + String get repeater_enterCommandFirst => 'Въведете първо команда.'; @override - String get repeater_cliCommandFrameTitle => - 'Рамка за команда CLI'; + String get repeater_cliCommandFrameTitle => 'Рамка за команда CLI'; @override String repeater_cliCommandError(String error) { - return 'Грешка: $error'; + return 'Грешка: $error'; } @override - String get repeater_cliQuickGetName => 'Получи име'; + String get repeater_cliQuickGetName => 'Получи име'; @override - String get repeater_cliQuickGetRadio => 'Получи радио'; + String get repeater_cliQuickGetRadio => 'Получи радио'; @override - String get repeater_cliQuickGetTx => 'Получи TX'; + String get repeater_cliQuickGetTx => 'Получи TX'; @override - String get repeater_cliQuickNeighbors => 'Съседи'; + String get repeater_cliQuickNeighbors => 'Съседи'; @override - String get repeater_cliQuickVersion => 'Версия'; + String get repeater_cliQuickVersion => 'Версия'; @override - String get repeater_cliQuickAdvertise => 'Рекламирай'; + String get repeater_cliQuickAdvertise => 'Рекламирай'; @override - String get repeater_cliQuickClock => 'Часовник'; + String get repeater_cliQuickClock => 'Часовник'; @override - String get repeater_cliHelpAdvert => - 'Изпраща рекламен пакет'; + String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет'; @override String get repeater_cliHelpReboot => - 'Рестартира устройството. (Забележка, може да получите \'Timeout\', което е нормално)'; + 'Рестартира устройството. (Забележка, може да получите \'Timeout\', което е нормално)'; @override String get repeater_cliHelpClock => - 'Показва текущото време според часовника на всяко устройство.'; + 'Показва текущото време според часовника на всяко устройство.'; @override String get repeater_cliHelpPassword => - 'Задава се нова администраторска парола за устройството.'; + 'Задава се нова администраторска парола за устройството.'; @override String get repeater_cliHelpVersion => - 'Показва версията на устройството и датата на компилация на фърмуера.'; + 'Показва версията на устройството и датата на компилация на фърмуера.'; @override String get repeater_cliHelpClearStats => - 'Рестартира различни статистики броячи до нула.'; + 'Рестартира различни статистики броячи до нула.'; @override - String get repeater_cliHelpSetAf => - 'Задава времето на фактора.'; + String get repeater_cliHelpSetAf => 'Задава времето на фактора.'; @override String get repeater_cliHelpSetTx => - 'Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).'; + 'Задава се мощността на предаване на LoRa в dBm (отчитане спрямо референтно ниво).'; @override String get repeater_cliHelpSetRepeat => - 'Активира или деактивира ролята на репитера за този възел.'; + 'Активира или деактивира ролята на репитера за този възел.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).'; + '(Сървър на стаята) Ако е \"включено\", тогава влизането с празен парола ще бъде разрешено, но не може да публикува в стаята (само четене).'; @override String get repeater_cliHelpSetFloodMax => - 'Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).'; + 'Задава максималния брой хопове на входящ пакет за заливване (ако >= max, пакетът не се предава).'; @override String get repeater_cliHelpSetIntThresh => - 'Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.'; + 'Задава праг на интерференцията (в dB). По подразбиране е 14. Задайте на 0, за да деактивирате откриването на интерференция на каналите.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.'; + 'Задава интервала за рестартиране на Автоматичния контролер за усилване. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetMultiAcks => - 'Активира или деактивира функцията \'двойни ACKs\'.'; + 'Активира или деактивира функцията \'двойни ACKs\'.'; @override String get repeater_cliHelpSetAdvertInterval => - 'Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.'; + 'Задава интервала на таймера в минути за изпращане на локален (безпроблемен) рекламен пакет. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.'; + 'Задава интервала на таймера в часове за изпращане на пакет с реклама за наводнение. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetGuestPassword => - 'Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")'; + 'Задава/обновява паролата на гост. (за повторители, гостите могат да изпращат заявката \"Get Stats\")'; @override - String get repeater_cliHelpSetName => - 'Задава име на обявата.'; + String get repeater_cliHelpSetName => 'Задава име на обявата.'; @override String get repeater_cliHelpSetLat => - 'Задава географска ширина на картата с реклами (в десетими градуси).'; + 'Задава географска ширина на картата с реклами (в десетими градуси).'; @override String get repeater_cliHelpSetLon => - 'Задава обхвата на дължина на картата на рекламата. (десетими градуса)'; + 'Задава обхвата на дължина на картата на рекламата. (десетими градуса)'; @override String get repeater_cliHelpSetRadio => - 'Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.'; + 'Задава напълно нови радио параметри и ги запазва в предпочитанията. Изисква команда \"рестарт\", за да бъдат приложени.'; @override String get repeater_cliHelpSetRxDelay => - 'Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.'; + 'Зададени (експериментални) основи (трябва да е > 1 за ефект) за прилагане на леко забавяне на получените пакети, базирано на силата на сигнала/резултата. Задайте на 0, за да го деактивирате.'; @override String get repeater_cliHelpSetTxDelay => - 'Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).'; + 'Задава фактор, умножен по времето на въздух за пакет в режим на наводнение и с рандомизирана система за слотове, за да забави предаването му (за да намали вероятността от сблъсъци).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.'; + 'Същото като txdelay, но за прилагане на случайна забавяне при препращането на пакети в директен режим.'; @override String get repeater_cliHelpSetBridgeEnabled => - 'Активиране/Деактивиране на мост.'; + 'Активиране/Деактивиране на мост.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Задайте забавяне преди преизпращане на пакети.'; + 'Задайте забавяне преди преизпращане на пакети.'; @override String get repeater_cliHelpSetBridgeSource => - 'Изберете дали мостът ще предава препратени пакети или получени пакети.'; + 'Изберете дали мостът ще предава препратени пакети или получени пакети.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Задайте скоростта на предаване за RS232 мостовете.'; + 'Задайте скоростта на предаване за RS232 мостовете.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Задайте тайна за мостовете на EspNow.'; + 'Задайте тайна за мостовете на EspNow.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).'; + 'Задава персонализиран коефициент за коригиране на отчетеното напрежение на батерията (поддържа се само на избрани дъски).'; @override String get repeater_cliHelpTempRadio => - 'Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).'; + 'Задава временни радио параметри за посочения брой минути, връщайки се към оригиналните радио параметри след това. (не се запазва в предпочитанията).'; @override String get repeater_cliHelpSetPerm => - 'Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).'; + 'Променя ACL. Премахва съответстващия запис (по префикс на pubkey), ако \"permissions\" е нула. Добавя нов запис, ако pubkey-hex е с пълна дължина и не е в ACL. Актуализира запис, съответстващ на префикса на pubkey. Битовете за разрешения варират според ролята на firmware, но долните 2 бита са: 0 (Гост), 1 (Само четене), 2 (Четене и писане), 3 (Администратор).'; @override String get repeater_cliHelpGetBridgeType => - 'Получава тип мост none, rs232, espnow'; + 'Получава тип мост none, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Започва записване на пакети във файловата система.'; + 'Започва записване на пакети във файловата система.'; @override String get repeater_cliHelpLogStop => - 'Спира записването на пакети във файловата система.'; + 'Спира записването на пакети във файловата система.'; @override String get repeater_cliHelpLogErase => - 'Изтрива логовете от пакета от файловата система.'; + 'Изтрива логовете от пакета от файловата система.'; @override String get repeater_cliHelpNeighbors => - 'Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4'; + 'Показва списък с други възли на репитер, чути чрез нулев хоп реклами. Всяка линия е id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.'; + 'Премахва първия съвпадащ запис (по префикси на pubkey (hex)) от списъка с съседи.'; @override String get repeater_cliHelpRegion => - '(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.'; + '(сериен режим) Изброява всички дефинирани региони и текущите разрешения за наводнения.'; @override String get repeater_cliHelpRegionLoad => - 'Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.'; + 'Забележка: това е специално многокомандно извикване. Всяка следваща команда е име на регион (отстъпен с интервали, за да се покаже йерархията, с минимум един интервал). Завършва се чрез изпращане на празен ред/команда.'; @override String get repeater_cliHelpRegionGet => - 'Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) \'F\'\"'; + 'Търси регион с даден префикс на име (или \"\" за глобалния обхват). Отговаря с \"-> region-name (parent-name) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Добавя или актуализира дефиниция на регион с дадено име.'; + 'Добавя или актуализира дефиниция на регион с дадено име.'; @override String get repeater_cliHelpRegionRemove => - 'Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)'; + 'Премахва дефиниция на регион с дадено име. (трябва да съвпада точно и да няма подрегиони)'; @override String get repeater_cliHelpRegionAllowf => - 'Задава \'Потоп\' разрешение за посочената област. (\'\' за глобалния/стария обхват)'; + 'Задава \'Потоп\' разрешение за посочената област. (\'\' за глобалния/стария обхват)'; @override String get repeater_cliHelpRegionDenyf => - 'Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )'; + 'Премахва разрешението \"F\"лоуд за посочената област. (ЗАБЕЛЕЖКА: в момента не се препоръчва да се използва на глобалното/старото ниво!! )'; @override String get repeater_cliHelpRegionHome => - 'Отговаря с текущия \'home\' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).'; + 'Отговаря с текущия \'home\' регион. (Забележка: не е приложена никъде, запазена за бъдещи нужди).'; @override - String get repeater_cliHelpRegionHomeSet => - 'Задава \'домашно\' региона.'; + String get repeater_cliHelpRegionHomeSet => 'Задава \'домашно\' региона.'; @override String get repeater_cliHelpRegionSave => - 'Запазва списъка/картата с региони в съхранение.'; + 'Запазва списъка/картата с региони в съхранение.'; @override String get repeater_cliHelpGps => - 'Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.'; + 'Показва статуса на GPS. Когато GPS е изключен, отговаря само с \"off\", ако е включен отговаря с \"on\", статус, fix, брой на сателити.'; @override - String get repeater_cliHelpGpsOnOff => - 'Включва/Изключва GPS захранването.'; + String get repeater_cliHelpGpsOnOff => 'Включва/Изключва GPS захранването.'; @override String get repeater_cliHelpGpsSync => - 'Синхронизира времето на възела с GPS часовника.'; + 'Синхронизира времето на възела с GPS часовника.'; @override String get repeater_cliHelpGpsSetLoc => - 'Задава координатите на нодата по GPS и запазва предпочитанията.'; + 'Задава координатите на нодата по GPS и запазва предпочитанията.'; @override String get repeater_cliHelpGpsAdvert => - 'Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията'; + 'Предоставя конфигурацията на рекламата за местоположението на възела:\n- none: не включвайте местоположението в рекламите\n- share: споделяйте gps местоположението (от SensorManager)\n- prefs: рекламирайте местоположението, съхранено в предпочитанията'; @override String get repeater_cliHelpGpsAdvertSet => - 'Задава конфигурация на обявите за местоположение.'; + 'Задава конфигурация на обявите за местоположение.'; @override - String get repeater_commandsListTitle => 'Списък с команди'; + String get repeater_commandsListTitle => 'Списък с команди'; @override String get repeater_commandsListNote => - 'ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".'; + 'ЗАБЕЛЕЖКА: за различните команди \"set ...\", също така съществува команда \"get ...\".'; @override - String get repeater_general => 'Общо'; + String get repeater_general => 'Общо'; @override - String get repeater_settingsCategory => 'Настройки'; + String get repeater_settingsCategory => 'Настройки'; @override - String get repeater_bridge => 'Мост'; + String get repeater_bridge => 'Мост'; @override - String get repeater_logging => 'Логване'; + String get repeater_logging => 'Логване'; @override - String get repeater_neighborsRepeaterOnly => - 'Съседи (Само за повтаряне)'; + String get repeater_neighborsRepeaterOnly => 'Съседи (Само за повтаряне)'; @override String get repeater_regionManagementRepeaterOnly => - 'Управление на региони (Само за повтарящ се канал)'; + 'Управление на региони (Само за повтарящ се канал)'; @override String get repeater_regionNote => - 'Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.'; + 'Регионните команди са въведени, за да управляват дефинициите и разрешенията на регионите.'; @override - String get repeater_gpsManagement => 'Управление на GPS'; + String get repeater_gpsManagement => 'Управление на GPS'; @override String get repeater_gpsNote => - 'GPS командата е въведена, за да управлява теми, свързани с местоположението.'; + 'GPS командата е въведена, за да управлява теми, свързани с местоположението.'; @override - String get telemetry_receivedData => - 'Получени телеметрични данни'; + String get telemetry_receivedData => 'Получени телеметрични данни'; @override - String get telemetry_requestTimeout => - 'Заявката за телеметрия е прекъсната.'; + String get telemetry_requestTimeout => 'Заявката за телеметрия е прекъсната.'; @override String telemetry_errorLoading(String error) { - return 'Грешка при зареждане на телеметрията: $error'; + return 'Грешка при зареждане на телеметрията: $error'; } @override - String get telemetry_noData => - 'Няма налични данни за телеметрията.'; + String get telemetry_noData => 'Няма налични данни за телеметрията.'; @override String telemetry_channelTitle(int channel) { - return 'Канал $channel'; + return 'Канал $channel'; } @override - String get telemetry_batteryLabel => 'Батерия'; + String get telemetry_batteryLabel => 'Батерия'; @override - String get telemetry_voltageLabel => 'Напрежение'; + String get telemetry_voltageLabel => 'Напрежение'; @override - String get telemetry_mcuTemperatureLabel => 'Температура на MCU'; + String get telemetry_mcuTemperatureLabel => 'Температура на MCU'; @override - String get telemetry_temperatureLabel => 'Температура'; + String get telemetry_temperatureLabel => 'Температура'; @override - String get telemetry_currentLabel => 'Текущо'; + String get telemetry_currentLabel => 'Текущо'; @override String telemetry_batteryValue(int percent, String volts) { @@ -2610,87 +2507,79 @@ class AppLocalizationsBg extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => - 'Получени данни за съседи'; + String get neighbors_receivedData => 'Получени данни за съседи'; @override - String get neighbors_requestTimedOut => - 'Съседите поискат изтичане на време.'; + String get neighbors_requestTimedOut => 'Съседите поискат изтичане на време.'; @override String neighbors_errorLoading(String error) { - return 'Грешка при зареждане на съседи: $error'; + return 'Грешка при зареждане на съседи: $error'; } @override - String get neighbors_repeatersNeighbors => - 'Повторители Съседи'; + String get neighbors_repeatersNeighbors => 'Повторители Съседи'; @override - String get neighbors_noData => - 'Няма налични данни за съседи.'; + String get neighbors_noData => 'Няма налични данни за съседи.'; @override String neighbors_unknownContact(String pubkey) { - return 'Неизвестна $pubkey'; + return 'Неизвестна $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Слушано преди $time.'; + return 'Слушано преди $time.'; } @override - String get channelPath_title => 'Пътеки пъзел'; + String get channelPath_title => 'Пътеки пъзел'; @override - String get channelPath_viewMap => 'Преглед на картата'; + String get channelPath_viewMap => 'Преглед на картата'; @override - String get channelPath_otherObservedPaths => - 'Други Наблюдавани Пътища'; + String get channelPath_otherObservedPaths => 'Други Наблюдавани Пътища'; @override - String get channelPath_repeaterHops => - 'Повтарящи се скокове'; + String get channelPath_repeaterHops => 'Повтарящи се скокове'; @override String get channelPath_noHopDetails => - 'Детайлите за пакета не са предоставени.'; + 'Детайлите за пакета не са предоставени.'; @override - String get channelPath_messageDetails => - 'Подробности на съобщението'; + String get channelPath_messageDetails => 'Подробности на съобщението'; @override - String get channelPath_senderLabel => 'Изпращач'; + String get channelPath_senderLabel => 'Изпращач'; @override - String get channelPath_timeLabel => 'Време'; + String get channelPath_timeLabel => 'Време'; @override - String get channelPath_repeatsLabel => 'Повтаря'; + String get channelPath_repeatsLabel => 'Повтаря'; @override String channelPath_pathLabel(int index) { - return 'Път $index'; + return 'Път $index'; } @override - String get channelPath_observedLabel => 'Наблюдавано'; + String get channelPath_observedLabel => 'Наблюдавано'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Наблюдаван път $index • $hops'; + return 'Наблюдаван път $index • $hops'; } @override - String get channelPath_noLocationData => - 'Няма данни за местоположение.'; + String get channelPath_noLocationData => 'Няма данни за местоположение.'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2703,358 +2592,340 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Неизвестно'; + String get channelPath_unknownPath => 'Неизвестно'; @override - String get channelPath_floodPath => 'Поливане'; + String get channelPath_floodPath => 'Поливане'; @override - String get channelPath_directPath => 'Директно'; + String get channelPath_directPath => 'Директно'; @override String channelPath_observedZeroOf(int total) { - return '0 от $total скокове'; + return '0 от $total скокове'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed от $total скокове'; + return '$observed от $total скокове'; } @override - String get channelPath_mapTitle => 'Карта на пътя'; + String get channelPath_mapTitle => 'Карта на пътя'; @override String get channelPath_noRepeaterLocations => - 'Няма налични местоположения на повторителите за този път.'; + 'Няма налични местоположения на повторителите за този път.'; @override String channelPath_primaryPath(int index) { - return 'Път $index (Основен)'; + return 'Път $index (Основен)'; } @override - String get channelPath_pathLabelTitle => 'Пътекино'; + String get channelPath_pathLabelTitle => 'Пътекино'; @override - String get channelPath_observedPathHeader => 'Наблюдаван път'; + String get channelPath_observedPathHeader => 'Наблюдаван път'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Няма налични детайли за този пакет.'; + 'Няма налични детайли за този пакет.'; @override - String get channelPath_unknownRepeater => - 'Неизвестен повторител'; + String get channelPath_unknownRepeater => 'Неизвестен повторител'; @override - String get community_title => 'Общност'; + String get community_title => 'Общност'; @override - String get community_create => 'Създай общност'; + String get community_create => 'Създай общност'; @override String get community_createDesc => - 'Създайте нова общност и я споделете чрез QR код.'; + 'Създайте нова общност и я споделете чрез QR код.'; @override - String get community_join => 'Присъедини се'; + String get community_join => 'Присъедини се'; @override - String get community_joinTitle => - 'Присъедини се към общността'; + String get community_joinTitle => 'Присъедини се към общността'; @override String community_joinConfirmation(String name) { - return 'Искате ли да се присъедините към общността \"$name\"?'; + return 'Искате ли да се присъедините към общността \"$name\"?'; } @override - String get community_scanQr => - 'Сканирайте QR кода на общността'; + String get community_scanQr => 'Сканирайте QR кода на общността'; @override String get community_scanInstructions => - 'Насочете камерата към QR код на общността'; + 'Насочете камерата към QR код на общността'; @override - String get community_showQr => 'Покажи QR код'; + String get community_showQr => 'Покажи QR код'; @override - String get community_publicChannel => 'Обществено общност'; + String get community_publicChannel => 'Обществено общност'; @override - String get community_hashtagChannel => 'Хаштаг на общността'; + String get community_hashtagChannel => 'Хаштаг на общността'; @override - String get community_name => 'Име на общността'; + String get community_name => 'Име на общността'; @override - String get community_enterName => - 'Въведете име на общността'; + String get community_enterName => 'Въведете име на общността'; @override String community_created(String name) { - return 'Общността \"$name\" е създадена'; + return 'Общността \"$name\" е създадена'; } @override String community_joined(String name) { - return 'Присъединено общност \"$name\"'; + return 'Присъединено общност \"$name\"'; } @override - String get community_qrTitle => 'Споделяне в общността'; + String get community_qrTitle => 'Споделяне в общността'; @override String community_qrInstructions(String name) { - return 'Сканирайте този QR код, за да се присъедините към $name.'; + return 'Сканирайте този QR код, за да се присъедините към $name.'; } @override String get community_hashtagPrivacyHint => - 'Хаштаг каналите на общността са достъпни само за членове на общността'; + 'Хаштаг каналите на общността са достъпни само за членове на общността'; @override - String get community_invalidQrCode => - 'Невалиден QR код на общността'; + String get community_invalidQrCode => 'Невалиден QR код на общността'; @override - String get community_alreadyMember => 'Вече съм член'; + String get community_alreadyMember => 'Вече съм член'; @override String community_alreadyMemberMessage(String name) { - return 'Вие вече сте член на \"$name\".'; + return 'Вие вече сте член на \"$name\".'; } @override - String get community_addPublicChannel => - 'Добави публичен общностен канал'; + String get community_addPublicChannel => 'Добави публичен общностен канал'; @override String get community_addPublicChannelHint => - 'Автоматично добавете публичния канал за тази общност.'; + 'Автоматично добавете публичния канал за тази общност.'; @override - String get community_noCommunities => - 'Няма присъединени общности още.'; + String get community_noCommunities => 'Няма присъединени общности още.'; @override String get community_scanOrCreate => - 'Сканирайте QR код или създайте общност, за да започнете.'; + 'Сканирайте QR код или създайте общност, за да започнете.'; @override - String get community_manageCommunities => - 'Управление на общности'; + String get community_manageCommunities => 'Управление на общности'; @override - String get community_delete => 'Напусни общността'; + String get community_delete => 'Напусни общността'; @override String community_deleteConfirm(String name) { - return 'Напускате \"$name\"?'; + return 'Напускате \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Това ще изтрие също $count канал(а) и техните съобщения.'; + return 'Това ще изтрие също $count канал(а) и техните съобщения.'; } @override String community_deleted(String name) { - return 'Остави общността \"$name\"'; + return 'Остави общността \"$name\"'; } @override - String get community_regenerateSecret => - 'Регенерейрай секрет'; + String get community_regenerateSecret => 'Регенерейрай секрет'; @override String community_regenerateSecretConfirm(String name) { - return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.'; + return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.'; } @override - String get community_regenerate => 'Регенерация'; + String get community_regenerate => 'Регенерация'; @override String community_secretRegenerated(String name) { - return 'Секретно презареждане за \"$name\"'; + return 'Секретно презареждане за \"$name\"'; } @override - String get community_updateSecret => 'Актуализирай тайна'; + String get community_updateSecret => 'Актуализирай тайна'; @override String community_secretUpdated(String name) { - return 'Секретно обновено за \"$name\"'; + return 'Секретно обновено за \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"'; + return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"'; } @override - String get community_addHashtagChannel => - 'Добави общностен хаштаг'; + String get community_addHashtagChannel => 'Добави общностен хаштаг'; @override String get community_addHashtagChannelDesc => - 'Добавете хаштаг канал за тази общност'; + 'Добавете хаштаг канал за тази общност'; @override - String get community_selectCommunity => 'Изберете общност'; + String get community_selectCommunity => 'Изберете общност'; @override - String get community_regularHashtag => 'Обикновен хаштаг'; + String get community_regularHashtag => 'Обикновен хаштаг'; @override String get community_regularHashtagDesc => - 'Общ хаштаг (всеки може да се присъедини)'; + 'Общ хаштаг (всеки може да се присъедини)'; @override - String get community_communityHashtag => 'Общностен хаштаг'; + String get community_communityHashtag => 'Общностен хаштаг'; @override - String get community_communityHashtagDesc => - 'Само за членове на общността'; + String get community_communityHashtagDesc => 'Само за членове на общността'; @override String community_forCommunity(String name) { - return 'За $name'; + return 'За $name'; } @override - String get listFilter_tooltip => - 'Филтрирайте и сортирайте'; + String get listFilter_tooltip => 'Филтрирайте и сортирайте'; @override - String get listFilter_sortBy => 'Сортирай по'; + String get listFilter_sortBy => 'Сортирай по'; @override - String get listFilter_latestMessages => 'Последни съобщения'; + String get listFilter_latestMessages => 'Последни съобщения'; @override - String get listFilter_heardRecently => 'Слушано е наскоро'; + String get listFilter_heardRecently => 'Слушано е наскоро'; @override String get listFilter_az => 'A-Z'; @override - String get listFilter_filters => 'Филтри'; + String get listFilter_filters => 'Филтри'; @override - String get listFilter_all => 'Всички'; + String get listFilter_all => 'Всички'; @override - String get listFilter_favorites => 'Любими'; + String get listFilter_favorites => 'Любими'; @override - String get listFilter_addToFavorites => 'Добави към любими'; + String get listFilter_addToFavorites => 'Добави към любими'; @override - String get listFilter_removeFromFavorites => - 'Премахване от списъка с любими'; + String get listFilter_removeFromFavorites => 'Премахване от списъка с любими'; @override - String get listFilter_users => 'Потребители'; + String get listFilter_users => 'Потребители'; @override - String get listFilter_repeaters => 'Повторители'; + String get listFilter_repeaters => 'Повторители'; @override - String get listFilter_roomServers => 'Сървъри на стая'; + String get listFilter_roomServers => 'Сървъри на стая'; @override - String get listFilter_unreadOnly => 'Само непрочетените'; + String get listFilter_unreadOnly => 'Само непрочетените'; @override - String get listFilter_newGroup => 'Нова група'; + String get listFilter_newGroup => 'Нова група'; @override - String get pathTrace_you => 'Вие'; + String get pathTrace_you => 'Вие'; @override - String get pathTrace_failed => - 'Пътят за проследяване не успя.'; + String get pathTrace_failed => 'Пътят за проследяване не успя.'; @override - String get pathTrace_notAvailable => - 'Пътека за проследяване не е достъпна.'; + String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.'; @override - String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; + String get pathTrace_refreshTooltip => 'Обнови Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Един или повече от хмелите липсва местоположение!'; + 'Един или повече от хмелите липсва местоположение!'; @override - String get pathTrace_clearTooltip => 'Изчисти пътя'; + String get pathTrace_clearTooltip => 'Изчисти пътя'; @override - String get losSelectStartEnd => - 'Изберете начални и крайни възли за LOS.'; + String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.'; @override String losRunFailed(String error) { - return 'Проверката на пряката видимост е неуспешна: $error'; + return 'Проверката на пряката видимост е неуспешна: $error'; } @override - String get losClearAllPoints => 'Изчистете всички точки'; + String get losClearAllPoints => 'Изчистете всички точки'; @override String get losRunToViewElevationProfile => - 'Стартирайте LOS, за да видите профила на надморската височина'; + 'Стартирайте LOS, за да видите профила на надморската височина'; @override - String get losMenuTitle => 'LOS меню'; + String get losMenuTitle => 'LOS меню'; @override String get losMenuSubtitle => - 'Докоснете възли или натиснете продължително карта за персонализирани точки'; + 'Докоснете възли или натиснете продължително карта за персонализирани точки'; @override - String get losShowDisplayNodes => - 'Показване на възли на дисплея'; + String get losShowDisplayNodes => 'Показване на възли на дисплея'; @override - String get losCustomPoints => 'Персонализирани точки'; + String get losCustomPoints => 'Персонализирани точки'; @override String losCustomPointLabel(int index) { - return 'Персонализирано $index'; + return 'Персонализирано $index'; } @override - String get losPointA => 'Точка А'; + String get losPointA => 'Точка А'; @override - String get losPointB => 'Точка Б'; + String get losPointB => 'Точка Б'; @override String losAntennaA(String value, String unit) { - return 'Антена A: $value $unit'; + return 'Антена A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Антена B: $value $unit'; + return 'Антена B: $value $unit'; } @override - String get losRun => 'Стартирайте LOS'; + String get losRun => 'Стартирайте LOS'; @override - String get losNoElevationData => - 'Няма данни за надморска височина'; + String get losNoElevationData => 'Няма данни за надморска височина'; @override String losProfileClear( @@ -3063,7 +2934,7 @@ class AppLocalizationsBg extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit'; + return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit'; } @override @@ -3073,64 +2944,61 @@ class AppLocalizationsBg extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, блокиран от $obstruction $heightUnit'; + return '$distance $distanceUnit, блокиран от $obstruction $heightUnit'; } @override - String get losStatusChecking => 'LOS: проверка...'; + String get losStatusChecking => 'LOS: проверка...'; @override - String get losStatusNoData => 'LOS: няма данни'; + String get losStatusNoData => 'LOS: няма данни'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно'; + return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно'; } @override String get losErrorElevationUnavailable => - 'Няма налични данни за надморска височина за една или повече проби.'; + 'Няма налични данни за надморска височина за една или повече проби.'; @override String get losErrorInvalidInput => - 'Невалидни данни за точки/надморска височина за изчисляване на LOS.'; + 'Невалидни данни за точки/надморска височина за изчисляване на LOS.'; @override - String get losRenameCustomPoint => - 'Преименувайте персонализирана точка'; + String get losRenameCustomPoint => 'Преименувайте персонализирана точка'; @override - String get losPointName => 'Име на точката'; + String get losPointName => 'Име на точката'; @override - String get losShowPanelTooltip => 'Показване на LOS панел'; + String get losShowPanelTooltip => 'Показване на LOS панел'; @override - String get losHidePanelTooltip => 'Скриване на LOS панела'; + String get losHidePanelTooltip => 'Скриване на LOS панела'; @override String get losElevationAttribution => - 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; + 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Радиохоризонт'; + String get losLegendRadioHorizon => 'Радиохоризонт'; @override - String get losLegendLosBeam => 'Линия на видимост'; + String get losLegendLosBeam => 'Линия на видимост'; @override - String get losLegendTerrain => 'Терен'; + String get losLegendTerrain => 'Терен'; @override - String get losFrequencyLabel => 'Честота'; + String get losFrequencyLabel => 'Честота'; @override - String get losFrequencyInfoTooltip => - 'Преглед на детайли за изчислението'; + String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението'; @override - String get losFrequencyDialogTitle => - 'Изчисляване на радиохоризонта'; + String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта'; @override String losFrequencyDialogDescription( @@ -3139,101 +3007,91 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; } @override - String get contacts_pathTrace => 'Пътен проследяване'; + String get contacts_pathTrace => 'Пътен проследяване'; @override - String get contacts_ping => 'Пинг'; + String get contacts_ping => 'Пинг'; @override - String get contacts_repeaterPathTrace => - 'Трасировка до повторител'; + String get contacts_repeaterPathTrace => 'Трасировка до повторител'; @override - String get contacts_repeaterPing => - 'Пингване на повторителя'; + String get contacts_repeaterPing => 'Пингване на повторителя'; @override - String get contacts_roomPathTrace => - 'Трасиране на път до съ'; + String get contacts_roomPathTrace => 'Трасиране на път до съ'; @override - String get contacts_roomPing => 'Ping на сървъра на стаята'; + String get contacts_roomPing => 'Ping на сървъра на стаята'; @override - String get contacts_chatTraceRoute => 'Трасиране на път'; + String get contacts_chatTraceRoute => 'Трасиране на път'; @override String contacts_pathTraceTo(String name) { - return 'Проследи маршрут към $name'; + return 'Проследи маршрут към $name'; } @override - String get contacts_clipboardEmpty => 'Клипборда е празна.'; + String get contacts_clipboardEmpty => 'Клипборда е празна.'; @override - String get contacts_invalidAdvertFormat => - 'Невалидни данни за контакт'; + String get contacts_invalidAdvertFormat => 'Невалидни данни за контакт'; @override - String get contacts_contactImported => - 'Контактът е импортиран.'; + String get contacts_contactImported => 'Контактът е импортиран.'; @override String get contacts_contactImportFailed => - 'Контактът не е успешно импортиран.'; + 'Контактът не е успешно импортиран.'; @override - String get contacts_zeroHopAdvert => 'Реклама без скок'; + String get contacts_zeroHopAdvert => 'Реклама без скок'; @override - String get contacts_floodAdvert => 'Потопна реклама'; + String get contacts_floodAdvert => 'Потопна реклама'; @override - String get contacts_copyAdvertToClipboard => - 'Копирай обявата в клипборда'; + String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда'; @override - String get contacts_addContactFromClipboard => - 'Добави контакт от клипборда'; + String get contacts_addContactFromClipboard => 'Добави контакт от клипборда'; @override - String get contacts_ShareContact => - 'Копирай контакт в клипборда'; + String get contacts_ShareContact => 'Копирай контакт в клипборда'; @override - String get contacts_ShareContactZeroHop => - 'Сподели контакт чрез обява'; + String get contacts_ShareContactZeroHop => 'Сподели контакт чрез обява'; @override - String get contacts_zeroHopContactAdvertSent => - 'Изпратен контакт по обява.'; + String get contacts_zeroHopContactAdvertSent => 'Изпратен контакт по обява.'; @override String get contacts_zeroHopContactAdvertFailed => - 'Неуспешно изпращане на контакт.'; + 'Неуспешно изпращане на контакт.'; @override String get contacts_contactAdvertCopied => - 'Рекламата е копирана в клипборда.'; + 'Рекламата е копирана в клипборда.'; @override String get contacts_contactAdvertCopyFailed => - 'Копирането на обявата в клипборда не успя.'; + 'Копирането на обявата в клипборда не успя.'; @override - String get notification_activityTitle => 'Активност на MeshCore'; + String get notification_activityTitle => 'Активност на MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'съобщения', - one: 'съобщение', + other: 'съобщения', + one: 'съобщение', ); return '$count $_temp0'; } @@ -3243,8 +3101,8 @@ class AppLocalizationsBg extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'съобщения в канали', - one: 'съобщение в канал', + other: 'съобщения в канали', + one: 'съобщение в канал', ); return '$count $_temp0'; } @@ -3254,85 +3112,77 @@ class AppLocalizationsBg extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'нови възли', - one: 'нов възел', + other: 'нови възли', + one: 'нов възел', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Открит нов $contactType'; + return 'Открит нов $contactType'; } @override - String get notification_receivedNewMessage => - 'Получено ново съобщение'; + String get notification_receivedNewMessage => 'Получено ново съобщение'; @override String get settings_gpxExportRepeaters => - 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; + 'Експортиране на повтарящи се устройства / сървър на стаята до GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Изпраща повторители / roomserver с местоположение в GPX файл.'; + 'Изпраща повторители / roomserver с местоположение в GPX файл.'; @override - String get settings_gpxExportContacts => - 'Експортирай спътници към GPX'; + String get settings_gpxExportContacts => 'Експортирай спътници към GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Експортира спътници с местоположение в GPX файл.'; + 'Експортира спътници с местоположение в GPX файл.'; @override - String get settings_gpxExportAll => - 'Експортирай всички контакти в GPX'; + String get settings_gpxExportAll => 'Експортирай всички контакти в GPX'; @override String get settings_gpxExportAllSubtitle => - 'Експортира всички контакти с местоположение в файл GPX.'; + 'Експортира всички контакти с местоположение в файл GPX.'; @override - String get settings_gpxExportSuccess => - 'Успешно изlexport на файл GPX.'; + String get settings_gpxExportSuccess => 'Успешно изlexport на файл GPX.'; @override - String get settings_gpxExportNoContacts => - 'Няма контакти за изlexport.'; + String get settings_gpxExportNoContacts => 'Няма контакти за изlexport.'; @override String get settings_gpxExportNotAvailable => - 'Не е поддържан на вашето устройство/ОС'; + 'Не е поддържан на вашето устройство/ОС'; @override - String get settings_gpxExportError => - 'Възникна грешка при изнасяне.'; + String get settings_gpxExportError => 'Възникна грешка при изнасяне.'; @override String get settings_gpxExportRepeatersRoom => - 'Местоположения на повторител и сървър на стаята'; + 'Местоположения на повторител и сървър на стаята'; @override - String get settings_gpxExportChat => - 'Местоположения на спътници'; + String get settings_gpxExportChat => 'Местоположения на спътници'; @override String get settings_gpxExportAllContacts => - 'Местоположения на всички контакти'; + 'Местоположения на всички контакти'; @override String get settings_gpxExportShareText => - 'Картинни данни изнесени от meshcore-open'; + 'Картинни данни изнесени от meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open износ на данни за карта в формат GPX'; + 'meshcore-open износ на данни за карта в формат GPX'; @override - String get snrIndicator_nearByRepeaters => - 'Близки повтарящи се устройства'; + String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства'; @override - String get snrIndicator_lastSeen => 'Последно видян'; + String get snrIndicator_lastSeen => 'Последно видян'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 373467a..ed0ecf0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -15,7 +15,7 @@ class AppLocalizationsDe extends AppLocalizations { String get nav_contacts => 'Kontakte'; @override - String get nav_channels => 'Kanäle'; + String get nav_channels => 'Kanäle'; @override String get nav_map => 'Karte'; @@ -30,22 +30,22 @@ class AppLocalizationsDe extends AppLocalizations { String get common_connect => 'Verbinden'; @override - String get common_unknownDevice => 'Unbekanntes Gerät'; + String get common_unknownDevice => 'Unbekanntes Gerät'; @override String get common_save => 'Speichern'; @override - String get common_delete => 'Löschen'; + String get common_delete => 'Löschen'; @override - String get common_close => 'Schließen'; + String get common_close => 'Schließen'; @override String get common_edit => 'Bearbeiten'; @override - String get common_add => 'Hinzufügen'; + String get common_add => 'Hinzufügen'; @override String get common_settings => 'Einstellungen'; @@ -78,7 +78,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_hide => 'Ausblenden'; @override - String get common_remove => 'Löschen'; + String get common_remove => 'Löschen'; @override String get common_enable => 'Aktivieren'; @@ -93,7 +93,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_loading => 'Laden...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -115,25 +115,69 @@ class AppLocalizationsDe extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Über USB verbinden'; + String get usbScreenTitle => 'Über USB verbinden'; @override String get usbScreenSubtitle => - 'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.'; + 'Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.'; @override - String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus'; + String get usbScreenStatus => 'Wählen Sie ein USB-Gerät aus'; @override String get usbScreenNote => - 'USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; + 'USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; @override String get usbScreenEmptyState => - 'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.'; + 'Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.'; @override - String get scanner_scanning => 'Scannen nach Geräten...'; + String get usbErrorPermissionDenied => + 'Die USB-Berechtigung wurde abgelehnt.'; + + @override + String get usbErrorDeviceMissing => + 'Das ausgewählte USB-Gerät ist nicht mehr verfügbar.'; + + @override + String get usbErrorInvalidPort => 'Wählen Sie ein gültiges USB-Gerät aus.'; + + @override + String get usbErrorBusy => + 'Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.'; + + @override + String get usbErrorNotConnected => 'Es ist kein USB-Gerät angeschlossen.'; + + @override + String get usbErrorOpenFailed => + 'Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.'; + + @override + String get usbErrorConnectFailed => + 'Keine Verbindung zum ausgewählten USB-Gerät hergestellt.'; + + @override + String get usbErrorUnsupported => + 'Die Unterstützung für USB-Seriellschnittstellen ist auf dieser Plattform nicht vorhanden.'; + + @override + String get usbErrorAlreadyActive => + 'Eine USB-Verbindung ist bereits hergestellt.'; + + @override + String get usbErrorNoDeviceSelected => 'Kein USB-Gerät wurde ausgewählt.'; + + @override + String get usbErrorPortClosed => 'Die USB-Verbindung ist nicht aktiv.'; + + @override + String get usbErrorConnectTimedOut => + 'Die Wartezeit ist abgelaufen, da keine Antwort vom Gerät empfangen wurde.'; + + @override + String get scanner_scanning => 'Scannen nach Geräten...'; @override String get scanner_connecting => 'Verbunden...'; @@ -150,11 +194,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get scanner_searchingDevices => 'Suche nach MeshCore-Geräten...'; + String get scanner_searchingDevices => 'Suche nach MeshCore-Geräten...'; @override String get scanner_tapToScan => - 'Tippen Sie auf Scan, um MeshCore-Geräte zu finden.'; + 'Tippen Sie auf Scan, um MeshCore-Geräte zu finden.'; @override String scanner_connectionFailed(String error) { @@ -172,14 +216,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_bluetoothOffMessage => - 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; + 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; @override String get scanner_chromeRequired => 'Chrome Browser erforderlich'; @override String get scanner_chromeRequiredMessage => - 'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.'; + 'Diese Webanwendung erfordert Google Chrome oder einen Chromium-basierten Browser für die Bluetooth-Unterstützung.'; @override String get scanner_enableBluetooth => 'Bluetooth aktivieren'; @@ -194,7 +238,7 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_title => 'Einstellungen'; @override - String get settings_deviceInfo => 'Geräteinformationen'; + String get settings_deviceInfo => 'Geräteinformationen'; @override String get settings_appSettings => 'App-Einstellungen'; @@ -239,11 +283,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_locationBothRequired => - 'Bitte geben Sie sowohl Breite als auch Längengrad ein.'; + 'Bitte geben Sie sowohl Breite als auch Längengrad ein.'; @override - String get settings_locationInvalid => - 'Ungültige Breiten- oder Längengrade.'; + String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.'; @override String get settings_locationGPSEnable => 'GPS aktivieren'; @@ -253,7 +296,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Aktiviert GPS zur automatischen Aktualisierung des Standorts.'; @override - String get settings_locationIntervalSec => 'Intervall für GPS (Sekunden)'; + String get settings_locationIntervalSec => 'Intervall für GPS (Sekunden)'; @override String get settings_locationIntervalInvalid => @@ -263,18 +306,18 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_latitude => 'Breitengrad'; @override - String get settings_longitude => 'Längengrad'; + String get settings_longitude => 'Längengrad'; @override - String get settings_privacyMode => 'Privatsphäreeinstellung'; + String get settings_privacyMode => 'Privatsphäreeinstellung'; @override String get settings_privacyModeSubtitle => - 'Verstecken Sie Name/Ort in Ankündigungen'; + 'Verstecken Sie Name/Ort in Ankündigungen'; @override String get settings_privacyModeToggle => - 'Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.'; + 'Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.'; @override String get settings_privacyModeEnabled => 'Datenschutzmodus aktiviert'; @@ -286,20 +329,20 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_actions => 'Aktionen'; @override - String get settings_sendAdvertisement => 'Sende Ankündigung'; + String get settings_sendAdvertisement => 'Sende Ankündigung'; @override - String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung'; + String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung'; @override - String get settings_advertisementSent => 'Ankündigung gesendet'; + String get settings_advertisementSent => 'Ankündigung gesendet'; @override String get settings_syncTime => 'Zeitsynchronisierung'; @override String get settings_syncTimeSubtitle => - 'Stelle die Gerätezeit auf die Uhrzeit des Telefons ein'; + 'Stelle die Gerätezeit auf die Uhrzeit des Telefons ein'; @override String get settings_timeSynchronized => 'Zeit synchronisiert'; @@ -309,17 +352,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_refreshContactsSubtitle => - 'Kontakt-Liste vom Gerät neu laden'; + 'Kontakt-Liste vom Gerät neu laden'; @override - String get settings_rebootDevice => 'Gerät neu starten'; + String get settings_rebootDevice => 'Gerät neu starten'; @override - String get settings_rebootDeviceSubtitle => 'MeshCore-Gerät neu starten'; + String get settings_rebootDeviceSubtitle => 'MeshCore-Gerät neu starten'; @override String get settings_rebootDeviceConfirm => - 'Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.'; + 'Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.'; @override String get settings_debug => 'Fehlerbehebung'; @@ -338,7 +381,7 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_appDebugLogSubtitle => 'Anwendung Debug-Nachrichten'; @override - String get settings_about => 'Über'; + String get settings_about => 'Über'; @override String settings_aboutVersion(String version) { @@ -350,11 +393,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_aboutDescription => - 'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.'; + 'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.'; @override String get settings_aboutOpenMeteoAttribution => - 'LOS-Höhendaten: Open-Meteo (CC BY 4.0)'; + 'LOS-Höhendaten: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Name'; @@ -369,13 +412,13 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_infoBattery => 'Akku'; @override - String get settings_infoPublicKey => 'Öffentlicher Schlüssel'; + String get settings_infoPublicKey => 'Öffentlicher Schlüssel'; @override String get settings_infoContactsCount => 'Anzahl Kontakte'; @override - String get settings_infoChannelCount => 'Anzahl Kanäle'; + String get settings_infoChannelCount => 'Anzahl Kanäle'; @override String get settings_presets => 'Voreinstellungen'; @@ -387,7 +430,7 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_frequencyHelper => '300,00 - 2.500,00'; @override - String get settings_frequencyInvalid => 'Ungültige Frequenz (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Ungültige Frequenz (300-2500 MHz)'; @override String get settings_bandwidth => 'Bandbreite'; @@ -405,14 +448,14 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)'; + String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)'; @override String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss'; @override String get settings_clientRepeatSubtitle => - 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; + 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; @override String get settings_clientRepeatFreqWarning => @@ -451,10 +494,10 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -463,16 +506,16 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -481,10 +524,10 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russisch'; @@ -498,7 +541,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_enableMessageTracingSubtitle => - 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; + 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; @override String get appSettings_notifications => 'Benachrichtigungen'; @@ -508,7 +551,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_enableNotificationsSubtitle => - 'Erhalte Benachrichtigungen für Nachrichten und Ankündigungen'; + 'Erhalte Benachrichtigungen für Nachrichten und Ankündigungen'; @override String get appSettings_notificationPermissionDenied => @@ -539,7 +582,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_advertisementNotifications => - 'Ankündigungsbenachrichtigungen'; + 'Ankündigungsbenachrichtigungen'; @override String get appSettings_advertisementNotificationsSubtitle => @@ -550,19 +593,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetry => - 'Lösche Pfade bei Max Wiederholungsversuchen'; + 'Lösche Pfade bei Max Wiederholungsversuchen'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen'; + 'Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen'; @override String get appSettings_pathsWillBeCleared => - 'Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.'; + 'Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.'; @override String get appSettings_pathsWillNotBeCleared => - 'Die Pfade werden nicht automatisch gelöscht.'; + 'Die Pfade werden nicht automatisch gelöscht.'; @override String get appSettings_autoRouteRotation => 'Automatische Routenrotation'; @@ -587,21 +630,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Konfiguriert pro Gerät ($deviceName)'; + return 'Konfiguriert pro Gerät ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Verbinde ein Gerät, um zu wählen'; + 'Verbinde ein Gerät, um zu wählen'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0–4,2 V)'; + String get appSettings_batteryNmc => '18650 NMC (3,0–4,2 V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)'; + String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)'; @override String get appSettings_mapDisplay => 'Kartendarstellung'; @@ -673,11 +716,11 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_unitsImperial => 'Imperial (ft/mi)'; @override - String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt'; + String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Ausgewählte Fläche (Zoom $minZoom-$maxZoom)'; + return 'Ausgewählte Fläche (Zoom $minZoom-$maxZoom)'; } @override @@ -706,7 +749,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactsWillAppear => - 'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.'; + 'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.'; @override String get contacts_unread => 'Ungelesen'; @@ -747,7 +790,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Keine Kontakte oder Gruppen gefunden.'; @override - String get contacts_deleteContact => 'Lösche den Kontakt'; + String get contacts_deleteContact => 'Lösche den Kontakt'; @override String contacts_removeConfirm(String contactName) { @@ -764,17 +807,17 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_roomLogin => 'Raum-Login'; @override - String get contacts_openChat => 'Öffne Chat'; + String get contacts_openChat => 'Öffne Chat'; @override String get contacts_editGroup => 'Gruppe bearbeiten'; @override - String get contacts_deleteGroup => 'Löschen Gruppe'; + String get contacts_deleteGroup => 'Löschen Gruppe'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Löschen von \"$groupName\"?'; + return 'Löschen von \"$groupName\"?'; } @override @@ -826,19 +869,19 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get channels_title => 'Kanäle'; + String get channels_title => 'Kanäle'; @override - String get channels_noChannelsConfigured => 'Keine Kanäle konfiguriert'; + String get channels_noChannelsConfigured => 'Keine Kanäle konfiguriert'; @override - String get channels_addPublicChannel => 'Öffentlichen Kanal hinzufügen'; + String get channels_addPublicChannel => 'Öffentlichen Kanal hinzufügen'; @override - String get channels_searchChannels => 'Suche Kanäle...'; + String get channels_searchChannels => 'Suche Kanäle...'; @override - String get channels_noChannelsFound => 'Keine Kanäle gefunden'; + String get channels_noChannelsFound => 'Keine Kanäle gefunden'; @override String channels_channelIndex(int index) { @@ -849,13 +892,13 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_hashtagChannel => 'Hashtag-Kanal'; @override - String get channels_public => 'Öffentlich'; + String get channels_public => 'Öffentlich'; @override String get channels_private => 'Privat'; @override - String get channels_publicChannel => 'Öffentlicher Kanal'; + String get channels_publicChannel => 'Öffentlicher Kanal'; @override String get channels_privateChannel => 'Privater Kanal'; @@ -870,25 +913,25 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben'; @override - String get channels_deleteChannel => 'Lösche den Kanal'; + String get channels_deleteChannel => 'Lösche den Kanal'; @override String channels_deleteChannelConfirm(String name) { - return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; + return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; } @override String channels_channelDeleteFailed(String name) { - return 'Kanal $name konnte nicht gelöscht werden'; + return 'Kanal $name konnte nicht gelöscht werden'; } @override String channels_channelDeleted(String name) { - return 'Kanal \"$name\" gelöscht'; + return 'Kanal \"$name\" gelöscht'; } @override - String get channels_addChannel => 'Kanal hinzufügen'; + String get channels_addChannel => 'Kanal hinzufügen'; @override String get channels_channelIndexLabel => 'Kanalindex'; @@ -897,16 +940,16 @@ class AppLocalizationsDe extends AppLocalizations { String get channels_channelName => 'Kanalname'; @override - String get channels_usePublicChannel => 'Verwende öffentlichen Kanal'; + String get channels_usePublicChannel => 'Verwende öffentlichen Kanal'; @override - String get channels_standardPublicPsk => 'Öffentliche Standard PSK'; + String get channels_standardPublicPsk => 'Öffentliche Standard PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Zufällige PSK generieren'; + String get channels_generateRandomPsk => 'Zufällige PSK generieren'; @override String get channels_enterChannelName => @@ -918,7 +961,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channels_channelAdded(String name) { - return 'Kanal \"$name\" hinzugefügt'; + return 'Kanal \"$name\" hinzugefügt'; } @override @@ -935,7 +978,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Öffentlicher Kanal hinzugefügt'; + String get channels_publicChannelAdded => 'Öffentlicher Kanal hinzugefügt'; @override String get channels_sortBy => 'Sortiere nach'; @@ -957,7 +1000,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_createPrivateChannelDesc => - 'Verschlüsselt mit einem geheimen Schlüssel.'; + 'Verschlüsselt mit einem geheimen Schlüssel.'; @override String get channels_joinPrivateChannel => @@ -965,10 +1008,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_joinPrivateChannelDesc => - 'Manuelle Eingabe eines geheimen Schlüssels.'; + 'Manuelle Eingabe eines geheimen Schlüssels.'; @override - String get channels_joinPublicChannel => 'Tritt dem öffentlichen Kanal bei'; + String get channels_joinPublicChannel => 'Tritt dem öffentlichen Kanal bei'; @override String get channels_joinPublicChannelDesc => @@ -980,13 +1023,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_joinHashtagChannelDesc => - 'Jeder kann sich bei Hashtag-Kanälen beteiligen.'; + 'Jeder kann sich bei Hashtag-Kanälen beteiligen.'; @override String get channels_scanQrCode => 'Scannen Sie einen QR-Code'; @override - String get channels_scanQrCodeComingSoon => 'Bald verfügbar'; + String get channels_scanQrCodeComingSoon => 'Bald verfügbar'; @override String get channels_enterHashtag => 'Gib Hashtag ein'; @@ -1033,7 +1076,7 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_messageCopied => 'Nachricht kopiert'; @override - String get chat_messageDeleted => 'Nachricht gelöscht'; + String get chat_messageDeleted => 'Nachricht gelöscht'; @override String get chat_retryingMessage => 'Versuche es erneut.'; @@ -1050,7 +1093,7 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_reply => 'Beantworten'; @override - String get chat_addReaction => 'Reaktion hinzufügen'; + String get chat_addReaction => 'Reaktion hinzufügen'; @override String get chat_me => 'Ich'; @@ -1068,7 +1111,7 @@ class AppLocalizationsDe extends AppLocalizations { String get emojiCategoryObjects => 'Objekte'; @override - String get gifPicker_title => 'Wähle ein GIF'; + String get gifPicker_title => 'Wähle ein GIF'; @override String get gifPicker_searchHint => 'Suche nach GIFs...'; @@ -1098,7 +1141,7 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_copyLog => 'Kopieren des Protokolls'; @override - String get debugLog_clearLog => 'Protokoll löschen'; + String get debugLog_clearLog => 'Protokoll löschen'; @override String get debugLog_copied => 'Debug-Protokoll kopiert'; @@ -1107,7 +1150,7 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_bleCopied => 'BLE-Protokoll kopiert'; @override - String get debugLog_noEntries => 'No Debug-Protokolle noch verfügbar'; + String get debugLog_noEntries => 'No Debug-Protokolle noch verfügbar'; @override String get debugLog_enableInSettings => @@ -1120,11 +1163,11 @@ class AppLocalizationsDe extends AppLocalizations { String get debugLog_rawLogRx => 'Roh-Log-RX'; @override - String get debugLog_noBleActivity => 'Bisher keine BLE-Aktivität'; + String get debugLog_noBleActivity => 'Bisher keine BLE-Aktivität'; @override String debugFrame_length(int count) { - return 'Rahmenlänge: $count Bytes'; + return 'Rahmenlänge: $count Bytes'; } @override @@ -1137,7 +1180,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String debugFrame_destinationPubKey(String pubKey) { - return '- Ziel-Public-Schlüssel: $pubKey'; + return '- Ziel-Public-Schlüssel: $pubKey'; } @override @@ -1191,20 +1234,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_pathHistoryFull => - 'Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.'; + 'Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.'; @override String get chat_hopSingular => 'Sprung'; @override - String get chat_hopPlural => 'Sprünge'; + String get chat_hopPlural => 'Sprünge'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'Sprünge', + other: 'Sprünge', one: 'Sprung', ); return '$count $_temp0'; @@ -1230,15 +1273,15 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_setCustomPathSubtitle => 'Manuellen Routenpfad festlegen'; @override - String get chat_clearPath => 'Pfad zurücksetzen'; + String get chat_clearPath => 'Pfad zurücksetzen'; @override String get chat_clearPathSubtitle => - 'Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.'; + 'Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.'; @override String get chat_pathCleared => - 'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.'; + 'Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.'; @override String get chat_floodModeSubtitle => @@ -1248,11 +1291,11 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_floodModeEnabled => 'Flutmodus aktiviert.'; @override - String get chat_fullPath => 'Vollständiger Pfad'; + String get chat_fullPath => 'Vollständiger Pfad'; @override String get chat_pathDetailsNotAvailable => - 'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.'; + 'Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1270,10 +1313,10 @@ class AppLocalizationsDe extends AppLocalizations { 'Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.'; @override - String get chat_pathDeviceConfirmed => 'Gerät bestätigt.'; + String get chat_pathDeviceConfirmed => 'Gerät bestätigt.'; @override - String get chat_pathDeviceNotConfirmed => 'Gerät noch nicht bestätigt.'; + String get chat_pathDeviceNotConfirmed => 'Gerät noch nicht bestätigt.'; @override String get chat_type => 'Gebe ein'; @@ -1282,7 +1325,7 @@ class AppLocalizationsDe extends AppLocalizations { String get chat_path => 'Pfad'; @override - String get chat_publicKey => 'Öffentlicher Schlüssel'; + String get chat_publicKey => 'Öffentlicher Schlüssel'; @override String get chat_compressOutgoingMessages => @@ -1296,7 +1339,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String chat_hopsForced(int count) { - return '$count Sprünge (erzwungen)'; + return '$count Sprünge (erzwungen)'; } @override @@ -1314,22 +1357,22 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get chat_openLink => 'Link öffnen?'; + String get chat_openLink => 'Link öffnen?'; @override String get chat_openLinkConfirmation => - 'Möchten Sie diesen Link in Ihrem Browser öffnen?'; + 'Möchten Sie diesen Link in Ihrem Browser öffnen?'; @override - String get chat_open => 'Öffnen'; + String get chat_open => 'Öffnen'; @override String chat_couldNotOpenLink(String url) { - return 'Link konnte nicht geöffnet werden: $url'; + return 'Link konnte nicht geöffnet werden: $url'; } @override - String get chat_invalidLink => 'Ungültiges Link-Format'; + String get chat_invalidLink => 'Ungültiges Link-Format'; @override String get map_title => 'Karte'; @@ -1345,7 +1388,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_nodesNeedGps => - 'Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.'; + 'Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.'; @override String map_nodesCount(int count) { @@ -1383,7 +1426,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_disconnectConfirm => - 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; + 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; @override String get map_from => 'Von'; @@ -1413,19 +1456,19 @@ class AppLocalizationsDe extends AppLocalizations { String get map_sendToChannel => 'Senden an Kanal'; @override - String get map_noChannelsAvailable => 'Keine Kanäle verfügbar'; + String get map_noChannelsAvailable => 'Keine Kanäle verfügbar'; @override - String get map_publicLocationShare => 'Öffentliche Standortfreigabe'; + String get map_publicLocationShare => 'Öffentliche Standortfreigabe'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Sie werden kurz darauf einen Ort in $channelLabel teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.'; + return 'Sie werden kurz darauf einen Ort in $channelLabel teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.'; } @override String get map_connectToShareMarkers => - 'Verbinde ein Gerät, um Marker zu teilen'; + 'Verbinde ein Gerät, um Marker zu teilen'; @override String get map_filterNodes => 'Knotenfilter'; @@ -1443,13 +1486,13 @@ class AppLocalizationsDe extends AppLocalizations { String get map_otherNodes => 'Andere Knoten'; @override - String get map_keyPrefix => 'Schlüsselpräfix'; + String get map_keyPrefix => 'Schlüsselpräfix'; @override - String get map_filterByKeyPrefix => 'Filter nach Schlüsselpräfix'; + String get map_filterByKeyPrefix => 'Filter nach Schlüsselpräfix'; @override - String get map_publicKeyPrefix => 'Schlüsselpräfix'; + String get map_publicKeyPrefix => 'Schlüsselpräfix'; @override String get map_markers => 'Marker'; @@ -1471,10 +1514,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_tapToAdd => - 'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.'; + 'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.'; @override - String get map_runTrace => 'Pfadverlauf ausführen'; + String get map_runTrace => 'Pfadverlauf ausführen'; @override String get map_removeLast => 'Letztes Entfernen'; @@ -1487,18 +1530,18 @@ class AppLocalizationsDe extends AppLocalizations { @override String get mapCache_selectAreaFirst => - 'Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.'; + 'Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.'; @override String get mapCache_noTilesToDownload => - 'Keine Kacheln für diese Region zum Herunterladen verfügbar.'; + 'Keine Kacheln für diese Region zum Herunterladen verfügbar.'; @override String get mapCache_downloadTilesTitle => 'Herunterladen von Kacheln'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Laden $count Kacheln für den Offline-Bereich herunter?'; + return 'Laden $count Kacheln für den Offline-Bereich herunter?'; } @override @@ -1522,10 +1565,10 @@ class AppLocalizationsDe extends AppLocalizations { 'Alle zwischengespeicherten Kartenraster entfernen?'; @override - String get mapCache_offlineCacheCleared => 'Offline-Cache gelöscht'; + String get mapCache_offlineCacheCleared => 'Offline-Cache gelöscht'; @override - String get mapCache_noAreaSelected => 'Kein Bereich ausgewählt'; + String get mapCache_noAreaSelected => 'Kein Bereich ausgewählt'; @override String get mapCache_cacheArea => 'Zwischenspeicherbereich'; @@ -1538,7 +1581,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String mapCache_estimatedTiles(int count) { - return 'Geschätzte Kacheln: $count'; + return 'Geschätzte Kacheln: $count'; } @override @@ -1620,7 +1663,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dialog_disconnectConfirm => - 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; + 'Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?'; @override String get login_repeaterLogin => 'Beim Repeater anmelden'; @@ -1639,7 +1682,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'Das Passwort wird auf diesem Gerät sicher gespeichert.'; + 'Das Passwort wird auf diesem Gerät sicher gespeichert.'; @override String get login_repeaterDescription => @@ -1686,7 +1729,7 @@ class AppLocalizationsDe extends AppLocalizations { String get common_reload => 'Neu laden'; @override - String get common_clear => 'Löschen'; + String get common_clear => 'Löschen'; @override String path_currentPath(String path) { @@ -1712,21 +1755,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.'; + 'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.'; @override String get path_hexPrefixExample => - 'Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)'; + 'Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)'; @override - String get path_labelHexPrefixes => 'Pfad (Hex-Präfixe)'; + String get path_labelHexPrefixes => 'Pfad (Hex-Präfixe)'; @override String get path_helperMaxHops => - 'Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)'; + 'Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)'; @override - String get path_selectFromContacts => 'Oder wähle aus Kontakten aus:'; + String get path_selectFromContacts => 'Oder wähle aus Kontakten aus:'; @override String get path_noRepeatersFound => @@ -1734,11 +1777,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get path_customPathsRequire => - 'Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.'; + 'Benutzerdefinierte Pfade erfordern Zwischen-Hops, die Nachrichten weiterleiten können.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Ungültige Hexadezimal-Präfixe: $prefixes'; + return 'Ungültige Hexadezimal-Präfixe: $prefixes'; } @override @@ -1826,10 +1869,10 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_clockAtLogin => 'Uhr (bei Anmeldung)'; @override - String get repeater_uptime => 'Verfügbarkeit'; + String get repeater_uptime => 'Verfügbarkeit'; @override - String get repeater_queueLength => 'Warteschlangenlänge'; + String get repeater_queueLength => 'Warteschlangenlänge'; @override String get repeater_debugFlags => 'Fehlerbehebungsoptionen'; @@ -1904,7 +1947,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_repeaterName => 'Repeater Name'; @override - String get repeater_repeaterNameHelper => 'Anzeigename für diesen Repeater'; + String get repeater_repeaterNameHelper => 'Anzeigename für diesen Repeater'; @override String get repeater_adminPassword => 'Admin-Passwort'; @@ -1917,7 +1960,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_guestPasswordHelper => - 'Schreibgeschütztes Zugriffspasswort'; + 'Schreibgeschütztes Zugriffspasswort'; @override String get repeater_radioSettings => 'Funk Einstellungen'; @@ -1953,7 +1996,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_latitudeHelper => 'Dezimalgrad (z.B. 37,7749)'; @override - String get repeater_longitude => 'Längengrad'; + String get repeater_longitude => 'Längengrad'; @override String get repeater_longitudeHelper => 'Dezimalgrad (z.B. -122,4194)'; @@ -1973,21 +2016,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_guestAccessSubtitle => - 'Gast-Zugriff mit beschränkten Rechten zulassen'; + 'Gast-Zugriff mit beschränkten Rechten zulassen'; @override - String get repeater_privacyMode => 'Privatsphäreeinstellung'; + String get repeater_privacyMode => 'Privatsphäreeinstellung'; @override String get repeater_privacyModeSubtitle => - 'Verstecken Sie Name/Ort in Ankündigungen'; + 'Verstecken Sie Name/Ort in Ankündigungen'; @override - String get repeater_advertisementSettings => 'Ankündigungseinstellungen'; + String get repeater_advertisementSettings => 'Ankündigungseinstellungen'; @override String get repeater_localAdvertInterval => - 'Intervall der lokalen Ankündigungen'; + 'Intervall der lokalen Ankündigungen'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -1996,7 +2039,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervall der gefluteten Ankündigungen'; + 'Intervall der gefluteten Ankündigungen'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2005,7 +2048,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervall der verschlüsselten Ankündigung'; + 'Intervall der verschlüsselten Ankündigung'; @override String get repeater_dangerZone => 'Gefahrenzone'; @@ -2014,26 +2057,26 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_rebootRepeater => 'Neustart Repeater'; @override - String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.'; + String get repeater_rebootRepeaterSubtitle => 'Repeater-Gerät neu starten.'; @override String get repeater_rebootRepeaterConfirm => - 'Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?'; + 'Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?'; @override String get repeater_regenerateIdentityKey => - 'Schlüssel für die Identitätswiederherstellung'; + 'Schlüssel für die Identitätswiederherstellung'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Neuen öffentlichen/privaten Schlüsselpaar generieren'; + 'Neuen öffentlichen/privaten Schlüsselpaar generieren'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Dies generiert eine neue Identität für den Repeater. Fortfahren?'; + 'Dies generiert eine neue Identität für den Repeater. Fortfahren?'; @override - String get repeater_eraseFileSystem => 'Dateisystem löschen'; + String get repeater_eraseFileSystem => 'Dateisystem löschen'; @override String get repeater_eraseFileSystemSubtitle => @@ -2041,11 +2084,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!'; + 'WARNUNG: Dies löscht alle Daten auf dem Repeater. Dies kann nicht rückgängig gemacht werden!'; @override String get repeater_eraseSerialOnly => - 'Löschen ist nur über die serielle Konsole möglich.'; + 'Löschen ist nur über die serielle Konsole möglich.'; @override String repeater_commandSent(String command) { @@ -2058,7 +2101,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get repeater_confirm => 'Bestätigen'; + String get repeater_confirm => 'Bestätigen'; @override String get repeater_settingsSaved => 'Einstellungen erfolgreich gespeichert'; @@ -2096,7 +2139,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_refreshAdvertisementSettings => - 'Aktualisieren Sie die Ankündigungseinstellungen'; + 'Aktualisieren Sie die Ankündigungseinstellungen'; @override String repeater_refreshed(String label) { @@ -2112,13 +2155,13 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliTitle => 'Repeater CLI'; @override - String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls'; + String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls'; @override String get repeater_commandHelp => 'Hilfe'; @override - String get repeater_clearHistory => 'Löschen der Historie'; + String get repeater_clearHistory => 'Löschen der Historie'; @override String get repeater_noCommandsSent => 'Noch keine Befehle gesendet.'; @@ -2134,7 +2177,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_previousCommand => 'Vorhergehende Aktion'; @override - String get repeater_nextCommand => 'Nächste Aktion'; + String get repeater_nextCommand => 'Nächste Aktion'; @override String get repeater_enterCommandFirst => 'Geben Sie zuerst einen Befehl ein'; @@ -2163,52 +2206,52 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliQuickVersion => 'Version'; @override - String get repeater_cliQuickAdvertise => 'Ankündigungen'; + String get repeater_cliQuickAdvertise => 'Ankündigungen'; @override String get repeater_cliQuickClock => 'Uhr'; @override - String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; + String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung'; @override String get repeater_cliHelpReboot => - 'Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer \'Timeout\'-Situation kommt, was normal ist.)'; + 'Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer \'Timeout\'-Situation kommt, was normal ist.)'; @override String get repeater_cliHelpClock => - 'Zeigt die aktuelle Uhrzeit pro Gerät an.'; + 'Zeigt die aktuelle Uhrzeit pro Gerät an.'; @override String get repeater_cliHelpPassword => - 'Legt ein neues Administrator-Passwort für das Gerät fest.'; + 'Legt ein neues Administrator-Passwort für das Gerät fest.'; @override String get repeater_cliHelpVersion => - 'Zeigt die Geräteversion und das Datum des Firmware-Builds an.'; + 'Zeigt die Geräteversion und das Datum des Firmware-Builds an.'; @override String get repeater_cliHelpClearStats => - 'Setzt verschiedene Statistikberechnungen auf Null zurück.'; + 'Setzt verschiedene Statistikberechnungen auf Null zurück.'; @override String get repeater_cliHelpSetAf => 'Legt den Luftzeitfaktor fest.'; @override String get repeater_cliHelpSetTx => - 'Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)'; + 'Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)'; @override String get repeater_cliHelpSetRepeat => - 'Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.'; + 'Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Raumspeicher) Wenn \'an\', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).'; + '(Raumspeicher) Wenn \'an\', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).'; @override String get repeater_cliHelpSetFloodMax => - 'Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)'; + 'Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)'; @override String get repeater_cliHelpSetIntThresh => @@ -2216,7 +2259,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetAgcResetInterval => - 'Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetMultiAcks => @@ -2224,78 +2267,78 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.'; + 'Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.'; @override String get repeater_cliHelpSetGuestPassword => - 'Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)'; + 'Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)'; @override String get repeater_cliHelpSetName => 'Legt den Anzeigenamen fest.'; @override String get repeater_cliHelpSetLat => - 'Legt die Breitengrad der Ankündigung fest. (dezimale Grad)'; + 'Legt die Breitengrad der Ankündigung fest. (dezimale Grad)'; @override String get repeater_cliHelpSetLon => - 'Legt die Längengrade der Ankündigung fest. (dezimale Grad)'; + 'Legt die Längengrade der Ankündigung fest. (dezimale Grad)'; @override String get repeater_cliHelpSetRadio => - 'Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.'; + 'Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.'; @override String get repeater_cliHelpSetRxDelay => - 'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetTxDelay => - 'Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).'; + 'Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.'; + 'Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.'; @override String get repeater_cliHelpSetBridgeEnabled => - 'Brücke aktivieren/deaktivieren.'; + 'Brücke aktivieren/deaktivieren.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Setze Verzögerung vor erneuter Übertragung von Paketen.'; + 'Setze Verzögerung vor erneuter Übertragung von Paketen.'; @override String get repeater_cliHelpSetBridgeSource => - 'Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.'; + 'Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Setze die serielle Link-Baudrate für RS232-Brücken.'; + 'Setze die serielle Link-Baudrate für RS232-Brücken.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Richte das Brückenpassword ein.'; + 'Richte das Brückenpassword ein.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).'; + 'Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).'; @override String get repeater_cliHelpTempRadio => - 'Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).'; + 'Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).'; @override String get repeater_cliHelpSetPerm => - 'Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)'; + 'Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => - 'Ruft Brückentyp: none, rs232, espnow ab.'; + 'Ruft Brückentyp: none, rs232, espnow ab.'; @override String get repeater_cliHelpLogStart => @@ -2307,46 +2350,46 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpLogErase => - 'Löscht die Paketprotokolle aus dem Dateisystem.'; + 'Löscht die Paketprotokolle aus dem Dateisystem.'; @override String get repeater_cliHelpNeighbors => - 'Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4'; + 'Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.'; + 'Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.'; @override String get repeater_cliHelpRegion => 'Listet alle definierten Regionen auf.'; @override String get repeater_cliHelpRegionLoad => - 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.'; + 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.'; @override String get repeater_cliHelpRegionGet => - 'Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) \'F\'\".'; + 'Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) \'F\'\".'; @override String get repeater_cliHelpRegionPut => - 'Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.'; + 'Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.'; @override String get repeater_cliHelpRegionRemove => - 'Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)'; + 'Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)'; @override String get repeater_cliHelpRegionAllowf => - 'Legt die \'Flut\'-Berechtigung für die angegebene Region fest. (\'\' für den globalen/legacy-Bereich)'; + 'Legt die \'Flut\'-Berechtigung für die angegebene Region fest. (\'\' für den globalen/legacy-Bereich)'; @override String get repeater_cliHelpRegionDenyf => - 'Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)'; + 'Entfernt die \"F\"lood-Berechtigung für die angegebene Region. (ANMERKUNG: in dieser Phase wird nicht empfohlen, dies auf dem globalen/legacy-Bereich zu verwenden!!)'; @override String get repeater_cliHelpRegionHome => - 'Antwortet mit der aktuellen \'Home\'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)'; + 'Antwortet mit der aktuellen \'Home\'-Region. (Hinweis wurde bisher nirgendwo angewendet, für zukünftige Zwecke reserviert)'; @override String get repeater_cliHelpRegionHomeSet => 'Legt die \'Home\'-Region fest.'; @@ -2368,11 +2411,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpGpsSetLoc => - 'Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.'; + 'Setze die Position des Knotens auf GPS-Koordinaten und speichere die Präferenzen.'; @override String get repeater_cliHelpGpsAdvert => - 'Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen'; + 'Gibt Konfiguration für die Standortanzeige des Knotens:\n- none: Standort nicht in Anzeigen einbeziehen\n- share: GPS-Standort teilen (von SensorManager)\n- prefs: Standort aus Einstellungen anzeigen'; @override String get repeater_cliHelpGpsAdvertSet => @@ -2383,7 +2426,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_commandsListNote => - 'ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.'; + 'ACHTUNG: Für die verschiedenen „set ...“-Befehle gibt es auch einen „get ...“-Befehl.'; @override String get repeater_general => 'Allgemein'; @@ -2392,7 +2435,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_settingsCategory => 'Einstellungen'; @override - String get repeater_bridge => 'Brücke'; + String get repeater_bridge => 'Brücke'; @override String get repeater_logging => 'Protokollierung'; @@ -2406,14 +2449,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_regionNote => - 'Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.'; + 'Region-Befehle wurden eingeführt, um Region-Definitionen und Berechtigungen zu verwalten.'; @override String get repeater_gpsManagement => 'GPS-Verwaltung'; @override String get repeater_gpsNote => - 'Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.'; + 'Der GPS-Befehl wurde eingeführt, um Standortbezogene Themen zu verwalten.'; @override String get telemetry_receivedData => 'Empfangene Telemetriedaten'; @@ -2428,7 +2471,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get telemetry_noData => 'Keine Telemetriedaten verfügbar.'; + String get telemetry_noData => 'Keine Telemetriedaten verfügbar.'; @override String telemetry_channelTitle(int channel) { @@ -2467,7 +2510,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2486,7 +2529,7 @@ class AppLocalizationsDe extends AppLocalizations { String get neighbors_repeatersNeighbors => 'Nachbarn'; @override - String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; + String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; @override String neighbors_unknownContact(String pubkey) { @@ -2495,7 +2538,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Gehört vor: $time'; + return 'Gehört vor: $time'; } @override @@ -2508,11 +2551,11 @@ class AppLocalizationsDe extends AppLocalizations { String get channelPath_otherObservedPaths => 'Sonstige beobachtete Pfade'; @override - String get channelPath_repeaterHops => 'Repeater-Sprünge'; + String get channelPath_repeaterHops => 'Repeater-Sprünge'; @override String get channelPath_noHopDetails => - 'Die Detailangaben für dieses Paket sind nicht verfügbar.'; + 'Die Detailangaben für dieses Paket sind nicht verfügbar.'; @override String get channelPath_messageDetails => 'Nachrichtendetails'; @@ -2536,7 +2579,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Beobachteter Pfad $index • $hops'; + return 'Beobachteter Pfad $index • $hops'; } @override @@ -2563,12 +2606,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String channelPath_observedZeroOf(int total) { - return '0 von $total Sprüngen'; + return '0 von $total Sprüngen'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed von $total Sprüngen'; + return '$observed von $total Sprüngen'; } @override @@ -2576,11 +2619,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.'; + 'Für diesen Pfad stehen keine Repeater-Positionen zur Verfügung.'; @override String channelPath_primaryPath(int index) { - return 'Pfad $index (Primär)'; + return 'Pfad $index (Primär)'; } @override @@ -2591,12 +2634,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Keine Informationen zu dieser Paketroute verfügbar.'; + 'Keine Informationen zu dieser Paketroute verfügbar.'; @override String get channelPath_unknownRepeater => 'Unbekannter Repeater'; @@ -2609,7 +2652,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get community_createDesc => - 'Erstelle eine neue Community und teile sie über den QR-Code.'; + 'Erstelle eine neue Community und teile sie über den QR-Code.'; @override String get community_join => 'Beitreten'; @@ -2619,7 +2662,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_joinConfirmation(String name) { - return 'Möchten Sie sich der Community \"$name\" anschließen?'; + return 'Möchten Sie sich der Community \"$name\" anschließen?'; } @override @@ -2633,7 +2676,7 @@ class AppLocalizationsDe extends AppLocalizations { String get community_showQr => 'Zeige QR-Code'; @override - String get community_publicChannel => 'Community Öffentlich'; + String get community_publicChannel => 'Community Öffentlich'; @override String get community_hashtagChannel => 'Community Hashtag'; @@ -2659,15 +2702,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Scannen Sie diesen QR-Code, um sich \"$name\" anzuschließen.'; + return 'Scannen Sie diesen QR-Code, um sich \"$name\" anzuschließen.'; } @override String get community_hashtagPrivacyHint => - 'Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden'; + 'Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden'; @override - String get community_invalidQrCode => 'Ungültiger Community-QR-Code'; + String get community_invalidQrCode => 'Ungültiger Community-QR-Code'; @override String get community_alreadyMember => 'Bereits registriert'; @@ -2679,11 +2722,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get community_addPublicChannel => - 'Füge einen öffentlichen Community-Kanal hinzu'; + 'Füge einen öffentlichen Community-Kanal hinzu'; @override String get community_addPublicChannelHint => - 'Automatisch den öffentlichen Kanal für diese Community hinzufügen'; + 'Automatisch den öffentlichen Kanal für diese Community hinzufügen'; @override String get community_noCommunities => 'Noch keiner Community beigetreten'; @@ -2705,7 +2748,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Dies löscht auch $count Kanal/Kanäle und deren Nachrichten.'; + return 'Dies löscht auch $count Kanal/Kanäle und deren Nachrichten.'; } @override @@ -2714,11 +2757,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get community_regenerateSecret => 'Neugenerierung des Schlüssels'; + String get community_regenerateSecret => 'Neugenerierung des Schlüssels'; @override String community_regenerateSecretConfirm(String name) { - return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.'; + return 'Nehmen Sie den geheimen Schlüssel für \"$name\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.'; } @override @@ -2726,50 +2769,50 @@ class AppLocalizationsDe extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich'; + return 'Wiederherstellung des Schlüssels für \"$name\" erfolgreich'; } @override - String get community_updateSecret => 'Aktualisieren Sie den Schlüssel'; + String get community_updateSecret => 'Aktualisieren Sie den Schlüssel'; @override String community_secretUpdated(String name) { - return 'Schlüssel für \"$name\" aktualisiert'; + return 'Schlüssel für \"$name\" aktualisiert'; } @override String community_scanToUpdateSecret(String name) { - return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.'; + return 'Scannen Sie den neuen QR-Code, um das Geheimnis für \"$name\" zu aktualisieren.'; } @override String get community_addHashtagChannel => - 'Füge einen Community-Hashtag hinzu'; + 'Füge einen Community-Hashtag hinzu'; @override String get community_addHashtagChannelDesc => - 'Füge einen Hashtag-Kanal für diese Community hinzu'; + 'Füge einen Hashtag-Kanal für diese Community hinzu'; @override - String get community_selectCommunity => 'Wählen Sie eine Community'; + String get community_selectCommunity => 'Wählen Sie eine Community'; @override - String get community_regularHashtag => 'Regulärer Hashtag'; + String get community_regularHashtag => 'Regulärer Hashtag'; @override String get community_regularHashtagDesc => - 'Öffentlicher Hashtag (jeder kann teilnehmen)'; + 'Öffentlicher Hashtag (jeder kann teilnehmen)'; @override String get community_communityHashtag => 'Community Hashtag'; @override String get community_communityHashtagDesc => - 'Nur für Mitglieder der Community'; + 'Nur für Mitglieder der Community'; @override String community_forCommunity(String name) { - return 'Für $name'; + return 'Für $name'; } @override @@ -2782,7 +2825,7 @@ class AppLocalizationsDe extends AppLocalizations { String get listFilter_latestMessages => 'Letzte Nachrichten'; @override - String get listFilter_heardRecently => 'Kürzlich gehört'; + String get listFilter_heardRecently => 'Kürzlich gehört'; @override String get listFilter_az => 'A-Z'; @@ -2797,7 +2840,7 @@ class AppLocalizationsDe extends AppLocalizations { String get listFilter_favorites => 'Favoriten'; @override - String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen'; + String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen'; @override String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen'; @@ -2824,7 +2867,7 @@ class AppLocalizationsDe extends AppLocalizations { String get pathTrace_failed => 'Pfadverfolgung fehlgeschlagen.'; @override - String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.'; + String get pathTrace_notAvailable => 'Pfadverfolgung nicht verfügbar.'; @override String get pathTrace_refreshTooltip => 'Path Trace aktualisieren.'; @@ -2834,30 +2877,30 @@ class AppLocalizationsDe extends AppLocalizations { 'Bei einer oder mehreren Knoten fehlt der Standort!'; @override - String get pathTrace_clearTooltip => 'Pfad löschen'; + String get pathTrace_clearTooltip => 'Pfad löschen'; @override String get losSelectStartEnd => - 'Wählen Sie Start- und Endknoten für LOS aus.'; + 'Wählen Sie Start- und Endknoten für LOS aus.'; @override String losRunFailed(String error) { - return 'Sichtlinienprüfung fehlgeschlagen: $error'; + return 'Sichtlinienprüfung fehlgeschlagen: $error'; } @override - String get losClearAllPoints => 'Löschen Sie alle Punkte'; + String get losClearAllPoints => 'Löschen Sie alle Punkte'; @override String get losRunToViewElevationProfile => - 'Führen Sie LOS aus, um das Höhenprofil anzuzeigen'; + 'Führen Sie LOS aus, um das Höhenprofil anzuzeigen'; @override - String get losMenuTitle => 'LOS-Menü'; + String get losMenuTitle => 'LOS-Menü'; @override String get losMenuSubtitle => - 'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen'; + 'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen'; @override String get losShowDisplayNodes => 'Anzeigeknoten anzeigen'; @@ -2887,10 +2930,10 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get losRun => 'Führen Sie LOS aus'; + String get losRun => 'Führen Sie LOS aus'; @override - String get losNoElevationData => 'Keine Höhendaten'; + String get losNoElevationData => 'Keine Höhendaten'; @override String losProfileClear( @@ -2913,7 +2956,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get losStatusChecking => 'LOS: Überprüfen...'; + String get losStatusChecking => 'LOS: Überprüfen...'; @override String get losStatusNoData => 'LOS: keine Daten'; @@ -2925,11 +2968,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.'; + 'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.'; @override String get losErrorInvalidInput => - 'Ungültige Punkte/Höhendaten für die LOS-Berechnung.'; + 'Ungültige Punkte/Höhendaten für die LOS-Berechnung.'; @override String get losRenameCustomPoint => @@ -2945,7 +2988,7 @@ class AppLocalizationsDe extends AppLocalizations { String get losHidePanelTooltip => 'LOS-Panel ausblenden'; @override - String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; + String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Funkhorizont'; @@ -2954,7 +2997,7 @@ class AppLocalizationsDe extends AppLocalizations { String get losLegendLosBeam => 'Sichtlinie'; @override - String get losLegendTerrain => 'Gelände'; + String get losLegendTerrain => 'Gelände'; @override String get losFrequencyLabel => 'Frequenz'; @@ -2972,7 +3015,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; } @override @@ -3005,7 +3048,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_clipboardEmpty => 'Die Zwischenablage ist leer.'; @override - String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten'; + String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten'; @override String get contacts_contactImported => 'Kontakt wurde importiert.'; @@ -3015,28 +3058,28 @@ class AppLocalizationsDe extends AppLocalizations { 'Kontakt konnte nicht importiert werden'; @override - String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung'; + String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung'; @override - String get contacts_floodAdvert => 'Flut-Ankündigung'; + String get contacts_floodAdvert => 'Flut-Ankündigung'; @override String get contacts_copyAdvertToClipboard => - 'Ankündigung in die Zwischenablage kopieren'; + 'Ankündigung in die Zwischenablage kopieren'; @override String get contacts_addContactFromClipboard => - 'Kontakt aus Zwischenablage hinzufügen'; + 'Kontakt aus Zwischenablage hinzufügen'; @override String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren'; @override - String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen'; + String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen'; @override String get contacts_zeroHopContactAdvertSent => - 'Kontakt über Anzeige gesendet'; + 'Kontakt über Anzeige gesendet'; @override String get contacts_zeroHopContactAdvertFailed => @@ -3048,10 +3091,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => - 'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.'; + 'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.'; @override - String get notification_activityTitle => 'MeshCore Aktivität'; + String get notification_activityTitle => 'MeshCore Aktivität'; @override String notification_messagesCount(int count) { @@ -3124,7 +3167,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_gpxExportNotAvailable => - 'Nicht auf Ihrem Gerät/Betriebssystem unterstützt'; + 'Nicht auf Ihrem Gerät/Betriebssystem unterstützt'; @override String get settings_gpxExportError => @@ -3149,8 +3192,7 @@ class AppLocalizationsDe extends AppLocalizations { 'GPX-Kartendaten aus meshcore-open exportieren'; @override - String get snrIndicator_nearByRepeaters => - 'In der Nähe befindliche Repeater'; + String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater'; @override String get snrIndicator_lastSeen => 'Zuletzt gesehen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a39d473..0e4e55d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -132,6 +132,47 @@ class AppLocalizationsEn extends AppLocalizations { String get usbScreenEmptyState => 'No USB devices found. Plug one in and refresh.'; + @override + String get usbErrorPermissionDenied => 'USB permission was denied.'; + + @override + String get usbErrorDeviceMissing => + 'The selected USB device is no longer available.'; + + @override + String get usbErrorInvalidPort => 'Select a valid USB device.'; + + @override + String get usbErrorBusy => + 'Another USB connection request is already in progress.'; + + @override + String get usbErrorNotConnected => 'No USB device is connected.'; + + @override + String get usbErrorOpenFailed => 'Failed to open the selected USB device.'; + + @override + String get usbErrorConnectFailed => + 'Failed to connect to the selected USB device.'; + + @override + String get usbErrorUnsupported => + 'USB serial is not supported on this platform.'; + + @override + String get usbErrorAlreadyActive => 'A USB connection is already active.'; + + @override + String get usbErrorNoDeviceSelected => 'No USB device was selected.'; + + @override + String get usbErrorPortClosed => 'The USB connection is not open.'; + + @override + String get usbErrorConnectTimedOut => + 'Timed out waiting for the device to respond.'; + @override String get scanner_scanning => 'Scanning for devices...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 2604928..98a3a1e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -45,10 +45,10 @@ class AppLocalizationsEs extends AppLocalizations { String get common_edit => 'Editar'; @override - String get common_add => 'Añadir'; + String get common_add => 'Añadir'; @override - String get common_settings => 'Configuración'; + String get common_settings => 'Configuración'; @override String get common_disconnect => 'Desconectar'; @@ -93,7 +93,7 @@ class AppLocalizationsEs extends AppLocalizations { String get common_loading => 'Cargando...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -119,19 +119,63 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbScreenSubtitle => - 'Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.'; + 'Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.'; @override String get usbScreenStatus => 'Seleccione un dispositivo USB'; @override String get usbScreenNote => - 'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.'; + 'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.'; @override String get usbScreenEmptyState => 'No se encontraron dispositivos USB. Conecte uno y vuelva a cargar.'; + @override + String get usbErrorPermissionDenied => + 'Se denegó el permiso de acceso a través de USB.'; + + @override + String get usbErrorDeviceMissing => + 'El dispositivo USB seleccionado ya no está disponible.'; + + @override + String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.'; + + @override + String get usbErrorBusy => + 'Ya se ha iniciado una solicitud de conexión USB adicional.'; + + @override + String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.'; + + @override + String get usbErrorOpenFailed => + 'No se pudo abrir el dispositivo USB seleccionado.'; + + @override + String get usbErrorConnectFailed => + 'No se pudo conectar con el dispositivo USB seleccionado.'; + + @override + String get usbErrorUnsupported => + 'La comunicación serial mediante USB no está soportada en esta plataforma.'; + + @override + String get usbErrorAlreadyActive => 'La conexión USB ya está activa.'; + + @override + String get usbErrorNoDeviceSelected => + 'No se ha seleccionado ningún dispositivo USB.'; + + @override + String get usbErrorPortClosed => 'La conexión USB no está activa.'; + + @override + String get usbErrorConnectTimedOut => + 'Se ha producido un error debido a la espera prolongada para recibir una respuesta del dispositivo.'; + @override String get scanner_scanning => 'Escaneando dispositivos...'; @@ -142,7 +186,7 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_disconnecting => 'Desconectando...'; @override - String get scanner_notConnected => 'No está conectado'; + String get scanner_notConnected => 'No está conectado'; @override String scanner_connectedTo(String deviceName) { @@ -158,7 +202,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Error de conexión: $error'; + return 'Error de conexión: $error'; } @override @@ -168,7 +212,7 @@ class AppLocalizationsEs extends AppLocalizations { String get scanner_scan => 'Escanea'; @override - String get scanner_bluetoothOff => 'Bluetooth está desactivado.'; + String get scanner_bluetoothOff => 'Bluetooth está desactivado.'; @override String get scanner_bluetoothOffMessage => @@ -179,38 +223,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_chromeRequiredMessage => - 'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.'; + 'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.'; @override String get scanner_enableBluetooth => 'Habilitar Bluetooth'; @override - String get device_quickSwitch => 'Cambiar rápidamente'; + String get device_quickSwitch => 'Cambiar rápidamente'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Configuración'; + String get settings_title => 'Configuración'; @override - String get settings_deviceInfo => 'Información del dispositivo'; + String get settings_deviceInfo => 'Información del dispositivo'; @override - String get settings_appSettings => 'Configuración de la App'; + String get settings_appSettings => 'Configuración de la App'; @override String get settings_appSettingsSubtitle => 'Notificaciones, mensajes y preferencias de mapa'; @override - String get settings_nodeSettings => 'Configuración del Nodo'; + String get settings_nodeSettings => 'Configuración del Nodo'; @override String get settings_nodeName => 'Nombre del nodo'; @override - String get settings_nodeNameNotSet => 'No está configurado'; + String get settings_nodeNameNotSet => 'No está configurado'; @override String get settings_nodeNameHint => 'Introducir nombre de nodo'; @@ -219,37 +263,37 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_nodeNameUpdated => 'Nombre actualizado'; @override - String get settings_radioSettings => 'Configuración de Radio'; + String get settings_radioSettings => 'Configuración de Radio'; @override String get settings_radioSettingsSubtitle => - 'Frecuencia, potencia, factor de dispersión'; + 'Frecuencia, potencia, factor de dispersión'; @override String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados'; @override - String get settings_location => 'Ubicación'; + String get settings_location => 'Ubicación'; @override String get settings_locationSubtitle => 'Coordenadas GPS'; @override - String get settings_locationUpdated => 'Ubicación actualizada'; + String get settings_locationUpdated => 'Ubicación actualizada'; @override String get settings_locationBothRequired => 'Introduzca tanto la latitud como la longitud.'; @override - String get settings_locationInvalid => 'Latitud o longitud inválidos.'; + String get settings_locationInvalid => 'Latitud o longitud inválidos.'; @override String get settings_locationGPSEnable => 'Habilitar GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Habilita la actualización automática de la ubicación mediante GPS.'; + 'Habilita la actualización automática de la ubicación mediante GPS.'; @override String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)'; @@ -269,11 +313,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_privacyModeSubtitle => - 'Ocultar nombre/ubicación en anuncios'; + 'Ocultar nombre/ubicación en anuncios'; @override String get settings_privacyModeToggle => - 'Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.'; + 'Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.'; @override String get settings_privacyModeEnabled => 'Modo de privacidad activado'; @@ -289,17 +333,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_sendAdvertisementSubtitle => - 'Presencia de transmisión ahora'; + 'Presencia de transmisión ahora'; @override String get settings_advertisementSent => 'Anuncio enviado'; @override - String get settings_syncTime => 'Tiempo de Sincronización'; + String get settings_syncTime => 'Tiempo de Sincronización'; @override String get settings_syncTimeSubtitle => - 'Establecer la hora del dispositivo al tiempo del teléfono'; + 'Establecer la hora del dispositivo al tiempo del teléfono'; @override String get settings_timeSynchronized => 'Sincronizado en el tiempo'; @@ -320,24 +364,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - '¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.'; + '¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.'; @override String get settings_debug => 'Depurar'; @override - String get settings_bleDebugLog => 'Registro de Depuración BLE'; + String get settings_bleDebugLog => 'Registro de Depuración BLE'; @override String get settings_bleDebugLogSubtitle => 'Comandos, respuestas y datos brutos de BLE'; @override - String get settings_appDebugLog => 'Registro de Depuración de la App'; + String get settings_appDebugLog => 'Registro de Depuración de la App'; @override String get settings_appDebugLogSubtitle => - 'Mensajes de depuración de la aplicación'; + 'Mensajes de depuración de la aplicación'; @override String get settings_about => 'Acerca de'; @@ -352,11 +396,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_aboutDescription => - 'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.'; + 'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Datos de elevación LOS: Open-Meteo (CC BY 4.0)'; + 'Datos de elevación LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Nombre'; @@ -368,16 +412,16 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_infoStatus => 'Estado'; @override - String get settings_infoBattery => 'Batería'; + String get settings_infoBattery => 'Batería'; @override - String get settings_infoPublicKey => 'Clave Pública'; + String get settings_infoPublicKey => 'Clave Pública'; @override - String get settings_infoContactsCount => 'Número de contactos'; + String get settings_infoContactsCount => 'Número de contactos'; @override - String get settings_infoChannelCount => 'Número de canales'; + String get settings_infoChannelCount => 'Número de canales'; @override String get settings_presets => 'Preajustes'; @@ -389,16 +433,16 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_frequencyHelper => '300,0 - 2500,0'; @override - String get settings_frequencyInvalid => 'Frecuencia inválida (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Frecuencia inválida (300-2500 MHz)'; @override String get settings_bandwidth => 'Ancho de banda'; @override - String get settings_spreadingFactor => 'Factor de propagación'; + String get settings_spreadingFactor => 'Factor de propagación'; @override - String get settings_codingRate => 'Tasa de Programación'; + String get settings_codingRate => 'Tasa de Programación'; @override String get settings_txPower => 'TX Potencia (dBm)'; @@ -407,10 +451,10 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)'; + String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)'; @override - String get settings_clientRepeat => 'Repetir sin conexión'; + String get settings_clientRepeat => 'Repetir sin conexión'; @override String get settings_clientRepeatSubtitle => @@ -418,7 +462,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_clientRepeatFreqWarning => - 'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.'; + 'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.'; @override String settings_error(String message) { @@ -426,7 +470,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get appSettings_title => 'Configuración de la App'; + String get appSettings_title => 'Configuración de la App'; @override String get appSettings_appearance => 'Apariencia'; @@ -453,10 +497,10 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -465,16 +509,16 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -483,10 +527,10 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Ruso'; @@ -514,7 +558,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_notificationPermissionDenied => - 'Permiso de notificación denegado'; + 'Permiso de notificación denegado'; @override String get appSettings_notificationsEnabled => 'Notificaciones activadas'; @@ -527,7 +571,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_messageNotificationsSubtitle => - 'Mostrar notificación al recibir nuevos mensajes'; + 'Mostrar notificación al recibir nuevos mensajes'; @override String get appSettings_channelMessageNotifications => @@ -535,7 +579,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_channelMessageNotificationsSubtitle => - 'Mostrar notificación al recibir mensajes del canal'; + 'Mostrar notificación al recibir mensajes del canal'; @override String get appSettings_advertisementNotifications => @@ -543,10 +587,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'Mostrar notificación cuando se descubren nuevos nodos'; + 'Mostrar notificación cuando se descubren nuevos nodos'; @override - String get appSettings_messaging => 'Mensajería'; + String get appSettings_messaging => 'Mensajería'; @override String get appSettings_clearPathOnMaxRetry => @@ -554,45 +598,45 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Restablecer la ruta de contacto después de 5 intentos de envío fallidos'; + 'Restablecer la ruta de contacto después de 5 intentos de envío fallidos'; @override String get appSettings_pathsWillBeCleared => - 'Los caminos se limpiarán después de 5 intentos fallidos.'; + 'Los caminos se limpiarán después de 5 intentos fallidos.'; @override String get appSettings_pathsWillNotBeCleared => - 'Las rutas no se eliminarán automáticamente.'; + 'Las rutas no se eliminarán automáticamente.'; @override - String get appSettings_autoRouteRotation => 'Rotación de Ruta Automática'; + String get appSettings_autoRouteRotation => 'Rotación de Ruta Automática'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alternar entre las mejores rutas y el modo inundación'; + 'Alternar entre las mejores rutas y el modo inundación'; @override String get appSettings_autoRouteRotationEnabled => - 'Rotación de ruta automática habilitada'; + 'Rotación de ruta automática habilitada'; @override String get appSettings_autoRouteRotationDisabled => - 'Rotación de ruta automática desactivada'; + 'Rotación de ruta automática desactivada'; @override - String get appSettings_battery => 'Batería'; + String get appSettings_battery => 'Batería'; @override - String get appSettings_batteryChemistry => 'Química de la batería'; + String get appSettings_batteryChemistry => 'Química de la batería'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Configuración por dispositivo ($deviceName)'; + return 'Configuración por dispositivo ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Conéctate a un dispositivo para elegir'; + 'Conéctate a un dispositivo para elegir'; @override String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; @@ -604,7 +648,7 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; @override - String get appSettings_mapDisplay => 'Visualización del Mapa'; + String get appSettings_mapDisplay => 'Visualización del Mapa'; @override String get appSettings_showRepeaters => 'Mostrar Repetidores'; @@ -635,7 +679,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String appSettings_timeFilterShowLast(int hours) { - return 'Mostrar nodos de las últimas $hours horas'; + return 'Mostrar nodos de las últimas $hours horas'; } @override @@ -649,68 +693,67 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_allTime => 'Todo el tiempo'; @override - String get appSettings_lastHour => 'Última hora'; + String get appSettings_lastHour => 'Última hora'; @override - String get appSettings_last6Hours => 'Últimas 6 horas'; + String get appSettings_last6Hours => 'Últimas 6 horas'; @override - String get appSettings_last24Hours => 'Últimas 24 horas'; + String get appSettings_last24Hours => 'Últimas 24 horas'; @override String get appSettings_lastWeek => 'La semana pasada'; @override - String get appSettings_offlineMapCache => 'Caché de Mapa Offline'; + String get appSettings_offlineMapCache => 'Caché de Mapa Offline'; @override String get appSettings_unitsTitle => 'Unidades'; @override - String get appSettings_unitsMetric => 'Métrico (m/km)'; + String get appSettings_unitsMetric => 'Métrico (m/km)'; @override String get appSettings_unitsImperial => 'Imperial (pies/millas)'; @override - String get appSettings_noAreaSelected => - 'No se ha seleccionado ningún área'; + String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Área seleccionada (zoom $minZoom-$maxZoom)'; + return 'Área seleccionada (zoom $minZoom-$maxZoom)'; } @override String get appSettings_debugCard => 'Depurar'; @override - String get appSettings_appDebugLogging => 'Registro de Depuración de la App'; + String get appSettings_appDebugLogging => 'Registro de Depuración de la App'; @override String get appSettings_appDebugLoggingSubtitle => - 'Registrar mensajes de depuración de la app de registro para solucionar problemas'; + 'Registrar mensajes de depuración de la app de registro para solucionar problemas'; @override String get appSettings_appDebugLoggingEnabled => - 'Registro de depuración de la aplicación habilitado'; + 'Registro de depuración de la aplicación habilitado'; @override String get appSettings_appDebugLoggingDisabled => - 'El registro de depuración de la aplicación está desactivado'; + 'El registro de depuración de la aplicación está desactivado'; @override String get contacts_title => 'Contactos'; @override - String get contacts_noContacts => 'Aún no hay contactos.'; + String get contacts_noContacts => 'Aún no hay contactos.'; @override String get contacts_contactsWillAppear => - 'Los contactos aparecerán cuando los dispositivos anuncien.'; + 'Los contactos aparecerán cuando los dispositivos anuncien.'; @override - String get contacts_unread => 'No leído'; + String get contacts_unread => 'No leído'; @override String get contacts_searchContactsNoNumber => 'Buscar contactos...'; @@ -759,7 +802,7 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_manageRepeater => 'Gestionar Repetidor'; @override - String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; + String get contacts_manageRoom => 'Gestionar Servidor de Habitación'; @override String get contacts_roomLogin => 'Inicio de Sala'; @@ -803,27 +846,27 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_noMembers => 'No miembros'; @override - String get contacts_lastSeenNow => 'Última vez que se vio ahora'; + String get contacts_lastSeenNow => 'Última vez que se vio ahora'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Última vez visto hace $minutes minutos.'; + return 'Última vez visto hace $minutes minutos.'; } @override - String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora'; + String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Última vez visto hace $hours horas.'; + return 'Última vez visto hace $hours horas.'; } @override - String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día'; + String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Última vez visto hace $days días.'; + return 'Última vez visto hace $days días.'; } @override @@ -833,7 +876,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_noChannelsConfigured => 'No se han configurado canales'; @override - String get channels_addPublicChannel => 'Añadir Canal Público'; + String get channels_addPublicChannel => 'Añadir Canal Público'; @override String get channels_searchChannels => 'Buscar canales...'; @@ -850,13 +893,13 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_hashtagChannel => 'Canal con hashtag'; @override - String get channels_public => 'Público'; + String get channels_public => 'Público'; @override String get channels_private => 'Privado'; @override - String get channels_publicChannel => 'Canal público'; + String get channels_publicChannel => 'Canal público'; @override String get channels_privateChannel => 'Canal privado'; @@ -889,19 +932,19 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get channels_addChannel => 'Añadir Canal'; + String get channels_addChannel => 'Añadir Canal'; @override - String get channels_channelIndexLabel => 'Índice de Canal'; + String get channels_channelIndexLabel => 'Índice de Canal'; @override String get channels_channelName => 'Nombre del canal'; @override - String get channels_usePublicChannel => 'Usar Canal Público'; + String get channels_usePublicChannel => 'Usar Canal Público'; @override - String get channels_standardPublicPsk => 'PSK estándar público'; + String get channels_standardPublicPsk => 'PSK estándar público'; @override String get channels_pskHex => 'PSK (Hex)'; @@ -919,7 +962,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String channels_channelAdded(String name) { - return 'Canal \"$name\" añadido'; + return 'Canal \"$name\" añadido'; } @override @@ -928,7 +971,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get channels_smazCompression => 'Compresión SMAZ'; + String get channels_smazCompression => 'Compresión SMAZ'; @override String channels_channelUpdated(String name) { @@ -936,7 +979,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Canal público añadido'; + String get channels_publicChannelAdded => 'Canal público añadido'; @override String get channels_sortBy => 'Ordenar por'; @@ -948,7 +991,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Últimos mensajes'; + String get channels_sortLatestMessages => 'Últimos mensajes'; @override String get channels_sortUnread => 'Sin leer'; @@ -961,31 +1004,31 @@ class AppLocalizationsEs extends AppLocalizations { 'Cifrado con una clave secreta.'; @override - String get channels_joinPrivateChannel => 'Únete a un Canal Privado'; + String get channels_joinPrivateChannel => 'Únete a un Canal Privado'; @override String get channels_joinPrivateChannelDesc => 'Introducir manualmente una clave secreta.'; @override - String get channels_joinPublicChannel => 'Únete al Canal Público'; + String get channels_joinPublicChannel => 'Únete al Canal Público'; @override String get channels_joinPublicChannelDesc => 'Cualquiera puede unirse a este canal.'; @override - String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag'; + String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag'; @override String get channels_joinHashtagChannelDesc => 'Cualquiera puede unirse a los canales de hashtag.'; @override - String get channels_scanQrCode => 'Escanear un Código QR'; + String get channels_scanQrCode => 'Escanear un Código QR'; @override - String get channels_scanQrCodeComingSoon => 'Próximamente'; + String get channels_scanQrCodeComingSoon => 'Próximamente'; @override String get channels_enterHashtag => 'Introducir hashtag'; @@ -994,7 +1037,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channels_hashtagHint => 'ej. #equipo'; @override - String get chat_noMessages => 'Aún no hay mensajes'; + String get chat_noMessages => 'Aún no hay mensajes'; @override String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar'; @@ -1013,7 +1056,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_location => 'Ubicación'; + String get chat_location => 'Ubicación'; @override String chat_sendMessageTo(String contactName) { @@ -1025,7 +1068,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return 'Mensaje demasiado largo (máximo $maxBytes bytes).'; + return 'Mensaje demasiado largo (máximo $maxBytes bytes).'; } @override @@ -1035,7 +1078,7 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_messageDeleted => 'Mensaje borrado'; @override - String get chat_retryingMessage => 'Reintentando…'; + String get chat_retryingMessage => 'Reintentando…'; @override String chat_retryCount(int current, int max) { @@ -1049,7 +1092,7 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_reply => 'Responder'; @override - String get chat_addReaction => 'Añadir Reacción'; + String get chat_addReaction => 'Añadir Reacción'; @override String get chat_me => 'Yo'; @@ -1085,13 +1128,13 @@ class AppLocalizationsEs extends AppLocalizations { String get gifPicker_failedSearch => 'No se encontraron GIFs'; @override - String get gifPicker_noInternet => 'No hay conexión a internet'; + String get gifPicker_noInternet => 'No hay conexión a internet'; @override - String get debugLog_appTitle => 'Registro de Depuración de la App'; + String get debugLog_appTitle => 'Registro de Depuración de la App'; @override - String get debugLog_bleTitle => 'Registro de Depuración BLE'; + String get debugLog_bleTitle => 'Registro de Depuración BLE'; @override String get debugLog_copyLog => 'Copiar registro'; @@ -1100,17 +1143,17 @@ class AppLocalizationsEs extends AppLocalizations { String get debugLog_clearLog => 'Borrar registro'; @override - String get debugLog_copied => 'Registro de depuración copiado'; + String get debugLog_copied => 'Registro de depuración copiado'; @override String get debugLog_bleCopied => 'Registro BLE copiado'; @override - String get debugLog_noEntries => 'Aún no hay registros de depuración.'; + String get debugLog_noEntries => 'Aún no hay registros de depuración.'; @override String get debugLog_enableInSettings => - 'Habilitar el registro de depuración de la aplicación en la configuración'; + 'Habilitar el registro de depuración de la aplicación en la configuración'; @override String get debugLog_frames => 'Marcos'; @@ -1119,7 +1162,7 @@ class AppLocalizationsEs extends AppLocalizations { String get debugLog_rawLogRx => 'Registro Crudo-RX'; @override - String get debugLog_noBleActivity => 'Aún no hay actividad BLE'; + String get debugLog_noBleActivity => 'Aún no hay actividad BLE'; @override String debugFrame_length(int count) { @@ -1169,7 +1212,7 @@ class AppLocalizationsEs extends AppLocalizations { String get debugFrame_hexDump => 'Mapeo Hexadecimal:'; @override - String get chat_pathManagement => 'Gestión de Rutas'; + String get chat_pathManagement => 'Gestión de Rutas'; @override String get chat_ShowAllPaths => 'Mostrar todos los caminos'; @@ -1181,14 +1224,14 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_autoUseSavedPath => 'Auto (usar la ruta guardada)'; @override - String get chat_forceFloodMode => 'Modo Inundación Forzado'; + String get chat_forceFloodMode => 'Modo Inundación Forzado'; @override String get chat_recentAckPaths => 'Rutas de ACK Recientes (tocar para usar):'; @override String get chat_pathHistoryFull => - 'El historial de rutas está completo. Eliminar entradas para añadir nuevas.'; + 'El historial de rutas está completo. Eliminar entradas para añadir nuevas.'; @override String get chat_hopSingular => 'salta'; @@ -1208,14 +1251,14 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_successes => 'Éxitos'; + String get chat_successes => 'Éxitos'; @override String get chat_removePath => 'Eliminar ruta'; @override String get chat_noPathHistoryYet => - 'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.'; + 'Aún no hay historial de rutas.\nEnvía un mensaje para descubrir rutas.'; @override String get chat_pathActions => 'Acciones de Ruta:'; @@ -1232,11 +1275,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_clearPathSubtitle => - 'Forzar redescubrimiento en el próximo envío'; + 'Forzar redescubrimiento en el próximo envío'; @override String get chat_pathCleared => - 'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.'; + 'Ruta eliminada. El siguiente mensaje redescubrirá la ruta.'; @override String get chat_floodModeSubtitle => @@ -1244,14 +1287,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_floodModeEnabled => - 'El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.'; + 'El modo de inundación está habilitado. Desactívalo mediante el icono de enrutamiento en la barra de herramientas de la aplicación.'; @override String get chat_fullPath => 'Ruta completa'; @override String get chat_pathDetailsNotAvailable => - 'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.'; + 'Los detalles de la ruta aún no están disponibles. Intenta enviar un mensaje para refrescar.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1266,13 +1309,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_pathSavedLocally => - 'Guardado localmente. Conéctate para sincronizar.'; + 'Guardado localmente. Conéctate para sincronizar.'; @override String get chat_pathDeviceConfirmed => 'Dispositivo confirmado.'; @override - String get chat_pathDeviceNotConfirmed => 'Dispositivo aún no confirmado.'; + String get chat_pathDeviceNotConfirmed => 'Dispositivo aún no confirmado.'; @override String get chat_type => 'Escribe'; @@ -1281,13 +1324,13 @@ class AppLocalizationsEs extends AppLocalizations { String get chat_path => 'Ruta'; @override - String get chat_publicKey => 'Clave Pública'; + String get chat_publicKey => 'Clave Pública'; @override String get chat_compressOutgoingMessages => 'Comprimir mensajes salientes'; @override - String get chat_floodForced => 'Inundación (forzada)'; + String get chat_floodForced => 'Inundación (forzada)'; @override String get chat_directForced => 'Directo (forzado)'; @@ -1298,13 +1341,13 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_floodAuto => 'Inundación (automática)'; + String get chat_floodAuto => 'Inundación (automática)'; @override String get chat_direct => 'Guardar'; @override - String get chat_poiShared => 'Punto de Interés Compartido'; + String get chat_poiShared => 'Punto de Interés Compartido'; @override String chat_unread(int count) { @@ -1312,11 +1355,11 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_openLink => '¿Abrir enlace?'; + String get chat_openLink => '¿Abrir enlace?'; @override String get chat_openLinkConfirmation => - '¿Quiere abrir este enlace en su navegador?'; + '¿Quiere abrir este enlace en su navegador?'; @override String get chat_open => 'Abrir'; @@ -1327,19 +1370,19 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get chat_invalidLink => 'Formato de enlace no válido'; + String get chat_invalidLink => 'Formato de enlace no válido'; @override String get map_title => 'Mapa de Nodos'; @override - String get map_lineOfSight => 'Línea de visión'; + String get map_lineOfSight => 'Línea de visión'; @override - String get map_losScreenTitle => 'Línea de visión'; + String get map_losScreenTitle => 'Línea de visión'; @override - String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación'; + String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación'; @override String get map_nodesNeedGps => @@ -1362,7 +1405,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_repeater => 'Repetidor'; @override - String get map_room => 'Habitación'; + String get map_room => 'Habitación'; @override String get map_sensor => 'Sensor'; @@ -1374,14 +1417,14 @@ class AppLocalizationsEs extends AppLocalizations { String get map_pinPrivate => 'Bloqueo (Privado)'; @override - String get map_pinPublic => 'Clave (Pública)'; + String get map_pinPublic => 'Clave (Pública)'; @override - String get map_lastSeen => 'Última vez que se vio'; + String get map_lastSeen => 'Última vez que se vio'; @override String get map_disconnectConfirm => - '¿Está seguro de que desea desconectarse de este dispositivo?'; + '¿Está seguro de que desea desconectarse de este dispositivo?'; @override String get map_from => 'De'; @@ -1393,7 +1436,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_flags => 'Banderas'; @override - String get map_shareMarkerHere => 'Compartir marcador aquí'; + String get map_shareMarkerHere => 'Compartir marcador aquí'; @override String get map_pinLabel => 'Etiqueta de marcador'; @@ -1402,7 +1445,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_label => 'Etiqueta'; @override - String get map_pointOfInterest => 'Punto de interés'; + String get map_pointOfInterest => 'Punto de interés'; @override String get map_sendToContact => 'Enviar a contacto'; @@ -1414,16 +1457,16 @@ class AppLocalizationsEs extends AppLocalizations { String get map_noChannelsAvailable => 'No hay canales disponibles'; @override - String get map_publicLocationShare => 'Compartir ubicación pública'; + String get map_publicLocationShare => 'Compartir ubicación pública'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Estás a punto de compartir una ubicación en $channelLabel. Este canal es público y cualquiera con la PSK puede verlo.'; + return 'Estás a punto de compartir una ubicación en $channelLabel. Este canal es público y cualquiera con la PSK puede verlo.'; } @override String get map_connectToShareMarkers => - 'Conéctate a un dispositivo para compartir marcadores'; + 'Conéctate a un dispositivo para compartir marcadores'; @override String get map_filterNodes => 'Filtrar Nodos'; @@ -1447,7 +1490,7 @@ class AppLocalizationsEs extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtrar por prefijo clave'; @override - String get map_publicKeyPrefix => 'Prefijo de clave pública'; + String get map_publicKeyPrefix => 'Prefijo de clave pública'; @override String get map_markers => 'Marcadores'; @@ -1456,13 +1499,13 @@ class AppLocalizationsEs extends AppLocalizations { String get map_showSharedMarkers => 'Mostrar marcadores compartidos'; @override - String get map_lastSeenTime => 'Última vez que se vio'; + String get map_lastSeenTime => 'Última vez que se vio'; @override String get map_sharedPin => 'Pin compartido'; @override - String get map_joinRoom => 'Únete a la sala'; + String get map_joinRoom => 'Únete a la sala'; @override String get map_manageRepeater => 'Gestionar Repetidor'; @@ -1474,28 +1517,28 @@ class AppLocalizationsEs extends AppLocalizations { String get map_runTrace => 'Ejecutar Rastreo de Ruta'; @override - String get map_removeLast => 'Eliminar último'; + String get map_removeLast => 'Eliminar último'; @override String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.'; @override - String get mapCache_title => 'Caché de Mapa Offline'; + String get mapCache_title => 'Caché de Mapa Offline'; @override String get mapCache_selectAreaFirst => - 'Seleccionar un área para cachear primero'; + 'Seleccionar un área para cachear primero'; @override String get mapCache_noTilesToDownload => - 'No hay azulejos para descargar para este área.'; + 'No hay azulejos para descargar para este área.'; @override String get mapCache_downloadTilesTitle => 'Descargar ficheros'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Descargar $count ficheros para usar sin conexión?'; + return 'Descargar $count ficheros para usar sin conexión?'; } @override @@ -1512,21 +1555,21 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get mapCache_clearOfflineCacheTitle => 'Borrar caché offline'; + String get mapCache_clearOfflineCacheTitle => 'Borrar caché offline'; @override String get mapCache_clearOfflineCachePrompt => - 'Eliminar todas las baldosas en caché del mapa?'; + 'Eliminar todas las baldosas en caché del mapa?'; @override String get mapCache_offlineCacheCleared => - 'Almacén en caché sin conexión eliminado'; + 'Almacén en caché sin conexión eliminado'; @override - String get mapCache_noAreaSelected => 'No se ha seleccionado ningún área'; + String get mapCache_noAreaSelected => 'No se ha seleccionado ningún área'; @override - String get mapCache_cacheArea => 'Área de Caché'; + String get mapCache_cacheArea => 'Área de Caché'; @override String get mapCache_useCurrentView => 'Usar Vista Actual'; @@ -1548,7 +1591,7 @@ class AppLocalizationsEs extends AppLocalizations { String get mapCache_downloadTilesButton => 'Descargar Mosaicos'; @override - String get mapCache_clearCacheButton => 'Borrar Caché'; + String get mapCache_clearCacheButton => 'Borrar Caché'; @override String mapCache_failedDownloads(int count) { @@ -1580,7 +1623,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String time_daysAgo(int days) { - return '$days días hace'; + return '$days días hace'; } @override @@ -1590,10 +1633,10 @@ class AppLocalizationsEs extends AppLocalizations { String get time_hours => 'horas'; @override - String get time_day => 'día'; + String get time_day => 'día'; @override - String get time_days => 'días'; + String get time_days => 'días'; @override String get time_week => 'semana'; @@ -1618,34 +1661,34 @@ class AppLocalizationsEs extends AppLocalizations { @override String get dialog_disconnectConfirm => - '¿Está seguro de que desea desconectarse de este dispositivo?'; + '¿Está seguro de que desea desconectarse de este dispositivo?'; @override - String get login_repeaterLogin => 'Iniciar sesión en el Repetidor'; + String get login_repeaterLogin => 'Iniciar sesión en el Repetidor'; @override String get login_roomLogin => 'Inicio de Sala'; @override - String get login_password => 'Contraseña'; + String get login_password => 'Contraseña'; @override - String get login_enterPassword => 'Introducir contraseña'; + String get login_enterPassword => 'Introducir contraseña'; @override - String get login_savePassword => 'Guardar contraseña'; + String get login_savePassword => 'Guardar contraseña'; @override String get login_savePasswordSubtitle => - 'La contraseña se almacenará de forma segura en este dispositivo.'; + 'La contraseña se almacenará de forma segura en este dispositivo.'; @override String get login_repeaterDescription => - 'Ingrese la contraseña del repetidor para acceder a la configuración y el estado.'; + 'Ingrese la contraseña del repetidor para acceder a la configuración y el estado.'; @override String get login_roomDescription => - 'Ingrese la contraseña de la sala para acceder a la configuración y el estado.'; + 'Ingrese la contraseña de la sala para acceder a la configuración y el estado.'; @override String get login_routing => 'Enrutamiento'; @@ -1657,13 +1700,13 @@ class AppLocalizationsEs extends AppLocalizations { String get login_autoUseSavedPath => 'Auto (usar la ruta guardada)'; @override - String get login_forceFloodMode => 'Activar Modo Inundación Forzada'; + String get login_forceFloodMode => 'Activar Modo Inundación Forzada'; @override String get login_managePaths => 'Gestionar Rutas'; @override - String get login_login => 'Iniciar sesión'; + String get login_login => 'Iniciar sesión'; @override String login_attempt(int current, int max) { @@ -1677,7 +1720,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get login_failedMessage => - 'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.'; + 'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.'; @override String get common_reload => 'Recargar'; @@ -1713,14 +1756,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get path_hexPrefixExample => - 'Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).'; + 'Ejemplo: A1,F2,3C (cada nodo utiliza el primer byte de su clave pública).'; @override String get path_labelHexPrefixes => 'Prefijos hexadecimales'; @override String get path_helperMaxHops => - 'Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).'; + 'Máximo 64 saltos. Cada prefijo tiene 2 caracteres hexadecimales (1 byte).'; @override String get path_selectFromContacts => 'O seleccionar de contactos:'; @@ -1735,38 +1778,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String path_invalidHexPrefixes(String prefixes) { - return 'Prefijos hexadecimales inválidos: $prefixes'; + return 'Prefijos hexadecimales inválidos: $prefixes'; } @override String get path_tooLong => - 'La ruta es demasiado larga. Se permiten un máximo de 64 saltos.'; + 'La ruta es demasiado larga. Se permiten un máximo de 64 saltos.'; @override String get path_setPath => 'Establecer Ruta'; @override - String get repeater_management => 'Gestión de Repetidores'; + String get repeater_management => 'Gestión de Repetidores'; @override - String get room_management => 'Administración del Servidor de Habitación'; + String get room_management => 'Administración del Servidor de Habitación'; @override - String get repeater_managementTools => 'Herramientas de Gestión'; + String get repeater_managementTools => 'Herramientas de Gestión'; @override String get repeater_status => 'Estado'; @override String get repeater_statusSubtitle => - 'Ver el estado, las estadísticas y los vecinos del repetidor'; + 'Ver el estado, las estadísticas y los vecinos del repetidor'; @override String get repeater_telemetry => 'Telemetry'; @override String get repeater_telemetrySubtitle => - 'Ver la telemetría de los sensores y las estadísticas del sistema'; + 'Ver la telemetría de los sensores y las estadísticas del sistema'; @override String get repeater_cli => 'CLI'; @@ -1781,11 +1824,10 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.'; @override - String get repeater_settings => 'Configuración'; + String get repeater_settings => 'Configuración'; @override - String get repeater_settingsSubtitle => - 'Configurar parámetros del repetidor'; + String get repeater_settingsSubtitle => 'Configurar parámetros del repetidor'; @override String get repeater_statusTitle => 'Estado del Repetidor'; @@ -1797,16 +1839,16 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_autoUseSavedPath => 'Auto (usar la ruta guardada)'; @override - String get repeater_forceFloodMode => 'Modo Inundación Forzado'; + String get repeater_forceFloodMode => 'Modo Inundación Forzado'; @override - String get repeater_pathManagement => 'Gestión de rutas'; + String get repeater_pathManagement => 'Gestión de rutas'; @override String get repeater_refresh => 'Actualizar'; @override - String get repeater_statusRequestTimeout => 'Solicitud de estado caducó.'; + String get repeater_statusRequestTimeout => 'Solicitud de estado caducó.'; @override String repeater_errorLoadingStatus(String error) { @@ -1814,13 +1856,13 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get repeater_systemInformation => 'Información del sistema'; + String get repeater_systemInformation => 'Información del sistema'; @override - String get repeater_battery => 'Batería'; + String get repeater_battery => 'Batería'; @override - String get repeater_clockAtLogin => 'Reloj (al inicio de sesión)'; + String get repeater_clockAtLogin => 'Reloj (al inicio de sesión)'; @override String get repeater_uptime => 'Tiempo de actividad'; @@ -1829,16 +1871,16 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_queueLength => 'Longitud de la cola'; @override - String get repeater_debugFlags => 'Marcadores de Depuración'; + String get repeater_debugFlags => 'Marcadores de Depuración'; @override - String get repeater_radioStatistics => 'Estadísticas de Radio'; + String get repeater_radioStatistics => 'Estadísticas de Radio'; @override - String get repeater_lastRssi => 'Último RSSI'; + String get repeater_lastRssi => 'Último RSSI'; @override - String get repeater_lastSnr => 'Último SNR'; + String get repeater_lastSnr => 'Último SNR'; @override String get repeater_noiseFloor => 'Nivel de Ruido'; @@ -1850,7 +1892,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Estadísticas del Paquete'; + String get repeater_packetStatistics => 'Estadísticas del Paquete'; @override String get repeater_sent => 'Enviado'; @@ -1868,22 +1910,22 @@ class AppLocalizationsEs extends AppLocalizations { int minutes, int seconds, ) { - return '$days días ${hours}h ${minutes}m ${seconds}s'; + return '$days días ${hours}h ${minutes}m ${seconds}s'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundación: $flood, Directo: $direct'; + return 'Total: $total, Inundación: $flood, Directo: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundación: $flood, Directo: $direct'; + return 'Total: $total, Inundación: $flood, Directo: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Inundación: $flood, Directo: $direct'; + return 'Inundación: $flood, Directo: $direct'; } @override @@ -1892,10 +1934,10 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Configuración del Repetidor'; + String get repeater_settingsTitle => 'Configuración del Repetidor'; @override - String get repeater_basicSettings => 'Configuración Básica'; + String get repeater_basicSettings => 'Configuración Básica'; @override String get repeater_repeaterName => 'Nombre del Repetidor'; @@ -1905,20 +1947,20 @@ class AppLocalizationsEs extends AppLocalizations { 'Mostrar nombre para este repetidor'; @override - String get repeater_adminPassword => 'Contraseña de Administrador'; + String get repeater_adminPassword => 'Contraseña de Administrador'; @override - String get repeater_adminPasswordHelper => 'Contraseña de acceso completo'; + String get repeater_adminPasswordHelper => 'Contraseña de acceso completo'; @override - String get repeater_guestPassword => 'Contraseña de invitado'; + String get repeater_guestPassword => 'Contraseña de invitado'; @override String get repeater_guestPasswordHelper => - 'Acceso de solo lectura con contraseña'; + 'Acceso de solo lectura con contraseña'; @override - String get repeater_radioSettings => 'Configuración de Radio'; + String get repeater_radioSettings => 'Configuración de Radio'; @override String get repeater_frequencyMhz => 'Frecuencia (MHz)'; @@ -1936,13 +1978,13 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_bandwidth => 'Ancho de banda'; @override - String get repeater_spreadingFactor => 'Factor de propagación'; + String get repeater_spreadingFactor => 'Factor de propagación'; @override - String get repeater_codingRate => 'Tasa de Programación'; + String get repeater_codingRate => 'Tasa de Programación'; @override - String get repeater_locationSettings => 'Configuración de Ubicación'; + String get repeater_locationSettings => 'Configuración de Ubicación'; @override String get repeater_latitude => 'Latitud'; @@ -1959,7 +2001,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Grados decimales (por ejemplo, -122.4194)'; @override - String get repeater_features => 'Características'; + String get repeater_features => 'Características'; @override String get repeater_packetForwarding => 'Enrutamiento de Paquetes'; @@ -1980,10 +2022,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_privacyModeSubtitle => - 'Ocultar nombre/ubicación en anuncios'; + 'Ocultar nombre/ubicación en anuncios'; @override - String get repeater_advertisementSettings => 'Configuración de Anuncios'; + String get repeater_advertisementSettings => 'Configuración de Anuncios'; @override String get repeater_localAdvertInterval => 'Intervalo de Anuncio Local'; @@ -1995,7 +2037,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervalo de Anuncio de Inundación'; + 'Intervalo de Anuncio de Inundación'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2017,18 +2059,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_rebootRepeaterConfirm => - '¿Está seguro de que desea reiniciar este repetidor?'; + '¿Está seguro de que desea reiniciar este repetidor?'; @override String get repeater_regenerateIdentityKey => 'Regenerar Clave de Identidad'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Generar nueva pareja de clave pública/privada'; + 'Generar nueva pareja de clave pública/privada'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Esto generará una nueva identidad para el repetidor. Continuar?'; + 'Esto generará una nueva identidad para el repetidor. Continuar?'; @override String get repeater_eraseFileSystem => 'Borrar Sistema de Archivos'; @@ -2039,11 +2081,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!'; + 'ADVERTENCIA: Esto borrará todos los datos del repetidor. ¡Esto no se puede deshacer!'; @override String get repeater_eraseSerialOnly => - 'Borrar solo está disponible a través de la consola serial.'; + 'Borrar solo está disponible a través de la consola serial.'; @override String repeater_commandSent(String command) { @@ -2063,22 +2105,21 @@ class AppLocalizationsEs extends AppLocalizations { @override String repeater_errorSavingSettings(String error) { - return 'Error al guardar la configuración: $error'; + return 'Error al guardar la configuración: $error'; } @override - String get repeater_refreshBasicSettings => - 'Actualizar Configuración Básica'; + String get repeater_refreshBasicSettings => 'Actualizar Configuración Básica'; @override String get repeater_refreshRadioSettings => 'Actualizar Ajustes de Radio'; @override - String get repeater_refreshTxPower => 'Actualizar TX de energía'; + String get repeater_refreshTxPower => 'Actualizar TX de energía'; @override String get repeater_refreshLocationSettings => - 'Actualizar Configuración de Ubicación'; + 'Actualizar Configuración de Ubicación'; @override String get repeater_refreshPacketForwarding => @@ -2092,7 +2133,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_refreshAdvertisementSettings => - 'Actualizar Configuración de Anuncios'; + 'Actualizar Configuración de Anuncios'; @override String repeater_refreshed(String label) { @@ -2108,7 +2149,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliTitle => 'Repetidor CLI'; @override - String get repeater_debugNextCommand => 'Siguiente Comando de Depuración'; + String get repeater_debugNextCommand => 'Siguiente Comando de Depuración'; @override String get repeater_commandHelp => 'Ayuda'; @@ -2117,11 +2158,11 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_clearHistory => 'Borrar historial'; @override - String get repeater_noCommandsSent => 'Aún no se han enviado comandos.'; + String get repeater_noCommandsSent => 'Aún no se han enviado comandos.'; @override String get repeater_typeCommandOrUseQuick => - 'Escriba un comando a continuación o use comandos rápidos'; + 'Escriba un comando a continuación o use comandos rápidos'; @override String get repeater_enterCommandHint => 'Escribir comando...'; @@ -2156,7 +2197,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliQuickNeighbors => 'Vecinos'; @override - String get repeater_cliQuickVersion => 'Versión'; + String get repeater_cliQuickVersion => 'Versión'; @override String get repeater_cliQuickAdvertise => 'Anunciar'; @@ -2165,7 +2206,7 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliQuickClock => 'Reloj'; @override - String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; + String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad'; @override String get repeater_cliHelpReboot => @@ -2173,26 +2214,26 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpClock => - 'Muestra la hora actual según el reloj del dispositivo.'; + 'Muestra la hora actual según el reloj del dispositivo.'; @override String get repeater_cliHelpPassword => - 'Establece una nueva contraseña de administrador para el dispositivo.'; + 'Establece una nueva contraseña de administrador para el dispositivo.'; @override String get repeater_cliHelpVersion => - 'Muestra la versión del dispositivo y la fecha de compilación del firmware.'; + 'Muestra la versión del dispositivo y la fecha de compilación del firmware.'; @override String get repeater_cliHelpClearStats => - 'Reinicia varios contadores de estadísticas a cero.'; + 'Reinicia varios contadores de estadísticas a cero.'; @override String get repeater_cliHelpSetAf => 'Establece el factor de tiempo de aire.'; @override String get repeater_cliHelpSetTx => - 'Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).'; + 'Establece la potencia de transmisión LoRa en dBm (reboot para aplicar).'; @override String get repeater_cliHelpSetRepeat => @@ -2200,23 +2241,23 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetAllowReadOnly => - '(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).'; + '(Servidor de la sala) Si está \"activado\", entonces el inicio de sesión con una contraseña en blanco estará permitido, pero no se podrá publicar en la sala. (solo lectura).'; @override String get repeater_cliHelpSetFloodMax => - 'Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).'; + 'Establece el número máximo de saltos de paquetes de inundación entrantes (si es >= máximo, el paquete no se enruta).'; @override String get repeater_cliHelpSetIntThresh => - 'Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.'; + 'Establece el Umbral de Interferencia (en dB). El valor predeterminado es 14. Establecerlo en 0 desactiva la detección de interferencias del canal.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.'; + 'Establece el intervalo para restablecer el Control Automático de Ganancia. Establecer en 0 para desactivarlo.'; @override String get repeater_cliHelpSetMultiAcks => - 'Habilita o deshabilita la función de \'ACKs dobles\'.'; + 'Habilita o deshabilita la función de \'ACKs dobles\'.'; @override String get repeater_cliHelpSetAdvertInterval => @@ -2228,7 +2269,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetGuestPassword => - 'Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")'; + 'Establece/actualiza la contraseña del invitado. (para repetidores, los inicios de sesión de invitado pueden enviar la solicitud \"Obtener Estadísticas\")'; @override String get repeater_cliHelpSetName => 'Establece el nombre del anuncio.'; @@ -2243,15 +2284,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetRadio => - 'Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.'; + 'Establece parámetros de radio completamente nuevos y los guarda en las preferencias. Requiere un comando \"reboot\" para aplicarlos.'; @override String get repeater_cliHelpSetRxDelay => - 'Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.'; + 'Configura (experimental) la base para aplicar un ligero retraso a los paquetes recibidos, según la fuerza de la señal/puntuación. Establece en 0 para desactivar.'; @override String get repeater_cliHelpSetTxDelay => - 'Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).'; + 'Establece un factor multiplicado con el tiempo de aire para un paquete de modo de inundación y con un sistema de ranura aleatorio, para retrasar su reenvío (para disminuir la probabilidad de colisiones).'; @override String get repeater_cliHelpSetDirectTxDelay => @@ -2267,7 +2308,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetBridgeSource => - 'Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.'; + 'Elige si el puente retransmitirá paquetes recibidos o paquetes transmitidos.'; @override String get repeater_cliHelpSetBridgeBaud => @@ -2279,15 +2320,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpSetAdcMultiplier => - 'Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).'; + 'Establece un factor personalizado para ajustar el voltaje de la batería reportado (solo soportado en selectas placas).'; @override String get repeater_cliHelpTempRadio => - 'Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).'; + 'Establece parámetros de radio temporales para el número dado de minutos, volviendo a los parámetros de radio originales posteriormente. (no guarda en preferencias).'; @override String get repeater_cliHelpSetPerm => - 'Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).'; + 'Modifica el ACL. Elimina la entrada coincidente (por prefijo de pubkey) si \"permissions\" es cero. Añade una nueva entrada si el pubkey-hex tiene longitud completa y no está actualmente en el ACL. Actualiza la entrada mediante el prefijo de pubkey coincidente. Los bits de permiso varían según el rol del firmware, pero los dos bits inferiores son: 0 (Invitado), 1 (Solo lectura), 2 (Lectura/escritura), 3 (Administrador).'; @override String get repeater_cliHelpGetBridgeType => @@ -2307,7 +2348,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4'; + 'Muestra una lista de otros nodos repetidores escuchados a través de anuncios de un solo salto. Cada línea es id-prefijo-hex:marca de tiempo:times-snr-4'; @override String get repeater_cliHelpNeighborRemove => @@ -2315,38 +2356,38 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpRegion => - '(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.'; + '(solo serie) Lista todas las regiones definidas y los permisos de inundación actuales.'; @override String get repeater_cliHelpRegionLoad => - 'NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.'; + 'NOTA: este es un invocación multi-comando especial. Cada comando subsiguiente es un nombre de región (indentado con espacios para indicar la jerarquía padre, con un espacio mínimo). Terminado enviando una línea en blanco/comando.'; @override String get repeater_cliHelpRegionGet => - 'Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) \'F\'\"'; + 'Busca la región con el prefijo de nombre dado (o \"\" para el ámbito global). Responde con \"-> nombre-región (nombre-padre) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Agrega o actualiza una definición de región con el nombre dado.'; + 'Agrega o actualiza una definición de región con el nombre dado.'; @override String get repeater_cliHelpRegionRemove => - 'Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)'; + 'Elimina una definición de región con el nombre dado. (debe coincidir exactamente y no tener regiones hijas)'; @override String get repeater_cliHelpRegionAllowf => - 'Establece el permiso de \'F\'lujo para la región dada. (\'\' para el ámbito global/legado)'; + 'Establece el permiso de \'F\'lujo para la región dada. (\'\' para el ámbito global/legado)'; @override String get repeater_cliHelpRegionDenyf => - 'Elimina el permiso de \'F\'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)'; + 'Elimina el permiso de \'F\'lood para la región especificada. (NOTA: en esta etapa NO se recomienda utilizarlo en el ámbito global/legado!!)'; @override String get repeater_cliHelpRegionHome => - 'Responde con la región \'home\' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).'; + 'Responde con la región \'home\' actual. (Aún no se ha aplicado en ninguna parte, reservado para el futuro).'; @override - String get repeater_cliHelpRegionHomeSet => 'Establece la región \'hogar\'.'; + String get repeater_cliHelpRegionHomeSet => 'Establece la región \'hogar\'.'; @override String get repeater_cliHelpRegionSave => @@ -2354,7 +2395,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.'; + 'Muestra el estado del GPS. Cuando el GPS está apagado, responde solo con \"apagado\", si está encendido, responde con \"encendido\", estado, fijación, número de satélites.'; @override String get repeater_cliHelpGpsOnOff => 'Activa o desactiva el modo GPS.'; @@ -2365,28 +2406,28 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliHelpGpsSetLoc => - 'Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.'; + 'Establece la posición del nodo a las coordenadas GPS y guarda las preferencias.'; @override String get repeater_cliHelpGpsAdvert => - 'Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias'; + 'Da la configuración de la publicidad del nodo de ubicación:\n- ninguno: no incluir la ubicación en las publicidad\n- compartir: compartir la ubicación GPS (del SensorManager)\n- preferencias: publicidad la ubicación almacenada en preferencias'; @override String get repeater_cliHelpGpsAdvertSet => - 'Configura la configuración de la publicidad de la ubicación.'; + 'Configura la configuración de la publicidad de la ubicación.'; @override String get repeater_commandsListTitle => 'Lista de comandos'; @override String get repeater_commandsListNote => - 'NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".'; + 'NOTA: para los diversos comandos \"set...\", también existe un comando \"get...\".'; @override String get repeater_general => 'General'; @override - String get repeater_settingsCategory => 'Configuración'; + String get repeater_settingsCategory => 'Configuración'; @override String get repeater_bridge => 'Puente'; @@ -2399,33 +2440,32 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_regionManagementRepeaterOnly => - 'Gestión de Regiones (solo Repetidor)'; + 'Gestión de Regiones (solo Repetidor)'; @override String get repeater_regionNote => - 'Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.'; + 'Se han introducido los comandos de región para gestionar las definiciones y permisos de la región.'; @override - String get repeater_gpsManagement => 'Gestión de GPS'; + String get repeater_gpsManagement => 'Gestión de GPS'; @override String get repeater_gpsNote => - 'Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.'; + 'Se ha introducido un comando GPS para gestionar temas relacionados con la ubicación.'; @override - String get telemetry_receivedData => 'Datos de Telemetría Recibidos'; + String get telemetry_receivedData => 'Datos de Telemetría Recibidos'; @override - String get telemetry_requestTimeout => - 'Solicitud de telemetría ha expirado.'; + String get telemetry_requestTimeout => 'Solicitud de telemetría ha expirado.'; @override String telemetry_errorLoading(String error) { - return 'Error al cargar la telemetría: $error'; + return 'Error al cargar la telemetría: $error'; } @override - String get telemetry_noData => 'No hay datos de telemetría disponibles.'; + String get telemetry_noData => 'No hay datos de telemetría disponibles.'; @override String telemetry_channelTitle(int channel) { @@ -2433,7 +2473,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get telemetry_batteryLabel => 'Batería'; + String get telemetry_batteryLabel => 'Batería'; @override String get telemetry_voltageLabel => 'Voltaje'; @@ -2464,7 +2504,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2487,12 +2527,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String neighbors_unknownContact(String pubkey) { - return 'Clave pública desconocida $pubkey'; + return 'Clave pública desconocida $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Escuchado: $time hace atrás'; + return 'Escuchado: $time hace atrás'; } @override @@ -2509,7 +2549,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channelPath_noHopDetails => - 'Los detalles del paquete no están disponibles.'; + 'Los detalles del paquete no están disponibles.'; @override String get channelPath_messageDetails => 'Detalles del mensaje'; @@ -2533,11 +2573,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Ruta observada $index • $hops'; + return 'Ruta observada $index • $hops'; } @override - String get channelPath_noLocationData => 'No datos de ubicación'; + String get channelPath_noLocationData => 'No datos de ubicación'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2553,7 +2593,7 @@ class AppLocalizationsEs extends AppLocalizations { String get channelPath_unknownPath => 'Desconocido'; @override - String get channelPath_floodPath => 'Inundación'; + String get channelPath_floodPath => 'Inundación'; @override String get channelPath_directPath => 'Guardar'; @@ -2588,7 +2628,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2606,31 +2646,31 @@ class AppLocalizationsEs extends AppLocalizations { @override String get community_createDesc => - 'Crear una nueva comunidad y compartir a través de código QR.'; + 'Crear una nueva comunidad y compartir a través de código QR.'; @override - String get community_join => 'Únete'; + String get community_join => 'Únete'; @override - String get community_joinTitle => 'Únete a la comunidad'; + String get community_joinTitle => 'Únete a la comunidad'; @override String community_joinConfirmation(String name) { - return '¿Quieres unirte a la comunidad \"$name\"?'; + return '¿Quieres unirte a la comunidad \"$name\"?'; } @override - String get community_scanQr => 'Escanear Código QR de la Comunidad'; + String get community_scanQr => 'Escanear Código QR de la Comunidad'; @override String get community_scanInstructions => - 'Apunte la cámara a un código QR de la comunidad'; + 'Apunte la cámara a un código QR de la comunidad'; @override - String get community_showQr => 'Mostrar Código QR'; + String get community_showQr => 'Mostrar Código QR'; @override - String get community_publicChannel => 'Comunidad Pública'; + String get community_publicChannel => 'Comunidad Pública'; @override String get community_hashtagChannel => 'Hashtag de la Comunidad'; @@ -2648,7 +2688,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_joined(String name) { - return 'Se unió a la comunidad \"$name\"'; + return 'Se unió a la comunidad \"$name\"'; } @override @@ -2656,7 +2696,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Escanear este código QR para unirte a $name'; + return 'Escanear este código QR para unirte a $name'; } @override @@ -2664,7 +2704,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad'; @override - String get community_invalidQrCode => 'Código QR de comunidad no válido'; + String get community_invalidQrCode => 'Código QR de comunidad no válido'; @override String get community_alreadyMember => 'Ya eres Miembro'; @@ -2676,18 +2716,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get community_addPublicChannel => - 'Añadir Canal Público de la Comunidad'; + 'Añadir Canal Público de la Comunidad'; @override String get community_addPublicChannelHint => - 'Añade automáticamente el canal público para esta comunidad.'; + 'Añade automáticamente el canal público para esta comunidad.'; @override - String get community_noCommunities => 'Aún no se han unido comunidades.'; + String get community_noCommunities => 'Aún no se han unido comunidades.'; @override String get community_scanOrCreate => - 'Escanear un código QR o crear una comunidad para comenzar'; + 'Escanear un código QR o crear una comunidad para comenzar'; @override String get community_manageCommunities => 'Gestionar Comunidades'; @@ -2697,12 +2737,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_deleteConfirm(String name) { - return '¿Salir de \"$name\"?'; + return '¿Salir de \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Esto también eliminará $count canal(es) y sus mensajes.'; + return 'Esto también eliminará $count canal(es) y sus mensajes.'; } @override @@ -2711,11 +2751,11 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get community_regenerateSecret => 'Regenerar Contraseña Secreta'; + String get community_regenerateSecret => 'Regenerar Contraseña Secreta'; @override String community_regenerateSecretConfirm(String name) { - return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.'; + return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.'; } @override @@ -2723,11 +2763,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Código secreto regenerado para \"$name\"'; + return 'Código secreto regenerado para \"$name\"'; } @override - String get community_updateSecret => 'Actualizar Contraseña'; + String get community_updateSecret => 'Actualizar Contraseña'; @override String community_secretUpdated(String name) { @@ -2736,15 +2776,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"'; + return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"'; } @override - String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad'; + String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad'; @override String get community_addHashtagChannelDesc => - 'Añadir un canal con hashtag para esta comunidad'; + 'Añadir un canal con hashtag para esta comunidad'; @override String get community_selectCommunity => 'Seleccionar Comunidad'; @@ -2754,7 +2794,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get community_regularHashtagDesc => - 'Hashtag público (cualquiera puede unirse)'; + 'Hashtag público (cualquiera puede unirse)'; @override String get community_communityHashtag => 'Hashtag de la Comunidad'; @@ -2775,7 +2815,7 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_sortBy => 'Ordenar por'; @override - String get listFilter_latestMessages => 'Últimos mensajes'; + String get listFilter_latestMessages => 'Últimos mensajes'; @override String get listFilter_heardRecently => 'Escuchado recientemente'; @@ -2793,7 +2833,7 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_favorites => 'Favoritos'; @override - String get listFilter_addToFavorites => 'Añadir a favoritos'; + String get listFilter_addToFavorites => 'Añadir a favoritos'; @override String get listFilter_removeFromFavorites => 'Eliminar de las favoritas'; @@ -2814,21 +2854,20 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_newGroup => 'Nuevo grupo'; @override - String get pathTrace_you => 'Tú'; + String get pathTrace_you => 'Tú'; @override - String get pathTrace_failed => 'El trazado de ruta falló.'; + String get pathTrace_failed => 'El trazado de ruta falló.'; @override - String get pathTrace_notAvailable => - 'El trazado de ruta no está disponible.'; + String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.'; @override String get pathTrace_refreshTooltip => 'Actualizar Path Trace'; @override String get pathTrace_someHopsNoLocation => - 'Uno o más de los lúpulos carecen de una ubicación'; + 'Uno o más de los lúpulos carecen de una ubicación'; @override String get pathTrace_clearTooltip => 'Borrar ruta'; @@ -2839,7 +2878,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String losRunFailed(String error) { - return 'Error en la comprobación de la línea de visión: $error'; + return 'Error en la comprobación de la línea de visión: $error'; } @override @@ -2847,17 +2886,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get losRunToViewElevationProfile => - 'Ejecute LOS para ver el perfil de elevación'; + 'Ejecute LOS para ver el perfil de elevación'; @override - String get losMenuTitle => 'Menú LOS'; + String get losMenuTitle => 'Menú LOS'; @override String get losMenuSubtitle => 'Toque nodos o mantenga presionado el mapa para puntos personalizados'; @override - String get losShowDisplayNodes => 'Mostrar nodos de visualización'; + String get losShowDisplayNodes => 'Mostrar nodos de visualización'; @override String get losCustomPoints => 'Puntos personalizados'; @@ -2887,7 +2926,7 @@ class AppLocalizationsEs extends AppLocalizations { String get losRun => 'Ejecutar LOS'; @override - String get losNoElevationData => 'Sin datos de elevación'; + String get losNoElevationData => 'Sin datos de elevación'; @override String losProfileClear( @@ -2896,7 +2935,7 @@ class AppLocalizationsEs extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit'; + return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit'; } @override @@ -2922,11 +2961,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Datos de elevación no disponibles para una o más muestras.'; + 'Datos de elevación no disponibles para una o más muestras.'; @override String get losErrorInvalidInput => - 'Datos de puntos/elevación no válidos para el cálculo de LOS.'; + 'Datos de puntos/elevación no válidos para el cálculo de LOS.'; @override String get losRenameCustomPoint => @@ -2943,13 +2982,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get losElevationAttribution => - 'Datos de elevación: Open-Meteo (CC BY 4.0)'; + 'Datos de elevación: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; + String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; @override - String get losLegendLosBeam => 'Línea de visión'; + String get losLegendLosBeam => 'Línea de visión'; @override String get losLegendTerrain => 'Terreno'; @@ -2958,11 +2997,10 @@ class AppLocalizationsEs extends AppLocalizations { String get losFrequencyLabel => 'Frecuencia'; @override - String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; + String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; @override - String get losFrequencyDialogTitle => - 'Cálculo del horizonte radioeléctrico'; + String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico'; @override String losFrequencyDialogDescription( @@ -2971,7 +3009,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; + return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; } @override @@ -2988,7 +3026,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_roomPathTrace => - 'Rastreo de ruta al servidor de la habitación'; + 'Rastreo de ruta al servidor de la habitación'; @override String get contacts_roomPing => 'Pingar servidor de sala'; @@ -3002,23 +3040,23 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'El portapapeles está vacío.'; + String get contacts_clipboardEmpty => 'El portapapeles está vacío.'; @override - String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos'; + String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos'; @override String get contacts_contactImported => 'El contacto ha sido importado.'; @override String get contacts_contactImportFailed => - 'Contacto no se importó correctamente.'; + 'Contacto no se importó correctamente.'; @override String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop'; @override - String get contacts_floodAdvert => 'Anuncio de inundación'; + String get contacts_floodAdvert => 'Anuncio de inundación'; @override String get contacts_copyAdvertToClipboard => 'Copiar anuncio al portapapeles'; @@ -3034,8 +3072,7 @@ class AppLocalizationsEs extends AppLocalizations { String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio'; @override - String get contacts_zeroHopContactAdvertSent => - 'Envió contacto por anuncio.'; + String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.'; @override String get contacts_zeroHopContactAdvertFailed => @@ -3098,24 +3135,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportRepeatersSubtitle => - 'Exporta repetidores o roomserver con una ubicación a un archivo GPX.'; + 'Exporta repetidores o roomserver con una ubicación a un archivo GPX.'; @override - String get settings_gpxExportContacts => 'Exportar compañeros a GPX'; + String get settings_gpxExportContacts => 'Exportar compañeros a GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exporta compañeros con una ubicación a archivo GPX.'; + 'Exporta compañeros con una ubicación a archivo GPX.'; @override String get settings_gpxExportAll => 'Exportar todos los contactos a GPX'; @override String get settings_gpxExportAllSubtitle => - 'Exporta todos los contactos con una ubicación a un archivo GPX.'; + 'Exporta todos los contactos con una ubicación a un archivo GPX.'; @override - String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.'; + String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.'; @override String get settings_gpxExportNoContacts => 'No hay contactos para exportar.'; @@ -3132,7 +3169,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Ubicaciones del servidor de repetidor y sala'; @override - String get settings_gpxExportChat => 'Ubicaciones de compañero'; + String get settings_gpxExportChat => 'Ubicaciones de compañero'; @override String get settings_gpxExportAllContacts => @@ -3144,11 +3181,11 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportShareSubject => - 'meshcore-open exportación de datos de mapa GPX'; + 'meshcore-open exportación de datos de mapa GPX'; @override String get snrIndicator_nearByRepeaters => 'Repetidores cercanos'; @override - String get snrIndicator_lastSeen => 'Visto por última vez'; + String get snrIndicator_lastSeen => 'Visto por última vez'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 945a26b..7e17de9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -48,19 +48,19 @@ class AppLocalizationsFr extends AppLocalizations { String get common_add => 'Ajouter'; @override - String get common_settings => 'Paramètres'; + String get common_settings => 'Paramètres'; @override - String get common_disconnect => 'Déconnecter'; + String get common_disconnect => 'Déconnecter'; @override - String get common_connected => 'Connecté'; + String get common_connected => 'Connecté'; @override - String get common_disconnected => 'Déconnecté'; + String get common_disconnected => 'Déconnecté'; @override - String get common_create => 'Créer'; + String get common_create => 'Créer'; @override String get common_continue => 'Continuer'; @@ -72,7 +72,7 @@ class AppLocalizationsFr extends AppLocalizations { String get common_copy => 'Copier'; @override - String get common_retry => 'Réessayer'; + String get common_retry => 'Réessayer'; @override String get common_hide => 'Masquer'; @@ -84,16 +84,16 @@ class AppLocalizationsFr extends AppLocalizations { String get common_enable => 'Activer'; @override - String get common_disable => 'Désactiver'; + String get common_disable => 'Désactiver'; @override - String get common_reboot => 'Redémarrer'; + String get common_reboot => 'Redémarrer'; @override String get common_loading => 'Chargement...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -119,34 +119,77 @@ class AppLocalizationsFr extends AppLocalizations { @override String get usbScreenSubtitle => - 'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nÅ“ud MeshCore.'; + 'Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.'; @override - String get usbScreenStatus => 'Sélectionnez un périphérique USB'; + String get usbScreenStatus => 'Sélectionnez un périphérique USB'; @override String get usbScreenNote => - 'La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.'; + 'La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.'; @override String get usbScreenEmptyState => - 'Aucun périphérique USB n\'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.'; + 'Aucun périphérique USB n\'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.'; @override - String get scanner_scanning => 'Recherche de périphériques...'; + String get usbErrorPermissionDenied => 'L\'accès via USB a été refusé.'; + + @override + String get usbErrorDeviceMissing => + 'Le périphérique USB sélectionné n\'est plus disponible.'; + + @override + String get usbErrorInvalidPort => 'Sélectionnez un périphérique USB valide.'; + + @override + String get usbErrorBusy => + 'Une autre demande de connexion USB est déjà en cours.'; + + @override + String get usbErrorNotConnected => 'Aucun appareil USB n\'est connecté.'; + + @override + String get usbErrorOpenFailed => + 'Impossible d\'ouvrir l\'appareil USB sélectionné.'; + + @override + String get usbErrorConnectFailed => + 'Impossible de se connecter à l\'appareil USB sélectionné.'; + + @override + String get usbErrorUnsupported => + 'La communication série USB n\'est pas prise en charge sur cette plateforme.'; + + @override + String get usbErrorAlreadyActive => 'Une connexion USB est déjà établie.'; + + @override + String get usbErrorNoDeviceSelected => + 'Aucun appareil USB n\'a été sélectionné.'; + + @override + String get usbErrorPortClosed => 'La connexion USB n\'est pas établie.'; + + @override + String get usbErrorConnectTimedOut => + 'Attente avec délai, en attendant une réponse de l\'appareil.'; + + @override + String get scanner_scanning => 'Recherche de périphériques...'; @override String get scanner_connecting => 'Connexion en cours...'; @override - String get scanner_disconnecting => 'Déconnexion...'; + String get scanner_disconnecting => 'Déconnexion...'; @override - String get scanner_notConnected => 'Non connecté'; + String get scanner_notConnected => 'Non connecté'; @override String scanner_connectedTo(String deviceName) { - return 'Connecté à $deviceName'; + return 'Connecté à $deviceName'; } @override @@ -158,17 +201,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Échec de la connexion : $error'; + return 'Échec de la connexion : $error'; } @override - String get scanner_stop => 'Arrêter'; + String get scanner_stop => 'Arrêter'; @override String get scanner_scan => 'Scanner'; @override - String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.'; + String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.'; @override String get scanner_bluetoothOffMessage => @@ -179,7 +222,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_chromeRequiredMessage => - 'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.'; + 'Cette application web nécessite Google Chrome ou un navigateur basé sur Chromium pour le support Bluetooth.'; @override String get scanner_enableBluetooth => 'Activer le Bluetooth'; @@ -191,51 +234,51 @@ class AppLocalizationsFr extends AppLocalizations { String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Paramètres'; + String get settings_title => 'Paramètres'; @override - String get settings_deviceInfo => 'Informations du périphérique'; + String get settings_deviceInfo => 'Informations du périphérique'; @override - String get settings_appSettings => 'Paramètres de l\'application'; + String get settings_appSettings => 'Paramètres de l\'application'; @override String get settings_appSettingsSubtitle => - 'Notifications, messagerie et préférences de carte'; + 'Notifications, messagerie et préférences de carte'; @override - String get settings_nodeSettings => 'Paramètres du nÅ“ud'; + String get settings_nodeSettings => 'Paramètres du nœud'; @override - String get settings_nodeName => 'Nom du nÅ“ud'; + String get settings_nodeName => 'Nom du nœud'; @override - String get settings_nodeNameNotSet => 'Non défini'; + String get settings_nodeNameNotSet => 'Non défini'; @override - String get settings_nodeNameHint => 'Entrer le nom du nÅ“ud'; + String get settings_nodeNameHint => 'Entrer le nom du nœud'; @override - String get settings_nodeNameUpdated => 'Nom mis à jour'; + String get settings_nodeNameUpdated => 'Nom mis à jour'; @override - String get settings_radioSettings => 'Paramètres Radio'; + String get settings_radioSettings => 'Paramètres Radio'; @override String get settings_radioSettingsSubtitle => - 'Fréquence, puissance, facteur d\'espacement'; + 'Fréquence, puissance, facteur d\'espacement'; @override - String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour'; + String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour'; @override String get settings_location => 'Emplacement'; @override - String get settings_locationSubtitle => 'Coordonnées GPS'; + String get settings_locationSubtitle => 'Coordonnées GPS'; @override - String get settings_locationUpdated => 'Emplacement mis à jour'; + String get settings_locationUpdated => 'Emplacement mis à jour'; @override String get settings_locationBothRequired => @@ -249,15 +292,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Activer la mise à jour automatique de la position via GPS'; + 'Activer la mise à jour automatique de la position via GPS'; @override String get settings_locationIntervalSec => - 'Intervalle de mise-à-jour du GPS (Secondes)'; + 'Intervalle de mise-à-jour du GPS (Secondes)'; @override String get settings_locationIntervalInvalid => - 'L\'intervalle doit être compris entre 60 et 86400 secondes.'; + 'L\'intervalle doit être compris entre 60 et 86400 secondes.'; @override String get settings_latitude => 'Latitude'; @@ -266,7 +309,7 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_longitude => 'Longitude'; @override - String get settings_privacyMode => 'Mode de confidentialité'; + String get settings_privacyMode => 'Mode de confidentialité'; @override String get settings_privacyModeSubtitle => @@ -274,14 +317,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_privacyModeToggle => - 'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.'; + 'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.'; @override - String get settings_privacyModeEnabled => 'Mode de confidentialité activé'; + String get settings_privacyModeEnabled => 'Mode de confidentialité activé'; @override String get settings_privacyModeDisabled => - 'Mode de confidentialité désactivé'; + 'Mode de confidentialité désactivé'; @override String get settings_actions => 'Actions'; @@ -291,58 +334,57 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_sendAdvertisementSubtitle => - 'Présence diffusée maintenant'; + 'Présence diffusée maintenant'; @override - String get settings_advertisementSent => 'Annonce envoyée'; + String get settings_advertisementSent => 'Annonce envoyée'; @override String get settings_syncTime => 'Temps de synchronisation'; @override String get settings_syncTimeSubtitle => - 'Définir l\'heure de l\'appareil sur l\'heure du téléphone.'; + 'Définir l\'heure de l\'appareil sur l\'heure du téléphone.'; @override String get settings_timeSynchronized => 'Synchronisation temporelle'; @override - String get settings_refreshContacts => 'Rafraîchir les Contacts'; + String get settings_refreshContacts => 'Rafraîchir les Contacts'; @override String get settings_refreshContactsSubtitle => 'Recharger la liste des contacts depuis l\'appareil'; @override - String get settings_rebootDevice => 'Redémarrer l\'appareil'; + String get settings_rebootDevice => 'Redémarrer l\'appareil'; @override - String get settings_rebootDeviceSubtitle => - 'Redémarrer l\'appareil MeshCore'; + String get settings_rebootDeviceSubtitle => 'Redémarrer l\'appareil MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Êtes-vous sûr de vouloir redémarrer l\'appareil ? Vous serez déconnecté.'; + 'Êtes-vous sûr de vouloir redémarrer l\'appareil ? Vous serez déconnecté.'; @override - String get settings_debug => 'Déboguer'; + String get settings_debug => 'Déboguer'; @override - String get settings_bleDebugLog => 'Journal de débogage BLE'; + String get settings_bleDebugLog => 'Journal de débogage BLE'; @override String get settings_bleDebugLogSubtitle => - 'Commandes BLE, réponses et données brutes'; + 'Commandes BLE, réponses et données brutes'; @override - String get settings_appDebugLog => 'Journal de débogage de l\'application'; + String get settings_appDebugLog => 'Journal de débogage de l\'application'; @override String get settings_appDebugLogSubtitle => - 'Messages de débogage de l\'application'; + 'Messages de débogage de l\'application'; @override - String get settings_about => 'À propos'; + String get settings_about => 'À propos'; @override String settings_aboutVersion(String version) { @@ -354,11 +396,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_aboutDescription => - 'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.'; + 'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.'; @override String get settings_aboutOpenMeteoAttribution => - 'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)'; + 'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Nom'; @@ -367,13 +409,13 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_infoId => 'ID'; @override - String get settings_infoStatus => 'État'; + String get settings_infoStatus => 'État'; @override String get settings_infoBattery => 'Batterie'; @override - String get settings_infoPublicKey => 'Clé Publique'; + String get settings_infoPublicKey => 'Clé Publique'; @override String get settings_infoContactsCount => 'Nombre de contacts'; @@ -382,22 +424,22 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_infoChannelCount => 'Nombre de canaux'; @override - String get settings_presets => 'Préréglages'; + String get settings_presets => 'Préréglages'; @override - String get settings_frequency => 'Fréquence (MHz)'; + String get settings_frequency => 'Fréquence (MHz)'; @override String get settings_frequencyHelper => '300,0 - 2 500,0'; @override - String get settings_frequencyInvalid => 'Fréquence invalide (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Fréquence invalide (300-2500 MHz)'; @override String get settings_bandwidth => 'Bande passante'; @override - String get settings_spreadingFactor => 'Facteur de répartition'; + String get settings_spreadingFactor => 'Facteur de répartition'; @override String get settings_codingRate => 'Taux de codage'; @@ -412,15 +454,15 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)'; @override - String get settings_clientRepeat => 'Répétition hors réseau'; + String get settings_clientRepeat => 'Répétition hors réseau'; @override String get settings_clientRepeatSubtitle => - 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; + 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; @override String get settings_clientRepeatFreqWarning => - 'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.'; + 'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { @@ -428,19 +470,19 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get appSettings_title => 'Paramètres de l\'application'; + String get appSettings_title => 'Paramètres de l\'application'; @override String get appSettings_appearance => 'Apparence'; @override - String get appSettings_theme => 'Thème'; + String get appSettings_theme => 'Thème'; @override - String get appSettings_themeSystem => 'Défaut système'; + String get appSettings_themeSystem => 'Défaut système'; @override - String get appSettings_themeLight => 'Lumière'; + String get appSettings_themeLight => 'Lumière'; @override String get appSettings_themeDark => 'Sombre'; @@ -449,16 +491,16 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_language => 'Langue'; @override - String get appSettings_languageSystem => 'Par défaut du système'; + String get appSettings_languageSystem => 'Par défaut du système'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -467,16 +509,16 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -485,10 +527,10 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russe'; @@ -498,11 +540,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_enableMessageTracing => - 'Activer le traçage des messages'; + 'Activer le traçage des messages'; @override String get appSettings_enableMessageTracingSubtitle => - 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; + 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; @override String get appSettings_notifications => 'Notifications'; @@ -516,20 +558,20 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_notificationPermissionDenied => - 'Permission de notification refusée'; + 'Permission de notification refusée'; @override - String get appSettings_notificationsEnabled => 'Notifications activées'; + String get appSettings_notificationsEnabled => 'Notifications activées'; @override - String get appSettings_notificationsDisabled => 'Notifications désactivées'; + String get appSettings_notificationsDisabled => 'Notifications désactivées'; @override String get appSettings_messageNotifications => 'Notifications de Messages'; @override String get appSettings_messageNotificationsSubtitle => - 'Afficher une notification lors de la réception de nouveaux messages'; + 'Afficher une notification lors de la réception de nouveaux messages'; @override String get appSettings_channelMessageNotifications => @@ -537,7 +579,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_channelMessageNotificationsSubtitle => - 'Afficher une notification lors de la réception des messages de canal'; + 'Afficher une notification lors de la réception des messages de canal'; @override String get appSettings_advertisementNotifications => @@ -545,7 +587,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'Afficher une notification lors de la découverte de nouveaux nÅ“uds'; + 'Afficher une notification lors de la découverte de nouveaux nœuds'; @override String get appSettings_messaging => 'Messagerie'; @@ -556,31 +598,31 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Réinitialiser le chemin de contact après 5 tentatives d\'envoi infructueuses'; + 'Réinitialiser le chemin de contact après 5 tentatives d\'envoi infructueuses'; @override String get appSettings_pathsWillBeCleared => - 'Les chemins seront effacés après 5 tentatives infructueuses.'; + 'Les chemins seront effacés après 5 tentatives infructueuses.'; @override String get appSettings_pathsWillNotBeCleared => - 'Les chemins ne seront pas effacés automatiquement.'; + 'Les chemins ne seront pas effacés automatiquement.'; @override String get appSettings_autoRouteRotation => - 'Rotation de l\'itinéraire automatique'; + 'Rotation de l\'itinéraire automatique'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alterner entre les meilleurs chemins et le mode d\'envoi sur tout le réseau (flood)'; + 'Alterner entre les meilleurs chemins et le mode d\'envoi sur tout le réseau (flood)'; @override String get appSettings_autoRouteRotationEnabled => - 'Rotation du routage automatique activée'; + 'Rotation du routage automatique activée'; @override String get appSettings_autoRouteRotationDisabled => - 'Rotation de l\'itinéraire automatique désactivée'; + 'Rotation de l\'itinéraire automatique désactivée'; @override String get appSettings_battery => 'Batterie'; @@ -590,7 +632,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Définir par appareil ($deviceName)'; + return 'Définir par appareil ($deviceName)'; } @override @@ -610,35 +652,35 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_mapDisplay => 'Affichage de la carte'; @override - String get appSettings_showRepeaters => 'Afficher les répéteurs'; + String get appSettings_showRepeaters => 'Afficher les répéteurs'; @override String get appSettings_showRepeatersSubtitle => - 'Afficher les nÅ“uds répéteurs sur la carte'; + 'Afficher les nœuds répéteurs sur la carte'; @override - String get appSettings_showChatNodes => 'Afficher les nÅ“uds de discussion'; + String get appSettings_showChatNodes => 'Afficher les nœuds de discussion'; @override String get appSettings_showChatNodesSubtitle => - 'Afficher les nÅ“uds de chat sur la carte'; + 'Afficher les nœuds de chat sur la carte'; @override - String get appSettings_showOtherNodes => 'Afficher d\'autres nÅ“uds'; + String get appSettings_showOtherNodes => 'Afficher d\'autres nœuds'; @override String get appSettings_showOtherNodesSubtitle => - 'Afficher d\'autres types de nÅ“uds sur la carte'; + 'Afficher d\'autres types de nœuds sur la carte'; @override String get appSettings_timeFilter => 'Filtre du temps'; @override - String get appSettings_timeFilterShowAll => 'Afficher tous les nÅ“uds'; + String get appSettings_timeFilterShowAll => 'Afficher tous les nœuds'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Afficher les nÅ“uds des $hours dernières heures'; + return 'Afficher les nœuds des $hours dernières heures'; } @override @@ -646,71 +688,71 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_showNodesDiscoveredWithin => - 'Afficher les nÅ“uds découverts dans :'; + 'Afficher les nœuds découverts dans :'; @override String get appSettings_allTime => 'Tout le temps'; @override - String get appSettings_lastHour => 'Dernière heure'; + String get appSettings_lastHour => 'Dernière heure'; @override - String get appSettings_last6Hours => 'Dernières 6 heures'; + String get appSettings_last6Hours => 'Dernières 6 heures'; @override - String get appSettings_last24Hours => 'Dernières 24 heures'; + String get appSettings_last24Hours => 'Dernières 24 heures'; @override - String get appSettings_lastWeek => 'La semaine dernière'; + String get appSettings_lastWeek => 'La semaine dernière'; @override String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne'; @override - String get appSettings_unitsTitle => 'Unités'; + String get appSettings_unitsTitle => 'Unités'; @override - String get appSettings_unitsMetric => 'Métrique (m/km)'; + String get appSettings_unitsMetric => 'Métrique (m/km)'; @override - String get appSettings_unitsImperial => 'Impérial (ft / mi)'; + String get appSettings_unitsImperial => 'Impérial (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Aucune zone sélectionnée'; + String get appSettings_noAreaSelected => 'Aucune zone sélectionnée'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Zone sélectionnée (zoom $minZoom-$maxZoom)'; + return 'Zone sélectionnée (zoom $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Déboguer'; + String get appSettings_debugCard => 'Déboguer'; @override String get appSettings_appDebugLogging => - 'Journalisation de débogage de l\'application'; + 'Journalisation de débogage de l\'application'; @override String get appSettings_appDebugLoggingSubtitle => - 'Enregistrez les messages de débogage de l\'application Log pour le dépannage.'; + 'Enregistrez les messages de débogage de l\'application Log pour le dépannage.'; @override String get appSettings_appDebugLoggingEnabled => - 'Journalisation de débogage de l\'application activée'; + 'Journalisation de débogage de l\'application activée'; @override String get appSettings_appDebugLoggingDisabled => - 'Le débogage de l\'application est désactivé.'; + 'Le débogage de l\'application est désactivé.'; @override String get contacts_title => 'Contacts'; @override - String get contacts_noContacts => 'Aucun contact trouvé.'; + String get contacts_noContacts => 'Aucun contact trouvé.'; @override String get contacts_contactsWillAppear => - 'Les contacts apparaîtront lorsque les appareils font leur annonce.'; + 'Les contacts apparaîtront lorsque les appareils font leur annonce.'; @override String get contacts_unread => 'Non lu'; @@ -735,7 +777,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_searchRepeaters(int number, String str) { - return 'Rechercher $number$str Répéteurs...'; + return 'Rechercher $number$str Répéteurs...'; } @override @@ -747,7 +789,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_noUnreadContacts => 'Aucun contact non lu'; @override - String get contacts_noContactsFound => 'Aucun contact ou groupe trouvé.'; + String get contacts_noContactsFound => 'Aucun contact ou groupe trouvé.'; @override String get contacts_deleteContact => 'Supprimer le contact'; @@ -758,10 +800,10 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Gérer le répéteur'; + String get contacts_manageRepeater => 'Gérer le répéteur'; @override - String get contacts_manageRoom => 'Gérer le Room Server'; + String get contacts_manageRoom => 'Gérer le Room Server'; @override String get contacts_roomLogin => 'Connexion Room Server'; @@ -791,7 +833,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_groupAlreadyExists(String name) { - return 'Le groupe \"$name\" existe déjà.'; + return 'Le groupe \"$name\" existe déjà.'; } @override @@ -799,7 +841,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Aucun contact ne correspond à votre filtre.'; + 'Aucun contact ne correspond à votre filtre.'; @override String get contacts_noMembers => 'Aucun membre'; @@ -832,7 +874,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_title => 'Canaux'; @override - String get channels_noChannelsConfigured => 'Aucun canal configuré'; + String get channels_noChannelsConfigured => 'Aucun canal configuré'; @override String get channels_addPublicChannel => 'Ajouter un canal public'; @@ -841,7 +883,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_searchChannels => 'Rechercher des canaux...'; @override - String get channels_noChannelsFound => 'Aucun canal trouvé'; + String get channels_noChannelsFound => 'Aucun canal trouvé'; @override String channels_channelIndex(int index) { @@ -855,39 +897,39 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_public => 'Public'; @override - String get channels_private => 'Privé'; + String get channels_private => 'Privé'; @override String get channels_publicChannel => 'Canal public'; @override - String get channels_privateChannel => 'Canal privé'; + String get channels_privateChannel => 'Canal privé'; @override String get channels_editChannel => 'Modifier le canal'; @override - String get channels_muteChannel => 'Désactiver les notifications du canal'; + String get channels_muteChannel => 'Désactiver les notifications du canal'; @override - String get channels_unmuteChannel => 'Réactiver les notifications du canal'; + String get channels_unmuteChannel => 'Réactiver les notifications du canal'; @override String get channels_deleteChannel => 'Supprimer le canal'; @override String channels_deleteChannelConfirm(String name) { - return 'Supprimer $name? Cela ne peut pas être annulé.'; + return 'Supprimer $name? Cela ne peut pas être annulé.'; } @override String channels_channelDeleteFailed(String name) { - return 'Échec de la suppression de la chaîne \"$name\"'; + return 'Échec de la suppression de la chaîne \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Le canal \"$name\" a été supprimé'; + return 'Le canal \"$name\" a été supprimé'; } @override @@ -910,18 +952,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channels_generateRandomPsk => - 'Générer une clé de modulation PSK aléatoire'; + 'Générer une clé de modulation PSK aléatoire'; @override String get channels_enterChannelName => 'Veuillez entrer un nom de canal'; @override String get channels_pskMustBe32Hex => - 'Le PKS doit être composé de 32 caractères hexadécimaux.'; + 'Le PKS doit être composé de 32 caractères hexadécimaux.'; @override String channels_channelAdded(String name) { - return 'Le canal \"$name\" a été ajouté'; + return 'Le canal \"$name\" a été ajouté'; } @override @@ -934,11 +976,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String channels_channelUpdated(String name) { - return 'Le canal \"$name\" a été mis à jour'; + return 'Le canal \"$name\" a été mis à jour'; } @override - String get channels_publicChannelAdded => 'Le canal public a été ajouté'; + String get channels_publicChannelAdded => 'Le canal public a été ajouté'; @override String get channels_sortBy => 'Trier par'; @@ -947,7 +989,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_sortManual => 'Manuel'; @override - String get channels_sortAZ => 'A à Z'; + String get channels_sortAZ => 'A à Z'; @override String get channels_sortLatestMessages => 'Derniers messages'; @@ -956,18 +998,18 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_sortUnread => 'Non lu'; @override - String get channels_createPrivateChannel => 'Créer un Canal Privé'; + String get channels_createPrivateChannel => 'Créer un Canal Privé'; @override String get channels_createPrivateChannelDesc => - 'Sécurisé avec une clé secrète.'; + 'Sécurisé avec une clé secrète.'; @override - String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé'; + String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé'; @override String get channels_joinPrivateChannelDesc => - 'Entrer manuellement une clé secrète.'; + 'Entrer manuellement une clé secrète.'; @override String get channels_joinPublicChannel => 'Rejoindre le canal public'; @@ -987,7 +1029,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channels_scanQrCode => 'Scanner un code QR'; @override - String get channels_scanQrCodeComingSoon => 'Bientôt disponible'; + String get channels_scanQrCodeComingSoon => 'Bientôt disponible'; @override String get channels_enterHashtag => 'Entrez le hashtag'; @@ -1002,16 +1044,16 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_sendMessageToStart => 'Envoyer un message pour commencer'; @override - String get chat_originalMessageNotFound => 'Message d\'origine non trouvé'; + String get chat_originalMessageNotFound => 'Message d\'origine non trouvé'; @override String chat_replyingTo(String name) { - return 'Répondre à $name'; + return 'Répondre à $name'; } @override String chat_replyTo(String name) { - return 'Répondre à $name'; + return 'Répondre à $name'; } @override @@ -1019,7 +1061,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'Envoyer un message à $contactName'; + return 'Envoyer un message à $contactName'; } @override @@ -1031,13 +1073,13 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get chat_messageCopied => 'Message copié'; + String get chat_messageCopied => 'Message copié'; @override - String get chat_messageDeleted => 'Message supprimé'; + String get chat_messageDeleted => 'Message supprimé'; @override - String get chat_retryingMessage => 'Tentative de récupération.'; + String get chat_retryingMessage => 'Tentative de récupération.'; @override String chat_retryCount(int current, int max) { @@ -1048,22 +1090,22 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_sendGif => 'Envoyer GIF'; @override - String get chat_reply => 'Répondre'; + String get chat_reply => 'Répondre'; @override - String get chat_addReaction => 'Ajouter une Réaction'; + String get chat_addReaction => 'Ajouter une Réaction'; @override String get chat_me => 'Moi'; @override - String get emojiCategorySmileys => 'Émojis'; + String get emojiCategorySmileys => 'Émojis'; @override String get emojiCategoryGestures => 'Gestes'; @override - String get emojiCategoryHearts => 'CÅ“urs'; + String get emojiCategoryHearts => 'Cœurs'; @override String get emojiCategoryObjects => 'Objets'; @@ -1075,25 +1117,25 @@ class AppLocalizationsFr extends AppLocalizations { String get gifPicker_searchHint => 'Rechercher des GIF...'; @override - String get gifPicker_poweredBy => 'Propulsé par GIPHY'; + String get gifPicker_poweredBy => 'Propulsé par GIPHY'; @override - String get gifPicker_noGifsFound => 'Aucun GIF trouvé'; + String get gifPicker_noGifsFound => 'Aucun GIF trouvé'; @override String get gifPicker_failedLoad => 'Impossible de charger les GIFs'; @override - String get gifPicker_failedSearch => 'Recherche de GIFs échouée'; + String get gifPicker_failedSearch => 'Recherche de GIFs échouée'; @override String get gifPicker_noInternet => 'Aucune connexion internet'; @override - String get debugLog_appTitle => 'Journal de débogage de l\'application'; + String get debugLog_appTitle => 'Journal de débogage de l\'application'; @override - String get debugLog_bleTitle => 'Journal de débogage BLE'; + String get debugLog_bleTitle => 'Journal de débogage BLE'; @override String get debugLog_copyLog => 'Copier le journal'; @@ -1102,17 +1144,17 @@ class AppLocalizationsFr extends AppLocalizations { String get debugLog_clearLog => 'Effacer le journal'; @override - String get debugLog_copied => 'Journal de débogage copié'; + String get debugLog_copied => 'Journal de débogage copié'; @override - String get debugLog_bleCopied => 'Journal BLE copié'; + String get debugLog_bleCopied => 'Journal BLE copié'; @override - String get debugLog_noEntries => 'Aucun journal de débogage pour le moment.'; + String get debugLog_noEntries => 'Aucun journal de débogage pour le moment.'; @override String get debugLog_enableInSettings => - 'Activer le débogage de l\'application dans les paramètres'; + 'Activer le débogage de l\'application dans les paramètres'; @override String get debugLog_frames => 'Cadres'; @@ -1122,11 +1164,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get debugLog_noBleActivity => - 'Pas d\'activité BLE enregistrée pour le moment.'; + 'Pas d\'activité BLE enregistrée pour le moment.'; @override String debugFrame_length(int count) { - return 'Longueur du cadre : $count octets'; + return 'Longueur du cadre : $count octets'; } @override @@ -1154,7 +1196,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String debugFrame_textType(int type, String label) { - return '- Type de texte : $type ($label)'; + return '- Type de texte : $type ($label)'; } @override @@ -1169,7 +1211,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :'; + String get debugFrame_hexDump => 'Vidéo de Dump Hexadécimal :'; @override String get chat_pathManagement => 'Gestion des chemins'; @@ -1181,18 +1223,18 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_routingMode => 'Mode de routage'; @override - String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; + String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; @override - String get chat_forceFloodMode => 'Mode tout le réseau forcé'; + String get chat_forceFloodMode => 'Mode tout le réseau forcé'; @override String get chat_recentAckPaths => - 'Chemins ACK récents (touchez pour utiliser) :'; + 'Chemins ACK récents (touchez pour utiliser) :'; @override String get chat_pathHistoryFull => - 'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.'; + 'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.'; @override String get chat_hopSingular => 'saut'; @@ -1212,35 +1254,35 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get chat_successes => 'Succès'; + String get chat_successes => 'Succès'; @override String get chat_removePath => 'Supprimer le chemin'; @override String get chat_noPathHistoryYet => - 'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; + 'Aucune historique de parcours disponible.\nEnvoyez un message pour découvrir les parcours.'; @override - String get chat_pathActions => 'Actions du chemin :'; + String get chat_pathActions => 'Actions du chemin :'; @override - String get chat_setCustomPath => 'Définir un chemin personnalisé'; + String get chat_setCustomPath => 'Définir un chemin personnalisé'; @override String get chat_setCustomPathSubtitle => - 'Spécifier manuellement le chemin de routage'; + 'Spécifier manuellement le chemin de routage'; @override String get chat_clearPath => 'Effacer le chemin'; @override String get chat_clearPathSubtitle => - 'Forcer la redécouverte lors de la prochaine envoi'; + 'Forcer la redécouverte lors de la prochaine envoi'; @override String get chat_pathCleared => - 'Le chemin est dégagé. Le prochain message redécouvrira le tracé.'; + 'Le chemin est dégagé. Le prochain message redécouvrira le tracé.'; @override String get chat_floodModeSubtitle => @@ -1248,14 +1290,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.'; + 'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.'; @override String get chat_fullPath => 'Chemin complet'; @override String get chat_pathDetailsNotAvailable => - 'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.'; + 'Les détails du chemin ne sont pas encore disponibles. Essayez d\'envoyer un message pour rafraîchir.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1265,19 +1307,19 @@ class AppLocalizationsFr extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Chemin défini : $hopCount $_temp0 - $status'; + return 'Chemin défini : $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Sauvegardé localement. Connectez-vous pour synchroniser.'; + 'Sauvegardé localement. Connectez-vous pour synchroniser.'; @override - String get chat_pathDeviceConfirmed => 'Appareil confirmé.'; + String get chat_pathDeviceConfirmed => 'Appareil confirmé.'; @override String get chat_pathDeviceNotConfirmed => - 'L\'appareil n\'a pas encore été confirmé.'; + 'L\'appareil n\'a pas encore été confirmé.'; @override String get chat_type => 'Saisir'; @@ -1286,31 +1328,31 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_path => 'Chemin'; @override - String get chat_publicKey => 'Clé Publique'; + String get chat_publicKey => 'Clé Publique'; @override String get chat_compressOutgoingMessages => 'Compresser les messages sortants'; @override - String get chat_floodForced => 'Tout le réseau (forcée)'; + String get chat_floodForced => 'Tout le réseau (forcée)'; @override - String get chat_directForced => 'Direct (forcé)'; + String get chat_directForced => 'Direct (forcé)'; @override String chat_hopsForced(int count) { - return '$count sauts (forcés)'; + return '$count sauts (forcés)'; } @override - String get chat_floodAuto => 'Tout le réseau (auto)'; + String get chat_floodAuto => 'Tout le réseau (auto)'; @override String get chat_direct => 'Afficher'; @override - String get chat_poiShared => 'Point d\'intérêt Partagé'; + String get chat_poiShared => 'Point d\'intérêt Partagé'; @override String chat_unread(int count) { @@ -1336,7 +1378,7 @@ class AppLocalizationsFr extends AppLocalizations { String get chat_invalidLink => 'Format de lien invalide'; @override - String get map_title => 'Carte des nÅ“uds'; + String get map_title => 'Carte des nœuds'; @override String get map_lineOfSight => 'Ligne de vue'; @@ -1346,15 +1388,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_noNodesWithLocation => - 'Aucun nÅ“ud avec des données de localisation'; + 'Aucun nœud avec des données de localisation'; @override String get map_nodesNeedGps => - 'Les nÅ“uds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.'; + 'Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.'; @override String map_nodesCount(int count) { - return 'NÅ“uds : $count'; + return 'Nœuds : $count'; } @override @@ -1366,7 +1408,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_chat => 'Chat'; @override - String get map_repeater => 'Répéteur'; + String get map_repeater => 'Répéteur'; @override String get map_room => 'Salle'; @@ -1375,23 +1417,23 @@ class AppLocalizationsFr extends AppLocalizations { String get map_sensor => 'Capteur'; @override - String get map_pinDm => 'Clé (DM)'; + String get map_pinDm => 'Clé (DM)'; @override - String get map_pinPrivate => 'Verrouiller (Privé)'; + String get map_pinPrivate => 'Verrouiller (Privé)'; @override - String get map_pinPublic => 'Clé (Public)'; + String get map_pinPublic => 'Clé (Public)'; @override - String get map_lastSeen => 'Dernière fois vu'; + String get map_lastSeen => 'Dernière fois vu'; @override String get map_disconnectConfirm => - 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; + 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; @override - String get map_from => 'À partir de'; + String get map_from => 'À partir de'; @override String get map_source => 'Source'; @@ -1403,13 +1445,13 @@ class AppLocalizationsFr extends AppLocalizations { String get map_shareMarkerHere => 'Partager le marqueur ici'; @override - String get map_pinLabel => 'Étiquete de repin'; + String get map_pinLabel => 'Étiquete de repin'; @override - String get map_label => 'Étiquette'; + String get map_label => 'Étiquette'; @override - String get map_pointOfInterest => 'Point d\'intérêt'; + String get map_pointOfInterest => 'Point d\'intérêt'; @override String get map_sendToContact => 'Envoyer au contact'; @@ -1425,89 +1467,89 @@ class AppLocalizationsFr extends AppLocalizations { @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Vous êtes sur le point de partager un emplacement dans $channelLabel. Ce canal est public et toute personne disposant de la clé PSK peut le voir.'; + return 'Vous êtes sur le point de partager un emplacement dans $channelLabel. Ce canal est public et toute personne disposant de la clé PSK peut le voir.'; } @override String get map_connectToShareMarkers => - 'Connectez-vous à un appareil pour partager des marqueurs'; + 'Connectez-vous à un appareil pour partager des marqueurs'; @override - String get map_filterNodes => 'Filtrer les nÅ“uds'; + String get map_filterNodes => 'Filtrer les nœuds'; @override - String get map_nodeTypes => 'Types de nÅ“uds'; + String get map_nodeTypes => 'Types de nœuds'; @override - String get map_chatNodes => 'NÅ“uds de Chat'; + String get map_chatNodes => 'Nœuds de Chat'; @override - String get map_repeaters => 'Répéteurs'; + String get map_repeaters => 'Répéteurs'; @override - String get map_otherNodes => 'Autres nÅ“uds'; + String get map_otherNodes => 'Autres nœuds'; @override - String get map_keyPrefix => 'Préfixe clé'; + String get map_keyPrefix => 'Préfixe clé'; @override - String get map_filterByKeyPrefix => 'Filtrer par préfixe de clé'; + String get map_filterByKeyPrefix => 'Filtrer par préfixe de clé'; @override - String get map_publicKeyPrefix => 'Préfixe de clé publique'; + String get map_publicKeyPrefix => 'Préfixe de clé publique'; @override String get map_markers => 'Marqueurs'; @override - String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; + String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; @override - String get map_lastSeenTime => 'Dernière fois vu'; + String get map_lastSeenTime => 'Dernière fois vu'; @override - String get map_sharedPin => 'Clé partagée'; + String get map_sharedPin => 'Clé partagée'; @override String get map_joinRoom => 'Rejoindre la salle'; @override - String get map_manageRepeater => 'Gérer le répéteur'; + String get map_manageRepeater => 'Gérer le répéteur'; @override String get map_tapToAdd => - 'Appuyez sur les nÅ“uds pour les ajouter au chemin.'; + 'Appuyez sur les nœuds pour les ajouter au chemin.'; @override - String get map_runTrace => 'Exécuter la traçage de chemin'; + String get map_runTrace => 'Exécuter la traçage de chemin'; @override String get map_removeLast => 'Supprimer le dernier'; @override - String get map_pathTraceCancelled => 'Traçage de chemin annulé'; + String get map_pathTraceCancelled => 'Traçage de chemin annulé'; @override String get mapCache_title => 'Cache de Carte Hors Ligne'; @override String get mapCache_selectAreaFirst => - 'Sélectionner une zone pour la mise en cache en premier'; + 'Sélectionner une zone pour la mise en cache en premier'; @override String get mapCache_noTilesToDownload => - 'Aucun tuilage à télécharger pour cette zone.'; + 'Aucun tuilage à télécharger pour cette zone.'; @override - String get mapCache_downloadTilesTitle => 'Télécharger les tuiles'; + String get mapCache_downloadTilesTitle => 'Télécharger les tuiles'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Télécharger $count tuiles pour un usage hors ligne ?'; + return 'Télécharger $count tuiles pour un usage hors ligne ?'; } @override - String get mapCache_downloadAction => 'Télécharger'; + String get mapCache_downloadAction => 'Télécharger'; @override String mapCache_cachedTiles(int count) { @@ -1516,7 +1558,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Tuiles mis en cache ($downloaded) ($failed ratés)'; + return 'Tuiles mis en cache ($downloaded) ($failed ratés)'; } @override @@ -1524,14 +1566,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get mapCache_clearOfflineCachePrompt => - 'Supprimer toutes les tuiles de carte mises en cache ?'; + 'Supprimer toutes les tuiles de carte mises en cache ?'; @override String get mapCache_offlineCacheCleared => - 'Le cache hors ligne a été effacé.'; + 'Le cache hors ligne a été effacé.'; @override - String get mapCache_noAreaSelected => 'Aucune zone sélectionnée'; + String get mapCache_noAreaSelected => 'Aucune zone sélectionnée'; @override String get mapCache_cacheArea => 'Zone de cache'; @@ -1549,18 +1591,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String mapCache_downloadedTiles(int completed, int total) { - return 'Téléchargé $completed / $total'; + return 'Téléchargé $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Télécharger les tuiles'; + String get mapCache_downloadTilesButton => 'Télécharger les tuiles'; @override String get mapCache_clearCacheButton => 'Vider le Cache'; @override String mapCache_failedDownloads(int count) { - return 'Téléchargements échoués : $count'; + return 'Téléchargements échoués : $count'; } @override @@ -1622,14 +1664,14 @@ class AppLocalizationsFr extends AppLocalizations { String get time_allTime => 'Tout le temps'; @override - String get dialog_disconnect => 'Déconnecter'; + String get dialog_disconnect => 'Déconnecter'; @override String get dialog_disconnectConfirm => - 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; + 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; @override - String get login_repeaterLogin => 'Connexion au répéteur'; + String get login_repeaterLogin => 'Connexion au répéteur'; @override String get login_roomLogin => 'Connexion Room Server'; @@ -1645,15 +1687,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'Le mot de passe sera stocké en toute sécurité sur cet appareil.'; + 'Le mot de passe sera stocké en toute sécurité sur cet appareil.'; @override String get login_repeaterDescription => - 'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.'; + 'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.'; @override String get login_roomDescription => - 'Entrez le mot de passe de la pièce pour accéder aux paramètres et à l\'état.'; + 'Entrez le mot de passe de la pièce pour accéder aux paramètres et à l\'état.'; @override String get login_routing => 'Redirection'; @@ -1662,13 +1704,13 @@ class AppLocalizationsFr extends AppLocalizations { String get login_routingMode => 'Mode de routage'; @override - String get login_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; + String get login_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)'; @override - String get login_forceFloodMode => 'Mode tout le réseau forcé'; + String get login_forceFloodMode => 'Mode tout le réseau forcé'; @override - String get login_managePaths => 'Gérer les chemins'; + String get login_managePaths => 'Gérer les chemins'; @override String get login_login => 'Connexion'; @@ -1680,12 +1722,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String login_failed(String error) { - return 'Connexion échouée : $error'; + return 'Connexion échouée : $error'; } @override String get login_failedMessage => - 'Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.'; + 'Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.'; @override String get common_reload => 'Recharger'; @@ -1710,52 +1752,51 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get path_enterCustomPath => 'Entrer un chemin personnalisé'; + String get path_enterCustomPath => 'Entrer un chemin personnalisé'; @override String get path_currentPathLabel => 'Chemin actuel'; @override String get path_hexPrefixInstructions => - 'Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.'; + 'Entrez les préfixes hexadécimaux de 2 caractères pour chaque saut, séparés par des virgules.'; @override String get path_hexPrefixExample => - 'Exemple : A1,F2,3C (chaque nÅ“ud utilise le premier octet de sa clé publique).'; + 'Exemple : A1,F2,3C (chaque nœud utilise le premier octet de sa clé publique).'; @override - String get path_labelHexPrefixes => 'Préfixes hexadécimaux'; + String get path_labelHexPrefixes => 'Préfixes hexadécimaux'; @override String get path_helperMaxHops => - 'Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)'; + 'Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)'; @override - String get path_selectFromContacts => - 'Sélectionner à partir des contacts :'; + String get path_selectFromContacts => 'Sélectionner à partir des contacts :'; @override String get path_noRepeatersFound => - 'Aucun répéteur ou serveur de salle n\'a été trouvé.'; + 'Aucun répéteur ou serveur de salle n\'a été trouvé.'; @override String get path_customPathsRequire => - 'Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.'; + 'Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Préfixes hexadécimaux invalides : $prefixes'; + return 'Préfixes hexadécimaux invalides : $prefixes'; } @override String get path_tooLong => - 'Le chemin est trop long. Maximum 64 sauts autorisés.'; + 'Le chemin est trop long. Maximum 64 sauts autorisés.'; @override - String get path_setPath => 'Définir le chemin'; + String get path_setPath => 'Définir le chemin'; @override - String get repeater_management => 'Gestion des répéteurs'; + String get repeater_management => 'Gestion des répéteurs'; @override String get room_management => 'Administrattion Room Server'; @@ -1764,24 +1805,24 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_managementTools => 'Outils de Gestion'; @override - String get repeater_status => 'État'; + String get repeater_status => 'État'; @override String get repeater_statusSubtitle => - 'Afficher l\'état, les statistiques et les voisins du répéteur'; + 'Afficher l\'état, les statistiques et les voisins du répéteur'; @override - String get repeater_telemetry => 'Télémetrie'; + String get repeater_telemetry => 'Télémetrie'; @override String get repeater_telemetrySubtitle => - 'Afficher la télémétrie des capteurs et les statistiques du système'; + 'Afficher la télémétrie des capteurs et les statistiques du système'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; + String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; @override String get repeater_neighbors => 'Voisins'; @@ -1790,34 +1831,34 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.'; @override - String get repeater_settings => 'Paramètres'; + String get repeater_settings => 'Paramètres'; @override String get repeater_settingsSubtitle => - 'Configurer les paramètres du répéteur'; + 'Configurer les paramètres du répéteur'; @override - String get repeater_statusTitle => 'État du répéteur'; + String get repeater_statusTitle => 'État du répéteur'; @override String get repeater_routingMode => 'Mode de routage'; @override String get repeater_autoUseSavedPath => - 'Auto (utiliser le chemin sauvegardé)'; + 'Auto (utiliser le chemin sauvegardé)'; @override - String get repeater_forceFloodMode => 'Mode tout le réseau forcé'; + String get repeater_forceFloodMode => 'Mode tout le réseau forcé'; @override String get repeater_pathManagement => 'Gestion des chemins'; @override - String get repeater_refresh => 'Rafraîchir'; + String get repeater_refresh => 'Rafraîchir'; @override String get repeater_statusRequestTimeout => - 'Demande de statut délai dépassé.'; + 'Demande de statut délai dépassé.'; @override String repeater_errorLoadingStatus(String error) { @@ -1825,22 +1866,22 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_systemInformation => 'Informations Système'; + String get repeater_systemInformation => 'Informations Système'; @override String get repeater_battery => 'Batterie'; @override - String get repeater_clockAtLogin => 'Horloge (au démarrage)'; + String get repeater_clockAtLogin => 'Horloge (au démarrage)'; @override - String get repeater_uptime => 'Disponibilité'; + String get repeater_uptime => 'Disponibilité'; @override String get repeater_queueLength => 'Longueur de la file d\'attente'; @override - String get repeater_debugFlags => 'Marqueurs de débogage'; + String get repeater_debugFlags => 'Marqueurs de débogage'; @override String get repeater_radioStatistics => 'Statistiques Radio'; @@ -1864,10 +1905,10 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_packetStatistics => 'Statistiques des paquets'; @override - String get repeater_sent => 'Envoyé'; + String get repeater_sent => 'Envoyé'; @override - String get repeater_received => 'Reçu'; + String get repeater_received => 'Reçu'; @override String get repeater_duplicates => 'Doublons'; @@ -1884,17 +1925,17 @@ class AppLocalizationsFr extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; + return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; + return 'Total : $total, Tout le réseau : $flood, Direct : $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Tout le réseau : $flood, Direct : $direct'; + return 'Tout le réseau : $flood, Direct : $direct'; } @override @@ -1903,35 +1944,35 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Paramètres du répéteur'; + String get repeater_settingsTitle => 'Paramètres du répéteur'; @override - String get repeater_basicSettings => 'Paramètres de base'; + String get repeater_basicSettings => 'Paramètres de base'; @override - String get repeater_repeaterName => 'Nom du répéteur'; + String get repeater_repeaterName => 'Nom du répéteur'; @override - String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur'; + String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur'; @override String get repeater_adminPassword => 'Mot de passe Administrateur'; @override - String get repeater_adminPasswordHelper => 'Mot de passe d\'accès complet'; + String get repeater_adminPasswordHelper => 'Mot de passe d\'accès complet'; @override - String get repeater_guestPassword => 'Mot de passe invité'; + String get repeater_guestPassword => 'Mot de passe invité'; @override String get repeater_guestPasswordHelper => - 'Accès en lecture seule avec mot de passe'; + 'Accès en lecture seule avec mot de passe'; @override - String get repeater_radioSettings => 'Paramètres Radio'; + String get repeater_radioSettings => 'Paramètres Radio'; @override - String get repeater_frequencyMhz => 'Fréquence (MHz)'; + String get repeater_frequencyMhz => 'Fréquence (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -1946,54 +1987,54 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_bandwidth => 'Bande passante'; @override - String get repeater_spreadingFactor => 'Facteur de répartition'; + String get repeater_spreadingFactor => 'Facteur de répartition'; @override String get repeater_codingRate => 'Taux de codage'; @override - String get repeater_locationSettings => 'Paramètres de localisation'; + String get repeater_locationSettings => 'Paramètres de localisation'; @override String get repeater_latitude => 'Latitude'; @override String get repeater_latitudeHelper => - 'Degrés décimaux (par exemple, 37.7749)'; + 'Degrés décimaux (par exemple, 37.7749)'; @override String get repeater_longitude => 'Longitude'; @override String get repeater_longitudeHelper => - 'Degrés décimaux (par exemple, -122,4194)'; + 'Degrés décimaux (par exemple, -122,4194)'; @override - String get repeater_features => 'Fonctionnalités'; + String get repeater_features => 'Fonctionnalités'; @override String get repeater_packetForwarding => 'Transfert de paquets'; @override String get repeater_packetForwardingSubtitle => - 'Activer le répéteur pour transmettre des paquets'; + 'Activer le répéteur pour transmettre des paquets'; @override - String get repeater_guestAccess => 'Accès Invité'; + String get repeater_guestAccess => 'Accès Invité'; @override String get repeater_guestAccessSubtitle => - 'Autoriser l\'accès invité en lecture seule'; + 'Autoriser l\'accès invité en lecture seule'; @override - String get repeater_privacyMode => 'Mode de confidentialité'; + String get repeater_privacyMode => 'Mode de confidentialité'; @override String get repeater_privacyModeSubtitle => 'Cacher le nom/l\'emplacement dans les annonces'; @override - String get repeater_advertisementSettings => 'Paramètres d\'annonces'; + String get repeater_advertisementSettings => 'Paramètres d\'annonces'; @override String get repeater_localAdvertInterval => @@ -2006,7 +2047,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervalle des annonces à tout le réseau (flood)'; + 'Intervalle des annonces à tout le réseau (flood)'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2015,52 +2056,51 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervalle d\'annonces cryptées'; + 'Intervalle d\'annonces cryptées'; @override String get repeater_dangerZone => 'Zone dangereuse'; @override - String get repeater_rebootRepeater => 'Redémarrer Répéteur'; + String get repeater_rebootRepeater => 'Redémarrer Répéteur'; @override String get repeater_rebootRepeaterSubtitle => - 'Réinitialiser l\'appareil répéteur'; + 'Réinitialiser l\'appareil répéteur'; @override String get repeater_rebootRepeaterConfirm => - 'Êtes-vous sûr de vouloir redémarrer ce répéteur ?'; + 'Êtes-vous sûr de vouloir redémarrer ce répéteur ?'; @override - String get repeater_regenerateIdentityKey => - 'Ré générer la clé d\'identité'; + String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Générer une nouvelle paire de clés publique/privée'; + 'Générer une nouvelle paire de clés publique/privée'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Cela générera une nouvelle identité pour le répéteur. Continuer ?'; + 'Cela générera une nouvelle identité pour le répéteur. Continuer ?'; @override - String get repeater_eraseFileSystem => 'Supprimer le système de fichiers'; + String get repeater_eraseFileSystem => 'Supprimer le système de fichiers'; @override String get repeater_eraseFileSystemSubtitle => - 'Formater le système de fichiers du répéteur'; + 'Formater le système de fichiers du répéteur'; @override String get repeater_eraseFileSystemConfirm => - 'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !'; + 'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !'; @override String get repeater_eraseSerialOnly => - 'Erase n\'est disponible que via la console série.'; + 'Erase n\'est disponible que via la console série.'; @override String repeater_commandSent(String command) { - return 'Commande envoyée : $command'; + return 'Commande envoyée : $command'; } @override @@ -2073,58 +2113,57 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_settingsSaved => - 'Les paramètres ont été enregistrés avec succès.'; + 'Les paramètres ont été enregistrés avec succès.'; @override String repeater_errorSavingSettings(String error) { - return 'Erreur lors de la sauvegarde des paramètres : $error'; + return 'Erreur lors de la sauvegarde des paramètres : $error'; } @override String get repeater_refreshBasicSettings => - 'Rafraîchir les paramètres de base'; + 'Rafraîchir les paramètres de base'; @override - String get repeater_refreshRadioSettings => - 'Rafraîchir les paramètres Radio'; + String get repeater_refreshRadioSettings => 'Rafraîchir les paramètres Radio'; @override - String get repeater_refreshTxPower => 'Rafraîchir la tension TX'; + String get repeater_refreshTxPower => 'Rafraîchir la tension TX'; @override String get repeater_refreshLocationSettings => - 'Rafraîchir les paramètres de localisation'; + 'Rafraîchir les paramètres de localisation'; @override String get repeater_refreshPacketForwarding => - 'Rafraîchir le routage des paquets'; + 'Rafraîchir le routage des paquets'; @override - String get repeater_refreshGuestAccess => 'Rafraîchir l\'accès invité'; + String get repeater_refreshGuestAccess => 'Rafraîchir l\'accès invité'; @override String get repeater_refreshPrivacyMode => - 'Rafraîchir le Mode Confidentialité'; + 'Rafraîchir le Mode Confidentialité'; @override String get repeater_refreshAdvertisementSettings => - 'Rafraîchir les Paramètres des annonces'; + 'Rafraîchir les Paramètres des annonces'; @override String repeater_refreshed(String label) { - return '$label rafraîchi'; + return '$label rafraîchi'; } @override String repeater_errorRefreshing(String label) { - return 'Erreur lors du rafraîchissement de $label'; + return 'Erreur lors du rafraîchissement de $label'; } @override - String get repeater_cliTitle => 'Répéteur CLI'; + String get repeater_cliTitle => 'Répéteur CLI'; @override - String get repeater_debugNextCommand => 'Déboguer Prochaine Commande'; + String get repeater_debugNextCommand => 'Déboguer Prochaine Commande'; @override String get repeater_commandHelp => 'Aide'; @@ -2134,7 +2173,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_noCommandsSent => - 'Aucune commande n\'a encore été envoyée.'; + 'Aucune commande n\'a encore été envoyée.'; @override String get repeater_typeCommandOrUseQuick => @@ -2144,7 +2183,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_enterCommandHint => 'Entrer la commande...'; @override - String get repeater_previousCommand => 'Commande précédente'; + String get repeater_previousCommand => 'Commande précédente'; @override String get repeater_nextCommand => 'Prochaine commande'; @@ -2186,7 +2225,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpReboot => - 'Redémarre l\'appareil. (Note, vous risquez d\'obtenir \'Timeout\' ce qui est normal)'; + 'Redémarre l\'appareil. (Note, vous risquez d\'obtenir \'Timeout\' ce qui est normal)'; @override String get repeater_cliHelpClock => @@ -2194,116 +2233,116 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpPassword => - 'Définit un nouveau mot de passe administrateur pour l\'appareil.'; + 'Définit un nouveau mot de passe administrateur pour l\'appareil.'; @override String get repeater_cliHelpVersion => - 'Affiche la version du périphérique et la date de construction du micrologiciel.'; + 'Affiche la version du périphérique et la date de construction du micrologiciel.'; @override String get repeater_cliHelpClearStats => - 'Réinitialise divers compteurs de statistiques à zéro.'; + 'Réinitialise divers compteurs de statistiques à zéro.'; @override - String get repeater_cliHelpSetAf => 'Définit le facteur de temps d\'air.'; + String get repeater_cliHelpSetAf => 'Définit le facteur de temps d\'air.'; @override String get repeater_cliHelpSetTx => - 'Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).'; + 'Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).'; @override String get repeater_cliHelpSetRepeat => - 'Active ou désactive le rôle du répéteur pour ce nÅ“ud.'; + 'Active ou désactive le rôle du répéteur pour ce nœud.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)'; + '(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)'; @override String get repeater_cliHelpSetFloodMax => - 'Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n\'est pas acheminé).'; + 'Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n\'est pas acheminé).'; @override String get repeater_cliHelpSetIntThresh => - 'Définit le seuil d\'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.'; + 'Définit le seuil d\'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Définit l\'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.'; + 'Définit l\'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.'; @override String get repeater_cliHelpSetMultiAcks => - 'Active ou désactive la fonctionnalité « double ACKs ».'; + 'Active ou désactive la fonctionnalité « double ACKs ».'; @override String get repeater_cliHelpSetAdvertInterval => - 'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.'; + 'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Définit l\'intervalle du minuteur en heures pour envoyer un paquet d\'annonce massive. Définir sur 0 pour désactiver.'; + 'Définit l\'intervalle du minuteur en heures pour envoyer un paquet d\'annonce massive. Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetGuestPassword => - 'Définit/met à jour le mot de passe de l\'invité. (pour les répéteurs, les connexions d\'invités peuvent envoyer la requête \"Get Stats\")'; + 'Définit/met à jour le mot de passe de l\'invité. (pour les répéteurs, les connexions d\'invités peuvent envoyer la requête \"Get Stats\")'; @override - String get repeater_cliHelpSetName => 'Définit le nom de l\'annonce.'; + String get repeater_cliHelpSetName => 'Définit le nom de l\'annonce.'; @override String get repeater_cliHelpSetLat => - 'Définit la latitude de la carte des annonces. (degrés décimaux)'; + 'Définit la latitude de la carte des annonces. (degrés décimaux)'; @override String get repeater_cliHelpSetLon => - 'Définit la longitude de la carte de l\'annonce. (degrés décimaux)'; + 'Définit la longitude de la carte de l\'annonce. (degrés décimaux)'; @override String get repeater_cliHelpSetRadio => - 'Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.'; + 'Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.'; @override String get repeater_cliHelpSetRxDelay => - 'Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.'; + 'Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetTxDelay => - 'Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).'; + 'Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.'; + 'Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Activer/Désactiver le pont.'; + String get repeater_cliHelpSetBridgeEnabled => 'Activer/Désactiver le pont.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Définir le délai avant de renvoyer les paquets.'; + 'Définir le délai avant de renvoyer les paquets.'; @override String get repeater_cliHelpSetBridgeSource => - 'Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.'; + 'Choisissez si le pont retransmettra les paquets reçus ou les paquets transmis.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Définir la vitesse de communication série pour les ponts Rs232.'; + 'Définir la vitesse de communication série pour les ponts Rs232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Définir le secret du pont pour les ponts espnow.'; + 'Définir le secret du pont pour les ponts espnow.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).'; + 'Définit un facteur personnalisé pour ajuster la tension de la batterie signalée (uniquement pris en charge sur certains cartes).'; @override String get repeater_cliHelpTempRadio => - 'Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d\'origine. (ne sauvegarde pas dans les préférences).'; + 'Définit des paramètres radio temporaires pour le nombre de minutes donné, puis revient aux paramètres radio d\'origine. (ne sauvegarde pas dans les préférences).'; @override String get repeater_cliHelpSetPerm => - 'Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).'; + 'Modifie l’ACL. Supprime l’entrée correspondante (par préfixe de clé publique) si \"permissions\" est égal à zéro. Ajoute une nouvelle entrée si la clé publique hexadécimale a une longueur complète et n’est pas actuellement dans l’ACL. Met à jour l’entrée en fonction du préfixe de clé publique. Les bits de permission varient en fonction du rôle du firmware, mais les 2 bits inférieurs sont : 0 (Invité), 1 (Lecture seule), 2 (Lecture/écriture), 3 (Administrateur).'; @override String get repeater_cliHelpGetBridgeType => @@ -2311,98 +2350,98 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpLogStart => - 'Démarre l\'enregistrement des paquets dans le système de fichiers.'; + 'Démarre l\'enregistrement des paquets dans le système de fichiers.'; @override String get repeater_cliHelpLogStop => - 'Arrêter de journaliser les paquets vers le système de fichiers.'; + 'Arrêter de journaliser les paquets vers le système de fichiers.'; @override String get repeater_cliHelpLogErase => - 'Supprime les journaux de paquets du système de fichiers.'; + 'Supprime les journaux de paquets du système de fichiers.'; @override String get repeater_cliHelpNeighbors => - 'Affiche une liste d\'autres nÅ“uds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; + 'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; @override String get repeater_cliHelpNeighborRemove => - 'Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.'; + 'Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.'; @override String get repeater_cliHelpRegion => - '(série uniquement) Liste toutes les régions définies et les autorisations actuelles d\'annonces sur tout le réseau (flood).'; + '(série uniquement) Liste toutes les régions définies et les autorisations actuelles d\'annonces sur tout le réseau (flood).'; @override String get repeater_cliHelpRegionLoad => - 'REMARQUE : il s\'agit d\'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d\'un espace). Terminé par l\'envoi d\'une ligne vide/commande.'; + 'REMARQUE : il s\'agit d\'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d\'un espace). Terminé par l\'envoi d\'une ligne vide/commande.'; @override String get repeater_cliHelpRegionGet => - 'Recherche la région avec le préfixe de nom donné (ou \"\" pour l\'étendue globale). Répond avec \"-> nom-de-région (nom-parent) \'F\'\"'; + 'Recherche la région avec le préfixe de nom donné (ou \"\" pour l\'étendue globale). Répond avec \"-> nom-de-région (nom-parent) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Ajoute ou met à jour une définition de région avec le nom donné.'; + 'Ajoute ou met à jour une définition de région avec le nom donné.'; @override String get repeater_cliHelpRegionRemove => - 'Supprime une définition de région avec le nom donné.'; + 'Supprime une définition de région avec le nom donné.'; @override String get repeater_cliHelpRegionAllowf => - 'Définit les autorisations de \"Flot\" pour la région donnée. (\'\' pour la portée globale/héritée)'; + 'Définit les autorisations de \"Flot\" pour la région donnée. (\'\' pour la portée globale/héritée)'; @override String get repeater_cliHelpRegionDenyf => - 'Supprime l\'autorisation \'F\'lood\' pour la région donnée. (NOTE : à ce stade, il n\'est pas conseillé de l\'utiliser sur l\'étendue globale/héritée !! )'; + 'Supprime l\'autorisation \'F\'lood\' pour la région donnée. (NOTE : à ce stade, il n\'est pas conseillé de l\'utiliser sur l\'étendue globale/héritée !! )'; @override String get repeater_cliHelpRegionHome => - 'Répond avec la région \'maison\' actuelle. (Note appliquée nulle part pour l\'instant, réservée à une utilisation future)'; + 'Répond avec la région \'maison\' actuelle. (Note appliquée nulle part pour l\'instant, réservée à une utilisation future)'; @override - String get repeater_cliHelpRegionHomeSet => 'Définit la région \'maison\'.'; + String get repeater_cliHelpRegionHomeSet => 'Définit la région \'maison\'.'; @override String get repeater_cliHelpRegionSave => - 'Conserve la liste/la carte des régions dans le stockage.'; + 'Conserve la liste/la carte des régions dans le stockage.'; @override String get repeater_cliHelpGps => - 'Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.'; + 'Affiche l’état du GPS. Lorsque le GPS est éteint, il répond uniquement « éteint », si allumé, il répond avec « allumé », l’état, la correction, le nombre de satellites.'; @override - String get repeater_cliHelpGpsOnOff => 'Activer/désactiver le GPS.'; + String get repeater_cliHelpGpsOnOff => 'Activer/désactiver le GPS.'; @override String get repeater_cliHelpGpsSync => - 'Synchronise l\'heure du nÅ“ud avec l\'horloge GPS.'; + 'Synchronise l\'heure du nœud avec l\'horloge GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Définit la position du nÅ“ud aux coordonnées GPS et enregistre les préférences.'; + 'Définit la position du nœud aux coordonnées GPS et enregistre les préférences.'; @override String get repeater_cliHelpGpsAdvert => - 'Donne la configuration de l\'annonce de la localisation du nÅ“ud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences'; + 'Donne la configuration de l\'annonce de la localisation du nœud :\n- none : ne pas inclure la localisation dans les annonces\n- share : partager la localisation GPS (du SensorManager)\n- prefs : annoncer la localisation stockée dans les préférences'; @override String get repeater_cliHelpGpsAdvertSet => - 'Définit la configuration de l\'annonce de localisation.'; + 'Définit la configuration de l\'annonce de localisation.'; @override String get repeater_commandsListTitle => 'Liste des commandes'; @override String get repeater_commandsListNote => - 'NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...'; + 'NOTE : pour les diverses commandes « set »..., il existe également une commande « get »...'; @override - String get repeater_general => 'Général'; + String get repeater_general => 'Général'; @override - String get repeater_settingsCategory => 'Paramètres'; + String get repeater_settingsCategory => 'Paramètres'; @override String get repeater_bridge => 'Pont'; @@ -2411,37 +2450,36 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_logging => 'Journalisation'; @override - String get repeater_neighborsRepeaterOnly => - 'Voisins (Uniquement répéteur)'; + String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)'; @override String get repeater_regionManagementRepeaterOnly => - 'Gestion des régions (uniquement pour le répéteur)'; + 'Gestion des régions (uniquement pour le répéteur)'; @override String get repeater_regionNote => - 'Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.'; + 'Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.'; @override String get repeater_gpsManagement => 'Gestion GPS'; @override String get repeater_gpsNote => - 'La commande GPS a été introduite pour gérer les sujets liés à la localisation.'; + 'La commande GPS a été introduite pour gérer les sujets liés à la localisation.'; @override - String get telemetry_receivedData => 'Données de télémétrie reçues'; + String get telemetry_receivedData => 'Données de télémétrie reçues'; @override - String get telemetry_requestTimeout => 'Demande de télémétrie expirée.'; + String get telemetry_requestTimeout => 'Demande de télémétrie expirée.'; @override String telemetry_errorLoading(String error) { - return 'Erreur lors du chargement de la télémétrie : $error'; + return 'Erreur lors du chargement de la télémétrie : $error'; } @override - String get telemetry_noData => 'Aucune donnée de télémétrie disponible.'; + String get telemetry_noData => 'Aucune donnée de télémétrie disponible.'; @override String telemetry_channelTitle(int channel) { @@ -2455,10 +2493,10 @@ class AppLocalizationsFr extends AppLocalizations { String get telemetry_voltageLabel => 'Tension'; @override - String get telemetry_mcuTemperatureLabel => 'Température du MCU'; + String get telemetry_mcuTemperatureLabel => 'Température du MCU'; @override - String get telemetry_temperatureLabel => 'Température'; + String get telemetry_temperatureLabel => 'Température'; @override String get telemetry_currentLabel => 'Actuellement'; @@ -2480,14 +2518,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Données des voisins reçues'; + String get neighbors_receivedData => 'Données des voisins reçues'; @override - String get neighbors_requestTimedOut => 'Les voisins demandent un délai.'; + String get neighbors_requestTimedOut => 'Les voisins demandent un délai.'; @override String neighbors_errorLoading(String error) { @@ -2495,20 +2533,20 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get neighbors_repeatersNeighbors => 'Répéteurs Voisins'; + String get neighbors_repeatersNeighbors => 'Répéteurs Voisins'; @override String get neighbors_noData => - 'Aucune donnée concernant les voisins disponible.'; + 'Aucune donnée concernant les voisins disponible.'; @override String neighbors_unknownContact(String pubkey) { - return 'Clé publique inconnue $pubkey'; + return 'Clé publique inconnue $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Écouté : $time auparavant'; + return 'Écouté : $time auparavant'; } @override @@ -2518,26 +2556,26 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_viewMap => 'Afficher la carte'; @override - String get channelPath_otherObservedPaths => 'Autres chemins observés'; + String get channelPath_otherObservedPaths => 'Autres chemins observés'; @override - String get channelPath_repeaterHops => 'Sauts du répéteur'; + String get channelPath_repeaterHops => 'Sauts du répéteur'; @override String get channelPath_noHopDetails => - 'Les détails de l\'envoi ne sont pas fournis pour ce paquet.'; + 'Les détails de l\'envoi ne sont pas fournis pour ce paquet.'; @override - String get channelPath_messageDetails => 'Détails du message'; + String get channelPath_messageDetails => 'Détails du message'; @override - String get channelPath_senderLabel => 'Expéditeur'; + String get channelPath_senderLabel => 'Expéditeur'; @override String get channelPath_timeLabel => 'Temps'; @override - String get channelPath_repeatsLabel => 'Répétitions'; + String get channelPath_repeatsLabel => 'Répétitions'; @override String channelPath_pathLabel(int index) { @@ -2545,15 +2583,15 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get channelPath_observedLabel => 'Observé'; + String get channelPath_observedLabel => 'Observé'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Chemin observé $index • $hops'; + return 'Chemin observé $index • $hops'; } @override - String get channelPath_noLocationData => 'Aucune donnée de localisation'; + String get channelPath_noLocationData => 'Aucune donnée de localisation'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2569,7 +2607,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_unknownPath => 'Inconnu'; @override - String get channelPath_floodPath => 'Tout le réseau'; + String get channelPath_floodPath => 'Tout le réseau'; @override String get channelPath_directPath => 'Afficher'; @@ -2589,7 +2627,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Aucune position de répéteur disponible pour ce chemin.'; + 'Aucune position de répéteur disponible pour ce chemin.'; @override String channelPath_primaryPath(int index) { @@ -2600,43 +2638,43 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_pathLabelTitle => 'Chemin'; @override - String get channelPath_observedPathHeader => 'Chemin observé'; + String get channelPath_observedPathHeader => 'Chemin observé'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Aucun détail de saut disponible pour ce paquet.'; + 'Aucun détail de saut disponible pour ce paquet.'; @override - String get channelPath_unknownRepeater => 'Répéteur Inconnu'; + String get channelPath_unknownRepeater => 'Répéteur Inconnu'; @override - String get community_title => 'Communauté'; + String get community_title => 'Communauté'; @override - String get community_create => 'Créer une Communauté'; + String get community_create => 'Créer une Communauté'; @override String get community_createDesc => - 'Créer une nouvelle communauté et la partager via QR code.'; + 'Créer une nouvelle communauté et la partager via QR code.'; @override String get community_join => 'Rejoindre'; @override - String get community_joinTitle => 'Rejoindre la communauté'; + String get community_joinTitle => 'Rejoindre la communauté'; @override String community_joinConfirmation(String name) { - return 'Souhaitez-vous rejoindre la communauté \"$name\" ?'; + return 'Souhaitez-vous rejoindre la communauté \"$name\" ?'; } @override - String get community_scanQr => 'Scanner la communauté QR'; + String get community_scanQr => 'Scanner la communauté QR'; @override String get community_scanInstructions => @@ -2646,29 +2684,29 @@ class AppLocalizationsFr extends AppLocalizations { String get community_showQr => 'Afficher le QR Code'; @override - String get community_publicChannel => 'Communauté Publique'; + String get community_publicChannel => 'Communauté Publique'; @override - String get community_hashtagChannel => 'Hashtag Communauté'; + String get community_hashtagChannel => 'Hashtag Communauté'; @override - String get community_name => 'Nom de la communauté'; + String get community_name => 'Nom de la communauté'; @override - String get community_enterName => 'Entrez le nom de la communauté'; + String get community_enterName => 'Entrez le nom de la communauté'; @override String community_created(String name) { - return 'Communauté \"$name\" créée'; + return 'Communauté \"$name\" créée'; } @override String community_joined(String name) { - return 'Rejoint la communauté \"$name\"'; + return 'Rejoint la communauté \"$name\"'; } @override - String get community_qrTitle => 'Partager Communauté'; + String get community_qrTitle => 'Partager Communauté'; @override String community_qrInstructions(String name) { @@ -2677,40 +2715,40 @@ class AppLocalizationsFr extends AppLocalizations { @override String get community_hashtagPrivacyHint => - 'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté'; + 'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté'; @override - String get community_invalidQrCode => 'Code QR de communauté non valide'; + String get community_invalidQrCode => 'Code QR de communauté non valide'; @override - String get community_alreadyMember => 'Déjà membre'; + String get community_alreadyMember => 'Déjà membre'; @override String community_alreadyMemberMessage(String name) { - return 'Vous êtes déjà membre de \"$name\".'; + return 'Vous êtes déjà membre de \"$name\".'; } @override String get community_addPublicChannel => - 'Ajouter un Canal Public de la Communauté'; + 'Ajouter un Canal Public de la Communauté'; @override String get community_addPublicChannelHint => - 'Ajouter automatiquement le canal public pour cette communauté'; + 'Ajouter automatiquement le canal public pour cette communauté'; @override String get community_noCommunities => - 'Aucun groupe n\'a été rejoint pour le moment.'; + 'Aucun groupe n\'a été rejoint pour le moment.'; @override String get community_scanOrCreate => - 'Scanner un code QR ou créer une communauté pour commencer'; + 'Scanner un code QR ou créer une communauté pour commencer'; @override - String get community_manageCommunities => 'Gérer les Communautés'; + String get community_manageCommunities => 'Gérer les Communautés'; @override - String get community_delete => 'Quitter la communauté'; + String get community_delete => 'Quitter la communauté'; @override String community_deleteConfirm(String name) { @@ -2719,66 +2757,66 @@ class AppLocalizationsFr extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Cela supprimera également $count canal/canaux et leurs messages.'; + return 'Cela supprimera également $count canal/canaux et leurs messages.'; } @override String community_deleted(String name) { - return 'Communauté \"$name\" quittée'; + return 'Communauté \"$name\" quittée'; } @override - String get community_regenerateSecret => 'Régénérer le secret'; + String get community_regenerateSecret => 'Régénérer le secret'; @override String community_regenerateSecretConfirm(String name) { - return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.'; + return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.'; } @override - String get community_regenerate => 'Régénérer'; + String get community_regenerate => 'Régénérer'; @override String community_secretRegenerated(String name) { - return 'Mot de passe secret régénéré pour \"$name\"'; + return 'Mot de passe secret régénéré pour \"$name\"'; } @override - String get community_updateSecret => 'Mettre à jour le secret'; + String get community_updateSecret => 'Mettre à jour le secret'; @override String community_secretUpdated(String name) { - return 'Modification secrète mise à jour pour \"$name\"'; + return 'Modification secrète mise à jour pour \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"'; + return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"'; } @override - String get community_addHashtagChannel => 'Ajouter un Hashtag Communauté'; + String get community_addHashtagChannel => 'Ajouter un Hashtag Communauté'; @override String get community_addHashtagChannelDesc => - 'Ajouter un canal hashtag pour cette communauté'; + 'Ajouter un canal hashtag pour cette communauté'; @override - String get community_selectCommunity => 'Sélectionner Communauté'; + String get community_selectCommunity => 'Sélectionner Communauté'; @override - String get community_regularHashtag => 'Hashtag régulier'; + String get community_regularHashtag => 'Hashtag régulier'; @override String get community_regularHashtagDesc => 'Hashtag public (tout le monde peut rejoindre)'; @override - String get community_communityHashtag => 'Hashtag de la communauté'; + String get community_communityHashtag => 'Hashtag de la communauté'; @override String get community_communityHashtagDesc => - 'Exclusif aux membres de la communauté'; + 'Exclusif aux membres de la communauté'; @override String community_forCommunity(String name) { @@ -2795,10 +2833,10 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_latestMessages => 'Derniers messages'; @override - String get listFilter_heardRecently => 'Écoute récemment'; + String get listFilter_heardRecently => 'Écoute récemment'; @override - String get listFilter_az => 'A à Z'; + String get listFilter_az => 'A à Z'; @override String get listFilter_filters => 'Filtres'; @@ -2807,10 +2845,10 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_all => 'Tout'; @override - String get listFilter_favorites => 'Préférences'; + String get listFilter_favorites => 'Préférences'; @override - String get listFilter_addToFavorites => 'Ajouter à mes favoris'; + String get listFilter_addToFavorites => 'Ajouter à mes favoris'; @override String get listFilter_removeFromFavorites => 'Supprimer des favoris'; @@ -2819,7 +2857,7 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_users => 'Utilisateurs'; @override - String get listFilter_repeaters => 'Répéteurs'; + String get listFilter_repeaters => 'Répéteurs'; @override String get listFilter_roomServers => 'Room servers'; @@ -2834,10 +2872,10 @@ class AppLocalizationsFr extends AppLocalizations { String get pathTrace_you => 'Vous'; @override - String get pathTrace_failed => 'Traçage du chemin échoué.'; + String get pathTrace_failed => 'Traçage du chemin échoué.'; @override - String get pathTrace_notAvailable => 'Tracé de chemin non disponible.'; + String get pathTrace_notAvailable => 'Tracé de chemin non disponible.'; @override String get pathTrace_refreshTooltip => 'Actualiser Path Trace'; @@ -2851,11 +2889,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losSelectStartEnd => - 'Sélectionnez les nÅ“uds de début et de fin pour LOS.'; + 'Sélectionnez les nœuds de début et de fin pour LOS.'; @override String losRunFailed(String error) { - return 'Échec de la vérification de la ligne de vue : $error'; + return 'Échec de la vérification de la ligne de vue : $error'; } @override @@ -2863,24 +2901,24 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losRunToViewElevationProfile => - 'Exécutez LOS pour afficher le profil d\'altitude'; + 'Exécutez LOS pour afficher le profil d\'altitude'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Appuyez sur les nÅ“uds ou appuyez longuement sur la carte pour des points personnalisés'; + 'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés'; @override - String get losShowDisplayNodes => 'Afficher les nÅ“uds d\'affichage'; + String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage'; @override - String get losCustomPoints => 'Points personnalisés'; + String get losCustomPoints => 'Points personnalisés'; @override String losCustomPointLabel(int index) { - return 'Personnalisé $index'; + return 'Personnalisé $index'; } @override @@ -2891,19 +2929,19 @@ class AppLocalizationsFr extends AppLocalizations { @override String losAntennaA(String value, String unit) { - return 'Antenne A : $value $unit'; + return 'Antenne A : $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Antenne B : $value $unit'; + return 'Antenne B : $value $unit'; } @override - String get losRun => 'Exécuter la LOS'; + String get losRun => 'Exécuter la LOS'; @override - String get losNoElevationData => 'Aucune donnée d\'altitude'; + String get losNoElevationData => 'Aucune donnée d\'altitude'; @override String losProfileClear( @@ -2922,30 +2960,30 @@ class AppLocalizationsFr extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, bloqué par $obstruction $heightUnit'; + return '$distance $distanceUnit, bloqué par $obstruction $heightUnit'; } @override - String get losStatusChecking => 'LOS : vérification...'; + String get losStatusChecking => 'LOS : vérification...'; @override - String get losStatusNoData => 'LOS : aucune donnée'; + String get losStatusNoData => 'LOS : aucune donnée'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu'; + return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu'; } @override String get losErrorElevationUnavailable => - 'Données d\'altitude indisponibles pour un ou plusieurs échantillons.'; + 'Données d\'altitude indisponibles pour un ou plusieurs échantillons.'; @override String get losErrorInvalidInput => - 'Données de points/d\'altitude non valides pour le calcul de la LOS.'; + 'Données de points/d\'altitude non valides pour le calcul de la LOS.'; @override - String get losRenameCustomPoint => 'Renommer le point personnalisé'; + String get losRenameCustomPoint => 'Renommer le point personnalisé'; @override String get losPointName => 'Nom du point'; @@ -2958,25 +2996,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losElevationAttribution => - 'Données d’altitude : Open-Meteo (CC BY 4.0)'; + 'Données d’altitude : Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Horizon radio'; @override - String get losLegendLosBeam => 'Ligne de visée'; + String get losLegendLosBeam => 'Ligne de visée'; @override String get losLegendTerrain => 'Terrain'; @override - String get losFrequencyLabel => 'Fréquence'; + String get losFrequencyLabel => 'Fréquence'; @override - String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; + String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; @override - String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; + String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; @override String losFrequencyDialogDescription( @@ -2985,25 +3023,24 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; + return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; } @override - String get contacts_pathTrace => 'Traçage de chemin'; + String get contacts_pathTrace => 'Traçage de chemin'; @override String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => - 'Tracer le chemin vers le répéteur'; + String get contacts_repeaterPathTrace => 'Tracer le chemin vers le répéteur'; @override - String get contacts_repeaterPing => 'Pinguer le répéteur'; + String get contacts_repeaterPing => 'Pinguer le répéteur'; @override String get contacts_roomPathTrace => - 'Traçage du chemin vers le serveur de la salle'; + 'Traçage du chemin vers le serveur de la salle'; @override String get contacts_roomPing => 'Pinguer le serveur de la salle'; @@ -3013,27 +3050,27 @@ class AppLocalizationsFr extends AppLocalizations { @override String contacts_pathTraceTo(String name) { - return 'Tracer l\'itinéraire vers $name'; + return 'Tracer l\'itinéraire vers $name'; } @override String get contacts_clipboardEmpty => 'Le presse-papiers est vide.'; @override - String get contacts_invalidAdvertFormat => 'Données de contact non valides'; + String get contacts_invalidAdvertFormat => 'Données de contact non valides'; @override - String get contacts_contactImported => 'Le contact a été importé.'; + String get contacts_contactImported => 'Le contact a été importé.'; @override String get contacts_contactImportFailed => - 'Échec de l\'importation du contact.'; + 'Échec de l\'importation du contact.'; @override String get contacts_zeroHopAdvert => 'Annonce Zero saut'; @override - String get contacts_floodAdvert => 'Annonce à tout le réseau'; + String get contacts_floodAdvert => 'Annonce à tout le réseau'; @override String get contacts_copyAdvertToClipboard => @@ -3056,18 +3093,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_zeroHopContactAdvertFailed => - 'Échec de l\'envoi du contact.'; + 'Échec de l\'envoi du contact.'; @override String get contacts_contactAdvertCopied => - 'Annonce copiée dans le presse-papiers.'; + 'Annonce copiée dans le presse-papiers.'; @override String get contacts_contactAdvertCopyFailed => - 'La copie de l\'annonce vers le presse-papiers a échoué.'; + 'La copie de l\'annonce vers le presse-papiers a échoué.'; @override - String get notification_activityTitle => 'Activité MeshCore'; + String get notification_activityTitle => 'Activité MeshCore'; @override String notification_messagesCount(int count) { @@ -3096,27 +3133,27 @@ class AppLocalizationsFr extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'nouveaux nÅ“uds', - one: 'nouveau nÅ“ud', + other: 'nouveaux nœuds', + one: 'nouveau nœud', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Nouveau $contactType découvert'; + return 'Nouveau $contactType découvert'; } @override - String get notification_receivedNewMessage => 'Nouveau message reçu'; + String get notification_receivedNewMessage => 'Nouveau message reçu'; @override String get settings_gpxExportRepeaters => - 'Exporter les répéteurs / serveur de salle au format GPX'; + 'Exporter les répéteurs / serveur de salle au format GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.'; + 'Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.'; @override String get settings_gpxExportContacts => @@ -3135,14 +3172,14 @@ class AppLocalizationsFr extends AppLocalizations { 'Exporte tous les contacts avec une localisation vers un fichier GPX.'; @override - String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.'; + String get settings_gpxExportSuccess => 'Fichier GPX exporté avec succès.'; @override - String get settings_gpxExportNoContacts => 'Aucun contact à exporter.'; + String get settings_gpxExportNoContacts => 'Aucun contact à exporter.'; @override String get settings_gpxExportNotAvailable => - 'Non pris en charge sur votre appareil/Système d\'exploitation'; + 'Non pris en charge sur votre appareil/Système d\'exploitation'; @override String get settings_gpxExportError => @@ -3150,7 +3187,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_gpxExportRepeatersRoom => - 'Emplacements des serveurs de répéteur et de salle'; + 'Emplacements des serveurs de répéteur et de salle'; @override String get settings_gpxExportChat => 'Emplacements des compagnons'; @@ -3161,15 +3198,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_gpxExportShareText => - 'Données de carte exportées à partir de meshcore-open'; + 'Données de carte exportées à partir de meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open exporter les données de carte GPX'; + 'meshcore-open exporter les données de carte GPX'; @override - String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité'; + String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité'; @override - String get snrIndicator_lastSeen => 'Dernière fois vu'; + String get snrIndicator_lastSeen => 'Dernière fois vu'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 128f6e4..d2a832e 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -93,7 +93,7 @@ class AppLocalizationsIt extends AppLocalizations { String get common_loading => 'Caricamento...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -126,12 +126,56 @@ class AppLocalizationsIt extends AppLocalizations { @override String get usbScreenNote => - 'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.'; + 'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.'; @override String get usbScreenEmptyState => 'Nessun dispositivo USB rilevato. Collegare uno e riavviare.'; + @override + String get usbErrorPermissionDenied => + 'È stato negato l\'accesso tramite USB.'; + + @override + String get usbErrorDeviceMissing => + 'Il dispositivo USB selezionato non è più disponibile.'; + + @override + String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.'; + + @override + String get usbErrorBusy => + 'Un\'altra richiesta di connessione tramite USB è già in corso.'; + + @override + String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.'; + + @override + String get usbErrorOpenFailed => + 'Impossibile aprire il dispositivo USB selezionato.'; + + @override + String get usbErrorConnectFailed => + 'Impossibile connettersi al dispositivo USB selezionato.'; + + @override + String get usbErrorUnsupported => + 'La comunicazione seriale tramite USB non è supportata su questa piattaforma.'; + + @override + String get usbErrorAlreadyActive => 'La connessione USB è già attiva.'; + + @override + String get usbErrorNoDeviceSelected => + 'Non è stato selezionato alcun dispositivo USB.'; + + @override + String get usbErrorPortClosed => 'La connessione USB non è attiva.'; + + @override + String get usbErrorConnectTimedOut => + 'Attesa superata, in attesa di una risposta dal dispositivo.'; + @override String get scanner_scanning => 'Scansione in corso per i dispositivi...'; @@ -168,7 +212,7 @@ class AppLocalizationsIt extends AppLocalizations { String get scanner_scan => 'Scansiona'; @override - String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.'; + String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.'; @override String get scanner_bluetoothOffMessage => @@ -265,7 +309,7 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_longitude => 'Longitudine'; @override - String get settings_privacyMode => 'Modalità Privacy'; + String get settings_privacyMode => 'Modalità Privacy'; @override String get settings_privacyModeSubtitle => @@ -273,13 +317,13 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_privacyModeToggle => - 'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.'; + 'Attiva la modalità privacy per nascondere il tuo nome e la tua posizione negli annunci.'; @override - String get settings_privacyModeEnabled => 'Modalità privacy abilitata'; + String get settings_privacyModeEnabled => 'Modalità privacy abilitata'; @override - String get settings_privacyModeDisabled => 'Modalità privacy disabilitata'; + String get settings_privacyModeDisabled => 'Modalità privacy disabilitata'; @override String get settings_actions => 'Azioni'; @@ -417,7 +461,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_clientRepeatFreqWarning => - 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; + 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; @override String settings_error(String message) { @@ -452,10 +496,10 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -464,16 +508,16 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -482,10 +526,10 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russo'; @@ -568,7 +612,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_autoRouteRotationSubtitle => - 'Alterna tra i percorsi migliori e la modalità alluvione'; + 'Alterna tra i percorsi migliori e la modalità alluvione'; @override String get appSettings_autoRouteRotationEnabled => @@ -663,7 +707,7 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_offlineMapCache => 'Cache Mappa Offline'; @override - String get appSettings_unitsTitle => 'Unità'; + String get appSettings_unitsTitle => 'Unità'; @override String get appSettings_unitsMetric => 'Metrico (m/km)'; @@ -782,12 +826,11 @@ class AppLocalizationsIt extends AppLocalizations { String get contacts_groupName => 'Nome gruppo'; @override - String get contacts_groupNameRequired => - 'Il nome del gruppo è obbligatorio.'; + String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.'; @override String contacts_groupAlreadyExists(String name) { - return 'Il gruppo \"$name\" esiste già.'; + return 'Il gruppo \"$name\" esiste già.'; } @override @@ -873,7 +916,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return 'Eliminare \"$name\"? Non può essere annullato.'; + return 'Eliminare \"$name\"? Non può essere annullato.'; } @override @@ -970,20 +1013,20 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channels_joinPublicChannelDesc => - 'Chiunque può unirsi a questo canale.'; + 'Chiunque può unirsi a questo canale.'; @override String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag'; @override String get channels_joinHashtagChannelDesc => - 'Chiunque può unirsi ai canali hashtag.'; + 'Chiunque può unirsi ai canali hashtag.'; @override String get channels_scanQrCode => 'Scansiona un codice QR'; @override - String get channels_scanQrCodeComingSoon => 'Arriverà presto'; + String get channels_scanQrCodeComingSoon => 'Arriverà presto'; @override String get channels_enterHashtag => 'Inserisci hashtag'; @@ -1117,7 +1160,7 @@ class AppLocalizationsIt extends AppLocalizations { String get debugLog_rawLogRx => 'Log Raw-RX'; @override - String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.'; + String get debugLog_noBleActivity => 'Nessuna attività BLE rilevata ancora.'; @override String debugFrame_length(int count) { @@ -1173,20 +1216,20 @@ class AppLocalizationsIt extends AppLocalizations { String get chat_ShowAllPaths => 'Mostra tutti i percorsi'; @override - String get chat_routingMode => 'Modalità di routing'; + String get chat_routingMode => 'Modalità di routing'; @override String get chat_autoUseSavedPath => 'Utilizza il percorso salvato'; @override - String get chat_forceFloodMode => 'Modalità Inondamento Forzato'; + String get chat_forceFloodMode => 'Modalità Inondamento Forzato'; @override String get chat_recentAckPaths => 'Percorsi ACK Recenti (tocca per usare):'; @override String get chat_pathHistoryFull => - 'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.'; + 'La cronologia del percorso è piena. Rimuovi gli elementi per aggiungere nuovi.'; @override String get chat_hopSingular => 'salta'; @@ -1213,7 +1256,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.'; + 'Non c\'è ancora una cronologia del percorso.\nInvia un messaggio per scoprire i percorsi.'; @override String get chat_pathActions => 'Azioni Percorso:'; @@ -1234,7 +1277,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_pathCleared => - 'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; + 'Percorso sgomberato. Il prossimo messaggio riidentifierà il percorso.'; @override String get chat_floodModeSubtitle => @@ -1242,7 +1285,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.'; + 'Modalità alluvione abilitata. Disattivala tramite l\'icona di routing nella barra in alto.'; @override String get chat_fullPath => 'Percorso Completo'; @@ -1417,7 +1460,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.'; + return 'Stai per condividere una posizione in $channelLabel. Questo canale è pubblico e chiunque abbia la PSK può vederlo.'; } @override @@ -1635,7 +1678,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'La password verrà memorizzata in modo sicuro su questo dispositivo.'; + 'La password verrà memorizzata in modo sicuro su questo dispositivo.'; @override String get login_repeaterDescription => @@ -1649,13 +1692,13 @@ class AppLocalizationsIt extends AppLocalizations { String get login_routing => 'Instradamento'; @override - String get login_routingMode => 'Modalità di routing'; + String get login_routingMode => 'Modalità di routing'; @override String get login_autoUseSavedPath => 'Utilizza il percorso salvato'; @override - String get login_forceFloodMode => 'Modalità Inondamento Forzato'; + String get login_forceFloodMode => 'Modalità Inondamento Forzato'; @override String get login_managePaths => 'Gestisci Percorsi'; @@ -1675,7 +1718,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get login_failedMessage => - 'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.'; + 'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.'; @override String get common_reload => 'Ricaricare'; @@ -1718,7 +1761,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get path_helperMaxHops => - 'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)'; + 'Massimo 64 salti. Ogni prefisso è composto da 2 caratteri esadecimali (1 byte)'; @override String get path_selectFromContacts => 'Seleziona da contatti:'; @@ -1738,7 +1781,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get path_tooLong => - 'Il percorso è troppo lungo. Massimo 64 salti consentiti.'; + 'Il percorso è troppo lungo. Massimo 64 salti consentiti.'; @override String get path_setPath => 'Imposta Percorso'; @@ -1790,13 +1833,13 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_statusTitle => 'Stato del Ripetitore'; @override - String get repeater_routingMode => 'Modalità di routing'; + String get repeater_routingMode => 'Modalità di routing'; @override String get repeater_autoUseSavedPath => 'Percorso salvato automatico'; @override - String get repeater_forceFloodMode => 'Modalità Inondamento Forzato'; + String get repeater_forceFloodMode => 'Modalità Inondamento Forzato'; @override String get repeater_pathManagement => 'Gestione dei percorsi'; @@ -1822,7 +1865,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_clockAtLogin => 'Orologio (all\'accesso)'; @override - String get repeater_uptime => 'Disponibilità'; + String get repeater_uptime => 'Disponibilità'; @override String get repeater_queueLength => 'Lunghezza della coda'; @@ -1974,7 +2017,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Consenti l\'accesso ospite in sola lettura'; @override - String get repeater_privacyMode => 'Modalità Privacy'; + String get repeater_privacyMode => 'Modalità Privacy'; @override String get repeater_privacyModeSubtitle => @@ -1984,7 +2027,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_advertisementSettings => 'Impostazioni Annuncio'; @override - String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale'; + String get repeater_localAdvertInterval => 'Intervallo Pubblicità Locale'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -1993,7 +2036,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervallo Pubblicità Inondazione'; + 'Intervallo Pubblicità Inondazione'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2019,7 +2062,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Sei sicuro di voler riavviare questo ripetitore?'; @override - String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità'; + String get repeater_regenerateIdentityKey => 'Rigenera Chiave Identità'; @override String get repeater_regenerateIdentityKeySubtitle => @@ -2027,7 +2070,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'Questo genererà una nuova identità per il ripetitore. Procedere?'; + 'Questo genererà una nuova identità per il ripetitore. Procedere?'; @override String get repeater_eraseFileSystem => 'Elimina File System'; @@ -2038,11 +2081,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!'; + 'ATTENZIONE: Ciò cancellerà tutti i dati sul ripetitore. Non può essere annullato!'; @override String get repeater_eraseSerialOnly => - 'Elimina è disponibile solo tramite console seriale.'; + 'Elimina è disponibile solo tramite console seriale.'; @override String repeater_commandSent(String command) { @@ -2086,7 +2129,7 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_refreshGuestAccess => 'Aggiorna Accesso Ospite'; @override - String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy'; + String get repeater_refreshPrivacyMode => 'Aggiorna Modalità Privacy'; @override String get repeater_refreshAdvertisementSettings => @@ -2167,7 +2210,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpReboot => - 'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)'; + 'Riavvia il dispositivo. (nota, potresti ottenere \'Timeout\' che è normale)'; @override String get repeater_cliHelpClock => @@ -2199,7 +2242,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetAllowReadOnly => - '(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).'; + '(Server della stanza) Se \'on\', allora l\'accesso con una password vuota sarà consentito, ma non sarà possibile pubblicare nella stanza. (solo lettura).'; @override String get repeater_cliHelpSetFloodMax => @@ -2207,7 +2250,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetIntThresh => - 'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.'; + 'Imposta il Limite di Interferenza (in dB). Il valore predefinito è 14. Imposta su 0 per disabilitare il rilevamento delle interferenze del canale.'; @override String get repeater_cliHelpSetAgcResetInterval => @@ -2219,7 +2262,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.'; + 'Imposta l\'intervallo del timer in minuti per inviare un pacchetto di pubblicità locale (senza salto). Imposta su 0 per disabilitare.'; @override String get repeater_cliHelpSetFloodAdvertInterval => @@ -2250,11 +2293,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetTxDelay => - 'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).'; + 'Imposta un fattore moltiplicato con il tempo di mantenimento per un pacchetto di modalità allagamento e con un sistema di slot casuale, per ritardarne la trasmissione (per diminuire la probabilità di collisioni).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.'; + 'Uguale a txdelay, ma per applicare un ritardo casuale alla inoltrata di pacchetti in modalità diretta.'; @override String get repeater_cliHelpSetBridgeEnabled => 'Abilita/Disabilita ponte.'; @@ -2265,11 +2308,11 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetBridgeSource => - 'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.'; + 'Scegliere se il ponte dovrà ritrasmettere i pacchetti ricevuti o i pacchetti trasmessi.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Imposta la velocità di trasmissione per i ponti rs232.'; + 'Imposta la velocità di trasmissione per i ponti rs232.'; @override String get repeater_cliHelpSetBridgeSecret => @@ -2285,7 +2328,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpSetPerm => - 'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)'; + 'Modifica l\'ACL. Rimuove l\'entrata corrispondente (per prefisso di pubkey) se \"permissions\" è zero. Aggiunge una nuova entrata se il pubkey-hex ha lunghezza completa e non è attualmente nell\'ACL. Aggiorna l\'entrata per corrispondenza del prefisso di pubkey. I bit di permesso variano per ogni ruolo di firmware, ma i primi 2 bit sono: 0 (Guest), 1 (solo lettura), 2 (lettura/scrittura), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => @@ -2305,7 +2348,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4'; + 'Mostra un elenco di altri nodi repeater ricevuti tramite annunci zero-hop. Ogni riga è id-prefisso-esadecimale:timestamp:snr-volte-4'; @override String get repeater_cliHelpNeighborRemove => @@ -2317,7 +2360,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.'; + 'NOTA: questo è un\'invocazione multi-comando speciale. Ogni comando successivo è un nome di regione (indentato con spazi per indicare la gerarchia parentale, con almeno uno spazio). Terminata inviando una riga vuota/comando.'; @override String get repeater_cliHelpRegionGet => @@ -2337,7 +2380,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpRegionDenyf => - 'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).'; + 'Rimuove il permesso \'F\'lood per la regione specificata. (NOTA: a questo stadio non è consigliato utilizzarlo sullo scope globale/legacy!!).'; @override String get repeater_cliHelpRegionHome => @@ -2352,7 +2395,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.'; + 'Mostra lo stato del GPS. Quando il GPS è spento, risponde solo \"spento\", se è acceso risponde con \"acceso\", \"stato\", \"fix\" e numero di satelliti.'; @override String get repeater_cliHelpGpsOnOff => @@ -2409,7 +2452,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_gpsNote => - 'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.'; + 'è stata introdotta una funzione gps per gestire le tematiche relative alla posizione.'; @override String get telemetry_receivedData => 'Dati Telemetria Ricevuti'; @@ -2462,7 +2505,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2530,7 +2573,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Percorso osservato $index • $hops'; + return 'Percorso osservato $index • $hops'; } @override @@ -2585,7 +2628,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2596,14 +2639,14 @@ class AppLocalizationsIt extends AppLocalizations { String get channelPath_unknownRepeater => 'Ripetitore sconosciuto'; @override - String get community_title => 'Comunità'; + String get community_title => 'Comunità'; @override - String get community_create => 'Crea Comunità'; + String get community_create => 'Crea Comunità'; @override String get community_createDesc => - 'Crea una nuova comunità e condividila tramite codice QR.'; + 'Crea una nuova comunità e condividila tramite codice QR.'; @override String get community_join => 'Unisciti'; @@ -2621,35 +2664,35 @@ class AppLocalizationsIt extends AppLocalizations { @override String get community_scanInstructions => - 'Punta la fotocamera su un codice QR della comunità'; + 'Punta la fotocamera su un codice QR della comunità'; @override String get community_showQr => 'Mostra il codice QR'; @override - String get community_publicChannel => 'Comunità Pubblica'; + String get community_publicChannel => 'Comunità Pubblica'; @override - String get community_hashtagChannel => 'Hashtag della Comunità'; + String get community_hashtagChannel => 'Hashtag della Comunità'; @override - String get community_name => 'Nome della Comunità'; + String get community_name => 'Nome della Comunità'; @override - String get community_enterName => 'Inserisci il nome della comunità'; + String get community_enterName => 'Inserisci il nome della comunità'; @override String community_created(String name) { - return 'Comunità \"$name\" creata'; + return 'Comunità \"$name\" creata'; } @override String community_joined(String name) { - return 'Unito alla comunità \"$name\"'; + return 'Unito alla comunità \"$name\"'; } @override - String get community_qrTitle => 'Condividi Comunità'; + String get community_qrTitle => 'Condividi Comunità'; @override String community_qrInstructions(String name) { @@ -2664,16 +2707,16 @@ class AppLocalizationsIt extends AppLocalizations { String get community_invalidQrCode => 'Codice QR della community non valido'; @override - String get community_alreadyMember => 'Già membro'; + String get community_alreadyMember => 'Già membro'; @override String community_alreadyMemberMessage(String name) { - return 'Sei già un membro di \"$name\".'; + return 'Sei già un membro di \"$name\".'; } @override String get community_addPublicChannel => - 'Aggiungi Canale Pubblico della Comunità'; + 'Aggiungi Canale Pubblico della Comunità'; @override String get community_addPublicChannelHint => @@ -2687,10 +2730,10 @@ class AppLocalizationsIt extends AppLocalizations { 'Scansiona un codice QR o crea una community per iniziare.'; @override - String get community_manageCommunities => 'Gestisci Comunità'; + String get community_manageCommunities => 'Gestisci Comunità'; @override - String get community_delete => 'Lascia la Comunità'; + String get community_delete => 'Lascia la Comunità'; @override String community_deleteConfirm(String name) { @@ -2699,12 +2742,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Questo eliminerà anche $count canale/i e i loro messaggi.'; + return 'Questo eliminerà anche $count canale/i e i loro messaggi.'; } @override String community_deleted(String name) { - return 'Hai lasciato la comunità \"$name\"'; + return 'Hai lasciato la comunità \"$name\"'; } @override @@ -2744,21 +2787,21 @@ class AppLocalizationsIt extends AppLocalizations { 'Aggiungi un canale con hashtag per questa community'; @override - String get community_selectCommunity => 'Seleziona Comunità'; + String get community_selectCommunity => 'Seleziona Comunità'; @override String get community_regularHashtag => 'Hashtag regolare'; @override String get community_regularHashtagDesc => - 'Hashtag pubblico (chiunque può unirsi)'; + 'Hashtag pubblico (chiunque può unirsi)'; @override - String get community_communityHashtag => 'Hashtag della Comunità'; + String get community_communityHashtag => 'Hashtag della Comunità'; @override String get community_communityHashtagDesc => - 'Visibile solo ai membri della comunità'; + 'Visibile solo ai membri della comunità'; @override String community_forCommunity(String name) { @@ -2825,7 +2868,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => - 'Uno o più dei luppoli mancano di una posizione!'; + 'Uno o più dei luppoli mancano di una posizione!'; @override String get pathTrace_clearTooltip => 'Pulisci percorso'; @@ -2847,7 +2890,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Eseguire LOS per visualizzare il profilo altimetrico'; @override - String get losMenuTitle => 'Menù LOS'; + String get losMenuTitle => 'Menù LOS'; @override String get losMenuSubtitle => @@ -2919,7 +2962,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Dati di elevazione non disponibili per uno o più campioni.'; + 'Dati di elevazione non disponibili per uno o più campioni.'; @override String get losErrorInvalidInput => @@ -2957,7 +3000,7 @@ class AppLocalizationsIt extends AppLocalizations { String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; @override - String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; + String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; @override String losFrequencyDialogDescription( @@ -2997,13 +3040,13 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'La clipboard è vuota.'; + String get contacts_clipboardEmpty => 'La clipboard è vuota.'; @override String get contacts_invalidAdvertFormat => 'Dati di contatto non validi'; @override - String get contacts_contactImported => 'Il contatto è stato importato.'; + String get contacts_contactImported => 'Il contatto è stato importato.'; @override String get contacts_contactImportFailed => @@ -3045,7 +3088,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Copia dell\'annuncio nella Clipboard non riuscita.'; @override - String get notification_activityTitle => 'Attività MeshCore'; + String get notification_activityTitle => 'Attività MeshCore'; @override String notification_messagesCount(int count) { @@ -3123,7 +3166,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_gpxExportError => - 'Si è verificato un errore durante l\'esportazione.'; + 'Si è verificato un errore durante l\'esportazione.'; @override String get settings_gpxExportRepeatersRoom => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 21d2fd4..c83cab8 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -69,7 +69,7 @@ class AppLocalizationsNl extends AppLocalizations { String get common_share => 'Delen'; @override - String get common_copy => 'Kopiëren'; + String get common_copy => 'Kopiëren'; @override String get common_retry => 'Nogmaals proberen'; @@ -93,7 +93,7 @@ class AppLocalizationsNl extends AppLocalizations { String get common_loading => 'Laden...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -119,7 +119,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get usbScreenSubtitle => - 'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; + 'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; @override String get usbScreenStatus => 'Selecteer een USB-apparaat'; @@ -132,6 +132,48 @@ class AppLocalizationsNl extends AppLocalizations { String get usbScreenEmptyState => 'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.'; + @override + String get usbErrorPermissionDenied => 'Toegang via USB is geweigerd.'; + + @override + String get usbErrorDeviceMissing => + 'Het geselecteerde USB-apparaat is niet meer beschikbaar.'; + + @override + String get usbErrorInvalidPort => 'Selecteer een geldig USB-apparaat.'; + + @override + String get usbErrorBusy => + 'Een andere verzoek om een USB-verbinding is al in behandeling.'; + + @override + String get usbErrorNotConnected => 'Er is geen USB-apparaat aangesloten.'; + + @override + String get usbErrorOpenFailed => + 'Kon het geselecteerde USB-apparaat niet openen.'; + + @override + String get usbErrorConnectFailed => + 'Kon niet verbinding maken met het geselecteerde USB-apparaat.'; + + @override + String get usbErrorUnsupported => + 'USB-serieel is niet ondersteund op deze platform.'; + + @override + String get usbErrorAlreadyActive => 'Een USB-verbinding is al actief.'; + + @override + String get usbErrorNoDeviceSelected => 'Geen USB-apparaat is geselecteerd.'; + + @override + String get usbErrorPortClosed => 'De USB-verbinding is niet actief.'; + + @override + String get usbErrorConnectTimedOut => + 'Wachtperiode is verlopen, er is geen reactie ontvangen van het apparaat.'; + @override String get scanner_scanning => 'Scannen naar apparaten...'; @@ -231,7 +273,7 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_location => 'Locatie'; @override - String get settings_locationSubtitle => 'GPS coördinaten'; + String get settings_locationSubtitle => 'GPS coördinaten'; @override String get settings_locationUpdated => 'Locatie bijgewerkt'; @@ -450,10 +492,10 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -462,16 +504,16 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -480,16 +522,16 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russisch'; @override - String get appSettings_languageUk => 'Oekraïens'; + String get appSettings_languageUk => 'Oekraïens'; @override String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; @@ -847,7 +889,7 @@ class AppLocalizationsNl extends AppLocalizations { String get channels_public => 'Openbaar'; @override - String get channels_private => 'Privé'; + String get channels_private => 'Privé'; @override String get channels_publicChannel => 'Open kanaal'; @@ -947,14 +989,14 @@ class AppLocalizationsNl extends AppLocalizations { String get channels_sortUnread => 'Ongelezen'; @override - String get channels_createPrivateChannel => 'Maak een Privé Kanaal'; + String get channels_createPrivateChannel => 'Maak een Privé Kanaal'; @override String get channels_createPrivateChannelDesc => 'Beveiligd met een geheime sleutel.'; @override - String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan'; + String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan'; @override String get channels_joinPrivateChannelDesc => @@ -1336,7 +1378,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_nodesNeedGps => - 'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen'; + 'Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen'; @override String map_nodesCount(int count) { @@ -1364,7 +1406,7 @@ class AppLocalizationsNl extends AppLocalizations { String get map_pinDm => 'Verzenden als bericht (DM)'; @override - String get map_pinPrivate => 'Beveiligd (Privé)'; + String get map_pinPrivate => 'Beveiligd (Privé)'; @override String get map_pinPublic => 'Openbaar spikken'; @@ -2031,7 +2073,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_eraseSerialOnly => - 'Verwijderen is alleen beschikbaar via de seriële console.'; + 'Verwijderen is alleen beschikbaar via de seriële console.'; @override String repeater_commandSent(String command) { @@ -2259,7 +2301,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpSetBridgeBaud => - 'Stel de seriële link baudrate in voor rs232 bruggen.'; + 'Stel de seriële link baudrate in voor rs232 bruggen.'; @override String get repeater_cliHelpSetBridgeSecret => @@ -2275,7 +2317,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpSetPerm => - 'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)'; + 'Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => @@ -2307,7 +2349,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.'; + 'LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.'; @override String get repeater_cliHelpRegionGet => @@ -2352,7 +2394,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliHelpGpsSetLoc => - 'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.'; + 'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.'; @override String get repeater_cliHelpGpsAdvert => @@ -2390,14 +2432,14 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_regionNote => - 'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.'; + 'Regio-commando\'s zijn geïntroduceerd om regio-definities en permissies te beheren.'; @override String get repeater_gpsManagement => 'Beheer GPS'; @override String get repeater_gpsNote => - 'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.'; + 'De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.'; @override String get telemetry_receivedData => 'Ontvangen Telemetriedata'; @@ -2450,7 +2492,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2519,7 +2561,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Waargenomen pad $index • $hops'; + return 'Waargenomen pad $index • $hops'; } @override @@ -2574,7 +2616,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2991,11 +3033,11 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens'; @override - String get contacts_contactImported => 'Contact is geïmporteerd.'; + String get contacts_contactImported => 'Contact is geïmporteerd.'; @override String get contacts_contactImportFailed => - 'Contact kon niet geïmporteerd worden.'; + 'Contact kon niet geïmporteerd worden.'; @override String get contacts_zeroHopAdvert => 'Zero Hop Reclame'; @@ -3004,14 +3046,14 @@ class AppLocalizationsNl extends AppLocalizations { String get contacts_floodAdvert => 'Overstromingsadvertentie'; @override - String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren'; + String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren'; @override String get contacts_addContactFromClipboard => 'Contact uit klembord toevoegen'; @override - String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren'; + String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren'; @override String get contacts_ShareContactZeroHop => 'Contact delen via advertentie'; @@ -3030,7 +3072,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => - 'Kopiëren van advertentie naar Clipboard is mislukt.'; + 'Kopiëren van advertentie naar Clipboard is mislukt.'; @override String get notification_activityTitle => 'MeshCore Activiteit'; @@ -3099,8 +3141,7 @@ class AppLocalizationsNl extends AppLocalizations { 'Exporteert alle contacten met een locatie naar een GPX-bestand.'; @override - String get settings_gpxExportSuccess => - 'Succesvol GPX-bestand geëxporteerd.'; + String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.'; @override String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.'; @@ -3124,7 +3165,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_gpxExportShareText => - 'Kaartgegevens geëxporteerd uit meshcore-open'; + 'Kaartgegevens geëxporteerd uit meshcore-open'; @override String get settings_gpxExportShareSubject => diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 8f26f1c..fe9dedb 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -15,7 +15,7 @@ class AppLocalizationsPl extends AppLocalizations { String get nav_contacts => 'Kontakty'; @override - String get nav_channels => 'KanaÅ‚y'; + String get nav_channels => 'Kanały'; @override String get nav_map => 'Mapa'; @@ -27,19 +27,19 @@ class AppLocalizationsPl extends AppLocalizations { String get common_ok => 'OK'; @override - String get common_connect => 'Połącz'; + String get common_connect => 'Połącz'; @override - String get common_unknownDevice => 'Nieznane urzÄ…dzenie'; + String get common_unknownDevice => 'Nieznane urządzenie'; @override String get common_save => 'Zapisz'; @override - String get common_delete => 'UsuÅ„'; + String get common_delete => 'Usuń'; @override - String get common_close => 'Zamknąć'; + String get common_close => 'Zamknąć'; @override String get common_edit => 'Edytuj'; @@ -51,49 +51,49 @@ class AppLocalizationsPl extends AppLocalizations { String get common_settings => 'Ustawienia'; @override - String get common_disconnect => 'Odłącz'; + String get common_disconnect => 'Odłącz'; @override - String get common_connected => 'Połączono'; + String get common_connected => 'Połączono'; @override - String get common_disconnected => 'Odłączony'; + String get common_disconnected => 'Odłączony'; @override - String get common_create => 'Utwórz'; + String get common_create => 'Utwórz'; @override String get common_continue => 'Kontynuuj'; @override - String get common_share => 'UdostÄ™pnij'; + String get common_share => 'Udostępnij'; @override String get common_copy => 'Kopiuj'; @override - String get common_retry => 'Spróbować'; + String get common_retry => 'Spróbować'; @override String get common_hide => 'Ukryj'; @override - String get common_remove => 'UsuÅ„'; + String get common_remove => 'Usuń'; @override - String get common_enable => 'Włącz'; + String get common_enable => 'Włącz'; @override - String get common_disable => 'Wyłączyć'; + String get common_disable => 'Wyłączyć'; @override - String get common_reboot => 'Zrestartować'; + String get common_reboot => 'Zrestartować'; @override - String get common_loading => 'Ładowanie...'; + String get common_loading => 'Ładowanie...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -115,50 +115,94 @@ class AppLocalizationsPl extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Połącz przez USB'; + String get usbScreenTitle => 'Połącz przez USB'; @override String get usbScreenSubtitle => - 'Wybierz wykryty urzÄ…dzenie szeregowe i podłącz je bezpoÅ›rednio do swojego wÄ™zÅ‚a MeshCore.'; + 'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.'; @override - String get usbScreenStatus => 'Wybierz urzÄ…dzenie USB'; + String get usbScreenStatus => 'Wybierz urządzenie USB'; @override String get usbScreenNote => - 'Port szeregowy USB jest aktywny na urzÄ…dzeniach z Androidem i platformach stacjonarnych, które obsÅ‚ugujÄ… tÄ™ funkcjÄ™.'; + 'Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.'; @override String get usbScreenEmptyState => - 'Nie znaleziono żadnych urzÄ…dzeÅ„ USB. Podłącz jedno i zaktualizuj.'; + 'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.'; @override - String get scanner_scanning => 'Skanowanie urzÄ…dzeÅ„...'; + String get usbErrorPermissionDenied => + 'Zostało odrzucone żądanie dostępu przez USB.'; @override - String get scanner_connecting => 'Łączenie...'; + String get usbErrorDeviceMissing => + 'Wybór urządzenia USB już nie jest dostępny.'; @override - String get scanner_disconnecting => 'Odłączanie...'; + String get usbErrorInvalidPort => 'Wybierz prawidłowe urządzenie USB.'; @override - String get scanner_notConnected => 'Niepołączony'; + String get usbErrorBusy => + 'Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.'; + + @override + String get usbErrorNotConnected => 'Brak podłączonego urządzenia USB.'; + + @override + String get usbErrorOpenFailed => + 'Nie udało się otworzyć wybranego urządzenia USB.'; + + @override + String get usbErrorConnectFailed => + 'Nie udało się nawiązać połączenia z wybranym urządzeniem USB.'; + + @override + String get usbErrorUnsupported => + 'Port szeregowy USB nie jest obsługiwany na tym urządzeniu.'; + + @override + String get usbErrorAlreadyActive => 'Połączenie USB jest już aktywne.'; + + @override + String get usbErrorNoDeviceSelected => + 'Nie został wybrany żaden urządzenie USB.'; + + @override + String get usbErrorPortClosed => 'Połączenie USB nie jest aktywne.'; + + @override + String get usbErrorConnectTimedOut => + 'Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji.'; + + @override + String get scanner_scanning => 'Skanowanie urządzeń...'; + + @override + String get scanner_connecting => 'Łączenie...'; + + @override + String get scanner_disconnecting => 'Odłączanie...'; + + @override + String get scanner_notConnected => 'Niepołączony'; @override String scanner_connectedTo(String deviceName) { - return 'Połączono z $deviceName'; + return 'Połączono z $deviceName'; } @override - String get scanner_searchingDevices => 'Wyszukiwanie urzÄ…dzeÅ„ MeshCore...'; + String get scanner_searchingDevices => 'Wyszukiwanie urządzeń MeshCore...'; @override String get scanner_tapToScan => - 'NaciÅ›nij Skan, aby znaleźć urzÄ…dzenia MeshCore'; + 'Naciśnij Skan, aby znaleźć urządzenia MeshCore'; @override String scanner_connectionFailed(String error) { - return 'Połączenie nieudane: $error'; + return 'Połączenie nieudane: $error'; } @override @@ -168,21 +212,21 @@ class AppLocalizationsPl extends AppLocalizations { String get scanner_scan => 'Przeskanuj'; @override - String get scanner_bluetoothOff => 'Bluetooth jest wyłączony'; + String get scanner_bluetoothOff => 'Bluetooth jest wyłączony'; @override String get scanner_bluetoothOffMessage => - 'Prosimy włączyć Bluetooth, aby przeskanować urzÄ…dzenia.'; + 'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.'; @override - String get scanner_chromeRequired => 'Wymagana przeglÄ…darka Chrome'; + String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome'; @override String get scanner_chromeRequiredMessage => - 'Ta aplikacja internetowa wymaga przeglÄ…darki Google Chrome lub opartej na Chromium do obsÅ‚ugi Bluetooth.'; + 'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.'; @override - String get scanner_enableBluetooth => 'Włącz Bluetooth'; + String get scanner_enableBluetooth => 'Włącz Bluetooth'; @override String get device_quickSwitch => 'Szybka zmiana'; @@ -194,40 +238,40 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_title => 'Ustawienia'; @override - String get settings_deviceInfo => 'Informacje o urzÄ…dzeniu'; + String get settings_deviceInfo => 'Informacje o urządzeniu'; @override String get settings_appSettings => 'Ustawienia aplikacji'; @override String get settings_appSettingsSubtitle => - 'Powiadomienia, wiadomoÅ›ci i preferencje mapy'; + 'Powiadomienia, wiadomości i preferencje mapy'; @override - String get settings_nodeSettings => 'Ustawienia wÄ™zÅ‚a'; + String get settings_nodeSettings => 'Ustawienia węzła'; @override - String get settings_nodeName => 'Nazwa wÄ™zÅ‚a'; + String get settings_nodeName => 'Nazwa węzła'; @override String get settings_nodeNameNotSet => 'Nie ustawione'; @override - String get settings_nodeNameHint => 'Wprowadź nazwÄ™ wÄ™zÅ‚a'; + String get settings_nodeNameHint => 'Wprowadź nazwę węzła'; @override - String get settings_nodeNameUpdated => 'ImiÄ™ zaktualizowane'; + String get settings_nodeNameUpdated => 'Imię zaktualizowane'; @override String get settings_radioSettings => 'Ustawienia radia'; @override String get settings_radioSettingsSubtitle => - 'CzÄ™stotliwość, moc, współczynnik rozpraszania'; + 'Częstotliwość, moc, współczynnik rozpraszania'; @override String get settings_radioSettingsUpdated => - 'Ustawienia radia zostaÅ‚y zaktualizowane'; + 'Ustawienia radia zostały zaktualizowane'; @override String get settings_location => 'Lokalizacja'; @@ -240,94 +284,94 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_locationBothRequired => - 'Wprowadź zarówno szerokość, jak i dÅ‚ugość geograficznÄ….'; + 'Wprowadź zarówno szerokość, jak i długość geograficzną.'; @override String get settings_locationInvalid => - 'NieprawidÅ‚owa szerokość geograficzna lub dÅ‚ugość geograficzna.'; + 'Nieprawidłowa szerokość geograficzna lub długość geograficzna.'; @override - String get settings_locationGPSEnable => 'Włącz GPS'; + String get settings_locationGPSEnable => 'Włącz GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Włącza automatyczne aktualizowanie pozycji za pomocÄ… GPS.'; + 'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.'; @override - String get settings_locationIntervalSec => 'InterwaÅ‚ dla GPS (Sekundy)'; + String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)'; @override String get settings_locationIntervalInvalid => - 'InterwaÅ‚ musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.'; + 'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.'; @override - String get settings_latitude => 'Szerokość'; + String get settings_latitude => 'Szerokość'; @override - String get settings_longitude => 'DÅ‚ugość'; + String get settings_longitude => 'Długość'; @override String get settings_privacyMode => 'Tryb Prywatny'; @override String get settings_privacyModeSubtitle => - 'Ukryj imiÄ™/lokalizacjÄ™ w reklamach'; + 'Ukryj imię/lokalizację w reklamach'; @override String get settings_privacyModeToggle => - 'Włącz tryb prywatnoÅ›ci, aby ukryć swoje imiÄ™ i lokalizacjÄ™ w reklamach.'; + 'Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w reklamach.'; @override - String get settings_privacyModeEnabled => 'Tryb prywatnoÅ›ci włączony'; + String get settings_privacyModeEnabled => 'Tryb prywatności włączony'; @override - String get settings_privacyModeDisabled => 'Tryb prywatnoÅ›ci wyłączony'; + String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony'; @override - String get settings_actions => 'DziaÅ‚ania'; + String get settings_actions => 'Działania'; @override - String get settings_sendAdvertisement => 'WyÅ›lij ReklamÄ™'; + String get settings_sendAdvertisement => 'Wyślij Reklamę'; @override String get settings_sendAdvertisementSubtitle => - 'Obecność transmisji jest teraz'; + 'Obecność transmisji jest teraz'; @override - String get settings_advertisementSent => 'Reklama wysÅ‚ana'; + String get settings_advertisementSent => 'Reklama wysłana'; @override String get settings_syncTime => 'Czas synchronizacji'; @override String get settings_syncTimeSubtitle => - 'Ustaw zegar urzÄ…dzenia na czas telefonu.'; + 'Ustaw zegar urządzenia na czas telefonu.'; @override String get settings_timeSynchronized => 'Synchronizacja czasu'; @override - String get settings_refreshContacts => 'OdÅ›wież Kontakty'; + String get settings_refreshContacts => 'Odśwież Kontakty'; @override String get settings_refreshContactsSubtitle => - 'OdÅ›wież listÄ™ kontaktów z urzÄ…dzenia'; + 'Odśwież listę kontaktów z urządzenia'; @override - String get settings_rebootDevice => 'Zrestartuj UrzÄ…dzenie'; + String get settings_rebootDevice => 'Zrestartuj Urządzenie'; @override - String get settings_rebootDeviceSubtitle => 'Zrestartuj urzÄ…dzenie MeshCore'; + String get settings_rebootDeviceSubtitle => 'Zrestartuj urządzenie MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Czy na pewno chcesz zrestartować urzÄ…dzenie? BÄ™dziesz odłączony.'; + 'Czy na pewno chcesz zrestartować urządzenie? Będziesz odłączony.'; @override String get settings_debug => 'Debug'; @override - String get settings_bleDebugLog => 'Log błędów BLE'; + String get settings_bleDebugLog => 'Log błędów BLE'; @override String get settings_bleDebugLogSubtitle => @@ -352,14 +396,14 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_aboutDescription => - 'Otwarty kod źródÅ‚owy klient Flutter dla urzÄ…dzeÅ„ do sieci mesh LoRa MeshCore.'; + 'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Dane wysokoÅ›ciowe LOS: Open-Meteo (CC BY 4.0)'; + 'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'ImiÄ™'; + String get settings_infoName => 'Imię'; @override String get settings_infoId => 'ID'; @@ -374,29 +418,29 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_infoPublicKey => 'Klucz Publiczny'; @override - String get settings_infoContactsCount => 'Liczba kontaktów'; + String get settings_infoContactsCount => 'Liczba kontaktów'; @override - String get settings_infoChannelCount => 'Liczba kanałów'; + String get settings_infoChannelCount => 'Liczba kanałów'; @override String get settings_presets => 'Preset'; @override - String get settings_frequency => 'CzÄ™stotliwość (MHz)'; + String get settings_frequency => 'Częstotliwość (MHz)'; @override String get settings_frequencyHelper => '300,0 - 2500,0'; @override String get settings_frequencyInvalid => - 'NieprawidÅ‚owa czÄ™stotliwość (300-2500 MHz)'; + 'Nieprawidłowa częstotliwość (300-2500 MHz)'; @override - String get settings_bandwidth => 'Przepustowość'; + String get settings_bandwidth => 'Przepustowość'; @override - String get settings_spreadingFactor => 'RozkÅ‚ad Czynnika'; + String get settings_spreadingFactor => 'Rozkład Czynnika'; @override String get settings_codingRate => 'Stawka Kodowania'; @@ -408,35 +452,35 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'NieprawidÅ‚owa moc TX (0-22 dBm)'; + String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)'; @override - String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci'; + String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci'; @override String get settings_clientRepeatSubtitle => - 'Pozwól temu urzÄ…dzeniu powtarzać pakiety danych dla innych urzÄ…dzeÅ„.'; + 'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.'; @override String get settings_clientRepeatFreqWarning => - 'Powtórka poza sieciÄ… wymaga czÄ™stotliwoÅ›ci 433, 869 lub 918 MHz.'; + 'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.'; @override String settings_error(String message) { - return 'Błąd: $message'; + return 'Błąd: $message'; } @override String get appSettings_title => 'Ustawienia aplikacji'; @override - String get appSettings_appearance => 'WyglÄ…d'; + String get appSettings_appearance => 'Wygląd'; @override String get appSettings_theme => 'Motyw'; @override - String get appSettings_themeSystem => 'DomyÅ›lne ustawienia systemu'; + String get appSettings_themeSystem => 'Domyślne ustawienia systemu'; @override String get appSettings_themeLight => 'Jasne'; @@ -445,19 +489,19 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_themeDark => 'Ciemny'; @override - String get appSettings_language => 'JÄ™zyk'; + String get appSettings_language => 'Język'; @override - String get appSettings_languageSystem => 'DomyÅ›lny systemowy'; + String get appSettings_languageSystem => 'Domyślny systemowy'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -466,16 +510,16 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -484,60 +528,59 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Rosyjski'; @override - String get appSettings_languageUk => 'UkraiÅ„ska'; + String get appSettings_languageUk => 'Ukraińska'; @override - String get appSettings_enableMessageTracing => - 'Włącz Å›ledzenie wiadomoÅ›ci'; + String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości'; @override String get appSettings_enableMessageTracingSubtitle => - 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomoÅ›ci'; + 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości'; @override String get appSettings_notifications => 'Powiadomienia'; @override - String get appSettings_enableNotifications => 'Włącz Powiadomienia'; + String get appSettings_enableNotifications => 'Włącz Powiadomienia'; @override String get appSettings_enableNotificationsSubtitle => - 'Otrzymuj powiadomienia o wiadomoÅ›ciach i reklamach.'; + 'Otrzymuj powiadomienia o wiadomościach i reklamach.'; @override String get appSettings_notificationPermissionDenied => 'Odmowa zezwolenia na powiadomienia'; @override - String get appSettings_notificationsEnabled => 'Powiadomienia włączone'; + String get appSettings_notificationsEnabled => 'Powiadomienia włączone'; @override - String get appSettings_notificationsDisabled => 'Powiadomienia wyłączone'; + String get appSettings_notificationsDisabled => 'Powiadomienia wyłączone'; @override String get appSettings_messageNotifications => - 'Powiadomienia o wiadomoÅ›ciach'; + 'Powiadomienia o wiadomościach'; @override String get appSettings_messageNotificationsSubtitle => - 'Pokaż powiadomienie przy otrzymywaniu nowych wiadomoÅ›ci'; + 'Pokaż powiadomienie przy otrzymywaniu nowych wiadomości'; @override String get appSettings_channelMessageNotifications => - 'Powiadomienia o WiadomoÅ›ciach na KanaÅ‚ach'; + 'Powiadomienia o Wiadomościach na Kanałach'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Pokaż powiadomienie przy odbieraniu wiadomoÅ›ci z kanaÅ‚u'; + 'Pokaż powiadomienie przy odbieraniu wiadomości z kanału'; @override String get appSettings_advertisementNotifications => @@ -545,22 +588,22 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_advertisementNotificationsSubtitle => - 'WyÅ›wietl powiadomienie, gdy zostanÄ… odkryte nowe wÄ™zÅ‚y.'; + 'Wyświetl powiadomienie, gdy zostaną odkryte nowe węzły.'; @override - String get appSettings_messaging => 'WiadomoÅ›ci'; + String get appSettings_messaging => 'Wiadomości'; @override String get appSettings_clearPathOnMaxRetry => - 'Wyczyść ÅšcieżkÄ™ na Maksymalnej Próbie'; + 'Wyczyść Ścieżkę na Maksymalnej Próbie'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Resetuj Å›cieżkÄ™ kontaktu po 5 nieudanych próbach wysÅ‚ania'; + 'Resetuj ścieżkę kontaktu po 5 nieudanych próbach wysłania'; @override String get appSettings_pathsWillBeCleared => - 'Droga bÄ™dzie wyczyszczona po 5 nieudanych próbach.'; + 'Droga będzie wyczyszczona po 5 nieudanych próbach.'; @override String get appSettings_pathsWillNotBeCleared => @@ -571,15 +614,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_autoRouteRotationSubtitle => - 'Przełączaj siÄ™ miÄ™dzy najlepszymi Å›cieżkami a trybem zalewowym.'; + 'Przełączaj się między najlepszymi ścieżkami a trybem zalewowym.'; @override String get appSettings_autoRouteRotationEnabled => - 'Automatyczne obracanie tras włączone'; + 'Automatyczne obracanie tras włączone'; @override String get appSettings_autoRouteRotationDisabled => - 'Automatyczne obracanie tras wyłączone'; + 'Automatyczne obracanie tras wyłączone'; @override String get appSettings_battery => 'Bateria'; @@ -589,12 +632,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Ustawione na urzÄ…dzenie ($deviceName)'; + return 'Ustawione na urządzenie ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Połącz siÄ™ z urzÄ…dzeniem, aby wybrać'; + 'Połącz się z urządzeniem, aby wybrać'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; @@ -606,46 +649,45 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override - String get appSettings_mapDisplay => 'WyÅ›wietlanie mapy'; + String get appSettings_mapDisplay => 'Wyświetlanie mapy'; @override - String get appSettings_showRepeaters => 'Pokaż Powtórniki'; + String get appSettings_showRepeaters => 'Pokaż Powtórniki'; @override String get appSettings_showRepeatersSubtitle => - 'WyÅ›wietl wÄ™zÅ‚y powtarzajÄ…ce siÄ™ na mapie'; + 'Wyświetl węzły powtarzające się na mapie'; @override - String get appSettings_showChatNodes => 'Pokaż WÄ™zÅ‚y Rozmowy'; + String get appSettings_showChatNodes => 'Pokaż Węzły Rozmowy'; @override String get appSettings_showChatNodesSubtitle => - 'WyÅ›wietl wÄ™zÅ‚y czatu na mapie'; + 'Wyświetl węzły czatu na mapie'; @override - String get appSettings_showOtherNodes => 'Pokaż inne wÄ™zÅ‚y'; + String get appSettings_showOtherNodes => 'Pokaż inne węzły'; @override String get appSettings_showOtherNodesSubtitle => - 'WyÅ›wietl inne typy wÄ™złów na mapie'; + 'Wyświetl inne typy węzłów na mapie'; @override String get appSettings_timeFilter => 'Filtrowanie Czasu'; @override - String get appSettings_timeFilterShowAll => 'Pokaż wszystkie wÄ™zÅ‚y'; + String get appSettings_timeFilterShowAll => 'Pokaż wszystkie węzły'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Pokaż wÄ™zÅ‚y z ostatnich $hours godzin'; + return 'Pokaż węzły z ostatnich $hours godzin'; } @override String get appSettings_mapTimeFilter => 'Filtrowanie Czasu Mapy'; @override - String get appSettings_showNodesDiscoveredWithin => - 'Pokaż wÄ™zÅ‚y odkryte w:'; + String get appSettings_showNodesDiscoveredWithin => 'Pokaż węzły odkryte w:'; @override String get appSettings_allTime => 'Wszystko czasowo'; @@ -660,7 +702,7 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_last24Hours => 'Ostatnie 24 godziny'; @override - String get appSettings_lastWeek => 'TydzieÅ„ temu'; + String get appSettings_lastWeek => 'Tydzień temu'; @override String get appSettings_offlineMapCache => 'Bufor Map Offline'; @@ -675,8 +717,7 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_unitsImperial => 'Imperialne (ft / mi)'; @override - String get appSettings_noAreaSelected => - 'Nie zaznaczono żadnej powierzchni.'; + String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { @@ -691,25 +732,25 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_appDebugLoggingSubtitle => - 'Loguj wiadomoÅ›ci debugowania aplikacji w celu rozwiÄ…zywania problemów.'; + 'Loguj wiadomości debugowania aplikacji w celu rozwiązywania problemów.'; @override String get appSettings_appDebugLoggingEnabled => - 'Zdebugowanie aplikacji włączone'; + 'Zdebugowanie aplikacji włączone'; @override String get appSettings_appDebugLoggingDisabled => - 'Zasubskrybowane logi debugowania aplikacji wyłączone.'; + 'Zasubskrybowane logi debugowania aplikacji wyłączone.'; @override String get contacts_title => 'Kontakty'; @override - String get contacts_noContacts => 'Brak jeszcze kontaktów.'; + String get contacts_noContacts => 'Brak jeszcze kontaktów.'; @override String get contacts_contactsWillAppear => - 'Kontakty bÄ™dÄ… wyÅ›wietlane, gdy urzÄ…dzenia reklamujÄ… siÄ™.'; + 'Kontakty będą wyświetlane, gdy urządzenia reklamują się.'; @override String get contacts_unread => 'Nieprzeczytane'; @@ -729,55 +770,55 @@ class AppLocalizationsPl extends AppLocalizations { @override String contacts_searchUsers(int number, String str) { - return 'Wyszukaj $number$str Użytkowników...'; + return 'Wyszukaj $number$str Użytkowników...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Wyszukaj $number$str powtórników...'; + return 'Wyszukaj $number$str powtórników...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Wyszukaj $number$str serwerów Room...'; + return 'Wyszukaj $number$str serwerów Room...'; } @override - String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów'; + String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów'; @override String get contacts_noContactsFound => - 'Brak znalezionych kontaktów ani grup.'; + 'Brak znalezionych kontaktów ani grup.'; @override - String get contacts_deleteContact => 'UsuÅ„ Kontakt'; + String get contacts_deleteContact => 'Usuń Kontakt'; @override String contacts_removeConfirm(String contactName) { - return 'UsuÅ„ $contactName z kontaktów?'; + return 'Usuń $contactName z kontaktów?'; } @override - String get contacts_manageRepeater => 'ZarzÄ…dzaj Powtórzami'; + String get contacts_manageRepeater => 'Zarządzaj Powtórzami'; @override - String get contacts_manageRoom => 'ZarzÄ…dzaj Serwerem Pokoju'; + String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju'; @override String get contacts_roomLogin => 'Logowanie do pokoju'; @override - String get contacts_openChat => 'Otwórz czat'; + String get contacts_openChat => 'Otwórz czat'; @override - String get contacts_editGroup => 'Edytuj GrupÄ™'; + String get contacts_editGroup => 'Edytuj Grupę'; @override - String get contacts_deleteGroup => 'UsuÅ„ GrupÄ™'; + String get contacts_deleteGroup => 'Usuń Grupę'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'UsuÅ„ \"$groupName\"?'; + return 'Usuń \"$groupName\"?'; } @override @@ -791,7 +832,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String contacts_groupAlreadyExists(String name) { - return 'Grupa \"$name\" już istnieje'; + return 'Grupa \"$name\" już istnieje'; } @override @@ -799,57 +840,57 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Brak pasujÄ…cych kontaktów do Twojego filtra'; + 'Brak pasujących kontaktów do Twojego filtra'; @override - String get contacts_noMembers => 'Brak czÅ‚onków'; + String get contacts_noMembers => 'Brak członków'; @override - String get contacts_lastSeenNow => 'Ostatnie połączenie'; + String get contacts_lastSeenNow => 'Ostatnie połączenie'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Ostatnie połączenie $minutes min temu'; + return 'Ostatnie połączenie $minutes min temu'; } @override - String get contacts_lastSeenHourAgo => 'Ostatni raz widziany 1 godzinÄ™ temu'; + String get contacts_lastSeenHourAgo => 'Ostatni raz widziany 1 godzinę temu'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Ostatnie połączenie $hours godzin temu'; + return 'Ostatnie połączenie $hours godzin temu'; } @override - String get contacts_lastSeenDayAgo => 'Ostatni raz widziany 1 dzieÅ„ temu'; + String get contacts_lastSeenDayAgo => 'Ostatni raz widziany 1 dzień temu'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Ostatnie połączenie $days dni temu'; + return 'Ostatnie połączenie $days dni temu'; } @override - String get channels_title => 'KanaÅ‚y'; + String get channels_title => 'Kanały'; @override - String get channels_noChannelsConfigured => 'Brak skonfigurowanych kanałów'; + String get channels_noChannelsConfigured => 'Brak skonfigurowanych kanałów'; @override - String get channels_addPublicChannel => 'Dodaj kanaÅ‚ publiczny'; + String get channels_addPublicChannel => 'Dodaj kanał publiczny'; @override - String get channels_searchChannels => 'Wyszukaj kanaÅ‚y...'; + String get channels_searchChannels => 'Wyszukaj kanały...'; @override - String get channels_noChannelsFound => 'Brak znalezionych kanałów'; + String get channels_noChannelsFound => 'Brak znalezionych kanałów'; @override String channels_channelIndex(int index) { - return 'KanaÅ‚ $index'; + return 'Kanał $index'; } @override - String get channels_hashtagChannel => 'KanaÅ‚ z hashtagami'; + String get channels_hashtagChannel => 'Kanał z hashtagami'; @override String get channels_public => 'Publiczny'; @@ -858,49 +899,49 @@ class AppLocalizationsPl extends AppLocalizations { String get channels_private => 'Prywatne'; @override - String get channels_publicChannel => 'KanaÅ‚ publiczny'; + String get channels_publicChannel => 'Kanał publiczny'; @override - String get channels_privateChannel => 'Prywatny kanaÅ‚'; + String get channels_privateChannel => 'Prywatny kanał'; @override - String get channels_editChannel => 'Edytuj kanaÅ‚'; + String get channels_editChannel => 'Edytuj kanał'; @override - String get channels_muteChannel => 'Wycisz kanaÅ‚'; + String get channels_muteChannel => 'Wycisz kanał'; @override - String get channels_unmuteChannel => 'Wyłącz wyciszenie kanaÅ‚u'; + String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału'; @override - String get channels_deleteChannel => 'UsuÅ„ kanaÅ‚'; + String get channels_deleteChannel => 'Usuń kanał'; @override String channels_deleteChannelConfirm(String name) { - return 'UsuÅ„ \"$name\"? Nie można tego cofnąć.'; + return 'Usuń \"$name\"? Nie można tego cofnąć.'; } @override String channels_channelDeleteFailed(String name) { - return 'Nie udaÅ‚o siÄ™ usunąć kanaÅ‚u \"$name\"'; + return 'Nie udało się usunąć kanału \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'KanaÅ‚ \"$name\" usuniÄ™to'; + return 'Kanał \"$name\" usunięto'; } @override - String get channels_addChannel => 'Dodaj KanaÅ‚'; + String get channels_addChannel => 'Dodaj Kanał'; @override - String get channels_channelIndexLabel => 'Indeks kanaÅ‚u'; + String get channels_channelIndexLabel => 'Indeks kanału'; @override - String get channels_channelName => 'Nazwa kanaÅ‚u'; + String get channels_channelName => 'Nazwa kanału'; @override - String get channels_usePublicChannel => 'Użyj kanaÅ‚u publicznego'; + String get channels_usePublicChannel => 'Użyj kanału publicznego'; @override String get channels_standardPublicPsk => 'Standard public PSK'; @@ -912,19 +953,19 @@ class AppLocalizationsPl extends AppLocalizations { String get channels_generateRandomPsk => 'Wygeneruj losowy klucz PSK'; @override - String get channels_enterChannelName => 'ProszÄ™ podać nazwÄ™ kanaÅ‚u.'; + String get channels_enterChannelName => 'Proszę podać nazwę kanału.'; @override - String get channels_pskMustBe32Hex => 'PSK musi mieć 32 znaki szesnastkowe.'; + String get channels_pskMustBe32Hex => 'PSK musi mieć 32 znaki szesnastkowe.'; @override String channels_channelAdded(String name) { - return 'KanaÅ‚ \"$name\" dodany'; + return 'Kanał \"$name\" dodany'; } @override String channels_editChannelTitle(int index) { - return 'Edytuj KanaÅ‚ $index'; + return 'Edytuj Kanał $index'; } @override @@ -932,77 +973,76 @@ class AppLocalizationsPl extends AppLocalizations { @override String channels_channelUpdated(String name) { - return 'KanaÅ‚ \"$name\" zostaÅ‚ zaktualizowany'; + return 'Kanał \"$name\" został zaktualizowany'; } @override - String get channels_publicChannelAdded => 'KanaÅ‚ publiczny dodany'; + String get channels_publicChannelAdded => 'Kanał publiczny dodany'; @override String get channels_sortBy => 'Sortuj po'; @override - String get channels_sortManual => 'RÄ™czna'; + String get channels_sortManual => 'Ręczna'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Najnowsze wiadomoÅ›ci'; + String get channels_sortLatestMessages => 'Najnowsze wiadomości'; @override - String get channels_sortUnread => 'NiezgÅ‚oszone'; + String get channels_sortUnread => 'Niezgłoszone'; @override - String get channels_createPrivateChannel => 'Utwórz Prywatny KanaÅ‚'; + String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał'; @override String get channels_createPrivateChannelDesc => 'Zabezpieczone kluczem szyfrowym.'; @override - String get channels_joinPrivateChannel => 'Dołącz do Prywatnego KanaÅ‚u'; + String get channels_joinPrivateChannel => 'Dołącz do Prywatnego Kanału'; @override - String get channels_joinPrivateChannelDesc => - 'RÄ™cznie wprowadź klucz tajny.'; + String get channels_joinPrivateChannelDesc => 'Ręcznie wprowadź klucz tajny.'; @override - String get channels_joinPublicChannel => 'Dołącz do kanaÅ‚u publicznego.'; + String get channels_joinPublicChannel => 'Dołącz do kanału publicznego.'; @override String get channels_joinPublicChannelDesc => - 'Każdy może dołączyć do tego kanaÅ‚u.'; + 'Każdy może dołączyć do tego kanału.'; @override String get channels_joinHashtagChannel => - 'Dołącz do kanaÅ‚u oznaczanego hashtagiem'; + 'Dołącz do kanału oznaczanego hashtagiem'; @override String get channels_joinHashtagChannelDesc => - 'Każdy może dołączyć do kanałów z hashtagami.'; + 'Każdy może dołączyć do kanałów z hashtagami.'; @override String get channels_scanQrCode => 'Skanuj kod QR'; @override - String get channels_scanQrCodeComingSoon => 'Wkrótce'; + String get channels_scanQrCodeComingSoon => 'Wkrótce'; @override - String get channels_enterHashtag => 'Wprowadź hashtag'; + String get channels_enterHashtag => 'Wprowadź hashtag'; @override - String get channels_hashtagHint => 'np. #zespół'; + String get channels_hashtagHint => 'np. #zespół'; @override - String get chat_noMessages => 'Brak jeszcze wiadomoÅ›ci'; + String get chat_noMessages => 'Brak jeszcze wiadomości'; @override - String get chat_sendMessageToStart => 'WyÅ›lij wiadomość, aby rozpocząć.'; + String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.'; @override String get chat_originalMessageNotFound => - 'Błąd: Nie znaleziono oryginalnego komunikatu'; + 'Błąd: Nie znaleziono oryginalnego komunikatu'; @override String chat_replyingTo(String name) { @@ -1019,39 +1059,39 @@ class AppLocalizationsPl extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'WyÅ›lij wiadomość do $contactName'; + return 'Wyślij wiadomość do $contactName'; } @override - String get chat_typeMessage => 'Wpisz wiadomość...'; + String get chat_typeMessage => 'Wpisz wiadomość...'; @override String chat_messageTooLong(int maxBytes) { - return 'Wiadomość jest za dÅ‚uga (maksymalnie $maxBytes bajtów).'; + return 'Wiadomość jest za długa (maksymalnie $maxBytes bajtów).'; } @override - String get chat_messageCopied => 'Wiadomość skopiowana'; + String get chat_messageCopied => 'Wiadomość skopiowana'; @override - String get chat_messageDeleted => 'Wiadomość usuniÄ™ta'; + String get chat_messageDeleted => 'Wiadomość usunięta'; @override - String get chat_retryingMessage => 'Próba ponowienia'; + String get chat_retryingMessage => 'Próba ponowienia'; @override String chat_retryCount(int current, int max) { - return 'Spróbuj $current/$max'; + return 'Spróbuj $current/$max'; } @override - String get chat_sendGif => 'WyÅ›lij GIF'; + String get chat_sendGif => 'Wyślij GIF'; @override String get chat_reply => 'Odpowiedz'; @override - String get chat_addReaction => 'Dodaj ReakcjÄ™'; + String get chat_addReaction => 'Dodaj Reakcję'; @override String get chat_me => 'Ja'; @@ -1078,28 +1118,28 @@ class AppLocalizationsPl extends AppLocalizations { String get gifPicker_poweredBy => 'Zasilane przez GIPHY'; @override - String get gifPicker_noGifsFound => 'Nie znaleziono GIF-ów'; + String get gifPicker_noGifsFound => 'Nie znaleziono GIF-ów'; @override - String get gifPicker_failedLoad => 'Nie udaÅ‚o siÄ™ zaÅ‚adować GIF-ów'; + String get gifPicker_failedLoad => 'Nie udało się załadować GIF-ów'; @override - String get gifPicker_failedSearch => 'Nie udaÅ‚o siÄ™ znaleźć GIF-ów'; + String get gifPicker_failedSearch => 'Nie udało się znaleźć GIF-ów'; @override - String get gifPicker_noInternet => 'Brak połączenia internetowego'; + String get gifPicker_noInternet => 'Brak połączenia internetowego'; @override String get debugLog_appTitle => 'Log Wykonywania Aplikacji'; @override - String get debugLog_bleTitle => 'Log błędów BLE'; + String get debugLog_bleTitle => 'Log błędów BLE'; @override String get debugLog_copyLog => 'Kopiuj log'; @override - String get debugLog_clearLog => 'Wyczyść dziennik'; + String get debugLog_clearLog => 'Wyczyść dziennik'; @override String get debugLog_copied => 'Skopiowano dziennik debugowania'; @@ -1108,12 +1148,11 @@ class AppLocalizationsPl extends AppLocalizations { String get debugLog_bleCopied => 'Skopiowany log BLE'; @override - String get debugLog_noEntries => - 'Nie ma jeszcze żadnych logów debugowania.'; + String get debugLog_noEntries => 'Nie ma jeszcze żadnych logów debugowania.'; @override String get debugLog_enableInSettings => - 'Włącz logowanie debugowania aplikacji w ustawieniach'; + 'Włącz logowanie debugowania aplikacji w ustawieniach'; @override String get debugLog_frames => 'Ramy'; @@ -1122,11 +1161,11 @@ class AppLocalizationsPl extends AppLocalizations { String get debugLog_rawLogRx => 'Surowe Log-RX'; @override - String get debugLog_noBleActivity => 'Brak aktywnoÅ›ci BLE jeszcze.'; + String get debugLog_noBleActivity => 'Brak aktywności BLE jeszcze.'; @override String debugFrame_length(int count) { - return 'DÅ‚ugość ramy: $count bajtów'; + return 'Długość ramy: $count bajtów'; } @override @@ -1135,7 +1174,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => 'Wiadomość tekstowa:'; + String get debugFrame_textMessageHeader => 'Wiadomość tekstowa:'; @override String debugFrame_destinationPubKey(String pubKey) { @@ -1169,31 +1208,30 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get debugFrame_hexDump => 'WyjÅ›cie SzESZCZNULNE:'; + String get debugFrame_hexDump => 'Wyjście SzESZCZNULNE:'; @override - String get chat_pathManagement => 'ZarzÄ…dzanie Å›cieżkami'; + String get chat_pathManagement => 'Zarządzanie ścieżkami'; @override - String get chat_ShowAllPaths => 'Pokaż wszystkie Å›cieżki'; + String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki'; @override String get chat_routingMode => 'Tryb routingu'; @override - String get chat_autoUseSavedPath => - 'Automatyczne (użyj zapisanej Å›cieżki)'; + String get chat_autoUseSavedPath => 'Automatyczne (użyj zapisanej ścieżki)'; @override String get chat_forceFloodMode => 'Wymusz Tryb Powodowany'; @override String get chat_recentAckPaths => - 'Ostatnie Å›cieżki ACK (naciÅ›nij, aby użyć):'; + 'Ostatnie ścieżki ACK (naciśnij, aby użyć):'; @override String get chat_pathHistoryFull => - 'Historia Å›cieżek jest peÅ‚na. UsuÅ„ wpisy, aby dodać nowe.'; + 'Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.'; @override String get chat_hopSingular => 'Skacz'; @@ -1216,46 +1254,46 @@ class AppLocalizationsPl extends AppLocalizations { String get chat_successes => 'Sukcesy'; @override - String get chat_removePath => 'UsuÅ„ Å›cieżkÄ™'; + String get chat_removePath => 'Usuń ścieżkę'; @override String get chat_noPathHistoryYet => - 'Brak jeszcze historii Å›cieżek.\nWyÅ›lij wiadomość, aby odkryć Å›cieżki.'; + 'Brak jeszcze historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.'; @override - String get chat_pathActions => 'DziaÅ‚ania Å›cieżki:'; + String get chat_pathActions => 'Działania ścieżki:'; @override - String get chat_setCustomPath => 'Ustaw ÅšcieżkÄ™ DostosowanÄ…'; + String get chat_setCustomPath => 'Ustaw Ścieżkę Dostosowaną'; @override - String get chat_setCustomPathSubtitle => 'RÄ™cznie okreÅ›l trasÄ™.'; + String get chat_setCustomPathSubtitle => 'Ręcznie określ trasę.'; @override - String get chat_clearPath => 'Wyczyść ÅšcieżkÄ™'; + String get chat_clearPath => 'Wyczyść Ścieżkę'; @override String get chat_clearPathSubtitle => - 'Zmusz do ponownej identyfikacji przy nastÄ™pnym wysÅ‚aniu'; + 'Zmusz do ponownej identyfikacji przy następnym wysłaniu'; @override String get chat_pathCleared => - 'Åšcieżka oczyszczona. Kolejne powiadomienie odnajdzie trasÄ™.'; + 'Ścieżka oczyszczona. Kolejne powiadomienie odnajdzie trasę.'; @override String get chat_floodModeSubtitle => - 'Użyj przełącznika routingu w pasku narzÄ™dzi.'; + 'Użyj przełącznika routingu w pasku narzędzi.'; @override String get chat_floodModeEnabled => - 'Tryb powodziowy włączony. Włącz ponownie za pomocÄ… ikony routingu w pasku narzÄ™dzi.'; + 'Tryb powodziowy włączony. Włącz ponownie za pomocą ikony routingu w pasku narzędzi.'; @override - String get chat_fullPath => 'PeÅ‚na Å›cieżka'; + String get chat_fullPath => 'Pełna ścieżka'; @override String get chat_pathDetailsNotAvailable => - 'Szczegóły Å›cieżki jeszcze niedostÄ™pne. Spróbuj wysÅ‚ać wiadomość, aby odÅ›wieżyć.'; + 'Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1265,78 +1303,77 @@ class AppLocalizationsPl extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Åšcieżka ustawiona: $hopCount $_temp0 - $status'; + return 'Ścieżka ustawiona: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Zapisano lokalnie. Połącz siÄ™, aby zsynchronizować.'; + 'Zapisano lokalnie. Połącz się, aby zsynchronizować.'; @override - String get chat_pathDeviceConfirmed => 'UrzÄ…dzenie potwierdzone.'; + String get chat_pathDeviceConfirmed => 'Urządzenie potwierdzone.'; @override String get chat_pathDeviceNotConfirmed => - 'UrzÄ…dzenie nie zostaÅ‚o jeszcze potwierdzone.'; + 'Urządzenie nie zostało jeszcze potwierdzone.'; @override - String get chat_type => 'Wprowadź'; + String get chat_type => 'Wprowadź'; @override - String get chat_path => 'Åšcieżka'; + String get chat_path => 'Ścieżka'; @override String get chat_publicKey => 'Klucz Publiczny'; @override - String get chat_compressOutgoingMessages => - 'Kompresuj wychodzÄ…ce wiadomoÅ›ci'; + String get chat_compressOutgoingMessages => 'Kompresuj wychodzące wiadomości'; @override - String get chat_floodForced => 'Powodowana Powódź'; + String get chat_floodForced => 'Powodowana Powódź'; @override - String get chat_directForced => 'BezpoÅ›rednio (wymuszono)'; + String get chat_directForced => 'Bezpośrednio (wymuszono)'; @override String chat_hopsForced(int count) { - return '$count skoków (wymuszonych)'; + return '$count skoków (wymuszonych)'; } @override String get chat_floodAuto => 'Powodzie (automatyczne)'; @override - String get chat_direct => 'BezpoÅ›rednio'; + String get chat_direct => 'Bezpośrednio'; @override - String get chat_poiShared => 'Wspólny POI'; + String get chat_poiShared => 'Wspólny POI'; @override String chat_unread(int count) { - return 'NiezgÅ‚oszone: $count'; + return 'Niezgłoszone: $count'; } @override - String get chat_openLink => 'Otworzyć link?'; + String get chat_openLink => 'Otworzyć link?'; @override String get chat_openLinkConfirmation => - 'Czy chcesz otworzyć ten link w przeglÄ…darce?'; + 'Czy chcesz otworzyć ten link w przeglądarce?'; @override - String get chat_open => 'Otwórz'; + String get chat_open => 'Otwórz'; @override String chat_couldNotOpenLink(String url) { - return 'Nie można otworzyć linku: $url'; + return 'Nie można otworzyć linku: $url'; } @override - String get chat_invalidLink => 'NieprawidÅ‚owy format linku'; + String get chat_invalidLink => 'Nieprawidłowy format linku'; @override - String get map_title => 'Mapa wÄ™złów'; + String get map_title => 'Mapa węzłów'; @override String get map_lineOfSight => 'Linia wzroku'; @@ -1345,16 +1382,15 @@ class AppLocalizationsPl extends AppLocalizations { String get map_losScreenTitle => 'Linia wzroku'; @override - String get map_noNodesWithLocation => - 'Brak wÄ™złów z danymi lokalizacyjnymi'; + String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi'; @override String get map_nodesNeedGps => - 'WÄ™zÅ‚y muszÄ… udostÄ™pniać swoje współrzÄ™dne GPS,\naby pojawić siÄ™ na mapie.'; + 'Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.'; @override String map_nodesCount(int count) { - return 'WÄ™zÅ‚y: $count'; + return 'Węzły: $count'; } @override @@ -1366,10 +1402,10 @@ class AppLocalizationsPl extends AppLocalizations { String get map_chat => 'Rozmowa'; @override - String get map_repeater => 'Powtórzacz'; + String get map_repeater => 'Powtórzacz'; @override - String get map_room => 'Pokój'; + String get map_room => 'Pokój'; @override String get map_sensor => 'Czujnik'; @@ -1388,64 +1424,64 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_disconnectConfirm => - 'Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?'; + 'Czy na pewno chcesz się odłączyć od tego urządzenia?'; @override String get map_from => 'Od'; @override - String get map_source => 'ŹródÅ‚o'; + String get map_source => 'Źródło'; @override String get map_flags => 'Flagi'; @override - String get map_shareMarkerHere => 'UdostÄ™pnij znacznik tutaj'; + String get map_shareMarkerHere => 'Udostępnij znacznik tutaj'; @override - String get map_pinLabel => 'Oznacz etykietÄ™'; + String get map_pinLabel => 'Oznacz etykietę'; @override String get map_label => 'Etykieta'; @override - String get map_pointOfInterest => 'Punkt zainteresowaÅ„'; + String get map_pointOfInterest => 'Punkt zainteresowań'; @override - String get map_sendToContact => 'WyÅ›lij do kontaktu'; + String get map_sendToContact => 'Wyślij do kontaktu'; @override - String get map_sendToChannel => 'WyÅ›lij do kanaÅ‚u'; + String get map_sendToChannel => 'Wyślij do kanału'; @override - String get map_noChannelsAvailable => 'Brak dostÄ™pnych kanałów'; + String get map_noChannelsAvailable => 'Brak dostępnych kanałów'; @override - String get map_publicLocationShare => 'UdostÄ™pnij lokalizacjÄ™ publicznie'; + String get map_publicLocationShare => 'Udostępnij lokalizację publicznie'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Wkrótce udostÄ™pnisz lokalizacjÄ™ w $channelLabel. Ten kanaÅ‚ jest publiczny i każdy z PSK może go zobaczyć.'; + return 'Wkrótce udostępnisz lokalizację w $channelLabel. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.'; } @override String get map_connectToShareMarkers => - 'Połącz siÄ™ z urzÄ…dzeniem, aby udostÄ™pniać znacznik.'; + 'Połącz się z urządzeniem, aby udostępniać znacznik.'; @override - String get map_filterNodes => 'Filtruj WÄ™zÅ‚y'; + String get map_filterNodes => 'Filtruj Węzły'; @override - String get map_nodeTypes => 'Typy wÄ™złów'; + String get map_nodeTypes => 'Typy węzłów'; @override - String get map_chatNodes => 'WÄ™zÅ‚y czatu'; + String get map_chatNodes => 'Węzły czatu'; @override String get map_repeaters => 'Powtarzacze'; @override - String get map_otherNodes => 'Inne wÄ™zÅ‚y'; + String get map_otherNodes => 'Inne węzły'; @override String get map_keyPrefix => 'Prefiks klucza'; @@ -1454,13 +1490,13 @@ class AppLocalizationsPl extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtruj po prefiksie klucza'; @override - String get map_publicKeyPrefix => 'Przewód klucza publicznego'; + String get map_publicKeyPrefix => 'Przewód klucza publicznego'; @override String get map_markers => 'Oznaczarki'; @override - String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; + String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; @override String get map_lastSeenTime => 'Ostatni raz widiany'; @@ -1469,40 +1505,40 @@ class AppLocalizationsPl extends AppLocalizations { String get map_sharedPin => 'Podzielony PIN'; @override - String get map_joinRoom => 'Dołącz do pokoju'; + String get map_joinRoom => 'Dołącz do pokoju'; @override - String get map_manageRepeater => 'ZarzÄ…dzaj Powtórzami'; + String get map_manageRepeater => 'Zarządzaj Powtórzami'; @override - String get map_tapToAdd => 'Kliknij na wÄ™zÅ‚y, aby dodać je do Å›cieżki.'; + String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.'; @override - String get map_runTrace => 'Uruchom Å›lad Å›cieżki'; + String get map_runTrace => 'Uruchom ślad ścieżki'; @override - String get map_removeLast => 'UsuÅ„ ostatni'; + String get map_removeLast => 'Usuń ostatni'; @override - String get map_pathTraceCancelled => 'Åšledzenie Å›cieżki anulowano.'; + String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.'; @override String get mapCache_title => 'Bufor Map Offline'; @override String get mapCache_selectAreaFirst => - 'Wybierz obszar do wstÄ™pnego pobrania.'; + 'Wybierz obszar do wstępnego pobrania.'; @override String get mapCache_noTilesToDownload => - 'Brak dostÄ™pnych pÅ‚ytek do pobrania dla tego obszaru.'; + 'Brak dostępnych płytek do pobrania dla tego obszaru.'; @override - String get mapCache_downloadTilesTitle => 'Pobierz pÅ‚ytki'; + String get mapCache_downloadTilesTitle => 'Pobierz płytki'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Pobierz $count pÅ‚ytek do użytku offline?'; + return 'Pobierz $count płytek do użytku offline?'; } @override @@ -1510,41 +1546,41 @@ class AppLocalizationsPl extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return 'PamiÄ™tanych $count pÅ‚ytek'; + return 'Pamiętanych $count płytek'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'PamiÄ™tane $downloaded pÅ‚ytki ($failed nieudane)'; + return 'Pamiętane $downloaded płytki ($failed nieudane)'; } @override String get mapCache_clearOfflineCacheTitle => - 'Wyczyść pamięć podrÄ™cznÄ… offline'; + 'Wyczyść pamięć podręczną offline'; @override String get mapCache_clearOfflineCachePrompt => - 'UsuÅ„ wszystkie tymczasowe kafelki mapy?'; + 'Usuń wszystkie tymczasowe kafelki mapy?'; @override String get mapCache_offlineCacheCleared => - 'Pamięć podrÄ™czna offline zostaÅ‚a wyczyszczona'; + 'Pamięć podręczna offline została wyczyszczona'; @override - String get mapCache_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; + String get mapCache_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; @override - String get mapCache_cacheArea => 'Obszar pamiÄ™ci podrÄ™cznej'; + String get mapCache_cacheArea => 'Obszar pamięci podręcznej'; @override - String get mapCache_useCurrentView => 'Użyj aktualnego widoku'; + String get mapCache_useCurrentView => 'Użyj aktualnego widoku'; @override - String get mapCache_zoomRange => 'Zakres powiÄ™kszenia'; + String get mapCache_zoomRange => 'Zakres powiększenia'; @override String mapCache_estimatedTiles(int count) { - return 'Szacunkowa liczba pÅ‚ytek: $count'; + return 'Szacunkowa liczba płytek: $count'; } @override @@ -1556,7 +1592,7 @@ class AppLocalizationsPl extends AppLocalizations { String get mapCache_downloadTilesButton => 'Pobierz Paski'; @override - String get mapCache_clearCacheButton => 'Wyczyść pamięć podrÄ™cznÄ…'; + String get mapCache_clearCacheButton => 'Wyczyść pamięć podręczną'; @override String mapCache_failedDownloads(int count) { @@ -1574,7 +1610,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get time_justNow => 'WÅ‚aÅ›nie teraz'; + String get time_justNow => 'Właśnie teraz'; @override String time_minutesAgo(int minutes) { @@ -1598,19 +1634,19 @@ class AppLocalizationsPl extends AppLocalizations { String get time_hours => 'godziny'; @override - String get time_day => 'dzieÅ„'; + String get time_day => 'dzień'; @override String get time_days => 'dni'; @override - String get time_week => 'tydzieÅ„'; + String get time_week => 'tydzień'; @override String get time_weeks => 'tygodnie'; @override - String get time_month => 'miesiÄ…c'; + String get time_month => 'miesiąc'; @override String get time_months => 'miesiace'; @@ -1622,38 +1658,38 @@ class AppLocalizationsPl extends AppLocalizations { String get time_allTime => 'Wszystko czasowo'; @override - String get dialog_disconnect => 'Odłącz'; + String get dialog_disconnect => 'Odłącz'; @override String get dialog_disconnectConfirm => - 'Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?'; + 'Czy na pewno chcesz się odłączyć od tego urządzenia?'; @override - String get login_repeaterLogin => 'Powtórz Logowanie'; + String get login_repeaterLogin => 'Powtórz Logowanie'; @override String get login_roomLogin => 'Logowanie do pokoju'; @override - String get login_password => 'HasÅ‚o'; + String get login_password => 'Hasło'; @override - String get login_enterPassword => 'Wprowadź hasÅ‚o'; + String get login_enterPassword => 'Wprowadź hasło'; @override - String get login_savePassword => 'Zapisz hasÅ‚o'; + String get login_savePassword => 'Zapisz hasło'; @override String get login_savePasswordSubtitle => - 'HasÅ‚o bÄ™dzie bezpiecznie przechowywane na tym urzÄ…dzeniu.'; + 'Hasło będzie bezpiecznie przechowywane na tym urządzeniu.'; @override String get login_repeaterDescription => - 'Wprowadź hasÅ‚o do powtarzacza, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.'; + 'Wprowadź hasło do powtarzacza, aby uzyskać dostęp do ustawień i statusu.'; @override String get login_roomDescription => - 'Wprowadź hasÅ‚o do pokoju, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.'; + 'Wprowadź hasło do pokoju, aby uzyskać dostęp do ustawień i statusu.'; @override String get login_routing => 'Przekierowanie'; @@ -1662,41 +1698,40 @@ class AppLocalizationsPl extends AppLocalizations { String get login_routingMode => 'Tryb routingu'; @override - String get login_autoUseSavedPath => - 'Automatycznie (użyj zapisanej Å›cieżki)'; + String get login_autoUseSavedPath => 'Automatycznie (użyj zapisanej ścieżki)'; @override String get login_forceFloodMode => 'Wymusz Tryb Powodowany'; @override - String get login_managePaths => 'ZarzÄ…dzaj Åšcieżkami'; + String get login_managePaths => 'Zarządzaj Ścieżkami'; @override - String get login_login => 'Zaloguj siÄ™'; + String get login_login => 'Zaloguj się'; @override String login_attempt(int current, int max) { - return 'Próba $current/$max'; + return 'Próba $current/$max'; } @override String login_failed(String error) { - return 'Zalogowanie siÄ™ nie powiodÅ‚o: $error'; + return 'Zalogowanie się nie powiodło: $error'; } @override String get login_failedMessage => - 'Logowanie nie powiodÅ‚o siÄ™. HasÅ‚o jest nieprawidÅ‚owe albo repeater jest nieosiÄ…galny.'; + 'Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.'; @override - String get common_reload => 'Ponownie zaÅ‚adować'; + String get common_reload => 'Ponownie załadować'; @override - String get common_clear => 'Wyczyść'; + String get common_clear => 'Wyczyść'; @override String path_currentPath(String path) { - return 'Aktualny Å›cieżka: $path'; + return 'Aktualny ścieżka: $path'; } @override @@ -1707,88 +1742,88 @@ class AppLocalizationsPl extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Użyj Å›cieżki $count $_temp0.'; + return 'Użyj ścieżki $count $_temp0.'; } @override - String get path_enterCustomPath => 'Wprowadź wÅ‚asnÄ… Å›cieżkÄ™'; + String get path_enterCustomPath => 'Wprowadź własną ścieżkę'; @override - String get path_currentPathLabel => 'Aktualny Å›cieżka'; + String get path_currentPathLabel => 'Aktualny ścieżka'; @override String get path_hexPrefixInstructions => - 'Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.'; + 'Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (każedy wÄ™zeÅ‚ używa pierwszego bajtu swojego klucza publicznego)'; + 'A1,F2,3C (każedy węzeł używa pierwszego bajtu swojego klucza publicznego)'; @override - String get path_labelHexPrefixes => 'Åšcieżka (przesuniÄ™cia bitowe)'; + String get path_labelHexPrefixes => 'Ścieżka (przesunięcia bitowe)'; @override String get path_helperMaxHops => - 'Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).'; + 'Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).'; @override - String get path_selectFromContacts => 'Albo wybierz z kontaktów:'; + String get path_selectFromContacts => 'Albo wybierz z kontaktów:'; @override String get path_noRepeatersFound => - 'Nie znaleziono repeaterów ani serwerów pokoi.'; + 'Nie znaleziono repeaterów ani serwerów pokoi.'; @override String get path_customPathsRequire => - 'Dostosowane Å›cieżki wymagajÄ… poÅ›rednich skoków, które mogÄ… przekazywać wiadomoÅ›ci.'; + 'Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'NieprawidÅ‚owe prefiksy szesnastkowe: $prefixes'; + return 'Nieprawidłowe prefiksy szesnastkowe: $prefixes'; } @override String get path_tooLong => - 'Åšcieżka jest zbyt dÅ‚uga. Dozwolonych skoków wynosi 64.'; + 'Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.'; @override - String get path_setPath => 'Ustaw ÅšcieżkÄ™'; + String get path_setPath => 'Ustaw Ścieżkę'; @override - String get repeater_management => 'ZarzÄ…dzanie Powtórzami'; + String get repeater_management => 'Zarządzanie Powtórzami'; @override - String get room_management => 'ZarzÄ…dzanie Serwerem Pokoju'; + String get room_management => 'Zarządzanie Serwerem Pokoju'; @override - String get repeater_managementTools => 'NarzÄ™dzia ZarzÄ…dzania'; + String get repeater_managementTools => 'Narzędzia Zarządzania'; @override String get repeater_status => 'Status'; @override String get repeater_statusSubtitle => - 'WyÅ›wietl status powtarzacza, statystyki i sÄ…siadów.'; + 'Wyświetl status powtarzacza, statystyki i sąsiadów.'; @override String get repeater_telemetry => 'Telemetry'; @override String get repeater_telemetrySubtitle => - 'WyÅ›wietl dane telemetryczne z czujników i statystyki systemu'; + 'Wyświetl dane telemetryczne z czujników i statystyki systemu'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'WyÅ›lij polecenia do powielacza'; + String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza'; @override - String get repeater_neighbors => 'SÄ…siedzi'; + String get repeater_neighbors => 'Sąsiedzi'; @override String get repeater_neighborsSubtitle => - 'WyÅ›wietl sÄ…siedztwo zerowych hopów.'; + 'Wyświetl sąsiedztwo zerowych hopów.'; @override String get repeater_settings => 'Ustawienia'; @@ -1804,23 +1839,23 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_autoUseSavedPath => - 'Automatycznie (użyj zapisanej Å›cieżki)'; + 'Automatycznie (użyj zapisanej ścieżki)'; @override String get repeater_forceFloodMode => 'Wymusz Tryb Powodowany'; @override - String get repeater_pathManagement => 'ZarzÄ…dzanie Å›cieżkami'; + String get repeater_pathManagement => 'Zarządzanie ścieżkami'; @override - String get repeater_refresh => 'OdÅ›wież'; + String get repeater_refresh => 'Odśwież'; @override - String get repeater_statusRequestTimeout => 'Å»yczenie statusu timed out.'; + String get repeater_statusRequestTimeout => 'Życzenie statusu timed out.'; @override String repeater_errorLoadingStatus(String error) { - return 'Błąd podczas Å‚adowania statusu: $error'; + return 'Błąd podczas ładowania statusu: $error'; } @override @@ -1833,10 +1868,10 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_clockAtLogin => 'Godzina (przy logowaniu)'; @override - String get repeater_uptime => 'DostÄ™pność'; + String get repeater_uptime => 'Dostępność'; @override - String get repeater_queueLength => 'DÅ‚ugość kolejki'; + String get repeater_queueLength => 'Długość kolejki'; @override String get repeater_debugFlags => 'Opcje debugowania'; @@ -1851,7 +1886,7 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_lastSnr => 'Ostatnie SNR'; @override - String get repeater_noiseFloor => 'Poziom Szumów'; + String get repeater_noiseFloor => 'Poziom Szumów'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1860,16 +1895,16 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Statystyki pakietów'; + String get repeater_packetStatistics => 'Statystyki pakietów'; @override - String get repeater_sent => 'WysÅ‚ane'; + String get repeater_sent => 'Wysłane'; @override String get repeater_received => 'Otrzymano'; @override - String get repeater_duplicates => 'Powtórzenia'; + String get repeater_duplicates => 'Powtórzenia'; @override String repeater_daysHoursMinsSecs( @@ -1883,17 +1918,17 @@ class AppLocalizationsPl extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Razem: $total, Powodzenie: $flood, BezpoÅ›rednio: $direct'; + return 'Razem: $total, Powodzenie: $flood, Bezpośrednio: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Razem: $total, Powodzenie: $flood, BezpoÅ›rednio: $direct'; + return 'Razem: $total, Powodzenie: $flood, Bezpośrednio: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Powodzie: $flood, BezpoÅ›rednie: $direct'; + return 'Powodzie: $flood, Bezpośrednie: $direct'; } @override @@ -1902,34 +1937,34 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Ustawienia Powtórki'; + String get repeater_settingsTitle => 'Ustawienia Powtórki'; @override String get repeater_basicSettings => 'Podstawowe Ustawienia'; @override - String get repeater_repeaterName => 'Nazwa Powtórnika'; + String get repeater_repeaterName => 'Nazwa Powtórnika'; @override - String get repeater_repeaterNameHelper => 'WyÅ›wietl nazwÄ™ tego powtarzacza'; + String get repeater_repeaterNameHelper => 'Wyświetl nazwę tego powtarzacza'; @override - String get repeater_adminPassword => 'HasÅ‚o Administracyjne'; + String get repeater_adminPassword => 'Hasło Administracyjne'; @override - String get repeater_adminPasswordHelper => 'PeÅ‚ny dostÄ™p hasÅ‚o'; + String get repeater_adminPasswordHelper => 'Pełny dostęp hasło'; @override - String get repeater_guestPassword => 'HasÅ‚o goÅ›cia'; + String get repeater_guestPassword => 'Hasło gościa'; @override - String get repeater_guestPasswordHelper => 'DostÄ™p tylko do odczytu hasÅ‚o'; + String get repeater_guestPasswordHelper => 'Dostęp tylko do odczytu hasło'; @override String get repeater_radioSettings => 'Ustawienia radia'; @override - String get repeater_frequencyMhz => 'CzÄ™stotliwość (MHz)'; + String get repeater_frequencyMhz => 'Częstotliwość (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -1941,10 +1976,10 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Przepustowość'; + String get repeater_bandwidth => 'Przepustowość'; @override - String get repeater_spreadingFactor => 'RozkÅ‚ad Czynnika'; + String get repeater_spreadingFactor => 'Rozkład Czynnika'; @override String get repeater_codingRate => 'Stawka kodowania'; @@ -1953,46 +1988,46 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_locationSettings => 'Ustawienia Lokalizacji'; @override - String get repeater_latitude => 'Szerokość'; + String get repeater_latitude => 'Szerokość'; @override - String get repeater_latitudeHelper => 'Stopnie dziesiÄ™tne (np. 37.7749)'; + String get repeater_latitudeHelper => 'Stopnie dziesiętne (np. 37.7749)'; @override - String get repeater_longitude => 'DÅ‚ugość'; + String get repeater_longitude => 'Długość'; @override - String get repeater_longitudeHelper => 'Stopnie dziesiÄ™tne (np. -122,4194)'; + String get repeater_longitudeHelper => 'Stopnie dziesiętne (np. -122,4194)'; @override String get repeater_features => 'Funkcje'; @override - String get repeater_packetForwarding => 'Przekierowanie pakietów'; + String get repeater_packetForwarding => 'Przekierowanie pakietów'; @override String get repeater_packetForwardingSubtitle => - 'Włącz repeater, aby przekazywać pakiety.'; + 'Włącz repeater, aby przekazywać pakiety.'; @override - String get repeater_guestAccess => 'DostÄ™p dla goÅ›ci'; + String get repeater_guestAccess => 'Dostęp dla gości'; @override String get repeater_guestAccessSubtitle => - 'Umożliw dostÄ™p tylko do odczytu dla goÅ›ci.'; + 'Umożliw dostęp tylko do odczytu dla gości.'; @override - String get repeater_privacyMode => 'Tryb PrywatnoÅ›ci'; + String get repeater_privacyMode => 'Tryb Prywatności'; @override String get repeater_privacyModeSubtitle => - 'Ukryj imiÄ™/lokalizacjÄ™ w reklamach'; + 'Ukryj imię/lokalizację w reklamach'; @override String get repeater_advertisementSettings => 'Ustawienia Reklam'; @override - String get repeater_localAdvertInterval => 'InterwaÅ‚ Reklamy Lokalnej'; + String get repeater_localAdvertInterval => 'Interwał Reklamy Lokalnej'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -2000,7 +2035,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get repeater_floodAdvertInterval => 'InterwaÅ‚ Reklamy Powodziowej'; + String get repeater_floodAdvertInterval => 'Interwał Reklamy Powodziowej'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2009,149 +2044,146 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Zaszyfrowany InterwaÅ‚ Reklamowy'; + 'Zaszyfrowany Interwał Reklamowy'; @override - String get repeater_dangerZone => 'Strefa ZagrożeÅ„'; + String get repeater_dangerZone => 'Strefa Zagrożeń'; @override String get repeater_rebootRepeater => 'Zrestartuj Powtarzacz'; @override String get repeater_rebootRepeaterSubtitle => - 'Zrestartuj urzÄ…dzenie powtarzajÄ…ce.'; + 'Zrestartuj urządzenie powtarzające.'; @override String get repeater_rebootRepeaterConfirm => - 'Czy na pewno chcesz zrestartować ten repeater?'; + 'Czy na pewno chcesz zrestartować ten repeater?'; @override - String get repeater_regenerateIdentityKey => 'Wygeneruj klucz tożsamoÅ›ci'; + String get repeater_regenerateIdentityKey => 'Wygeneruj klucz tożsamości'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Wygeneruj nowÄ… parÄ™ kluczy publicznych/prywatnych'; + 'Wygeneruj nową parę kluczy publicznych/prywatnych'; @override String get repeater_regenerateIdentityKeyConfirm => - 'To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?'; + 'To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?'; @override - String get repeater_eraseFileSystem => 'Wyczyść System Plików'; + String get repeater_eraseFileSystem => 'Wyczyść System Plików'; @override String get repeater_eraseFileSystemSubtitle => - 'Sformatuj system plików powielacza'; + 'Sformatuj system plików powielacza'; @override String get repeater_eraseFileSystemConfirm => - 'OSTRZEÅ»ENIE: To spowoduje usuniÄ™cie wszystkich danych z powtarzacza. Nie da siÄ™ tego cofnąć!'; + 'OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z powtarzacza. Nie da się tego cofnąć!'; @override String get repeater_eraseSerialOnly => - 'UsuniÄ™cie jest dostÄ™pne tylko przez konsolÄ™ szeregowÄ….'; + 'Usunięcie jest dostępne tylko przez konsolę szeregową.'; @override String repeater_commandSent(String command) { - return 'Polecenie wysÅ‚ane: $command'; + return 'Polecenie wysłane: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Błąd podczas wysyÅ‚ania polecenia: $error'; + return 'Błąd podczas wysyłania polecenia: $error'; } @override - String get repeater_confirm => 'Potwierdź'; + String get repeater_confirm => 'Potwierdź'; @override - String get repeater_settingsSaved => - 'Ustawienia zostaÅ‚y pomyÅ›lnie zapisane.'; + String get repeater_settingsSaved => 'Ustawienia zostały pomyślnie zapisane.'; @override String repeater_errorSavingSettings(String error) { - return 'Błąd zapisu ustawieÅ„: $error'; + return 'Błąd zapisu ustawień: $error'; } @override - String get repeater_refreshBasicSettings => 'OdÅ›wież Podstawowe Ustawienia'; + String get repeater_refreshBasicSettings => 'Odśwież Podstawowe Ustawienia'; @override - String get repeater_refreshRadioSettings => 'OdÅ›wież Ustawienia Radio'; + String get repeater_refreshRadioSettings => 'Odśwież Ustawienia Radio'; @override - String get repeater_refreshTxPower => 'OdÅ›wież TX power'; + String get repeater_refreshTxPower => 'Odśwież TX power'; @override String get repeater_refreshLocationSettings => - 'OdÅ›wież Ustawienia Lokalizacji'; + 'Odśwież Ustawienia Lokalizacji'; @override - String get repeater_refreshPacketForwarding => - 'OdÅ›wież trasowanie pakietów'; + String get repeater_refreshPacketForwarding => 'Odśwież trasowanie pakietów'; @override - String get repeater_refreshGuestAccess => 'OdÅ›wież dostÄ™p goÅ›cia'; + String get repeater_refreshGuestAccess => 'Odśwież dostęp gościa'; @override - String get repeater_refreshPrivacyMode => 'OdÅ›wież Tryb PrywatnoÅ›ci'; + String get repeater_refreshPrivacyMode => 'Odśwież Tryb Prywatności'; @override String get repeater_refreshAdvertisementSettings => - 'OdÅ›wież Ustawienia Reklamy'; + 'Odśwież Ustawienia Reklamy'; @override String repeater_refreshed(String label) { - return '$label odÅ›wieżone'; + return '$label odświeżone'; } @override String repeater_errorRefreshing(String label) { - return 'Błąd podczas odÅ›wieżania $label'; + return 'Błąd podczas odświeżania $label'; } @override String get repeater_cliTitle => 'Powtarzacz CLI'; @override - String get repeater_debugNextCommand => 'Debug NastÄ™pnÄ… KomendÄ™'; + String get repeater_debugNextCommand => 'Debug Następną Komendę'; @override String get repeater_commandHelp => 'Pomoc'; @override - String get repeater_clearHistory => 'Wyczyść historiÄ™'; + String get repeater_clearHistory => 'Wyczyść historię'; @override - String get repeater_noCommandsSent => - 'Nie wysÅ‚ano jeszcze żadnych poleceÅ„'; + String get repeater_noCommandsSent => 'Nie wysłano jeszcze żadnych poleceń'; @override String get repeater_typeCommandOrUseQuick => - 'Wprowadź polecenie poniżej lub użyj szybkich poleceÅ„'; + 'Wprowadź polecenie poniżej lub użyj szybkich poleceń'; @override - String get repeater_enterCommandHint => 'Wprowadź polecenie...'; + String get repeater_enterCommandHint => 'Wprowadź polecenie...'; @override String get repeater_previousCommand => 'Poprzednia komenda'; @override - String get repeater_nextCommand => 'NastÄ™pna komenda'; + String get repeater_nextCommand => 'Następna komenda'; @override - String get repeater_enterCommandFirst => 'Wprowadź najpierw polecenie'; + String get repeater_enterCommandFirst => 'Wprowadź najpierw polecenie'; @override - String get repeater_cliCommandFrameTitle => 'OkreÅ›lony Wyraz Polecenia CLI'; + String get repeater_cliCommandFrameTitle => 'Określony Wyraz Polecenia CLI'; @override String repeater_cliCommandError(String error) { - return 'Błąd: $error'; + return 'Błąd: $error'; } @override - String get repeater_cliQuickGetName => 'Pobierz imiÄ™'; + String get repeater_cliQuickGetName => 'Pobierz imię'; @override String get repeater_cliQuickGetRadio => 'Uzyskaj Radio'; @@ -2160,7 +2192,7 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_cliQuickGetTx => 'Pobierz TX'; @override - String get repeater_cliQuickNeighbors => 'SÄ…siedzi'; + String get repeater_cliQuickNeighbors => 'Sąsiedzi'; @override String get repeater_cliQuickVersion => 'Wersja'; @@ -2172,81 +2204,81 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_cliQuickClock => 'Godzina'; @override - String get repeater_cliHelpAdvert => 'WysyÅ‚a pakiet reklamowy'; + String get repeater_cliHelpAdvert => 'Wysyła pakiet reklamowy'; @override String get repeater_cliHelpReboot => - 'Zresetuj urzÄ…dzenie. (Uwaga, może pojawić siÄ™ \'Timeout\', co jest normalne)'; + 'Zresetuj urządzenie. (Uwaga, może pojawić się \'Timeout\', co jest normalne)'; @override String get repeater_cliHelpClock => - 'WyÅ›wietla aktualny czas zgodnie z zegarem urzÄ…dzenia.'; + 'Wyświetla aktualny czas zgodnie z zegarem urządzenia.'; @override String get repeater_cliHelpPassword => - 'Ustawia nowe hasÅ‚o administratora dla urzÄ…dzenia.'; + 'Ustawia nowe hasło administratora dla urządzenia.'; @override String get repeater_cliHelpVersion => - 'WyÅ›wietla wersjÄ™ urzÄ…dzenia i datÄ™ budowy oprogramowania.'; + 'Wyświetla wersję urządzenia i datę budowy oprogramowania.'; @override String get repeater_cliHelpClearStats => - 'Resetuje różne wskaźniki statystyk do zera.'; + 'Resetuje różne wskaźniki statystyk do zera.'; @override String get repeater_cliHelpSetAf => 'Ustawia czynnik czasu powietrznego.'; @override String get repeater_cliHelpSetTx => - 'Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)'; + 'Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)'; @override String get repeater_cliHelpSetRepeat => - 'Włącza lub wyłącza rolÄ™ powtarzacza dla tego wÄ™zÅ‚a.'; + 'Włącza lub wyłącza rolę powtarzacza dla tego węzła.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Serwer pokoju) JeÅ›li \'włączone\', to logowanie z pustym hasÅ‚em bÄ™dzie dozwolone, ale nie można publikować w pokoju (tylko czytać).'; + '(Serwer pokoju) Jeśli \'włączone\', to logowanie z pustym hasłem będzie dozwolone, ale nie można publikować w pokoju (tylko czytać).'; @override String get repeater_cliHelpSetFloodMax => - 'Ustawia maksymalnÄ… liczbÄ™ skoków pakietu powrotnego (jeÅ›li >= max, pakiet nie jest przekierowywany)'; + 'Ustawia maksymalną liczbę skoków pakietu powrotnego (jeśli >= max, pakiet nie jest przekierowywany)'; @override String get repeater_cliHelpSetIntThresh => - 'Ustawia Próg Interferencji (w dB). DomyÅ›lnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceÅ„ kanaÅ‚u.'; + 'Ustawia Próg Interferencji (w dB). Domyślnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceń kanału.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Ustawia interwaÅ‚ do zresetowania Automatycznego Sterownika GÅ‚oÅ›noÅ›ci. Ustaw na 0, aby wyłączyć.'; + 'Ustawia interwał do zresetowania Automatycznego Sterownika Głośności. Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetMultiAcks => - 'Włącza lub wyłącza funkcjÄ™ \'podwójnych potwierdzeÅ„\'.'; + 'Włącza lub wyłącza funkcję \'podwójnych potwierdzeń\'.'; @override String get repeater_cliHelpSetAdvertInterval => - 'Ustawia interwaÅ‚ timera w minutach do wysyÅ‚ania pakietu reklamy lokalnej (bezpoÅ›redniej). Ustaw na 0, aby wyłączyć.'; + 'Ustawia interwał timera w minutach do wysyłania pakietu reklamy lokalnej (bezpośredniej). Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Ustawia interwaÅ‚ timera w godzinach do wysÅ‚ania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.'; + 'Ustawia interwał timera w godzinach do wysłania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetGuestPassword => - 'Ustawia/aktualizuje hasÅ‚o goÅ›cia. (dla repeaterów, loginy goÅ›ci mogÄ… wysyÅ‚ać żądanie \"Get Stats\")'; + 'Ustawia/aktualizuje hasło gościa. (dla repeaterów, loginy gości mogą wysyłać żądanie \"Get Stats\")'; @override - String get repeater_cliHelpSetName => 'Ustawia nazwÄ™ reklamy.'; + String get repeater_cliHelpSetName => 'Ustawia nazwę reklamy.'; @override String get repeater_cliHelpSetLat => - 'Ustawia współrzÄ™dnÄ… geograficzne (w stopniach dziesiÄ™tnych) mapy reklam.'; + 'Ustawia współrzędną geograficzne (w stopniach dziesiętnych) mapy reklam.'; @override String get repeater_cliHelpSetLon => - 'Ustawia współrzÄ™dnÄ… dÅ‚ugoÅ›ciowÄ… mapy reklamy. (stopnie dziesiÄ™tne)'; + 'Ustawia współrzędną długościową mapy reklamy. (stopnie dziesiętne)'; @override String get repeater_cliHelpSetRadio => @@ -2254,46 +2286,46 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliHelpSetRxDelay => - 'Ustawienia (eksperymentalne) bazowe (muszÄ… być > 1, aby dziaÅ‚ać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnaÅ‚u/wynik. Ustaw na 0, aby wyłączyć.'; + 'Ustawienia (eksperymentalne) bazowe (muszą być > 1, aby działać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnału/wynik. Ustaw na 0, aby wyłączyć.'; @override String get repeater_cliHelpSetTxDelay => - 'Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyÅ‚anie (zmniejszajÄ…c prawdopodobieÅ„stwo kolizji).'; + 'Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyłanie (zmniejszając prawdopodobieństwo kolizji).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpoÅ›rednim.'; + 'Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpośrednim.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Włącz/Wyłącz mostek.'; + String get repeater_cliHelpSetBridgeEnabled => 'Włącz/Wyłącz mostek.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Ustaw czas opóźnienia przed ponownym wysyÅ‚aniem pakietów.'; + 'Ustaw czas opóźnienia przed ponownym wysyłaniem pakietów.'; @override String get repeater_cliHelpSetBridgeSource => - 'Wybierz, czy most bÄ™dzie ponownie transmitowaÅ‚ otrzymywane pakiety, czy też wysyÅ‚ane.'; + 'Wybierz, czy most będzie ponownie transmitował otrzymywane pakiety, czy też wysyłane.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Ustaw prÄ™dkość transmisji magistrali szeregowej dla mostów rs232.'; + 'Ustaw prędkość transmisji magistrali szeregowej dla mostów rs232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Ustaw sekret dla mostów ESPNOW.'; + 'Ustaw sekret dla mostów ESPNOW.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Ustawia niestandardowy współczynnik do korekty zgÅ‚aszanego napiÄ™cia baterii (obsÅ‚uga tylko na wybranych pÅ‚ytach).'; + 'Ustawia niestandardowy współczynnik do korekty zgłaszanego napięcia baterii (obsługa tylko na wybranych płytach).'; @override String get repeater_cliHelpTempRadio => - 'Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a nastÄ™pnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).'; + 'Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a następnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).'; @override String get repeater_cliHelpSetPerm => - 'Modyfikuje ACL. Usuwa dopasowanÄ… wpis (z prefiksem pubkey), jeÅ›li \"permissions\" wynosi zero. Dodaje nowy wpis, jeÅ›li pubkey-hex ma peÅ‚nÄ… dÅ‚ugość i nie znajduje siÄ™ obecnie w ACL. Aktualizuje wpis, dopasowujÄ…c prefiks pubkey. Bit uprawnieÅ„ zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).'; + 'Modyfikuje ACL. Usuwa dopasowaną wpis (z prefiksem pubkey), jeśli \"permissions\" wynosi zero. Dodaje nowy wpis, jeśli pubkey-hex ma pełną długość i nie znajduje się obecnie w ACL. Aktualizuje wpis, dopasowując prefiks pubkey. Bit uprawnień zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).'; @override String get repeater_cliHelpGetBridgeType => @@ -2301,31 +2333,31 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliHelpLogStart => - 'Rozpoczyna siÄ™ logowanie pakietów do systemu plików.'; + 'Rozpoczyna się logowanie pakietów do systemu plików.'; @override String get repeater_cliHelpLogStop => - 'Zatrzymuje logowanie pakietów do systemu plików.'; + 'Zatrzymuje logowanie pakietów do systemu plików.'; @override String get repeater_cliHelpLogErase => - 'Usuwa logi pakietów z systemu plików.'; + 'Usuwa logi pakietów z systemu plików.'; @override String get repeater_cliHelpNeighbors => - 'WyÅ›wietla listÄ™ innych wÄ™złów powtarzajÄ…cych siÄ™, które usÅ‚yszano dziÄ™ki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4'; + 'Wyświetla listę innych węzłów powtarzających się, które usłyszano dzięki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Usuwa pierwszy pasujÄ…cy wpis (z prefiksem pubkey (hex)) z listy sÄ…siadów.'; + 'Usuwa pierwszy pasujący wpis (z prefiksem pubkey (hex)) z listy sąsiadów.'; @override String get repeater_cliHelpRegion => - '(tylko seria) WyÅ›wietla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.'; + '(tylko seria) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.'; @override String get repeater_cliHelpRegionLoad => - 'ZAPOMNIJ: to jest specjalne wywoÅ‚anie wielokomendowe. Każda nastÄ™pna komenda jest nazwÄ… regionu (wciÄ™ta spacjami, aby wskazywać hierarchiÄ™ nadrzÄ™dnÄ…, z minimum jednÄ… spacjÄ…). ZakoÅ„czona wysÅ‚aniem pustej linii/komendy.'; + 'ZAPOMNIJ: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.'; @override String get repeater_cliHelpRegionGet => @@ -2333,63 +2365,63 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliHelpRegionPut => - 'Dodaje lub aktualizuje definicjÄ™ regionu z podanÄ… nazwÄ….'; + 'Dodaje lub aktualizuje definicję regionu z podaną nazwą.'; @override String get repeater_cliHelpRegionRemove => - 'Usuwa definicjÄ™ regionu o podanej nazwie. (musi siÄ™ dokÅ‚adnie zgadzać i nie może mieć podregionów).'; + 'Usuwa definicję regionu o podanej nazwie. (musi się dokładnie zgadzać i nie może mieć podregionów).'; @override String get repeater_cliHelpRegionAllowf => - 'Ustawia uprawnienia \'P\'Å‚ytkowe dla podanego regionu. (\'\' dla zakresu globalnego/starszego)'; + 'Ustawia uprawnienia \'P\'łytkowe dla podanego regionu. (\'\' dla zakresu globalnego/starszego)'; @override String get repeater_cliHelpRegionDenyf => - 'Usuwa uprawnienie \'PÅ‚ywajÄ…ce\' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca siÄ™ używania tego na globalnym/starszym zakresie!!).'; + 'Usuwa uprawnienie \'Pływające\' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).'; @override String get repeater_cliHelpRegionHome => - 'Odpowiada z aktualnej \'home\' region. (Uwaga: nie zostaÅ‚o jeszcze zastosowane, zarezerwowane na przyszÅ‚ość).'; + 'Odpowiada z aktualnej \'home\' region. (Uwaga: nie zostało jeszcze zastosowane, zarezerwowane na przyszłość).'; @override String get repeater_cliHelpRegionHomeSet => 'Ustawia region \'domowe\'.'; @override String get repeater_cliHelpRegionSave => - 'Zapisuje listÄ™/mapÄ™ regionów do pamiÄ™ci.'; + 'Zapisuje listę/mapę regionów do pamięci.'; @override String get repeater_cliHelpGps => - 'WyÅ›wietla status GPS. JeÅ›li GPS jest wyłączony, odpowiada tylko \"off\", jeÅ›li jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbÄ… satelitów.'; + 'Wyświetla status GPS. Jeśli GPS jest wyłączony, odpowiada tylko \"off\", jeśli jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbą satelitów.'; @override - String get repeater_cliHelpGpsOnOff => 'Włącza/wyłącza nawigacjÄ™ GPS.'; + String get repeater_cliHelpGpsOnOff => 'Włącza/wyłącza nawigację GPS.'; @override String get repeater_cliHelpGpsSync => - 'Synchronizuje czas wÄ™zÅ‚a z zegarem GPS.'; + 'Synchronizuje czas węzła z zegarem GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Ustawia pozycjÄ™ wÄ™zÅ‚a na współrzÄ™dne GPS i zapisuje preferencje.'; + 'Ustawia pozycję węzła na współrzędne GPS i zapisuje preferencje.'; @override String get repeater_cliHelpGpsAdvert => - 'UdostÄ™pnia konfiguracjÄ™ reklamy lokalizacji wÄ™zÅ‚a:\n- brak: nie uwzglÄ™dniaj lokalizacji w reklamach\n- udostÄ™pnia: udostÄ™pnia lokalizacjÄ™ GPS (z SensorManager)\n- ustawienia: reklamuj lokalizacjÄ™ przechowywanÄ… w ustawieniach'; + 'Udostępnia konfigurację reklamy lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w reklamach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: reklamuj lokalizację przechowywaną w ustawieniach'; @override String get repeater_cliHelpGpsAdvertSet => - 'Ustawia konfiguracjÄ™ reklamy w lokalizacji.'; + 'Ustawia konfigurację reklamy w lokalizacji.'; @override - String get repeater_commandsListTitle => 'Lista poleceÅ„'; + String get repeater_commandsListTitle => 'Lista poleceń'; @override String get repeater_commandsListNote => - 'ZAPAMIĘTAJ: dla różnych poleceÅ„ \"set ...\" istnieje również polecenie \"get ...\".'; + 'ZAPAMIĘTAJ: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".'; @override - String get repeater_general => 'Ogólne'; + String get repeater_general => 'Ogólne'; @override String get repeater_settingsCategory => 'Ustawienia'; @@ -2401,48 +2433,48 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_logging => 'Rejestrowanie'; @override - String get repeater_neighborsRepeaterOnly => 'SÄ…siedzi (tylko powtarzacz)'; + String get repeater_neighborsRepeaterOnly => 'Sąsiedzi (tylko powtarzacz)'; @override String get repeater_regionManagementRepeaterOnly => - 'ZarzÄ…dzanie Regionem (tylko Powtarzacz)'; + 'Zarządzanie Regionem (tylko Powtarzacz)'; @override String get repeater_regionNote => - 'Wprowadzono komendy regionalne w celu zarzÄ…dzania definicjami i uprawnieniami regionów.'; + 'Wprowadzono komendy regionalne w celu zarządzania definicjami i uprawnieniami regionów.'; @override - String get repeater_gpsManagement => 'ZarzÄ…dzanie GPS'; + String get repeater_gpsManagement => 'Zarządzanie GPS'; @override String get repeater_gpsNote => - 'Polecenie GPS zostaÅ‚o wprowadzone w celu zarzÄ…dzania tematami zwiÄ…zanymi z lokalizacjÄ….'; + 'Polecenie GPS zostało wprowadzone w celu zarządzania tematami związanymi z lokalizacją.'; @override String get telemetry_receivedData => 'Otrzymano Dane Telemetrii'; @override String get telemetry_requestTimeout => - 'Å»yczenie o danych telemetrycznych nie udaÅ‚o siÄ™.'; + 'Życzenie o danych telemetrycznych nie udało się.'; @override String telemetry_errorLoading(String error) { - return 'Błąd podczas Å‚adowania telemetry: $error'; + return 'Błąd podczas ładowania telemetry: $error'; } @override - String get telemetry_noData => 'Brak dostÄ™pnych danych telemetrycznych.'; + String get telemetry_noData => 'Brak dostępnych danych telemetrycznych.'; @override String telemetry_channelTitle(int channel) { - return 'KanaÅ‚ $channel'; + return 'Kanał $channel'; } @override String get telemetry_batteryLabel => 'Bateria'; @override - String get telemetry_voltageLabel => 'NapiÄ™cie'; + String get telemetry_voltageLabel => 'Napięcie'; @override String get telemetry_mcuTemperatureLabel => 'Temperatura MCU'; @@ -2470,26 +2502,26 @@ class AppLocalizationsPl extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Otrzymano dane sÄ…siedztwa'; + String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa'; @override String get neighbors_requestTimedOut => - 'SÄ…siedzi proszÄ… o wyłączenie timingu.'; + 'Sąsiedzi proszą o wyłączenie timingu.'; @override String neighbors_errorLoading(String error) { - return 'Błąd podczas Å‚adowania sÄ…siadów: $error'; + return 'Błąd podczas ładowania sąsiadów: $error'; } @override - String get neighbors_repeatersNeighbors => 'Powtarzacze SÄ…siedzi'; + String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi'; @override - String get neighbors_noData => 'Brak danych dotyczÄ…cych sÄ…siadów.'; + String get neighbors_noData => 'Brak danych dotyczących sąsiadów.'; @override String neighbors_unknownContact(String pubkey) { @@ -2498,27 +2530,27 @@ class AppLocalizationsPl extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'UsÅ‚yszano: $time temu'; + return 'Usłyszano: $time temu'; } @override - String get channelPath_title => 'Åšcieżka pakietu'; + String get channelPath_title => 'Ścieżka pakietu'; @override - String get channelPath_viewMap => 'WyÅ›wietl mapÄ™'; + String get channelPath_viewMap => 'Wyświetl mapę'; @override - String get channelPath_otherObservedPaths => 'Inne Zauważone Åšcieżki'; + String get channelPath_otherObservedPaths => 'Inne Zauważone Ścieżki'; @override - String get channelPath_repeaterHops => 'Skoki Powtórki'; + String get channelPath_repeaterHops => 'Skoki Powtórki'; @override String get channelPath_noHopDetails => - 'Szczegóły dotyczÄ…ce tego pakietu nie zostaÅ‚y podane.'; + 'Szczegóły dotyczące tego pakietu nie zostały podane.'; @override - String get channelPath_messageDetails => 'Szczegóły wiadomoÅ›ci'; + String get channelPath_messageDetails => 'Szczegóły wiadomości'; @override String get channelPath_senderLabel => 'Nadawca'; @@ -2527,11 +2559,11 @@ class AppLocalizationsPl extends AppLocalizations { String get channelPath_timeLabel => 'Czas'; @override - String get channelPath_repeatsLabel => 'Powtórzenia'; + String get channelPath_repeatsLabel => 'Powtórzenia'; @override String channelPath_pathLabel(int index) { - return 'Åšcieżka $index'; + return 'Ścieżka $index'; } @override @@ -2539,7 +2571,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Obserwowany Å›cieżka $index • $hops'; + return 'Obserwowany ścieżka $index • $hops'; } @override @@ -2562,159 +2594,158 @@ class AppLocalizationsPl extends AppLocalizations { String get channelPath_floodPath => 'Powodzenie'; @override - String get channelPath_directPath => 'BezpoÅ›rednio'; + String get channelPath_directPath => 'Bezpośrednio'; @override String channelPath_observedZeroOf(int total) { - return '0 z $total skoków'; + return '0 z $total skoków'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed z $total skoków'; + return '$observed z $total skoków'; } @override - String get channelPath_mapTitle => 'Mapa Å›cieżek'; + String get channelPath_mapTitle => 'Mapa ścieżek'; @override String get channelPath_noRepeaterLocations => - 'Brak dostÄ™pnych lokalizacji powtarzaczy dla tego Å›cieżki.'; + 'Brak dostępnych lokalizacji powtarzaczy dla tego ścieżki.'; @override String channelPath_primaryPath(int index) { - return 'Åšcieżka $index (Główna)'; + return 'Ścieżka $index (Główna)'; } @override - String get channelPath_pathLabelTitle => 'Åšcieżka'; + String get channelPath_pathLabelTitle => 'Ścieżka'; @override - String get channelPath_observedPathHeader => 'Obserwowana Å›cieżka'; + String get channelPath_observedPathHeader => 'Obserwowana ścieżka'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Brak dostÄ™pnych szczegółów hopa dla tego pakietu.'; + 'Brak dostępnych szczegółów hopa dla tego pakietu.'; @override String get channelPath_unknownRepeater => 'Nieznany Powtarzacz'; @override - String get community_title => 'SpoÅ‚eczność'; + String get community_title => 'Społeczność'; @override - String get community_create => 'Utwórz SpoÅ‚eczność'; + String get community_create => 'Utwórz Społeczność'; @override String get community_createDesc => - 'Utwórz nowÄ… spoÅ‚eczność i udostÄ™pnij za pomocÄ… kodu QR.'; + 'Utwórz nową społeczność i udostępnij za pomocą kodu QR.'; @override - String get community_join => 'Dołącz'; + String get community_join => 'Dołącz'; @override - String get community_joinTitle => 'Dołącz do spoÅ‚ecznoÅ›ci'; + String get community_joinTitle => 'Dołącz do społeczności'; @override String community_joinConfirmation(String name) { - return 'Czy chcesz dołączyć do spoÅ‚ecznoÅ›ci \"$name\"?'; + return 'Czy chcesz dołączyć do społeczności \"$name\"?'; } @override - String get community_scanQr => 'Skanuj QR kod spoÅ‚ecznoÅ›ci'; + String get community_scanQr => 'Skanuj QR kod społeczności'; @override String get community_scanInstructions => - 'Skieruj kamerÄ™ w kierunku kodu QR spoÅ‚ecznoÅ›ci.'; + 'Skieruj kamerę w kierunku kodu QR społeczności.'; @override - String get community_showQr => 'Pokaż kod QR'; + String get community_showQr => 'Pokaż kod QR'; @override - String get community_publicChannel => 'SpoÅ‚eczność Publiczna'; + String get community_publicChannel => 'Społeczność Publiczna'; @override - String get community_hashtagChannel => 'Hashtag SpoÅ‚ecznoÅ›ci'; + String get community_hashtagChannel => 'Hashtag Społeczności'; @override - String get community_name => 'Nazwa SpoÅ‚ecznoÅ›ci'; + String get community_name => 'Nazwa Społeczności'; @override - String get community_enterName => 'Wprowadź nazwÄ™ spoÅ‚ecznoÅ›ci'; + String get community_enterName => 'Wprowadź nazwę społeczności'; @override String community_created(String name) { - return 'SpoÅ‚eczność \"$name\" zostaÅ‚a utworzona'; + return 'Społeczność \"$name\" została utworzona'; } @override String community_joined(String name) { - return 'DołączyÅ‚ do spoÅ‚ecznoÅ›ci \"$name\"'; + return 'Dołączył do społeczności \"$name\"'; } @override - String get community_qrTitle => 'Dziel siÄ™ SpoÅ‚ecznoÅ›ciÄ…'; + String get community_qrTitle => 'Dziel się Społecznością'; @override String community_qrInstructions(String name) { - return 'Skanuj ten kod QR, aby dołączyć $name'; + return 'Skanuj ten kod QR, aby dołączyć $name'; } @override String get community_hashtagPrivacyHint => - 'KanaÅ‚y hashtagowe spoÅ‚ecznoÅ›ci sÄ… dostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci'; + 'Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności'; @override - String get community_invalidQrCode => 'NieprawidÅ‚owy kod QR spoÅ‚ecznoÅ›ci.'; + String get community_invalidQrCode => 'Nieprawidłowy kod QR społeczności.'; @override - String get community_alreadyMember => 'Już jesteÅ› czÅ‚onkiem.'; + String get community_alreadyMember => 'Już jesteś członkiem.'; @override String community_alreadyMemberMessage(String name) { - return 'JesteÅ› już czÅ‚onkiem \"$name\".'; + return 'Jesteś już członkiem \"$name\".'; } @override - String get community_addPublicChannel => - 'Dodaj KanaÅ‚ Publiczny SpoÅ‚ecznoÅ›ci'; + String get community_addPublicChannel => 'Dodaj Kanał Publiczny Społeczności'; @override String get community_addPublicChannelHint => - 'Automatycznie dodaj kanaÅ‚ publiczny dla tej spoÅ‚ecznoÅ›ci.'; + 'Automatycznie dodaj kanał publiczny dla tej społeczności.'; @override String get community_noCommunities => - 'Nie dołączono jeszcze żadnych spoÅ‚ecznoÅ›ci.'; + 'Nie dołączono jeszcze żadnych społeczności.'; @override String get community_scanOrCreate => - 'Skanuj kod QR lub utwórz spoÅ‚eczność, aby zacząć.'; + 'Skanuj kod QR lub utwórz społeczność, aby zacząć.'; @override - String get community_manageCommunities => 'ZarzÄ…dzaj Grupami'; + String get community_manageCommunities => 'Zarządzaj Grupami'; @override - String get community_delete => 'Opuszczenie SpoÅ‚ecznoÅ›ci'; + String get community_delete => 'Opuszczenie Społeczności'; @override String community_deleteConfirm(String name) { - return 'OpuÅ›cić \"$name\"?'; + return 'Opuścić \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Spowoduje to również usuniÄ™cie $count kanaÅ‚u/kanałów i ich wiadomoÅ›ci.'; + return 'Spowoduje to również usunięcie $count kanału/kanałów i ich wiadomości.'; } @override String community_deleted(String name) { - return 'Opuszczono spoÅ‚eczność \"$name\"'; + return 'Opuszczono społeczność \"$name\"'; } @override @@ -2722,7 +2753,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy czÅ‚onkowie bÄ™dÄ… musieli zeskanować nowy kod QR, aby kontynuować komunikacjÄ™.'; + return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.'; } @override @@ -2730,7 +2761,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'HasÅ‚o ponownie wygenerowane dla \"$name\"'; + return 'Hasło ponownie wygenerowane dla \"$name\"'; } @override @@ -2738,37 +2769,37 @@ class AppLocalizationsPl extends AppLocalizations { @override String community_secretUpdated(String name) { - return 'HasÅ‚o zaktualizowane dla \"$name\"'; + return 'Hasło zaktualizowane dla \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"'; + return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"'; } @override - String get community_addHashtagChannel => 'Dodaj hashtag spoÅ‚ecznoÅ›ci'; + String get community_addHashtagChannel => 'Dodaj hashtag społeczności'; @override String get community_addHashtagChannelDesc => - 'Dodaj kanaÅ‚ z hashtagiem dla tej spoÅ‚ecznoÅ›ci'; + 'Dodaj kanał z hashtagiem dla tej społeczności'; @override - String get community_selectCommunity => 'Wybierz spoÅ‚eczność'; + String get community_selectCommunity => 'Wybierz społeczność'; @override String get community_regularHashtag => 'Hashtag regular'; @override String get community_regularHashtagDesc => - 'Publiczny hashtag (każdy może dołączyć)'; + 'Publiczny hashtag (każdy może dołączyć)'; @override - String get community_communityHashtag => 'Hashtag SpoÅ‚ecznoÅ›ci'; + String get community_communityHashtag => 'Hashtag Społeczności'; @override String get community_communityHashtagDesc => - 'DostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci'; + 'Dostępne tylko dla członków społeczności'; @override String community_forCommunity(String name) { @@ -2782,10 +2813,10 @@ class AppLocalizationsPl extends AppLocalizations { String get listFilter_sortBy => 'Sortuj po'; @override - String get listFilter_latestMessages => 'Najnowsze wiadomoÅ›ci'; + String get listFilter_latestMessages => 'Najnowsze wiadomości'; @override - String get listFilter_heardRecently => 'SÅ‚yszano niedawno'; + String get listFilter_heardRecently => 'Słyszano niedawno'; @override String get listFilter_az => 'A-Z'; @@ -2803,10 +2834,10 @@ class AppLocalizationsPl extends AppLocalizations { String get listFilter_addToFavorites => 'Dodaj do ulubionych'; @override - String get listFilter_removeFromFavorites => 'UsuÅ„ z ulubionych'; + String get listFilter_removeFromFavorites => 'Usuń z ulubionych'; @override - String get listFilter_users => 'Użytkownicy'; + String get listFilter_users => 'Użytkownicy'; @override String get listFilter_repeaters => 'Powtarzacze'; @@ -2824,46 +2855,45 @@ class AppLocalizationsPl extends AppLocalizations { String get pathTrace_you => 'Ty'; @override - String get pathTrace_failed => 'Åšledzenie Å›cieżki nie powiodÅ‚o siÄ™.'; + String get pathTrace_failed => 'Śledzenie ścieżki nie powiodło się.'; @override - String get pathTrace_notAvailable => 'Åšcieżka Å›ledzenia niedostÄ™pna.'; + String get pathTrace_notAvailable => 'Ścieżka śledzenia niedostępna.'; @override - String get pathTrace_refreshTooltip => 'OdÅ›wież Å›cieżkÄ™.'; + String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.'; @override String get pathTrace_someHopsNoLocation => - 'Jeden lub wiÄ™cej z chmieli nie ma okreÅ›lonej lokalizacji!'; + 'Jeden lub więcej z chmieli nie ma określonej lokalizacji!'; @override - String get pathTrace_clearTooltip => 'Wyczyść Å›cieżkÄ™'; + String get pathTrace_clearTooltip => 'Wyczyść ścieżkę'; @override - String get losSelectStartEnd => - 'Wybierz wÄ™zÅ‚y poczÄ…tkowe i koÅ„cowe dla LOS.'; + String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.'; @override String losRunFailed(String error) { - return 'Sprawdzenie pola widzenia nie powiodÅ‚o siÄ™: $error'; + return 'Sprawdzenie pola widzenia nie powiodło się: $error'; } @override - String get losClearAllPoints => 'Wyczyść wszystkie punkty'; + String get losClearAllPoints => 'Wyczyść wszystkie punkty'; @override String get losRunToViewElevationProfile => - 'Uruchom LOS, aby wyÅ›wietlić profil wysokoÅ›ci'; + 'Uruchom LOS, aby wyświetlić profil wysokości'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Stuknij wÄ™zÅ‚y lub naciÅ›nij i przytrzymaj mapÄ™, aby uzyskać niestandardowe punkty'; + 'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty'; @override - String get losShowDisplayNodes => 'Pokaż wÄ™zÅ‚y wyÅ›wietlajÄ…ce'; + String get losShowDisplayNodes => 'Pokaż węzły wyświetlające'; @override String get losCustomPoints => 'Punkty niestandardowe'; @@ -2893,7 +2923,7 @@ class AppLocalizationsPl extends AppLocalizations { String get losRun => 'Uruchom LOS-a'; @override - String get losNoElevationData => 'Brak danych o wysokoÅ›ci'; + String get losNoElevationData => 'Brak danych o wysokości'; @override String losProfileClear( @@ -2902,7 +2932,7 @@ class AppLocalizationsPl extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, czysty LOS, minimalny przeÅ›wit $clearance $heightUnit'; + return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit'; } @override @@ -2928,42 +2958,42 @@ class AppLocalizationsPl extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Dane dotyczÄ…ce wysokoÅ›ci sÄ… niedostÄ™pne dla jednej lub wiÄ™kszej liczby próbek.'; + 'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.'; @override String get losErrorInvalidInput => - 'NieprawidÅ‚owe dane punktów/wysokoÅ›ci do obliczenia LOS.'; + 'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.'; @override - String get losRenameCustomPoint => 'ZmieÅ„ nazwÄ™ punktu niestandardowego'; + String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego'; @override String get losPointName => 'Nazwa punktu'; @override - String get losShowPanelTooltip => 'Pokaż panel LOS'; + String get losShowPanelTooltip => 'Pokaż panel LOS'; @override String get losHidePanelTooltip => 'Ukryj panel LOS'; @override String get losElevationAttribution => - 'Dane dotyczÄ…ce wysokoÅ›ci: Open-Meteo (CC BY 4.0)'; + 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Horyzont radiowy'; @override - String get losLegendLosBeam => 'Linia widocznoÅ›ci'; + String get losLegendLosBeam => 'Linia widoczności'; @override String get losLegendTerrain => 'Teren'; @override - String get losFrequencyLabel => 'CzÄ™stotliwość'; + String get losFrequencyLabel => 'Częstotliwość'; @override - String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia'; + String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia'; @override String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego'; @@ -2975,48 +3005,48 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'ZaczynajÄ…c od k=$baselineK przy $baselineFreq MHz, obliczenia korygujÄ… współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; + return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; } @override - String get contacts_pathTrace => 'Åšledzenie Åšcieżek'; + String get contacts_pathTrace => 'Śledzenie Ścieżek'; @override - String get contacts_ping => 'Pingować'; + String get contacts_ping => 'Pingować'; @override - String get contacts_repeaterPathTrace => 'Åšledzenie Å›cieżki do repeatera'; + String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera'; @override String get contacts_repeaterPing => 'Repeater pingowy'; @override String get contacts_roomPathTrace => - 'Åšledzenie Å›cieżki do serwera pokojowego'; + 'Śledzenie ścieżki do serwera pokojowego'; @override String get contacts_roomPing => 'Pinguj serwer pokoju'; @override - String get contacts_chatTraceRoute => 'Åšledź trasÄ™ promienia'; + String get contacts_chatTraceRoute => 'Śledź trasę promienia'; @override String contacts_pathTraceTo(String name) { - return 'Åšledź trasÄ™ do $name'; + return 'Śledź trasę do $name'; } @override String get contacts_clipboardEmpty => 'Schowek jest pusty.'; @override - String get contacts_invalidAdvertFormat => 'NieprawidÅ‚owe dane kontaktowe'; + String get contacts_invalidAdvertFormat => 'Nieprawidłowe dane kontaktowe'; @override - String get contacts_contactImported => 'Kontakt zostaÅ‚ zaimportowany.'; + String get contacts_contactImported => 'Kontakt został zaimportowany.'; @override String get contacts_contactImportFailed => - 'Kontakt nie zostaÅ‚ zaimportowany.'; + 'Kontakt nie został zaimportowany.'; @override String get contacts_zeroHopAdvert => 'Reklama Zero Hop'; @@ -3025,7 +3055,7 @@ class AppLocalizationsPl extends AppLocalizations { String get contacts_floodAdvert => 'Reklama powodziowa'; @override - String get contacts_copyAdvertToClipboard => 'Kopiuj ogÅ‚oszenie do schowka'; + String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka'; @override String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka'; @@ -3035,35 +3065,35 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_ShareContactZeroHop => - 'UdostÄ™pnij kontakt przez ogÅ‚oszenie'; + 'Udostępnij kontakt przez ogłoszenie'; @override String get contacts_zeroHopContactAdvertSent => - 'WysÅ‚ano kontakt przez ogÅ‚oszenie.'; + 'Wysłano kontakt przez ogłoszenie.'; @override String get contacts_zeroHopContactAdvertFailed => - 'Nie udaÅ‚o siÄ™ wysÅ‚ać kontaktu.'; + 'Nie udało się wysłać kontaktu.'; @override String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.'; @override String get contacts_contactAdvertCopyFailed => - 'Kopiowanie ogÅ‚oszenia do schowka nie powiodÅ‚o siÄ™.'; + 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; @override - String get notification_activityTitle => 'Aktywność MeshCore'; + String get notification_activityTitle => 'Aktywność MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'wiadomoÅ›ci', - many: 'wiadomoÅ›ci', - few: 'wiadomoÅ›ci', - one: 'wiadomość', + other: 'wiadomości', + many: 'wiadomości', + few: 'wiadomości', + one: 'wiadomość', ); return '$count $_temp0'; } @@ -3073,10 +3103,10 @@ class AppLocalizationsPl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'wiadomoÅ›ci kanaÅ‚u', - many: 'wiadomoÅ›ci kanaÅ‚u', - few: 'wiadomoÅ›ci kanaÅ‚u', - one: 'wiadomość kanaÅ‚u', + other: 'wiadomości kanału', + many: 'wiadomości kanału', + few: 'wiadomości kanału', + one: 'wiadomość kanału', ); return '$count $_temp0'; } @@ -3086,10 +3116,10 @@ class AppLocalizationsPl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'nowych wÄ™złów', - many: 'nowych wÄ™złów', - few: 'nowe wÄ™zÅ‚y', - one: 'nowy wÄ™zeÅ‚', + other: 'nowych węzłów', + many: 'nowych węzłów', + few: 'nowe węzły', + one: 'nowy węzeł', ); return '$count $_temp0'; } @@ -3100,55 +3130,53 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get notification_receivedNewMessage => 'Otrzymano nowÄ… wiadomość'; + String get notification_receivedNewMessage => 'Otrzymano nową wiadomość'; @override String get settings_gpxExportRepeaters => - 'Eksportuj powtórki / serwer pokojowy do GPX'; + 'Eksportuj powtórki / serwer pokojowy do GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Eksportuje powtarzacze / roomserver z lokalizacjÄ… do pliku GPX.'; + 'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.'; @override String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Eksportuje towarzyszy z lokalizacjÄ… do pliku GPX.'; + 'Eksportuje towarzyszy z lokalizacją do pliku GPX.'; @override String get settings_gpxExportAll => 'Eksportuj wszystkie kontakty do GPX'; @override String get settings_gpxExportAllSubtitle => - 'Eksportuje wszystkie kontakty z lokalizacjÄ… do pliku GPX.'; + 'Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.'; @override - String get settings_gpxExportSuccess => 'PomyÅ›lnie wyeksportowano plik GPX.'; + String get settings_gpxExportSuccess => 'Pomyślnie wyeksportowano plik GPX.'; @override String get settings_gpxExportNoContacts => - 'Brak kontaktów do wyeksportowania.'; + 'Brak kontaktów do wyeksportowania.'; @override String get settings_gpxExportNotAvailable => - 'Nie obsÅ‚ugiwane na Twoim urzÄ…dzeniu/systemie operacyjnym'; + 'Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym'; @override - String get settings_gpxExportError => - 'WystÄ…piÅ‚ błąd podczas eksportowania.'; + String get settings_gpxExportError => 'Wystąpił błąd podczas eksportowania.'; @override String get settings_gpxExportRepeatersRoom => - 'Lokalizacje serwerów powtarzajÄ…cych i pomieszczeÅ„'; + 'Lokalizacje serwerów powtarzających i pomieszczeń'; @override String get settings_gpxExportChat => 'Lokalizacje towarzyszy'; @override - String get settings_gpxExportAllContacts => - 'Wszystkie lokalizacje kontaktów'; + String get settings_gpxExportAllContacts => 'Wszystkie lokalizacje kontaktów'; @override String get settings_gpxExportShareText => @@ -3159,7 +3187,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Eksport danych mapy GPX meshcore-open'; @override - String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu'; + String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu'; @override String get snrIndicator_lastSeen => 'Ostatnio widziany'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 2a2f9c2..a767807 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -48,7 +48,7 @@ class AppLocalizationsPt extends AppLocalizations { String get common_add => 'Adicionar'; @override - String get common_settings => 'Configurações'; + String get common_settings => 'Configurações'; @override String get common_disconnect => 'Desconectar'; @@ -93,7 +93,7 @@ class AppLocalizationsPt extends AppLocalizations { String get common_loading => 'Carregando...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -119,19 +119,63 @@ class AppLocalizationsPt extends AppLocalizations { @override String get usbScreenSubtitle => - 'Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.'; + 'Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.'; @override String get usbScreenStatus => 'Selecione um dispositivo USB'; @override String get usbScreenNote => - 'A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.'; + 'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.'; @override String get usbScreenEmptyState => 'Nenhum dispositivo USB encontrado. Conecte um e atualize.'; + @override + String get usbErrorPermissionDenied => + 'A permissão para acesso via USB foi negada.'; + + @override + String get usbErrorDeviceMissing => + 'O dispositivo USB selecionado não está mais disponível.'; + + @override + String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.'; + + @override + String get usbErrorBusy => + 'Já existe uma solicitação de conexão USB em andamento.'; + + @override + String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.'; + + @override + String get usbErrorOpenFailed => + 'Não foi possível abrir o dispositivo USB selecionado.'; + + @override + String get usbErrorConnectFailed => + 'Não foi possível conectar ao dispositivo USB selecionado.'; + + @override + String get usbErrorUnsupported => + 'A comunicação serial via USB não é suportada nesta plataforma.'; + + @override + String get usbErrorAlreadyActive => 'A conexão USB já está ativa.'; + + @override + String get usbErrorNoDeviceSelected => + 'Nenhum dispositivo USB foi selecionado.'; + + @override + String get usbErrorPortClosed => 'A conexão USB não está ativa.'; + + @override + String get usbErrorConnectTimedOut => + 'Tempo limite aguardando a resposta do dispositivo.'; + @override String get scanner_scanning => 'Procurando por dispositivos...'; @@ -142,7 +186,7 @@ class AppLocalizationsPt extends AppLocalizations { String get scanner_disconnecting => 'Desconectando...'; @override - String get scanner_notConnected => 'Não está conectado'; + String get scanner_notConnected => 'Não está conectado'; @override String scanner_connectedTo(String deviceName) { @@ -158,7 +202,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String scanner_connectionFailed(String error) { - return 'Falha na conexão: $error'; + return 'Falha na conexão: $error'; } @override @@ -168,18 +212,18 @@ class AppLocalizationsPt extends AppLocalizations { String get scanner_scan => 'Digitalizar'; @override - String get scanner_bluetoothOff => 'Bluetooth está desativado'; + String get scanner_bluetoothOff => 'Bluetooth está desativado'; @override String get scanner_bluetoothOffMessage => 'Por favor, ative o Bluetooth para escanear por dispositivos.'; @override - String get scanner_chromeRequired => 'Navegador Chrome necessário'; + String get scanner_chromeRequired => 'Navegador Chrome necessário'; @override String get scanner_chromeRequiredMessage => - 'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.'; + 'Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.'; @override String get scanner_enableBluetooth => 'Ative o Bluetooth'; @@ -191,66 +235,66 @@ class AppLocalizationsPt extends AppLocalizations { String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Configurações'; + String get settings_title => 'Configurações'; @override - String get settings_deviceInfo => 'Informações do Dispositivo'; + String get settings_deviceInfo => 'Informações do Dispositivo'; @override - String get settings_appSettings => 'Configurações do App'; + String get settings_appSettings => 'Configurações do App'; @override String get settings_appSettingsSubtitle => - 'Notificações, mensagens e preferências de mapa'; + 'Notificações, mensagens e preferências de mapa'; @override - String get settings_nodeSettings => 'Configurações do Nó'; + String get settings_nodeSettings => 'Configurações do Nó'; @override - String get settings_nodeName => 'Nome do Nó'; + String get settings_nodeName => 'Nome do Nó'; @override - String get settings_nodeNameNotSet => 'Não definido'; + String get settings_nodeNameNotSet => 'Não definido'; @override - String get settings_nodeNameHint => 'Insira o nome do nó'; + String get settings_nodeNameHint => 'Insira o nome do nó'; @override String get settings_nodeNameUpdated => 'Nome atualizado'; @override - String get settings_radioSettings => 'Configurações de Rádio'; + String get settings_radioSettings => 'Configurações de Rádio'; @override String get settings_radioSettingsSubtitle => - 'Frequência, potência, fator de espalhamento'; + 'Frequência, potência, fator de espalhamento'; @override String get settings_radioSettingsUpdated => - 'Configurações de rádio atualizadas'; + 'Configurações de rádio atualizadas'; @override - String get settings_location => 'Localização'; + String get settings_location => 'Localização'; @override String get settings_locationSubtitle => 'Coordenadas GPS'; @override - String get settings_locationUpdated => 'Localização atualizada'; + String get settings_locationUpdated => 'Localização atualizada'; @override String get settings_locationBothRequired => 'Insira a latitude e a longitude.'; @override - String get settings_locationInvalid => 'Latitude ou longitude inválidos.'; + String get settings_locationInvalid => 'Latitude ou longitude inválidos.'; @override String get settings_locationGPSEnable => 'Ativar GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Habilita a atualização automática da localização via GPS.'; + 'Habilita a atualização automática da localização via GPS.'; @override String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)'; @@ -270,11 +314,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_privacyModeSubtitle => - 'Esconder nome/localização em anúncios'; + 'Esconder nome/localização em anúncios'; @override String get settings_privacyModeToggle => - 'Ative o modo de privacidade para ocultar seu nome e localização em anúncios.'; + 'Ative o modo de privacidade para ocultar seu nome e localização em anúncios.'; @override String get settings_privacyModeEnabled => 'Modo de privacidade ativado'; @@ -283,24 +327,24 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_privacyModeDisabled => 'Modo de privacidade desativado'; @override - String get settings_actions => 'Ações'; + String get settings_actions => 'Ações'; @override String get settings_sendAdvertisement => 'Enviar Publicidade'; @override String get settings_sendAdvertisementSubtitle => - 'Presença de transmissão agora'; + 'Presença de transmissão agora'; @override - String get settings_advertisementSent => 'Anúncio enviado'; + String get settings_advertisementSent => 'Anúncio enviado'; @override - String get settings_syncTime => 'Tempo de Sincronização'; + String get settings_syncTime => 'Tempo de Sincronização'; @override String get settings_syncTimeSubtitle => - 'Definir o relógio do dispositivo para o horário do telefone'; + 'Definir o relógio do dispositivo para o horário do telefone'; @override String get settings_timeSynchronized => 'Sincronizado com o tempo'; @@ -321,24 +365,24 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - 'Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.'; + 'Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.'; @override String get settings_debug => 'Depurar'; @override - String get settings_bleDebugLog => 'Log de Depuração BLE'; + String get settings_bleDebugLog => 'Log de Depuração BLE'; @override String get settings_bleDebugLogSubtitle => 'Comandos, respostas e dados brutos do BLE'; @override - String get settings_appDebugLog => 'Log de Depuração do Aplicativo'; + String get settings_appDebugLog => 'Log de Depuração do Aplicativo'; @override String get settings_appDebugLogSubtitle => - 'Mensagens de depuração do aplicativo'; + 'Mensagens de depuração do aplicativo'; @override String get settings_about => 'Sobre'; @@ -349,16 +393,15 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get settings_aboutLegalese => - 'Projeto MeshCore de Código Aberto 2024'; + String get settings_aboutLegalese => 'Projeto MeshCore de Código Aberto 2024'; @override String get settings_aboutDescription => - 'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.'; + 'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Dados de elevação LOS: Open-Meteo (CC BY 4.0)'; + 'Dados de elevação LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Nome'; @@ -373,47 +416,46 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_infoBattery => 'Bateria'; @override - String get settings_infoPublicKey => 'Chave Pública'; + String get settings_infoPublicKey => 'Chave Pública'; @override - String get settings_infoContactsCount => 'Número de Contatos'; + String get settings_infoContactsCount => 'Número de Contatos'; @override - String get settings_infoChannelCount => 'Número do Canal'; + String get settings_infoChannelCount => 'Número do Canal'; @override String get settings_presets => 'Presets'; @override - String get settings_frequency => 'Frequência (MHz)'; + String get settings_frequency => 'Frequência (MHz)'; @override String get settings_frequencyHelper => '300,0 - 2500,0'; @override - String get settings_frequencyInvalid => - 'Frequência inválida (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Frequência inválida (300-2500 MHz)'; @override String get settings_bandwidth => 'Largura de banda'; @override - String get settings_spreadingFactor => 'Fator de Dispersão'; + String get settings_spreadingFactor => 'Fator de Dispersão'; @override - String get settings_codingRate => 'Taxa de Codificação'; + String get settings_codingRate => 'Taxa de Codificação'; @override - String get settings_txPower => 'TX Potência (dBm)'; + String get settings_txPower => 'TX Potência (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)'; + String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)'; @override - String get settings_clientRepeat => 'Repetição sem rede'; + String get settings_clientRepeat => 'Repetição sem rede'; @override String get settings_clientRepeatSubtitle => @@ -421,7 +463,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_clientRepeatFreqWarning => - 'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.'; + 'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { @@ -429,16 +471,16 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get appSettings_title => 'Configurações do App'; + String get appSettings_title => 'Configurações do App'; @override - String get appSettings_appearance => 'Aparência'; + String get appSettings_appearance => 'Aparência'; @override String get appSettings_theme => 'Tema'; @override - String get appSettings_themeSystem => 'Padrão do sistema'; + String get appSettings_themeSystem => 'Padrão do sistema'; @override String get appSettings_themeLight => 'Luz'; @@ -450,16 +492,16 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_language => 'Idioma'; @override - String get appSettings_languageSystem => 'Padrão do sistema'; + String get appSettings_languageSystem => 'Padrão do sistema'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -468,16 +510,16 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -486,10 +528,10 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Russo'; @@ -506,87 +548,87 @@ class AppLocalizationsPt extends AppLocalizations { 'Mostrar metadados detalhados de roteamento e tempo para as mensagens'; @override - String get appSettings_notifications => 'Notificações'; + String get appSettings_notifications => 'Notificações'; @override - String get appSettings_enableNotifications => 'Ativar Notificações'; + String get appSettings_enableNotifications => 'Ativar Notificações'; @override String get appSettings_enableNotificationsSubtitle => - 'Receber notificações para mensagens e anúncios'; + 'Receber notificações para mensagens e anúncios'; @override String get appSettings_notificationPermissionDenied => - 'Permissão de notificação negada'; + 'Permissão de notificação negada'; @override - String get appSettings_notificationsEnabled => 'Notificações ativadas'; + String get appSettings_notificationsEnabled => 'Notificações ativadas'; @override - String get appSettings_notificationsDisabled => 'Notificações desativadas'; + String get appSettings_notificationsDisabled => 'Notificações desativadas'; @override - String get appSettings_messageNotifications => 'Notificações de Mensagem'; + String get appSettings_messageNotifications => 'Notificações de Mensagem'; @override String get appSettings_messageNotificationsSubtitle => - 'Mostrar notificação ao receber novas mensagens'; + 'Mostrar notificação ao receber novas mensagens'; @override String get appSettings_channelMessageNotifications => - 'Notificações de Mensagens do Canal'; + 'Notificações de Mensagens do Canal'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Mostrar notificação ao receber mensagens do canal'; + 'Mostrar notificação ao receber mensagens do canal'; @override String get appSettings_advertisementNotifications => - 'Notificações de Anúncios'; + 'Notificações de Anúncios'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Mostrar notificação quando novos nós forem descobertos'; + 'Mostrar notificação quando novos nós forem descobertos'; @override String get appSettings_messaging => 'Mensagens'; @override String get appSettings_clearPathOnMaxRetry => - 'Limpar Caminho em Tentativas Máximas'; + 'Limpar Caminho em Tentativas Máximas'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Redefinir o caminho de contato após 5 tentativas de envio falhas'; + 'Redefinir o caminho de contato após 5 tentativas de envio falhas'; @override String get appSettings_pathsWillBeCleared => - 'Os caminhos serão limpos após 5 tentativas falhas.'; + 'Os caminhos serão limpos após 5 tentativas falhas.'; @override String get appSettings_pathsWillNotBeCleared => - 'Os caminhos não serão limpos automaticamente.'; + 'Os caminhos não serão limpos automaticamente.'; @override - String get appSettings_autoRouteRotation => 'Rotação de Rota Automática'; + String get appSettings_autoRouteRotation => 'Rotação de Rota Automática'; @override String get appSettings_autoRouteRotationSubtitle => - 'Alternar entre os melhores caminhos e o modo inundação'; + 'Alternar entre os melhores caminhos e o modo inundação'; @override String get appSettings_autoRouteRotationEnabled => - 'Rotação de roteamento automático habilitada'; + 'Rotação de roteamento automático habilitada'; @override String get appSettings_autoRouteRotationDisabled => - 'Rotação de roteamento automático desativada'; + 'Rotação de roteamento automático desativada'; @override String get appSettings_battery => 'Bateria'; @override - String get appSettings_batteryChemistry => 'Química da Bateria'; + String get appSettings_batteryChemistry => 'Química da Bateria'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { @@ -607,37 +649,37 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override - String get appSettings_mapDisplay => 'Exibição do Mapa'; + String get appSettings_mapDisplay => 'Exibição do Mapa'; @override String get appSettings_showRepeaters => 'Mostrar Repetidores'; @override String get appSettings_showRepeatersSubtitle => - 'Exibir nós de repetidor no mapa'; + 'Exibir nós de repetidor no mapa'; @override - String get appSettings_showChatNodes => 'Mostrar Nós de Chat'; + String get appSettings_showChatNodes => 'Mostrar Nós de Chat'; @override - String get appSettings_showChatNodesSubtitle => 'Exibir nós de chat no mapa'; + String get appSettings_showChatNodesSubtitle => 'Exibir nós de chat no mapa'; @override - String get appSettings_showOtherNodes => 'Mostrar Outros Nós'; + String get appSettings_showOtherNodes => 'Mostrar Outros Nós'; @override String get appSettings_showOtherNodesSubtitle => - 'Exibir outros tipos de nó no mapa'; + 'Exibir outros tipos de nó no mapa'; @override String get appSettings_timeFilter => 'Filtro de Tempo'; @override - String get appSettings_timeFilterShowAll => 'Mostrar todos os nós'; + String get appSettings_timeFilterShowAll => 'Mostrar todos os nós'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Mostrar nós das últimas $hours horas'; + return 'Mostrar nós das últimas $hours horas'; } @override @@ -645,22 +687,22 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_showNodesDiscoveredWithin => - 'Mostrar nós descobertos dentro de:'; + 'Mostrar nós descobertos dentro de:'; @override String get appSettings_allTime => 'Todos os tempos'; @override - String get appSettings_lastHour => 'Última hora'; + String get appSettings_lastHour => 'Última hora'; @override - String get appSettings_last6Hours => 'Últimos 6 horas'; + String get appSettings_last6Hours => 'Últimos 6 horas'; @override - String get appSettings_last24Hours => 'Últimas 24 horas'; + String get appSettings_last24Hours => 'Últimas 24 horas'; @override - String get appSettings_lastWeek => 'Da última semana'; + String get appSettings_lastWeek => 'Da última semana'; @override String get appSettings_offlineMapCache => 'Cache de Mapa Offline'; @@ -669,17 +711,17 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_unitsTitle => 'Unidades'; @override - String get appSettings_unitsMetric => 'Métrico (m/km)'; + String get appSettings_unitsMetric => 'Métrico (m/km)'; @override String get appSettings_unitsImperial => 'Imperial (ft/mi)'; @override - String get appSettings_noAreaSelected => 'Nenhuma área selecionada'; + String get appSettings_noAreaSelected => 'Nenhuma área selecionada'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Área selecionada (zoom $minZoom-$maxZoom)'; + return 'Área selecionada (zoom $minZoom-$maxZoom)'; } @override @@ -687,32 +729,32 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_appDebugLogging => - 'Rastreamento de Depuração do Aplicativo'; + 'Rastreamento de Depuração do Aplicativo'; @override String get appSettings_appDebugLoggingSubtitle => - 'Registrar mensagens de depuração do aplicativo Log para solucionar problemas'; + 'Registrar mensagens de depuração do aplicativo Log para solucionar problemas'; @override String get appSettings_appDebugLoggingEnabled => - 'Log de depuração do aplicativo habilitado'; + 'Log de depuração do aplicativo habilitado'; @override String get appSettings_appDebugLoggingDisabled => - 'O registro de depuração do aplicativo está desativado.'; + 'O registro de depuração do aplicativo está desativado.'; @override String get contacts_title => 'Contactos'; @override - String get contacts_noContacts => 'Ainda não existem contatos.'; + String get contacts_noContacts => 'Ainda não existem contatos.'; @override String get contacts_contactsWillAppear => - 'Os contatos serão exibidos quando os dispositivos anunciarem.'; + 'Os contatos serão exibidos quando os dispositivos anunciarem.'; @override - String get contacts_unread => 'Não lido'; + String get contacts_unread => 'Não lido'; @override String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...'; @@ -729,7 +771,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String contacts_searchUsers(int number, String str) { - return 'Pesquisar $number$str Usuários...'; + return 'Pesquisar $number$str Usuários...'; } @override @@ -743,11 +785,11 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get contacts_noUnreadContacts => 'Sem contatos não lidos.'; + String get contacts_noUnreadContacts => 'Sem contatos não lidos.'; @override String get contacts_noContactsFound => - 'Não foram encontrados contatos ou grupos.'; + 'Não foram encontrados contatos ou grupos.'; @override String get contacts_deleteContact => 'Excluir Contato'; @@ -787,11 +829,11 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_groupName => 'Nome do grupo'; @override - String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.'; + String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.'; @override String contacts_groupAlreadyExists(String name) { - return 'O grupo \"$name\" já existe'; + return 'O grupo \"$name\" já existe'; } @override @@ -799,46 +841,43 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Não existem contatos que correspondam ao seu filtro'; + 'Não existem contatos que correspondam ao seu filtro'; @override String get contacts_noMembers => 'Nenhum membro'; @override - String get contacts_lastSeenNow => 'Última vez que foi visto agora'; + String get contacts_lastSeenNow => 'Última vez que foi visto agora'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Última vez que foi visto $minutes minutos atrás'; + return 'Última vez que foi visto $minutes minutos atrás'; } @override - String get contacts_lastSeenHourAgo => - 'Última vez que foi visto há 1 hora.'; + String get contacts_lastSeenHourAgo => 'Última vez que foi visto há 1 hora.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Última vez visto $hours horas atrás'; + return 'Última vez visto $hours horas atrás'; } @override - String get contacts_lastSeenDayAgo => - 'Última vez que foi visto 1 dia atrás'; + String get contacts_lastSeenDayAgo => 'Última vez que foi visto 1 dia atrás'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Última vez visto $days dias atrás'; + return 'Última vez visto $days dias atrás'; } @override String get channels_title => 'Canais'; @override - String get channels_noChannelsConfigured => - 'Nenhuma canalização configurada'; + String get channels_noChannelsConfigured => 'Nenhuma canalização configurada'; @override - String get channels_addPublicChannel => 'Adicionar Canal Público'; + String get channels_addPublicChannel => 'Adicionar Canal Público'; @override String get channels_searchChannels => 'Pesquisar canais...'; @@ -855,13 +894,13 @@ class AppLocalizationsPt extends AppLocalizations { String get channels_hashtagChannel => 'Canal com hashtag'; @override - String get channels_public => 'Público'; + String get channels_public => 'Público'; @override String get channels_private => 'Privado'; @override - String get channels_publicChannel => 'Canal público'; + String get channels_publicChannel => 'Canal público'; @override String get channels_privateChannel => 'Canal privado'; @@ -880,7 +919,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return 'Excluir \"$name\"? Não pode ser desfeito.'; + return 'Excluir \"$name\"? Não pode ser desfeito.'; } @override @@ -890,29 +929,29 @@ class AppLocalizationsPt extends AppLocalizations { @override String channels_channelDeleted(String name) { - return 'Canal \"$name\" excluído'; + return 'Canal \"$name\" excluído'; } @override String get channels_addChannel => 'Adicionar Canal'; @override - String get channels_channelIndexLabel => 'Índice do Canal'; + String get channels_channelIndexLabel => 'Índice do Canal'; @override String get channels_channelName => 'Nome do Canal'; @override - String get channels_usePublicChannel => 'Usar Canal Público'; + String get channels_usePublicChannel => 'Usar Canal Público'; @override - String get channels_standardPublicPsk => 'PSK público padrão'; + String get channels_standardPublicPsk => 'PSK público padrão'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Gerar PSK aleatório'; + String get channels_generateRandomPsk => 'Gerar PSK aleatório'; @override String get channels_enterChannelName => 'Por favor, insira um nome de canal'; @@ -932,7 +971,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get channels_smazCompression => 'Compressão SMAZ'; + String get channels_smazCompression => 'Compressão SMAZ'; @override String channels_channelUpdated(String name) { @@ -940,7 +979,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Canal público adicionado'; + String get channels_publicChannelAdded => 'Canal público adicionado'; @override String get channels_sortBy => 'Ordenar por'; @@ -952,10 +991,10 @@ class AppLocalizationsPt extends AppLocalizations { String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Últimas mensagens'; + String get channels_sortLatestMessages => 'Últimas mensagens'; @override - String get channels_sortUnread => 'Não lido'; + String get channels_sortUnread => 'Não lido'; @override String get channels_createPrivateChannel => 'Criar um Canal Privado'; @@ -972,7 +1011,7 @@ class AppLocalizationsPt extends AppLocalizations { 'Inserir uma chave secreta manualmente.'; @override - String get channels_joinPublicChannel => 'Junte-se ao Canal Público'; + String get channels_joinPublicChannel => 'Junte-se ao Canal Público'; @override String get channels_joinPublicChannelDesc => @@ -986,7 +1025,7 @@ class AppLocalizationsPt extends AppLocalizations { 'Qualquer pessoa pode participar de canais com hashtag.'; @override - String get channels_scanQrCode => 'Digitalizar um Código QR'; + String get channels_scanQrCode => 'Digitalizar um Código QR'; @override String get channels_scanQrCodeComingSoon => 'Em breve'; @@ -998,14 +1037,13 @@ class AppLocalizationsPt extends AppLocalizations { String get channels_hashtagHint => 'ex. #equipe'; @override - String get chat_noMessages => 'Ainda não existem mensagens.'; + String get chat_noMessages => 'Ainda não existem mensagens.'; @override - String get chat_sendMessageToStart => 'Enviar uma mensagem para começar'; + String get chat_sendMessageToStart => 'Enviar uma mensagem para começar'; @override - String get chat_originalMessageNotFound => - 'Mensagem original não encontrada'; + String get chat_originalMessageNotFound => 'Mensagem original não encontrada'; @override String chat_replyingTo(String name) { @@ -1018,7 +1056,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get chat_location => 'Localização'; + String get chat_location => 'Localização'; @override String chat_sendMessageTo(String contactName) { @@ -1030,14 +1068,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return 'Mensagem muito longa (máximo $maxBytes bytes).'; + return 'Mensagem muito longa (máximo $maxBytes bytes).'; } @override String get chat_messageCopied => 'Mensagem copiada'; @override - String get chat_messageDeleted => 'Mensagem excluída'; + String get chat_messageDeleted => 'Mensagem excluída'; @override String get chat_retryingMessage => 'Tentando novamente'; @@ -1054,7 +1092,7 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_reply => 'Responder'; @override - String get chat_addReaction => 'Adicionar Reação'; + String get chat_addReaction => 'Adicionar Reação'; @override String get chat_me => 'Eu'; @@ -1066,7 +1104,7 @@ class AppLocalizationsPt extends AppLocalizations { String get emojiCategoryGestures => 'Gestos'; @override - String get emojiCategoryHearts => 'Corações'; + String get emojiCategoryHearts => 'Corações'; @override String get emojiCategoryObjects => 'Objetos'; @@ -1084,19 +1122,19 @@ class AppLocalizationsPt extends AppLocalizations { String get gifPicker_noGifsFound => 'Nenhum GIF encontrado'; @override - String get gifPicker_failedLoad => 'Não foi possível carregar os GIFs'; + String get gifPicker_failedLoad => 'Não foi possível carregar os GIFs'; @override String get gifPicker_failedSearch => 'Falha na pesquisa de GIFs'; @override - String get gifPicker_noInternet => 'Sem conexão com a internet'; + String get gifPicker_noInternet => 'Sem conexão com a internet'; @override - String get debugLog_appTitle => 'Log de Depuração do Aplicativo'; + String get debugLog_appTitle => 'Log de Depuração do Aplicativo'; @override - String get debugLog_bleTitle => 'Log de Depuração BLE'; + String get debugLog_bleTitle => 'Log de Depuração BLE'; @override String get debugLog_copyLog => 'Copiar log'; @@ -1105,17 +1143,17 @@ class AppLocalizationsPt extends AppLocalizations { String get debugLog_clearLog => 'Limpar log'; @override - String get debugLog_copied => 'Log de depuração copiado'; + String get debugLog_copied => 'Log de depuração copiado'; @override String get debugLog_bleCopied => 'Log BLE copiado'; @override - String get debugLog_noEntries => 'Ainda não existem logs de depuração.'; + String get debugLog_noEntries => 'Ainda não existem logs de depuração.'; @override String get debugLog_enableInSettings => - 'Ativar o log de depuração do aplicativo nas configurações'; + 'Ativar o log de depuração do aplicativo nas configurações'; @override String get debugLog_frames => 'Estruturas'; @@ -1124,7 +1162,7 @@ class AppLocalizationsPt extends AppLocalizations { String get debugLog_rawLogRx => 'Log Raw-RX'; @override - String get debugLog_noBleActivity => 'Ainda não há atividade BLE.'; + String get debugLog_noBleActivity => 'Ainda não há atividade BLE.'; @override String debugFrame_length(int count) { @@ -1171,7 +1209,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get debugFrame_hexDump => 'Espaço Hexadecimal:'; + String get debugFrame_hexDump => 'Espaço Hexadecimal:'; @override String get chat_pathManagement => 'Gerenciamento de Caminhos'; @@ -1186,14 +1224,14 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_autoUseSavedPath => 'Auto (usar caminho salvo)'; @override - String get chat_forceFloodMode => 'Modo de Inundação Forçado'; + String get chat_forceFloodMode => 'Modo de Inundação Forçado'; @override String get chat_recentAckPaths => 'Rotas de ACK Recentes (toque para usar):'; @override String get chat_pathHistoryFull => - 'O histórico está cheio. Remova entradas para adicionar novas.'; + 'O histórico está cheio. Remova entradas para adicionar novas.'; @override String get chat_hopSingular => 'pule'; @@ -1220,10 +1258,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.'; + 'Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.'; @override - String get chat_pathActions => 'Ações do Caminho:'; + String get chat_pathActions => 'Ações do Caminho:'; @override String get chat_setCustomPath => 'Definir Caminho Personalizado'; @@ -1237,11 +1275,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_clearPathSubtitle => - 'Forçar a descoberta na próxima transmissão'; + 'Forçar a descoberta na próxima transmissão'; @override String get chat_pathCleared => - 'Caminho limpo. A próxima mensagem redescobrirá a rota.'; + 'Caminho limpo. A próxima mensagem redescobrirá a rota.'; @override String get chat_floodModeSubtitle => @@ -1249,14 +1287,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.'; + 'Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.'; @override String get chat_fullPath => 'Caminho Completo'; @override String get chat_pathDetailsNotAvailable => - 'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.'; + 'Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1277,8 +1315,7 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_pathDeviceConfirmed => 'Dispositivo confirmado.'; @override - String get chat_pathDeviceNotConfirmed => - 'Dispositivo ainda não confirmado.'; + String get chat_pathDeviceNotConfirmed => 'Dispositivo ainda não confirmado.'; @override String get chat_type => 'Digite'; @@ -1287,24 +1324,24 @@ class AppLocalizationsPt extends AppLocalizations { String get chat_path => 'Caminho'; @override - String get chat_publicKey => 'Chave Pública'; + String get chat_publicKey => 'Chave Pública'; @override String get chat_compressOutgoingMessages => 'Comprimir mensagens enviadas'; @override - String get chat_floodForced => 'Inundação (forçada)'; + String get chat_floodForced => 'Inundação (forçada)'; @override - String get chat_directForced => 'Direto (forçado)'; + String get chat_directForced => 'Direto (forçado)'; @override String chat_hopsForced(int count) { - return '$count saltos (forçado)'; + return '$count saltos (forçado)'; } @override - String get chat_floodAuto => 'Inundação (automática)'; + String get chat_floodAuto => 'Inundação (automática)'; @override String get chat_direct => 'Salvar'; @@ -1314,7 +1351,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String chat_unread(int count) { - return 'Não lido: $count'; + return 'Não lido: $count'; } @override @@ -1329,32 +1366,32 @@ class AppLocalizationsPt extends AppLocalizations { @override String chat_couldNotOpenLink(String url) { - return 'Não foi possível abrir o link: $url'; + return 'Não foi possível abrir o link: $url'; } @override - String get chat_invalidLink => 'Formato de link inválido'; + String get chat_invalidLink => 'Formato de link inválido'; @override - String get map_title => 'Mapa de Nós'; + String get map_title => 'Mapa de Nós'; @override - String get map_lineOfSight => 'Linha de visão'; + String get map_lineOfSight => 'Linha de visão'; @override - String get map_losScreenTitle => 'Linha de visão'; + String get map_losScreenTitle => 'Linha de visão'; @override String get map_noNodesWithLocation => - 'Não existem nós com dados de localização.'; + 'Não existem nós com dados de localização.'; @override String get map_nodesNeedGps => - 'Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa'; + 'Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa'; @override String map_nodesCount(int count) { - return 'Nós: $count'; + return 'Nós: $count'; } @override @@ -1381,10 +1418,10 @@ class AppLocalizationsPt extends AppLocalizations { String get map_pinPrivate => 'Bloquear (Privado)'; @override - String get map_pinPublic => 'Pin (Público)'; + String get map_pinPublic => 'Pin (Público)'; @override - String get map_lastSeen => 'Última Visão'; + String get map_lastSeen => 'Última Visão'; @override String get map_disconnectConfirm => @@ -1403,10 +1440,10 @@ class AppLocalizationsPt extends AppLocalizations { String get map_shareMarkerHere => 'Compartilhar marcador aqui'; @override - String get map_pinLabel => 'Rótulo de marcador'; + String get map_pinLabel => 'Rótulo de marcador'; @override - String get map_label => 'Rótulo'; + String get map_label => 'Rótulo'; @override String get map_pointOfInterest => 'Ponto de interesse'; @@ -1418,14 +1455,14 @@ class AppLocalizationsPt extends AppLocalizations { String get map_sendToChannel => 'Enviar para o canal'; @override - String get map_noChannelsAvailable => 'Não existem canais disponíveis.'; + String get map_noChannelsAvailable => 'Não existem canais disponíveis.'; @override - String get map_publicLocationShare => 'Compartilhar local público'; + String get map_publicLocationShare => 'Compartilhar local público'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Você está prestes a compartilhar uma localização em $channelLabel. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.'; + return 'Você está prestes a compartilhar uma localização em $channelLabel. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.'; } @override @@ -1433,19 +1470,19 @@ class AppLocalizationsPt extends AppLocalizations { 'Conecte-se a um dispositivo para compartilhar marcadores'; @override - String get map_filterNodes => 'Filtrar Nós'; + String get map_filterNodes => 'Filtrar Nós'; @override - String get map_nodeTypes => 'Tipos de Nó'; + String get map_nodeTypes => 'Tipos de Nó'; @override - String get map_chatNodes => 'Nós de Chat'; + String get map_chatNodes => 'Nós de Chat'; @override String get map_repeaters => 'Repetidores'; @override - String get map_otherNodes => 'Outros Nós'; + String get map_otherNodes => 'Outros Nós'; @override String get map_keyPrefix => 'Prefixo Chave'; @@ -1454,7 +1491,7 @@ class AppLocalizationsPt extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtrar por prefixo-chave'; @override - String get map_publicKeyPrefix => 'Prefixo de chave pública'; + String get map_publicKeyPrefix => 'Prefixo de chave pública'; @override String get map_markers => 'Marcadores'; @@ -1463,25 +1500,25 @@ class AppLocalizationsPt extends AppLocalizations { String get map_showSharedMarkers => 'Mostrar marcadores compartilhados'; @override - String get map_lastSeenTime => 'Último Tempo de Visualização'; + String get map_lastSeenTime => 'Último Tempo de Visualização'; @override String get map_sharedPin => 'Pin compartilhado'; @override - String get map_joinRoom => 'Junte-se à Sala'; + String get map_joinRoom => 'Junte-se à Sala'; @override String get map_manageRepeater => 'Gerenciar Repetidor'; @override - String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.'; + String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.'; @override - String get map_runTrace => 'Executar Traçado de Caminho'; + String get map_runTrace => 'Executar Traçado de Caminho'; @override - String get map_removeLast => 'Remover Último'; + String get map_removeLast => 'Remover Último'; @override String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.'; @@ -1491,11 +1528,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get mapCache_selectAreaFirst => - 'Selecione uma área para armazenar em cache primeiro'; + 'Selecione uma área para armazenar em cache primeiro'; @override String get mapCache_noTilesToDownload => - 'Não há tiles para baixar para esta área.'; + 'Não há tiles para baixar para esta área.'; @override String get mapCache_downloadTilesTitle => 'Baixar tiles'; @@ -1529,13 +1566,13 @@ class AppLocalizationsPt extends AppLocalizations { String get mapCache_offlineCacheCleared => 'Cache offline limpa'; @override - String get mapCache_noAreaSelected => 'Nenhuma área selecionada'; + String get mapCache_noAreaSelected => 'Nenhuma área selecionada'; @override - String get mapCache_cacheArea => 'Área de Cache'; + String get mapCache_cacheArea => 'Área de Cache'; @override - String get mapCache_useCurrentView => 'Usar a Visualização Atual'; + String get mapCache_useCurrentView => 'Usar a Visualização Atual'; @override String get mapCache_zoomRange => 'Intervalo de Zoom'; @@ -1576,17 +1613,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String time_minutesAgo(int minutes) { - return '$minutes minutos atrás'; + return '$minutes minutos atrás'; } @override String time_hoursAgo(int hours) { - return '${hours}h atrás'; + return '${hours}h atrás'; } @override String time_daysAgo(int days) { - return '$days dias atrás'; + return '$days dias atrás'; } @override @@ -1608,7 +1645,7 @@ class AppLocalizationsPt extends AppLocalizations { String get time_weeks => 'semanas'; @override - String get time_month => 'mês'; + String get time_month => 'mês'; @override String get time_months => 'meses'; @@ -1643,15 +1680,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get login_savePasswordSubtitle => - 'A senha será armazenada com segurança neste dispositivo.'; + 'A senha será armazenada com segurança neste dispositivo.'; @override String get login_repeaterDescription => - 'Insira a senha do repetidor para acessar as configurações e o status.'; + 'Insira a senha do repetidor para acessar as configurações e o status.'; @override String get login_roomDescription => - 'Insira a senha da sala para acessar as configurações e o status.'; + 'Insira a senha da sala para acessar as configurações e o status.'; @override String get login_routing => 'Rotas'; @@ -1663,7 +1700,7 @@ class AppLocalizationsPt extends AppLocalizations { String get login_autoUseSavedPath => 'Auto (usar caminho salvo)'; @override - String get login_forceFloodMode => 'Modo de Inundação Forçado'; + String get login_forceFloodMode => 'Modo de Inundação Forçado'; @override String get login_managePaths => 'Gerenciar Caminhos'; @@ -1683,7 +1720,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get login_failedMessage => - 'Falha no login. A senha está incorreta ou o repetidor está inacessível.'; + 'Falha no login. A senha está incorreta ou o repetidor está inacessível.'; @override String get common_reload => 'Recarregar'; @@ -1715,38 +1752,38 @@ class AppLocalizationsPt extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.'; + 'Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)'; + 'A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)'; @override String get path_labelHexPrefixes => 'Prefixo Hexadecimal'; @override String get path_helperMaxHops => - 'Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)'; + 'Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)'; @override String get path_selectFromContacts => 'Ou selecione de contatos:'; @override String get path_noRepeatersFound => - 'Não foram encontrados repetidores ou servidores de sala.'; + 'Não foram encontrados repetidores ou servidores de sala.'; @override String get path_customPathsRequire => - 'Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.'; + 'Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Prefixos hexadecimais inválidos: $prefixes'; + return 'Prefixos hexadecimais inválidos: $prefixes'; } @override String get path_tooLong => - 'Caminho muito longo. Máximo de 64 saltos permitidos.'; + 'Caminho muito longo. Máximo de 64 saltos permitidos.'; @override String get path_setPath => 'Definir Caminho'; @@ -1765,14 +1802,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_statusSubtitle => - 'Visualizar status do repetidor, estatísticas e vizinhos.'; + 'Visualizar status do repetidor, estatísticas e vizinhos.'; @override String get repeater_telemetry => 'Telemetria'; @override String get repeater_telemetrySubtitle => - 'Visualizar telemetria de sensores e estatísticas do sistema'; + 'Visualizar telemetria de sensores e estatísticas do sistema'; @override String get repeater_cli => 'CLI'; @@ -1787,10 +1824,10 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.'; @override - String get repeater_settings => 'Configurações'; + String get repeater_settings => 'Configurações'; @override - String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; + String get repeater_settingsSubtitle => 'Configurar parâmetros do repetidor'; @override String get repeater_statusTitle => 'Status do Repetidor'; @@ -1802,7 +1839,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_autoUseSavedPath => 'Auto (usar caminho salvo)'; @override - String get repeater_forceFloodMode => 'Modo de Inundação Forçado'; + String get repeater_forceFloodMode => 'Modo de Inundação Forçado'; @override String get repeater_pathManagement => 'Gerenciamento de caminhos'; @@ -1811,8 +1848,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_refresh => 'Atualizar'; @override - String get repeater_statusRequestTimeout => - 'Solicitação de status expirou.'; + String get repeater_statusRequestTimeout => 'Solicitação de status expirou.'; @override String repeater_errorLoadingStatus(String error) { @@ -1820,13 +1856,13 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get repeater_systemInformation => 'Informações do Sistema'; + String get repeater_systemInformation => 'Informações do Sistema'; @override String get repeater_battery => 'Bateria'; @override - String get repeater_clockAtLogin => 'Relógio (no login)'; + String get repeater_clockAtLogin => 'Relógio (no login)'; @override String get repeater_uptime => 'Disponibilidade'; @@ -1835,19 +1871,19 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_queueLength => 'Comprimento da Fila'; @override - String get repeater_debugFlags => 'Marcar Flags de Depuração'; + String get repeater_debugFlags => 'Marcar Flags de Depuração'; @override - String get repeater_radioStatistics => 'Estatísticas de Rádio'; + String get repeater_radioStatistics => 'Estatísticas de Rádio'; @override - String get repeater_lastRssi => 'Último RSSI'; + String get repeater_lastRssi => 'Último RSSI'; @override - String get repeater_lastSnr => 'Último SNR'; + String get repeater_lastSnr => 'Último SNR'; @override - String get repeater_noiseFloor => 'Nível de Ruído'; + String get repeater_noiseFloor => 'Nível de Ruído'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1856,7 +1892,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Estatísticas de Pacote'; + String get repeater_packetStatistics => 'Estatísticas de Pacote'; @override String get repeater_sent => 'Enviado'; @@ -1879,17 +1915,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundação: $flood, Direto: $direct'; + return 'Total: $total, Inundação: $flood, Direto: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Total: $total, Inundação: $flood, Direto: $direct'; + return 'Total: $total, Inundação: $flood, Direto: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Inundação: $flood, Direto: $direct'; + return 'Inundação: $flood, Direto: $direct'; } @override @@ -1898,10 +1934,10 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Configurações do Repetidor'; + String get repeater_settingsTitle => 'Configurações do Repetidor'; @override - String get repeater_basicSettings => 'Configurações Básicas'; + String get repeater_basicSettings => 'Configurações Básicas'; @override String get repeater_repeaterName => 'Nome do Repetidor'; @@ -1923,10 +1959,10 @@ class AppLocalizationsPt extends AppLocalizations { 'Acesso com senha de leitura somente'; @override - String get repeater_radioSettings => 'Configurações de Rádio'; + String get repeater_radioSettings => 'Configurações de Rádio'; @override - String get repeater_frequencyMhz => 'Frequência (MHz)'; + String get repeater_frequencyMhz => 'Frequência (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @@ -1941,13 +1977,13 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_bandwidth => 'Largura de banda'; @override - String get repeater_spreadingFactor => 'Fator de Dispersão'; + String get repeater_spreadingFactor => 'Fator de Dispersão'; @override - String get repeater_codingRate => 'Taxa de Codificação'; + String get repeater_codingRate => 'Taxa de Codificação'; @override - String get repeater_locationSettings => 'Configurações de Localização'; + String get repeater_locationSettings => 'Configurações de Localização'; @override String get repeater_latitude => 'Latitude'; @@ -1984,13 +2020,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_privacyModeSubtitle => - 'Esconder nome/localização em anúncios'; + 'Esconder nome/localização em anúncios'; @override - String get repeater_advertisementSettings => 'Configurações de Anúncios'; + String get repeater_advertisementSettings => 'Configurações de Anúncios'; @override - String get repeater_localAdvertInterval => 'Intervalo de Anúncio Local'; + String get repeater_localAdvertInterval => 'Intervalo de Anúncio Local'; @override String repeater_localAdvertIntervalMinutes(int minutes) { @@ -1999,7 +2035,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Intervalo de Anúncio de Inundação'; + 'Intervalo de Anúncio de Inundação'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -2008,7 +2044,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Intervalo de Anúncio Criptografado'; + 'Intervalo de Anúncio Criptografado'; @override String get repeater_dangerZone => 'Zona de Perigo'; @@ -2029,11 +2065,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_regenerateIdentityKeySubtitle => - 'Gerar nova chave pública/privada'; + 'Gerar nova chave pública/privada'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Isso gerará uma nova identidade para o repetidor. Continuar?'; + 'Isso gerará uma nova identidade para o repetidor. Continuar?'; @override String get repeater_eraseFileSystem => 'Excluir Sistema de Arquivos'; @@ -2044,11 +2080,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!'; + 'AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!'; @override String get repeater_eraseSerialOnly => - 'Apagar está disponível apenas via console serial.'; + 'Apagar está disponível apenas via console serial.'; @override String repeater_commandSent(String command) { @@ -2064,27 +2100,26 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_confirm => 'Confirmar'; @override - String get repeater_settingsSaved => 'Configurações salvas com sucesso'; + String get repeater_settingsSaved => 'Configurações salvas com sucesso'; @override String repeater_errorSavingSettings(String error) { - return 'Erro ao salvar as configurações: $error'; + return 'Erro ao salvar as configurações: $error'; } @override - String get repeater_refreshBasicSettings => - 'Atualizar Configurações Básicas'; + String get repeater_refreshBasicSettings => 'Atualizar Configurações Básicas'; @override String get repeater_refreshRadioSettings => - 'Atualizar Configurações de Rádio'; + 'Atualizar Configurações de Rádio'; @override String get repeater_refreshTxPower => 'Atualizar TX de energia'; @override String get repeater_refreshLocationSettings => - 'Atualizar Configurações de Localização'; + 'Atualizar Configurações de Localização'; @override String get repeater_refreshPacketForwarding => @@ -2098,7 +2133,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_refreshAdvertisementSettings => - 'Atualizar Configurações do Anúncio'; + 'Atualizar Configurações do Anúncio'; @override String repeater_refreshed(String label) { @@ -2114,20 +2149,20 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliTitle => 'Repetidor CLI'; @override - String get repeater_debugNextCommand => 'Depurar Próximo Comando'; + String get repeater_debugNextCommand => 'Depurar Próximo Comando'; @override String get repeater_commandHelp => 'Ajuda'; @override - String get repeater_clearHistory => 'Limpar Histórico'; + String get repeater_clearHistory => 'Limpar Histórico'; @override - String get repeater_noCommandsSent => 'Ainda não foram enviadas comandos.'; + String get repeater_noCommandsSent => 'Ainda não foram enviadas comandos.'; @override String get repeater_typeCommandOrUseQuick => - 'Digite um comando abaixo ou use comandos rápidos'; + 'Digite um comando abaixo ou use comandos rápidos'; @override String get repeater_enterCommandHint => 'Insira o comando...'; @@ -2136,7 +2171,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_previousCommand => 'Comando anterior'; @override - String get repeater_nextCommand => 'Próxima ação'; + String get repeater_nextCommand => 'Próxima ação'; @override String get repeater_enterCommandFirst => 'Insira um comando primeiro'; @@ -2153,7 +2188,7 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliQuickGetName => 'Obter Nome'; @override - String get repeater_cliQuickGetRadio => 'Obter Rádio'; + String get repeater_cliQuickGetRadio => 'Obter Rádio'; @override String get repeater_cliQuickGetTx => 'Obter TX'; @@ -2162,24 +2197,24 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliQuickNeighbors => 'Vizinhos'; @override - String get repeater_cliQuickVersion => 'Versão'; + String get repeater_cliQuickVersion => 'Versão'; @override String get repeater_cliQuickAdvertise => 'Anunciar'; @override - String get repeater_cliQuickClock => 'Relógio'; + String get repeater_cliQuickClock => 'Relógio'; @override - String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; + String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios'; @override String get repeater_cliHelpReboot => - 'Reinicia o dispositivo. (note, você pode obter \'Timeout\' que é normal)'; + 'Reinicia o dispositivo. (note, você pode obter \'Timeout\' que é normal)'; @override String get repeater_cliHelpClock => - 'Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.'; + 'Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.'; @override String get repeater_cliHelpPassword => @@ -2187,38 +2222,38 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpVersion => - 'Mostra a versão do dispositivo e a data de construção do firmware.'; + 'Mostra a versão do dispositivo e a data de construção do firmware.'; @override String get repeater_cliHelpClearStats => - 'Reseta vários contadores de estatísticas para zero.'; + 'Reseta vários contadores de estatísticas para zero.'; @override String get repeater_cliHelpSetAf => 'Define o fator de tempo de ar.'; @override String get repeater_cliHelpSetTx => - 'Define a potência de transmissão LoRa em dBm (redefinir para aplicar).'; + 'Define a potência de transmissão LoRa em dBm (redefinir para aplicar).'; @override String get repeater_cliHelpSetRepeat => - 'Habilita ou desabilita o papel do repetidor para este nó.'; + 'Habilita ou desabilita o papel do repetidor para este nó.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Servidor de sala) Se \'ligado\', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).'; + '(Servidor de sala) Se \'ligado\', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).'; @override String get repeater_cliHelpSetFloodMax => - 'Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)'; + 'Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)'; @override String get repeater_cliHelpSetIntThresh => - 'Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.'; + 'Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.'; + 'Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.'; @override String get repeater_cliHelpSetMultiAcks => @@ -2226,42 +2261,42 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.'; + 'Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.'; + 'Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.'; @override String get repeater_cliHelpSetGuestPassword => - 'Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")'; + 'Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")'; @override - String get repeater_cliHelpSetName => 'Define o nome do anúncio.'; + String get repeater_cliHelpSetName => 'Define o nome do anúncio.'; @override String get repeater_cliHelpSetLat => - 'Define a latitude do mapa de anúncios. (graus decimais)'; + 'Define a latitude do mapa de anúncios. (graus decimais)'; @override String get repeater_cliHelpSetLon => - 'Define a longitude do mapa de anúncios. (graus decimais)'; + 'Define a longitude do mapa de anúncios. (graus decimais)'; @override String get repeater_cliHelpSetRadio => - 'Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.'; + 'Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.'; @override String get repeater_cliHelpSetRxDelay => - 'Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.'; + 'Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.'; @override String get repeater_cliHelpSetTxDelay => - 'Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)'; + 'Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.'; + 'Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.'; @override String get repeater_cliHelpSetBridgeEnabled => 'Ativar/Desativar ponte.'; @@ -2272,7 +2307,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpSetBridgeSource => - 'Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.'; + 'Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.'; @override String get repeater_cliHelpSetBridgeBaud => @@ -2288,15 +2323,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpTempRadio => - 'Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).'; + 'Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).'; @override String get repeater_cliHelpSetPerm => - 'Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)'; + 'Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)'; @override String get repeater_cliHelpGetBridgeType => - 'Obtém tipo de ponte nenhum, rs232, espnow'; + 'Obtém tipo de ponte nenhum, rs232, espnow'; @override String get repeater_cliHelpLogStart => @@ -2312,86 +2347,86 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4'; + 'Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4'; @override String get repeater_cliHelpNeighborRemove => - 'Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.'; + 'Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.'; @override String get repeater_cliHelpRegion => - '(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.'; + '(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.'; @override String get repeater_cliHelpRegionLoad => - 'NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.'; + 'NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.'; @override String get repeater_cliHelpRegionGet => - 'Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) \'F\'\"'; + 'Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Adiciona ou atualiza uma definição de região com o nome fornecido.'; + 'Adiciona ou atualiza uma definição de região com o nome fornecido.'; @override String get repeater_cliHelpRegionRemove => - 'Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)'; + 'Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)'; @override String get repeater_cliHelpRegionAllowf => - 'Define a permissão de \'F\'luido para a região especificada. (\'\' para o escopo global/legado)'; + 'Define a permissão de \'F\'luido para a região especificada. (\'\' para o escopo global/legado)'; @override String get repeater_cliHelpRegionDenyf => - 'Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)'; + 'Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)'; @override String get repeater_cliHelpRegionHome => - 'Responde com a região \'home\' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)'; + 'Responde com a região \'home\' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)'; @override - String get repeater_cliHelpRegionHomeSet => 'Define a região \'casa\'.'; + String get repeater_cliHelpRegionHomeSet => 'Define a região \'casa\'.'; @override String get repeater_cliHelpRegionSave => - 'Persiste a lista/mapa de regiões para o armazenamento.'; + 'Persiste a lista/mapa de regiões para o armazenamento.'; @override String get repeater_cliHelpGps => - 'Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.'; + 'Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.'; @override String get repeater_cliHelpGpsOnOff => 'Alterna o estado de energia do GPS.'; @override String get repeater_cliHelpGpsSync => - 'Sincroniza o tempo do nó com o relógio GPS.'; + 'Sincroniza o tempo do nó com o relógio GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Define a posição do nó para coordenadas GPS e salvar preferências.'; + 'Define a posição do nó para coordenadas GPS e salvar preferências.'; @override String get repeater_cliHelpGpsAdvert => - 'Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências'; + 'Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências'; @override String get repeater_cliHelpGpsAdvertSet => - 'Define a configuração do anúncio de localização.'; + 'Define a configuração do anúncio de localização.'; @override String get repeater_commandsListTitle => 'Lista de Comandos'; @override String get repeater_commandsListNote => - 'NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".'; + 'NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".'; @override String get repeater_general => 'Geral'; @override - String get repeater_settingsCategory => 'Configurações'; + String get repeater_settingsCategory => 'Configurações'; @override String get repeater_bridge => 'Ponte'; @@ -2404,25 +2439,25 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_regionManagementRepeaterOnly => - 'Gerenciamento de Região (Apenas Repetidor)'; + 'Gerenciamento de Região (Apenas Repetidor)'; @override String get repeater_regionNote => - 'Os comandos de região foram introduzidos para gerenciar definições e permissões de região.'; + 'Os comandos de região foram introduzidos para gerenciar definições e permissões de região.'; @override String get repeater_gpsManagement => 'Gerenciamento GPS'; @override String get repeater_gpsNote => - 'O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.'; + 'O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.'; @override String get telemetry_receivedData => 'Dados de Telemetria Recebidos'; @override String get telemetry_requestTimeout => - 'Solicitação de telemetria expirou o tempo.'; + 'Solicitação de telemetria expirou o tempo.'; @override String telemetry_errorLoading(String error) { @@ -2430,8 +2465,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get telemetry_noData => - 'Não estão disponíveis dados de telemetria.'; + String get telemetry_noData => 'Não estão disponíveis dados de telemetria.'; @override String telemetry_channelTitle(int channel) { @@ -2442,7 +2476,7 @@ class AppLocalizationsPt extends AppLocalizations { String get telemetry_batteryLabel => 'Bateria'; @override - String get telemetry_voltageLabel => 'Tensão'; + String get telemetry_voltageLabel => 'Tensão'; @override String get telemetry_mcuTemperatureLabel => 'Temperatura do MCU'; @@ -2470,7 +2504,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2489,7 +2523,7 @@ class AppLocalizationsPt extends AppLocalizations { String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos'; @override - String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.'; + String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.'; @override String neighbors_unknownContact(String pubkey) { @@ -2498,11 +2532,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Ouvido: $time atrás'; + return 'Ouvido: $time atrás'; } @override - String get channelPath_title => 'Rótulo de Caminho de Pacote'; + String get channelPath_title => 'Rótulo de Caminho de Pacote'; @override String get channelPath_viewMap => 'Ver mapa'; @@ -2515,7 +2549,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channelPath_noHopDetails => - 'Os detalhes do pacote não estão disponíveis.'; + 'Os detalhes do pacote não estão disponíveis.'; @override String get channelPath_messageDetails => 'Detalhes da Mensagem'; @@ -2539,11 +2573,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Rastreamento observado $index • $hops'; + return 'Rastreamento observado $index • $hops'; } @override - String get channelPath_noLocationData => 'Não há dados de localização.'; + String get channelPath_noLocationData => 'Não há dados de localização.'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2559,7 +2593,7 @@ class AppLocalizationsPt extends AppLocalizations { String get channelPath_unknownPath => 'Desconhecido'; @override - String get channelPath_floodPath => 'Inundação'; + String get channelPath_floodPath => 'Inundação'; @override String get channelPath_directPath => 'Salvar'; @@ -2579,11 +2613,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Não estão disponíveis localizações de repetidores para este caminho.'; + 'Não estão disponíveis localizações de repetidores para este caminho.'; @override String channelPath_primaryPath(int index) { - return 'Caminho $index (Primário)'; + return 'Caminho $index (Primário)'; } @override @@ -2594,12 +2628,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Não estão disponíveis detalhes de voo para este pacote.'; + 'Não estão disponíveis detalhes de voo para este pacote.'; @override String get channelPath_unknownRepeater => 'Repetidor Desconhecido'; @@ -2612,17 +2646,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get community_createDesc => - 'Crie uma nova comunidade e compartilhe via código QR.'; + 'Crie uma nova comunidade e compartilhe via código QR.'; @override String get community_join => 'Junte-se'; @override - String get community_joinTitle => 'Junte-se à Comunidade'; + String get community_joinTitle => 'Junte-se à Comunidade'; @override String community_joinConfirmation(String name) { - return 'Você gostaria de se juntar à comunidade \"$name\"?'; + return 'Você gostaria de se juntar à comunidade \"$name\"?'; } @override @@ -2630,13 +2664,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get community_scanInstructions => - 'Aponte a câmera para um código QR da comunidade'; + 'Aponte a câmera para um código QR da comunidade'; @override - String get community_showQr => 'Mostrar Código QR'; + String get community_showQr => 'Mostrar Código QR'; @override - String get community_publicChannel => 'Comunidade Pública'; + String get community_publicChannel => 'Comunidade Pública'; @override String get community_hashtagChannel => 'Hashtag da Comunidade'; @@ -2654,7 +2688,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_joined(String name) { - return 'Juntou-se à comunidade \"$name\"'; + return 'Juntou-se à comunidade \"$name\"'; } @override @@ -2662,39 +2696,39 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Escanear este código QR para juntar-se a $name'; + return 'Escanear este código QR para juntar-se a $name'; } @override String get community_hashtagPrivacyHint => - 'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade'; + 'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade'; @override - String get community_invalidQrCode => 'Código QR da comunidade inválido'; + String get community_invalidQrCode => 'Código QR da comunidade inválido'; @override - String get community_alreadyMember => 'Já é Membro'; + String get community_alreadyMember => 'Já é Membro'; @override String community_alreadyMemberMessage(String name) { - return 'Você já é membro de \"$name\".'; + return 'Você já é membro de \"$name\".'; } @override String get community_addPublicChannel => - 'Adicionar Canal Público da Comunidade'; + 'Adicionar Canal Público da Comunidade'; @override String get community_addPublicChannelHint => - 'Adicionar automaticamente o canal público para esta comunidade'; + 'Adicionar automaticamente o canal público para esta comunidade'; @override String get community_noCommunities => - 'Ainda não foram adicionadas comunidades.'; + 'Ainda não foram adicionadas comunidades.'; @override String get community_scanOrCreate => - 'Escaneie um código QR ou crie uma comunidade para começar.'; + 'Escaneie um código QR ou crie uma comunidade para começar.'; @override String get community_manageCommunities => 'Gerenciar Comunidades'; @@ -2709,7 +2743,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'Isso também excluirá $count canal/canais e suas mensagens.'; + return 'Isso também excluirá $count canal/canais e suas mensagens.'; } @override @@ -2722,7 +2756,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.'; + return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.'; } @override @@ -2743,7 +2777,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++'; + return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++'; } @override @@ -2761,7 +2795,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get community_regularHashtagDesc => - 'Hashtag público (qualquer pessoa pode participar)'; + 'Hashtag público (qualquer pessoa pode participar)'; @override String get community_communityHashtag => 'Hashtag da Comunidade'; @@ -2782,7 +2816,7 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_sortBy => 'Ordenar por'; @override - String get listFilter_latestMessages => 'Últimas mensagens'; + String get listFilter_latestMessages => 'Últimas mensagens'; @override String get listFilter_heardRecently => 'Ouvido recentemente'; @@ -2806,7 +2840,7 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_removeFromFavorites => 'Remover da lista de favoritos'; @override - String get listFilter_users => 'Usuários'; + String get listFilter_users => 'Usuários'; @override String get listFilter_repeaters => 'Repetidores'; @@ -2815,36 +2849,36 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_roomServers => 'Servidores de sala'; @override - String get listFilter_unreadOnly => 'Apenas não lido'; + String get listFilter_unreadOnly => 'Apenas não lido'; @override String get listFilter_newGroup => 'Novo grupo'; @override - String get pathTrace_you => 'Você'; + String get pathTrace_you => 'Você'; @override String get pathTrace_failed => 'Falha no rastreamento de caminho.'; @override - String get pathTrace_notAvailable => 'Traçado de caminho não disponível.'; + String get pathTrace_notAvailable => 'Traçado de caminho não disponível.'; @override String get pathTrace_refreshTooltip => 'Atualizar Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Um ou mais dos lúpulos estão sem localização!'; + 'Um ou mais dos lúpulos estão sem localização!'; @override String get pathTrace_clearTooltip => 'Limpar caminho'; @override - String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.'; + String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.'; @override String losRunFailed(String error) { - return 'Falha na verificação da linha de visão: $error'; + return 'Falha na verificação da linha de visão: $error'; } @override @@ -2852,17 +2886,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get losRunToViewElevationProfile => - 'Execute o LOS para visualizar o perfil de elevação'; + 'Execute o LOS para visualizar o perfil de elevação'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados'; + 'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados'; @override - String get losShowDisplayNodes => 'Mostrar nós de exibição'; + String get losShowDisplayNodes => 'Mostrar nós de exibição'; @override String get losCustomPoints => 'Pontos personalizados'; @@ -2892,7 +2926,7 @@ class AppLocalizationsPt extends AppLocalizations { String get losRun => 'Executar LOS'; @override - String get losNoElevationData => 'Sem dados de elevação'; + String get losNoElevationData => 'Sem dados de elevação'; @override String losProfileClear( @@ -2901,7 +2935,7 @@ class AppLocalizationsPt extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit'; + return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit'; } @override @@ -2927,11 +2961,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Dados de elevação indisponíveis para uma ou mais amostras.'; + 'Dados de elevação indisponíveis para uma ou mais amostras.'; @override String get losErrorInvalidInput => - 'Dados de pontos/elevação inválidos para cálculo de LOS.'; + 'Dados de pontos/elevação inválidos para cálculo de LOS.'; @override String get losRenameCustomPoint => 'Renomear ponto personalizado'; @@ -2947,10 +2981,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get losElevationAttribution => - 'Dados de elevação: Open-Meteo (CC BY 4.0)'; + 'Dados de elevação: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Horizonte de rádio'; + String get losLegendRadioHorizon => 'Horizonte de rádio'; @override String get losLegendLosBeam => 'Linha de visada'; @@ -2959,13 +2993,13 @@ class AppLocalizationsPt extends AppLocalizations { String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequência'; + String get losFrequencyLabel => 'Frequência'; @override - String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; + String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; @override - String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; + String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; @override String losFrequencyDialogDescription( @@ -2974,24 +3008,23 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; + return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; } @override - String get contacts_pathTrace => 'Traçado de Caminho'; + String get contacts_pathTrace => 'Traçado de Caminho'; @override String get contacts_ping => 'Pingar'; @override - String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor'; + String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor'; @override String get contacts_repeaterPing => 'Pingar repetidor'; @override - String get contacts_roomPathTrace => - 'Traçar caminho para o servidor da sala'; + String get contacts_roomPathTrace => 'Traçar caminho para o servidor da sala'; @override String get contacts_roomPing => 'Pingar servidor da sala'; @@ -3005,10 +3038,10 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.'; + String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.'; @override - String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos'; + String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos'; @override String get contacts_contactImported => 'Contato foi importado.'; @@ -3017,41 +3050,39 @@ class AppLocalizationsPt extends AppLocalizations { String get contacts_contactImportFailed => 'Contato falhou ao ser importado.'; @override - String get contacts_zeroHopAdvert => 'Anúncio Zero Hop'; + String get contacts_zeroHopAdvert => 'Anúncio Zero Hop'; @override - String get contacts_floodAdvert => 'Anúncio de Inundação'; + String get contacts_floodAdvert => 'Anúncio de Inundação'; @override String get contacts_copyAdvertToClipboard => - 'Copiar Anúncio para Área de Transferência'; + 'Copiar Anúncio para Área de Transferência'; @override String get contacts_addContactFromClipboard => - 'Adicionar Contato da Área de Transferência'; + 'Adicionar Contato da Área de Transferência'; @override String get contacts_ShareContact => - 'Copiar contato para Área de Transferência'; + 'Copiar contato para Área de Transferência'; @override - String get contacts_ShareContactZeroHop => - 'Compartilhar contato por anúncio'; + String get contacts_ShareContactZeroHop => 'Compartilhar contato por anúncio'; @override - String get contacts_zeroHopContactAdvertSent => - 'Enviou contato por anúncio.'; + String get contacts_zeroHopContactAdvertSent => 'Enviou contato por anúncio.'; @override String get contacts_zeroHopContactAdvertFailed => 'Falha ao enviar contato.'; @override String get contacts_contactAdvertCopied => - 'Anúncio copiado para a Área de Transferência.'; + 'Anúncio copiado para a Área de Transferência.'; @override String get contacts_contactAdvertCopyFailed => - 'Cópia do anúncio para a Área de Transferência falhou.'; + 'Cópia do anúncio para a Área de Transferência falhou.'; @override String get notification_activityTitle => 'Atividade MeshCore'; @@ -3083,8 +3114,8 @@ class AppLocalizationsPt extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'novos nós', - one: 'novo nó', + other: 'novos nós', + one: 'novo nó', ); return '$count $_temp0'; } @@ -3103,21 +3134,21 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportRepeatersSubtitle => - 'Exporta repetidores / roomserver com localização para arquivo GPX.'; + 'Exporta repetidores / roomserver com localização para arquivo GPX.'; @override String get settings_gpxExportContacts => 'Exportar companheiros para GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exporta companheiros com uma localização para um arquivo GPX.'; + 'Exporta companheiros com uma localização para um arquivo GPX.'; @override String get settings_gpxExportAll => 'Exportar todos os contatos para GPX'; @override String get settings_gpxExportAllSubtitle => - 'Exporta todos os contatos com uma localização para um arquivo GPX.'; + 'Exporta todos os contatos com uma localização para um arquivo GPX.'; @override String get settings_gpxExportSuccess => 'Arquivo GPX exportado com sucesso.'; @@ -3127,17 +3158,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportNotAvailable => - 'Não suportado no seu dispositivo/SO'; + 'Não suportado no seu dispositivo/SO'; @override String get settings_gpxExportError => 'Ocorreu um erro ao exportar.'; @override String get settings_gpxExportRepeatersRoom => - 'Localizações do servidor de repetidor e sala'; + 'Localizações do servidor de repetidor e sala'; @override - String get settings_gpxExportChat => 'Localizações de companheiros'; + String get settings_gpxExportChat => 'Localizações de companheiros'; @override String get settings_gpxExportAllContacts => 'Todos os locais de contatos'; @@ -3148,11 +3179,11 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportShareSubject => - 'meshcore-open exportação de dados de mapa GPX'; + 'meshcore-open exportação de dados de mapa GPX'; @override - String get snrIndicator_nearByRepeaters => 'Repetidores Próximos'; + String get snrIndicator_nearByRepeaters => 'Repetidores Próximos'; @override - String get snrIndicator_lastSeen => 'Visto pela última vez'; + String get snrIndicator_lastSeen => 'Visto pela última vez'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 456d497..dd95949 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -12,93 +12,92 @@ class AppLocalizationsRu extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Контакты'; + String get nav_contacts => 'Контакты'; @override - String get nav_channels => 'Каналы'; + String get nav_channels => 'Каналы'; @override - String get nav_map => 'Карта'; + String get nav_map => 'Карта'; @override - String get common_cancel => 'Отмена'; + String get common_cancel => 'Отмена'; @override String get common_ok => 'OK'; @override - String get common_connect => 'Коннект'; + String get common_connect => 'Коннект'; @override - String get common_unknownDevice => - 'Неизвестное устройство'; + String get common_unknownDevice => 'Неизвестное устройство'; @override - String get common_save => 'Сохранить'; + String get common_save => 'Сохранить'; @override - String get common_delete => 'Удалить'; + String get common_delete => 'Удалить'; @override - String get common_close => 'Закрыть'; + String get common_close => 'Закрыть'; @override - String get common_edit => 'Изменить'; + String get common_edit => 'Изменить'; @override - String get common_add => 'Добавить'; + String get common_add => 'Добавить'; @override - String get common_settings => 'Настройки'; + String get common_settings => 'Настройки'; @override - String get common_disconnect => 'Отключить'; + String get common_disconnect => 'Отключить'; @override - String get common_connected => 'Подключено'; + String get common_connected => 'Подключено'; @override - String get common_disconnected => 'Отключено'; + String get common_disconnected => 'Отключено'; @override - String get common_create => 'Создать'; + String get common_create => 'Создать'; @override - String get common_continue => 'Продолжить'; + String get common_continue => 'Продолжить'; @override - String get common_share => 'Поделиться'; + String get common_share => 'Поделиться'; @override - String get common_copy => 'Копировать'; + String get common_copy => 'Копировать'; @override - String get common_retry => 'Повторить'; + String get common_retry => 'Повторить'; @override - String get common_hide => 'Скрыть'; + String get common_hide => 'Скрыть'; @override - String get common_remove => 'Убрать'; + String get common_remove => 'Убрать'; @override - String get common_enable => 'Включить'; + String get common_enable => 'Включить'; @override - String get common_disable => 'Выключить'; + String get common_disable => 'Выключить'; @override - String get common_reboot => 'Перезагрузить'; + String get common_reboot => 'Перезагрузить'; @override - String get common_loading => 'Загрузка...'; + String get common_loading => 'Загрузка...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { - return '$volts Ð’'; + return '$volts В'; } @override @@ -116,247 +115,275 @@ class AppLocalizationsRu extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Подключение через USB'; + String get usbScreenTitle => 'Подключение через USB'; @override String get usbScreenSubtitle => - 'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.'; + 'Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.'; @override - String get usbScreenStatus => 'Выберите USB-устройство'; + String get usbScreenStatus => 'Выберите USB-устройство'; @override String get usbScreenNote => - 'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.'; + 'USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.'; @override String get usbScreenEmptyState => - 'Не обнаружено устройств USB. Подключите одно из них и обновите список.'; + 'Не обнаружено устройств USB. Подключите одно из них и обновите список.'; @override - String get scanner_scanning => 'Поиск устройств...'; + String get usbErrorPermissionDenied => + 'Запрос на доступ через USB был отклонен.'; @override - String get scanner_connecting => 'Подключение...'; + String get usbErrorDeviceMissing => + 'Выбранное USB-устройство больше недоступно.'; @override - String get scanner_disconnecting => 'Отключение...'; + String get usbErrorInvalidPort => 'Выберите действительное USB-устройство.'; @override - String get scanner_notConnected => 'Не подключено'; + String get usbErrorBusy => + 'Еще одно запрошенное соединение через USB уже находится в процессе.'; + + @override + String get usbErrorNotConnected => 'Ни одно устройство USB не подключено.'; + + @override + String get usbErrorOpenFailed => + 'Не удалось открыть выбранное USB-устройство.'; + + @override + String get usbErrorConnectFailed => + 'Не удалось установить соединение с выбранным USB-устройством.'; + + @override + String get usbErrorUnsupported => + 'Поддержка последовательного USB отсутствует на данной платформе.'; + + @override + String get usbErrorAlreadyActive => + 'USB-соединение уже установлено и работает.'; + + @override + String get usbErrorNoDeviceSelected => + 'Не было выбрано ни одно устройство USB.'; + + @override + String get usbErrorPortClosed => 'USB-соединение не установлено.'; + + @override + String get usbErrorConnectTimedOut => + 'Ожидание ответа от устройства превысило установленное время.'; + + @override + String get scanner_scanning => 'Поиск устройств...'; + + @override + String get scanner_connecting => 'Подключение...'; + + @override + String get scanner_disconnecting => 'Отключение...'; + + @override + String get scanner_notConnected => 'Не подключено'; @override String scanner_connectedTo(String deviceName) { - return 'Подключено к $deviceName'; + return 'Подключено к $deviceName'; } @override - String get scanner_searchingDevices => - 'Поиск устройств MeshCore...'; + String get scanner_searchingDevices => 'Поиск устройств MeshCore...'; @override - String get scanner_tapToScan => - 'Нажмите для поиска MeshCore устройств'; + String get scanner_tapToScan => 'Нажмите для поиска MeshCore устройств'; @override String scanner_connectionFailed(String error) { - return 'Подключение не удалось: $error'; + return 'Подключение не удалось: $error'; } @override - String get scanner_stop => 'Стоп'; + String get scanner_stop => 'Стоп'; @override - String get scanner_scan => 'Сканирование'; + String get scanner_scan => 'Сканирование'; @override - String get scanner_bluetoothOff => 'Bluetooth выключен'; + String get scanner_bluetoothOff => 'Bluetooth выключен'; @override String get scanner_bluetoothOffMessage => - 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; + 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; @override - String get scanner_chromeRequired => - 'Требуется браузер Chrome'; + String get scanner_chromeRequired => 'Требуется браузер Chrome'; @override String get scanner_chromeRequiredMessage => - 'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.'; + 'Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.'; @override - String get scanner_enableBluetooth => 'Включите Bluetooth'; + String get scanner_enableBluetooth => 'Включите Bluetooth'; @override - String get device_quickSwitch => 'Быстрое переключение'; + String get device_quickSwitch => 'Быстрое переключение'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Настройки'; + String get settings_title => 'Настройки'; @override - String get settings_deviceInfo => - 'Информация об устройстве'; + String get settings_deviceInfo => 'Информация об устройстве'; @override - String get settings_appSettings => 'Настройки приложения'; + String get settings_appSettings => 'Настройки приложения'; @override String get settings_appSettingsSubtitle => - 'Уведомления, сообщения и настройки карты'; + 'Уведомления, сообщения и настройки карты'; @override - String get settings_nodeSettings => 'Настройки ноды'; + String get settings_nodeSettings => 'Настройки ноды'; @override - String get settings_nodeName => 'Имя ноды'; + String get settings_nodeName => 'Имя ноды'; @override - String get settings_nodeNameNotSet => 'Не установлено'; + String get settings_nodeNameNotSet => 'Не установлено'; @override - String get settings_nodeNameHint => 'Введите имя ноды'; + String get settings_nodeNameHint => 'Введите имя ноды'; @override - String get settings_nodeNameUpdated => 'Имя обновлено'; + String get settings_nodeNameUpdated => 'Имя обновлено'; @override - String get settings_radioSettings => 'Настройки радио'; + String get settings_radioSettings => 'Настройки радио'; @override String get settings_radioSettingsSubtitle => - 'Частота, мощность и коэффициент распространения'; + 'Частота, мощность и коэффициент распространения'; @override - String get settings_radioSettingsUpdated => - 'Настройки радио обновлены'; + String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; @override - String get settings_location => 'Позиция'; + String get settings_location => 'Позиция'; @override - String get settings_locationSubtitle => 'Координаты GPS'; + String get settings_locationSubtitle => 'Координаты GPS'; @override - String get settings_locationUpdated => - 'Позиция и настройки GPS обновлены'; + String get settings_locationUpdated => 'Позиция и настройки GPS обновлены'; @override - String get settings_locationBothRequired => - 'Введите широту и долготу.'; + String get settings_locationBothRequired => 'Введите широту и долготу.'; @override - String get settings_locationInvalid => - 'Неверная широта или долгота.'; + String get settings_locationInvalid => 'Неверная широта или долгота.'; @override - String get settings_locationGPSEnable => 'Включить GPS'; + String get settings_locationGPSEnable => 'Включить GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Включение GPS для автоматического обновления позиции.'; + 'Включение GPS для автоматического обновления позиции.'; @override String get settings_locationIntervalSec => - 'Интервал для позиционирования GPS (секунды)'; + 'Интервал для позиционирования GPS (секунды)'; @override String get settings_locationIntervalInvalid => - 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; + 'Интервал должен составлять не менее 60 секунд и не более 86400 секунд.'; @override - String get settings_latitude => 'Широта'; + String get settings_latitude => 'Широта'; @override - String get settings_longitude => 'Долгота'; + String get settings_longitude => 'Долгота'; @override - String get settings_privacyMode => - 'Режим конфиденциальности'; + String get settings_privacyMode => 'Режим конфиденциальности'; @override String get settings_privacyModeSubtitle => - 'Скрыть имя/позицию в анонсировании'; + 'Скрыть имя/позицию в анонсировании'; @override String get settings_privacyModeToggle => - 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; + 'Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.'; @override - String get settings_privacyModeEnabled => - 'Режим конфиденциальности включен'; + String get settings_privacyModeEnabled => 'Режим конфиденциальности включен'; @override String get settings_privacyModeDisabled => - 'Режим конфиденциальности выключен'; + 'Режим конфиденциальности выключен'; @override - String get settings_actions => 'Действия'; + String get settings_actions => 'Действия'; @override - String get settings_sendAdvertisement => - 'Отправить анонсирование'; + String get settings_sendAdvertisement => 'Отправить анонсирование'; @override String get settings_sendAdvertisementSubtitle => - 'Отправить анонсирование о присутствии сейчас'; + 'Отправить анонсирование о присутствии сейчас'; @override - String get settings_advertisementSent => - 'Анонсирование отправлено'; + String get settings_advertisementSent => 'Анонсирование отправлено'; @override - String get settings_syncTime => 'Синхронизация времени'; + String get settings_syncTime => 'Синхронизация времени'; @override - String get settings_syncTimeSubtitle => - 'Синхронизировать время с телефоном'; + String get settings_syncTimeSubtitle => 'Синхронизировать время с телефоном'; @override - String get settings_timeSynchronized => - 'Время синхронизировано'; + String get settings_timeSynchronized => 'Время синхронизировано'; @override - String get settings_refreshContacts => 'Обновить контакты'; + String get settings_refreshContacts => 'Обновить контакты'; @override String get settings_refreshContactsSubtitle => - 'Перезагрузить список контактов с устройства'; + 'Перезагрузить список контактов с устройства'; @override - String get settings_rebootDevice => - 'Перезагрузить устройство'; + String get settings_rebootDevice => 'Перезагрузить устройство'; @override String get settings_rebootDeviceSubtitle => - 'Перезапустить устройство MeshCore'; + 'Перезапустить устройство MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Ð’Ñ‹ уверены, что хотите перезагрузить устройство? Ð’Ñ‹ будете отключены.'; + 'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.'; @override - String get settings_debug => 'Отладка'; + String get settings_debug => 'Отладка'; @override - String get settings_bleDebugLog => 'Журнал отладки BLE'; + String get settings_bleDebugLog => 'Журнал отладки BLE'; @override String get settings_bleDebugLogSubtitle => - 'Команды BLE, ответы и сырые данные'; + 'Команды BLE, ответы и сырые данные'; @override - String get settings_appDebugLog => - 'Журнал отладки приложения'; + String get settings_appDebugLog => 'Журнал отладки приложения'; @override - String get settings_appDebugLogSubtitle => - 'Сообщения отладки приложения'; + String get settings_appDebugLogSubtitle => 'Сообщения отладки приложения'; @override - String get settings_about => 'О программе'; + String get settings_about => 'О программе'; @override String settings_aboutVersion(String version) { @@ -368,1278 +395,1209 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_aboutDescription => - 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; + 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; @override String get settings_aboutOpenMeteoAttribution => - 'Данные о высоте LOS: Open-Meteo (CC BY 4.0)'; + 'Данные о высоте LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'Имя'; + String get settings_infoName => 'Имя'; @override String get settings_infoId => 'ID'; @override - String get settings_infoStatus => 'Статус'; + String get settings_infoStatus => 'Статус'; @override - String get settings_infoBattery => 'Батарея'; + String get settings_infoBattery => 'Батарея'; @override - String get settings_infoPublicKey => 'Публичный ключ'; + String get settings_infoPublicKey => 'Публичный ключ'; @override - String get settings_infoContactsCount => - 'Количество контактов'; + String get settings_infoContactsCount => 'Количество контактов'; @override - String get settings_infoChannelCount => 'Количество каналов'; + String get settings_infoChannelCount => 'Количество каналов'; @override - String get settings_presets => 'Пресеты'; + String get settings_presets => 'Пресеты'; @override - String get settings_frequency => 'Частота (МГц)'; + String get settings_frequency => 'Частота (МГц)'; @override - String get settings_frequencyHelper => '300.0 – 2500.0'; + String get settings_frequencyHelper => '300.0 – 2500.0'; @override - String get settings_frequencyInvalid => - 'Недопустимая частота (300–2500 МГц)'; + String get settings_frequencyInvalid => 'Недопустимая частота (300–2500 МГц)'; @override - String get settings_bandwidth => 'Полоса пропускания'; + String get settings_bandwidth => 'Полоса пропускания'; @override - String get settings_spreadingFactor => - 'Коэффициент расширения'; + String get settings_spreadingFactor => 'Коэффициент расширения'; @override - String get settings_codingRate => - 'Коэффициент кодирования'; + String get settings_codingRate => 'Коэффициент кодирования'; @override - String get settings_txPower => 'Мощность передачи (дБм)'; + String get settings_txPower => 'Мощность передачи (дБм)'; @override - String get settings_txPowerHelper => '0 – 22'; + String get settings_txPowerHelper => '0 – 22'; @override String get settings_txPowerInvalid => - 'Недопустимая мощность передачи (0–22 дБм)'; + 'Недопустимая мощность передачи (0–22 дБм)'; @override - String get settings_clientRepeat => - 'Повторение \"вне сети\"'; + String get settings_clientRepeat => 'Повторение \"вне сети\"'; @override String get settings_clientRepeatSubtitle => - 'Позвольте этому устройству повторять пакеты данных для других устройств.'; + 'Позвольте этому устройству повторять пакеты данных для других устройств.'; @override String get settings_clientRepeatFreqWarning => - 'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.'; + 'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.'; @override String settings_error(String message) { - return 'Ошибка: $message'; + return 'Ошибка: $message'; } @override - String get appSettings_title => 'Настройки приложения'; + String get appSettings_title => 'Настройки приложения'; @override - String get appSettings_appearance => 'Внешний вид'; + String get appSettings_appearance => 'Внешний вид'; @override - String get appSettings_theme => 'Тема'; + String get appSettings_theme => 'Тема'; @override - String get appSettings_themeSystem => 'Как в системе'; + String get appSettings_themeSystem => 'Как в системе'; @override - String get appSettings_themeLight => 'Светлая'; + String get appSettings_themeLight => 'Светлая'; @override - String get appSettings_themeDark => 'Тёмная'; + String get appSettings_themeDark => 'Тёмная'; @override - String get appSettings_language => 'Язык'; + String get appSettings_language => 'Язык'; @override - String get appSettings_languageSystem => 'Как в системе'; + String get appSettings_languageSystem => 'Как в системе'; @override - String get appSettings_languageEn => 'Английский'; + String get appSettings_languageEn => 'Английский'; @override - String get appSettings_languageFr => 'Французский'; + String get appSettings_languageFr => 'Французский'; @override - String get appSettings_languageEs => 'Испанский'; + String get appSettings_languageEs => 'Испанский'; @override - String get appSettings_languageDe => 'Немецкий'; + String get appSettings_languageDe => 'Немецкий'; @override - String get appSettings_languagePl => 'Польский'; + String get appSettings_languagePl => 'Польский'; @override - String get appSettings_languageSl => 'Словенский'; + String get appSettings_languageSl => 'Словенский'; @override - String get appSettings_languagePt => 'Португальский'; + String get appSettings_languagePt => 'Португальский'; @override - String get appSettings_languageIt => 'Итальянский'; + String get appSettings_languageIt => 'Итальянский'; @override - String get appSettings_languageZh => 'Китайский'; + String get appSettings_languageZh => 'Китайский'; @override - String get appSettings_languageSv => 'Шведский'; + String get appSettings_languageSv => 'Шведский'; @override - String get appSettings_languageNl => 'Нидерландский'; + String get appSettings_languageNl => 'Нидерландский'; @override - String get appSettings_languageSk => 'Словацкий'; + String get appSettings_languageSk => 'Словацкий'; @override - String get appSettings_languageBg => 'Болгарский'; + String get appSettings_languageBg => 'Болгарский'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Русский'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Українська'; @override String get appSettings_enableMessageTracing => - 'Включить трассировку сообщений'; + 'Включить трассировку сообщений'; @override String get appSettings_enableMessageTracingSubtitle => - 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; + 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; @override - String get appSettings_notifications => 'Уведомления'; + String get appSettings_notifications => 'Уведомления'; @override - String get appSettings_enableNotifications => - 'Включить уведомления'; + String get appSettings_enableNotifications => 'Включить уведомления'; @override String get appSettings_enableNotificationsSubtitle => - 'Получать уведомления о сообщениях и оповещениях'; + 'Получать уведомления о сообщениях и оповещениях'; @override String get appSettings_notificationPermissionDenied => - 'Разрешение на уведомления отклонено'; + 'Разрешение на уведомления отклонено'; @override - String get appSettings_notificationsEnabled => - 'Уведомления включены'; + String get appSettings_notificationsEnabled => 'Уведомления включены'; @override - String get appSettings_notificationsDisabled => - 'Уведомления отключены'; + String get appSettings_notificationsDisabled => 'Уведомления отключены'; @override - String get appSettings_messageNotifications => - 'Уведомления о сообщениях'; + String get appSettings_messageNotifications => 'Уведомления о сообщениях'; @override String get appSettings_messageNotificationsSubtitle => - 'Показывать уведомление при получении новых сообщений'; + 'Показывать уведомление при получении новых сообщений'; @override String get appSettings_channelMessageNotifications => - 'Уведомления о сообщениях в каналах'; + 'Уведомления о сообщениях в каналах'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Показывать уведомление при получении сообщений в каналах'; + 'Показывать уведомление при получении сообщений в каналах'; @override String get appSettings_advertisementNotifications => - 'Уведомления об анонсированиях'; + 'Уведомления об анонсированиях'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Показывать уведомление при обнаружении новых нод'; + 'Показывать уведомление при обнаружении новых нод'; @override - String get appSettings_messaging => 'Обмен сообщениями'; + String get appSettings_messaging => 'Обмен сообщениями'; @override String get appSettings_clearPathOnMaxRetry => - 'Сбросить маршрут после максимального числа попыток'; + 'Сбросить маршрут после максимального числа попыток'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; + 'Сбросить маршрут контакта после 5 неудачных попыток отправки'; @override String get appSettings_pathsWillBeCleared => - 'Маршруты будут сброшены после 5 неудачных попыток'; + 'Маршруты будут сброшены после 5 неудачных попыток'; @override String get appSettings_pathsWillNotBeCleared => - 'Маршруты не будут автоматически сбрасываться'; + 'Маршруты не будут автоматически сбрасываться'; @override String get appSettings_autoRouteRotation => - 'Автоматическое переключение маршрутов'; + 'Автоматическое переключение маршрутов'; @override String get appSettings_autoRouteRotationSubtitle => - 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; + 'Циклически переключаться между лучшими маршрутами и режимом рассылки'; @override String get appSettings_autoRouteRotationEnabled => - 'Автоматическое переключение маршрутов включено'; + 'Автоматическое переключение маршрутов включено'; @override String get appSettings_autoRouteRotationDisabled => - 'Автоматическое переключение маршрутов отключено'; + 'Автоматическое переключение маршрутов отключено'; @override - String get appSettings_battery => 'Батарея'; + String get appSettings_battery => 'Батарея'; @override - String get appSettings_batteryChemistry => 'Химия батареи'; + String get appSettings_batteryChemistry => 'Химия батареи'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Установить для устройства ($deviceName)'; + return 'Установить для устройства ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Подключитесь к устройству, чтобы выбрать'; + 'Подключитесь к устройству, чтобы выбрать'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 Ð’)'; + String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 В)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 Ð’)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 В)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0–4.2 Ð’)'; + String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; @override - String get appSettings_mapDisplay => 'Отображение карты'; + String get appSettings_mapDisplay => 'Отображение карты'; @override - String get appSettings_showRepeaters => - 'Показывать репитеры'; + String get appSettings_showRepeaters => 'Показывать репитеры'; @override String get appSettings_showRepeatersSubtitle => - 'Отображать репитеры на карте'; + 'Отображать репитеры на карте'; @override - String get appSettings_showChatNodes => - 'Показывать чат-ноды'; + String get appSettings_showChatNodes => 'Показывать чат-ноды'; @override String get appSettings_showChatNodesSubtitle => - 'Отображать чат-ноды на карте'; + 'Отображать чат-ноды на карте'; @override - String get appSettings_showOtherNodes => - 'Показывать другие ноды'; + String get appSettings_showOtherNodes => 'Показывать другие ноды'; @override String get appSettings_showOtherNodesSubtitle => - 'Отображать другие типы нод на карте'; + 'Отображать другие типы нод на карте'; @override - String get appSettings_timeFilter => 'Фильтр по времени'; + String get appSettings_timeFilter => 'Фильтр по времени'; @override - String get appSettings_timeFilterShowAll => - 'Показывать все ноды'; + String get appSettings_timeFilterShowAll => 'Показывать все ноды'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Показывать ноды за последние $hours ч'; + return 'Показывать ноды за последние $hours ч'; } @override - String get appSettings_mapTimeFilter => - 'Временной фильтр карты'; + String get appSettings_mapTimeFilter => 'Временной фильтр карты'; @override String get appSettings_showNodesDiscoveredWithin => - 'Показывать ноды, обнаруженные за:'; + 'Показывать ноды, обнаруженные за:'; @override - String get appSettings_allTime => 'Всё время'; + String get appSettings_allTime => 'Всё время'; @override - String get appSettings_lastHour => 'Последний час'; + String get appSettings_lastHour => 'Последний час'; @override - String get appSettings_last6Hours => 'Последние 6 часов'; + String get appSettings_last6Hours => 'Последние 6 часов'; @override - String get appSettings_last24Hours => 'Последние 24 часа'; + String get appSettings_last24Hours => 'Последние 24 часа'; @override - String get appSettings_lastWeek => 'Последнюю неделю'; + String get appSettings_lastWeek => 'Последнюю неделю'; @override - String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; + String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; @override - String get appSettings_unitsTitle => 'Единицы'; + String get appSettings_unitsTitle => 'Единицы'; @override - String get appSettings_unitsMetric => 'Метрическая (м/км)'; + String get appSettings_unitsMetric => 'Метрическая (м/км)'; @override - String get appSettings_unitsImperial => 'Имперская (ft / mi)'; + String get appSettings_unitsImperial => 'Имперская (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Область не выбрана'; + String get appSettings_noAreaSelected => 'Область не выбрана'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Область выбрана (масштаб $minZoom–$maxZoom)'; + return 'Область выбрана (масштаб $minZoom–$maxZoom)'; } @override - String get appSettings_debugCard => 'Отладка'; + String get appSettings_debugCard => 'Отладка'; @override - String get appSettings_appDebugLogging => - 'Журнал отладки приложения'; + String get appSettings_appDebugLogging => 'Журнал отладки приложения'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записывать отладочные сообщения приложения для диагностики'; + 'Записывать отладочные сообщения приложения для диагностики'; @override String get appSettings_appDebugLoggingEnabled => - 'Журнал отладки приложения включён'; + 'Журнал отладки приложения включён'; @override String get appSettings_appDebugLoggingDisabled => - 'Журнал отладки приложения отключён'; + 'Журнал отладки приложения отключён'; @override - String get contacts_title => 'Контакты'; + String get contacts_title => 'Контакты'; @override - String get contacts_noContacts => 'Контактов пока нет'; + String get contacts_noContacts => 'Контактов пока нет'; @override String get contacts_contactsWillAppear => - 'Контакты появятся, когда устройства начнут рассылать оповещения'; + 'Контакты появятся, когда устройства начнут рассылать оповещения'; @override - String get contacts_unread => 'Непрочитанное'; + String get contacts_unread => 'Непрочитанное'; @override - String get contacts_searchContactsNoNumber => - 'Поиск контактов...'; + String get contacts_searchContactsNoNumber => 'Поиск контактов...'; @override String contacts_searchContacts(int number, String str) { - return 'Поиск контактов...'; + return 'Поиск контактов...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Поиск $number$str избранного...'; + return 'Поиск $number$str избранного...'; } @override String contacts_searchUsers(int number, String str) { - return 'Поиск $number$str пользователей...'; + return 'Поиск $number$str пользователей...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Поиск $number$str ретрансляторов...'; + return 'Поиск $number$str ретрансляторов...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Поиск $number$str серверов комнат...'; + return 'Поиск $number$str серверов комнат...'; } @override - String get contacts_noUnreadContacts => - 'Нет непрочитанных контактов'; + String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; @override - String get contacts_noContactsFound => - 'Контакты или группы не найдены'; + String get contacts_noContactsFound => 'Контакты или группы не найдены'; @override - String get contacts_deleteContact => 'Удалить контакт'; + String get contacts_deleteContact => 'Удалить контакт'; @override String contacts_removeConfirm(String contactName) { - return 'Удалить $contactName из контактов?'; + return 'Удалить $contactName из контактов?'; } @override - String get contacts_manageRepeater => - 'Управление репитером'; + String get contacts_manageRepeater => 'Управление репитером'; @override - String get contacts_manageRoom => - 'Управление сервером комнат'; + String get contacts_manageRoom => 'Управление сервером комнат'; @override - String get contacts_roomLogin => 'Вход на сервер комнат'; + String get contacts_roomLogin => 'Вход на сервер комнат'; @override - String get contacts_openChat => 'Открыть чат'; + String get contacts_openChat => 'Открыть чат'; @override - String get contacts_editGroup => 'Изменить группу'; + String get contacts_editGroup => 'Изменить группу'; @override - String get contacts_deleteGroup => 'Удалить группу'; + String get contacts_deleteGroup => 'Удалить группу'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Удалить \"$groupName\"?'; + return 'Удалить \"$groupName\"?'; } @override - String get contacts_newGroup => 'Новая группа'; + String get contacts_newGroup => 'Новая группа'; @override - String get contacts_groupName => 'Имя группы'; + String get contacts_groupName => 'Имя группы'; @override - String get contacts_groupNameRequired => - 'Имя группы обязательно'; + String get contacts_groupNameRequired => 'Имя группы обязательно'; @override String contacts_groupAlreadyExists(String name) { - return 'Группа \"$name\" уже существует'; + return 'Группа \"$name\" уже существует'; } @override - String get contacts_filterContacts => 'Фильтр контактов...'; + String get contacts_filterContacts => 'Фильтр контактов...'; @override String get contacts_noContactsMatchFilter => - 'Нет контактов, соответствующих фильтру'; + 'Нет контактов, соответствующих фильтру'; @override - String get contacts_noMembers => 'Нет участников'; + String get contacts_noMembers => 'Нет участников'; @override - String get contacts_lastSeenNow => 'Видели только что'; + String get contacts_lastSeenNow => 'Видели только что'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Видели $minutes мин назад'; + return 'Видели $minutes мин назад'; } @override - String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; + String get contacts_lastSeenHourAgo => 'Видели 1 час назад'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Видели $hours ч назад'; + return 'Видели $hours ч назад'; } @override - String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; + String get contacts_lastSeenDayAgo => 'Видели 1 день назад'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Видели $days дн. назад'; + return 'Видели $days дн. назад'; } @override - String get channels_title => 'Каналы'; + String get channels_title => 'Каналы'; @override - String get channels_noChannelsConfigured => - 'Каналы не настроены'; + String get channels_noChannelsConfigured => 'Каналы не настроены'; @override - String get channels_addPublicChannel => - 'Добавить публичный канал'; + String get channels_addPublicChannel => 'Добавить публичный канал'; @override - String get channels_searchChannels => 'Поиск каналов...'; + String get channels_searchChannels => 'Поиск каналов...'; @override - String get channels_noChannelsFound => 'Каналы не найдены'; + String get channels_noChannelsFound => 'Каналы не найдены'; @override String channels_channelIndex(int index) { - return 'Канал $index'; + return 'Канал $index'; } @override - String get channels_hashtagChannel => 'Хэштег-канал'; + String get channels_hashtagChannel => 'Хэштег-канал'; @override - String get channels_public => 'Публичный'; + String get channels_public => 'Публичный'; @override - String get channels_private => 'Приватный'; + String get channels_private => 'Приватный'; @override - String get channels_publicChannel => 'Публичный канал'; + String get channels_publicChannel => 'Публичный канал'; @override - String get channels_privateChannel => 'Приватный канал'; + String get channels_privateChannel => 'Приватный канал'; @override - String get channels_editChannel => 'Изменить канал'; + String get channels_editChannel => 'Изменить канал'; @override - String get channels_muteChannel => - 'Отключить уведомления канала'; + String get channels_muteChannel => 'Отключить уведомления канала'; @override - String get channels_unmuteChannel => - 'Включить уведомления канала'; + String get channels_unmuteChannel => 'Включить уведомления канала'; @override - String get channels_deleteChannel => 'Удалить канал'; + String get channels_deleteChannel => 'Удалить канал'; @override String channels_deleteChannelConfirm(String name) { - return 'Удалить \"$name\"? Это действие нельзя отменить.'; + return 'Удалить \"$name\"? Это действие нельзя отменить.'; } @override String channels_channelDeleteFailed(String name) { - return 'Не удалось удалить канал $name.'; + return 'Не удалось удалить канал $name.'; } @override String channels_channelDeleted(String name) { - return 'Канал \"$name\" удалён'; + return 'Канал \"$name\" удалён'; } @override - String get channels_addChannel => 'Добавить канал'; + String get channels_addChannel => 'Добавить канал'; @override - String get channels_channelIndexLabel => 'Индекс канала'; + String get channels_channelIndexLabel => 'Индекс канала'; @override - String get channels_channelName => 'Имя канала'; + String get channels_channelName => 'Имя канала'; @override - String get channels_usePublicChannel => - 'Использовать публичный канал'; + String get channels_usePublicChannel => 'Использовать публичный канал'; @override - String get channels_standardPublicPsk => - 'Стандартный публичный PSK'; + String get channels_standardPublicPsk => 'Стандартный публичный PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => - 'Сгенерировать случайный PSK'; + String get channels_generateRandomPsk => 'Сгенерировать случайный PSK'; @override - String get channels_enterChannelName => 'Введите имя канала'; + String get channels_enterChannelName => 'Введите имя канала'; @override String get channels_pskMustBe32Hex => - 'PSK должен содержать 32 шестнадцатеричных символа'; + 'PSK должен содержать 32 шестнадцатеричных символа'; @override String channels_channelAdded(String name) { - return 'Канал \"$name\" добавлен'; + return 'Канал \"$name\" добавлен'; } @override String channels_editChannelTitle(int index) { - return 'Изменить канал $index'; + return 'Изменить канал $index'; } @override - String get channels_smazCompression => 'Сжатие SMAZ'; + String get channels_smazCompression => 'Сжатие SMAZ'; @override String channels_channelUpdated(String name) { - return 'Канал \"$name\" обновлён'; + return 'Канал \"$name\" обновлён'; } @override - String get channels_publicChannelAdded => - 'Публичный канал добавлен'; + String get channels_publicChannelAdded => 'Публичный канал добавлен'; @override - String get channels_sortBy => 'Сортировка'; + String get channels_sortBy => 'Сортировка'; @override - String get channels_sortManual => 'Вручную'; + String get channels_sortManual => 'Вручную'; @override - String get channels_sortAZ => 'По алфавиту'; + String get channels_sortAZ => 'По алфавиту'; @override - String get channels_sortLatestMessages => - 'По последним сообщениям'; + String get channels_sortLatestMessages => 'По последним сообщениям'; @override - String get channels_sortUnread => 'По непрочитанным'; + String get channels_sortUnread => 'По непрочитанным'; @override - String get channels_createPrivateChannel => - 'Создать приватный канал'; + String get channels_createPrivateChannel => 'Создать приватный канал'; @override - String get channels_createPrivateChannelDesc => - 'Защищён секретным ключом.'; + String get channels_createPrivateChannelDesc => 'Защищён секретным ключом.'; @override String get channels_joinPrivateChannel => - 'Присоединиться к приватному каналу'; + 'Присоединиться к приватному каналу'; @override String get channels_joinPrivateChannelDesc => - 'Введите секретный ключ вручную.'; + 'Введите секретный ключ вручную.'; @override - String get channels_joinPublicChannel => - 'Присоединиться к публичному каналу'; + String get channels_joinPublicChannel => 'Присоединиться к публичному каналу'; @override String get channels_joinPublicChannelDesc => - 'К этому каналу может присоединиться любой.'; + 'К этому каналу может присоединиться любой.'; @override - String get channels_joinHashtagChannel => - 'Присоединиться к хэштег-каналу'; + String get channels_joinHashtagChannel => 'Присоединиться к хэштег-каналу'; @override String get channels_joinHashtagChannelDesc => - 'К хэштег-каналам может присоединиться любой.'; + 'К хэштег-каналам может присоединиться любой.'; @override - String get channels_scanQrCode => 'Сканировать QR-код'; + String get channels_scanQrCode => 'Сканировать QR-код'; @override - String get channels_scanQrCodeComingSoon => 'Скоро будет'; + String get channels_scanQrCodeComingSoon => 'Скоро будет'; @override - String get channels_enterHashtag => 'Введите хэштег'; + String get channels_enterHashtag => 'Введите хэштег'; @override - String get channels_hashtagHint => 'например, #команда'; + String get channels_hashtagHint => 'например, #команда'; @override - String get chat_noMessages => 'Сообщений пока нет'; + String get chat_noMessages => 'Сообщений пока нет'; @override - String get chat_sendMessageToStart => - 'Отправьте сообщение, чтобы начать'; + String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать'; @override - String get chat_originalMessageNotFound => - 'Исходное сообщение не найдено'; + String get chat_originalMessageNotFound => 'Исходное сообщение не найдено'; @override String chat_replyingTo(String name) { - return 'Ответ для $name'; + return 'Ответ для $name'; } @override String chat_replyTo(String name) { - return 'Ответить $name'; + return 'Ответить $name'; } @override - String get chat_location => 'Местоположение'; + String get chat_location => 'Местоположение'; @override String chat_sendMessageTo(String contactName) { - return 'Отправить сообщение $contactName'; + return 'Отправить сообщение $contactName'; } @override - String get chat_typeMessage => 'Напишите сообщение...'; + String get chat_typeMessage => 'Напишите сообщение...'; @override String chat_messageTooLong(int maxBytes) { - return 'Сообщение слишком длинное (макс. $maxBytes байт).'; + return 'Сообщение слишком длинное (макс. $maxBytes байт).'; } @override - String get chat_messageCopied => 'Сообщение скопировано'; + String get chat_messageCopied => 'Сообщение скопировано'; @override - String get chat_messageDeleted => 'Сообщение удалено'; + String get chat_messageDeleted => 'Сообщение удалено'; @override - String get chat_retryingMessage => - 'Повтор отправки сообщения'; + String get chat_retryingMessage => 'Повтор отправки сообщения'; @override String chat_retryCount(int current, int max) { - return 'Попытка $current/$max'; + return 'Попытка $current/$max'; } @override - String get chat_sendGif => 'Отправить GIF'; + String get chat_sendGif => 'Отправить GIF'; @override - String get chat_reply => 'Ответить'; + String get chat_reply => 'Ответить'; @override - String get chat_addReaction => 'Добавить реакцию'; + String get chat_addReaction => 'Добавить реакцию'; @override - String get chat_me => 'Я'; + String get chat_me => 'Я'; @override - String get emojiCategorySmileys => 'Смайлы'; + String get emojiCategorySmileys => 'Смайлы'; @override - String get emojiCategoryGestures => 'Жесты'; + String get emojiCategoryGestures => 'Жесты'; @override - String get emojiCategoryHearts => 'Сердечки'; + String get emojiCategoryHearts => 'Сердечки'; @override - String get emojiCategoryObjects => 'Предметы'; + String get emojiCategoryObjects => 'Предметы'; @override - String get gifPicker_title => 'Выберите GIF'; + String get gifPicker_title => 'Выберите GIF'; @override - String get gifPicker_searchHint => 'Поиск GIF...'; + String get gifPicker_searchHint => 'Поиск GIF...'; @override - String get gifPicker_poweredBy => 'Работает на GIPHY'; + String get gifPicker_poweredBy => 'Работает на GIPHY'; @override - String get gifPicker_noGifsFound => 'GIF не найдены'; + String get gifPicker_noGifsFound => 'GIF не найдены'; @override - String get gifPicker_failedLoad => - 'Не удалось загрузить GIF'; + String get gifPicker_failedLoad => 'Не удалось загрузить GIF'; @override - String get gifPicker_failedSearch => - 'Не удалось выполнить поиск GIF'; + String get gifPicker_failedSearch => 'Не удалось выполнить поиск GIF'; @override - String get gifPicker_noInternet => - 'Нет подключения к интернету'; + String get gifPicker_noInternet => 'Нет подключения к интернету'; @override - String get debugLog_appTitle => - 'Журнал отладки приложения'; + String get debugLog_appTitle => 'Журнал отладки приложения'; @override - String get debugLog_bleTitle => 'Журнал отладки BLE'; + String get debugLog_bleTitle => 'Журнал отладки BLE'; @override - String get debugLog_copyLog => 'Копировать журнал'; + String get debugLog_copyLog => 'Копировать журнал'; @override - String get debugLog_clearLog => 'Очистить журнал'; + String get debugLog_clearLog => 'Очистить журнал'; @override - String get debugLog_copied => - 'Журнал отладки скопирован'; + String get debugLog_copied => 'Журнал отладки скопирован'; @override - String get debugLog_bleCopied => 'Журнал BLE скопирован'; + String get debugLog_bleCopied => 'Журнал BLE скопирован'; @override - String get debugLog_noEntries => - 'Журнал отладки пока пуст'; + String get debugLog_noEntries => 'Журнал отладки пока пуст'; @override String get debugLog_enableInSettings => - 'Включите запись журнала отладки в настройках'; + 'Включите запись журнала отладки в настройках'; @override - String get debugLog_frames => 'Фреймы'; + String get debugLog_frames => 'Фреймы'; @override - String get debugLog_rawLogRx => 'Сырой журнал приёма'; + String get debugLog_rawLogRx => 'Сырой журнал приёма'; @override - String get debugLog_noBleActivity => - 'Активность BLE пока отсутствует'; + String get debugLog_noBleActivity => 'Активность BLE пока отсутствует'; @override String debugFrame_length(int count) { - return 'Длина фрейма: $count байт'; + return 'Длина фрейма: $count байт'; } @override String debugFrame_command(String value) { - return 'Команда: 0x$value'; + return 'Команда: 0x$value'; } @override - String get debugFrame_textMessageHeader => - 'Фрейм текстового сообщения:'; + String get debugFrame_textMessageHeader => 'Фрейм текстового сообщения:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Публичный ключ получателя: $pubKey'; + return '- Публичный ключ получателя: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Временная метка: $timestamp'; + return '- Временная метка: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Флаги: 0x$value'; + return '- Флаги: 0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- Тип текста: $type ($label)'; + return '- Тип текста: $type ($label)'; } @override String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Обычный'; + String get debugFrame_textTypePlain => 'Обычный'; @override String debugFrame_text(String text) { - return '- Текст: \"$text\"'; + return '- Текст: \"$text\"'; } @override - String get debugFrame_hexDump => - 'Шестнадцатеричный дамп:'; + String get debugFrame_hexDump => 'Шестнадцатеричный дамп:'; @override - String get chat_pathManagement => 'Управление маршрутами'; + String get chat_pathManagement => 'Управление маршрутами'; @override - String get chat_ShowAllPaths => 'Показать все пути'; + String get chat_ShowAllPaths => 'Показать все пути'; @override - String get chat_routingMode => 'Режим маршрутизации'; + String get chat_routingMode => 'Режим маршрутизации'; @override - String get chat_autoUseSavedPath => - 'Авто (использовать сохранённый маршрут)'; + String get chat_autoUseSavedPath => 'Авто (использовать сохранённый маршрут)'; @override - String get chat_forceFloodMode => - 'Принудительный режим рассылки'; + String get chat_forceFloodMode => 'Принудительный режим рассылки'; @override String get chat_recentAckPaths => - 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; + 'Недавние подтверждённые маршруты (нажмите, чтобы использовать):'; @override String get chat_pathHistoryFull => - 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; + 'История маршрутов заполнена. Удалите записи, чтобы добавить новые.'; @override - String get chat_hopSingular => 'хоп'; + String get chat_hopSingular => 'хоп'; @override - String get chat_hopPlural => 'хопов'; + String get chat_hopPlural => 'хопов'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', ); return '$count $_temp0'; } @override - String get chat_successes => 'успешно'; + String get chat_successes => 'успешно'; @override - String get chat_removePath => 'Удалить маршрут'; + String get chat_removePath => 'Удалить маршрут'; @override String get chat_noPathHistoryYet => - 'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; + 'История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.'; @override - String get chat_pathActions => 'Действия с маршрутом:'; + String get chat_pathActions => 'Действия с маршрутом:'; @override - String get chat_setCustomPath => - 'Указать маршрут вручную'; + String get chat_setCustomPath => 'Указать маршрут вручную'; @override - String get chat_setCustomPathSubtitle => - 'Вручную задать маршрут передачи'; + String get chat_setCustomPathSubtitle => 'Вручную задать маршрут передачи'; @override - String get chat_clearPath => 'Очистить маршрут'; + String get chat_clearPath => 'Очистить маршрут'; @override String get chat_clearPathSubtitle => - 'Принудительно обновить маршрут при следующей отправке'; + 'Принудительно обновить маршрут при следующей отправке'; @override String get chat_pathCleared => - 'Маршрут очищен. Следующее сообщение обновит маршрут.'; + 'Маршрут очищен. Следующее сообщение обновит маршрут.'; @override String get chat_floodModeSubtitle => - 'Используйте переключатель маршрутизации в панели приложения'; + 'Используйте переключатель маршрутизации в панели приложения'; @override String get chat_floodModeEnabled => - 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; + 'Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.'; @override - String get chat_fullPath => 'Полный маршрут'; + String get chat_fullPath => 'Полный маршрут'; @override String get chat_pathDetailsNotAvailable => - 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; + 'Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.'; @override String chat_pathSetHops(int hopCount, String status) { String _temp0 = intl.Intl.pluralLogic( hopCount, locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', ); - return 'Маршрут установлен: $hopCount $_temp0 — $status'; + return 'Маршрут установлен: $hopCount $_temp0 — $status'; } @override String get chat_pathSavedLocally => - 'Сохранено локально. Подключитесь для синхронизации.'; + 'Сохранено локально. Подключитесь для синхронизации.'; @override - String get chat_pathDeviceConfirmed => - 'Подтверждено устройством.'; + String get chat_pathDeviceConfirmed => 'Подтверждено устройством.'; @override - String get chat_pathDeviceNotConfirmed => - 'Ещё не подтверждено устройством.'; + String get chat_pathDeviceNotConfirmed => 'Ещё не подтверждено устройством.'; @override - String get chat_type => 'Тип'; + String get chat_type => 'Тип'; @override - String get chat_path => 'Маршрут'; + String get chat_path => 'Маршрут'; @override - String get chat_publicKey => 'Публичный ключ'; + String get chat_publicKey => 'Публичный ключ'; @override - String get chat_compressOutgoingMessages => - 'Сжимать исходящие сообщения'; + String get chat_compressOutgoingMessages => 'Сжимать исходящие сообщения'; @override - String get chat_floodForced => - 'Рассылка (принудительно)'; + String get chat_floodForced => 'Рассылка (принудительно)'; @override - String get chat_directForced => 'Прямой (принудительно)'; + String get chat_directForced => 'Прямой (принудительно)'; @override String chat_hopsForced(int count) { - return '$count хоп(ов) (принудительно)'; + return '$count хоп(ов) (принудительно)'; } @override - String get chat_floodAuto => 'Рассылка (авто)'; + String get chat_floodAuto => 'Рассылка (авто)'; @override - String get chat_direct => 'Прямой'; + String get chat_direct => 'Прямой'; @override - String get chat_poiShared => - 'Точка интереса отправлена'; + String get chat_poiShared => 'Точка интереса отправлена'; @override String chat_unread(int count) { - return 'Непрочитанных: $count'; + return 'Непрочитанных: $count'; } @override - String get chat_openLink => 'Открыть ссылку?'; + String get chat_openLink => 'Открыть ссылку?'; @override String get chat_openLinkConfirmation => - 'Хотите открыть эту ссылку в вашем браузере?'; + 'Хотите открыть эту ссылку в вашем браузере?'; @override - String get chat_open => 'Открыть'; + String get chat_open => 'Открыть'; @override String chat_couldNotOpenLink(String url) { - return 'Не удалось открыть ссылку: $url'; + return 'Не удалось открыть ссылку: $url'; } @override - String get chat_invalidLink => - 'Неправильный формат ссылки'; + String get chat_invalidLink => 'Неправильный формат ссылки'; @override - String get map_title => 'Карта нод'; + String get map_title => 'Карта нод'; @override - String get map_lineOfSight => 'Линия видимости'; + String get map_lineOfSight => 'Линия видимости'; @override - String get map_losScreenTitle => 'Линия видимости'; + String get map_losScreenTitle => 'Линия видимости'; @override - String get map_noNodesWithLocation => - 'Нет нод с данными о местоположении'; + String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; @override String get map_nodesNeedGps => - 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; + 'Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте'; @override String map_nodesCount(int count) { - return 'Нод: $count'; + return 'Нод: $count'; } @override String map_pinsCount(int count) { - return 'Меток: $count'; + return 'Меток: $count'; } @override - String get map_chat => 'Чат'; + String get map_chat => 'Чат'; @override - String get map_repeater => 'Репитер'; + String get map_repeater => 'Репитер'; @override - String get map_room => 'Комната'; + String get map_room => 'Комната'; @override - String get map_sensor => 'Сенсор'; + String get map_sensor => 'Сенсор'; @override - String get map_pinDm => 'Метка (ЛС)'; + String get map_pinDm => 'Метка (ЛС)'; @override - String get map_pinPrivate => 'Метка (Приватная)'; + String get map_pinPrivate => 'Метка (Приватная)'; @override - String get map_pinPublic => 'Метка (Публичная)'; + String get map_pinPublic => 'Метка (Публичная)'; @override - String get map_lastSeen => 'Последнее появление'; + String get map_lastSeen => 'Последнее появление'; @override String get map_disconnectConfirm => - 'Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?'; + 'Вы уверены, что хотите отключиться от этого устройства?'; @override - String get map_from => 'От'; + String get map_from => 'От'; @override - String get map_source => 'Источник'; + String get map_source => 'Источник'; @override - String get map_flags => 'Флаги'; + String get map_flags => 'Флаги'; @override - String get map_shareMarkerHere => - 'Поделиться меткой здесь'; + String get map_shareMarkerHere => 'Поделиться меткой здесь'; @override - String get map_pinLabel => 'Метка'; + String get map_pinLabel => 'Метка'; @override - String get map_label => 'Подпись'; + String get map_label => 'Подпись'; @override - String get map_pointOfInterest => 'Точка интереса'; + String get map_pointOfInterest => 'Точка интереса'; @override - String get map_sendToContact => 'Отправить контакту'; + String get map_sendToContact => 'Отправить контакту'; @override - String get map_sendToChannel => 'Отправить в канал'; + String get map_sendToChannel => 'Отправить в канал'; @override - String get map_noChannelsAvailable => - 'Нет доступных каналов'; + String get map_noChannelsAvailable => 'Нет доступных каналов'; @override - String get map_publicLocationShare => - 'Публичная передача местоположения'; + String get map_publicLocationShare => 'Публичная передача местоположения'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Ð’Ñ‹ собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; + return 'Вы собираетесь поделиться местоположением в $channelLabel. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.'; } @override String get map_connectToShareMarkers => - 'Подключитесь к устройству, чтобы делиться метками'; + 'Подключитесь к устройству, чтобы делиться метками'; @override - String get map_filterNodes => 'Фильтр нод'; + String get map_filterNodes => 'Фильтр нод'; @override - String get map_nodeTypes => 'Типы нод'; + String get map_nodeTypes => 'Типы нод'; @override - String get map_chatNodes => 'Чат-ноды'; + String get map_chatNodes => 'Чат-ноды'; @override - String get map_repeaters => 'Репитеры'; + String get map_repeaters => 'Репитеры'; @override - String get map_otherNodes => 'Другие ноды'; + String get map_otherNodes => 'Другие ноды'; @override - String get map_keyPrefix => 'Префикс ключа'; + String get map_keyPrefix => 'Префикс ключа'; @override - String get map_filterByKeyPrefix => - 'Фильтр по префиксу ключа'; + String get map_filterByKeyPrefix => 'Фильтр по префиксу ключа'; @override - String get map_publicKeyPrefix => - 'Префикс публичного ключа'; + String get map_publicKeyPrefix => 'Префикс публичного ключа'; @override - String get map_markers => 'Метки'; + String get map_markers => 'Метки'; @override - String get map_showSharedMarkers => - 'Показывать общие метки'; + String get map_showSharedMarkers => 'Показывать общие метки'; @override - String get map_lastSeenTime => - 'Время последнего появления'; + String get map_lastSeenTime => 'Время последнего появления'; @override - String get map_sharedPin => 'Общая метка'; + String get map_sharedPin => 'Общая метка'; @override - String get map_joinRoom => 'Присоединиться к комнате'; + String get map_joinRoom => 'Присоединиться к комнате'; @override - String get map_manageRepeater => 'Управление репитером'; + String get map_manageRepeater => 'Управление репитером'; @override - String get map_tapToAdd => - 'Нажимайте на узлы, чтобы добавить их в путь.'; + String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.'; @override - String get map_runTrace => - 'Запустить трассировку пути'; + String get map_runTrace => 'Запустить трассировку пути'; @override - String get map_removeLast => 'Удалить последний'; + String get map_removeLast => 'Удалить последний'; @override - String get map_pathTraceCancelled => - 'Отмена трассировки пути'; + String get map_pathTraceCancelled => 'Отмена трассировки пути'; @override - String get mapCache_title => 'Кэш офлайн-карты'; + String get mapCache_title => 'Кэш офлайн-карты'; @override String get mapCache_selectAreaFirst => - 'Сначала выберите область для кэширования'; + 'Сначала выберите область для кэширования'; @override String get mapCache_noTilesToDownload => - 'Нет плиток для загрузки в этой области'; + 'Нет плиток для загрузки в этой области'; @override - String get mapCache_downloadTilesTitle => 'Загрузить плитки'; + String get mapCache_downloadTilesTitle => 'Загрузить плитки'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Загрузить $count плиток для офлайн-использования?'; + return 'Загрузить $count плиток для офлайн-использования?'; } @override - String get mapCache_downloadAction => 'Загрузить'; + String get mapCache_downloadAction => 'Загрузить'; @override String mapCache_cachedTiles(int count) { - return 'Закэшировано $count плиток'; + return 'Закэшировано $count плиток'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Закэшировано $downloaded плиток ($failed не загружено)'; + return 'Закэшировано $downloaded плиток ($failed не загружено)'; } @override - String get mapCache_clearOfflineCacheTitle => - 'Очистить офлайн-кэш'; + String get mapCache_clearOfflineCacheTitle => 'Очистить офлайн-кэш'; @override String get mapCache_clearOfflineCachePrompt => - 'Удалить все закэшированные плитки карты?'; + 'Удалить все закэшированные плитки карты?'; @override - String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; + String get mapCache_offlineCacheCleared => 'Офлайн-кэш очищен'; @override - String get mapCache_noAreaSelected => 'Область не выбрана'; + String get mapCache_noAreaSelected => 'Область не выбрана'; @override - String get mapCache_cacheArea => 'Область кэширования'; + String get mapCache_cacheArea => 'Область кэширования'; @override - String get mapCache_useCurrentView => - 'Использовать текущий вид'; + String get mapCache_useCurrentView => 'Использовать текущий вид'; @override - String get mapCache_zoomRange => 'Диапазон масштаба'; + String get mapCache_zoomRange => 'Диапазон масштаба'; @override String mapCache_estimatedTiles(int count) { - return 'Оценочное количество плиток: $count'; + return 'Оценочное количество плиток: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Загружено $completed из $total'; + return 'Загружено $completed из $total'; } @override - String get mapCache_downloadTilesButton => 'Загрузить плитки'; + String get mapCache_downloadTilesButton => 'Загрузить плитки'; @override - String get mapCache_clearCacheButton => 'Очистить кэш'; + String get mapCache_clearCacheButton => 'Очистить кэш'; @override String mapCache_failedDownloads(int count) { - return 'Неудачных загрузок: $count'; + return 'Неудачных загрузок: $count'; } @override @@ -1649,134 +1607,133 @@ class AppLocalizationsRu extends AppLocalizations { String east, String west, ) { - return 'С $north, Ю $south, Ð’ $east, З $west'; + return 'С $north, Ю $south, В $east, З $west'; } @override - String get time_justNow => 'Только что'; + String get time_justNow => 'Только что'; @override String time_minutesAgo(int minutes) { - return '$minutes мин назад'; + return '$minutes мин назад'; } @override String time_hoursAgo(int hours) { - return '$hours ч назад'; + return '$hours ч назад'; } @override String time_daysAgo(int days) { - return '$days дн. назад'; + return '$days дн. назад'; } @override - String get time_hour => 'час'; + String get time_hour => 'час'; @override - String get time_hours => 'часов'; + String get time_hours => 'часов'; @override - String get time_day => 'день'; + String get time_day => 'день'; @override - String get time_days => 'дней'; + String get time_days => 'дней'; @override - String get time_week => 'неделя'; + String get time_week => 'неделя'; @override - String get time_weeks => 'недель'; + String get time_weeks => 'недель'; @override - String get time_month => 'месяц'; + String get time_month => 'месяц'; @override - String get time_months => 'месяцев'; + String get time_months => 'месяцев'; @override - String get time_minutes => 'минут'; + String get time_minutes => 'минут'; @override - String get time_allTime => 'Всё время'; + String get time_allTime => 'Всё время'; @override - String get dialog_disconnect => 'Отключиться'; + String get dialog_disconnect => 'Отключиться'; @override String get dialog_disconnectConfirm => - 'Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?'; + 'Вы уверены, что хотите отключиться от этого устройства?'; @override - String get login_repeaterLogin => 'Вход в репитер'; + String get login_repeaterLogin => 'Вход в репитер'; @override - String get login_roomLogin => 'Вход на сервер комнат'; + String get login_roomLogin => 'Вход на сервер комнат'; @override - String get login_password => 'Пароль'; + String get login_password => 'Пароль'; @override - String get login_enterPassword => 'Введите пароль'; + String get login_enterPassword => 'Введите пароль'; @override - String get login_savePassword => 'Сохранить пароль'; + String get login_savePassword => 'Сохранить пароль'; @override String get login_savePasswordSubtitle => - 'Пароль будет надёжно сохранён на этом устройстве'; + 'Пароль будет надёжно сохранён на этом устройстве'; @override String get login_repeaterDescription => - 'Введите пароль репитера для доступа к настройкам и статусу.'; + 'Введите пароль репитера для доступа к настройкам и статусу.'; @override String get login_roomDescription => - 'Введите пароль комнаты для доступа к настройкам и статусу.'; + 'Введите пароль комнаты для доступа к настройкам и статусу.'; @override - String get login_routing => 'Маршрутизация'; + String get login_routing => 'Маршрутизация'; @override - String get login_routingMode => 'Режим маршрутизации'; + String get login_routingMode => 'Режим маршрутизации'; @override String get login_autoUseSavedPath => - 'Авто (использовать сохранённый маршрут)'; + 'Авто (использовать сохранённый маршрут)'; @override - String get login_forceFloodMode => - 'Принудительный режим рассылки'; + String get login_forceFloodMode => 'Принудительный режим рассылки'; @override - String get login_managePaths => 'Управление маршрутами'; + String get login_managePaths => 'Управление маршрутами'; @override - String get login_login => 'Войти'; + String get login_login => 'Войти'; @override String login_attempt(int current, int max) { - return 'Попытка $current/$max'; + return 'Попытка $current/$max'; } @override String login_failed(String error) { - return 'Ошибка входа: $error'; + return 'Ошибка входа: $error'; } @override String get login_failedMessage => - 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; + 'Не удалось войти. Либо пароль неверен, либо репитер недоступен.'; @override - String get common_reload => 'Обновить'; + String get common_reload => 'Обновить'; @override - String get common_clear => 'Очистить'; + String get common_clear => 'Очистить'; @override String path_currentPath(String path) { - return 'Текущий маршрут: $path'; + return 'Текущий маршрут: $path'; } @override @@ -1784,185 +1741,171 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'хопов', - many: 'хопов', - few: 'хопа', - one: 'хоп', + other: 'хопов', + many: 'хопов', + few: 'хопа', + one: 'хоп', ); - return 'Используется маршрут из $count $_temp0'; + return 'Используется маршрут из $count $_temp0'; } @override - String get path_enterCustomPath => - 'Введите маршрут вручную'; + String get path_enterCustomPath => 'Введите маршрут вручную'; @override - String get path_currentPathLabel => 'Текущий маршрут'; + String get path_currentPathLabel => 'Текущий маршрут'; @override String get path_hexPrefixInstructions => - 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; + 'Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.'; @override String get path_hexPrefixExample => - 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; + 'Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)'; @override - String get path_labelHexPrefixes => - 'Маршрут (шестнадцатеричные префиксы)'; + String get path_labelHexPrefixes => 'Маршрут (шестнадцатеричные префиксы)'; @override String get path_helperMaxHops => - 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; + 'Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)'; @override - String get path_selectFromContacts => - 'Или выберите из контактов:'; + String get path_selectFromContacts => 'Или выберите из контактов:'; @override - String get path_noRepeatersFound => - 'Репитеры или серверы комнат не найдены.'; + String get path_noRepeatersFound => 'Репитеры или серверы комнат не найдены.'; @override String get path_customPathsRequire => - 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; + 'Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; + return 'Недопустимые шестнадцатеричные префиксы: $prefixes'; } @override - String get path_tooLong => - 'Маршрут слишком длинный. Максимум 64 хопа.'; + String get path_tooLong => 'Маршрут слишком длинный. Максимум 64 хопа.'; @override - String get path_setPath => 'Установить маршрут'; + String get path_setPath => 'Установить маршрут'; @override - String get repeater_management => 'Управление репитером'; + String get repeater_management => 'Управление репитером'; @override - String get room_management => - 'Управление сервером комнат'; + String get room_management => 'Управление сервером комнат'; @override - String get repeater_managementTools => - 'Инструменты управления'; + String get repeater_managementTools => 'Инструменты управления'; @override - String get repeater_status => 'Статус'; + String get repeater_status => 'Статус'; @override String get repeater_statusSubtitle => - 'Просмотр статуса, статистики и соседей репитера'; + 'Просмотр статуса, статистики и соседей репитера'; @override - String get repeater_telemetry => 'Телеметрия'; + String get repeater_telemetry => 'Телеметрия'; @override String get repeater_telemetrySubtitle => - 'Просмотр телеметрии датчиков и системной статистики'; + 'Просмотр телеметрии датчиков и системной статистики'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => - 'Отправка команд репитеру'; + String get repeater_cliSubtitle => 'Отправка команд репитеру'; @override - String get repeater_neighbors => 'Соседи'; + String get repeater_neighbors => 'Соседи'; @override - String get repeater_neighborsSubtitle => - 'Просмотр соседей на нулевом хопе.'; + String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.'; @override - String get repeater_settings => 'Настройки'; + String get repeater_settings => 'Настройки'; @override - String get repeater_settingsSubtitle => - 'Настройка параметров репитера'; + String get repeater_settingsSubtitle => 'Настройка параметров репитера'; @override - String get repeater_statusTitle => 'Статус репитера'; + String get repeater_statusTitle => 'Статус репитера'; @override - String get repeater_routingMode => 'Режим маршрутизации'; + String get repeater_routingMode => 'Режим маршрутизации'; @override String get repeater_autoUseSavedPath => - 'Авто (использовать сохранённый маршрут)'; + 'Авто (использовать сохранённый маршрут)'; @override - String get repeater_forceFloodMode => - 'Принудительный режим рассылки'; + String get repeater_forceFloodMode => 'Принудительный режим рассылки'; @override - String get repeater_pathManagement => - 'Управление маршрутами'; + String get repeater_pathManagement => 'Управление маршрутами'; @override - String get repeater_refresh => 'Обновить'; + String get repeater_refresh => 'Обновить'; @override - String get repeater_statusRequestTimeout => - 'Время ожидания статуса истекло.'; + String get repeater_statusRequestTimeout => 'Время ожидания статуса истекло.'; @override String repeater_errorLoadingStatus(String error) { - return 'Ошибка загрузки статуса: $error'; + return 'Ошибка загрузки статуса: $error'; } @override - String get repeater_systemInformation => - 'Системная информация'; + String get repeater_systemInformation => 'Системная информация'; @override - String get repeater_battery => 'Батарея'; + String get repeater_battery => 'Батарея'; @override - String get repeater_clockAtLogin => 'Время (при входе)'; + String get repeater_clockAtLogin => 'Время (при входе)'; @override - String get repeater_uptime => 'Время работы'; + String get repeater_uptime => 'Время работы'; @override - String get repeater_queueLength => 'Длина очереди'; + String get repeater_queueLength => 'Длина очереди'; @override - String get repeater_debugFlags => 'Флаги отладки'; + String get repeater_debugFlags => 'Флаги отладки'; @override - String get repeater_radioStatistics => 'Радиостатистика'; + String get repeater_radioStatistics => 'Радиостатистика'; @override - String get repeater_lastRssi => 'Последний RSSI'; + String get repeater_lastRssi => 'Последний RSSI'; @override - String get repeater_lastSnr => 'Последний SNR'; + String get repeater_lastSnr => 'Последний SNR'; @override - String get repeater_noiseFloor => 'Уровень шума'; + String get repeater_noiseFloor => 'Уровень шума'; @override - String get repeater_txAirtime => 'Время эфира (передача)'; + String get repeater_txAirtime => 'Время эфира (передача)'; @override - String get repeater_rxAirtime => 'Время эфира (приём)'; + String get repeater_rxAirtime => 'Время эфира (приём)'; @override - String get repeater_packetStatistics => 'Статистика пакетов'; + String get repeater_packetStatistics => 'Статистика пакетов'; @override - String get repeater_sent => 'Отправлено'; + String get repeater_sent => 'Отправлено'; @override - String get repeater_received => 'Получено'; + String get repeater_received => 'Получено'; @override - String get repeater_duplicates => 'Дубликаты'; + String get repeater_duplicates => 'Дубликаты'; @override String repeater_daysHoursMinsSecs( @@ -1971,708 +1914,674 @@ class AppLocalizationsRu extends AppLocalizations { int minutes, int seconds, ) { - return '$days дн. $hoursч $minutesм $secondsс'; + return '$days дн. $hoursч $minutesм $secondsс'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; + return 'Всего: $total, Рассылка: $flood, Прямые: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Рассылка: $flood, Прямые: $direct'; + return 'Рассылка: $flood, Прямые: $direct'; } @override String repeater_duplicatesTotal(int total) { - return 'Всего: $total'; + return 'Всего: $total'; } @override - String get repeater_settingsTitle => 'Настройки репитера'; + String get repeater_settingsTitle => 'Настройки репитера'; @override - String get repeater_basicSettings => 'Основные настройки'; + String get repeater_basicSettings => 'Основные настройки'; @override - String get repeater_repeaterName => 'Имя репитера'; + String get repeater_repeaterName => 'Имя репитера'; @override - String get repeater_repeaterNameHelper => - 'Отображаемое имя этого репитера'; + String get repeater_repeaterNameHelper => 'Отображаемое имя этого репитера'; @override - String get repeater_adminPassword => - 'Пароль администратора'; + String get repeater_adminPassword => 'Пароль администратора'; @override - String get repeater_adminPasswordHelper => - 'Пароль с полным доступом'; + String get repeater_adminPasswordHelper => 'Пароль с полным доступом'; @override - String get repeater_guestPassword => 'Гостевой пароль'; + String get repeater_guestPassword => 'Гостевой пароль'; @override String get repeater_guestPasswordHelper => - 'Пароль для доступа только для чтения'; + 'Пароль для доступа только для чтения'; @override - String get repeater_radioSettings => 'Настройки радио'; + String get repeater_radioSettings => 'Настройки радио'; @override - String get repeater_frequencyMhz => 'Частота (МГц)'; + String get repeater_frequencyMhz => 'Частота (МГц)'; @override - String get repeater_frequencyHelper => '300–2500 МГц'; + String get repeater_frequencyHelper => '300–2500 МГц'; @override - String get repeater_txPower => 'Мощность передачи'; + String get repeater_txPower => 'Мощность передачи'; @override - String get repeater_txPowerHelper => '1–30 дБм'; + String get repeater_txPowerHelper => '1–30 дБм'; @override - String get repeater_bandwidth => 'Полоса пропускания'; + String get repeater_bandwidth => 'Полоса пропускания'; @override - String get repeater_spreadingFactor => - 'Коэффициент расширения'; + String get repeater_spreadingFactor => 'Коэффициент расширения'; @override - String get repeater_codingRate => - 'Коэффициент кодирования'; + String get repeater_codingRate => 'Коэффициент кодирования'; @override - String get repeater_locationSettings => - 'Настройки местоположения'; + String get repeater_locationSettings => 'Настройки местоположения'; @override - String get repeater_latitude => 'Широта'; + String get repeater_latitude => 'Широта'; @override String get repeater_latitudeHelper => - 'Ð’ десятичных градусах (напр., 37.7749)'; + 'В десятичных градусах (напр., 37.7749)'; @override - String get repeater_longitude => 'Долгота'; + String get repeater_longitude => 'Долгота'; @override String get repeater_longitudeHelper => - 'Ð’ десятичных градусах (напр., -122.4194)'; + 'В десятичных градусах (напр., -122.4194)'; @override - String get repeater_features => 'Функции'; + String get repeater_features => 'Функции'; @override - String get repeater_packetForwarding => 'Пересылка пакетов'; + String get repeater_packetForwarding => 'Пересылка пакетов'; @override String get repeater_packetForwardingSubtitle => - 'Разрешить репитеру пересылать пакеты'; + 'Разрешить репитеру пересылать пакеты'; @override - String get repeater_guestAccess => 'Гостевой доступ'; + String get repeater_guestAccess => 'Гостевой доступ'; @override String get repeater_guestAccessSubtitle => - 'Разрешить гостевой доступ только для чтения'; + 'Разрешить гостевой доступ только для чтения'; @override - String get repeater_privacyMode => - 'Режим конфиденциальности'; + String get repeater_privacyMode => 'Режим конфиденциальности'; @override String get repeater_privacyModeSubtitle => - 'Скрывать имя/местоположение в оповещениях'; + 'Скрывать имя/местоположение в оповещениях'; @override - String get repeater_advertisementSettings => - 'Настройки анонсирования'; + String get repeater_advertisementSettings => 'Настройки анонсирования'; @override - String get repeater_localAdvertInterval => - 'Интервал локальных анонсирований'; + String get repeater_localAdvertInterval => 'Интервал локальных анонсирований'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes минут'; + return '$minutes минут'; } @override String get repeater_floodAdvertInterval => - 'Интервал анонсирований рассылкой (flood)'; + 'Интервал анонсирований рассылкой (flood)'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours часов'; + return '$hours часов'; } @override String get repeater_encryptedAdvertInterval => - 'Интервал зашифрованных анонсирований'; + 'Интервал зашифрованных анонсирований'; @override - String get repeater_dangerZone => 'Опасная зона'; + String get repeater_dangerZone => 'Опасная зона'; @override - String get repeater_rebootRepeater => - 'Перезагрузить репитер'; + String get repeater_rebootRepeater => 'Перезагрузить репитер'; @override String get repeater_rebootRepeaterSubtitle => - 'Перезапустить устройство репитера'; + 'Перезапустить устройство репитера'; @override String get repeater_rebootRepeaterConfirm => - 'Ð’Ñ‹ уверены, что хотите перезагрузить этот репитер?'; + 'Вы уверены, что хотите перезагрузить этот репитер?'; @override - String get repeater_regenerateIdentityKey => - 'Пересоздать ключ идентификации'; + String get repeater_regenerateIdentityKey => 'Пересоздать ключ идентификации'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Сгенерировать новую пару публичного/приватного ключей'; + 'Сгенерировать новую пару публичного/приватного ключей'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Это создаст новую идентичность для репитера. Продолжить?'; + 'Это создаст новую идентичность для репитера. Продолжить?'; @override - String get repeater_eraseFileSystem => - 'Стереть файловую систему'; + String get repeater_eraseFileSystem => 'Стереть файловую систему'; @override String get repeater_eraseFileSystemSubtitle => - 'Отформатировать файловую систему репитера'; + 'Отформатировать файловую систему репитера'; @override String get repeater_eraseFileSystemConfirm => - 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; + 'ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!'; @override String get repeater_eraseSerialOnly => - 'Очистка доступна только через последовательную консоль.'; + 'Очистка доступна только через последовательную консоль.'; @override String repeater_commandSent(String command) { - return 'Команда отправлена: $command'; + return 'Команда отправлена: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Ошибка отправки команды: $error'; + return 'Ошибка отправки команды: $error'; } @override - String get repeater_confirm => 'Подтвердить'; + String get repeater_confirm => 'Подтвердить'; @override - String get repeater_settingsSaved => - 'Настройки успешно сохранены'; + String get repeater_settingsSaved => 'Настройки успешно сохранены'; @override String repeater_errorSavingSettings(String error) { - return 'Ошибка сохранения настроек: $error'; + return 'Ошибка сохранения настроек: $error'; } @override - String get repeater_refreshBasicSettings => - 'Обновить основные настройки'; + String get repeater_refreshBasicSettings => 'Обновить основные настройки'; @override - String get repeater_refreshRadioSettings => - 'Обновить настройки радио'; + String get repeater_refreshRadioSettings => 'Обновить настройки радио'; @override - String get repeater_refreshTxPower => - 'Обновить мощность передачи'; + String get repeater_refreshTxPower => 'Обновить мощность передачи'; @override String get repeater_refreshLocationSettings => - 'Обновить настройки местоположения'; + 'Обновить настройки местоположения'; @override - String get repeater_refreshPacketForwarding => - 'Обновить пересылку пакетов'; + String get repeater_refreshPacketForwarding => 'Обновить пересылку пакетов'; @override - String get repeater_refreshGuestAccess => - 'Обновить гостевой доступ'; + String get repeater_refreshGuestAccess => 'Обновить гостевой доступ'; @override - String get repeater_refreshPrivacyMode => - 'Обновить режим конфиденциальности'; + String get repeater_refreshPrivacyMode => 'Обновить режим конфиденциальности'; @override String get repeater_refreshAdvertisementSettings => - 'Обновить настройки анонсирований'; + 'Обновить настройки анонсирований'; @override String repeater_refreshed(String label) { - return '$label обновлён'; + return '$label обновлён'; } @override String repeater_errorRefreshing(String label) { - return 'Ошибка обновления $label'; + return 'Ошибка обновления $label'; } @override - String get repeater_cliTitle => 'CLI репитера'; + String get repeater_cliTitle => 'CLI репитера'; @override - String get repeater_debugNextCommand => - 'Отладка следующей команды'; + String get repeater_debugNextCommand => 'Отладка следующей команды'; @override - String get repeater_commandHelp => 'Справка по командам'; + String get repeater_commandHelp => 'Справка по командам'; @override - String get repeater_clearHistory => 'Очистить историю'; + String get repeater_clearHistory => 'Очистить историю'; @override - String get repeater_noCommandsSent => - 'Команды ещё не отправлялись'; + String get repeater_noCommandsSent => 'Команды ещё не отправлялись'; @override String get repeater_typeCommandOrUseQuick => - 'Введите команду ниже или используйте быстрые команды'; + 'Введите команду ниже или используйте быстрые команды'; @override - String get repeater_enterCommandHint => 'Введите команду...'; + String get repeater_enterCommandHint => 'Введите команду...'; @override - String get repeater_previousCommand => 'Предыдущая команда'; + String get repeater_previousCommand => 'Предыдущая команда'; @override - String get repeater_nextCommand => 'Следующая команда'; + String get repeater_nextCommand => 'Следующая команда'; @override - String get repeater_enterCommandFirst => - 'Сначала введите команду'; + String get repeater_enterCommandFirst => 'Сначала введите команду'; @override - String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; + String get repeater_cliCommandFrameTitle => 'Фрейм CLI-команды'; @override String repeater_cliCommandError(String error) { - return 'Ошибка: $error'; + return 'Ошибка: $error'; } @override - String get repeater_cliQuickGetName => 'Получить имя'; + String get repeater_cliQuickGetName => 'Получить имя'; @override - String get repeater_cliQuickGetRadio => 'Получить радио'; + String get repeater_cliQuickGetRadio => 'Получить радио'; @override - String get repeater_cliQuickGetTx => 'Получить TX'; + String get repeater_cliQuickGetTx => 'Получить TX'; @override - String get repeater_cliQuickNeighbors => 'Соседи'; + String get repeater_cliQuickNeighbors => 'Соседи'; @override - String get repeater_cliQuickVersion => 'Версия'; + String get repeater_cliQuickVersion => 'Версия'; @override - String get repeater_cliQuickAdvertise => 'Анонсировать'; + String get repeater_cliQuickAdvertise => 'Анонсировать'; @override - String get repeater_cliQuickClock => 'Время'; + String get repeater_cliQuickClock => 'Время'; @override - String get repeater_cliHelpAdvert => - 'Отправляет пакет анонсирования'; + String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования'; @override String get repeater_cliHelpReboot => - 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; + 'Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)'; @override String get repeater_cliHelpClock => - 'Показывает текущее время по часам устройства.'; + 'Показывает текущее время по часам устройства.'; @override String get repeater_cliHelpPassword => - 'Устанавливает новый пароль администратора для устройства.'; + 'Устанавливает новый пароль администратора для устройства.'; @override String get repeater_cliHelpVersion => - 'Показывает версию устройства и дату сборки прошивки.'; + 'Показывает версию устройства и дату сборки прошивки.'; @override String get repeater_cliHelpClearStats => - 'Сбрасывает различные счётчики статистики в ноль.'; + 'Сбрасывает различные счётчики статистики в ноль.'; @override String get repeater_cliHelpSetAf => - 'Устанавливает коэффициент времени в эфире.'; + 'Устанавливает коэффициент времени в эфире.'; @override String get repeater_cliHelpSetTx => - 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; + 'Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)'; @override String get repeater_cliHelpSetRepeat => - 'Включает или отключает роль репитера для этой ноды.'; + 'Включает или отключает роль репитера для этой ноды.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; + '(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)'; @override String get repeater_cliHelpSetFloodMax => - 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; + 'Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)'; @override String get repeater_cliHelpSetIntThresh => - 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; + 'Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; + 'Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetMultiAcks => - 'Включает или отключает функцию «двойных ACK».'; + 'Включает или отключает функцию «двойных ACK».'; @override String get repeater_cliHelpSetAdvertInterval => - 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; + 'Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; + 'Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetGuestPassword => - 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; + 'Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)'; @override - String get repeater_cliHelpSetName => - 'Устанавливает имя в оповещениях.'; + String get repeater_cliHelpSetName => 'Устанавливает имя в оповещениях.'; @override String get repeater_cliHelpSetLat => - 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; + 'Устанавливает широту для карты в оповещениях. (десятичные градусы)'; @override String get repeater_cliHelpSetLon => - 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; + 'Устанавливает долготу для карты в оповещениях. (десятичные градусы)'; @override String get repeater_cliHelpSetRadio => - 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; + 'Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.'; @override String get repeater_cliHelpSetRxDelay => - 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; + 'Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.'; @override String get repeater_cliHelpSetTxDelay => - 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; + 'Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; + 'То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.'; @override - String get repeater_cliHelpSetBridgeEnabled => - 'Включить/выключить мост.'; + String get repeater_cliHelpSetBridgeEnabled => 'Включить/выключить мост.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Установить задержку перед ретрансляцией пакетов.'; + 'Установить задержку перед ретрансляцией пакетов.'; @override String get repeater_cliHelpSetBridgeSource => - 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; + 'Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Установить скорость последовательного соединения для мостов RS232.'; + 'Установить скорость последовательного соединения для мостов RS232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Установить секрет моста для мостов ESP-NOW.'; + 'Установить секрет моста для мостов ESP-NOW.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; + 'Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).'; @override String get repeater_cliHelpTempRadio => - 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; + 'Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).'; @override String get repeater_cliHelpSetPerm => - 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; + 'Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)'; @override String get repeater_cliHelpGetBridgeType => - 'Получает тип моста: none, rs232, espnow'; + 'Получает тип моста: none, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Начинает запись пакетов в файловую систему.'; + 'Начинает запись пакетов в файловую систему.'; @override String get repeater_cliHelpLogStop => - 'Останавливает запись пакетов в файловую систему.'; + 'Останавливает запись пакетов в файловую систему.'; @override String get repeater_cliHelpLogErase => - 'Удаляет журналы пакетов из файловой системы.'; + 'Удаляет журналы пакетов из файловой системы.'; @override String get repeater_cliHelpNeighbors => - 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; + 'Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4'; @override String get repeater_cliHelpNeighborRemove => - 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; + 'Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.'; @override String get repeater_cliHelpRegion => - '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; + '(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.'; @override String get repeater_cliHelpRegionLoad => - 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; + 'ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.'; @override String get repeater_cliHelpRegionGet => - 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; + 'Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) \'F\'»'; @override String get repeater_cliHelpRegionPut => - 'Добавляет или обновляет определение региона с заданным именем.'; + 'Добавляет или обновляет определение региона с заданным именем.'; @override String get repeater_cliHelpRegionRemove => - 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; + 'Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)'; @override String get repeater_cliHelpRegionAllowf => - 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; + 'Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)'; @override String get repeater_cliHelpRegionDenyf => - 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; + 'Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)'; @override String get repeater_cliHelpRegionHome => - 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; + 'Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)'; @override String get repeater_cliHelpRegionHomeSet => - 'Устанавливает «домашний» регион.'; + 'Устанавливает «домашний» регион.'; @override String get repeater_cliHelpRegionSave => - 'Сохраняет список/карту регионов в память.'; + 'Сохраняет список/карту регионов в память.'; @override String get repeater_cliHelpGps => - 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; + 'Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.'; @override - String get repeater_cliHelpGpsOnOff => - 'Переключает состояние питания GPS.'; + String get repeater_cliHelpGpsOnOff => 'Переключает состояние питания GPS.'; @override String get repeater_cliHelpGpsSync => - 'Синхронизирует время ноды с часами GPS.'; + 'Синхронизирует время ноды с часами GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; + 'Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.'; @override String get repeater_cliHelpGpsAdvert => - 'Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек'; + 'Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек'; @override String get repeater_cliHelpGpsAdvertSet => - 'Устанавливает конфигурацию передачи местоположения.'; + 'Устанавливает конфигурацию передачи местоположения.'; @override - String get repeater_commandsListTitle => 'Список команд'; + String get repeater_commandsListTitle => 'Список команд'; @override String get repeater_commandsListNote => - 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; + 'ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».'; @override - String get repeater_general => 'Общие'; + String get repeater_general => 'Общие'; @override - String get repeater_settingsCategory => 'Настройки'; + String get repeater_settingsCategory => 'Настройки'; @override - String get repeater_bridge => 'Мост'; + String get repeater_bridge => 'Мост'; @override - String get repeater_logging => 'Журналирование'; + String get repeater_logging => 'Журналирование'; @override - String get repeater_neighborsRepeaterOnly => - 'Соседи (только для репитеров)'; + String get repeater_neighborsRepeaterOnly => 'Соседи (только для репитеров)'; @override String get repeater_regionManagementRepeaterOnly => - 'Управление регионами (только для репитеров)'; + 'Управление регионами (только для репитеров)'; @override String get repeater_regionNote => - 'Команды регионов введены для управления определениями регионов и правами доступа.'; + 'Команды регионов введены для управления определениями регионов и правами доступа.'; @override - String get repeater_gpsManagement => 'Управление GPS'; + String get repeater_gpsManagement => 'Управление GPS'; @override String get repeater_gpsNote => - 'Команда gps введена для управления параметрами, связанными с местоположением.'; + 'Команда gps введена для управления параметрами, связанными с местоположением.'; @override - String get telemetry_receivedData => - 'Полученные телеметрические данные'; + String get telemetry_receivedData => 'Полученные телеметрические данные'; @override - String get telemetry_requestTimeout => - 'Время ожидания телеметрии истекло.'; + String get telemetry_requestTimeout => 'Время ожидания телеметрии истекло.'; @override String telemetry_errorLoading(String error) { - return 'Ошибка загрузки телеметрии: $error'; + return 'Ошибка загрузки телеметрии: $error'; } @override - String get telemetry_noData => - 'Данные телеметрии недоступны.'; + String get telemetry_noData => 'Данные телеметрии недоступны.'; @override String telemetry_channelTitle(int channel) { - return 'Канал $channel'; + return 'Канал $channel'; } @override - String get telemetry_batteryLabel => 'Батарея'; + String get telemetry_batteryLabel => 'Батарея'; @override - String get telemetry_voltageLabel => 'Напряжение'; + String get telemetry_voltageLabel => 'Напряжение'; @override - String get telemetry_mcuTemperatureLabel => 'Температура МК'; + String get telemetry_mcuTemperatureLabel => 'Температура МК'; @override - String get telemetry_temperatureLabel => 'Температура'; + String get telemetry_temperatureLabel => 'Температура'; @override - String get telemetry_currentLabel => 'Ток'; + String get telemetry_currentLabel => 'Ток'; @override String telemetry_batteryValue(int percent, String volts) { - return '$percent% / $voltsÐ’'; + return '$percent% / $voltsВ'; } @override String telemetry_voltageValue(String volts) { - return '$voltsÐ’'; + return '$voltsВ'; } @override String telemetry_currentValue(String amps) { - return '$ampsА'; + return '$ampsА'; } @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => - 'Полученные данные о соседях'; + String get neighbors_receivedData => 'Полученные данные о соседях'; @override String get neighbors_requestTimedOut => - 'Время ожидания данных о соседях истекло.'; + 'Время ожидания данных о соседях истекло.'; @override String neighbors_errorLoading(String error) { - return 'Ошибка загрузки соседей: $error'; + return 'Ошибка загрузки соседей: $error'; } @override - String get neighbors_repeatersNeighbors => 'Соседи репитеров'; + String get neighbors_repeatersNeighbors => 'Соседи репитеров'; @override - String get neighbors_noData => - 'Данные о соседях недоступны.'; + String get neighbors_noData => 'Данные о соседях недоступны.'; @override String neighbors_unknownContact(String pubkey) { - return 'Неизвестный $pubkey'; + return 'Неизвестный $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Слушал(а): $time назад'; + return 'Слушал(а): $time назад'; } @override - String get channelPath_title => 'Путь пакета'; + String get channelPath_title => 'Путь пакета'; @override - String get channelPath_viewMap => 'Посмотреть на карте'; + String get channelPath_viewMap => 'Посмотреть на карте'; @override - String get channelPath_otherObservedPaths => - 'Другие наблюдаемые пути'; + String get channelPath_otherObservedPaths => 'Другие наблюдаемые пути'; @override - String get channelPath_repeaterHops => 'Хопы через репитеры'; + String get channelPath_repeaterHops => 'Хопы через репитеры'; @override String get channelPath_noHopDetails => - 'Детали хопов для этого пакета не предоставлены.'; + 'Детали хопов для этого пакета не предоставлены.'; @override - String get channelPath_messageDetails => 'Детали сообщения'; + String get channelPath_messageDetails => 'Детали сообщения'; @override - String get channelPath_senderLabel => 'Отправитель'; + String get channelPath_senderLabel => 'Отправитель'; @override - String get channelPath_timeLabel => 'Время'; + String get channelPath_timeLabel => 'Время'; @override - String get channelPath_repeatsLabel => 'Повторы'; + String get channelPath_repeatsLabel => 'Повторы'; @override String channelPath_pathLabel(int index) { - return 'Путь $index'; + return 'Путь $index'; } @override - String get channelPath_observedLabel => 'Наблюдаемый'; + String get channelPath_observedLabel => 'Наблюдаемый'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Наблюдаемый путь $index • $hops'; + return 'Наблюдаемый путь $index • $hops'; } @override - String get channelPath_noLocationData => - 'Нет данных о местоположении'; + String get channelPath_noLocationData => 'Нет данных о местоположении'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2685,361 +2594,343 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Неизвестный'; + String get channelPath_unknownPath => 'Неизвестный'; @override - String get channelPath_floodPath => 'Рассылка'; + String get channelPath_floodPath => 'Рассылка'; @override - String get channelPath_directPath => 'Прямой'; + String get channelPath_directPath => 'Прямой'; @override String channelPath_observedZeroOf(int total) { - return '0 из $total хопов'; + return '0 из $total хопов'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed из $total хопов'; + return '$observed из $total хопов'; } @override - String get channelPath_mapTitle => 'Карта пути'; + String get channelPath_mapTitle => 'Карта пути'; @override String get channelPath_noRepeaterLocations => - 'Нет данных о местоположении репитеров для этого пути.'; + 'Нет данных о местоположении репитеров для этого пути.'; @override String channelPath_primaryPath(int index) { - return 'Путь $index (Основной)'; + return 'Путь $index (Основной)'; } @override - String get channelPath_pathLabelTitle => 'Путь'; + String get channelPath_pathLabelTitle => 'Путь'; @override - String get channelPath_observedPathHeader => - 'Наблюдаемый путь'; + String get channelPath_observedPathHeader => 'Наблюдаемый путь'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Детали хопов для этого пакета недоступны.'; + 'Детали хопов для этого пакета недоступны.'; @override - String get channelPath_unknownRepeater => - 'Неизвестный репитер'; + String get channelPath_unknownRepeater => 'Неизвестный репитер'; @override - String get community_title => 'Сообщество'; + String get community_title => 'Сообщество'; @override - String get community_create => 'Создать сообщество'; + String get community_create => 'Создать сообщество'; @override String get community_createDesc => - 'Создать новое сообщество и поделиться через QR-код.'; + 'Создать новое сообщество и поделиться через QR-код.'; @override - String get community_join => 'Присоединиться'; + String get community_join => 'Присоединиться'; @override - String get community_joinTitle => - 'Присоединиться к сообществу'; + String get community_joinTitle => 'Присоединиться к сообществу'; @override String community_joinConfirmation(String name) { - return 'Ð’Ñ‹ хотите присоединиться к сообществу \"$name\"?'; + return 'Вы хотите присоединиться к сообществу \"$name\"?'; } @override - String get community_scanQr => - 'Сканировать QR-код сообщества'; + String get community_scanQr => 'Сканировать QR-код сообщества'; @override String get community_scanInstructions => - 'Наведите камеру на QR-код сообщества'; + 'Наведите камеру на QR-код сообщества'; @override - String get community_showQr => 'Показать QR-код'; + String get community_showQr => 'Показать QR-код'; @override - String get community_publicChannel => - 'Публичный канал сообщества'; + String get community_publicChannel => 'Публичный канал сообщества'; @override - String get community_hashtagChannel => - 'Хэштег-канал сообщества'; + String get community_hashtagChannel => 'Хэштег-канал сообщества'; @override - String get community_name => 'Имя сообщества'; + String get community_name => 'Имя сообщества'; @override - String get community_enterName => - 'Введите имя сообщества'; + String get community_enterName => 'Введите имя сообщества'; @override String community_created(String name) { - return 'Сообщество \"$name\" создано'; + return 'Сообщество \"$name\" создано'; } @override String community_joined(String name) { - return 'Присоединились к сообществу \"$name\"'; + return 'Присоединились к сообществу \"$name\"'; } @override - String get community_qrTitle => 'Поделиться сообществом'; + String get community_qrTitle => 'Поделиться сообществом'; @override String community_qrInstructions(String name) { - return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; + return 'Отсканируйте этот QR-код, чтобы присоединиться к \"$name\"'; } @override String get community_hashtagPrivacyHint => - 'Хэштег-каналы сообщества доступны только его участникам'; + 'Хэштег-каналы сообщества доступны только его участникам'; @override - String get community_invalidQrCode => - 'Недопустимый QR-код сообщества'; + String get community_invalidQrCode => 'Недопустимый QR-код сообщества'; @override - String get community_alreadyMember => 'Уже участник'; + String get community_alreadyMember => 'Уже участник'; @override String community_alreadyMemberMessage(String name) { - return 'Ð’Ñ‹ уже участник сообщества \"$name\".'; + return 'Вы уже участник сообщества \"$name\".'; } @override String get community_addPublicChannel => - 'Добавить публичный канал сообщества'; + 'Добавить публичный канал сообщества'; @override String get community_addPublicChannelHint => - 'Автоматически добавить публичный канал для этого сообщества'; + 'Автоматически добавить публичный канал для этого сообщества'; @override String get community_noCommunities => - 'Ð’Ñ‹ ещё не присоединились ни к одному сообществу'; + 'Вы ещё не присоединились ни к одному сообществу'; @override String get community_scanOrCreate => - 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; + 'Отсканируйте QR-код или создайте сообщество, чтобы начать'; @override - String get community_manageCommunities => - 'Управление сообществами'; + String get community_manageCommunities => 'Управление сообществами'; @override - String get community_delete => 'Покинуть сообщество'; + String get community_delete => 'Покинуть сообщество'; @override String community_deleteConfirm(String name) { - return 'Покинуть \"$name\"?'; + return 'Покинуть \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Это также удалит $count канал(ов) и их сообщения.'; + return 'Это также удалит $count канал(ов) и их сообщения.'; } @override String community_deleted(String name) { - return 'Покинули сообщество \"$name\"'; + return 'Покинули сообщество \"$name\"'; } @override - String get community_regenerateSecret => - 'Пересоздать секрет'; + String get community_regenerateSecret => 'Пересоздать секрет'; @override String community_regenerateSecretConfirm(String name) { - return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; + return 'Пересоздать секретный ключ для \"$name\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.'; } @override - String get community_regenerate => 'Пересоздать'; + String get community_regenerate => 'Пересоздать'; @override String community_secretRegenerated(String name) { - return 'Секрет пересоздан для \"$name\"'; + return 'Секрет пересоздан для \"$name\"'; } @override - String get community_updateSecret => 'Обновить секрет'; + String get community_updateSecret => 'Обновить секрет'; @override String community_secretUpdated(String name) { - return 'Секрет обновлён для \"$name\"'; + return 'Секрет обновлён для \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; + return 'Отсканируйте новый QR-код, чтобы обновить секрет для \"$name\"'; } @override - String get community_addHashtagChannel => - 'Добавить хэштег-канал сообщества'; + String get community_addHashtagChannel => 'Добавить хэштег-канал сообщества'; @override String get community_addHashtagChannelDesc => - 'Добавить хэштег-канал для этого сообщества'; + 'Добавить хэштег-канал для этого сообщества'; @override - String get community_selectCommunity => 'Выбрать сообщество'; + String get community_selectCommunity => 'Выбрать сообщество'; @override - String get community_regularHashtag => 'Обычный хэштег'; + String get community_regularHashtag => 'Обычный хэштег'; @override String get community_regularHashtagDesc => - 'Публичный хэштег (любой может присоединиться)'; + 'Публичный хэштег (любой может присоединиться)'; @override - String get community_communityHashtag => 'Хэштег сообщества'; + String get community_communityHashtag => 'Хэштег сообщества'; @override String get community_communityHashtagDesc => - 'Доступен только участникам сообщества'; + 'Доступен только участникам сообщества'; @override String community_forCommunity(String name) { - return 'Для $name'; + return 'Для $name'; } @override - String get listFilter_tooltip => 'Фильтр и сортировка'; + String get listFilter_tooltip => 'Фильтр и сортировка'; @override - String get listFilter_sortBy => 'Сортировка по'; + String get listFilter_sortBy => 'Сортировка по'; @override - String get listFilter_latestMessages => - 'Последние сообщения'; + String get listFilter_latestMessages => 'Последние сообщения'; @override - String get listFilter_heardRecently => 'Слышали недавно'; + String get listFilter_heardRecently => 'Слышали недавно'; @override - String get listFilter_az => 'По алфавиту'; + String get listFilter_az => 'По алфавиту'; @override - String get listFilter_filters => 'Фильтры'; + String get listFilter_filters => 'Фильтры'; @override - String get listFilter_all => 'Все'; + String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Избранное'; + String get listFilter_favorites => 'Избранное'; @override - String get listFilter_addToFavorites => - 'Добавить в избранное'; + String get listFilter_addToFavorites => 'Добавить в избранное'; @override - String get listFilter_removeFromFavorites => - 'Удалить из избранного'; + String get listFilter_removeFromFavorites => 'Удалить из избранного'; @override - String get listFilter_users => 'Пользователи'; + String get listFilter_users => 'Пользователи'; @override - String get listFilter_repeaters => 'Репитеры'; + String get listFilter_repeaters => 'Репитеры'; @override - String get listFilter_roomServers => 'Серверы комнат'; + String get listFilter_roomServers => 'Серверы комнат'; @override - String get listFilter_unreadOnly => 'Только непрочитанные'; + String get listFilter_unreadOnly => 'Только непрочитанные'; @override - String get listFilter_newGroup => 'Новая группа'; + String get listFilter_newGroup => 'Новая группа'; @override - String get pathTrace_you => 'Ð’Ñ‹'; + String get pathTrace_you => 'Вы'; @override - String get pathTrace_failed => - 'Путь трассировки не выполнен.'; + String get pathTrace_failed => 'Путь трассировки не выполнен.'; @override - String get pathTrace_notAvailable => - 'Трассировка пути недоступна.'; + String get pathTrace_notAvailable => 'Трассировка пути недоступна.'; @override - String get pathTrace_refreshTooltip => 'Обновить Path Trace'; + String get pathTrace_refreshTooltip => 'Обновить Path Trace'; @override String get pathTrace_someHopsNoLocation => - 'Одному или нескольким хмелям не указано местоположение!'; + 'Одному или нескольким хмелям не указано местоположение!'; @override - String get pathTrace_clearTooltip => 'Очистить путь'; + String get pathTrace_clearTooltip => 'Очистить путь'; @override - String get losSelectStartEnd => - 'Выберите начальный и конечный узлы для LOS.'; + String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.'; @override String losRunFailed(String error) { - return 'Проверка прямой видимости не удалась: $error'; + return 'Проверка прямой видимости не удалась: $error'; } @override - String get losClearAllPoints => 'Очистить все точки'; + String get losClearAllPoints => 'Очистить все точки'; @override String get losRunToViewElevationProfile => - 'Запустите LOS, чтобы просмотреть профиль высот.'; + 'Запустите LOS, чтобы просмотреть профиль высот.'; @override - String get losMenuTitle => 'ЛОС Меню'; + String get losMenuTitle => 'ЛОС Меню'; @override String get losMenuSubtitle => - 'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.'; + 'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.'; @override - String get losShowDisplayNodes => - 'Показать узлы отображения'; + String get losShowDisplayNodes => 'Показать узлы отображения'; @override - String get losCustomPoints => 'Пользовательские точки'; + String get losCustomPoints => 'Пользовательские точки'; @override String losCustomPointLabel(int index) { - return 'Пользовательский $index'; + return 'Пользовательский $index'; } @override - String get losPointA => 'Точка А'; + String get losPointA => 'Точка А'; @override - String get losPointB => 'Точка Б'; + String get losPointB => 'Точка Б'; @override String losAntennaA(String value, String unit) { - return 'Антенна А: $value $unit'; + return 'Антенна А: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Антенна Б: $value $unit'; + return 'Антенна Б: $value $unit'; } @override - String get losRun => 'Запустить ЛОС'; + String get losRun => 'Запустить ЛОС'; @override - String get losNoElevationData => 'Нет данных о высоте'; + String get losNoElevationData => 'Нет данных о высоте'; @override String losProfileClear( @@ -3048,7 +2939,7 @@ class AppLocalizationsRu extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit'; + return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit'; } @override @@ -3058,64 +2949,61 @@ class AppLocalizationsRu extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, заблокирован $obstruction $heightUnit'; + return '$distance $distanceUnit, заблокирован $obstruction $heightUnit'; } @override - String get losStatusChecking => 'ЛОС: проверяю...'; + String get losStatusChecking => 'ЛОС: проверяю...'; @override - String get losStatusNoData => 'ЛОС: нет данных'; + String get losStatusNoData => 'ЛОС: нет данных'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.'; + return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.'; } @override String get losErrorElevationUnavailable => - 'Данные о высоте недоступны для одного или нескольких образцов.'; + 'Данные о высоте недоступны для одного или нескольких образцов.'; @override String get losErrorInvalidInput => - 'Неверные данные о точках/высоте для расчета LOS.'; + 'Неверные данные о точках/высоте для расчета LOS.'; @override - String get losRenameCustomPoint => - 'Переименовать пользовательскую точку'; + String get losRenameCustomPoint => 'Переименовать пользовательскую точку'; @override - String get losPointName => 'Имя точки'; + String get losPointName => 'Имя точки'; @override - String get losShowPanelTooltip => 'Показать панель LOS'; + String get losShowPanelTooltip => 'Показать панель LOS'; @override - String get losHidePanelTooltip => 'Скрыть панель LOS'; + String get losHidePanelTooltip => 'Скрыть панель LOS'; @override String get losElevationAttribution => - 'Данные о высоте: Open-Meteo (CC BY 4.0)'; + 'Данные о высоте: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Радиогоризонт'; + String get losLegendRadioHorizon => 'Радиогоризонт'; @override - String get losLegendLosBeam => 'Линия прямой видимости'; + String get losLegendLosBeam => 'Линия прямой видимости'; @override - String get losLegendTerrain => 'Рельеф'; + String get losLegendTerrain => 'Рельеф'; @override - String get losFrequencyLabel => 'Частота'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => - 'Просмотреть детали расчёта'; + String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта'; @override - String get losFrequencyDialogTitle => - 'Расчёт радиогоризонта'; + String get losFrequencyDialogTitle => 'Расчёт радиогоризонта'; @override String losFrequencyDialogDescription( @@ -3124,105 +3012,97 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; + return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; } @override - String get contacts_pathTrace => 'Трассировка пути'; + String get contacts_pathTrace => 'Трассировка пути'; @override - String get contacts_ping => 'Пинговать'; + String get contacts_ping => 'Пинговать'; @override - String get contacts_repeaterPathTrace => - 'Отследить путь к ретранслятору'; + String get contacts_repeaterPathTrace => 'Отследить путь к ретранслятору'; @override - String get contacts_repeaterPing => - 'Пинговать повторитель'; + String get contacts_repeaterPing => 'Пинговать повторитель'; @override - String get contacts_roomPathTrace => - 'Трассировка пути к серверу комнаты'; + String get contacts_roomPathTrace => 'Трассировка пути к серверу комнаты'; @override - String get contacts_roomPing => - 'Пинговать сервер комнаты'; + String get contacts_roomPing => 'Пинговать сервер комнаты'; @override - String get contacts_chatTraceRoute => - 'Трассировка маршрута'; + String get contacts_chatTraceRoute => 'Трассировка маршрута'; @override String contacts_pathTraceTo(String name) { - return 'Показать маршрут к $name'; + return 'Показать маршрут к $name'; } @override - String get contacts_clipboardEmpty => 'Буфер обмена пуст.'; + String get contacts_clipboardEmpty => 'Буфер обмена пуст.'; @override String get contacts_invalidAdvertFormat => - 'Недействительные контактные данные'; + 'Недействительные контактные данные'; @override - String get contacts_contactImported => - 'Контакт был импортирован'; + String get contacts_contactImported => 'Контакт был импортирован'; @override - String get contacts_contactImportFailed => - 'Контакт не удалось импортировать'; + String get contacts_contactImportFailed => 'Контакт не удалось импортировать'; @override - String get contacts_zeroHopAdvert => 'Реклама Zero Hop'; + String get contacts_zeroHopAdvert => 'Реклама Zero Hop'; @override - String get contacts_floodAdvert => 'Рекламный поток'; + String get contacts_floodAdvert => 'Рекламный поток'; @override String get contacts_copyAdvertToClipboard => - 'Копировать рекламу в буфер обмена'; + 'Копировать рекламу в буфер обмена'; @override String get contacts_addContactFromClipboard => - 'Добавить контакт из буфера обмена'; + 'Добавить контакт из буфера обмена'; @override - String get contacts_ShareContact => - 'Копировать контакт в буфер обмена'; + String get contacts_ShareContact => 'Копировать контакт в буфер обмена'; @override String get contacts_ShareContactZeroHop => - 'Поделиться контактом по объявлению'; + 'Поделиться контактом по объявлению'; @override String get contacts_zeroHopContactAdvertSent => - 'Отправлено сообщение по объявлению.'; + 'Отправлено сообщение по объявлению.'; @override String get contacts_zeroHopContactAdvertFailed => - 'Не удалось отправить контакт.'; + 'Не удалось отправить контакт.'; @override String get contacts_contactAdvertCopied => - 'Реклама скопирована в буфер обмена.'; + 'Реклама скопирована в буфер обмена.'; @override String get contacts_contactAdvertCopyFailed => - 'Копирование рекламы в буфер обмена не удалось.'; + 'Копирование рекламы в буфер обмена не удалось.'; @override - String get notification_activityTitle => 'Активность MeshCore'; + String get notification_activityTitle => 'Активность MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'сообщений', - many: 'сообщений', - few: 'сообщения', - one: 'сообщение', + other: 'сообщений', + many: 'сообщений', + few: 'сообщения', + one: 'сообщение', ); return '$count $_temp0'; } @@ -3232,10 +3112,10 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'сообщений канала', - many: 'сообщений канала', - few: 'сообщения канала', - one: 'сообщение канала', + other: 'сообщений канала', + many: 'сообщений канала', + few: 'сообщения канала', + one: 'сообщение канала', ); return '$count $_temp0'; } @@ -3245,87 +3125,78 @@ class AppLocalizationsRu extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'новых узлов', - many: 'новых узлов', - few: 'новых узла', - one: 'новый узел', + other: 'новых узлов', + many: 'новых узлов', + few: 'новых узла', + one: 'новый узел', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Обнаружен новый $contactType'; + return 'Обнаружен новый $contactType'; } @override - String get notification_receivedNewMessage => - 'Получено новое сообщение'; + String get notification_receivedNewMessage => 'Получено новое сообщение'; @override String get settings_gpxExportRepeaters => - 'Экспортировать рипитеры / сервер комнаты в GPX'; + 'Экспортировать рипитеры / сервер комнаты в GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.'; + 'Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.'; @override - String get settings_gpxExportContacts => - 'Экспортировать спутников в GPX'; + String get settings_gpxExportContacts => 'Экспортировать спутников в GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Экспортирует спутников с местоположением в файл GPX.'; + 'Экспортирует спутников с местоположением в файл GPX.'; @override - String get settings_gpxExportAll => - 'Экспортировать все контакты в GPX'; + String get settings_gpxExportAll => 'Экспортировать все контакты в GPX'; @override String get settings_gpxExportAllSubtitle => - 'Экспортирует все контакты с местоположением в файл GPX.'; + 'Экспортирует все контакты с местоположением в файл GPX.'; @override - String get settings_gpxExportSuccess => - 'Успешно экспортирован файл GPX.'; + String get settings_gpxExportSuccess => 'Успешно экспортирован файл GPX.'; @override - String get settings_gpxExportNoContacts => - 'Нет контактов для экспорта.'; + String get settings_gpxExportNoContacts => 'Нет контактов для экспорта.'; @override String get settings_gpxExportNotAvailable => - 'Не поддерживается на вашем устройстве/ОС'; + 'Не поддерживается на вашем устройстве/ОС'; @override - String get settings_gpxExportError => - 'Произошла ошибка при экспорте.'; + String get settings_gpxExportError => 'Произошла ошибка при экспорте.'; @override String get settings_gpxExportRepeatersRoom => - 'Местоположения повторителей и серверов комнат'; + 'Местоположения повторителей и серверов комнат'; @override - String get settings_gpxExportChat => - 'Местоположения спутников'; + String get settings_gpxExportChat => 'Местоположения спутников'; @override - String get settings_gpxExportAllContacts => - 'Все местоположения контактов'; + String get settings_gpxExportAllContacts => 'Все местоположения контактов'; @override String get settings_gpxExportShareText => - 'Данные карты экспортированы из meshcore-open'; + 'Данные карты экспортированы из meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open экспорт данных карты GPX'; + 'meshcore-open экспорт данных карты GPX'; @override - String get snrIndicator_nearByRepeaters => - 'Ближайшие ретрансляторы'; + String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы'; @override - String get snrIndicator_lastSeen => 'Последний раз видели'; + String get snrIndicator_lastSeen => 'Последний раз видели'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 713c860..65e6d36 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -15,85 +15,85 @@ class AppLocalizationsSk extends AppLocalizations { String get nav_contacts => 'Kontakty'; @override - String get nav_channels => 'Kanály'; + String get nav_channels => 'Kanály'; @override String get nav_map => 'Mapa'; @override - String get common_cancel => 'ZruÅ¡iÅ¥'; + String get common_cancel => 'Zrušiť'; @override String get common_ok => 'OK\nDobre'; @override - String get common_connect => 'PripojiÅ¥'; + String get common_connect => 'Pripojiť'; @override - String get common_unknownDevice => 'Neznáme zariadenie'; + String get common_unknownDevice => 'Neznáme zariadenie'; @override - String get common_save => 'UložiÅ¥'; + String get common_save => 'Uložiť'; @override - String get common_delete => 'OdstrániÅ¥'; + String get common_delete => 'Odstrániť'; @override - String get common_close => 'ZavrieÅ¥'; + String get common_close => 'Zavrieť'; @override - String get common_edit => 'UpraviÅ¥'; + String get common_edit => 'Upraviť'; @override - String get common_add => 'PridaÅ¥'; + String get common_add => 'Pridať'; @override String get common_settings => 'Nastavenia'; @override - String get common_disconnect => 'OdpojiÅ¥'; + String get common_disconnect => 'Odpojiť'; @override - String get common_connected => 'Pripojené'; + String get common_connected => 'Pripojené'; @override - String get common_disconnected => 'Odpojené'; + String get common_disconnected => 'Odpojené'; @override - String get common_create => 'VytvoriÅ¥'; + String get common_create => 'Vytvoriť'; @override - String get common_continue => 'PokračovaÅ¥'; + String get common_continue => 'Pokračovať'; @override - String get common_share => 'ZdieľaÅ¥'; + String get common_share => 'Zdieľať'; @override - String get common_copy => 'KopírovaÅ¥'; + String get common_copy => 'Kopírovať'; @override - String get common_retry => 'PokusÅ¥ znova'; + String get common_retry => 'Pokusť znova'; @override - String get common_hide => 'SkryÅ¥'; + String get common_hide => 'Skryť'; @override - String get common_remove => 'OdstrániÅ¥'; + String get common_remove => 'Odstrániť'; @override String get common_enable => 'Povolit'; @override - String get common_disable => 'ZakázaÅ¥'; + String get common_disable => 'Zakázať'; @override - String get common_reboot => 'RestartovaÅ¥'; + String get common_reboot => 'Restartovať'; @override - String get common_loading => 'Načítavanie...'; + String get common_loading => 'Načítavanie...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -119,21 +119,65 @@ class AppLocalizationsSk extends AppLocalizations { @override String get usbScreenSubtitle => - 'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vaÅ¡ej MeshCore uzlu.'; + 'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.'; @override String get usbScreenStatus => 'Vyberte USB zariadenie'; @override String get usbScreenNote => - 'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.'; + 'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.'; @override String get usbScreenEmptyState => - 'NenaÅ¡li sa žiadne USB zariadenia. Pripojte jedno a obnovte.'; + 'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.'; @override - String get scanner_scanning => 'Skrívania zariadení...'; + String get usbErrorPermissionDenied => + 'Žiadosť o prístup cez USB bola zamietnutá.'; + + @override + String get usbErrorDeviceMissing => + 'Vybrané USB zariadenie už nie je dostupné.'; + + @override + String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.'; + + @override + String get usbErrorBusy => + 'Ďalšia požiadavka na pripojenie cez USB je aktuálne v prebiehajúcom procese.'; + + @override + String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.'; + + @override + String get usbErrorOpenFailed => + 'Nepodarilo sa otvoriť vybrané USB zariadenie.'; + + @override + String get usbErrorConnectFailed => + 'Nezvládlo sa pripojenie k vybranému USB zariadeniu.'; + + @override + String get usbErrorUnsupported => + 'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.'; + + @override + String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.'; + + @override + String get usbErrorNoDeviceSelected => + 'Nebolo vybrané žiadne USB zariadenie.'; + + @override + String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.'; + + @override + String get usbErrorConnectTimedOut => + 'Čakal som, kým sa zariadenie neozvými, ale časový limit sa dosiahol.'; + + @override + String get scanner_scanning => 'Skrívania zariadení...'; @override String get scanner_connecting => 'Pripojujem sa...'; @@ -142,19 +186,19 @@ class AppLocalizationsSk extends AppLocalizations { String get scanner_disconnecting => 'Odpojuje sa...'; @override - String get scanner_notConnected => 'Nezriadené'; + String get scanner_notConnected => 'Nezriadené'; @override String scanner_connectedTo(String deviceName) { - return 'Pripojené k $deviceName'; + return 'Pripojené k $deviceName'; } @override - String get scanner_searchingDevices => 'Hľadám zariadenia MeshCore...'; + String get scanner_searchingDevices => 'Hľadám zariadenia MeshCore...'; @override String get scanner_tapToScan => - 'Stlač skenovanie na nájdenie zariadení MeshCore.'; + 'Stlač skenovanie na nájdenie zariadení MeshCore.'; @override String scanner_connectionFailed(String error) { @@ -165,27 +209,27 @@ class AppLocalizationsSk extends AppLocalizations { String get scanner_stop => 'Zastavte'; @override - String get scanner_scan => 'SkončiÅ¥'; + String get scanner_scan => 'Skončiť'; @override - String get scanner_bluetoothOff => 'Bluetooth je vypnutý'; + String get scanner_bluetoothOff => 'Bluetooth je vypnutý'; @override String get scanner_bluetoothOffMessage => - 'Prosím, zapnite Bluetooth, aby ste mohli skenovaÅ¥ pre zariadenia.'; + 'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.'; @override - String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome'; + String get scanner_chromeRequired => 'Vyžaduje sa prehliadač Chrome'; @override String get scanner_chromeRequiredMessage => - 'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.'; + 'Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.'; @override String get scanner_enableBluetooth => 'Povolte Bluetooth'; @override - String get device_quickSwitch => 'Rýchle prepínač'; + String get device_quickSwitch => 'Rýchle prepínač'; @override String get device_meshcore => 'MeshCore'; @@ -194,125 +238,123 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_title => 'Nastavenia'; @override - String get settings_deviceInfo => 'Informácie o zariadení'; + String get settings_deviceInfo => 'Informácie o zariadení'; @override - String get settings_appSettings => 'Nastavenia aplikácie'; + String get settings_appSettings => 'Nastavenia aplikácie'; @override String get settings_appSettingsSubtitle => - 'Upozornenia, správy a nastavenia mapy'; + 'Upozornenia, správy a nastavenia mapy'; @override String get settings_nodeSettings => 'Nastavenia uzla'; @override - String get settings_nodeName => 'Názov uzla'; + String get settings_nodeName => 'Názov uzla'; @override - String get settings_nodeNameNotSet => 'Nezriadené'; + String get settings_nodeNameNotSet => 'Nezriadené'; @override - String get settings_nodeNameHint => 'Zadajte názov uzla'; + String get settings_nodeNameHint => 'Zadajte názov uzla'; @override - String get settings_nodeNameUpdated => 'Meno aktualizované'; + String get settings_nodeNameUpdated => 'Meno aktualizované'; @override - String get settings_radioSettings => 'Nastavenia rádia'; + String get settings_radioSettings => 'Nastavenia rádia'; @override String get settings_radioSettingsSubtitle => - 'Frekvencia, výkon, rozptylovací faktor'; + 'Frekvencia, výkon, rozptylovací faktor'; @override - String get settings_radioSettingsUpdated => - 'Nastavenia rádia aktualizované'; + String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované'; @override String get settings_location => 'Lokalita'; @override - String get settings_locationSubtitle => 'GPS súradnice'; + String get settings_locationSubtitle => 'GPS súradnice'; @override - String get settings_locationUpdated => 'Lokalita aktualizovaná'; + String get settings_locationUpdated => 'Lokalita aktualizovaná'; @override String get settings_locationBothRequired => - 'Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.'; + 'Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.'; @override - String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.'; + String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.'; @override - String get settings_locationGPSEnable => 'AktivovaÅ¥ GPS'; + String get settings_locationGPSEnable => 'Aktivovať GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Povolí automatické aktualizovanie polohy pomocou GPS.'; + 'Povolí automatické aktualizovanie polohy pomocou GPS.'; @override String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)'; @override String get settings_locationIntervalInvalid => - 'Interval musí byÅ¥ aspoň 60 sekúnd a menej ako 86400 sekúnd.'; + 'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.'; @override - String get settings_latitude => 'Súradnica'; + String get settings_latitude => 'Súradnica'; @override - String get settings_longitude => 'Dĺžka'; + String get settings_longitude => 'Dĺžka'; @override - String get settings_privacyMode => 'Režim ochrany súkromia'; + String get settings_privacyMode => 'Režim ochrany súkromia'; @override - String get settings_privacyModeSubtitle => 'SkryÅ¥ meno/poloha v reklamách'; + String get settings_privacyModeSubtitle => 'Skryť meno/poloha v reklamách'; @override String get settings_privacyModeToggle => - 'Prepínač súkromného režimu skryje vaÅ¡e meno a polohu v reklamách.'; + 'Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.'; @override - String get settings_privacyModeEnabled => 'Ochranný režim je povolený.'; + String get settings_privacyModeEnabled => 'Ochranný režim je povolený.'; @override - String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý'; + String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý'; @override - String get settings_actions => 'Možné akcie'; + String get settings_actions => 'Možné akcie'; @override - String get settings_sendAdvertisement => 'OdoslaÅ¥ reklamu'; + String get settings_sendAdvertisement => 'Odoslať reklamu'; @override - String get settings_sendAdvertisementSubtitle => - 'Momentálne priezornejÅ¡ie.'; + String get settings_sendAdvertisementSubtitle => 'Momentálne priezornejšie.'; @override - String get settings_advertisementSent => 'Reklama odeslaná'; + String get settings_advertisementSent => 'Reklama odeslaná'; @override - String get settings_syncTime => 'ÄŒas synchronizácie'; + String get settings_syncTime => 'Čas synchronizácie'; @override String get settings_syncTimeSubtitle => - 'NastaviÅ¥ hodiny zariadenia na čas telefónu'; + 'Nastaviť hodiny zariadenia na čas telefónu'; @override - String get settings_timeSynchronized => 'ÄŒas synchronizovaný'; + String get settings_timeSynchronized => 'Čas synchronizovaný'; @override - String get settings_refreshContacts => 'NačítaÅ¥ Kontakty'; + String get settings_refreshContacts => 'Načítať Kontakty'; @override String get settings_refreshContactsSubtitle => - 'NačítaÅ¥ zoznam kontaktov z zariadenia'; + 'Načítať zoznam kontaktov z zariadenia'; @override - String get settings_rebootDevice => 'RestartovaÅ¥ zariadenie'; + String get settings_rebootDevice => 'Restartovať zariadenie'; @override String get settings_rebootDeviceSubtitle => @@ -320,7 +362,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - 'Ste si istý, že chcete zariadenie reÅ¡tartovaÅ¥? Budete odpojení.'; + 'Ste si istý, že chcete zariadenie reštartovať? Budete odpojení.'; @override String get settings_debug => 'Ladenie'; @@ -330,16 +372,16 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_bleDebugLogSubtitle => - 'Príkazy BLE, odpovede a surové dáta'; + 'Príkazy BLE, odpovede a surové dáta'; @override - String get settings_appDebugLog => 'Záznam ladenia aplikácie'; + String get settings_appDebugLog => 'Záznam ladenia aplikácie'; @override - String get settings_appDebugLogSubtitle => 'Správy z ladenia aplikácie'; + String get settings_appDebugLogSubtitle => 'Správy z ladenia aplikácie'; @override - String get settings_about => 'O nás'; + String get settings_about => 'O nás'; @override String settings_aboutVersion(String version) { @@ -351,11 +393,11 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_aboutDescription => - 'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieÅ¥ové zariadenia.'; + 'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.'; @override String get settings_aboutOpenMeteoAttribution => - 'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)'; + 'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Meno'; @@ -367,16 +409,16 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_infoStatus => 'Status'; @override - String get settings_infoBattery => 'Batéria'; + String get settings_infoBattery => 'Batéria'; @override - String get settings_infoPublicKey => 'Verejný kľúč'; + String get settings_infoPublicKey => 'Verejný kľúč'; @override - String get settings_infoContactsCount => 'Počet kontaktov'; + String get settings_infoContactsCount => 'Počet kontaktov'; @override - String get settings_infoChannelCount => 'Počet kanálov'; + String get settings_infoChannelCount => 'Počet kanálov'; @override String get settings_presets => 'Prednastavenia'; @@ -385,41 +427,39 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_frequency => 'Frekvencia (MHz)'; @override - String get settings_frequencyHelper => '300,0 – 2500,0'; + String get settings_frequencyHelper => '300,0 – 2500,0'; @override - String get settings_frequencyInvalid => 'Neplatná frekvencia (300-2500 MHz)'; + String get settings_frequencyInvalid => 'Neplatná frekvencia (300-2500 MHz)'; @override - String get settings_bandwidth => 'Šírka pásma'; + String get settings_bandwidth => 'Šírka pásma'; @override - String get settings_spreadingFactor => 'Rozptýľovací faktor'; + String get settings_spreadingFactor => 'Rozptýľovací faktor'; @override - String get settings_codingRate => 'Cenový kurz pre programovanie'; + String get settings_codingRate => 'Cenový kurz pre programovanie'; @override - String get settings_txPower => 'TX Výkon (dBm)'; + String get settings_txPower => 'TX Výkon (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => - 'Neplatná hodnota výkonu TX (0-22 dBm)'; + String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)'; @override - String get settings_clientRepeat => - 'Opätovné použitie bez elektrickej siete'; + String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete'; @override String get settings_clientRepeatSubtitle => - 'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.'; + 'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.'; @override String get settings_clientRepeatFreqWarning => - 'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.'; + 'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.'; @override String settings_error(String message) { @@ -427,37 +467,37 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get appSettings_title => 'Nastavenia aplikácie'; + String get appSettings_title => 'Nastavenia aplikácie'; @override - String get appSettings_appearance => 'Vzhľad'; + String get appSettings_appearance => 'Vzhľad'; @override - String get appSettings_theme => 'Téma'; + String get appSettings_theme => 'Téma'; @override - String get appSettings_themeSystem => 'Predvolený systém'; + String get appSettings_themeSystem => 'Predvolený systém'; @override String get appSettings_themeLight => 'Svetlo'; @override - String get appSettings_themeDark => 'Tmavé'; + String get appSettings_themeDark => 'Tmavé'; @override String get appSettings_language => 'Jazyk'; @override - String get appSettings_languageSystem => 'Predvolený systém'; + String get appSettings_languageSystem => 'Predvolený systém'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -466,16 +506,16 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -484,104 +524,103 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'RuÅ¡tina'; + String get appSettings_languageRu => 'Ruština'; @override - String get appSettings_languageUk => 'Ukrajinská'; + String get appSettings_languageUk => 'Ukrajinská'; @override - String get appSettings_enableMessageTracing => 'PovoliÅ¥ sledovanie správ'; + String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ'; @override String get appSettings_enableMessageTracingSubtitle => - 'ZobraziÅ¥ podrobné metadáta o smerovaní a časovaní správ'; + 'Zobraziť podrobné metadáta o smerovaní a časovaní správ'; @override String get appSettings_notifications => 'Upozornenia'; @override - String get appSettings_enableNotifications => 'Povolte Notifikácie'; + String get appSettings_enableNotifications => 'Povolte Notifikácie'; @override String get appSettings_enableNotificationsSubtitle => - 'ZísÅ¥ o upozornenia na správy a inzeráty'; + 'Zísť o upozornenia na správy a inzeráty'; @override String get appSettings_notificationPermissionDenied => - 'Odmietená povolenie notifikácií'; + 'Odmietená povolenie notifikácií'; @override - String get appSettings_notificationsEnabled => 'Upozornenia povolené'; + String get appSettings_notificationsEnabled => 'Upozornenia povolené'; @override - String get appSettings_notificationsDisabled => 'Upozornenia sú vypnuté'; + String get appSettings_notificationsDisabled => 'Upozornenia sú vypnuté'; @override - String get appSettings_messageNotifications => 'Správy od upozornení'; + String get appSettings_messageNotifications => 'Správy od upozornení'; @override String get appSettings_messageNotificationsSubtitle => - 'ZobraziÅ¥ upozornenie pri prijímaní nových správ'; + 'Zobraziť upozornenie pri prijímaní nových správ'; @override - String get appSettings_channelMessageNotifications => - 'Notifikácie z kanálov'; + String get appSettings_channelMessageNotifications => 'Notifikácie z kanálov'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'ZobraziÅ¥ upozornenie pri prijímaní správ z kanálu'; + 'Zobraziť upozornenie pri prijímaní správ z kanálu'; @override String get appSettings_advertisementNotifications => 'Upozornenia na reklamy'; @override String get appSettings_advertisementNotificationsSubtitle => - 'ZobraziÅ¥ upozornenie, keď sa objavia nové uzly.'; + 'Zobraziť upozornenie, keď sa objavia nové uzly.'; @override - String get appSettings_messaging => 'Správy'; + String get appSettings_messaging => 'Správy'; @override - String get appSettings_clearPathOnMaxRetry => 'Vyčisti cestu na Max Retry'; + String get appSettings_clearPathOnMaxRetry => 'Vyčisti cestu na Max Retry'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'ResetovaÅ¥ kontaktný priebeh po 5 neúspeÅ¡ných pokusoch o doručenie'; + 'Resetovať kontaktný priebeh po 5 neúspešných pokusoch o doručenie'; @override String get appSettings_pathsWillBeCleared => - 'Cesty budú vymazané po 5 neúspeÅ¡ných pokusoch.'; + 'Cesty budú vymazané po 5 neúspešných pokusoch.'; @override String get appSettings_pathsWillNotBeCleared => - 'Cesty sa automaticky nevymazávajú.'; + 'Cesty sa automaticky nevymazávajú.'; @override - String get appSettings_autoRouteRotation => 'Automatické prechodové trasy'; + String get appSettings_autoRouteRotation => 'Automatické prechodové trasy'; @override String get appSettings_autoRouteRotationSubtitle => - 'Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.'; + 'Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.'; @override String get appSettings_autoRouteRotationEnabled => - 'Automatické otáčanie trasy povolené'; + 'Automatické otáčanie trasy povolené'; @override String get appSettings_autoRouteRotationDisabled => - 'Automatické prekladanie trás pozastavené'; + 'Automatické prekladanie trás pozastavené'; @override - String get appSettings_battery => 'Batéria'; + String get appSettings_battery => 'Batéria'; @override - String get appSettings_batteryChemistry => 'Chemická zloženie batérie'; + String get appSettings_batteryChemistry => 'Chemická zloženie batérie'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { @@ -590,13 +629,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_batteryChemistryConnectFirst => - 'Pripojte sa k zariadeniu na výber'; + 'Pripojte sa k zariadeniu na výber'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @@ -605,410 +644,408 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_mapDisplay => 'Zobrazenie mapy'; @override - String get appSettings_showRepeaters => 'ZobraziÅ¥ opakovače'; + String get appSettings_showRepeaters => 'Zobraziť opakovače'; @override String get appSettings_showRepeatersSubtitle => - 'ZobraziÅ¥ opakujúce sa uzly na mape'; + 'Zobraziť opakujúce sa uzly na mape'; @override - String get appSettings_showChatNodes => 'ZobraziÅ¥ uzly chatových správ'; + String get appSettings_showChatNodes => 'Zobraziť uzly chatových správ'; @override String get appSettings_showChatNodesSubtitle => - 'ZobraziÅ¥ chatové uzly na mape'; + 'Zobraziť chatové uzly na mape'; @override - String get appSettings_showOtherNodes => 'ZobraziÅ¥ ďalÅ¡ie uzly'; + String get appSettings_showOtherNodes => 'Zobraziť ďalšie uzly'; @override String get appSettings_showOtherNodesSubtitle => - 'ZobraziÅ¥ ostatné typy uzlov na mape'; + 'Zobraziť ostatné typy uzlov na mape'; @override - String get appSettings_timeFilter => 'Filtrovacie ÄŒasové Obdoby'; + String get appSettings_timeFilter => 'Filtrovacie Časové Obdoby'; @override - String get appSettings_timeFilterShowAll => 'ZobraziÅ¥ vÅ¡etky uzly'; + String get appSettings_timeFilterShowAll => 'Zobraziť všetky uzly'; @override String appSettings_timeFilterShowLast(int hours) { - return 'ZobraziÅ¥ uzly z posledných $hours hodín'; + return 'Zobraziť uzly z posledných $hours hodín'; } @override - String get appSettings_mapTimeFilter => 'Filtračný čas mapy'; + String get appSettings_mapTimeFilter => 'Filtračný čas mapy'; @override String get appSettings_showNodesDiscoveredWithin => - 'ZobraziÅ¥ uzly objavené v:'; + 'Zobraziť uzly objavené v:'; @override - String get appSettings_allTime => 'VÅ¡etky časy'; + String get appSettings_allTime => 'Všetky časy'; @override - String get appSettings_lastHour => 'Posledná hodina'; + String get appSettings_lastHour => 'Posledná hodina'; @override - String get appSettings_last6Hours => 'Posledné 6 hodín'; + String get appSettings_last6Hours => 'Posledné 6 hodín'; @override - String get appSettings_last24Hours => 'Posledných 24 hodín'; + String get appSettings_last24Hours => 'Posledných 24 hodín'; @override - String get appSettings_lastWeek => 'Minul týždeň'; + String get appSettings_lastWeek => 'Minul týždeň'; @override - String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; + String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; @override String get appSettings_unitsTitle => 'Jednotky'; @override - String get appSettings_unitsMetric => 'Metrické (m / km)'; + String get appSettings_unitsMetric => 'Metrické (m / km)'; @override - String get appSettings_unitsImperial => 'Imperiálne (ft / mi)'; + String get appSettings_unitsImperial => 'Imperiálne (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasÅ¥'; + String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Vyberená oblasÅ¥ (zoom $minZoom-$maxZoom)'; + return 'Vyberená oblasť (zoom $minZoom-$maxZoom)'; } @override String get appSettings_debugCard => 'Ladenie'; @override - String get appSettings_appDebugLogging => 'Záznamy ladenia aplikácie'; + String get appSettings_appDebugLogging => 'Záznamy ladenia aplikácie'; @override String get appSettings_appDebugLoggingSubtitle => - 'Logovací správy aplikácie pre ladenie'; + 'Logovací správy aplikácie pre ladenie'; @override String get appSettings_appDebugLoggingEnabled => - 'Aplikácia povolila ladenie protokolmi'; + 'Aplikácia povolila ladenie protokolmi'; @override String get appSettings_appDebugLoggingDisabled => - 'Zabudované ladenie aplikácie je vypnuté.'; + 'Zabudované ladenie aplikácie je vypnuté.'; @override String get contacts_title => 'Kontakty'; @override - String get contacts_noContacts => 'Zatiaľ žiadne kontakty.'; + String get contacts_noContacts => 'Zatiaľ žiadne kontakty.'; @override String get contacts_contactsWillAppear => - 'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.'; + 'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.'; @override - String get contacts_unread => 'Neprečítané'; + String get contacts_unread => 'Neprečítané'; @override - String get contacts_searchContactsNoNumber => 'HľadaÅ¥ kontakty...'; + String get contacts_searchContactsNoNumber => 'Hľadať kontakty...'; @override String contacts_searchContacts(int number, String str) { - return 'Vyhľadávajte kontakty...'; + return 'Vyhľadávajte kontakty...'; } @override String contacts_searchFavorites(int number, String str) { - return 'HľadaÅ¥ $number$str obľúbené...'; + return 'Hľadať $number$str obľúbené...'; } @override String contacts_searchUsers(int number, String str) { - return 'HľadaÅ¥ $number$str používateľov...'; + return 'Hľadať $number$str používateľov...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'HľadaÅ¥ $number$str opakovače...'; + return 'Hľadať $number$str opakovače...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Hľadaj $number$str serverov miestností...'; + return 'Hľadaj $number$str serverov miestností...'; } @override - String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty'; + String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty'; @override String get contacts_noContactsFound => - 'Neboli nájdených žiadnych kontaktov ani skupiny.'; + 'Neboli nájdených žiadnych kontaktov ani skupiny.'; @override - String get contacts_deleteContact => 'OdstrániÅ¥ kontakt'; + String get contacts_deleteContact => 'Odstrániť kontakt'; @override String contacts_removeConfirm(String contactName) { - return 'OdstrániÅ¥ $contactName z kontaktov?'; + return 'Odstrániť $contactName z kontaktov?'; } @override - String get contacts_manageRepeater => 'SpravovaÅ¥ opakované zoznamy'; + String get contacts_manageRepeater => 'Spravovať opakované zoznamy'; @override - String get contacts_manageRoom => 'SpravovaÅ¥ server miestnosti'; + String get contacts_manageRoom => 'Spravovať server miestnosti'; @override - String get contacts_roomLogin => 'Prihlásenie do miestnosti'; + String get contacts_roomLogin => 'Prihlásenie do miestnosti'; @override - String get contacts_openChat => 'Otvorené Chat'; + String get contacts_openChat => 'Otvorené Chat'; @override - String get contacts_editGroup => 'UpraviÅ¥ skupinu'; + String get contacts_editGroup => 'Upraviť skupinu'; @override - String get contacts_deleteGroup => 'Vymažť skupinu'; + String get contacts_deleteGroup => 'Vymažť skupinu'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'OdstrániÅ¥ \"$groupName\"?'; + return 'Odstrániť \"$groupName\"?'; } @override - String get contacts_newGroup => 'Nová skupina'; + String get contacts_newGroup => 'Nová skupina'; @override - String get contacts_groupName => 'Názov skupiny'; + String get contacts_groupName => 'Názov skupiny'; @override - String get contacts_groupNameRequired => 'Skupina musí maÅ¥ názov.'; + String get contacts_groupNameRequired => 'Skupina musí mať názov.'; @override String contacts_groupAlreadyExists(String name) { - return 'Skupina \"$name\" už existuje'; + return 'Skupina \"$name\" už existuje'; } @override - String get contacts_filterContacts => 'FiltrovaÅ¥ kontakty...'; + String get contacts_filterContacts => 'Filtrovať kontakty...'; @override String get contacts_noContactsMatchFilter => - 'Žiadne kontakty neodídu vášmu filtru.'; + 'Žiadne kontakty neodídu vášmu filtru.'; @override - String get contacts_noMembers => 'Žiadni členovia'; + String get contacts_noMembers => 'Žiadni členovia'; @override - String get contacts_lastSeenNow => 'Posledné zreteľné zobrazenie teraz'; + String get contacts_lastSeenNow => 'Posledné zreteľné zobrazenie teraz'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Posledné zobrazenie $minutes min. dozadu'; + return 'Posledné zobrazenie $minutes min. dozadu'; } @override String get contacts_lastSeenHourAgo => - 'Zobral/Zabral poslednýkrát pred hodinou.'; + 'Zobral/Zabral poslednýkrát pred hodinou.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Posledné zobrazenie $hours hodín dozadu'; + return 'Posledné zobrazenie $hours hodín dozadu'; } @override String get contacts_lastSeenDayAgo => - 'Zobral/Zabral posledný raz pred 1 dňom.'; + 'Zobral/Zabral posledný raz pred 1 dňom.'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Posledné zobrazenie $days dní dozadu'; + return 'Posledné zobrazenie $days dní dozadu'; } @override - String get channels_title => 'Kanály'; + String get channels_title => 'Kanály'; @override - String get channels_noChannelsConfigured => 'Neobsiahnuté žiadne kanály'; + String get channels_noChannelsConfigured => 'Neobsiahnuté žiadne kanály'; @override - String get channels_addPublicChannel => 'PridaÅ¥ verejný kanál'; + String get channels_addPublicChannel => 'Pridať verejný kanál'; @override - String get channels_searchChannels => 'Vyhľadávajte kanály...'; + String get channels_searchChannels => 'Vyhľadávajte kanály...'; @override - String get channels_noChannelsFound => 'Neobsiahlo sa žiadnych kanálov.'; + String get channels_noChannelsFound => 'Neobsiahlo sa žiadnych kanálov.'; @override String channels_channelIndex(int index) { - return 'Kanál $index'; + return 'Kanál $index'; } @override - String get channels_hashtagChannel => 'Kanál s hashtagom'; + String get channels_hashtagChannel => 'Kanál s hashtagom'; @override - String get channels_public => 'Veľké verejné'; + String get channels_public => 'Veľké verejné'; @override - String get channels_private => 'Osobné'; + String get channels_private => 'Osobné'; @override - String get channels_publicChannel => 'Veľké verejne kanály'; + String get channels_publicChannel => 'Veľké verejne kanály'; @override - String get channels_privateChannel => 'Osobné kanál'; + String get channels_privateChannel => 'Osobné kanál'; @override - String get channels_editChannel => 'UpraviÅ¥ kanál'; + String get channels_editChannel => 'Upraviť kanál'; @override - String get channels_muteChannel => 'StlmiÅ¥ kanál'; + String get channels_muteChannel => 'Stlmiť kanál'; @override - String get channels_unmuteChannel => 'ZruÅ¡iÅ¥ stlmenie kanála'; + String get channels_unmuteChannel => 'Zrušiť stlmenie kanála'; @override - String get channels_deleteChannel => 'OdstrániÅ¥ kanál'; + String get channels_deleteChannel => 'Odstrániť kanál'; @override String channels_deleteChannelConfirm(String name) { - return 'OdstrániÅ¥ \"$name\"? To sa nedá zruÅ¡iÅ¥.'; + return 'Odstrániť \"$name\"? To sa nedá zrušiť.'; } @override String channels_channelDeleteFailed(String name) { - return 'Kanál \"$name\" sa nepodarilo odstrániÅ¥'; + return 'Kanál \"$name\" sa nepodarilo odstrániť'; } @override String channels_channelDeleted(String name) { - return 'Kanál \"$name\" bol odstránený'; + return 'Kanál \"$name\" bol odstránený'; } @override - String get channels_addChannel => 'PridaÅ¥ kanál'; + String get channels_addChannel => 'Pridať kanál'; @override - String get channels_channelIndexLabel => 'Index kanála'; + String get channels_channelIndexLabel => 'Index kanála'; @override - String get channels_channelName => 'Názov kanálu'; + String get channels_channelName => 'Názov kanálu'; @override - String get channels_usePublicChannel => 'Použite verejný kanál'; + String get channels_usePublicChannel => 'Použite verejný kanál'; @override - String get channels_standardPublicPsk => 'Å tandardný verejný PSK'; + String get channels_standardPublicPsk => 'Štandardný verejný PSK'; @override - String get channels_pskHex => 'PSK (Å ifrovacia kľúčik)'; + String get channels_pskHex => 'PSK (Šifrovacia kľúčik)'; @override - String get channels_generateRandomPsk => 'GenerovaÅ¥ náhodný PSK'; + String get channels_generateRandomPsk => 'Generovať náhodný PSK'; @override - String get channels_enterChannelName => 'Prosím, zadajte názov kanála.'; + String get channels_enterChannelName => 'Prosím, zadajte názov kanála.'; @override String get channels_pskMustBe32Hex => - 'PSK musí maÅ¥ 32 hexadecimálových znakov.'; + 'PSK musí mať 32 hexadecimálových znakov.'; @override String channels_channelAdded(String name) { - return 'Kanál \"$name\" pridaný'; + return 'Kanál \"$name\" pridaný'; } @override String channels_editChannelTitle(int index) { - return 'UpraviÅ¥ kanál $index'; + return 'Upraviť kanál $index'; } @override - String get channels_smazCompression => 'Odstránenie kompresie SMAZ'; + String get channels_smazCompression => 'Odstránenie kompresie SMAZ'; @override String channels_channelUpdated(String name) { - return 'Kanál \"$name\" bol aktualizovaný'; + return 'Kanál \"$name\" bol aktualizovaný'; } @override - String get channels_publicChannelAdded => 'Veľký kanál pridaný'; + String get channels_publicChannelAdded => 'Veľký kanál pridaný'; @override - String get channels_sortBy => 'TriediÅ¥ podľa'; + String get channels_sortBy => 'Triediť podľa'; @override - String get channels_sortManual => 'Ručne'; + String get channels_sortManual => 'Ručne'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'Posledné správy'; + String get channels_sortLatestMessages => 'Posledné správy'; @override - String get channels_sortUnread => 'Nezriadené'; + String get channels_sortUnread => 'Nezriadené'; @override - String get channels_createPrivateChannel => 'Vytvorte súkromný kanál'; + String get channels_createPrivateChannel => 'Vytvorte súkromný kanál'; @override String get channels_createPrivateChannelDesc => - 'Zabezpečené pomocou tajného kľúča.'; + 'Zabezpečené pomocou tajného kľúča.'; @override - String get channels_joinPrivateChannel => - 'PripojiÅ¥ sa k súkromnému kanálu'; + String get channels_joinPrivateChannel => 'Pripojiť sa k súkromnému kanálu'; @override - String get channels_joinPrivateChannelDesc => - 'Ručne zadajte tajný kľúč.'; + String get channels_joinPrivateChannelDesc => 'Ručne zadajte tajný kľúč.'; @override - String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu'; + String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu'; @override String get channels_joinPublicChannelDesc => - 'Któvek sátó na tutó kanalizovát.'; + 'Któvek sátó na tutó kanalizovát.'; @override - String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu'; + String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu'; @override String get channels_joinHashtagChannelDesc => - 'Ktoekolikoľvek sa môže pridaÅ¥ do hashtag kanálov.'; + 'Ktoekolikoľvek sa môže pridať do hashtag kanálov.'; @override - String get channels_scanQrCode => 'Skenujte QR kód'; + String get channels_scanQrCode => 'Skenujte QR kód'; @override - String get channels_scanQrCodeComingSoon => 'ÄŒoskoro'; + String get channels_scanQrCodeComingSoon => 'Čoskoro'; @override String get channels_enterHashtag => 'Zadajte hashtag'; @override - String get channels_hashtagHint => 'napr. #tím'; + String get channels_hashtagHint => 'napr. #tím'; @override - String get chat_noMessages => 'Zatiaľ žiadne správy.'; + String get chat_noMessages => 'Zatiaľ žiadne správy.'; @override - String get chat_sendMessageToStart => 'PoÅ¡lite správu na začiatok'; + String get chat_sendMessageToStart => 'Pošlite správu na začiatok'; @override - String get chat_originalMessageNotFound => 'Neznámy pôvodný odkaz.'; + String get chat_originalMessageNotFound => 'Neznámy pôvodný odkaz.'; @override String chat_replyingTo(String name) { - return 'Odpovedám $name'; + return 'Odpovedám $name'; } @override String chat_replyTo(String name) { - return 'OdpovedaÅ¥ $name'; + return 'Odpovedať $name'; } @override @@ -1016,39 +1053,39 @@ class AppLocalizationsSk extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'PoÅ¡li správu $contactName'; + return 'Pošli správu $contactName'; } @override - String get chat_typeMessage => 'NapiÅ¡te správu...'; + String get chat_typeMessage => 'Napište správu...'; @override String chat_messageTooLong(int maxBytes) { - return 'Správa je príliÅ¡ dlhá (max $maxBytes bytov).'; + return 'Správa je príliš dlhá (max $maxBytes bytov).'; } @override - String get chat_messageCopied => 'Správa skopírovaná'; + String get chat_messageCopied => 'Správa skopírovaná'; @override - String get chat_messageDeleted => 'Posolstvo odstránené'; + String get chat_messageDeleted => 'Posolstvo odstránené'; @override String get chat_retryingMessage => 'Pokus o obnovenie'; @override String chat_retryCount(int current, int max) { - return 'SkúsiÅ¥ $current/$max'; + return 'Skúsiť $current/$max'; } @override - String get chat_sendGif => 'OdoslaÅ¥ GIF'; + String get chat_sendGif => 'Odoslať GIF'; @override - String get chat_reply => 'OdpovedaÅ¥'; + String get chat_reply => 'Odpovedať'; @override - String get chat_addReaction => 'PridaÅ¥ Reakciu'; + String get chat_addReaction => 'Pridať Reakciu'; @override String get chat_me => 'Mne'; @@ -1057,7 +1094,7 @@ class AppLocalizationsSk extends AppLocalizations { String get emojiCategorySmileys => 'Emoji'; @override - String get emojiCategoryGestures => 'Gestá'; + String get emojiCategoryGestures => 'Gestá'; @override String get emojiCategoryHearts => 'Srdcia'; @@ -1069,84 +1106,84 @@ class AppLocalizationsSk extends AppLocalizations { String get gifPicker_title => 'Vyberte GIF'; @override - String get gifPicker_searchHint => 'Vyhľadávajte GIFy...'; + String get gifPicker_searchHint => 'Vyhľadávajte GIFy...'; @override - String get gifPicker_poweredBy => 'Napájané spoločnosÅ¥ou GIPHY'; + String get gifPicker_poweredBy => 'Napájané spoločnosťou GIPHY'; @override - String get gifPicker_noGifsFound => 'Neboli nájdené žiadne GIFy.'; + String get gifPicker_noGifsFound => 'Neboli nájdené žiadne GIFy.'; @override - String get gifPicker_failedLoad => 'Nepodarilo sa načítaÅ¥ GIFy'; + String get gifPicker_failedLoad => 'Nepodarilo sa načítať GIFy'; @override - String get gifPicker_failedSearch => 'Nepodarilo sa vyhľadaÅ¥ GIFy'; + String get gifPicker_failedSearch => 'Nepodarilo sa vyhľadať GIFy'; @override - String get gifPicker_noInternet => 'Žiadna internetová konektivita'; + String get gifPicker_noInternet => 'Žiadna internetová konektivita'; @override - String get debugLog_appTitle => 'Záznam ladenia aplikácie'; + String get debugLog_appTitle => 'Záznam ladenia aplikácie'; @override String get debugLog_bleTitle => 'Log BLE Debug'; @override - String get debugLog_copyLog => 'KopírovaÅ¥ záznam'; + String get debugLog_copyLog => 'Kopírovať záznam'; @override - String get debugLog_clearLog => 'VymažaÅ¥ záznam'; + String get debugLog_clearLog => 'Vymažať záznam'; @override - String get debugLog_copied => 'Záznam ladenia skopírovaný'; + String get debugLog_copied => 'Záznam ladenia skopírovaný'; @override - String get debugLog_bleCopied => 'Kopírovaný záznam z BLE.'; + String get debugLog_bleCopied => 'Kopírovaný záznam z BLE.'; @override String get debugLog_noEntries => - 'Zatiaľ neboli zaznamenané žiadne debug logy.'; + 'Zatiaľ neboli zaznamenané žiadne debug logy.'; @override String get debugLog_enableInSettings => - 'Povolte ladicové logy v nastaveniach'; + 'Povolte ladicové logy v nastaveniach'; @override - String get debugLog_frames => 'Rámce'; + String get debugLog_frames => 'Rámce'; @override String get debugLog_rawLogRx => 'Raw Log-RX'; @override - String get debugLog_noBleActivity => 'Zatiaľ žiadna aktivita BLE.'; + String get debugLog_noBleActivity => 'Zatiaľ žiadna aktivita BLE.'; @override String debugFrame_length(int count) { - return 'Dĺžka rámca: $count bajtov'; + return 'Dĺžka rámca: $count bajtov'; } @override String debugFrame_command(String value) { - return 'PrikázÌŒ: 0x$value'; + return 'Prikáž: 0x$value'; } @override - String get debugFrame_textMessageHeader => 'Textová zvesÅ¥:'; + String get debugFrame_textMessageHeader => 'Textová zvesť:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- Cieľový PubKey: $pubKey'; + return '- Cieľový PubKey: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- ÄŒasové označenie: $timestamp'; + return '- Časové označenie: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Žiadne vlajky: 0x$value'; + return '- Žiadne vlajky: 0x$value'; } @override @@ -1158,7 +1195,7 @@ class AppLocalizationsSk extends AppLocalizations { String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Jednoduché'; + String get debugFrame_textTypePlain => 'Jednoduché'; @override String debugFrame_text(String text) { @@ -1169,33 +1206,33 @@ class AppLocalizationsSk extends AppLocalizations { String get debugFrame_hexDump => 'Hex Dump:'; @override - String get chat_pathManagement => 'Správa ciest'; + String get chat_pathManagement => 'Správa ciest'; @override - String get chat_ShowAllPaths => 'ZobraziÅ¥ vÅ¡etky cesty'; + String get chat_ShowAllPaths => 'Zobraziť všetky cesty'; @override - String get chat_routingMode => 'Režim trasy'; + String get chat_routingMode => 'Režim trasy'; @override - String get chat_autoUseSavedPath => 'PoužiÅ¥ uloženú cestu'; + String get chat_autoUseSavedPath => 'Použiť uloženú cestu'; @override String get chat_forceFloodMode => - 'ZavrieÅ¥ režim núdzového povodňového režimu'; + 'Zavrieť režim núdzového povodňového režimu'; @override - String get chat_recentAckPaths => 'Nedávne cesty ACK (klepni na použitie):'; + String get chat_recentAckPaths => 'Nedávne cesty ACK (klepni na použitie):'; @override String get chat_pathHistoryFull => - 'História ciest je plná. Odstráňte záznamy, aby ste mohli pridaÅ¥ nové.'; + 'História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.'; @override String get chat_hopSingular => 'Skok'; @override - String get chat_hopPlural => 'SkákaÅ¥'; + String get chat_hopPlural => 'Skákať'; @override String chat_hopsCount(int count) { @@ -1209,49 +1246,49 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get chat_successes => 'Úspechy'; + String get chat_successes => 'Úspechy'; @override - String get chat_removePath => 'OdstrániÅ¥ cestu'; + String get chat_removePath => 'Odstrániť cestu'; @override String get chat_noPathHistoryYet => - 'Zatiaľ žiadna história trás.\nPoÅ¡lite správu a objavte trasy.'; + 'Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.'; @override String get chat_pathActions => 'Cesty:'; @override - String get chat_setCustomPath => 'NastaviÅ¥ vlastnú cestu'; + String get chat_setCustomPath => 'Nastaviť vlastnú cestu'; @override - String get chat_setCustomPathSubtitle => 'Ručne zadajte trasu.'; + String get chat_setCustomPathSubtitle => 'Ručne zadajte trasu.'; @override - String get chat_clearPath => 'VyčistiÅ¡ cestu'; + String get chat_clearPath => 'Vyčistiš cestu'; @override String get chat_clearPathSubtitle => - 'Znovu nájsÅ¥ vynútene pri nasledujúcej poÅ¡lite'; + 'Znovu nájsť vynútene pri nasledujúcej pošlite'; @override String get chat_pathCleared => - 'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.'; + 'Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.'; @override String get chat_floodModeSubtitle => - 'Použite prepínanie trasy v navigačnom paneli.'; + 'Použite prepínanie trasy v navigačnom paneli.'; @override String get chat_floodModeEnabled => - 'Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.'; + 'Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.'; @override - String get chat_fullPath => 'Celá cesta'; + String get chat_fullPath => 'Celá cesta'; @override String get chat_pathDetailsNotAvailable => - 'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslaÅ¥ správu na obnovenie.'; + 'Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1261,41 +1298,41 @@ class AppLocalizationsSk extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Cesta nastavená: $hopCount $_temp0 - $status'; + return 'Cesta nastavená: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Uložené lokálne. Spojte sa na synchronizáciu.'; + 'Uložené lokálne. Spojte sa na synchronizáciu.'; @override - String get chat_pathDeviceConfirmed => 'Zariadenie potvrdené.'; + String get chat_pathDeviceConfirmed => 'Zariadenie potvrdené.'; @override String get chat_pathDeviceNotConfirmed => - 'Zariadenie zatiaľ nebolo potvrdené.'; + 'Zariadenie zatiaľ nebolo potvrdené.'; @override - String get chat_type => 'NapiÅ¡te'; + String get chat_type => 'Napište'; @override String get chat_path => 'Cesta'; @override - String get chat_publicKey => 'Verejný kľúč'; + String get chat_publicKey => 'Verejný kľúč'; @override - String get chat_compressOutgoingMessages => 'KomprimovaÅ¥ odoslané správy'; + String get chat_compressOutgoingMessages => 'Komprimovať odoslané správy'; @override - String get chat_floodForced => 'Povodňová (nutená)'; + String get chat_floodForced => 'Povodňová (nutená)'; @override - String get chat_directForced => 'Priame (donútené)'; + String get chat_directForced => 'Priame (donútené)'; @override String chat_hopsForced(int count) { - return '$count skokov (nutené)'; + return '$count skokov (nutené)'; } @override @@ -1305,30 +1342,30 @@ class AppLocalizationsSk extends AppLocalizations { String get chat_direct => 'Priamo'; @override - String get chat_poiShared => 'Zdieľané body záujmu'; + String get chat_poiShared => 'Zdieľané body záujmu'; @override String chat_unread(int count) { - return 'Nezriadené: $count'; + return 'Nezriadené: $count'; } @override - String get chat_openLink => 'OtvoriÅ¥ odkaz?'; + String get chat_openLink => 'Otvoriť odkaz?'; @override String get chat_openLinkConfirmation => - 'Chcete otvoriÅ¥ tento odkaz v prehliadači?'; + 'Chcete otvoriť tento odkaz v prehliadači?'; @override - String get chat_open => 'OtvoriÅ¥'; + String get chat_open => 'Otvoriť'; @override String chat_couldNotOpenLink(String url) { - return 'Nepodarilo sa otvoriÅ¥ odkaz: $url'; + return 'Nepodarilo sa otvoriť odkaz: $url'; } @override - String get chat_invalidLink => 'Neplatný formát odkazu'; + String get chat_invalidLink => 'Neplatný formát odkazu'; @override String get map_title => 'Mapa uzlov'; @@ -1340,11 +1377,11 @@ class AppLocalizationsSk extends AppLocalizations { String get map_losScreenTitle => 'Line of Sight'; @override - String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe'; + String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe'; @override String get map_nodesNeedGps => - 'Uholníky musia zdieľaÅ¥ svoje GPS súradnice, aby sa zobrazili na mape.'; + 'Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.'; @override String map_nodesCount(int count) { @@ -1353,7 +1390,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String map_pinsCount(int count) { - return 'Krúžky: $count'; + return 'Krúžky: $count'; } @override @@ -1372,17 +1409,17 @@ class AppLocalizationsSk extends AppLocalizations { String get map_pinDm => 'Zabudka (DM)'; @override - String get map_pinPrivate => 'Zabudka (Osobná)'; + String get map_pinPrivate => 'Zabudka (Osobná)'; @override - String get map_pinPublic => 'ZablokovaÅ¥ (verejne)'; + String get map_pinPublic => 'Zablokovať (verejne)'; @override - String get map_lastSeen => 'Posledné zreteľné zobrazenie'; + String get map_lastSeen => 'Posledné zreteľné zobrazenie'; @override String get map_disconnectConfirm => - 'Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?'; + 'Ste si istý/á, že chcete odpojiť od tohto zariadenia?'; @override String get map_from => 'Od'; @@ -1391,170 +1428,167 @@ class AppLocalizationsSk extends AppLocalizations { String get map_source => 'Zdroj'; @override - String get map_flags => 'Zástavy'; + String get map_flags => 'Zástavy'; @override - String get map_shareMarkerHere => 'Zdieľte značku tu'; + String get map_shareMarkerHere => 'Zdieľte značku tu'; @override - String get map_pinLabel => 'Označka upozornenia'; + String get map_pinLabel => 'Označka upozornenia'; @override - String get map_label => 'Značka'; + String get map_label => 'Značka'; @override - String get map_pointOfInterest => 'Bod záujmu'; + String get map_pointOfInterest => 'Bod záujmu'; @override - String get map_sendToContact => 'PoÅ¡leÅ¥ na kontakt'; + String get map_sendToContact => 'Pošleť na kontakt'; @override - String get map_sendToChannel => 'PoslaÅ¥ do kanálu'; + String get map_sendToChannel => 'Poslať do kanálu'; @override - String get map_noChannelsAvailable => 'Неexistujú žiadne kanály.'; + String get map_noChannelsAvailable => 'Неexistujú žiadne kanály.'; @override - String get map_publicLocationShare => 'ZdieľiÅ¥ verejnú lokalitu'; + String get map_publicLocationShare => 'Zdieľiť verejnú lokalitu'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'ÄŒoskoro budete zdieľaÅ¥ polohu v $channelLabel. Tento kanál je verejný a môže ho vidieÅ¥ každý s PSK.'; + return 'Čoskoro budete zdieľať polohu v $channelLabel. Tento kanál je verejný a môže ho vidieť každý s PSK.'; } @override String get map_connectToShareMarkers => - 'Pripojte sa k zariadeniu na zdieľanie značiek'; + 'Pripojte sa k zariadeniu na zdieľanie značiek'; @override - String get map_filterNodes => 'FiltrovaÅ¥ uzly'; + String get map_filterNodes => 'Filtrovať uzly'; @override String get map_nodeTypes => 'Typy uzlov'; @override - String get map_chatNodes => 'Chatové uzly'; + String get map_chatNodes => 'Chatové uzly'; @override - String get map_repeaters => 'Opakovadlá'; + String get map_repeaters => 'Opakovadlá'; @override - String get map_otherNodes => 'Ostatné uzly'; + String get map_otherNodes => 'Ostatné uzly'; @override - String get map_keyPrefix => 'Päťciferné predpona'; + String get map_keyPrefix => 'Päťciferné predpona'; @override - String get map_filterByKeyPrefix => - 'FiltrovaÅ¥ podľa predponového kľúča'; + String get map_filterByKeyPrefix => 'Filtrovať podľa predponového kľúča'; @override - String get map_publicKeyPrefix => 'Prefix verejného kľúča'; + String get map_publicKeyPrefix => 'Prefix verejného kľúča'; @override - String get map_markers => 'Označkovače'; + String get map_markers => 'Označkovače'; @override - String get map_showSharedMarkers => 'ZobraziÅ¥ zdieľané značky'; + String get map_showSharedMarkers => 'Zobraziť zdieľané značky'; @override - String get map_lastSeenTime => 'Posledný čas sledovania'; + String get map_lastSeenTime => 'Posledný čas sledovania'; @override - String get map_sharedPin => 'Zdieľaný PIN'; + String get map_sharedPin => 'Zdieľaný PIN'; @override - String get map_joinRoom => 'PripojiÅ¥ miestnosÅ¥'; + String get map_joinRoom => 'Pripojiť miestnosť'; @override - String get map_manageRepeater => 'SpravovaÅ¥ Opakovanie'; + String get map_manageRepeater => 'Spravovať Opakovanie'; @override String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.'; @override - String get map_runTrace => 'SpustiÅ¥ trasovaním cesty'; + String get map_runTrace => 'Spustiť trasovaním cesty'; @override - String get map_removeLast => 'OdstrániÅ¥ posledný'; + String get map_removeLast => 'Odstrániť posledný'; @override - String get map_pathTraceCancelled => - 'ZruÅ¡enie stopáže cesty bolo zruÅ¡ené.'; + String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.'; @override - String get mapCache_title => 'Offline Mapa Pamäť'; + String get mapCache_title => 'Offline Mapa Pamäť'; @override - String get mapCache_selectAreaFirst => - 'Vyberte si oblasÅ¥ na predprerúčenie.'; + String get mapCache_selectAreaFirst => 'Vyberte si oblasť na predprerúčenie.'; @override String get mapCache_noTilesToDownload => - 'Žiadne dlaždice na stiahnutie pre toto zóna'; + 'Žiadne dlaždice na stiahnutie pre toto zóna'; @override - String get mapCache_downloadTilesTitle => 'StiahnuÅ¥ dlaždice'; + String get mapCache_downloadTilesTitle => 'Stiahnuť dlaždice'; @override String mapCache_downloadTilesPrompt(int count) { - return 'StiahnuÅ¥ $count dlaždíc na offline použitie?'; + return 'Stiahnuť $count dlaždíc na offline použitie?'; } @override - String get mapCache_downloadAction => 'StiahnuÅ¥'; + String get mapCache_downloadAction => 'Stiahnuť'; @override String mapCache_cachedTiles(int count) { - return 'Zabudené $count dlaždíc'; + return 'Zabudené $count dlaždíc'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Uložené $downloaded dlaždice ($failed neúspeÅ¡né)'; + return 'Uložené $downloaded dlaždice ($failed neúspešné)'; } @override - String get mapCache_clearOfflineCacheTitle => 'VymazaÅ¥ offline uloženie'; + String get mapCache_clearOfflineCacheTitle => 'Vymazať offline uloženie'; @override String get mapCache_clearOfflineCachePrompt => - 'OdstrániÅ¥ vÅ¡etky uložené mapové dlaždice?'; + 'Odstrániť všetky uložené mapové dlaždice?'; @override - String get mapCache_offlineCacheCleared => 'Offline polia vymazaná'; + String get mapCache_offlineCacheCleared => 'Offline polia vymazaná'; @override - String get mapCache_noAreaSelected => 'Neoznačila sa žiadna oblasÅ¥'; + String get mapCache_noAreaSelected => 'Neoznačila sa žiadna oblasť'; @override - String get mapCache_cacheArea => 'Obdĺžková oblasÅ¥'; + String get mapCache_cacheArea => 'Obdĺžková oblasť'; @override - String get mapCache_useCurrentView => 'Použite aktuálny zobrazenie'; + String get mapCache_useCurrentView => 'Použite aktuálny zobrazenie'; @override - String get mapCache_zoomRange => 'Rozsah zväčšenia'; + String get mapCache_zoomRange => 'Rozsah zväčšenia'; @override String mapCache_estimatedTiles(int count) { - return 'Odhadnuté dlaždice: $count'; + return 'Odhadnuté dlaždice: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Stiahnuté $completed / $total'; + return 'Stiahnuté $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'StiahnuÅ¥ dlaždice'; + String get mapCache_downloadTilesButton => 'Stiahnuť dlaždice'; @override - String get mapCache_clearCacheButton => 'VyprázdniÅ¥ VädsÅ¥'; + String get mapCache_clearCacheButton => 'Vyprázdniť Vädsť'; @override String mapCache_failedDownloads(int count) { - return 'NeúspeÅ¡né stiahnutia: $count'; + return 'Neúspešné stiahnutia: $count'; } @override @@ -1568,7 +1602,7 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get time_justNow => 'Príbeh'; + String get time_justNow => 'Príbeh'; @override String time_minutesAgo(int minutes) { @@ -1582,7 +1616,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String time_daysAgo(int days) { - return '$days dní dozadu'; + return '$days dní dozadu'; } @override @@ -1592,16 +1626,16 @@ class AppLocalizationsSk extends AppLocalizations { String get time_hours => 'hodiny'; @override - String get time_day => 'deň'; + String get time_day => 'deň'; @override String get time_days => 'dni'; @override - String get time_week => 'týždeň'; + String get time_week => 'týždeň'; @override - String get time_weeks => 'týždne'; + String get time_weeks => 'týždne'; @override String get time_month => 'mesiac'; @@ -1610,23 +1644,23 @@ class AppLocalizationsSk extends AppLocalizations { String get time_months => 'mesiace'; @override - String get time_minutes => 'minúty'; + String get time_minutes => 'minúty'; @override - String get time_allTime => 'VÅ¡etko ÄŒasom'; + String get time_allTime => 'Všetko Časom'; @override - String get dialog_disconnect => 'OdpojiÅ¥'; + String get dialog_disconnect => 'Odpojiť'; @override String get dialog_disconnectConfirm => - 'Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?'; + 'Ste si istý/á, že chcete odpojiť od tohto zariadenia?'; @override - String get login_repeaterLogin => 'Opätovné prihlásenie'; + String get login_repeaterLogin => 'Opätovné prihlásenie'; @override - String get login_roomLogin => 'Prihlásenie do miestnosti'; + String get login_roomLogin => 'Prihlásenie do miestnosti'; @override String get login_password => 'Heslo'; @@ -1635,62 +1669,62 @@ class AppLocalizationsSk extends AppLocalizations { String get login_enterPassword => 'Zadajte heslo'; @override - String get login_savePassword => 'UložiÅ¥ heslo'; + String get login_savePassword => 'Uložiť heslo'; @override String get login_savePasswordSubtitle => - 'Heslo bude bezpečne uložené na tomto zariadení.'; + 'Heslo bude bezpečne uložené na tomto zariadení.'; @override String get login_repeaterDescription => - 'Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.'; + 'Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.'; @override String get login_roomDescription => - 'Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.'; + 'Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.'; @override - String get login_routing => 'Rútiace'; + String get login_routing => 'Rútiace'; @override - String get login_routingMode => 'Režim trasy'; + String get login_routingMode => 'Režim trasy'; @override - String get login_autoUseSavedPath => 'PoužiÅ¥ uloženú cestu'; + String get login_autoUseSavedPath => 'Použiť uloženú cestu'; @override String get login_forceFloodMode => - 'ZavrieÅ¥ režim núdzového povodňového režimu'; + 'Zavrieť režim núdzového povodňového režimu'; @override - String get login_managePaths => 'SpravovaÅ¥ Cesty'; + String get login_managePaths => 'Spravovať Cesty'; @override - String get login_login => 'PrihlásiÅ¥'; + String get login_login => 'Prihlásiť'; @override String login_attempt(int current, int max) { - return 'Skúšaj $current/$max'; + return 'Skúšaj $current/$max'; } @override String login_failed(String error) { - return 'Prihlásenie zlyhalo: $error'; + return 'Prihlásenie zlyhalo: $error'; } @override String get login_failedMessage => - 'Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.'; + 'Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.'; @override - String get common_reload => 'NačítaÅ¥'; + String get common_reload => 'Načítať'; @override - String get common_clear => 'ZmazaÅ¥'; + String get common_clear => 'Zmazať'; @override String path_currentPath(String path) { - return 'Aktívna cesta: $path'; + return 'Aktívna cesta: $path'; } @override @@ -1701,151 +1735,150 @@ class AppLocalizationsSk extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Používa $count $_temp0 cestu'; + return 'Používa $count $_temp0 cestu'; } @override - String get path_enterCustomPath => 'Zadajte vlastný priebeh'; + String get path_enterCustomPath => 'Zadajte vlastný priebeh'; @override - String get path_currentPathLabel => 'Aktuálny priebeh'; + String get path_currentPathLabel => 'Aktuálny priebeh'; @override String get path_hexPrefixInstructions => - 'Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.'; + 'Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.'; @override String get path_hexPrefixExample => - 'A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)'; + 'A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)'; @override - String get path_labelHexPrefixes => 'Cesty (hexové predpony)'; + String get path_labelHexPrefixes => 'Cesty (hexové predpony)'; @override String get path_helperMaxHops => - 'Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).'; + 'Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).'; @override String get path_selectFromContacts => 'Vyberte sa z kontaktov:'; @override String get path_noRepeatersFound => - 'NenaÅ¡li sa žiadne opakovače ani serverové miestnosti.'; + 'Nenašli sa žiadne opakovače ani serverové miestnosti.'; @override String get path_customPathsRequire => - 'Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášaÅ¥ správky.'; + 'Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášať správky.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Neplatné hexové predpony: $prefixes'; + return 'Neplatné hexové predpony: $prefixes'; } @override String get path_tooLong => - 'Cesta je príliÅ¡ dlhá. Umožnené je maximum 64 skokov.'; + 'Cesta je príliš dlhá. Umožnené je maximum 64 skokov.'; @override - String get path_setPath => 'NastaviÅ¥ cestu'; + String get path_setPath => 'Nastaviť cestu'; @override - String get repeater_management => 'Správa opakérov'; + String get repeater_management => 'Správa opakérov'; @override - String get room_management => 'Správa servera miestnosti'; + String get room_management => 'Správa servera miestnosti'; @override - String get repeater_managementTools => 'Nástroje na správu'; + String get repeater_managementTools => 'Nástroje na správu'; @override String get repeater_status => 'Status'; @override String get repeater_statusSubtitle => - 'ZobraziÅ¥ stav, Å¡tatistiky a susedov repeatera'; + 'Zobraziť stav, štatistiky a susedov repeatera'; @override String get repeater_telemetry => 'Telemetria'; @override String get repeater_telemetrySubtitle => - 'ZobraziÅ¥ telemetriu senzorov a systémových Å¡tatistík'; + 'Zobraziť telemetriu senzorov a systémových štatistík'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'PoÅ¡lite príkazy opakovaču'; + String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču'; @override - String get repeater_neighbors => 'Súsezný'; + String get repeater_neighbors => 'Súsezný'; @override - String get repeater_neighborsSubtitle => - 'ZobraziÅ¥ susedné body bez skokov.'; + String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.'; @override String get repeater_settings => 'Nastavenia'; @override - String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; + String get repeater_settingsSubtitle => 'Konfigurujte parametre opakovača'; @override - String get repeater_statusTitle => 'Status opakého zboru'; + String get repeater_statusTitle => 'Status opakého zboru'; @override - String get repeater_routingMode => 'Režim trasy'; + String get repeater_routingMode => 'Režim trasy'; @override - String get repeater_autoUseSavedPath => 'PoužiÅ¥ uloženú cestu'; + String get repeater_autoUseSavedPath => 'Použiť uloženú cestu'; @override String get repeater_forceFloodMode => - 'ZavrieÅ¥ režim núdzového povodňového režimu'; + 'Zavrieť režim núdzového povodňového režimu'; @override - String get repeater_pathManagement => 'Správa trás'; + String get repeater_pathManagement => 'Správa trás'; @override - String get repeater_refresh => 'ObnoviÅ¥'; + String get repeater_refresh => 'Obnoviť'; @override - String get repeater_statusRequestTimeout => 'Požiadavka stavu zlyhala.'; + String get repeater_statusRequestTimeout => 'Požiadavka stavu zlyhala.'; @override String repeater_errorLoadingStatus(String error) { - return 'Chyba pri načítaní stavu: $error'; + return 'Chyba pri načítaní stavu: $error'; } @override - String get repeater_systemInformation => 'Informácie o systéme'; + String get repeater_systemInformation => 'Informácie o systéme'; @override - String get repeater_battery => 'Batéria'; + String get repeater_battery => 'Batéria'; @override - String get repeater_clockAtLogin => 'ÄŒas (pÅ™i pÅ™ihlášení)'; + String get repeater_clockAtLogin => 'Čas (při přihlášení)'; @override - String get repeater_uptime => 'DostupnosÅ¥'; + String get repeater_uptime => 'Dostupnosť'; @override - String get repeater_queueLength => 'Dĺžka fronty'; + String get repeater_queueLength => 'Dĺžka fronty'; @override - String get repeater_debugFlags => 'Kontrolné značky'; + String get repeater_debugFlags => 'Kontrolné značky'; @override - String get repeater_radioStatistics => 'Rádio Å tatistiky'; + String get repeater_radioStatistics => 'Rádio Štatistiky'; @override - String get repeater_lastRssi => 'Posledná RSSI'; + String get repeater_lastRssi => 'Posledná RSSI'; @override - String get repeater_lastSnr => 'Posledný SNR'; + String get repeater_lastSnr => 'Posledný SNR'; @override - String get repeater_noiseFloor => 'Hladina Å¡umu'; + String get repeater_noiseFloor => 'Hladina šumu'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1854,16 +1887,16 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_rxAirtime => 'RX Airtime'; @override - String get repeater_packetStatistics => 'Statistiky balíka'; + String get repeater_packetStatistics => 'Statistiky balíka'; @override - String get repeater_sent => 'Odoslané'; + String get repeater_sent => 'Odoslané'; @override - String get repeater_received => 'PriÅ¡lo'; + String get repeater_received => 'Prišlo'; @override - String get repeater_duplicates => 'Duplikáty'; + String get repeater_duplicates => 'Duplikáty'; @override String repeater_daysHoursMinsSecs( @@ -1872,17 +1905,17 @@ class AppLocalizationsSk extends AppLocalizations { int minutes, int seconds, ) { - return '$days dní ${hours}h ${minutes}m ${seconds}s'; + return '$days dní ${hours}h ${minutes}m ${seconds}s'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; + return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; + return 'Celkem: $total, Povodňový režim: $flood, Priamy: $direct'; } @override @@ -1896,33 +1929,31 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Nastavenia OpakovacÌŒa'; + String get repeater_settingsTitle => 'Nastavenia Opakovača'; @override - String get repeater_basicSettings => 'Základné nastavenia'; + String get repeater_basicSettings => 'Základné nastavenia'; @override - String get repeater_repeaterName => 'Opakovacia názov'; + String get repeater_repeaterName => 'Opakovacia názov'; @override - String get repeater_repeaterNameHelper => - 'Zobrazenie názvu tohto opakovača'; + String get repeater_repeaterNameHelper => 'Zobrazenie názvu tohto opakovača'; @override - String get repeater_adminPassword => 'Heslo administrátora'; + String get repeater_adminPassword => 'Heslo administrátora'; @override - String get repeater_adminPasswordHelper => 'Celý prístupový heslo'; + String get repeater_adminPasswordHelper => 'Celý prístupový heslo'; @override - String get repeater_guestPassword => 'Heslo hosÅ¥a'; + String get repeater_guestPassword => 'Heslo hosťa'; @override - String get repeater_guestPasswordHelper => - 'Prístupový heslo iba na čítanie'; + String get repeater_guestPasswordHelper => 'Prístupový heslo iba na čítanie'; @override - String get repeater_radioSettings => 'Nastavenia rádia'; + String get repeater_radioSettings => 'Nastavenia rádia'; @override String get repeater_frequencyMhz => 'Frekvencia (MHz)'; @@ -1937,28 +1968,28 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Šírka pásma'; + String get repeater_bandwidth => 'Šírka pásma'; @override - String get repeater_spreadingFactor => 'Šírenie faktoru'; + String get repeater_spreadingFactor => 'Šírenie faktoru'; @override - String get repeater_codingRate => 'RýchlosÅ¥ kódovania'; + String get repeater_codingRate => 'Rýchlosť kódovania'; @override String get repeater_locationSettings => 'Nastavenia polohy'; @override - String get repeater_latitude => 'Súradnica'; + String get repeater_latitude => 'Súradnica'; @override - String get repeater_latitudeHelper => 'Desatinné zložky (napr. 37.7749)'; + String get repeater_latitudeHelper => 'Desatinné zložky (napr. 37.7749)'; @override - String get repeater_longitude => 'Dĺžka'; + String get repeater_longitude => 'Dĺžka'; @override - String get repeater_longitudeHelper => 'Desatinné zložky (napr. -122.4194)'; + String get repeater_longitudeHelper => 'Desatinné zložky (napr. -122.4194)'; @override String get repeater_features => 'Funkcie'; @@ -1968,176 +1999,173 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_packetForwardingSubtitle => - 'Povolte opakovač na smerovanie paketov.'; + 'Povolte opakovač na smerovanie paketov.'; @override - String get repeater_guestAccess => 'Prístup pre hostí'; + String get repeater_guestAccess => 'Prístup pre hostí'; @override String get repeater_guestAccessSubtitle => - 'UmožniÅ¥ prístup hosta iba na čítanie.'; + 'Umožniť prístup hosta iba na čítanie.'; @override - String get repeater_privacyMode => 'Režim ochrany súkromia'; + String get repeater_privacyMode => 'Režim ochrany súkromia'; @override - String get repeater_privacyModeSubtitle => 'SkryÅ¥ meno/poloha v reklamách'; + String get repeater_privacyModeSubtitle => 'Skryť meno/poloha v reklamách'; @override String get repeater_advertisementSettings => 'Nastavenia reklamy'; @override - String get repeater_localAdvertInterval => - 'Lokálna reklamná časová obdoba'; + String get repeater_localAdvertInterval => 'Lokálna reklamná časová obdoba'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes minút'; + return '$minutes minút'; } @override String get repeater_floodAdvertInterval => - 'Interval reklamnej povodňovej reklamy'; + 'Interval reklamnej povodňovej reklamy'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours hodín'; + return '$hours hodín'; } @override - String get repeater_encryptedAdvertInterval => - 'Å ifrovaný reklamný interval'; + String get repeater_encryptedAdvertInterval => 'Šifrovaný reklamný interval'; @override - String get repeater_dangerZone => 'Nebezpečná zóna'; + String get repeater_dangerZone => 'Nebezpečná zóna'; @override - String get repeater_rebootRepeater => 'Restart Repetér'; + String get repeater_rebootRepeater => 'Restart Repetér'; @override - String get repeater_rebootRepeaterSubtitle => - 'ResetovaÅ¥ vysielací prístroj'; + String get repeater_rebootRepeaterSubtitle => 'Resetovať vysielací prístroj'; @override String get repeater_rebootRepeaterConfirm => - 'Ste si istý, že chcete tento opakovač restartovaÅ¥?'; + 'Ste si istý, že chcete tento opakovač restartovať?'; @override - String get repeater_regenerateIdentityKey => 'GenerovaÅ¥ kľúč identity'; + String get repeater_regenerateIdentityKey => 'Generovať kľúč identity'; @override String get repeater_regenerateIdentityKeySubtitle => - 'GenerovaÅ¥ nový pár verejných/privátnych kľúčov'; + 'Generovať nový pár verejných/privátnych kľúčov'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Toto vytvorí nový identitu pre opakovač. PokračovaÅ¥?'; + 'Toto vytvorí nový identitu pre opakovač. Pokračovať?'; @override - String get repeater_eraseFileSystem => 'VymažaÅ¥ Systémový ReÅ¥azec'; + String get repeater_eraseFileSystem => 'Vymažať Systémový Reťazec'; @override String get repeater_eraseFileSystemSubtitle => - 'FormátovaÅ¥ systém opakujúcich sa súborov'; + 'Formátovať systém opakujúcich sa súborov'; @override String get repeater_eraseFileSystemConfirm => - 'VAROVANIE: Toto zmaže vÅ¡etky dáta na opakovači. To sa nedá zruÅ¡iÅ¥!'; + 'VAROVANIE: Toto zmaže všetky dáta na opakovači. To sa nedá zrušiť!'; @override String get repeater_eraseSerialOnly => - 'Odstránenie je dostupné len cez sériové rozhranie.'; + 'Odstránenie je dostupné len cez sériové rozhranie.'; @override String repeater_commandSent(String command) { - return 'Poforovaný príkaz: $command'; + return 'Poforovaný príkaz: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Chyba pri odeslaní príkazu: $error'; + return 'Chyba pri odeslaní príkazu: $error'; } @override - String get repeater_confirm => 'PotvrdiÅ¥'; + String get repeater_confirm => 'Potvrdiť'; @override - String get repeater_settingsSaved => 'Nastavenia boli uložené úspeÅ¡ne.'; + String get repeater_settingsSaved => 'Nastavenia boli uložené úspešne.'; @override String repeater_errorSavingSettings(String error) { - return 'Chyba pri ukladaní nastavení: $error'; + return 'Chyba pri ukladaní nastavení: $error'; } @override - String get repeater_refreshBasicSettings => 'ObnoviÅ¥ základné nastavenia'; + String get repeater_refreshBasicSettings => 'Obnoviť základné nastavenia'; @override - String get repeater_refreshRadioSettings => 'ObnoviÅ¥ Nastavenia Rádií'; + String get repeater_refreshRadioSettings => 'Obnoviť Nastavenia Rádií'; @override - String get repeater_refreshTxPower => 'ObnoviÅ¥ TX napájanie'; + String get repeater_refreshTxPower => 'Obnoviť TX napájanie'; @override - String get repeater_refreshLocationSettings => 'ObnoviÅ¥ Nastavenia Miesta'; + String get repeater_refreshLocationSettings => 'Obnoviť Nastavenia Miesta'; @override - String get repeater_refreshPacketForwarding => 'ObnoviÅ¥ smerovanie paketov'; + String get repeater_refreshPacketForwarding => 'Obnoviť smerovanie paketov'; @override - String get repeater_refreshGuestAccess => 'ObnoviÅ¥ prístup hosÅ¥a'; + String get repeater_refreshGuestAccess => 'Obnoviť prístup hosťa'; @override - String get repeater_refreshPrivacyMode => 'ObnoviÅ¥ Ochranný režim'; + String get repeater_refreshPrivacyMode => 'Obnoviť Ochranný režim'; @override String get repeater_refreshAdvertisementSettings => - 'ObnoviÅ¥ nastavenia reklamy'; + 'Obnoviť nastavenia reklamy'; @override String repeater_refreshed(String label) { - return '$label sa znova načítalo'; + return '$label sa znova načítalo'; } @override String repeater_errorRefreshing(String label) { - return 'Chyba pri obnovení $label'; + return 'Chyba pri obnovení $label'; } @override String get repeater_cliTitle => 'Opakovacia CLI'; @override - String get repeater_debugNextCommand => 'Oprava Nasledujúceho Príkaz'; + String get repeater_debugNextCommand => 'Oprava Nasledujúceho Príkaz'; @override String get repeater_commandHelp => 'Pomoc'; @override - String get repeater_clearHistory => 'VymazaÅ¥ históriu'; + String get repeater_clearHistory => 'Vymazať históriu'; @override String get repeater_noCommandsSent => - 'Zatiaľ neboli odeslané žiadne príkazy.'; + 'Zatiaľ neboli odeslané žiadne príkazy.'; @override String get repeater_typeCommandOrUseQuick => - 'Zadajte príkaz nižšie alebo použite rýchle príkazy'; + 'Zadajte príkaz nižšie alebo použite rýchle príkazy'; @override - String get repeater_enterCommandHint => 'Zadajte príkaz...'; + String get repeater_enterCommandHint => 'Zadajte príkaz...'; @override - String get repeater_previousCommand => 'Predchádzajúci príkaz'; + String get repeater_previousCommand => 'Predchádzajúci príkaz'; @override - String get repeater_nextCommand => 'Nasledujúci príkaz'; + String get repeater_nextCommand => 'Nasledujúci príkaz'; @override - String get repeater_enterCommandFirst => 'Zadajte najprv príkaz'; + String get repeater_enterCommandFirst => 'Zadajte najprv príkaz'; @override - String get repeater_cliCommandFrameTitle => 'Rámok Príkaz CLI'; + String get repeater_cliCommandFrameTitle => 'Rámok Príkaz CLI'; @override String repeater_cliCommandError(String error) { @@ -2145,16 +2173,16 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'ZísÅ¥ meno'; + String get repeater_cliQuickGetName => 'Zísť meno'; @override - String get repeater_cliQuickGetRadio => 'ZísÅ¥ po rádiu'; + String get repeater_cliQuickGetRadio => 'Zísť po rádiu'; @override - String get repeater_cliQuickGetTx => 'ZísÅ¥ TX'; + String get repeater_cliQuickGetTx => 'Zísť TX'; @override - String get repeater_cliQuickNeighbors => 'Súsezný'; + String get repeater_cliQuickNeighbors => 'Súsezný'; @override String get repeater_cliQuickVersion => 'Verzia'; @@ -2166,224 +2194,224 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_cliQuickClock => 'Hodiny'; @override - String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; + String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.'; @override String get repeater_cliHelpReboot => - 'Resetuje zariadenie. (pozor, môže dôjsÅ¥ k \'Timeoutu\', čo je normálne)'; + 'Resetuje zariadenie. (pozor, môže dôjsť k \'Timeoutu\', čo je normálne)'; @override String get repeater_cliHelpClock => - 'Zobrazuje aktuálny čas podľa hodiniek zariadenia.'; + 'Zobrazuje aktuálny čas podľa hodiniek zariadenia.'; @override String get repeater_cliHelpPassword => - 'Nastaví nový administrátorský prístupový údaj pre zariadenie.'; + 'Nastaví nový administrátorský prístupový údaj pre zariadenie.'; @override String get repeater_cliHelpVersion => - 'Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.'; + 'Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.'; @override String get repeater_cliHelpClearStats => - 'Resetuje rôzne Å¡tatistické počítadlá na nulu.'; + 'Resetuje rôzne štatistické počítadlá na nulu.'; @override - String get repeater_cliHelpSetAf => 'Nastavuje časový faktor.'; + String get repeater_cliHelpSetAf => 'Nastavuje časový faktor.'; @override String get repeater_cliHelpSetTx => - 'Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reÅ¡tart na aplikáciu)'; + 'Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reštart na aplikáciu)'; @override String get repeater_cliHelpSetRepeat => - 'Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.'; + 'Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Server miestnosti) Ak je \'zapnuté\', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielaÅ¥ správu do miestnosti. (iba čítaÅ¥).'; + '(Server miestnosti) Ak je \'zapnuté\', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielať správu do miestnosti. (iba čítať).'; @override String get repeater_cliHelpSetFloodMax => - 'Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)'; + 'Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)'; @override String get repeater_cliHelpSetIntThresh => - 'Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.'; + 'Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Nastavuje interval na reÅ¡tartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.'; + 'Nastavuje interval na reštartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.'; @override String get repeater_cliHelpSetMultiAcks => - 'Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".'; + 'Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".'; @override String get repeater_cliHelpSetAdvertInterval => - 'Nastavuje interval časovača v minútach na odoÅ¡le miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.'; + 'Nastavuje interval časovača v minútach na odošle miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.'; + 'Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.'; @override String get repeater_cliHelpSetGuestPassword => - 'Nastavuje/aktualizuje heslo hosÅ¥a. (pre opakované pripojenia môžu hosÅ¥ovské prihlásenia posielaÅ¥ požadanie \"Get Stats\")'; + 'Nastavuje/aktualizuje heslo hosťa. (pre opakované pripojenia môžu hosťovské prihlásenia posielať požadanie \"Get Stats\")'; @override - String get repeater_cliHelpSetName => 'Nastaví názov reklamy.'; + String get repeater_cliHelpSetName => 'Nastaví názov reklamy.'; @override String get repeater_cliHelpSetLat => - 'Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)'; + 'Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)'; @override String get repeater_cliHelpSetLon => - 'Nastavuje longitudinu reklamnej mapy. (desatinné stupne)'; + 'Nastavuje longitudinu reklamnej mapy. (desatinné stupne)'; @override String get repeater_cliHelpSetRadio => - 'Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.'; + 'Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.'; @override String get repeater_cliHelpSetRxDelay => - 'Nastavenia (experimentálne) základné (musi byÅ¥ > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.'; + 'Nastavenia (experimentálne) základné (musi byť > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.'; @override String get repeater_cliHelpSetTxDelay => - 'Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiÅ¥ pravdepodobnosÅ¥ kolízii).'; + 'Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiť pravdepodobnosť kolízii).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.'; + 'Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'AktivovaÅ¥/ZatváraÅ¥ most.'; + String get repeater_cliHelpSetBridgeEnabled => 'Aktivovať/Zatvárať most.'; @override String get repeater_cliHelpSetBridgeDelay => - 'NastaviÅ¥ odklad pred retransmisiou paketov.'; + 'Nastaviť odklad pred retransmisiou paketov.'; @override String get repeater_cliHelpSetBridgeSource => - 'Zvolte, či bude most retransmitovaÅ¥ prijaté alebo vysielané balíčky.'; + 'Zvolte, či bude most retransmitovať prijaté alebo vysielané balíčky.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Nastavte sériový link baudrate pre rs232 mosty.'; + 'Nastavte sériový link baudrate pre rs232 mosty.'; @override String get repeater_cliHelpSetBridgeSecret => - 'NastaviÅ¥ tajomstvo mosta pre eshnow mosty.'; + 'Nastaviť tajomstvo mosta pre eshnow mosty.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).'; + 'Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).'; @override String get repeater_cliHelpTempRadio => - 'Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).'; + 'Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).'; @override String get repeater_cliHelpSetPerm => - 'Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).'; + 'Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).'; @override String get repeater_cliHelpGetBridgeType => - 'ZísÅ¥ typ mosta: žiadny, rs232, espnow'; + 'Zísť typ mosta: žiadny, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Začína protokolovanie balíkov do systému súborov.'; + 'Začína protokolovanie balíkov do systému súborov.'; @override String get repeater_cliHelpLogStop => - 'Zastaví protokolovanie paketov do systémového súboru.'; + 'Zastaví protokolovanie paketov do systémového súboru.'; @override String get repeater_cliHelpLogErase => - 'Odstráni záznamy z balíkov z systému súborov.'; + 'Odstráni záznamy z balíkov z systému súborov.'; @override String get repeater_cliHelpNeighbors => - 'Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4'; + 'Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.'; + 'Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.'; @override String get repeater_cliHelpRegion => - '(len sériál) Zobrazuje vÅ¡etky definované regióny a aktuálne povolenia pre povodňové situácie.'; + '(len sériál) Zobrazuje všetky definované regióny a aktuálne povolenia pre povodňové situácie.'; @override String get repeater_cliHelpRegionLoad => - 'Poznámka: toto je Å¡peciálna multi-príkázová inÅ¡tancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.'; + 'Poznámka: toto je špeciálna multi-príkázová inštancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.'; @override String get repeater_cliHelpRegionGet => - 'Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) \'F\'\"'; + 'Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Pridá alebo aktualizuje definíciu regiónu s daným menom.'; + 'Pridá alebo aktualizuje definíciu regiónu s daným menom.'; @override String get repeater_cliHelpRegionRemove => - 'Odstráni definíciu oblasti s daným názvom. (musí zodpovedaÅ¥ presne a nemala by maÅ¥ podoblasti)'; + 'Odstráni definíciu oblasti s daným názvom. (musí zodpovedať presne a nemala by mať podoblasti)'; @override String get repeater_cliHelpRegionAllowf => - 'Nastavuje povolenie \'P\'lávu pre zadanú oblasÅ¥. (\'\' pre globálny/dedičský rozsah)'; + 'Nastavuje povolenie \'P\'lávu pre zadanú oblasť. (\'\' pre globálny/dedičský rozsah)'; @override String get repeater_cliHelpRegionDenyf => - 'Odstráni povolenie \'F\'lood\' pre zadanú oblasÅ¥. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používaÅ¥ na globálnom/dedskom rozsahu!!).'; + 'Odstráni povolenie \'F\'lood\' pre zadanú oblasť. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používať na globálnom/dedskom rozsahu!!).'; @override String get repeater_cliHelpRegionHome => - 'Odpovedá s aktuálnou \'domovskou\' oblasÅ¥ou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)'; + 'Odpovedá s aktuálnou \'domovskou\' oblasťou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)'; @override - String get repeater_cliHelpRegionHomeSet => 'Nastaví \'domovskú\' oblasÅ¥.'; + String get repeater_cliHelpRegionHomeSet => 'Nastaví \'domovskú\' oblasť.'; @override String get repeater_cliHelpRegionSave => - 'Uloží zoznam/mapu regiónov do úložiska.'; + 'Uloží zoznam/mapu regiónov do úložiska.'; @override String get repeater_cliHelpGps => - 'Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.'; + 'Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.'; @override - String get repeater_cliHelpGpsOnOff => 'Prepínač stavu GPS napájania.'; + String get repeater_cliHelpGpsOnOff => 'Prepínač stavu GPS napájania.'; @override String get repeater_cliHelpGpsSync => - 'Synchronizuje čas uzla s GPS hodinami.'; + 'Synchronizuje čas uzla s GPS hodinami.'; @override String get repeater_cliHelpGpsSetLoc => - 'Nastaví polohu uzla na GPS súradnice a uloží preferencie.'; + 'Nastaví polohu uzla na GPS súradnice a uloží preferencie.'; @override String get repeater_cliHelpGpsAdvert => - 'Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľaÅ¥: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach'; + 'Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľať: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach'; @override String get repeater_cliHelpGpsAdvertSet => - 'Nastavuje konfiguráciu reklamy na zadané miesto.'; + 'Nastavuje konfiguráciu reklamy na zadané miesto.'; @override - String get repeater_commandsListTitle => 'Zoznam príkazov'; + String get repeater_commandsListTitle => 'Zoznam príkazov'; @override String get repeater_commandsListNote => - 'Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".'; + 'Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".'; @override - String get repeater_general => 'Obecné'; + String get repeater_general => 'Obecné'; @override String get repeater_settingsCategory => 'Nastavenia'; @@ -2392,51 +2420,50 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_bridge => 'Most'; @override - String get repeater_logging => 'Záznamy'; + String get repeater_logging => 'Záznamy'; @override - String get repeater_neighborsRepeaterOnly => 'Súseznýci (iba opakovač)'; + String get repeater_neighborsRepeaterOnly => 'Súseznýci (iba opakovač)'; @override String get repeater_regionManagementRepeaterOnly => - 'Správa regiónov (iba opakovač)'; + 'Správa regiónov (iba opakovač)'; @override String get repeater_regionNote => - 'Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.'; + 'Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.'; @override - String get repeater_gpsManagement => 'Správa GPS'; + String get repeater_gpsManagement => 'Správa GPS'; @override String get repeater_gpsNote => - 'GPS príkaz bol zavádzaný na riadenie lokalitných tém.'; + 'GPS príkaz bol zavádzaný na riadenie lokalitných tém.'; @override - String get telemetry_receivedData => 'Obdolené Telemetrické dáta'; + String get telemetry_receivedData => 'Obdolené Telemetrické dáta'; @override - String get telemetry_requestTimeout => 'Požiadavka telemetrie zlyhala.'; + String get telemetry_requestTimeout => 'Požiadavka telemetrie zlyhala.'; @override String telemetry_errorLoading(String error) { - return 'Chyba pri načítaní telemetrie: $error'; + return 'Chyba pri načítaní telemetrie: $error'; } @override - String get telemetry_noData => - 'Nejsú dostupné žiadne údaje z telemetrie.'; + String get telemetry_noData => 'Nejsú dostupné žiadne údaje z telemetrie.'; @override String telemetry_channelTitle(int channel) { - return 'Kanál $channel'; + return 'Kanál $channel'; } @override - String get telemetry_batteryLabel => 'Batéria'; + String get telemetry_batteryLabel => 'Batéria'; @override - String get telemetry_voltageLabel => 'Napätie'; + String get telemetry_voltageLabel => 'Napätie'; @override String get telemetry_mcuTemperatureLabel => 'MCU teplota'; @@ -2445,7 +2472,7 @@ class AppLocalizationsSk extends AppLocalizations { String get telemetry_temperatureLabel => 'Teplota'; @override - String get telemetry_currentLabel => 'Aktuálne'; + String get telemetry_currentLabel => 'Aktuálne'; @override String telemetry_batteryValue(int percent, String volts) { @@ -2464,62 +2491,61 @@ class AppLocalizationsSk extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => 'Obdielo dáta suseda'; + String get neighbors_receivedData => 'Obdielo dáta suseda'; @override - String get neighbors_requestTimedOut => - 'Súďia žiadajú o časové ukončenie.'; + String get neighbors_requestTimedOut => 'Súďia žiadajú o časové ukončenie.'; @override String neighbors_errorLoading(String error) { - return 'Chyba pri načítaní susedov: $error'; + return 'Chyba pri načítaní susedov: $error'; } @override - String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná'; + String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná'; @override String get neighbors_noData => - 'Nie je dostupná žiadna informácia o susedoch.'; + 'Nie je dostupná žiadna informácia o susedoch.'; @override String neighbors_unknownContact(String pubkey) { - return 'Neznáma $pubkey'; + return 'Neznáma $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Počuli sme to: $time dozadu'; + return 'Počuli sme to: $time dozadu'; } @override - String get channelPath_title => 'Cesta balíka'; + String get channelPath_title => 'Cesta balíka'; @override - String get channelPath_viewMap => 'ZobraziÅ¥ mapu'; + String get channelPath_viewMap => 'Zobraziť mapu'; @override - String get channelPath_otherObservedPaths => 'Ostatné pozorovacie cesty'; + String get channelPath_otherObservedPaths => 'Ostatné pozorovacie cesty'; @override - String get channelPath_repeaterHops => 'Skoky opakovača'; + String get channelPath_repeaterHops => 'Skoky opakovača'; @override String get channelPath_noHopDetails => - 'Podrobnosti o balíčku zatiaľ nie sú dostupné.'; + 'Podrobnosti o balíčku zatiaľ nie sú dostupné.'; @override - String get channelPath_messageDetails => 'Podrobnosti o zprávach'; + String get channelPath_messageDetails => 'Podrobnosti o zprávach'; @override - String get channelPath_senderLabel => 'Posielateľ'; + String get channelPath_senderLabel => 'Posielateľ'; @override - String get channelPath_timeLabel => 'ÄŒas'; + String get channelPath_timeLabel => 'Čas'; @override String get channelPath_repeatsLabel => 'Opakovanie'; @@ -2530,15 +2556,15 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get channelPath_observedLabel => 'Pozorované'; + String get channelPath_observedLabel => 'Pozorované'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Sledovaný postup $index • $hops'; + return 'Sledovaný postup $index • $hops'; } @override - String get channelPath_noLocationData => 'Žiadne údaje o polohe'; + String get channelPath_noLocationData => 'Žiadne údaje o polohe'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2551,10 +2577,10 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Neznáme'; + String get channelPath_unknownPath => 'Neznáme'; @override - String get channelPath_floodPath => 'Povodňová'; + String get channelPath_floodPath => 'Povodňová'; @override String get channelPath_directPath => 'Priamo'; @@ -2574,162 +2600,161 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Pre túto cestu nie je dostupných žiadne polohy opakovačov.'; + 'Pre túto cestu nie je dostupných žiadne polohy opakovačov.'; @override String channelPath_primaryPath(int index) { - return 'Cesta $index (Hlavná)'; + return 'Cesta $index (Hlavná)'; } @override String get channelPath_pathLabelTitle => 'Cesta'; @override - String get channelPath_observedPathHeader => 'Sledovaná cesta'; + String get channelPath_observedPathHeader => 'Sledovaná cesta'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Pre toto balíček nie sú dostupné údaje o skokoch.'; + 'Pre toto balíček nie sú dostupné údaje o skokoch.'; @override - String get channelPath_unknownRepeater => 'Neznáme opakovače'; + String get channelPath_unknownRepeater => 'Neznáme opakovače'; @override String get community_title => 'Komunita'; @override - String get community_create => 'VytvoriÅ¥ komunitu'; + String get community_create => 'Vytvoriť komunitu'; @override String get community_createDesc => - 'Vytvorte novú komunitu a zdieľajte cez QR kód.'; + 'Vytvorte novú komunitu a zdieľajte cez QR kód.'; @override - String get community_join => 'PripojiÅ¥'; + String get community_join => 'Pripojiť'; @override - String get community_joinTitle => 'PripojiÅ¥ sa k spoločenstvu'; + String get community_joinTitle => 'Pripojiť sa k spoločenstvu'; @override String community_joinConfirmation(String name) { - return 'ChceÅ¡ sa pridaÅ¥ do komunity \"$name\"?'; + return 'Chceš sa pridať do komunity \"$name\"?'; } @override - String get community_scanQr => 'Skontrolujte komunitný QR kód'; + String get community_scanQr => 'Skontrolujte komunitný QR kód'; @override String get community_scanInstructions => - 'Zamerte kameru na komunitný QR kód.'; + 'Zamerte kameru na komunitný QR kód.'; @override - String get community_showQr => 'ZobraziÅ¥ QR kód'; + String get community_showQr => 'Zobraziť QR kód'; @override - String get community_publicChannel => 'Komunita verejná'; + String get community_publicChannel => 'Komunita verejná'; @override - String get community_hashtagChannel => 'Komunitný Hashtag'; + String get community_hashtagChannel => 'Komunitný Hashtag'; @override String get community_name => 'Komunita'; @override - String get community_enterName => 'Zadajte názov komunity'; + String get community_enterName => 'Zadajte názov komunity'; @override String community_created(String name) { - return 'Komunita \"$name\" vytvorená'; + return 'Komunita \"$name\" vytvorená'; } @override String community_joined(String name) { - return 'Pripojená komunita \"$name\"'; + return 'Pripojená komunita \"$name\"'; } @override - String get community_qrTitle => 'Zdieľť komunitu'; + String get community_qrTitle => 'Zdieľť komunitu'; @override String community_qrInstructions(String name) { - return 'Skenejte tento QR kód, aby ste sa pripojili k $name.'; + return 'Skenejte tento QR kód, aby ste sa pripojili k $name.'; } @override String get community_hashtagPrivacyHint => - 'Hashtagové kanály komunity sú prístupné len členom komunity'; + 'Hashtagové kanály komunity sú prístupné len členom komunity'; @override - String get community_invalidQrCode => 'Neplatná QR kód komunity.'; + String get community_invalidQrCode => 'Neplatná QR kód komunity.'; @override - String get community_alreadyMember => 'Už ste členom.'; + String get community_alreadyMember => 'Už ste členom.'; @override String community_alreadyMemberMessage(String name) { - return 'Vy ste už členom \"$name\".'; + return 'Vy ste už členom \"$name\".'; } @override - String get community_addPublicChannel => - 'PridaÅ¥ verejný komunikačný kanál'; + String get community_addPublicChannel => 'Pridať verejný komunikačný kanál'; @override String get community_addPublicChannelHint => - 'Automaticky prida verejný kanál pre túto komunitu.'; + 'Automaticky prida verejný kanál pre túto komunitu.'; @override String get community_noCommunities => - 'Zatiaľ ste sa nepripojili k žiadnej komunite'; + 'Zatiaľ ste sa nepripojili k žiadnej komunite'; @override String get community_scanOrCreate => - 'Skene QR kód alebo vytvor komunitu na začiatok.'; + 'Skene QR kód alebo vytvor komunitu na začiatok.'; @override - String get community_manageCommunities => 'SpravovaÅ¥ komunity'; + String get community_manageCommunities => 'Spravovať komunity'; @override String get community_delete => 'Nechajte komunitu'; @override String community_deleteConfirm(String name) { - return 'OpustiÅ¥ \"$name\"?'; + return 'Opustiť \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.'; + return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.'; } @override String community_deleted(String name) { - return 'Opustená komunita \"$name\"'; + return 'Opustená komunita \"$name\"'; } @override - String get community_regenerateSecret => 'ZobraziÅ¥ nový tajný kód'; + String get community_regenerateSecret => 'Zobraziť nový tajný kód'; @override String community_regenerateSecretConfirm(String name) { - return 'Znovu vygenerovaÅ¥ tajný kľúč pre \"$name\"? VÅ¡etci členovia budú musieÅ¥ skanovaÅ¥ nový QR kód, aby mohli nadviazaÅ¥ komunikáciu.'; + return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.'; } @override - String get community_regenerate => 'Znovu vygenerovaÅ¥'; + String get community_regenerate => 'Znovu vygenerovať'; @override String community_secretRegenerated(String name) { - return 'Záznam pre \"$name\" bol regenerovaný tajne'; + return 'Záznam pre \"$name\" bol regenerovaný tajne'; } @override - String get community_updateSecret => 'AktualizovaÅ¥ tajné heslo'; + String get community_updateSecret => 'Aktualizovať tajné heslo'; @override String community_secretUpdated(String name) { @@ -2738,32 +2763,31 @@ class AppLocalizationsSk extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"'; + return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"'; } @override - String get community_addHashtagChannel => 'PridaÅ¥ komunitný hashtag'; + String get community_addHashtagChannel => 'Pridať komunitný hashtag'; @override String get community_addHashtagChannelDesc => - 'Pridajte hashtagový kanál pre túto komunitu.'; + 'Pridajte hashtagový kanál pre túto komunitu.'; @override String get community_selectCommunity => 'Vyberte komunitu'; @override - String get community_regularHashtag => 'Zvyčajný hashtag'; + String get community_regularHashtag => 'Zvyčajný hashtag'; @override String get community_regularHashtagDesc => - 'Veľký hashtag (ktočokoľvek sa môže pridaÅ¥)'; + 'Veľký hashtag (ktočokoľvek sa môže pridať)'; @override - String get community_communityHashtag => 'Komunitný Hashtag'; + String get community_communityHashtag => 'Komunitný Hashtag'; @override - String get community_communityHashtagDesc => - 'Å pecifické pre členov komunity'; + String get community_communityHashtagDesc => 'Špecifické pre členov komunity'; @override String community_forCommunity(String name) { @@ -2771,16 +2795,16 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get listFilter_tooltip => 'FiltrovaÅ¥ a triediÅ¥'; + String get listFilter_tooltip => 'Filtrovať a triediť'; @override - String get listFilter_sortBy => 'TriediÅ¥ podľa'; + String get listFilter_sortBy => 'Triediť podľa'; @override - String get listFilter_latestMessages => 'Posledné správy'; + String get listFilter_latestMessages => 'Posledné správy'; @override - String get listFilter_heardRecently => 'Nedávno počuli.'; + String get listFilter_heardRecently => 'Nedávno počuli.'; @override String get listFilter_az => 'A-Z'; @@ -2789,31 +2813,31 @@ class AppLocalizationsSk extends AppLocalizations { String get listFilter_filters => 'Filtre'; @override - String get listFilter_all => 'VÅ¡etko'; + String get listFilter_all => 'Všetko'; @override - String get listFilter_favorites => 'Obľúbené'; + String get listFilter_favorites => 'Obľúbené'; @override - String get listFilter_addToFavorites => 'Pridaj do obľúbených'; + String get listFilter_addToFavorites => 'Pridaj do obľúbených'; @override - String get listFilter_removeFromFavorites => 'OdstrániÅ¥ z označení'; + String get listFilter_removeFromFavorites => 'Odstrániť z označení'; @override - String get listFilter_users => 'Používatelia'; + String get listFilter_users => 'Používatelia'; @override - String get listFilter_repeaters => 'Opakovadlá'; + String get listFilter_repeaters => 'Opakovadlá'; @override - String get listFilter_roomServers => 'Servéry miestnosti'; + String get listFilter_roomServers => 'Servéry miestnosti'; @override - String get listFilter_unreadOnly => 'Nezaregistrované len'; + String get listFilter_unreadOnly => 'Nezaregistrované len'; @override - String get listFilter_newGroup => 'Nová skupina'; + String get listFilter_newGroup => 'Nová skupina'; @override String get pathTrace_you => 'Vy'; @@ -2822,50 +2846,49 @@ class AppLocalizationsSk extends AppLocalizations { String get pathTrace_failed => 'Sledovanie cesty zlyhalo.'; @override - String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.'; + String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.'; @override - String get pathTrace_refreshTooltip => 'ObnoviÅ¥ Path Trace.'; + String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Jedna alebo viac chmeľov chýba lokalita!'; + 'Jedna alebo viac chmeľov chýba lokalita!'; @override - String get pathTrace_clearTooltip => 'ZmazaÅ¥ cestu'; + String get pathTrace_clearTooltip => 'Zmazať cestu'; @override - String get losSelectStartEnd => - 'Vyberte počiatočný a koncový uzol pre LOS.'; + String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.'; @override String losRunFailed(String error) { - return 'Kontrola priamej viditeľnosti zlyhala: $error'; + return 'Kontrola priamej viditeľnosti zlyhala: $error'; } @override - String get losClearAllPoints => 'VymazaÅ¥ vÅ¡etky body'; + String get losClearAllPoints => 'Vymazať všetky body'; @override String get losRunToViewElevationProfile => - 'Ak chcete zobraziÅ¥ výškový profil, spustite LOS'; + 'Ak chcete zobraziť výškový profil, spustite LOS'; @override String get losMenuTitle => 'Menu LOS'; @override String get losMenuSubtitle => - 'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body'; + 'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body'; @override - String get losShowDisplayNodes => 'ZobraziÅ¥ uzly zobrazenia'; + String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia'; @override - String get losCustomPoints => 'Vlastné body'; + String get losCustomPoints => 'Vlastné body'; @override String losCustomPointLabel(int index) { - return 'Vlastné $index'; + return 'Vlastné $index'; } @override @@ -2876,19 +2899,19 @@ class AppLocalizationsSk extends AppLocalizations { @override String losAntennaA(String value, String unit) { - return 'Anténa A: $value $unit'; + return 'Anténa A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Anténa B: $value $unit'; + return 'Anténa B: $value $unit'; } @override String get losRun => 'Spustite LOS'; @override - String get losNoElevationData => 'Žiadne údaje o nadmorskej výške'; + String get losNoElevationData => 'Žiadne údaje o nadmorskej výške'; @override String losProfileClear( @@ -2897,7 +2920,7 @@ class AppLocalizationsSk extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, vymazaÅ¥ LOS, min. vôľa $clearance $heightUnit'; + return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit'; } @override @@ -2907,61 +2930,61 @@ class AppLocalizationsSk extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, blokovaný $obstruction $heightUnit'; + return '$distance $distanceUnit, blokovaný $obstruction $heightUnit'; } @override String get losStatusChecking => 'LOS: kontrolujem...'; @override - String get losStatusNoData => 'LOS: žiadne údaje'; + String get losStatusNoData => 'LOS: žiadne údaje'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme'; + return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme'; } @override String get losErrorElevationUnavailable => - 'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.'; + 'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.'; @override String get losErrorInvalidInput => - 'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.'; + 'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.'; @override - String get losRenameCustomPoint => 'PremenovaÅ¥ vlastný bod'; + String get losRenameCustomPoint => 'Premenovať vlastný bod'; @override - String get losPointName => 'Názov bodu'; + String get losPointName => 'Názov bodu'; @override - String get losShowPanelTooltip => 'ZobraziÅ¥ panel LOS'; + String get losShowPanelTooltip => 'Zobraziť panel LOS'; @override - String get losHidePanelTooltip => 'SkryÅ¥ panel LOS'; + String get losHidePanelTooltip => 'Skryť panel LOS'; @override String get losElevationAttribution => - 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; + 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Rádiový horizont'; + String get losLegendRadioHorizon => 'Rádiový horizont'; @override - String get losLegendLosBeam => 'Priama viditeľnosÅ¥'; + String get losLegendLosBeam => 'Priama viditeľnosť'; @override - String get losLegendTerrain => 'Terén'; + String get losLegendTerrain => 'Terén'; @override String get losFrequencyLabel => 'Frekvencia'; @override - String get losFrequencyInfoTooltip => 'ZobraziÅ¥ podrobnosti výpočtu'; + String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu'; @override - String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; + String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; @override String losFrequencyDialogDescription( @@ -2970,20 +2993,20 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; + return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; } @override - String get contacts_pathTrace => 'Sledovanie lúčov'; + String get contacts_pathTrace => 'Sledovanie lúčov'; @override - String get contacts_ping => 'PingovaÅ¥'; + String get contacts_ping => 'Pingovať'; @override - String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču'; + String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču'; @override - String get contacts_repeaterPing => 'PingovaÅ¥ opakovač'; + String get contacts_repeaterPing => 'Pingovať opakovač'; @override String get contacts_roomPathTrace => 'Sledovanie cesty k serveru miestnosti'; @@ -2992,48 +3015,46 @@ class AppLocalizationsSk extends AppLocalizations { String get contacts_roomPing => 'Ping server miestnosti'; @override - String get contacts_chatTraceRoute => 'SledovaÅ¥ trasu lúča'; + String get contacts_chatTraceRoute => 'Sledovať trasu lúča'; @override String contacts_pathTraceTo(String name) { - return 'SledovaÅ¥ trasu k $name'; + return 'Sledovať trasu k $name'; } @override - String get contacts_clipboardEmpty => 'Schránka je prázdna.'; + String get contacts_clipboardEmpty => 'Schránka je prázdna.'; @override - String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje'; + String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje'; @override - String get contacts_contactImported => 'Kontakt bol importovaný.'; + String get contacts_contactImported => 'Kontakt bol importovaný.'; @override String get contacts_contactImportFailed => - 'Kontakt sa nepodarilo importovaÅ¥.'; + 'Kontakt sa nepodarilo importovať.'; @override - String get contacts_zeroHopAdvert => 'Inzerát Zero Hop'; + String get contacts_zeroHopAdvert => 'Inzerát Zero Hop'; @override - String get contacts_floodAdvert => 'Inzerát povodní'; + String get contacts_floodAdvert => 'Inzerát povodní'; @override - String get contacts_copyAdvertToClipboard => - 'KopírovaÅ¥ reklamu do schránky'; + String get contacts_copyAdvertToClipboard => 'Kopírovať reklamu do schránky'; @override - String get contacts_addContactFromClipboard => 'PridaÅ¥ kontakt z schránky'; + String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky'; @override - String get contacts_ShareContact => 'KopírovaÅ¥ kontakt do schránky'; + String get contacts_ShareContact => 'Kopírovať kontakt do schránky'; @override - String get contacts_ShareContactZeroHop => 'ZdieľaÅ¥ kontakt cez inzerát'; + String get contacts_ShareContactZeroHop => 'Zdieľať kontakt cez inzerát'; @override - String get contacts_zeroHopContactAdvertSent => - 'Poslal kontakt cez inzerát.'; + String get contacts_zeroHopContactAdvertSent => 'Poslal kontakt cez inzerát.'; @override String get contacts_zeroHopContactAdvertFailed => @@ -3041,11 +3062,11 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_contactAdvertCopied => - 'Inzerát bol skopírovaný do schránky.'; + 'Inzerát bol skopírovaný do schránky.'; @override String get contacts_contactAdvertCopyFailed => - 'Kopírovanie inzerátu do schránky zlyhalo.'; + 'Kopírovanie inzerátu do schránky zlyhalo.'; @override String get notification_activityTitle => 'Aktivita MeshCore'; @@ -3055,9 +3076,9 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'správ', - few: 'správy', - one: 'správa', + other: 'správ', + few: 'správy', + one: 'správa', ); return '$count $_temp0'; } @@ -3067,9 +3088,9 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'správ kanálu', - few: 'správy kanálu', - one: 'správa kanálu', + other: 'správ kanálu', + few: 'správy kanálu', + one: 'správa kanálu', ); return '$count $_temp0'; } @@ -3079,77 +3100,77 @@ class AppLocalizationsSk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'nových uzlov', - few: 'nové uzly', - one: 'nový uzol', + other: 'nových uzlov', + few: 'nové uzly', + one: 'nový uzol', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Nový $contactType objavený'; + return 'Nový $contactType objavený'; } @override - String get notification_receivedNewMessage => 'Prijatá nová správa'; + String get notification_receivedNewMessage => 'Prijatá nová správa'; @override String get settings_gpxExportRepeaters => - 'ExportovaÅ¥ repeater / server miestnosti do GPX'; + 'Exportovať repeater / server miestnosti do GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Exportuje repeater / roomserver s lokalitou do súboru GPX.'; + 'Exportuje repeater / roomserver s lokalitou do súboru GPX.'; @override String get settings_gpxExportContacts => 'Export sprievodcov do GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exportuje sprievodcov s umiestnením do súboru GPX.'; + 'Exportuje sprievodcov s umiestnením do súboru GPX.'; @override - String get settings_gpxExportAll => 'ExportovaÅ¥ vÅ¡etky kontakty do GPX'; + String get settings_gpxExportAll => 'Exportovať všetky kontakty do GPX'; @override String get settings_gpxExportAllSubtitle => - 'Exportuje vÅ¡etky kontakty s lokalitou do súboru GPX.'; + 'Exportuje všetky kontakty s lokalitou do súboru GPX.'; @override - String get settings_gpxExportSuccess => 'ÚspeÅ¡ne exportovaný súbor GPX.'; + String get settings_gpxExportSuccess => 'Úspešne exportovaný súbor GPX.'; @override - String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.'; + String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.'; @override String get settings_gpxExportNotAvailable => - 'Nie je podporované na vaÅ¡om zariadení/operáciomnom systéme'; + 'Nie je podporované na vašom zariadení/operáciomnom systéme'; @override - String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.'; + String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.'; @override String get settings_gpxExportRepeatersRoom => - 'Umiestnenia opakovačov a serverov miestností'; + 'Umiestnenia opakovačov a serverov miestností'; @override - String get settings_gpxExportChat => 'Lokácie sprievodcov'; + String get settings_gpxExportChat => 'Lokácie sprievodcov'; @override - String get settings_gpxExportAllContacts => 'VÅ¡etky kontaktné lokality'; + String get settings_gpxExportAllContacts => 'Všetky kontaktné lokality'; @override String get settings_gpxExportShareText => - 'Mapové údaje exportované z meshcore-open'; + 'Mapové údaje exportované z meshcore-open'; @override String get settings_gpxExportShareSubject => - 'meshcore-open export dát GPX mapových údajov'; + 'meshcore-open export dát GPX mapových údajov'; @override - String get snrIndicator_nearByRepeaters => 'Miestne opakovače'; + String get snrIndicator_nearByRepeaters => 'Miestne opakovače'; @override - String get snrIndicator_lastSeen => 'Naposledy videný'; + String get snrIndicator_lastSeen => 'Naposledy videný'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ddc26ac..d8d1d53 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -21,13 +21,13 @@ class AppLocalizationsSl extends AppLocalizations { String get nav_map => 'Karta'; @override - String get common_cancel => 'Prekliči'; + String get common_cancel => 'Prekliči'; @override String get common_ok => 'V redu'; @override - String get common_connect => 'Poveži se'; + String get common_connect => 'Poveži se'; @override String get common_unknownDevice => 'Nepoznano naprave'; @@ -81,7 +81,7 @@ class AppLocalizationsSl extends AppLocalizations { String get common_remove => 'Izbrisati'; @override - String get common_enable => 'Omogoči'; + String get common_enable => 'Omogoči'; @override String get common_disable => 'Izklopiti'; @@ -90,10 +90,10 @@ class AppLocalizationsSl extends AppLocalizations { String get common_reboot => 'Ponoviti'; @override - String get common_loading => 'Naložanje...'; + String get common_loading => 'Naložanje...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -115,11 +115,11 @@ class AppLocalizationsSl extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Povežite preko USB'; + String get usbScreenTitle => 'Povežite preko USB'; @override String get usbScreenSubtitle => - 'Izberite zaznano serijsko napravo in se neposredno povežite z vaÅ¡im MeshCore-om.'; + 'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.'; @override String get usbScreenStatus => 'Izberite USB naprave.'; @@ -130,7 +130,47 @@ class AppLocalizationsSl extends AppLocalizations { @override String get usbScreenEmptyState => - 'Niti en USB naprave niso najdeni. Povežite eno in posodobite.'; + 'Niti en USB naprave niso najdeni. Povežite eno in posodobite.'; + + @override + String get usbErrorPermissionDenied => + 'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.'; + + @override + String get usbErrorDeviceMissing => 'Izbrani USB napravega več ni na voljo.'; + + @override + String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.'; + + @override + String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.'; + + @override + String get usbErrorNotConnected => 'Ni priklopljenih USB naprav.'; + + @override + String get usbErrorOpenFailed => 'Niso uspeli odkriti izbrane USB naprave.'; + + @override + String get usbErrorConnectFailed => + 'Niso bilo mogoče uskladiti povezave z izbranim USB napom.'; + + @override + String get usbErrorUnsupported => + 'USB serijska komunikacija ni podprta na tej platformi.'; + + @override + String get usbErrorAlreadyActive => 'USB povezava je že aktivirana.'; + + @override + String get usbErrorNoDeviceSelected => 'Ni bilo izbranega USB naprave.'; + + @override + String get usbErrorPortClosed => 'USB povezava ni aktivirana.'; + + @override + String get usbErrorConnectTimedOut => + 'Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval.'; @override String get scanner_scanning => 'Skeniram za naprave...'; @@ -154,15 +194,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_tapToScan => - 'NagneÅ¡ na skeniranje za najdene naprave MeshCore.'; + 'Nagneš na skeniranje za najdene naprave MeshCore.'; @override String scanner_connectionFailed(String error) { - return 'PoÅ¡lo je z povezavo: $error'; + return 'Pošlo je z povezavo: $error'; } @override - String get scanner_stop => 'Prekliči'; + String get scanner_stop => 'Prekliči'; @override String get scanner_scan => 'Skeniraj'; @@ -172,7 +212,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_bluetoothOffMessage => - 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; + 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; @override String get scanner_chromeRequired => 'Zahtevan brskalnik Chrome'; @@ -182,7 +222,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.'; @override - String get scanner_enableBluetooth => 'Omogočite Bluetooth'; + String get scanner_enableBluetooth => 'Omogočite Bluetooth'; @override String get device_quickSwitch => 'Hitro preklop'; @@ -201,10 +241,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_appSettingsSubtitle => - 'Obveščanja, sporoščanje in zemljevidi.'; + 'Obveščanja, sporoščanje in zemljevidi.'; @override - String get settings_nodeSettings => 'Nastavitev časa'; + String get settings_nodeSettings => 'Nastavitev časa'; @override String get settings_nodeName => 'Ime node-a'; @@ -223,7 +263,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_radioSettingsSubtitle => - 'Frekvenca, moč, razÅ¡iritveni faktor'; + 'Frekvenca, moč, razširitveni faktor'; @override String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene'; @@ -238,18 +278,18 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_locationUpdated => 'Lokacija posodobljena'; @override - String get settings_locationBothRequired => 'Vnesite Å¡irino in dolžino.'; + String get settings_locationBothRequired => 'Vnesite širino in dolžino.'; @override String get settings_locationInvalid => - 'Neveljavna zemeljska Å¡irina ali dolžina.'; + 'Neveljavna zemeljska širina ali dolžina.'; @override - String get settings_locationGPSEnable => 'Omogoči GPS'; + String get settings_locationGPSEnable => 'Omogoči GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Omogoči samodejno posodabljanje lokacije z GPS-jem.'; + 'Omogoči samodejno posodabljanje lokacije z GPS-jem.'; @override String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)'; @@ -259,10 +299,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.'; @override - String get settings_latitude => 'Å irina'; + String get settings_latitude => 'Širina'; @override - String get settings_longitude => 'Dolžina'; + String get settings_longitude => 'Dolžina'; @override String get settings_privacyMode => 'Zasebnost'; @@ -272,19 +312,19 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_privacyModeToggle => - 'Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.'; + 'Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.'; @override - String get settings_privacyModeEnabled => 'Privatni način je omogočen.'; + String get settings_privacyModeEnabled => 'Privatni način je omogočen.'; @override - String get settings_privacyModeDisabled => 'Privatni način je onemogočen.'; + String get settings_privacyModeDisabled => 'Privatni način je onemogočen.'; @override String get settings_actions => 'Akcije'; @override - String get settings_sendAdvertisement => 'PoÅ¡lji Oglas'; + String get settings_sendAdvertisement => 'Pošlji Oglas'; @override String get settings_sendAdvertisementSubtitle => @@ -297,35 +337,33 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_syncTime => 'Nastavi uro'; @override - String get settings_syncTimeSubtitle => - 'Nastavi uro naprave na čas telefona'; + String get settings_syncTimeSubtitle => 'Nastavi uro naprave na čas telefona'; @override String get settings_timeSynchronized => 'Ura sinhronizirana'; @override - String get settings_refreshContacts => 'Ponovno obišči kontakte'; + String get settings_refreshContacts => 'Ponovno obišči kontakte'; @override String get settings_refreshContactsSubtitle => - 'Ponovno naloži seznam stikov v napravi'; + 'Ponovno naloži seznam stikov v napravi'; @override String get settings_rebootDevice => 'Ponovni zagon naprave'; @override - String get settings_rebootDeviceSubtitle => - 'Ponovno zaženi MeshCore napravo'; + String get settings_rebootDeviceSubtitle => 'Ponovno zaženi MeshCore napravo'; @override String get settings_rebootDeviceConfirm => - 'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.'; + 'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.'; @override String get settings_debug => 'Debug'; @override - String get settings_bleDebugLog => 'BLE debug log (razhroščevanje)'; + String get settings_bleDebugLog => 'BLE debug log (razhroščevanje)'; @override String get settings_bleDebugLogSubtitle => @@ -335,7 +373,7 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_appDebugLog => 'Logi aplikacije'; @override - String get settings_appDebugLogSubtitle => 'Debug sporočila aplikacije'; + String get settings_appDebugLogSubtitle => 'Debug sporočila aplikacije'; @override String get settings_about => 'Oglejte si'; @@ -350,11 +388,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_aboutDescription => - 'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.'; + 'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.'; @override String get settings_aboutOpenMeteoAttribution => - 'Podatki o viÅ¡ini LOS: Open-Meteo (CC BY 4.0)'; + 'Podatki o višini LOS: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Ime'; @@ -369,13 +407,13 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_infoBattery => 'Baterija'; @override - String get settings_infoPublicKey => 'Javni ključ'; + String get settings_infoPublicKey => 'Javni ključ'; @override - String get settings_infoContactsCount => 'Å tevilo stikov'; + String get settings_infoContactsCount => 'Število stikov'; @override - String get settings_infoChannelCount => 'Å tevilo kanalov'; + String get settings_infoChannelCount => 'Število kanalov'; @override String get settings_presets => 'Prednastavitve'; @@ -390,33 +428,33 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_frequencyInvalid => 'Neveljavna frekvenca (300-2500 MHz)'; @override - String get settings_bandwidth => 'Pasovna Å¡irina'; + String get settings_bandwidth => 'Pasovna širina'; @override - String get settings_spreadingFactor => 'RazÅ¡iritveni faktor'; + String get settings_spreadingFactor => 'Razširitveni faktor'; @override String get settings_codingRate => 'Programska hitrost'; @override - String get settings_txPower => 'TX Moč (dBm)'; + String get settings_txPower => 'TX Moč (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; + String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; @override String get settings_clientRepeat => 'Neovadno ponavljanje'; @override String get settings_clientRepeatSubtitle => - 'Omogočite temu naprave, da ponavlja paketne sporočila za druge.'; + 'Omogočite temu naprave, da ponavlja paketne sporočila za druge.'; @override String get settings_clientRepeatFreqWarning => - 'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.'; + 'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.'; @override String settings_error(String message) { @@ -427,7 +465,7 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_title => 'Nastavitve aplikacije'; @override - String get appSettings_appearance => 'Prikaži'; + String get appSettings_appearance => 'Prikaži'; @override String get appSettings_theme => 'Tema'; @@ -451,10 +489,10 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -463,16 +501,16 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -481,41 +519,40 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Ruščina'; + String get appSettings_languageRu => 'Ruščina'; @override String get appSettings_languageUk => 'Ukrajinsko'; @override - String get appSettings_enableMessageTracing => - 'Omogoči sledenje sporočilom'; + String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom'; @override String get appSettings_enableMessageTracingSubtitle => - 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; + 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; @override String get appSettings_notifications => 'Obvestila'; @override - String get appSettings_enableNotifications => 'Omogoči obvestila'; + String get appSettings_enableNotifications => 'Omogoči obvestila'; @override String get appSettings_enableNotificationsSubtitle => - 'Prejmite obvestila o sporočilih in oglasih'; + 'Prejmite obvestila o sporočilih in oglasih'; @override String get appSettings_notificationPermissionDenied => 'Odobritev obvestila zavrnjena'; @override - String get appSettings_notificationsEnabled => 'Obvestila omogočena'; + String get appSettings_notificationsEnabled => 'Obvestila omogočena'; @override String get appSettings_notificationsDisabled => 'Obvestila so izklopljena'; @@ -525,41 +562,41 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_messageNotificationsSubtitle => - 'Pokaži obvestilo ob prejemu novih sporočil.'; + 'Pokaži obvestilo ob prejemu novih sporočil.'; @override String get appSettings_channelMessageNotifications => - 'Obvestila o sporočilih kanala'; + 'Obvestila o sporočilih kanala'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Pokaži obvestilo ob prejemanju sporočil kanala'; + 'Pokaži obvestilo ob prejemanju sporočil kanala'; @override String get appSettings_advertisementNotifications => 'Opozorila o oglasih'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Pokaži obvestilo, ko so najdene nove naprave.'; + 'Pokaži obvestilo, ko so najdene nove naprave.'; @override String get appSettings_messaging => 'Komuniciranje'; @override String get appSettings_clearPathOnMaxRetry => - 'Ponovite pot do cilja na največjem Å¡tetju'; + 'Ponovite pot do cilja na največjem štetju'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Ponovi pot zimske obveščevalne poti po 5 neuspeÅ¡nih poskusih poÅ¡iljanja'; + 'Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja'; @override String get appSettings_pathsWillBeCleared => - 'Počisti pot po 5 neuspeÅ¡nih poskusih.'; + 'Počisti pot po 5 neuspešnih poskusih.'; @override String get appSettings_pathsWillNotBeCleared => - 'Poti ne bodo samodejno čiščene.'; + 'Poti ne bodo samodejno čiščene.'; @override String get appSettings_autoRouteRotation => @@ -567,15 +604,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_autoRouteRotationSubtitle => - 'Menjaj med boljÅ¡o potjo in flood načinom'; + 'Menjaj med boljšo potjo in flood načinom'; @override String get appSettings_autoRouteRotationEnabled => - 'Samodejno krmilno rotiranje omogočeno'; + 'Samodejno krmilno rotiranje omogočeno'; @override String get appSettings_autoRouteRotationDisabled => - 'Samodejno krmilno rotiranje je onemogočeno'; + 'Samodejno krmilno rotiranje je onemogočeno'; @override String get appSettings_battery => 'Baterija'; @@ -590,13 +627,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_batteryChemistryConnectFirst => - 'Za izbiro se poveži z napravo'; + 'Za izbiro se poveži z napravo'; @override String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @@ -605,43 +642,42 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_mapDisplay => 'Prikaz zemljevida'; @override - String get appSettings_showRepeaters => 'Prikaži repetitorje'; + String get appSettings_showRepeaters => 'Prikaži repetitorje'; @override - String get appSettings_showRepeatersSubtitle => - 'Prikaži repetitorje na mapi'; + String get appSettings_showRepeatersSubtitle => 'Prikaži repetitorje na mapi'; @override - String get appSettings_showChatNodes => 'Prikaži naprave za klepet'; + String get appSettings_showChatNodes => 'Prikaži naprave za klepet'; @override String get appSettings_showChatNodesSubtitle => - 'Prikaži naprave na zemljevidu'; + 'Prikaži naprave na zemljevidu'; @override - String get appSettings_showOtherNodes => 'Pokaži druge naprave'; + String get appSettings_showOtherNodes => 'Pokaži druge naprave'; @override String get appSettings_showOtherNodesSubtitle => - 'Pokaži druge vrste naprav na zemljevidu.'; + 'Pokaži druge vrste naprav na zemljevidu.'; @override - String get appSettings_timeFilter => 'Filter po času'; + String get appSettings_timeFilter => 'Filter po času'; @override - String get appSettings_timeFilterShowAll => 'Pokaži vse naprave'; + String get appSettings_timeFilterShowAll => 'Pokaži vse naprave'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Pokaži naprave v zadnjih $hours urah'; + return 'Pokaži naprave v zadnjih $hours urah'; } @override - String get appSettings_mapTimeFilter => 'Filter časa na zemljevidu'; + String get appSettings_mapTimeFilter => 'Filter časa na zemljevidu'; @override String get appSettings_showNodesDiscoveredWithin => - 'Pokaži naprave odkrite v:'; + 'Pokaži naprave odkrite v:'; @override String get appSettings_allTime => 'Brez omejitev'; @@ -656,7 +692,7 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_last24Hours => 'Zadnjih 24 ur'; @override - String get appSettings_lastWeek => 'PrejÅ¡nji teden'; + String get appSettings_lastWeek => 'Prejšnji teden'; @override String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; @@ -665,36 +701,36 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_unitsTitle => 'Enote'; @override - String get appSettings_unitsMetric => 'Metrična (m/km)'; + String get appSettings_unitsMetric => 'Metrična (m/km)'; @override String get appSettings_unitsImperial => 'Imperialno (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Območje ni izbrano'; + String get appSettings_noAreaSelected => 'Območje ni izbrano'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Izbrano območje (povečava $minZoom-$maxZoom)'; + return 'Izbrano območje (povečava $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Razhroščevanje'; + String get appSettings_debugCard => 'Razhroščevanje'; @override String get appSettings_appDebugLogging => 'Programski dnevnik'; @override String get appSettings_appDebugLoggingSubtitle => - 'Dnevnik debug sporočil za odpravljanje težav'; + 'Dnevnik debug sporočil za odpravljanje težav'; @override String get appSettings_appDebugLoggingEnabled => - 'Beleženje napak v aplikaciji omogočeno'; + 'Beleženje napak v aplikaciji omogočeno'; @override String get appSettings_appDebugLoggingDisabled => - 'Beleženje napak v aplikacije onemogočeno.'; + 'Beleženje napak v aplikacije onemogočeno.'; @override String get contacts_title => 'Stiki'; @@ -724,17 +760,17 @@ class AppLocalizationsSl extends AppLocalizations { @override String contacts_searchUsers(int number, String str) { - return 'Išči $number$str uporabnikov...'; + return 'Išči $number$str uporabnikov...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Išči $number$str ponavljalnike...'; + return 'Išči $number$str ponavljalnike...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Išči $number$str strežnikov sob...'; + return 'Išči $number$str strežnikov sob...'; } @override @@ -744,18 +780,18 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_noContactsFound => 'Stiki niso najdeni.'; @override - String get contacts_deleteContact => 'IzbriÅ¡i stik'; + String get contacts_deleteContact => 'Izbriši stik'; @override String contacts_removeConfirm(String contactName) { - return 'IzbriÅ¡em $contactName iz stikov?'; + return 'Izbrišem $contactName iz stikov?'; } @override String get contacts_manageRepeater => 'Upravljaj Ponovitve'; @override - String get contacts_manageRoom => 'Upravljajte strežnik sobe'; + String get contacts_manageRoom => 'Upravljajte strežnik sobe'; @override String get contacts_roomLogin => 'Prijava v sobo'; @@ -767,11 +803,11 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_editGroup => 'Uredi skupino'; @override - String get contacts_deleteGroup => 'IzbriÅ¡i skupino'; + String get contacts_deleteGroup => 'Izbriši skupino'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'IzbriÅ¡i $groupName?'; + return 'Izbriši $groupName?'; } @override @@ -785,7 +821,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String contacts_groupAlreadyExists(String name) { - return 'Skupina \"$name\" že obstaja'; + return 'Skupina \"$name\" že obstaja'; } @override @@ -793,46 +829,46 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_noContactsMatchFilter => - 'Noben stik ne ustreza vaÅ¡emu kriteriju.'; + 'Noben stik ne ustreza vašemu kriteriju.'; @override - String get contacts_noMembers => 'Ni članov.'; + String get contacts_noMembers => 'Ni članov.'; @override String get contacts_lastSeenNow => 'Nazadnje viden zdaj'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Zadnjič viden pred $minutes minutami'; + return 'Zadnjič viden pred $minutes minutami'; } @override - String get contacts_lastSeenHourAgo => 'Zadnjič viden pred 1 uro.'; + String get contacts_lastSeenHourAgo => 'Zadnjič viden pred 1 uro.'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Zadnjič viden pred $hours urami'; + return 'Zadnjič viden pred $hours urami'; } @override - String get contacts_lastSeenDayAgo => 'Zadnjič viden pred 1 dnem'; + String get contacts_lastSeenDayAgo => 'Zadnjič viden pred 1 dnem'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Zadnjič viden pred $days dnem'; + return 'Zadnjič viden pred $days dnem'; } @override String get channels_title => 'Kanali'; @override - String get channels_noChannelsConfigured => 'Kanali Å¡e niso konfigurirani'; + String get channels_noChannelsConfigured => 'Kanali še niso konfigurirani'; @override String get channels_addPublicChannel => 'Dodaj javni kanal'; @override - String get channels_searchChannels => 'Poišči kanale...'; + String get channels_searchChannels => 'Poišči kanale...'; @override String get channels_noChannelsFound => 'Ne najdem kanalov.'; @@ -861,22 +897,22 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_editChannel => 'Uredi kanal'; @override - String get channels_muteChannel => 'UtiÅ¡aj kanal'; + String get channels_muteChannel => 'Utišaj kanal'; @override String get channels_unmuteChannel => 'Vklopi obvestila kanala'; @override - String get channels_deleteChannel => 'PoÅ¡lji kanal'; + String get channels_deleteChannel => 'Pošlji kanal'; @override String channels_deleteChannelConfirm(String name) { - return 'IzbriÅ¡em \"$name\"? To se ne da povrniti.'; + return 'Izbrišem \"$name\"? To se ne da povrniti.'; } @override String channels_channelDeleteFailed(String name) { - return 'Kanala $name ni bilo mogoče izbrisati'; + return 'Kanala $name ni bilo mogoče izbrisati'; } @override @@ -900,10 +936,10 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_standardPublicPsk => 'Standardni javni PSK'; @override - String get channels_pskHex => 'PSK (Å estnajstbinska)'; + String get channels_pskHex => 'PSK (Šestnajstbinska)'; @override - String get channels_generateRandomPsk => 'Generiraj naključni PSK'; + String get channels_generateRandomPsk => 'Generiraj naključni PSK'; @override String get channels_enterChannelName => 'Vnesi ime kanala'; @@ -937,50 +973,49 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_sortBy => 'Sortiraj po'; @override - String get channels_sortManual => 'Ročno'; + String get channels_sortManual => 'Ročno'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => 'NajnovejÅ¡e sporočilo'; + String get channels_sortLatestMessages => 'Najnovejše sporočilo'; @override - String get channels_sortUnread => 'NereÅ¡eno'; + String get channels_sortUnread => 'Nerešeno'; @override String get channels_createPrivateChannel => 'Ustvari zasebno kanal.'; @override String get channels_createPrivateChannelDesc => - 'Varno zaklenjeno s skrivnim ključem.'; + 'Varno zaklenjeno s skrivnim ključem.'; @override - String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini'; + String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini'; @override - String get channels_joinPrivateChannelDesc => - 'Ročno vnesite zaporni ključ.'; + String get channels_joinPrivateChannelDesc => 'Ročno vnesite zaporni ključ.'; @override - String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu'; + String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu'; @override String get channels_joinPublicChannelDesc => - 'Kdor karkoli je, lahko se pridruži tej skupini.'; + 'Kdor karkoli je, lahko se pridruži tej skupini.'; @override - String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom'; + String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom'; @override String get channels_joinHashtagChannelDesc => - 'Kdor karkoli, lahko se pridruži hashtag kanalom.'; + 'Kdor karkoli, lahko se pridruži hashtag kanalom.'; @override String get channels_scanQrCode => 'Skeniraj QR kodo'; @override - String get channels_scanQrCodeComingSoon => 'Prihajajoča'; + String get channels_scanQrCodeComingSoon => 'Prihajajoča'; @override String get channels_enterHashtag => 'Vnesite hashtag'; @@ -989,14 +1024,14 @@ class AppLocalizationsSl extends AppLocalizations { String get channels_hashtagHint => 'npr. #ekipa'; @override - String get chat_noMessages => 'Å e ni sporočil.'; + String get chat_noMessages => 'Še ni sporočil.'; @override - String get chat_sendMessageToStart => 'PoÅ¡lji sporočilo za začetek.'; + String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.'; @override String get chat_originalMessageNotFound => - 'Opozorilo: Sporočilo ni bilo najdeno'; + 'Opozorilo: Sporočilo ni bilo najdeno'; @override String chat_replyingTo(String name) { @@ -1005,7 +1040,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String chat_replyTo(String name) { - return 'OdpoÅ¡lji odgovor $name'; + return 'Odpošlji odgovor $name'; } @override @@ -1013,22 +1048,22 @@ class AppLocalizationsSl extends AppLocalizations { @override String chat_sendMessageTo(String contactName) { - return 'PoÅ¡lji sporočilo $contactName'; + return 'Pošlji sporočilo $contactName'; } @override - String get chat_typeMessage => 'Vnesi sporočilo...'; + String get chat_typeMessage => 'Vnesi sporočilo...'; @override String chat_messageTooLong(int maxBytes) { - return 'PoÅ¡iljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes byte-ov).'; + return 'Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno $maxBytes byte-ov).'; } @override - String get chat_messageCopied => 'Sporočilo poslano'; + String get chat_messageCopied => 'Sporočilo poslano'; @override - String get chat_messageDeleted => 'Sporočilo izbrisano'; + String get chat_messageDeleted => 'Sporočilo izbrisano'; @override String get chat_retryingMessage => 'Ponovni poskus.'; @@ -1039,7 +1074,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get chat_sendGif => 'PoÅ¡lji GIF'; + String get chat_sendGif => 'Pošlji GIF'; @override String get chat_reply => 'Odgovori'; @@ -1066,7 +1101,7 @@ class AppLocalizationsSl extends AppLocalizations { String get gifPicker_title => 'Izberi GIF'; @override - String get gifPicker_searchHint => 'Išči GIF-e...'; + String get gifPicker_searchHint => 'Išči GIF-e...'; @override String get gifPicker_poweredBy => 'Napredno z GIPHY'; @@ -1075,10 +1110,10 @@ class AppLocalizationsSl extends AppLocalizations { String get gifPicker_noGifsFound => 'Ne najdem GIF-ov.'; @override - String get gifPicker_failedLoad => 'NeuspeÅ¡no nalaganje GIF-a'; + String get gifPicker_failedLoad => 'Neuspešno nalaganje GIF-a'; @override - String get gifPicker_failedSearch => 'Iskanje neuspeÅ¡no.'; + String get gifPicker_failedSearch => 'Iskanje neuspešno.'; @override String get gifPicker_noInternet => 'Ni internetne povezave'; @@ -1093,26 +1128,26 @@ class AppLocalizationsSl extends AppLocalizations { String get debugLog_copyLog => 'Kopiraj dnevnik'; @override - String get debugLog_clearLog => 'BriÅ¡i log'; + String get debugLog_clearLog => 'Briši log'; @override - String get debugLog_copied => 'Beležka kopirana.'; + String get debugLog_copied => 'Beležka kopirana.'; @override - String get debugLog_bleCopied => 'Kopirana beležka iz BLE'; + String get debugLog_bleCopied => 'Kopirana beležka iz BLE'; @override String get debugLog_noEntries => 'Ni ustvarjenih debug zapisov.'; @override String get debugLog_enableInSettings => - 'Omogoči beleženje napak v nastavitvah aplikacije'; + 'Omogoči beleženje napak v nastavitvah aplikacije'; @override String get debugLog_frames => 'Okvirji'; @override - String get debugLog_rawLogRx => 'Svež Log-RX'; + String get debugLog_rawLogRx => 'Svež Log-RX'; @override String get debugLog_noBleActivity => 'Ni BLE aktivnosti.'; @@ -1132,12 +1167,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String debugFrame_destinationPubKey(String pubKey) { - return '- Destinirano Ključno Besedilo: $pubKey'; + return '- Destinirano Ključno Besedilo: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- ÄŒasovnik: $timestamp'; + return '- Časovnik: $timestamp'; } @override @@ -1168,23 +1203,23 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_pathManagement => 'Upravljanje poti'; @override - String get chat_ShowAllPaths => 'Prikaži vse poti'; + String get chat_ShowAllPaths => 'Prikaži vse poti'; @override - String get chat_routingMode => 'Navodilo za usmerjevalni način'; + String get chat_routingMode => 'Navodilo za usmerjevalni način'; @override String get chat_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; @override - String get chat_forceFloodMode => 'Nasilje obvezati v način'; + String get chat_forceFloodMode => 'Nasilje obvezati v način'; @override String get chat_recentAckPaths => 'Nedavni poti ACK (tap za uporabo):'; @override String get chat_pathHistoryFull => - 'Zapiske o poti so popolni. IzbriÅ¡i vnose, da dodaÅ¡ nove.'; + 'Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.'; @override String get chat_hopSingular => 'skok'; @@ -1204,14 +1239,14 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get chat_successes => 'UspeÅ¡ni'; + String get chat_successes => 'Uspešni'; @override - String get chat_removePath => 'IzbriÅ¡i pot'; + String get chat_removePath => 'Izbriši pot'; @override String get chat_noPathHistoryYet => - 'Ni shranjenih poti.\nPoÅ¡lji sporočilo za odkrivanje poti.'; + 'Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.'; @override String get chat_pathActions => 'Potni ukazi:'; @@ -1220,17 +1255,17 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_setCustomPath => 'Nastavi Prilozeno Pot'; @override - String get chat_setCustomPathSubtitle => 'Ročno določite potniÅ¡ko pot.'; + String get chat_setCustomPathSubtitle => 'Ročno določite potniško pot.'; @override - String get chat_clearPath => 'Počisti pot'; + String get chat_clearPath => 'Počisti pot'; @override - String get chat_clearPathSubtitle => 'Ob naslednji poÅ¡iljanju znova zbrati.'; + String get chat_clearPathSubtitle => 'Ob naslednji pošiljanju znova zbrati.'; @override String get chat_pathCleared => - 'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.'; + 'Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.'; @override String get chat_floodModeSubtitle => @@ -1238,14 +1273,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_floodModeEnabled => - 'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.'; + 'Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.'; @override String get chat_fullPath => 'Polna pot'; @override String get chat_pathDetailsNotAvailable => - 'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.'; + 'Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1260,13 +1295,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_pathSavedLocally => - 'Shrano lokalno. Povežite se za sinhronizacijo.'; + 'Shrano lokalno. Povežite se za sinhronizacijo.'; @override String get chat_pathDeviceConfirmed => 'Naprave potrjeno.'; @override - String get chat_pathDeviceNotConfirmed => 'Naprave Å¡e niso potrdile.'; + String get chat_pathDeviceNotConfirmed => 'Naprave še niso potrdile.'; @override String get chat_type => 'Vnesite'; @@ -1275,16 +1310,16 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_path => 'Pot'; @override - String get chat_publicKey => 'Ključ javnega tipa'; + String get chat_publicKey => 'Ključ javnega tipa'; @override - String get chat_compressOutgoingMessages => 'Stisnite izhodne sporočila'; + String get chat_compressOutgoingMessages => 'Stisnite izhodne sporočila'; @override String get chat_floodForced => 'Porolni (nasilje).'; @override - String get chat_directForced => 'NezglaÅ¡en (nasilje)'; + String get chat_directForced => 'Nezglašen (nasilje)'; @override String chat_hopsForced(int count) { @@ -1298,11 +1333,11 @@ class AppLocalizationsSl extends AppLocalizations { String get chat_direct => 'Neposredni'; @override - String get chat_poiShared => 'Deljeno točke MN'; + String get chat_poiShared => 'Deljeno točke MN'; @override String chat_unread(int count) { - return 'NereÅ¡eno: $count'; + return 'Nerešeno: $count'; } @override @@ -1310,21 +1345,21 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_openLinkConfirmation => - 'Ali želite odpreti to povezavo v brskalniku?'; + 'Ali želite odpreti to povezavo v brskalniku?'; @override String get chat_open => 'Odpri'; @override String chat_couldNotOpenLink(String url) { - return 'Povezave ni bilo mogoče odpreti: $url'; + return 'Povezave ni bilo mogoče odpreti: $url'; } @override String get chat_invalidLink => 'Neveljavna oblika povezave'; @override - String get map_title => 'Mapa omrežja'; + String get map_title => 'Mapa omrežja'; @override String get map_lineOfSight => 'Linija vida'; @@ -1334,11 +1369,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_noNodesWithLocation => - 'Nihče od notranjih elementov nima podatkov o lokaciji.'; + 'Nihče od notranjih elementov nima podatkov o lokaciji.'; @override String get map_nodesNeedGps => - 'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.'; + 'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.'; @override String map_nodesCount(int count) { @@ -1347,11 +1382,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String map_pinsCount(int count) { - return 'Žigovi: $count'; + return 'Žigovi: $count'; } @override - String get map_chat => 'ÄŒistemar'; + String get map_chat => 'Čistemar'; @override String get map_repeater => 'Ponovitelj'; @@ -1363,20 +1398,20 @@ class AppLocalizationsSl extends AppLocalizations { String get map_sensor => 'Senzor'; @override - String get map_pinDm => 'Zavežite (DM)'; + String get map_pinDm => 'Zavežite (DM)'; @override - String get map_pinPrivate => 'Zasebno označit'; + String get map_pinPrivate => 'Zasebno označit'; @override String get map_pinPublic => 'Oznaka (javna)'; @override - String get map_lastSeen => 'Zadnjič Zazet'; + String get map_lastSeen => 'Zadnjič Zazet'; @override String get map_disconnectConfirm => - 'Ste prepričani, da želite se odklopiti s tega naprave?'; + 'Ste prepričani, da želite se odklopiti s tega naprave?'; @override String get map_from => 'Od'; @@ -1388,7 +1423,7 @@ class AppLocalizationsSl extends AppLocalizations { String get map_flags => 'Zapestnice'; @override - String get map_shareMarkerHere => 'Delite točke tukaj.'; + String get map_shareMarkerHere => 'Delite točke tukaj.'; @override String get map_pinLabel => 'Oznaka za pritrditev'; @@ -1397,16 +1432,16 @@ class AppLocalizationsSl extends AppLocalizations { String get map_label => 'Oznaka'; @override - String get map_pointOfInterest => 'Točka zanimivosti'; + String get map_pointOfInterest => 'Točka zanimivosti'; @override - String get map_sendToContact => 'PoÅ¡lji v kontakt'; + String get map_sendToContact => 'Pošlji v kontakt'; @override - String get map_sendToChannel => 'PoÅ¡lji v kanal'; + String get map_sendToChannel => 'Pošlji v kanal'; @override - String get map_noChannelsAvailable => 'Nihče kanalov na voljo.'; + String get map_noChannelsAvailable => 'Nihče kanalov na voljo.'; @override String get map_publicLocationShare => 'Deljenje javne lokacije'; @@ -1418,37 +1453,37 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_connectToShareMarkers => - 'Povežite se z napravo za deljenje oznak.'; + 'Povežite se z napravo za deljenje oznak.'; @override - String get map_filterNodes => 'Filtirirajte člene'; + String get map_filterNodes => 'Filtirirajte člene'; @override String get map_nodeTypes => 'Vrste knope'; @override - String get map_chatNodes => 'ÄŒuti zvezde'; + String get map_chatNodes => 'Čuti zvezde'; @override String get map_repeaters => 'Ponovljalniki'; @override - String get map_otherNodes => 'Druge vozlišča'; + String get map_otherNodes => 'Druge vozlišča'; @override - String get map_keyPrefix => 'Predpona ključa'; + String get map_keyPrefix => 'Predpona ključa'; @override - String get map_filterByKeyPrefix => 'Filtri po predpomniku ključa'; + String get map_filterByKeyPrefix => 'Filtri po predpomniku ključa'; @override - String get map_publicKeyPrefix => 'Predifika javnega ključa'; + String get map_publicKeyPrefix => 'Predifika javnega ključa'; @override - String get map_markers => 'Označitelji'; + String get map_markers => 'Označitelji'; @override - String get map_showSharedMarkers => 'Pokaži skupno označenja'; + String get map_showSharedMarkers => 'Pokaži skupno označenja'; @override String get map_lastSeenTime => 'Datum zadnjega vpogleda'; @@ -1457,16 +1492,16 @@ class AppLocalizationsSl extends AppLocalizations { String get map_sharedPin => 'Deljeno naslovno geslo'; @override - String get map_joinRoom => 'Pridružiti sobo'; + String get map_joinRoom => 'Pridružiti sobo'; @override String get map_manageRepeater => 'Upravljajte Ponovitve'; @override - String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.'; + String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.'; @override - String get map_runTrace => 'Zaženi sledenje poti'; + String get map_runTrace => 'Zaženi sledenje poti'; @override String get map_removeLast => 'Odstrani Zadnji'; @@ -1476,51 +1511,51 @@ class AppLocalizationsSl extends AppLocalizations { @override String get mapCache_title => - 'Omrezni predpomnilnik zemljeÅ¡kih zemljejevskih slik'; + 'Omrezni predpomnilnik zemljeških zemljejevskih slik'; @override String get mapCache_selectAreaFirst => - 'Izberite območje za prvo predpomnilnik.'; + 'Izberite območje za prvo predpomnilnik.'; @override String get mapCache_noTilesToDownload => - 'Nihče slik ne bo naložil za to območje.'; + 'Nihče slik ne bo naložil za to območje.'; @override - String get mapCache_downloadTilesTitle => 'Naloži ploščice'; + String get mapCache_downloadTilesTitle => 'Naloži ploščice'; @override String mapCache_downloadTilesPrompt(int count) { - return 'NaložiÅ¥ $count plošč za uporabo v režimu brez povezave?'; + return 'Naložiť $count plošč za uporabo v režimu brez povezave?'; } @override - String get mapCache_downloadAction => 'Naloži'; + String get mapCache_downloadAction => 'Naloži'; @override String mapCache_cachedTiles(int count) { - return 'PospeÅ¡eno shranjeni $count plošč'; + return 'Pospešeno shranjeni $count plošč'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Shranjeni $downloaded ploščad ($failed neuspeÅ¡no)'; + return 'Shranjeni $downloaded ploščad ($failed neuspešno)'; } @override String get mapCache_clearOfflineCacheTitle => - 'Ponovite arhiv za offline način'; + 'Ponovite arhiv za offline način'; @override String get mapCache_clearOfflineCachePrompt => - 'IzbriÅ¡i vse predpomnilnikovane kartografske ploščice?'; + 'Izbriši vse predpomnilnikovane kartografske ploščice?'; @override String get mapCache_offlineCacheCleared => 'Omrezni predpomnik je bil izbrisal.'; @override - String get mapCache_noAreaSelected => 'Nizona označena povrÅ¡ina'; + String get mapCache_noAreaSelected => 'Nizona označena površina'; @override String get mapCache_cacheArea => 'Omanski prostor'; @@ -1529,27 +1564,27 @@ class AppLocalizationsSl extends AppLocalizations { String get mapCache_useCurrentView => 'Uporabi trenutni prikaz'; @override - String get mapCache_zoomRange => 'Občutek razpona'; + String get mapCache_zoomRange => 'Občutek razpona'; @override String mapCache_estimatedTiles(int count) { - return 'Predvideni ploščadi: $count'; + return 'Predvideni ploščadi: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Naloženo $completed / $total'; + return 'Naloženo $completed / $total'; } @override - String get mapCache_downloadTilesButton => 'Naloži ploščice'; + String get mapCache_downloadTilesButton => 'Naloži ploščice'; @override String get mapCache_clearCacheButton => 'Ponoviti arhiv'; @override String mapCache_failedDownloads(int count) { - return 'PoslovniÅ¡ki izniki: $count'; + return 'Poslovniški izniki: $count'; } @override @@ -1608,14 +1643,14 @@ class AppLocalizationsSl extends AppLocalizations { String get time_minutes => 'minute'; @override - String get time_allTime => 'Vse časovno obdobje'; + String get time_allTime => 'Vse časovno obdobje'; @override String get dialog_disconnect => 'Odklopiti'; @override String get dialog_disconnectConfirm => - 'Ste prepričani, da želite se odklopiti s tega naprave?'; + 'Ste prepričani, da želite se odklopiti s tega naprave?'; @override String get login_repeaterLogin => 'Ponovni vnos'; @@ -1648,36 +1683,36 @@ class AppLocalizationsSl extends AppLocalizations { String get login_routing => 'Usmerjanje'; @override - String get login_routingMode => 'Navodilo za usmerjevalni način'; + String get login_routingMode => 'Navodilo za usmerjevalni način'; @override String get login_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; @override - String get login_forceFloodMode => 'Nasilje obvezati v način'; + String get login_forceFloodMode => 'Nasilje obvezati v način'; @override - String get login_managePaths => 'Upravljajte PotniÅ¡ke Proti'; + String get login_managePaths => 'Upravljajte Potniške Proti'; @override String get login_login => 'Prijava'; @override String login_attempt(int current, int max) { - return 'PoskuÅ¡ajo $current/$max'; + return 'Poskušajo $current/$max'; } @override String login_failed(String error) { - return 'Prijava je bila neuspeÅ¡na: $error'; + return 'Prijava je bila neuspešna: $error'; } @override String get login_failedMessage => - 'Prijava je bila neuspeÅ¡na. Geslo je napačno ali pa je repetitor nedosegljiv.'; + 'Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.'; @override - String get common_reload => 'Ponovno naloži'; + String get common_reload => 'Ponovno naloži'; @override String get common_clear => 'Ponoviti'; @@ -1706,14 +1741,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.'; + 'Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.'; @override String get path_hexPrefixExample => - 'Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)'; + 'Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)'; @override - String get path_labelHexPrefixes => 'Pot (heksafixne skrajÅ¡ave)'; + String get path_labelHexPrefixes => 'Pot (heksafixne skrajšave)'; @override String get path_helperMaxHops => @@ -1724,19 +1759,19 @@ class AppLocalizationsSl extends AppLocalizations { @override String get path_noRepeatersFound => - 'Ne najdenih ponoviteljev ali strežnikov sob.'; + 'Ne najdenih ponoviteljev ali strežnikov sob.'; @override String get path_customPathsRequire => - 'Prilojene poti zahtevajo medhodne prenose, ki lahko prenaÅ¡ajo sporočila.'; + 'Prilojene poti zahtevajo medhodne prenose, ki lahko prenašajo sporočila.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Neveljačni Å¡esteročlenski prefiksi: $prefixes'; + return 'Neveljačni šesteročlenski prefiksi: $prefixes'; } @override - String get path_tooLong => 'Pot je prevelika. Dovoljeno največ 64 skokov.'; + String get path_tooLong => 'Pot je prevelika. Dovoljeno največ 64 skokov.'; @override String get path_setPath => 'Nastavi Pot'; @@ -1745,7 +1780,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_management => 'Upravljanje ponovitve'; @override - String get room_management => 'Upravljanje stremlišča'; + String get room_management => 'Upravljanje stremlišča'; @override String get repeater_managementTools => 'Upravne orodje'; @@ -1769,13 +1804,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliSubtitle => - 'PoÅ¡lji ukazne povelje na ponovitveno enoto.'; + 'Pošlji ukazne povelje na ponovitveno enoto.'; @override String get repeater_neighbors => 'Sosedi'; @override - String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.'; + String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.'; @override String get repeater_settings => 'Nastavitve'; @@ -1788,13 +1823,13 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_statusTitle => 'Status ponovitelja'; @override - String get repeater_routingMode => 'Navodilo za usmerjevalni način'; + String get repeater_routingMode => 'Navodilo za usmerjevalni način'; @override String get repeater_autoUseSavedPath => 'Avto (uporabi shranjeno pot)'; @override - String get repeater_forceFloodMode => 'Nasilje obvezati v način'; + String get repeater_forceFloodMode => 'Nasilje obvezati v način'; @override String get repeater_pathManagement => 'Upravljanje poti'; @@ -1807,7 +1842,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String repeater_errorLoadingStatus(String error) { - return 'Napaka pri obnaÅ¡anju: $error'; + return 'Napaka pri obnašanju: $error'; } @override @@ -1820,10 +1855,10 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_clockAtLogin => 'Ure (pri prijavi)'; @override - String get repeater_uptime => 'ÄŒas delovanja'; + String get repeater_uptime => 'Čas delovanja'; @override - String get repeater_queueLength => 'Dolžina čakalne vrste'; + String get repeater_queueLength => 'Dolžina čakalne vrste'; @override String get repeater_debugFlags => 'Nastavitve odpravilnosti'; @@ -1835,10 +1870,10 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_lastRssi => 'Potredno RSSI'; @override - String get repeater_lastSnr => 'Nazadnje zabeležena SNR'; + String get repeater_lastSnr => 'Nazadnje zabeležena SNR'; @override - String get repeater_noiseFloor => 'Å umovita raven'; + String get repeater_noiseFloor => 'Šumovita raven'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1850,7 +1885,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_packetStatistics => 'Statistika paketa'; @override - String get repeater_sent => 'PoÅ¡ljeno'; + String get repeater_sent => 'Pošljeno'; @override String get repeater_received => 'Prejeto'; @@ -1907,7 +1942,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_adminPasswordHelper => 'Polni dostopno geslo'; @override - String get repeater_guestPassword => 'Geslo gostača'; + String get repeater_guestPassword => 'Geslo gostača'; @override String get repeater_guestPasswordHelper => 'Odpovedni dostopni geslo'; @@ -1922,16 +1957,16 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX Moč'; + String get repeater_txPower => 'TX Moč'; @override String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => 'Pasovna Å¡irina'; + String get repeater_bandwidth => 'Pasovna širina'; @override - String get repeater_spreadingFactor => 'RazÅ¡iritveni faktor'; + String get repeater_spreadingFactor => 'Razširitveni faktor'; @override String get repeater_codingRate => 'Programska hitrost'; @@ -1940,37 +1975,37 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_locationSettings => 'Nastavitve lokacije'; @override - String get repeater_latitude => 'Å irina'; + String get repeater_latitude => 'Širina'; @override String get repeater_latitudeHelper => 'Desetbinske protiure (npr. 37.7749)'; @override - String get repeater_longitude => 'Dolžina'; + String get repeater_longitude => 'Dolžina'; @override String get repeater_longitudeHelper => 'Desetbinske protiure (npr. -122,4194)'; @override - String get repeater_features => 'Značilnosti'; + String get repeater_features => 'Značilnosti'; @override String get repeater_packetForwarding => 'Usmerjanje paketa'; @override String get repeater_packetForwardingSubtitle => - 'Omogoči ponovitelja za usmerjanje paketov.'; + 'Omogoči ponovitelja za usmerjanje paketov.'; @override String get repeater_guestAccess => 'Prijemnik'; @override String get repeater_guestAccessSubtitle => - 'Omogoči dostop gostom v samo bralni načinu.'; + 'Omogoči dostop gostom v samo bralni načinu.'; @override - String get repeater_privacyMode => 'Privatni način'; + String get repeater_privacyMode => 'Privatni način'; @override String get repeater_privacyModeSubtitle => 'Skrita imena/lokacije v oglasih'; @@ -1996,7 +2031,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_encryptedAdvertInterval => - 'Å ifrirana Oglasovalska Trajanje'; + 'Šifrirana Oglasovalska Trajanje'; @override String get repeater_dangerZone => 'Opozorilo'; @@ -2009,21 +2044,21 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_rebootRepeaterConfirm => - 'Ste prepričani, da želite ponovno zagon tega ponovitelja?'; + 'Ste prepričani, da želite ponovno zagon tega ponovitelja?'; @override - String get repeater_regenerateIdentityKey => 'Ponovite Ključ Identnosti'; + String get repeater_regenerateIdentityKey => 'Ponovite Ključ Identnosti'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Ustvarite novo par javnih/zasebnih ključev'; + 'Ustvarite novo par javnih/zasebnih ključev'; @override String get repeater_regenerateIdentityKeyConfirm => 'To bo ustvaril novo identiteto za ponavljalnik. Prijavite se?'; @override - String get repeater_eraseFileSystem => 'Počisti Sustav Vajah'; + String get repeater_eraseFileSystem => 'Počisti Sustav Vajah'; @override String get repeater_eraseFileSystemSubtitle => @@ -2031,7 +2066,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_eraseFileSystemConfirm => - 'OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!'; + 'OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!'; @override String get repeater_eraseSerialOnly => @@ -2044,14 +2079,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String repeater_errorSendingCommand(String error) { - return 'Napaka pri poÅ¡iljanju ukaznega: $error'; + return 'Napaka pri pošiljanju ukaznega: $error'; } @override String get repeater_confirm => 'Potrdit'; @override - String get repeater_settingsSaved => 'Nastavitve so shranjene uspeÅ¡no.'; + String get repeater_settingsSaved => 'Nastavitve so shranjene uspešno.'; @override String repeater_errorSavingSettings(String error) { @@ -2066,7 +2101,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_refreshRadioSettings => 'Ponovno Nastavitve Radija'; @override - String get repeater_refreshTxPower => 'Ponovno nastavi TX moč'; + String get repeater_refreshTxPower => 'Ponovno nastavi TX moč'; @override String get repeater_refreshLocationSettings => @@ -2081,7 +2116,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_refreshPrivacyMode => - 'Ponovno aktiviraj način zasebnosti'; + 'Ponovno aktiviraj način zasebnosti'; @override String get repeater_refreshAdvertisementSettings => @@ -2094,14 +2129,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String repeater_errorRefreshing(String label) { - return 'Napaka pri osveževanju $label'; + return 'Napaka pri osveževanju $label'; } @override String get repeater_cliTitle => 'Ponovitelj CLI'; @override - String get repeater_debugNextCommand => 'Popravi naslednje ukazne možnosti'; + String get repeater_debugNextCommand => 'Popravi naslednje ukazne možnosti'; @override String get repeater_commandHelp => 'Pomoc'; @@ -2111,7 +2146,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_noCommandsSent => - 'Niti ena ukazne povratne informacije Å¡e ni poslana.'; + 'Niti ena ukazne povratne informacije še ni poslana.'; @override String get repeater_typeCommandOrUseQuick => @@ -2121,7 +2156,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_enterCommandHint => 'Vnesite ukaz...'; @override - String get repeater_previousCommand => 'PrejÅ¡nji ukaz'; + String get repeater_previousCommand => 'Prejšnji ukaz'; @override String get repeater_nextCommand => 'Naslednja ukazna'; @@ -2150,7 +2185,7 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_cliQuickNeighbors => 'Sosedi'; @override - String get repeater_cliQuickVersion => 'Različica'; + String get repeater_cliQuickVersion => 'Različica'; @override String get repeater_cliQuickAdvertise => 'Oglasite'; @@ -2159,14 +2194,14 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_cliQuickClock => 'Ura'; @override - String get repeater_cliHelpAdvert => 'PoÅ¡lje paket oglasov'; + String get repeater_cliHelpAdvert => 'Pošlje paket oglasov'; @override String get repeater_cliHelpReboot => 'Ponastavi naprave. (Opomba, lahko pride do \'Timeouta\', kar je normalno)'; @override - String get repeater_cliHelpClock => 'Prikaže trenutno uro po uri naprave.'; + String get repeater_cliHelpClock => 'Prikaže trenutno uro po uri naprave.'; @override String get repeater_cliHelpPassword => @@ -2174,65 +2209,65 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpVersion => - 'Prikaže različico naprave in datum izrabe strojne opreme.'; + 'Prikaže različico naprave in datum izrabe strojne opreme.'; @override String get repeater_cliHelpClearStats => - 'Ponastavi različne statistične Å¡tevke na nič.'; + 'Ponastavi različne statistične števke na nič.'; @override - String get repeater_cliHelpSetAf => 'Nastavi časovni koeficient.'; + String get repeater_cliHelpSetAf => 'Nastavi časovni koeficient.'; @override String get repeater_cliHelpSetTx => - 'Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)'; + 'Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)'; @override String get repeater_cliHelpSetRepeat => - 'Omogoči ali onemogoči vlogo ponovitelja za tono.'; + 'Omogoči ali onemogoči vlogo ponovitelja za tono.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Osebni strežnik) ÄŒe je \'vklopljeno\', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).'; + '(Osebni strežnik) Če je \'vklopljeno\', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).'; @override String get repeater_cliHelpSetFloodMax => - 'Nastavi največjo Å¡tevilo skokov za vstopne poplave (če je >= maks, paket ni usmerjen)'; + 'Nastavi največjo število skokov za vstopne poplave (če je >= maks, paket ni usmerjen)'; @override String get repeater_cliHelpSetIntThresh => - 'Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.'; + 'Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.'; + 'Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.'; @override String get repeater_cliHelpSetMultiAcks => - 'Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".'; + 'Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".'; @override String get repeater_cliHelpSetAdvertInterval => - 'Nastavi časovno obmesto v minutah za poÅ¡iljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.'; + 'Nastavi časovno obmesto v minutah za pošiljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Nastavi časovno obmesto v urah za poÅ¡iljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.'; + 'Nastavi časovno obmesto v urah za pošiljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.'; @override String get repeater_cliHelpSetGuestPassword => - 'Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi poÅ¡iljajo zahtevo \"Get Stats\")'; + 'Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi pošiljajo zahtevo \"Get Stats\")'; @override String get repeater_cliHelpSetName => 'Nastavi ime oglasnika.'; @override String get repeater_cliHelpSetLat => - 'Nastavi zemljepisno Å¡irino oglaÅ¡evalskega zemljevida (desetdeljne).'; + 'Nastavi zemljepisno širino oglaševalskega zemljevida (desetdeljne).'; @override String get repeater_cliHelpSetLon => - 'Nastavi zemljevidno Å¡irino oglasnika. (desetdelne stopnje)'; + 'Nastavi zemljevidno širino oglasnika. (desetdelne stopnje)'; @override String get repeater_cliHelpSetRadio => @@ -2240,18 +2275,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpSetRxDelay => - 'Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.'; + 'Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.'; @override String get repeater_cliHelpSetTxDelay => - 'Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjÅ¡a verjetnost kolizij)'; + 'Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjša verjetnost kolizij)'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.'; + 'Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.'; @override - String get repeater_cliHelpSetBridgeEnabled => 'Omogoči/Preklopi most.'; + String get repeater_cliHelpSetBridgeEnabled => 'Omogoči/Preklopi most.'; @override String get repeater_cliHelpSetBridgeDelay => @@ -2271,39 +2306,39 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpSetAdcMultiplier => - 'Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).'; + 'Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).'; @override String get repeater_cliHelpTempRadio => - 'Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).'; + 'Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).'; @override String get repeater_cliHelpSetPerm => - 'Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).'; + 'Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).'; @override String get repeater_cliHelpGetBridgeType => - 'DobrodoÅ¡li pri izbiri vrste mostu: brez, rs232, espnow'; + 'Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Začnete beleženje paketov v datotekovni sistem.'; + 'Začnete beleženje paketov v datotekovni sistem.'; @override String get repeater_cliHelpLogStop => - 'Ustavite beleženje paketov v datotečno sistem.'; + 'Ustavite beleženje paketov v datotečno sistem.'; @override String get repeater_cliHelpLogErase => - 'IzbriÅ¡e pakete zapisov iz datotek sistema.'; + 'Izbriše pakete zapisov iz datotek sistema.'; @override String get repeater_cliHelpNeighbors => - 'Prikaže seznam drugih ponovnih knopov, do katerih je priÅ¡lo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4'; + 'Prikaže seznam drugih ponovnih knopov, do katerih je prišlo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4'; @override String get repeater_cliHelpNeighborRemove => - 'IzbriÅ¡e prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.'; + 'Izbriše prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.'; @override String get repeater_cliHelpRegion => @@ -2311,11 +2346,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s poÅ¡iljanjem praznega reda/ukaza.'; + 'Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s pošiljanjem praznega reda/ukaza.'; @override String get repeater_cliHelpRegionGet => - 'Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) \'F\'\"'; + 'Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) \'F\'\"'; @override String get repeater_cliHelpRegionPut => @@ -2323,7 +2358,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpRegionRemove => - 'IzbriÅ¡e definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)'; + 'Izbriše definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)'; @override String get repeater_cliHelpRegionAllowf => @@ -2331,11 +2366,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpRegionDenyf => - 'Odstrani dovoljenje \'F\'lood\' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)'; + 'Odstrani dovoljenje \'F\'lood\' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)'; @override String get repeater_cliHelpRegionHome => - 'Odgovori z trenutnim \'domovim\' območjem. (Opomba je bila Å¡e nujno uporabljena, rezervirano za prihodnost)'; + 'Odgovori z trenutnim \'domovim\' območjem. (Opomba je bila še nujno uporabljena, rezervirano za prihodnost)'; @override String get repeater_cliHelpRegionHomeSet => 'Nastavi regijo \'domov\'.'; @@ -2346,37 +2381,36 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Pokaže status GPS-ja. ÄŒe je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in Å¡tetjem satelitiv.'; + 'Pokaže status GPS-ja. Če je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in štetjem satelitiv.'; @override - String get repeater_cliHelpGpsOnOff => - 'Omogoči/onameni GPS način delovanja.'; + String get repeater_cliHelpGpsOnOff => 'Omogoči/onameni GPS način delovanja.'; @override String get repeater_cliHelpGpsSync => - 'Sinhronizira čas časa ničala z gps uro.'; + 'Sinhronizira čas časa ničala z gps uro.'; @override String get repeater_cliHelpGpsSetLoc => - 'Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.'; + 'Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.'; @override String get repeater_cliHelpGpsAdvert => - 'Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaÅ¡evati lokacijo shranjeno v preferencah'; + 'Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaševati lokacijo shranjeno v preferencah'; @override String get repeater_cliHelpGpsAdvertSet => - 'Nastavi konfiguracijo oglasa na določenem mestu.'; + 'Nastavi konfiguracijo oglasa na določenem mestu.'; @override String get repeater_commandsListTitle => 'Seznam ukazov'; @override String get repeater_commandsListNote => - 'Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".'; + 'Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".'; @override - String get repeater_general => 'Općenito'; + String get repeater_general => 'Općenito'; @override String get repeater_settingsCategory => 'Nastavitve'; @@ -2403,17 +2437,17 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_gpsNote => - 'GPS ukaz je bil uveden za upravljanje z vpraÅ¡anji, povezanimi z lokacijo.'; + 'GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.'; @override - String get telemetry_receivedData => 'Prejeto Telemetrično podatke'; + String get telemetry_receivedData => 'Prejeto Telemetrično podatke'; @override String get telemetry_requestTimeout => 'Zahtev telemetrije je iztekla.'; @override String telemetry_errorLoading(String error) { - return 'Napaka pri obnaÅ¡anju telemetrije: $error'; + return 'Napaka pri obnašanju telemetrije: $error'; } @override @@ -2456,7 +2490,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2468,7 +2502,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String neighbors_errorLoading(String error) { - return 'Napaka pri obnaÅ¡anju sosedov: $error'; + return 'Napaka pri obnašanju sosedov: $error'; } @override @@ -2484,14 +2518,14 @@ class AppLocalizationsSl extends AppLocalizations { @override String neighbors_heardAgo(String time) { - return 'Udeleženec je prejel sporočilo $time nazaj.'; + return 'Udeleženec je prejel sporočilo $time nazaj.'; } @override String get channelPath_title => 'Pot do paketa'; @override - String get channelPath_viewMap => 'Prikaži zemljeznico'; + String get channelPath_viewMap => 'Prikaži zemljeznico'; @override String get channelPath_otherObservedPaths => 'Drugi opazovani poti'; @@ -2504,10 +2538,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Podrobnosti o paketu za dostavo niso navedene.'; @override - String get channelPath_messageDetails => 'Podrobnosti sporočila'; + String get channelPath_messageDetails => 'Podrobnosti sporočila'; @override - String get channelPath_senderLabel => 'PoÅ¡iljatelj'; + String get channelPath_senderLabel => 'Pošiljatelj'; @override String get channelPath_timeLabel => 'Ura'; @@ -2525,11 +2559,11 @@ class AppLocalizationsSl extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Opazovana pot $index • $hops'; + return 'Opazovana pot $index • $hops'; } @override - String get channelPath_noLocationData => 'Nihče ni določil lokacije.'; + String get channelPath_noLocationData => 'Nihče ni določil lokacije.'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2580,7 +2614,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override @@ -2601,14 +2635,14 @@ class AppLocalizationsSl extends AppLocalizations { 'Ustvari novo skupnost in jo deli preko QR kode.'; @override - String get community_join => 'Pridružiti se'; + String get community_join => 'Pridružiti se'; @override - String get community_joinTitle => 'Pridružite se skupnosti'; + String get community_joinTitle => 'Pridružite se skupnosti'; @override String community_joinConfirmation(String name) { - return 'ŽeliÅ¡ se pridružiti skupnosti \"$name\"?'; + return 'Želiš se pridružiti skupnosti \"$name\"?'; } @override @@ -2619,7 +2653,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Nasmerite kamero s skupnostnim QR kodom.'; @override - String get community_showQr => 'Pokaži QR kodo'; + String get community_showQr => 'Pokaži QR kodo'; @override String get community_publicChannel => 'Skupnostna javna'; @@ -2648,22 +2682,22 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Skenirajte to QR kodo za vključitev $name.'; + return 'Skenirajte to QR kodo za vključitev $name.'; } @override String get community_hashtagPrivacyHint => - 'Hashtag kanali skupnosti so dostopni samo članom skupnosti'; + 'Hashtag kanali skupnosti so dostopni samo članom skupnosti'; @override String get community_invalidQrCode => 'Neveljaven QR koden skupnosti'; @override - String get community_alreadyMember => 'Že član'; + String get community_alreadyMember => 'Že član'; @override String community_alreadyMemberMessage(String name) { - return 'Kljub temu ste že član/ka $name.'; + return 'Kljub temu ste že član/ka $name.'; } @override @@ -2674,12 +2708,11 @@ class AppLocalizationsSl extends AppLocalizations { 'Samodejno dodaj javni kanal za to skupnost.'; @override - String get community_noCommunities => - 'Å e nobena skupnost se ni pridružila.'; + String get community_noCommunities => 'Še nobena skupnost se ni pridružila.'; @override String get community_scanOrCreate => - 'Skeniraj QR kodo ali ustvari skupnost za začetek.'; + 'Skeniraj QR kodo ali ustvari skupnost za začetek.'; @override String get community_manageCommunities => 'Upravljanje skupnosti'; @@ -2694,7 +2727,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_deleteChannelsWarning(int count) { - return 'To bo izbrisalo tudi $count kanal/kanalov in njihova sporočila.'; + return 'To bo izbrisalo tudi $count kanal/kanalov in njihova sporočila.'; } @override @@ -2707,7 +2740,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.'; + return 'Preberite novo tajno geslo za \"$name\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.'; } @override @@ -2719,7 +2752,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get community_updateSecret => 'Ažuriraj ključ'; + String get community_updateSecret => 'Ažuriraj ključ'; @override String community_secretUpdated(String name) { @@ -2728,7 +2761,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String community_scanToUpdateSecret(String name) { - return 'Skeniraj novo QR kodo za posodabljanje ključa za $name'; + return 'Skeniraj novo QR kodo za posodabljanje ključa za $name'; } @override @@ -2753,7 +2786,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String get community_communityHashtagDesc => - 'Izključeno za uporabnike skupnosti'; + 'Izključeno za uporabnike skupnosti'; @override String community_forCommunity(String name) { @@ -2761,16 +2794,16 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get listFilter_tooltip => 'Filtri in vrstiči'; + String get listFilter_tooltip => 'Filtri in vrstiči'; @override String get listFilter_sortBy => 'Sortiraj po'; @override - String get listFilter_latestMessages => 'NajnovejÅ¡e sporočilo'; + String get listFilter_latestMessages => 'Najnovejše sporočilo'; @override - String get listFilter_heardRecently => 'Nedavno sliÅ¡an'; + String get listFilter_heardRecently => 'Nedavno slišan'; @override String get listFilter_az => 'A-Z'; @@ -2815,18 +2848,17 @@ class AppLocalizationsSl extends AppLocalizations { String get pathTrace_notAvailable => 'Potni sled ni na voljo.'; @override - String get pathTrace_refreshTooltip => 'Osveži Path Trace.'; + String get pathTrace_refreshTooltip => 'Osveži Path Trace.'; @override String get pathTrace_someHopsNoLocation => - 'Ena ali več hmelju manjka lokacija!'; + 'Ena ali več hmelju manjka lokacija!'; @override - String get pathTrace_clearTooltip => 'Počisti pot'; + String get pathTrace_clearTooltip => 'Počisti pot'; @override - String get losSelectStartEnd => - 'Izberite začetno in končno vozlišče za LOS.'; + String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.'; @override String losRunFailed(String error) { @@ -2834,24 +2866,24 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get losClearAllPoints => 'Počisti vse točke'; + String get losClearAllPoints => 'Počisti vse točke'; @override String get losRunToViewElevationProfile => - 'Zaženite LOS za ogled viÅ¡inskega profila'; + 'Zaženite LOS za ogled višinskega profila'; @override String get losMenuTitle => 'LOS meni'; @override String get losMenuSubtitle => - 'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri'; + 'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri'; @override - String get losShowDisplayNodes => 'Pokaži prikazna vozlišča'; + String get losShowDisplayNodes => 'Pokaži prikazna vozlišča'; @override - String get losCustomPoints => 'Točke po meri'; + String get losCustomPoints => 'Točke po meri'; @override String losCustomPointLabel(int index) { @@ -2859,10 +2891,10 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get losPointA => 'Točka A'; + String get losPointA => 'Točka A'; @override - String get losPointB => 'Točka B'; + String get losPointB => 'Točka B'; @override String losAntennaA(String value, String unit) { @@ -2875,10 +2907,10 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get losRun => 'Zaženi LOS'; + String get losRun => 'Zaženi LOS'; @override - String get losNoElevationData => 'Ni podatkov o viÅ¡ini'; + String get losNoElevationData => 'Ni podatkov o višini'; @override String losProfileClear( @@ -2887,7 +2919,7 @@ class AppLocalizationsSl extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, čisti LOS, najmanjÅ¡a razdalja $clearance $heightUnit'; + return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit'; } @override @@ -2913,27 +2945,27 @@ class AppLocalizationsSl extends AppLocalizations { @override String get losErrorElevationUnavailable => - 'Podatki o nadmorski viÅ¡ini niso na voljo za enega ali več vzorcev.'; + 'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.'; @override String get losErrorInvalidInput => - 'Neveljavni podatki o točkah/viÅ¡ini za izračun LOS.'; + 'Neveljavni podatki o točkah/višini za izračun LOS.'; @override - String get losRenameCustomPoint => 'Preimenujte točko po meri'; + String get losRenameCustomPoint => 'Preimenujte točko po meri'; @override - String get losPointName => 'Ime točke'; + String get losPointName => 'Ime točke'; @override - String get losShowPanelTooltip => 'Pokaži ploščo LOS'; + String get losShowPanelTooltip => 'Pokaži ploščo LOS'; @override - String get losHidePanelTooltip => 'Skrij ploščo LOS'; + String get losHidePanelTooltip => 'Skrij ploščo LOS'; @override String get losElevationAttribution => - 'Podatki o viÅ¡ini: Open-Meteo (CC BY 4.0)'; + 'Podatki o višini: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Radijski horizont'; @@ -2948,10 +2980,10 @@ class AppLocalizationsSl extends AppLocalizations { String get losFrequencyLabel => 'Frekvenca'; @override - String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; + String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; @override - String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; + String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; @override String losFrequencyDialogDescription( @@ -2960,7 +2992,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'ZačenÅ¡i od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; + return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; } @override @@ -2976,13 +3008,13 @@ class AppLocalizationsSl extends AppLocalizations { String get contacts_repeaterPing => 'Pinguj ponavljalnik'; @override - String get contacts_roomPathTrace => 'Sledenje poti do strežnika sobe'; + String get contacts_roomPathTrace => 'Sledenje poti do strežnika sobe'; @override - String get contacts_roomPing => 'Ping strežnik sobe'; + String get contacts_roomPing => 'Ping strežnik sobe'; @override - String get contacts_chatTraceRoute => 'Slediti poti žarkov'; + String get contacts_chatTraceRoute => 'Slediti poti žarkov'; @override String contacts_pathTraceTo(String name) { @@ -2990,31 +3022,31 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get contacts_clipboardEmpty => 'Odložišče je prazno.'; + String get contacts_clipboardEmpty => 'Odložišče je prazno.'; @override String get contacts_invalidAdvertFormat => 'Neveljavni kontaktne podatke'; @override - String get contacts_contactImported => 'Kontakt je bil uvožen.'; + String get contacts_contactImported => 'Kontakt je bil uvožen.'; @override - String get contacts_contactImportFailed => 'Kontakt ni bil uspeÅ¡no uvožen.'; + String get contacts_contactImportFailed => 'Kontakt ni bil uspešno uvožen.'; @override String get contacts_zeroHopAdvert => 'Reklama brez posrednikov'; @override - String get contacts_floodAdvert => 'Poplavna oglás'; + String get contacts_floodAdvert => 'Poplavna oglás'; @override - String get contacts_copyAdvertToClipboard => 'Kopiraj oglas v odložišče'; + String get contacts_copyAdvertToClipboard => 'Kopiraj oglas v odložišče'; @override - String get contacts_addContactFromClipboard => 'Dodaj stik iz odložišča'; + String get contacts_addContactFromClipboard => 'Dodaj stik iz odložišča'; @override - String get contacts_ShareContact => 'Kopiraj stik v Odložišče'; + String get contacts_ShareContact => 'Kopiraj stik v Odložišče'; @override String get contacts_ShareContactZeroHop => 'Deliti kontakt prek oglasa'; @@ -3024,15 +3056,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_zeroHopContactAdvertFailed => - 'PoÅ¡iljanje kontakta ni uspelo.'; + 'Pošiljanje kontakta ni uspelo.'; @override String get contacts_contactAdvertCopied => - 'Oglas je bil kopiran v odložišče.'; + 'Oglas je bil kopiran v odložišče.'; @override String get contacts_contactAdvertCopyFailed => - 'Kopiranje oglasa v odložišče je spodletelo.'; + 'Kopiranje oglasa v odložišče je spodletelo.'; @override String get notification_activityTitle => 'Aktivnost MeshCore'; @@ -3042,10 +3074,10 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'sporočil', - few: 'sporočila', - two: 'sporočili', - one: 'sporočilo', + other: 'sporočil', + few: 'sporočila', + two: 'sporočili', + one: 'sporočilo', ); return '$count $_temp0'; } @@ -3055,10 +3087,10 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'sporočil kanala', - few: 'sporočila kanala', - two: 'sporočili kanala', - one: 'sporočilo kanala', + other: 'sporočil kanala', + few: 'sporočila kanala', + two: 'sporočili kanala', + one: 'sporočilo kanala', ); return '$count $_temp0'; } @@ -3068,10 +3100,10 @@ class AppLocalizationsSl extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'novih vozlišč', - few: 'nova vozlišča', - two: 'novi vozlišči', - one: 'novo vozlišče', + other: 'novih vozlišč', + few: 'nova vozlišča', + two: 'novi vozlišči', + one: 'novo vozlišče', ); return '$count $_temp0'; } @@ -3082,15 +3114,15 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get notification_receivedNewMessage => 'Prejeto novo sporočilo'; + String get notification_receivedNewMessage => 'Prejeto novo sporočilo'; @override String get settings_gpxExportRepeaters => - 'Izvoz ponoviteljev / strežnika sobe v GPX'; + 'Izvoz ponoviteljev / strežnika sobe v GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.'; + 'Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.'; @override String get settings_gpxExportContacts => 'Izvoz spremljevalcev v GPX'; @@ -3107,21 +3139,21 @@ class AppLocalizationsSl extends AppLocalizations { 'Izvozi vse kontakte z lokacijo v datoteko GPX.'; @override - String get settings_gpxExportSuccess => 'UspeÅ¡no izvoz GPX datoteke.'; + String get settings_gpxExportSuccess => 'Uspešno izvoz GPX datoteke.'; @override String get settings_gpxExportNoContacts => 'Ni stikov za izvoz.'; @override String get settings_gpxExportNotAvailable => - 'Ni podprto na vaÅ¡em napravi/operacijskem sistemu'; + 'Ni podprto na vašem napravi/operacijskem sistemu'; @override - String get settings_gpxExportError => 'Pri izvozu je priÅ¡lo do napake.'; + String get settings_gpxExportError => 'Pri izvozu je prišlo do napake.'; @override String get settings_gpxExportRepeatersRoom => - 'Lokacije ponovljivca in strežnika sobe'; + 'Lokacije ponovljivca in strežnika sobe'; @override String get settings_gpxExportChat => 'Lokacije spremljevalcev'; @@ -3131,15 +3163,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_gpxExportShareText => - 'Podatki kart izvoženi iz meshcore-open'; + 'Podatki kart izvoženi iz meshcore-open'; @override String get settings_gpxExportShareSubject => 'meshcore-open izvoz podatkov GPX karte'; @override - String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji'; + String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji'; @override - String get snrIndicator_lastSeen => 'Zadnjič videno'; + String get snrIndicator_lastSeen => 'Zadnjič videno'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 3fab6c2..c68faf5 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -30,7 +30,7 @@ class AppLocalizationsSv extends AppLocalizations { String get common_connect => 'Anslut'; @override - String get common_unknownDevice => 'Okänd enhet'; + String get common_unknownDevice => 'Okänd enhet'; @override String get common_save => 'Spara'; @@ -39,19 +39,19 @@ class AppLocalizationsSv extends AppLocalizations { String get common_delete => 'Radera'; @override - String get common_close => 'Stänga'; + String get common_close => 'Stänga'; @override String get common_edit => 'Redigera'; @override - String get common_add => 'Lägg till'; + String get common_add => 'Lägg till'; @override - String get common_settings => 'Inställningar'; + String get common_settings => 'Inställningar'; @override - String get common_disconnect => 'Koppla frÃ¥n'; + String get common_disconnect => 'Koppla från'; @override String get common_connected => 'Ansluten'; @@ -63,7 +63,7 @@ class AppLocalizationsSv extends AppLocalizations { String get common_create => 'Skapa'; @override - String get common_continue => 'Fortsätt'; + String get common_continue => 'Fortsätt'; @override String get common_share => 'Dela'; @@ -72,10 +72,10 @@ class AppLocalizationsSv extends AppLocalizations { String get common_copy => 'Kopiera'; @override - String get common_retry => 'Försök igen'; + String get common_retry => 'Försök igen'; @override - String get common_hide => 'Dölj'; + String get common_hide => 'Dölj'; @override String get common_remove => 'Ta bort'; @@ -93,7 +93,7 @@ class AppLocalizationsSv extends AppLocalizations { String get common_loading => 'Laddar...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -119,21 +119,62 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbScreenSubtitle => - 'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.'; + 'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.'; @override - String get usbScreenStatus => 'Välj en USB-enhet'; + String get usbScreenStatus => 'Välj en USB-enhet'; @override String get usbScreenNote => - 'USB-seriell kommunikation är aktiv pÃ¥ kompatibla Android-enheter och datorplattformar.'; + 'USB-seriell kommunikation är aktiv på stöderdade Android-enheter och skrivbordsplattformar.'; @override String get usbScreenEmptyState => 'Inga USB-enheter hittades. Anslut en och uppdatera.'; @override - String get scanner_scanning => 'Söker efter enheter...'; + String get usbErrorPermissionDenied => 'Tillgången via USB nekas.'; + + @override + String get usbErrorDeviceMissing => + 'Den valda USB-enheten är inte längre tillgänglig.'; + + @override + String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.'; + + @override + String get usbErrorBusy => + 'En annan förfrågan om USB-anslutning är redan pågående.'; + + @override + String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.'; + + @override + String get usbErrorOpenFailed => 'Kunde inte öppna det valda USB-enheten.'; + + @override + String get usbErrorConnectFailed => + 'Kunde inte ansluta till det valda USB-enheten.'; + + @override + String get usbErrorUnsupported => + 'USB-seriell kommunikation stöds inte på denna plattform.'; + + @override + String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.'; + + @override + String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.'; + + @override + String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.'; + + @override + String get usbErrorConnectTimedOut => + 'Tiden har löpt ut medan vi väntade på att enheten skulle svara.'; + + @override + String get scanner_scanning => 'Söker efter enheter...'; @override String get scanner_connecting => 'Anslutning...'; @@ -150,11 +191,10 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get scanner_searchingDevices => 'Söker efter MeshCore-enheter...'; + String get scanner_searchingDevices => 'Söker efter MeshCore-enheter...'; @override - String get scanner_tapToScan => - 'Tryck Skanna för att hitta MeshCore-enheter'; + String get scanner_tapToScan => 'Tryck Skanna för att hitta MeshCore-enheter'; @override String scanner_connectionFailed(String error) { @@ -168,43 +208,43 @@ class AppLocalizationsSv extends AppLocalizations { String get scanner_scan => 'Skanna'; @override - String get scanner_bluetoothOff => 'Bluetooth är avstängt'; + String get scanner_bluetoothOff => 'Bluetooth är avstängt'; @override String get scanner_bluetoothOffMessage => - 'Vänligen aktivera Bluetooth för att söka efter enheter.'; + 'Vänligen aktivera Bluetooth för att söka efter enheter.'; @override - String get scanner_chromeRequired => 'Chrome-webbläsare krävs'; + String get scanner_chromeRequired => 'Chrome-webbläsare krävs'; @override String get scanner_chromeRequiredMessage => - 'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.'; + 'Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.'; @override String get scanner_enableBluetooth => 'Aktivera Bluetooth'; @override - String get device_quickSwitch => 'Snabb växling'; + String get device_quickSwitch => 'Snabb växling'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Inställningar'; + String get settings_title => 'Inställningar'; @override String get settings_deviceInfo => 'Enhetens information'; @override - String get settings_appSettings => 'Appinställningar'; + String get settings_appSettings => 'Appinställningar'; @override String get settings_appSettingsSubtitle => - 'Meddelanden, notiser och kartinställningar'; + 'Meddelanden, notiser och kartinställningar'; @override - String get settings_nodeSettings => 'Nodinställningar'; + String get settings_nodeSettings => 'Nodinställningar'; @override String get settings_nodeName => 'Nodnamn'; @@ -219,7 +259,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_nodeNameUpdated => 'Namn uppdaterat'; @override - String get settings_radioSettings => 'Radioinställningar'; + String get settings_radioSettings => 'Radioinställningar'; @override String get settings_radioSettingsSubtitle => @@ -227,7 +267,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_radioSettingsUpdated => - 'Radioinställningarna har uppdaterats'; + 'Radioinställningarna har uppdaterats'; @override String get settings_location => 'Plats'; @@ -239,8 +279,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_locationUpdated => 'Plats uppdaterad'; @override - String get settings_locationBothRequired => - 'Ange bÃ¥de latitud och longitud.'; + String get settings_locationBothRequired => 'Ange både latitud och longitud.'; @override String get settings_locationInvalid => 'Ogiltig latitud eller longitud.'; @@ -250,45 +289,45 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_locationGPSEnableSubtitle => - 'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.'; + 'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.'; @override - String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)'; + String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)'; @override String get settings_locationIntervalInvalid => - 'Intervalet mÃ¥ste vara minst 60 sekunder och mindre än 86400 sekunder.'; + 'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.'; @override String get settings_latitude => 'Latitud'; @override - String get settings_longitude => 'Längdgrad'; + String get settings_longitude => 'Längdgrad'; @override - String get settings_privacyMode => 'Privatläge'; + String get settings_privacyMode => 'Privatläge'; @override - String get settings_privacyModeSubtitle => 'Dölj namn/plats i annonser'; + String get settings_privacyModeSubtitle => 'Dölj namn/plats i annonser'; @override String get settings_privacyModeToggle => - 'Aktivera privatläge för att dölja ditt namn och din plats i annonser.'; + 'Aktivera privatläge för att dölja ditt namn och din plats i annonser.'; @override - String get settings_privacyModeEnabled => 'Privatläget är aktiverat'; + String get settings_privacyModeEnabled => 'Privatläget är aktiverat'; @override - String get settings_privacyModeDisabled => 'Privatläge är avstängt'; + String get settings_privacyModeDisabled => 'Privatläge är avstängt'; @override - String get settings_actions => 'Ã…tgärder'; + String get settings_actions => 'Åtgärder'; @override String get settings_sendAdvertisement => 'Skicka Annons'; @override - String get settings_sendAdvertisementSubtitle => 'Sändning finns nu'; + String get settings_sendAdvertisementSubtitle => 'Sändning finns nu'; @override String get settings_advertisementSent => 'Annons skickad'; @@ -297,7 +336,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_syncTime => 'Synkroniseringstid'; @override - String get settings_syncTimeSubtitle => 'Ställ enheten till telefonens tid'; + String get settings_syncTimeSubtitle => 'Ställ enheten till telefonens tid'; @override String get settings_timeSynchronized => 'Tidssynkroniserat'; @@ -307,7 +346,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_refreshContactsSubtitle => - 'Ladda om kontaktlistan frÃ¥n enheten'; + 'Ladda om kontaktlistan från enheten'; @override String get settings_rebootDevice => 'Starta om enheten'; @@ -317,23 +356,23 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_rebootDeviceConfirm => - 'Är du säker pÃ¥ att du vill starta om enheten? Du kommer att bli avkopplad.'; + 'Är du säker på att du vill starta om enheten? Du kommer att bli avkopplad.'; @override - String get settings_debug => 'Felsök'; + String get settings_debug => 'Felsök'; @override - String get settings_bleDebugLog => 'BLE-felsökning'; + String get settings_bleDebugLog => 'BLE-felsökning'; @override - String get settings_bleDebugLogSubtitle => 'BLE-kommandon, svar och rÃ¥data'; + String get settings_bleDebugLogSubtitle => 'BLE-kommandon, svar och rådata'; @override - String get settings_appDebugLog => 'Appfelsökning'; + String get settings_appDebugLog => 'Appfelsökning'; @override String get settings_appDebugLogSubtitle => - 'Applikations felsökningsmeddelanden'; + 'Applikations felsökningsmeddelanden'; @override String get settings_about => 'Om'; @@ -344,15 +383,15 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get settings_aboutLegalese => '2024 MeshCore Öppen Källkodsprojekt'; + String get settings_aboutLegalese => '2024 MeshCore Öppen Källkodsprojekt'; @override String get settings_aboutDescription => - 'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.'; + 'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.'; @override String get settings_aboutOpenMeteoAttribution => - 'LOS-höjddata: Open-Meteo (CC BY 4.0)'; + 'LOS-höjddata: Open-Meteo (CC BY 4.0)'; @override String get settings_infoName => 'Namn'; @@ -367,7 +406,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_infoBattery => 'Batteri'; @override - String get settings_infoPublicKey => 'Allmänt nyckel'; + String get settings_infoPublicKey => 'Allmänt nyckel'; @override String get settings_infoContactsCount => 'Kontakterantal'; @@ -376,7 +415,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_infoChannelCount => 'Kanalantal'; @override - String get settings_presets => 'Fördefinierade inställningar'; + String get settings_presets => 'Fördefinierade inställningar'; @override String get settings_frequency => 'Frekvens (MHz)'; @@ -406,15 +445,15 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)'; @override - String get settings_clientRepeat => 'Upprepa utan elnät'; + String get settings_clientRepeat => 'Upprepa utan elnät'; @override String get settings_clientRepeatSubtitle => - 'LÃ¥t enheten repetera nätpaket för andra användare.'; + 'Låt enheten repetera nätpaket för andra användare.'; @override String get settings_clientRepeatFreqWarning => - 'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.'; + 'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.'; @override String settings_error(String message) { @@ -422,7 +461,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get appSettings_title => 'Appinställningar'; + String get appSettings_title => 'Appinställningar'; @override String get appSettings_appearance => 'Utseende'; @@ -437,10 +476,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_themeLight => 'Ljus'; @override - String get appSettings_themeDark => 'Mörk'; + String get appSettings_themeDark => 'Mörk'; @override - String get appSettings_language => 'SprÃ¥k'; + String get appSettings_language => 'Språk'; @override String get appSettings_languageSystem => 'Systemstandard'; @@ -449,10 +488,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -461,16 +500,16 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -479,10 +518,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override String get appSettings_languageRu => 'Ryska'; @@ -491,11 +530,11 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageUk => 'Ukrainska'; @override - String get appSettings_enableMessageTracing => 'Aktivera meddelandespÃ¥rning'; + String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning'; @override String get appSettings_enableMessageTracingSubtitle => - 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; + 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; @override String get appSettings_notifications => 'Meddelanden'; @@ -505,71 +544,71 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_enableNotificationsSubtitle => - 'Ta emot notiser för meddelanden och reklam'; + 'Ta emot notiser för meddelanden och reklam'; @override String get appSettings_notificationPermissionDenied => - 'TillÃ¥telse för notifikationer nekad'; + 'Tillåtelse för notifikationer nekad'; @override String get appSettings_notificationsEnabled => 'Notifikationer aktiverade'; @override - String get appSettings_notificationsDisabled => 'Meddelanden är avstängda'; + String get appSettings_notificationsDisabled => 'Meddelanden är avstängda'; @override String get appSettings_messageNotifications => 'Meddelandekrav'; @override String get appSettings_messageNotificationsSubtitle => - 'Visa notis när nya meddelanden tas emot'; + 'Visa notis när nya meddelanden tas emot'; @override String get appSettings_channelMessageNotifications => 'Kanalmeddelandena'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Visa notis när meddelanden i kanal mottas'; + 'Visa notis när meddelanden i kanal mottas'; @override String get appSettings_advertisementNotifications => 'Annonsmeddelanden'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Visa notis när nya noder upptäcks'; + 'Visa notis när nya noder upptäcks'; @override String get appSettings_messaging => 'Meddelanden'; @override - String get appSettings_clearPathOnMaxRetry => 'Rensa Vägen pÃ¥ Max Försök'; + String get appSettings_clearPathOnMaxRetry => 'Rensa Vägen på Max Försök'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Ã…terställ kontaktväg efter 5 misslyckade försök att skicka'; + 'Återställ kontaktväg efter 5 misslyckade försök att skicka'; @override String get appSettings_pathsWillBeCleared => - 'Sökvägar kommer att tömmas efter 5 misslyckade försök.'; + 'Sökvägar kommer att tömmas efter 5 misslyckade försök.'; @override String get appSettings_pathsWillNotBeCleared => - 'Sökvägar kommer inte att rensas automatiskt.'; + 'Sökvägar kommer inte att rensas automatiskt.'; @override - String get appSettings_autoRouteRotation => 'Automatisk Rutväxling'; + String get appSettings_autoRouteRotation => 'Automatisk Rutväxling'; @override String get appSettings_autoRouteRotationSubtitle => - 'Blixtra mellan bästa vägar och flödesläge'; + 'Blixtra mellan bästa vägar och flödesläge'; @override String get appSettings_autoRouteRotationEnabled => - 'Automatisk ruttrotation är aktiverad'; + 'Automatisk ruttrotation är aktiverad'; @override String get appSettings_autoRouteRotationDisabled => - 'Automatisk ruttrotation är avstängd'; + 'Automatisk ruttrotation är avstängd'; @override String get appSettings_battery => 'Batteri'; @@ -579,18 +618,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Ställ in per enhet ($deviceName)'; + return 'Ställ in per enhet ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Anslut till en enhet för att välja'; + 'Anslut till en enhet för att välja'; @override String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; @override String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; @@ -599,24 +638,24 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_mapDisplay => 'Kartvisning'; @override - String get appSettings_showRepeaters => 'Visa Ã¥teruppslag'; + String get appSettings_showRepeaters => 'Visa återuppslag'; @override String get appSettings_showRepeatersSubtitle => - 'Visa Ã¥terspelsnoder pÃ¥ kartan'; + 'Visa återspelsnoder på kartan'; @override String get appSettings_showChatNodes => 'Visa Chattnoder'; @override - String get appSettings_showChatNodesSubtitle => 'Visa chattnoder pÃ¥ kartan'; + String get appSettings_showChatNodesSubtitle => 'Visa chattnoder på kartan'; @override String get appSettings_showOtherNodes => 'Visa andra noder'; @override String get appSettings_showOtherNodesSubtitle => - 'Visa andra nodtyper pÃ¥ kartan'; + 'Visa andra nodtyper på kartan'; @override String get appSettings_timeFilter => 'Tidsfilter'; @@ -626,7 +665,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String appSettings_timeFilterShowLast(int hours) { - return 'Visa noder frÃ¥n de senaste $hours timmarna'; + return 'Visa noder från de senaste $hours timmarna'; } @override @@ -634,7 +673,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_showNodesDiscoveredWithin => - 'Visa noder som upptäckts inom:'; + 'Visa noder som upptäckts inom:'; @override String get appSettings_allTime => 'Totalen'; @@ -649,7 +688,7 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_last24Hours => 'De senaste 24 timmarna'; @override - String get appSettings_lastWeek => 'Förra veckan'; + String get appSettings_lastWeek => 'Förra veckan'; @override String get appSettings_offlineMapCache => 'Offline Kartcache'; @@ -668,70 +707,70 @@ class AppLocalizationsSv extends AppLocalizations { @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'OmrÃ¥de markerat (zoom $minZoom-$maxZoom)'; + return 'Område markerat (zoom $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Felsök'; + String get appSettings_debugCard => 'Felsök'; @override - String get appSettings_appDebugLogging => 'App-felsökning och loggning'; + String get appSettings_appDebugLogging => 'App-felsökning och loggning'; @override String get appSettings_appDebugLoggingSubtitle => - 'Logga appens felsökningsmeddelanden för felsökning'; + 'Logga appens felsökningsmeddelanden för felsökning'; @override String get appSettings_appDebugLoggingEnabled => - 'App felsökning loggning aktiverad'; + 'App felsökning loggning aktiverad'; @override String get appSettings_appDebugLoggingDisabled => - 'App felsökning är avstängd'; + 'App felsökning är avstängd'; @override String get contacts_title => 'Kontakter'; @override - String get contacts_noContacts => 'Inga kontakter ännu'; + String get contacts_noContacts => 'Inga kontakter ännu'; @override String get contacts_contactsWillAppear => - 'Kontakter kommer att visas när enheter annonserar.'; + 'Kontakter kommer att visas när enheter annonserar.'; @override - String get contacts_unread => 'Oläst'; + String get contacts_unread => 'Oläst'; @override - String get contacts_searchContactsNoNumber => 'Sök kontakter...'; + String get contacts_searchContactsNoNumber => 'Sök kontakter...'; @override String contacts_searchContacts(int number, String str) { - return 'Sök kontakter...'; + return 'Sök kontakter...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Sök $number$str Favoriter...'; + return 'Sök $number$str Favoriter...'; } @override String contacts_searchUsers(int number, String str) { - return 'Sök $number$str användare...'; + return 'Sök $number$str användare...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Sök $number$str upprepningsenheter...'; + return 'Sök $number$str upprepningsenheter...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Sök $number$str Room-servrar...'; + return 'Sök $number$str Room-servrar...'; } @override - String get contacts_noUnreadContacts => 'Inga oinlästa kontakter'; + String get contacts_noUnreadContacts => 'Inga oinlästa kontakter'; @override String get contacts_noContactsFound => @@ -742,7 +781,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String contacts_removeConfirm(String contactName) { - return 'Ta bort $contactName frÃ¥n kontakter?'; + return 'Ta bort $contactName från kontakter?'; } @override @@ -755,7 +794,7 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_roomLogin => 'Rum Inloggning'; @override - String get contacts_openChat => 'Öppna Chatt'; + String get contacts_openChat => 'Öppna Chatt'; @override String get contacts_editGroup => 'Redigera Grupp'; @@ -775,7 +814,7 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_groupName => 'Gruppnamn'; @override - String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt'; + String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt'; @override String contacts_groupAlreadyExists(String name) { @@ -801,7 +840,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get contacts_lastSeenHourAgo => 'Senast sedd för 1 timme sedan'; + String get contacts_lastSeenHourAgo => 'Senast sedd för 1 timme sedan'; @override String contacts_lastSeenHoursAgo(int hours) { @@ -809,7 +848,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get contacts_lastSeenDayAgo => 'Senast sedd för 1 dag sedan'; + String get contacts_lastSeenDayAgo => 'Senast sedd för 1 dag sedan'; @override String contacts_lastSeenDaysAgo(int days) { @@ -823,10 +862,10 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_noChannelsConfigured => 'Inga kanaler konfigurerade'; @override - String get channels_addPublicChannel => 'Lägg till publik kanal'; + String get channels_addPublicChannel => 'Lägg till publik kanal'; @override - String get channels_searchChannels => 'Sök kanaler...'; + String get channels_searchChannels => 'Sök kanaler...'; @override String get channels_noChannelsFound => 'Inga kanaler hittades'; @@ -846,7 +885,7 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_private => 'Privat'; @override - String get channels_publicChannel => 'Allmänt kanal'; + String get channels_publicChannel => 'Allmänt kanal'; @override String get channels_privateChannel => 'Privat kanal'; @@ -858,14 +897,14 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_muteChannel => 'Tysta kanal'; @override - String get channels_unmuteChannel => 'SlÃ¥ pÃ¥ ljud för kanal'; + String get channels_unmuteChannel => 'Slå på ljud för kanal'; @override String get channels_deleteChannel => 'Ta bort kanal'; @override String channels_deleteChannelConfirm(String name) { - return 'Radera \"$name\"? Detta kan inte Ã¥ngras.'; + return 'Radera \"$name\"? Detta kan inte ångras.'; } @override @@ -879,7 +918,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channels_addChannel => 'Lägg till kanal'; + String get channels_addChannel => 'Lägg till kanal'; @override String get channels_channelIndexLabel => 'Kanalindex'; @@ -888,23 +927,22 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_channelName => 'Kanalnamn'; @override - String get channels_usePublicChannel => 'Använd Publikkanal'; + String get channels_usePublicChannel => 'Använd Publikkanal'; @override - String get channels_standardPublicPsk => 'Standard allmän PSK'; + String get channels_standardPublicPsk => 'Standard allmän PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => 'Generera slumpmässig PSK'; + String get channels_generateRandomPsk => 'Generera slumpmässig PSK'; @override String get channels_enterChannelName => 'Ange en kanalnamn'; @override - String get channels_pskMustBe32Hex => - 'PSK mÃ¥ste vara 32 hexadecimala tecken'; + String get channels_pskMustBe32Hex => 'PSK måste vara 32 hexadecimala tecken'; @override String channels_channelAdded(String name) { @@ -925,7 +963,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channels_publicChannelAdded => 'Allmänt kanal tillagd'; + String get channels_publicChannelAdded => 'Allmänt kanal tillagd'; @override String get channels_sortBy => 'Sortera efter'; @@ -940,7 +978,7 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_sortLatestMessages => 'Senaste meddelanden'; @override - String get channels_sortUnread => 'Oläst'; + String get channels_sortUnread => 'Oläst'; @override String get channels_createPrivateChannel => 'Skapa en privat kanal'; @@ -950,25 +988,25 @@ class AppLocalizationsSv extends AppLocalizations { 'Skyddat med en hemlig nyckel.'; @override - String get channels_joinPrivateChannel => 'GÃ¥ med i en Privat Kanal'; + String get channels_joinPrivateChannel => 'Gå med i en Privat Kanal'; @override String get channels_joinPrivateChannelDesc => 'Ange en hemlig nyckel manuellt.'; @override - String get channels_joinPublicChannel => 'GÃ¥ med i den Offentliga Kanalen'; + String get channels_joinPublicChannel => 'Gå med i den Offentliga Kanalen'; @override String get channels_joinPublicChannelDesc => - 'Vem som helst kan gÃ¥ med i denna kanal.'; + 'Vem som helst kan gå med i denna kanal.'; @override - String get channels_joinHashtagChannel => 'GÃ¥ med i en Hashtagkanal'; + String get channels_joinHashtagChannel => 'Gå med i en Hashtagkanal'; @override String get channels_joinHashtagChannelDesc => - 'Väldigt enkelt att gÃ¥ med i hashtag-kanaler.'; + 'Väldigt enkelt att gå med i hashtag-kanaler.'; @override String get channels_scanQrCode => 'Skanna en QR-kod'; @@ -983,11 +1021,11 @@ class AppLocalizationsSv extends AppLocalizations { String get channels_hashtagHint => 't.ex. #team'; @override - String get chat_noMessages => 'Inga meddelanden ännu'; + String get chat_noMessages => 'Inga meddelanden ännu'; @override String get chat_sendMessageToStart => - 'Skicka ett meddelande för att komma igÃ¥ng'; + 'Skicka ett meddelande för att komma igång'; @override String get chat_originalMessageNotFound => @@ -1016,7 +1054,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return 'Meddelandet är för lÃ¥ngt (max $maxBytes byte).'; + return 'Meddelandet är för långt (max $maxBytes byte).'; } @override @@ -1026,11 +1064,11 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_messageDeleted => 'Meddelandet raderat'; @override - String get chat_retryingMessage => 'Försöker igen'; + String get chat_retryingMessage => 'Försöker igen'; @override String chat_retryCount(int current, int max) { - return 'Försök igen $current/$max'; + return 'Försök igen $current/$max'; } @override @@ -1040,7 +1078,7 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_reply => 'Svara'; @override - String get chat_addReaction => 'Lägg till reaktion'; + String get chat_addReaction => 'Lägg till reaktion'; @override String get chat_me => 'Mig'; @@ -1052,16 +1090,16 @@ class AppLocalizationsSv extends AppLocalizations { String get emojiCategoryGestures => 'Gestikuleringar'; @override - String get emojiCategoryHearts => 'Hjärtan'; + String get emojiCategoryHearts => 'Hjärtan'; @override String get emojiCategoryObjects => 'Objekt'; @override - String get gifPicker_title => 'Välj en GIF'; + String get gifPicker_title => 'Välj en GIF'; @override - String get gifPicker_searchHint => 'Sök GIF:ar...'; + String get gifPicker_searchHint => 'Sök GIF:ar...'; @override String get gifPicker_poweredBy => 'Drivet av GIPHY'; @@ -1073,16 +1111,16 @@ class AppLocalizationsSv extends AppLocalizations { String get gifPicker_failedLoad => 'Kunde inte ladda GIF-filer'; @override - String get gifPicker_failedSearch => 'Sökningen misslyckades.'; + String get gifPicker_failedSearch => 'Sökningen misslyckades.'; @override String get gifPicker_noInternet => 'Ingen internetanslutning'; @override - String get debugLog_appTitle => 'Appfelsökning'; + String get debugLog_appTitle => 'Appfelsökning'; @override - String get debugLog_bleTitle => 'BLE-felsökning'; + String get debugLog_bleTitle => 'BLE-felsökning'; @override String get debugLog_copyLog => 'Kopiera logg'; @@ -1091,26 +1129,26 @@ class AppLocalizationsSv extends AppLocalizations { String get debugLog_clearLog => 'Rensa logg'; @override - String get debugLog_copied => 'Felsökningslogg kopierad'; + String get debugLog_copied => 'Felsökningslogg kopierad'; @override String get debugLog_bleCopied => 'BLE-logg kopierad'; @override - String get debugLog_noEntries => 'Inga felsökningsloggar ännu'; + String get debugLog_noEntries => 'Inga felsökningsloggar ännu'; @override String get debugLog_enableInSettings => - 'Aktivera appens felsökningsloggning i inställningarna'; + 'Aktivera appens felsökningsloggning i inställningarna'; @override String get debugLog_frames => 'Rammar'; @override - String get debugLog_rawLogRx => 'RÃ¥ Log-RX'; + String get debugLog_rawLogRx => 'Rå Log-RX'; @override - String get debugLog_noBleActivity => 'Ingen BLE-aktivitet ännu'; + String get debugLog_noBleActivity => 'Ingen BLE-aktivitet ännu'; @override String debugFrame_length(int count) { @@ -1123,16 +1161,16 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => 'Textmeddelandefält:'; + String get debugFrame_textMessageHeader => 'Textmeddelandefält:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '– Destination PubKey: $pubKey'; + return '– Destination PubKey: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Tidsstämpel: $timestamp'; + return '- Tidsstämpel: $timestamp'; } @override @@ -1163,24 +1201,24 @@ class AppLocalizationsSv extends AppLocalizations { String get chat_pathManagement => 'Stigarhantering'; @override - String get chat_ShowAllPaths => 'Visa alla vägar'; + String get chat_ShowAllPaths => 'Visa alla vägar'; @override - String get chat_routingMode => 'Ruttläge'; + String get chat_routingMode => 'Ruttläge'; @override - String get chat_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; + String get chat_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; @override - String get chat_forceFloodMode => 'Tvinga Översvämningsläge'; + String get chat_forceFloodMode => 'Tvinga Översvämningsläge'; @override String get chat_recentAckPaths => - 'Nyligen Ack-vägar (tryck för att använda):'; + 'Nyligen Ack-vägar (tryck för att använda):'; @override String get chat_pathHistoryFull => - 'Historisk sökväg är full. Ta bort poster för att lägga till nya.'; + 'Historisk sökväg är full. Ta bort poster för att lägga till nya.'; @override String get chat_hopSingular => 'hoppa'; @@ -1200,49 +1238,47 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get chat_successes => 'framgÃ¥ngar'; + String get chat_successes => 'framgångar'; @override - String get chat_removePath => 'Ta bort sökväg'; + String get chat_removePath => 'Ta bort sökväg'; @override String get chat_noPathHistoryYet => - 'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spÃ¥r.'; + 'Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.'; @override String get chat_pathActions => 'Stigar:'; @override - String get chat_setCustomPath => 'Ange anpassad sökväg'; + String get chat_setCustomPath => 'Ange anpassad sökväg'; @override - String get chat_setCustomPathSubtitle => 'Ange ruttväg manuellt'; + String get chat_setCustomPathSubtitle => 'Ange ruttväg manuellt'; @override - String get chat_clearPath => 'Rensa Vägen'; + String get chat_clearPath => 'Rensa Vägen'; @override - String get chat_clearPathSubtitle => - 'Tvinga fram omstart vid nästa sändning'; + String get chat_clearPathSubtitle => 'Tvinga fram omstart vid nästa sändning'; @override String get chat_pathCleared => - 'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.'; + 'Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.'; @override - String get chat_floodModeSubtitle => - 'Använd routningsomkopplaren i appraden'; + String get chat_floodModeSubtitle => 'Använd routningsomkopplaren i appraden'; @override String get chat_floodModeEnabled => - 'Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.'; + 'Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.'; @override - String get chat_fullPath => 'Fullständig sökväg'; + String get chat_fullPath => 'Fullständig sökväg'; @override String get chat_pathDetailsNotAvailable => - 'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.'; + 'Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1252,34 +1288,33 @@ class AppLocalizationsSv extends AppLocalizations { other: 'hoppar', one: 'hopp', ); - return 'Sökväg inställd: $hopCount $_temp0 - $status'; + return 'Sökväg inställd: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Sparat lokalt. Anslut för att synkronisera.'; + 'Sparat lokalt. Anslut för att synkronisera.'; @override - String get chat_pathDeviceConfirmed => 'Enheten bekräftad.'; + String get chat_pathDeviceConfirmed => 'Enheten bekräftad.'; @override - String get chat_pathDeviceNotConfirmed => - 'Enheten har inte bekräftats ännu.'; + String get chat_pathDeviceNotConfirmed => 'Enheten har inte bekräftats ännu.'; @override String get chat_type => 'Skriv'; @override - String get chat_path => 'Sökväg'; + String get chat_path => 'Sökväg'; @override - String get chat_publicKey => 'Allmänt nyckel'; + String get chat_publicKey => 'Allmänt nyckel'; @override - String get chat_compressOutgoingMessages => 'Kryptera utgÃ¥ende meddelanden'; + String get chat_compressOutgoingMessages => 'Kryptera utgående meddelanden'; @override - String get chat_floodForced => 'Översvämning (tvingad)'; + String get chat_floodForced => 'Översvämning (tvingad)'; @override String get chat_directForced => 'Direkt (tvingad)'; @@ -1290,7 +1325,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get chat_floodAuto => 'Översvämning (auto)'; + String get chat_floodAuto => 'Översvämning (auto)'; @override String get chat_direct => 'Direkt'; @@ -1300,26 +1335,26 @@ class AppLocalizationsSv extends AppLocalizations { @override String chat_unread(int count) { - return 'Olästa: $count'; + return 'Olästa: $count'; } @override - String get chat_openLink => 'Öppna länk?'; + String get chat_openLink => 'Öppna länk?'; @override String get chat_openLinkConfirmation => - 'Vill du öppna den här länken i din webbläsare?'; + 'Vill du öppna den här länken i din webbläsare?'; @override - String get chat_open => 'Öppna'; + String get chat_open => 'Öppna'; @override String chat_couldNotOpenLink(String url) { - return 'Kunde inte öppna länken: $url'; + return 'Kunde inte öppna länken: $url'; } @override - String get chat_invalidLink => 'Ogiltigt länkformat'; + String get chat_invalidLink => 'Ogiltigt länkformat'; @override String get map_title => 'Nodkarta'; @@ -1335,7 +1370,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_nodesNeedGps => - 'Noder mÃ¥ste dela sina GPS-koordinater\nför att visas pÃ¥ kartan'; + 'Noder måste dela sina GPS-koordinater\nför att visas på kartan'; @override String map_nodesCount(int count) { @@ -1351,7 +1386,7 @@ class AppLocalizationsSv extends AppLocalizations { String get map_chat => 'Chat'; @override - String get map_repeater => 'Ã…teruppspelare'; + String get map_repeater => 'Återuppspelare'; @override String get map_room => 'Rum'; @@ -1360,35 +1395,35 @@ class AppLocalizationsSv extends AppLocalizations { String get map_sensor => 'Sensor'; @override - String get map_pinDm => 'LÃ¥s (DM)'; + String get map_pinDm => 'Lås (DM)'; @override - String get map_pinPrivate => 'LÃ¥s (Privat)'; + String get map_pinPrivate => 'Lås (Privat)'; @override - String get map_pinPublic => 'AnslÃ¥ (Offentligt)'; + String get map_pinPublic => 'Anslå (Offentligt)'; @override String get map_lastSeen => 'Senast sedd'; @override String get map_disconnectConfirm => - 'Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?'; + 'Är du säker på att du vill koppla från enheten?'; @override - String get map_from => 'FrÃ¥n'; + String get map_from => 'Från'; @override - String get map_source => 'Källa'; + String get map_source => 'Källa'; @override String get map_flags => 'Flaggor'; @override - String get map_shareMarkerHere => 'Dela markeringen här'; + String get map_shareMarkerHere => 'Dela markeringen här'; @override - String get map_pinLabel => 'Fästetikett'; + String get map_pinLabel => 'Fästetikett'; @override String get map_label => 'Etikett'; @@ -1403,19 +1438,19 @@ class AppLocalizationsSv extends AppLocalizations { String get map_sendToChannel => 'Skicka till kanal'; @override - String get map_noChannelsAvailable => 'Inga kanaler tillgängliga'; + String get map_noChannelsAvailable => 'Inga kanaler tillgängliga'; @override String get map_publicLocationShare => 'Dela offentlig plats'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Du hÃ¥ller pÃ¥ att dela en plats i $channelLabel. Denna kanal är offentlig och alla med PSK kan se den.'; + return 'Du håller på att dela en plats i $channelLabel. Denna kanal är offentlig och alla med PSK kan se den.'; } @override String get map_connectToShareMarkers => - 'Anslut till en enhet för att dela markörer'; + 'Anslut till en enhet för att dela markörer'; @override String get map_filterNodes => 'Filtrera noder'; @@ -1439,13 +1474,13 @@ class AppLocalizationsSv extends AppLocalizations { String get map_filterByKeyPrefix => 'Filtrera efter nyckelprefix'; @override - String get map_publicKeyPrefix => 'Allmänt nyckelprästegenskap'; + String get map_publicKeyPrefix => 'Allmänt nyckelprästegenskap'; @override - String get map_markers => 'Markörer'; + String get map_markers => 'Markörer'; @override - String get map_showSharedMarkers => 'Visa delade markörer'; + String get map_showSharedMarkers => 'Visa delade markörer'; @override String get map_lastSeenTime => 'Senaste Visats Tid'; @@ -1454,40 +1489,39 @@ class AppLocalizationsSv extends AppLocalizations { String get map_sharedPin => 'Delad PIN'; @override - String get map_joinRoom => 'GÃ¥ med i rum'; + String get map_joinRoom => 'Gå med i rum'; @override String get map_manageRepeater => 'Hantera Upprepare'; @override - String get map_tapToAdd => - 'Tryck pÃ¥ noder för att lägga till dem i banan.'; + String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.'; @override - String get map_runTrace => 'Kör spÃ¥rsökning'; + String get map_runTrace => 'Kör spårsökning'; @override String get map_removeLast => 'Ta bort sista'; @override - String get map_pathTraceCancelled => 'SökvägsspÃ¥rning avbruten.'; + String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.'; @override String get mapCache_title => 'Offline Kartcache'; @override - String get mapCache_selectAreaFirst => 'Välj ett omrÃ¥de att cachera först'; + String get mapCache_selectAreaFirst => 'Välj ett område att cachera först'; @override String get mapCache_noTilesToDownload => - 'Inga kuber att ladda ner för detta omrÃ¥de'; + 'Inga kuber att ladda ner för detta område'; @override String get mapCache_downloadTilesTitle => 'Ladda ner klick'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Ladda ner $count kuber för offlineanvändning?'; + return 'Ladda ner $count kuber för offlineanvändning?'; } @override @@ -1516,13 +1550,13 @@ class AppLocalizationsSv extends AppLocalizations { String get mapCache_noAreaSelected => 'Ingen area markerad'; @override - String get mapCache_cacheArea => 'CacheomrÃ¥de'; + String get mapCache_cacheArea => 'Cacheområde'; @override - String get mapCache_useCurrentView => 'Använd Aktuell Visning'; + String get mapCache_useCurrentView => 'Använd Aktuell Visning'; @override - String get mapCache_zoomRange => 'Zoombegränsning'; + String get mapCache_zoomRange => 'Zoombegränsning'; @override String mapCache_estimatedTiles(int count) { @@ -1592,10 +1626,10 @@ class AppLocalizationsSv extends AppLocalizations { String get time_weeks => 'veckor'; @override - String get time_month => 'mÃ¥nad'; + String get time_month => 'månad'; @override - String get time_months => 'mÃ¥nader'; + String get time_months => 'månader'; @override String get time_minutes => 'minuter'; @@ -1604,60 +1638,60 @@ class AppLocalizationsSv extends AppLocalizations { String get time_allTime => 'Alla tider'; @override - String get dialog_disconnect => 'Koppla frÃ¥n'; + String get dialog_disconnect => 'Koppla från'; @override String get dialog_disconnectConfirm => - 'Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?'; + 'Är du säker på att du vill koppla från enheten?'; @override - String get login_repeaterLogin => 'Ã…teruppta Inloggning'; + String get login_repeaterLogin => 'Återuppta Inloggning'; @override String get login_roomLogin => 'Rum Inloggning'; @override - String get login_password => 'Lösenord'; + String get login_password => 'Lösenord'; @override - String get login_enterPassword => 'Ange lösenord'; + String get login_enterPassword => 'Ange lösenord'; @override - String get login_savePassword => 'Spara lösenord'; + String get login_savePassword => 'Spara lösenord'; @override String get login_savePasswordSubtitle => - 'Lösenord kommer att lagras säkert pÃ¥ enheten.'; + 'Lösenord kommer att lagras säkert på enheten.'; @override String get login_repeaterDescription => - 'Ange Ã¥teruppspelarens lösenord för att komma Ã¥t inställningar och status.'; + 'Ange återuppspelarens lösenord för att komma åt inställningar och status.'; @override String get login_roomDescription => - 'Ange rummets lösenord för att komma Ã¥t inställningar och status.'; + 'Ange rummets lösenord för att komma åt inställningar och status.'; @override String get login_routing => 'Ruttning'; @override - String get login_routingMode => 'Ruttläge'; + String get login_routingMode => 'Ruttläge'; @override - String get login_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; + String get login_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; @override - String get login_forceFloodMode => 'Tvinga Översvämningsläge'; + String get login_forceFloodMode => 'Tvinga Översvämningsläge'; @override - String get login_managePaths => 'Hantera Sökvägar'; + String get login_managePaths => 'Hantera Sökvägar'; @override String get login_login => 'Logga in'; @override String login_attempt(int current, int max) { - return 'Försök $current/$max'; + return 'Försök $current/$max'; } @override @@ -1667,7 +1701,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get login_failedMessage => - 'Inloggning misslyckades. Antingen är lösenordet fel eller sÃ¥ gÃ¥r det inte att nÃ¥ repeatern.'; + 'Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.'; @override String get common_reload => 'Ladda om'; @@ -1677,7 +1711,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String path_currentPath(String path) { - return 'Nuvarande sökväg: $path'; + return 'Nuvarande sökväg: $path'; } @override @@ -1688,40 +1722,40 @@ class AppLocalizationsSv extends AppLocalizations { other: 'hops', one: 'hop', ); - return 'Använda $count $_temp0 sökväg'; + return 'Använda $count $_temp0 sökväg'; } @override - String get path_enterCustomPath => 'Ange anpassad sökväg'; + String get path_enterCustomPath => 'Ange anpassad sökväg'; @override - String get path_currentPathLabel => 'Nuvarande sökväg'; + String get path_currentPathLabel => 'Nuvarande sökväg'; @override String get path_hexPrefixInstructions => - 'Ange 2-tecknets hex-prefett för varje hopp, Ã¥tskilda med komma.'; + 'Ange 2-tecknets hex-prefett för varje hopp, åtskilda med komma.'; @override String get path_hexPrefixExample => - 'Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)'; + 'Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)'; @override String get path_labelHexPrefixes => 'Hexprefixer'; @override String get path_helperMaxHops => - 'Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)'; + 'Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)'; @override - String get path_selectFromContacts => 'Välj istället frÃ¥n kontakter:'; + String get path_selectFromContacts => 'Välj istället från kontakter:'; @override String get path_noRepeatersFound => - 'Inga Ã¥teruppspelare eller rumsservrar hittades.'; + 'Inga återuppspelare eller rumsservrar hittades.'; @override String get path_customPathsRequire => - 'Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.'; + 'Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.'; @override String path_invalidHexPrefixes(String prefixes) { @@ -1729,14 +1763,13 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get path_tooLong => - 'Sökvägen är för lÃ¥ng. Max 64 hopp tillÃ¥tna.'; + String get path_tooLong => 'Sökvägen är för lång. Max 64 hopp tillåtna.'; @override - String get path_setPath => 'Ange Sökväg'; + String get path_setPath => 'Ange Sökväg'; @override - String get repeater_management => 'Ã…teruppspelarens Hantering'; + String get repeater_management => 'Återuppspelarens Hantering'; @override String get room_management => 'Rumserverhantering'; @@ -1749,14 +1782,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_statusSubtitle => - 'Visa Ã¥terspolningsstatus, statistik och grannar'; + 'Visa återspolningsstatus, statistik och grannar'; @override String get repeater_telemetry => 'Telemetry'; @override String get repeater_telemetrySubtitle => - 'Visa telemetri för sensorer och systemstatistik'; + 'Visa telemetri för sensorer och systemstatistik'; @override String get repeater_cli => 'CLI'; @@ -1771,23 +1804,22 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.'; @override - String get repeater_settings => 'Inställningar'; + String get repeater_settings => 'Inställningar'; @override - String get repeater_settingsSubtitle => 'Konfigurera Ã¥terspolarparametrar'; + String get repeater_settingsSubtitle => 'Konfigurera återspolarparametrar'; @override - String get repeater_statusTitle => 'Ã…terspelsstatus'; + String get repeater_statusTitle => 'Återspelsstatus'; @override - String get repeater_routingMode => 'Ruttläge'; + String get repeater_routingMode => 'Ruttläge'; @override - String get repeater_autoUseSavedPath => - 'Automatisk (använd sparad sökväg)'; + String get repeater_autoUseSavedPath => 'Automatisk (använd sparad sökväg)'; @override - String get repeater_forceFloodMode => 'Tvinga Översvämningsläge'; + String get repeater_forceFloodMode => 'Tvinga Översvämningsläge'; @override String get repeater_pathManagement => 'Stigarhantering'; @@ -1797,11 +1829,11 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_statusRequestTimeout => - 'StatusförfrÃ¥gan gick inte att hämta.'; + 'Statusförfrågan gick inte att hämta.'; @override String repeater_errorLoadingStatus(String error) { - return 'Fel vid inläsning av status: $error'; + return 'Fel vid inläsning av status: $error'; } @override @@ -1814,13 +1846,13 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_clockAtLogin => 'Klocka (vid inloggning)'; @override - String get repeater_uptime => 'Tillgänglighet'; + String get repeater_uptime => 'Tillgänglighet'; @override - String get repeater_queueLength => 'Köans längd'; + String get repeater_queueLength => 'Köans längd'; @override - String get repeater_debugFlags => 'Felsökningsflaggor'; + String get repeater_debugFlags => 'Felsökningsflaggor'; @override String get repeater_radioStatistics => 'Radiostatistik'; @@ -1832,7 +1864,7 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_lastSnr => 'Sista SNR'; @override - String get repeater_noiseFloor => 'LjudnivÃ¥'; + String get repeater_noiseFloor => 'Ljudnivå'; @override String get repeater_txAirtime => 'TX Airtime'; @@ -1864,17 +1896,17 @@ class AppLocalizationsSv extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; + return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; + return 'Totalt: $total, Översvämning: $flood, Direkt: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Översvämning: $flood, Direkt: $direct'; + return 'Översvämning: $flood, Direkt: $direct'; } @override @@ -1883,32 +1915,31 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Ã…teruppspelarens Inställningar'; + String get repeater_settingsTitle => 'Återuppspelarens Inställningar'; @override - String get repeater_basicSettings => 'Grundinställningar'; + String get repeater_basicSettings => 'Grundinställningar'; @override String get repeater_repeaterName => 'Upprepare Namn'; @override - String get repeater_repeaterNameHelper => - 'Visa namn för denna Ã¥terupprepare'; + String get repeater_repeaterNameHelper => 'Visa namn för denna återupprepare'; @override - String get repeater_adminPassword => 'Adminlösenord'; + String get repeater_adminPassword => 'Adminlösenord'; @override - String get repeater_adminPasswordHelper => 'Fullständig Ã¥tkomstlösenord'; + String get repeater_adminPasswordHelper => 'Fullständig åtkomstlösenord'; @override - String get repeater_guestPassword => 'Gästlösenhet'; + String get repeater_guestPassword => 'Gästlösenhet'; @override - String get repeater_guestPasswordHelper => 'Läs-skyddspassord'; + String get repeater_guestPasswordHelper => 'Läs-skyddspassord'; @override - String get repeater_radioSettings => 'Radioinställningar'; + String get repeater_radioSettings => 'Radioinställningar'; @override String get repeater_frequencyMhz => 'Frekvens (MHz)'; @@ -1932,7 +1963,7 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_codingRate => 'Kodningsgrad'; @override - String get repeater_locationSettings => 'Platsinställningar'; + String get repeater_locationSettings => 'Platsinställningar'; @override String get repeater_latitude => 'Latitud'; @@ -1941,7 +1972,7 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_latitudeHelper => 'Decimalgrader (t.ex. 37.7749)'; @override - String get repeater_longitude => 'Längdgrad'; + String get repeater_longitude => 'Längdgrad'; @override String get repeater_longitudeHelper => 'Decimalgrader (t.ex. -122.4194)'; @@ -1950,27 +1981,27 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_features => 'Funktioner'; @override - String get repeater_packetForwarding => 'Paketväxling'; + String get repeater_packetForwarding => 'Paketväxling'; @override String get repeater_packetForwardingSubtitle => - 'Aktivera Ã¥teruppspelaren för att vidarebefordra paket'; + 'Aktivera återuppspelaren för att vidarebefordra paket'; @override - String get repeater_guestAccess => 'GästÃ¥tkomst'; + String get repeater_guestAccess => 'Gäståtkomst'; @override String get repeater_guestAccessSubtitle => - 'TillÃ¥t läsbehörigheter för gäster.'; + 'Tillåt läsbehörigheter för gäster.'; @override - String get repeater_privacyMode => 'Privatläge'; + String get repeater_privacyMode => 'Privatläge'; @override - String get repeater_privacyModeSubtitle => 'Dölj namn/plats i annonser'; + String get repeater_privacyModeSubtitle => 'Dölj namn/plats i annonser'; @override - String get repeater_advertisementSettings => 'Annonsinställningar'; + String get repeater_advertisementSettings => 'Annonsinställningar'; @override String get repeater_localAdvertInterval => 'Lokalt Annonsintervall'; @@ -1982,7 +2013,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_floodAdvertInterval => - 'Översvämnadsannonsens tidsintervall'; + 'Översvämnadsannonsens tidsintervall'; @override String repeater_floodAdvertIntervalHours(int hours) { @@ -1993,17 +2024,17 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_encryptedAdvertInterval => 'Krypterad Annonsintervall'; @override - String get repeater_dangerZone => 'FaraomrÃ¥de'; + String get repeater_dangerZone => 'Faraområde'; @override - String get repeater_rebootRepeater => 'Starta Ã…teruppspelaren'; + String get repeater_rebootRepeater => 'Starta Återuppspelaren'; @override String get repeater_rebootRepeaterSubtitle => 'Starta om repeternheten'; @override String get repeater_rebootRepeaterConfirm => - 'Är du säker pÃ¥ att du vill starta om denna repeater?'; + 'Är du säker på att du vill starta om denna repeater?'; @override String get repeater_regenerateIdentityKey => 'Generera Identitetsknyckel'; @@ -2014,22 +2045,22 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'Detta kommer att generera en ny identitet för Ã¥terspelaren. Fortsätta?'; + 'Detta kommer att generera en ny identitet för återspelaren. Fortsätta?'; @override String get repeater_eraseFileSystem => 'Radera Filsystem'; @override String get repeater_eraseFileSystemSubtitle => - 'Formatera Ã¥terspelsfilsystemet'; + 'Formatera återspelsfilsystemet'; @override String get repeater_eraseFileSystemConfirm => - 'VARNING: Detta kommer att radera all data pÃ¥ repeatern. Detta kan inte Ã¥ngras!'; + 'VARNING: Detta kommer att radera all data på repeatern. Detta kan inte ångras!'; @override String get repeater_eraseSerialOnly => - 'Rensa är endast tillgängligt via seriell konsol.'; + 'Rensa är endast tillgängligt via seriell konsol.'; @override String repeater_commandSent(String command) { @@ -2042,44 +2073,43 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_confirm => 'Bekräfta'; + String get repeater_confirm => 'Bekräfta'; @override String get repeater_settingsSaved => - 'Inställningarna sparades framgÃ¥ngsrikt.'; + 'Inställningarna sparades framgångsrikt.'; @override String repeater_errorSavingSettings(String error) { - return 'Fel vid sparande av inställningar: $error'; + return 'Fel vid sparande av inställningar: $error'; } @override String get repeater_refreshBasicSettings => - 'Ã…terställ Grundläggande Inställningar'; + 'Återställ Grundläggande Inställningar'; @override - String get repeater_refreshRadioSettings => - 'Ã…terställ Radiosinställningar'; + String get repeater_refreshRadioSettings => 'Återställ Radiosinställningar'; @override - String get repeater_refreshTxPower => 'Ã…terställ TX-effekt'; + String get repeater_refreshTxPower => 'Återställ TX-effekt'; @override String get repeater_refreshLocationSettings => - 'Uppdatera Lokationsinställningar'; + 'Uppdatera Lokationsinställningar'; @override - String get repeater_refreshPacketForwarding => 'Ã…terställ Paketväxling'; + String get repeater_refreshPacketForwarding => 'Återställ Paketväxling'; @override - String get repeater_refreshGuestAccess => 'Ã…terställ GästÃ¥tkomst'; + String get repeater_refreshGuestAccess => 'Återställ Gäståtkomst'; @override - String get repeater_refreshPrivacyMode => 'Ã…terställ Sekretessläge'; + String get repeater_refreshPrivacyMode => 'Återställ Sekretessläge'; @override String get repeater_refreshAdvertisementSettings => - 'Ã…terställ Annonsinställningar'; + 'Återställ Annonsinställningar'; @override String repeater_refreshed(String label) { @@ -2092,23 +2122,23 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_cliTitle => 'Ã…teruppspelaren CLI'; + String get repeater_cliTitle => 'Återuppspelaren CLI'; @override - String get repeater_debugNextCommand => 'Felsök Nästa Kommando'; + String get repeater_debugNextCommand => 'Felsök Nästa Kommando'; @override - String get repeater_commandHelp => 'Hjälp'; + String get repeater_commandHelp => 'Hjälp'; @override String get repeater_clearHistory => 'Rensa Historik'; @override - String get repeater_noCommandsSent => 'Inga kommandon skickats ännu'; + String get repeater_noCommandsSent => 'Inga kommandon skickats ännu'; @override String get repeater_typeCommandOrUseQuick => - 'Skriv en kommando nedan eller använd snabba kommandon'; + 'Skriv en kommando nedan eller använd snabba kommandon'; @override String get repeater_enterCommandHint => 'Ange kommando...'; @@ -2117,13 +2147,13 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_previousCommand => 'Tidigare kommando'; @override - String get repeater_nextCommand => 'Nästa kommando'; + String get repeater_nextCommand => 'Nästa kommando'; @override - String get repeater_enterCommandFirst => 'Ange en kommando först'; + String get repeater_enterCommandFirst => 'Ange en kommando först'; @override - String get repeater_cliCommandFrameTitle => 'Kommandofönster'; + String get repeater_cliCommandFrameTitle => 'Kommandofönster'; @override String repeater_cliCommandError(String error) { @@ -2131,13 +2161,13 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get repeater_cliQuickGetName => 'Hämta namn'; + String get repeater_cliQuickGetName => 'Hämta namn'; @override - String get repeater_cliQuickGetRadio => 'FÃ¥ Radio'; + String get repeater_cliQuickGetRadio => 'Få Radio'; @override - String get repeater_cliQuickGetTx => 'Hämta TX'; + String get repeater_cliQuickGetTx => 'Hämta TX'; @override String get repeater_cliQuickNeighbors => 'Grannar'; @@ -2156,14 +2186,14 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpReboot => - 'Startar om enheten. (notera, du fÃ¥r kanske \'Timeout\' vilket är normalt)'; + 'Startar om enheten. (notera, du får kanske \'Timeout\' vilket är normalt)'; @override String get repeater_cliHelpClock => 'Visar aktuell tid per enhetens klocka.'; @override String get repeater_cliHelpPassword => - 'Ställer in ett nytt administratörslösenord för enheten.'; + 'Ställer in ett nytt administratörslösenord för enheten.'; @override String get repeater_cliHelpVersion => @@ -2171,34 +2201,34 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpClearStats => - 'Ã…terställer olika statistikräknare till noll.'; + 'Återställer olika statistikräknare till noll.'; @override - String get repeater_cliHelpSetAf => 'Ställer in lufttidsfaktor.'; + String get repeater_cliHelpSetAf => 'Ställer in lufttidsfaktor.'; @override String get repeater_cliHelpSetTx => - 'Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)'; + 'Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)'; @override String get repeater_cliHelpSetRepeat => - 'Aktiverar eller inaktiverar Ã¥teruppspelarens roll för denna nod.'; + 'Aktiverar eller inaktiverar återuppspelarens roll för denna nod.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Rumserver) Om \'pÃ¥\', sÃ¥ tillÃ¥ts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).'; + '(Rumserver) Om \'på\', så tillåts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).'; @override String get repeater_cliHelpSetFloodMax => - 'Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).'; + 'Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).'; @override String get repeater_cliHelpSetIntThresh => - 'Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den pÃ¥ 0 för att inaktivera detektion av kanalinterferens.'; + 'Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den på 0 för att inaktivera detektion av kanalinterferens.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Ställer in intervallet för att Ã¥terställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.'; + 'Ställer in intervallet för att återställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.'; @override String get repeater_cliHelpSetMultiAcks => @@ -2206,77 +2236,77 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.'; + 'Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in pÃ¥ 0 för att inaktivera.'; + 'Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in på 0 för att inaktivera.'; @override String get repeater_cliHelpSetGuestPassword => - 'Ställer in/uppdaterar gästlösenordet. (för Ã¥tervändare kan gästloggar skicka \"Get Stats\"-förfrÃ¥gan)'; + 'Ställer in/uppdaterar gästlösenordet. (för återvändare kan gästloggar skicka \"Get Stats\"-förfrågan)'; @override - String get repeater_cliHelpSetName => 'Ställer in annonstexterna namn.'; + String get repeater_cliHelpSetName => 'Ställer in annonstexterna namn.'; @override String get repeater_cliHelpSetLat => - 'Ställer in annonskartans latitud. (decimalgrader)'; + 'Ställer in annonskartans latitud. (decimalgrader)'; @override String get repeater_cliHelpSetLon => - 'Ställer in annonskartans longitud (decimalgrader).'; + 'Ställer in annonskartans longitud (decimalgrader).'; @override String get repeater_cliHelpSetRadio => - 'Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.'; + 'Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.'; @override String get repeater_cliHelpSetRxDelay => - 'Ställer (experimentell) basvärde (mÃ¥ste vara > 1 för effekt) för att applicera en liten fördröjning pÃ¥ mottagna paket, baserat pÃ¥ signalstyrka/poäng. Ställ in pÃ¥ 0 för att inaktivera.'; + 'Ställer (experimentell) basvärde (måste vara > 1 för effekt) för att applicera en liten fördröjning på mottagna paket, baserat på signalstyrka/poäng. Ställ in på 0 för att inaktivera.'; @override String get repeater_cliHelpSetTxDelay => - 'Ställer in en faktor som multipliceras med tid pÃ¥ luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).'; + 'Ställer in en faktor som multipliceras med tid på luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.'; + 'Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.'; @override String get repeater_cliHelpSetBridgeEnabled => 'Aktivera/Inaktivera brygga.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Ställ in fördröjning innan paket Ã¥ter sänder.'; + 'Ställ in fördröjning innan paket åter sänder.'; @override String get repeater_cliHelpSetBridgeSource => - 'Välj om bron ska Ã¥terända mottagna paket eller sända paket.'; + 'Välj om bron ska återända mottagna paket eller sända paket.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Ställ baudgränsen för rs232-bryggarna.'; + 'Ställ baudgränsen för rs232-bryggarna.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Ställ bro-hemlighet för espnow-broar.'; + 'Ställ bro-hemlighet för espnow-broar.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd pÃ¥ utvalda kort).'; + 'Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd på utvalda kort).'; @override String get repeater_cliHelpTempRadio => - 'Ställer temporära radioparametrar för det angivna antalet minuter, vilket Ã¥tergÃ¥r till de ursprungliga radioparametrarna efterÃ¥t. (sparar inte i inställningar).'; + 'Ställer temporära radioparametrar för det angivna antalet minuter, vilket återgår till de ursprungliga radioparametrarna efteråt. (sparar inte i inställningar).'; @override String get repeater_cliHelpSetPerm => - 'Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. TillstÃ¥ndsbiten varierar per firmware-roll, men de lÃ¥ga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).'; + 'Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. Tillståndsbiten varierar per firmware-roll, men de låga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).'; @override String get repeater_cliHelpGetBridgeType => - 'FÃ¥r brotyperna ingen, rs232, espnow'; + 'Får brotyperna ingen, rs232, espnow'; @override String get repeater_cliHelpLogStart => 'Starta paketloggning till filsystem.'; @@ -2286,50 +2316,50 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpLogErase => - 'Raderar pakets loggar frÃ¥n filsystemet.'; + 'Raderar pakets loggar från filsystemet.'; @override String get repeater_cliHelpNeighbors => - 'Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-gæ’®-4'; + 'Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-g撮-4'; @override String get repeater_cliHelpNeighborRemove => - 'Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) frÃ¥n grannlistan.'; + 'Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) från grannlistan.'; @override String get repeater_cliHelpRegion => - '(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.'; + '(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.'; @override String get repeater_cliHelpRegionLoad => - 'MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.'; + 'MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.'; @override String get repeater_cliHelpRegionGet => - 'Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) \'F\'\"'; + 'Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) \'F\'\"'; @override String get repeater_cliHelpRegionPut => - 'Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.'; + 'Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.'; @override String get repeater_cliHelpRegionRemove => - 'Tar bort en regionsdefinition med det angivna namnet. (mÃ¥ste matcha exakt och inte ha nÃ¥gra barnregioner)'; + 'Tar bort en regionsdefinition med det angivna namnet. (måste matcha exakt och inte ha några barnregioner)'; @override String get repeater_cliHelpRegionAllowf => - 'Ställer \'Flöde\'-behörighet för det angivna omrÃ¥det. (\'\' för det globala/gamla scopet)'; + 'Ställer \'Flöde\'-behörighet för det angivna området. (\'\' för det globala/gamla scopet)'; @override String get repeater_cliHelpRegionDenyf => - 'Tar bort \'F\'lood-behörigheten för det angivna omrÃ¥det. (OBS: rekommenderas inte att använda detta i detta skede pÃ¥ den globala/gamla omfattningen!!).'; + 'Tar bort \'F\'lood-behörigheten för det angivna området. (OBS: rekommenderas inte att använda detta i detta skede på den globala/gamla omfattningen!!).'; @override String get repeater_cliHelpRegionHome => - 'Svarar med den aktuella \'hem\'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).'; + 'Svarar med den aktuella \'hem\'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).'; @override - String get repeater_cliHelpRegionHomeSet => 'Ställer in \'hemregionen\'.'; + String get repeater_cliHelpRegionHomeSet => 'Ställer in \'hemregionen\'.'; @override String get repeater_cliHelpRegionSave => @@ -2337,40 +2367,40 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliHelpGps => - 'Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"pÃ¥\", status, fix, antal satelliter.'; + 'Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"på\", status, fix, antal satelliter.'; @override String get repeater_cliHelpGpsOnOff => - 'Aktiverar/inaktiverar GPS-strömsättningen.'; + 'Aktiverar/inaktiverar GPS-strömsättningen.'; @override String get repeater_cliHelpGpsSync => - 'Synkroniserar nätverks tid med GPS-klockan.'; + 'Synkroniserar nätverks tid med GPS-klockan.'; @override String get repeater_cliHelpGpsSetLoc => - 'Ställer nodens position till GPS-koordinater och sparar inställningar.'; + 'Ställer nodens position till GPS-koordinater och sparar inställningar.'; @override String get repeater_cliHelpGpsAdvert => - 'Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (frÃ¥n SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar'; + 'Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (från SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar'; @override String get repeater_cliHelpGpsAdvertSet => - 'Ställer in annonsplatskonfiguration.'; + 'Ställer in annonsplatskonfiguration.'; @override - String get repeater_commandsListTitle => 'Inställningslista'; + String get repeater_commandsListTitle => 'Inställningslista'; @override String get repeater_commandsListNote => - 'OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.'; + 'OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.'; @override - String get repeater_general => 'Allmänt'; + String get repeater_general => 'Allmänt'; @override - String get repeater_settingsCategory => 'Inställningar'; + String get repeater_settingsCategory => 'Inställningar'; @override String get repeater_bridge => 'Bro'; @@ -2379,28 +2409,28 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_logging => 'Logga'; @override - String get repeater_neighborsRepeaterOnly => 'Grannar (Endast Ã¥terspelare)'; + String get repeater_neighborsRepeaterOnly => 'Grannar (Endast återspelare)'; @override String get repeater_regionManagementRepeaterOnly => - 'Regionhantering (endast Ã¥teruppspelare)'; + 'Regionhantering (endast återuppspelare)'; @override String get repeater_regionNote => - 'Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.'; + 'Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.'; @override String get repeater_gpsManagement => 'GPS Hantering'; @override String get repeater_gpsNote => - 'GPS-kommando har introducerats för att hantera platsrelaterade ämnen.'; + 'GPS-kommando har introducerats för att hantera platsrelaterade ämnen.'; @override String get telemetry_receivedData => 'Mottagen Telemetridata'; @override - String get telemetry_requestTimeout => 'TelemetryförfrÃ¥gan gick ut.'; + String get telemetry_requestTimeout => 'Telemetryförfrågan gick ut.'; @override String telemetry_errorLoading(String error) { @@ -2408,7 +2438,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get telemetry_noData => 'Inga telemetridata tillgängliga.'; + String get telemetry_noData => 'Inga telemetridata tillgängliga.'; @override String telemetry_channelTitle(int channel) { @@ -2419,7 +2449,7 @@ class AppLocalizationsSv extends AppLocalizations { String get telemetry_batteryLabel => 'Batteri'; @override - String get telemetry_voltageLabel => 'Spänning'; + String get telemetry_voltageLabel => 'Spänning'; @override String get telemetry_mcuTemperatureLabel => 'MCU Temperatur'; @@ -2447,58 +2477,57 @@ class AppLocalizationsSv extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override String get neighbors_receivedData => 'Mottagna grannars data'; @override - String get neighbors_requestTimedOut => - 'Grannar begär tidsinställd utskick.'; + String get neighbors_requestTimedOut => 'Grannar begär tidsinställd utskick.'; @override String neighbors_errorLoading(String error) { - return 'Fel vid inläsning av grannar: $error'; + return 'Fel vid inläsning av grannar: $error'; } @override String get neighbors_repeatersNeighbors => 'Upprepar grannar'; @override - String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.'; + String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.'; @override String neighbors_unknownContact(String pubkey) { - return 'Okänd $pubkey'; + return 'Okänd $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Hördes: $time sedan'; + return 'Hördes: $time sedan'; } @override - String get channelPath_title => 'Paketväg'; + String get channelPath_title => 'Paketväg'; @override String get channelPath_viewMap => 'Visa karta'; @override - String get channelPath_otherObservedPaths => 'Övriga observerade stigar'; + String get channelPath_otherObservedPaths => 'Övriga observerade stigar'; @override - String get channelPath_repeaterHops => 'Ã…terupptagningssteg'; + String get channelPath_repeaterHops => 'Återupptagningssteg'; @override String get channelPath_noHopDetails => - 'Detaljer för denna paket är inte angivna.'; + 'Detaljer för denna paket är inte angivna.'; @override String get channelPath_messageDetails => 'Meddelandets detaljer'; @override - String get channelPath_senderLabel => 'Avsändare'; + String get channelPath_senderLabel => 'Avsändare'; @override String get channelPath_timeLabel => 'Tid'; @@ -2508,7 +2537,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String channelPath_pathLabel(int index) { - return 'Sökväg $index'; + return 'Sökväg $index'; } @override @@ -2516,7 +2545,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Observerad bana $index • $hops'; + return 'Observerad bana $index • $hops'; } @override @@ -2533,10 +2562,10 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Okänt'; + String get channelPath_unknownPath => 'Okänt'; @override - String get channelPath_floodPath => 'Översvämning'; + String get channelPath_floodPath => 'Översvämning'; @override String get channelPath_directPath => 'Direkt'; @@ -2552,34 +2581,34 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get channelPath_mapTitle => 'Sökvägskarta'; + String get channelPath_mapTitle => 'Sökvägskarta'; @override String get channelPath_noRepeaterLocations => - 'Inga Ã¥terupprepningsplatser finns tillgängliga för denna väg.'; + 'Inga återupprepningsplatser finns tillgängliga för denna väg.'; @override String channelPath_primaryPath(int index) { - return 'Sökväg $index (Primär)'; + return 'Sökväg $index (Primär)'; } @override - String get channelPath_pathLabelTitle => 'Sökväg'; + String get channelPath_pathLabelTitle => 'Sökväg'; @override - String get channelPath_observedPathHeader => 'Observerad Sökväg'; + String get channelPath_observedPathHeader => 'Observerad Sökväg'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Inga hoppdetaljer finns tillgängliga för detta paket.'; + 'Inga hoppdetaljer finns tillgängliga för detta paket.'; @override - String get channelPath_unknownRepeater => 'Okänt Upprepare'; + String get channelPath_unknownRepeater => 'Okänt Upprepare'; @override String get community_title => 'Gemenskap'; @@ -2592,14 +2621,14 @@ class AppLocalizationsSv extends AppLocalizations { 'Skapa en ny gemenskap och dela via QR-kod.'; @override - String get community_join => 'GÃ¥ med'; + String get community_join => 'Gå med'; @override - String get community_joinTitle => 'GÃ¥ med i gemenskapen'; + String get community_joinTitle => 'Gå med i gemenskapen'; @override String community_joinConfirmation(String name) { - return 'Vill du gÃ¥ med i communityn \"$name\"?'; + return 'Vill du gå med i communityn \"$name\"?'; } @override @@ -2613,7 +2642,7 @@ class AppLocalizationsSv extends AppLocalizations { String get community_showQr => 'Visa QR-kod'; @override - String get community_publicChannel => 'Föreningens Offentliga'; + String get community_publicChannel => 'Föreningens Offentliga'; @override String get community_hashtagChannel => 'Community Hashtag'; @@ -2639,58 +2668,58 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_qrInstructions(String name) { - return 'Skanna denna QR-kod för att gÃ¥ med i \"$name\"'; + return 'Skanna denna QR-kod för att gå med i \"$name\"'; } @override String get community_hashtagPrivacyHint => - 'Community-hashtagkanaler kan endast nÃ¥s av medlemmar i communityn'; + 'Community-hashtagkanaler kan endast nås av medlemmar i communityn'; @override String get community_invalidQrCode => 'Ogiltig community QR-kod'; @override - String get community_alreadyMember => 'Är redan medlem'; + String get community_alreadyMember => 'Är redan medlem'; @override String community_alreadyMemberMessage(String name) { - return 'Du är redan medlem av \"$name\".'; + return 'Du är redan medlem av \"$name\".'; } @override String get community_addPublicChannel => - 'Lägg till Gemenskapskanal (Offentlig)'; + 'Lägg till Gemenskapskanal (Offentlig)'; @override String get community_addPublicChannelHint => - 'Lägg automatiskt till den offentliga kanalen för denna community'; + 'Lägg automatiskt till den offentliga kanalen för denna community'; @override - String get community_noCommunities => 'Inga gemenskaper har anslutats ännu'; + String get community_noCommunities => 'Inga gemenskaper har anslutats ännu'; @override String get community_scanOrCreate => - 'Skanna en QR-kod eller skapa en community för att komma igÃ¥ng'; + 'Skanna en QR-kod eller skapa en community för att komma igång'; @override String get community_manageCommunities => 'Hantera Gemenskaper'; @override - String get community_delete => 'Lämna Gemenskap'; + String get community_delete => 'Lämna Gemenskap'; @override String community_deleteConfirm(String name) { - return 'Lämna \"$name\"?'; + return 'Lämna \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return 'Detta kommer ocksÃ¥ att radera $count kanal/kanaler och deras meddelanden.'; + return 'Detta kommer också att radera $count kanal/kanaler och deras meddelanden.'; } @override String community_deleted(String name) { - return 'Lämnade community \"$name\"'; + return 'Lämnade community \"$name\"'; } @override @@ -2698,7 +2727,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_regenerateSecretConfirm(String name) { - return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar mÃ¥ste scanna den nya QR-koden för att fortsätta kommunicera.'; + return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.'; } @override @@ -2706,7 +2735,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_secretRegenerated(String name) { - return 'Lösenord Ã¥terskapad för \"$name\"'; + return 'Lösenord återskapad för \"$name\"'; } @override @@ -2714,40 +2743,40 @@ class AppLocalizationsSv extends AppLocalizations { @override String community_secretUpdated(String name) { - return 'Hemlighet uppdaterad för \"$name\"'; + return 'Hemlighet uppdaterad för \"$name\"'; } @override String community_scanToUpdateSecret(String name) { - return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"'; + return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"'; } @override - String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag'; + String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag'; @override String get community_addHashtagChannelDesc => - 'Lägg till en hashtag-kanal för denna community'; + 'Lägg till en hashtag-kanal för denna community'; @override - String get community_selectCommunity => 'Välj Gemenskap'; + String get community_selectCommunity => 'Välj Gemenskap'; @override String get community_regularHashtag => 'Vanlig Hash Tag'; @override String get community_regularHashtagDesc => - 'Offentlig hashtag (alla kan gÃ¥ med)'; + 'Offentlig hashtag (alla kan gå med)'; @override String get community_communityHashtag => 'Community Hashtag'; @override - String get community_communityHashtagDesc => 'Endast för medlemmar'; + String get community_communityHashtagDesc => 'Endast för medlemmar'; @override String community_forCommunity(String name) { - return 'För $name'; + return 'För $name'; } @override @@ -2760,7 +2789,7 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_latestMessages => 'Senaste meddelanden'; @override - String get listFilter_heardRecently => 'Hörts nyligen'; + String get listFilter_heardRecently => 'Hörts nyligen'; @override String get listFilter_az => 'A-Z'; @@ -2775,13 +2804,13 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_favorites => 'Favoriter'; @override - String get listFilter_addToFavorites => 'Lägg till i favoriter'; + String get listFilter_addToFavorites => 'Lägg till i favoriter'; @override - String get listFilter_removeFromFavorites => 'Ta bort frÃ¥n favoriter'; + String get listFilter_removeFromFavorites => 'Ta bort från favoriter'; @override - String get listFilter_users => 'Användare'; + String get listFilter_users => 'Användare'; @override String get listFilter_repeaters => 'Upprepare'; @@ -2790,7 +2819,7 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_roomServers => 'Rumservrar'; @override - String get listFilter_unreadOnly => 'Endast oinlästa'; + String get listFilter_unreadOnly => 'Endast oinlästa'; @override String get listFilter_newGroup => 'Ny grupp'; @@ -2799,10 +2828,10 @@ class AppLocalizationsSv extends AppLocalizations { String get pathTrace_you => 'Du'; @override - String get pathTrace_failed => 'Sökvägsföljning misslyckades.'; + String get pathTrace_failed => 'Sökvägsföljning misslyckades.'; @override - String get pathTrace_notAvailable => 'Path trace ej tillgänglig.'; + String get pathTrace_notAvailable => 'Path trace ej tillgänglig.'; @override String get pathTrace_refreshTooltip => 'Uppdatera Path Trace'; @@ -2812,10 +2841,10 @@ class AppLocalizationsSv extends AppLocalizations { 'En eller flera av humlen saknar en plats!'; @override - String get pathTrace_clearTooltip => 'Rensa väg'; + String get pathTrace_clearTooltip => 'Rensa väg'; @override - String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.'; + String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.'; @override String losRunFailed(String error) { @@ -2826,20 +2855,20 @@ class AppLocalizationsSv extends AppLocalizations { String get losClearAllPoints => 'Rensa alla punkter'; @override - String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil'; + String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil'; @override String get losMenuTitle => 'LOS-menyn'; @override String get losMenuSubtitle => - 'Tryck pÃ¥ noder eller tryck länge pÃ¥ kartan för anpassade punkter'; + 'Tryck på noder eller tryck länge på kartan för anpassade punkter'; @override String get losShowDisplayNodes => 'Visa displaynoder'; @override - String get losCustomPoints => 'Anpassade poäng'; + String get losCustomPoints => 'Anpassade poäng'; @override String losCustomPointLabel(int index) { @@ -2863,10 +2892,10 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get losRun => 'Kör LOS'; + String get losRun => 'Kör LOS'; @override - String get losNoElevationData => 'Inga höjddata'; + String get losNoElevationData => 'Inga höjddata'; @override String losProfileClear( @@ -2896,19 +2925,19 @@ class AppLocalizationsSv extends AppLocalizations { @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd'; + return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd'; } @override String get losErrorElevationUnavailable => - 'Höjddata är inte tillgänglig för ett eller flera prover.'; + 'Höjddata är inte tillgänglig för ett eller flera prover.'; @override String get losErrorInvalidInput => - 'Ogiltiga poäng/höjddata för LOS-beräkning.'; + 'Ogiltiga poäng/höjddata för LOS-beräkning.'; @override - String get losRenameCustomPoint => 'Byt namn pÃ¥ anpassad punkt'; + String get losRenameCustomPoint => 'Byt namn på anpassad punkt'; @override String get losPointName => 'Punktnamn'; @@ -2917,10 +2946,10 @@ class AppLocalizationsSv extends AppLocalizations { String get losShowPanelTooltip => 'Visa LOS-panelen'; @override - String get losHidePanelTooltip => 'Dölj LOS-panelen'; + String get losHidePanelTooltip => 'Dölj LOS-panelen'; @override - String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; + String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; @override String get losLegendRadioHorizon => 'Radiohorisont'; @@ -2929,16 +2958,16 @@ class AppLocalizationsSv extends AppLocalizations { String get losLegendLosBeam => 'Siktlinje'; @override - String get losLegendTerrain => 'Terräng'; + String get losLegendTerrain => 'Terräng'; @override String get losFrequencyLabel => 'Frekvens'; @override - String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen'; + String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen'; @override - String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten'; + String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten'; @override String losFrequencyDialogDescription( @@ -2947,7 +2976,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Med start frÃ¥n k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; + return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; } @override @@ -2957,27 +2986,27 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => 'VägspÃ¥rning till repeater'; + String get contacts_repeaterPathTrace => 'Vägspårning till repeater'; @override String get contacts_repeaterPing => 'Ping-repeater'; @override - String get contacts_roomPathTrace => 'VägspÃ¥rning till rumserver'; + String get contacts_roomPathTrace => 'Vägspårning till rumserver'; @override String get contacts_roomPing => 'Ping rumsserver'; @override - String get contacts_chatTraceRoute => 'SpÃ¥ra rutt'; + String get contacts_chatTraceRoute => 'Spåra rutt'; @override String contacts_pathTraceTo(String name) { - return 'SpÃ¥ra rutt till $name'; + return 'Spåra rutt till $name'; } @override - String get contacts_clipboardEmpty => 'Urklipp är tomt.'; + String get contacts_clipboardEmpty => 'Urklipp är tomt.'; @override String get contacts_invalidAdvertFormat => 'Ogiltiga kontaktuppgifter'; @@ -2992,14 +3021,14 @@ class AppLocalizationsSv extends AppLocalizations { String get contacts_zeroHopAdvert => 'Reklam med nollhopp'; @override - String get contacts_floodAdvert => 'Översvämningsannons'; + String get contacts_floodAdvert => 'Översvämningsannons'; @override String get contacts_copyAdvertToClipboard => 'Kopiera annons till urklipp'; @override String get contacts_addContactFromClipboard => - 'Lägg till kontakt frÃ¥n urklipp'; + 'Lägg till kontakt från urklipp'; @override String get contacts_ShareContact => 'Kopiera kontakt till Urklipp'; @@ -3059,7 +3088,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String notification_newTypeDiscovered(String contactType) { - return 'Ny $contactType upptäckt'; + return 'Ny $contactType upptäckt'; } @override @@ -3074,11 +3103,11 @@ class AppLocalizationsSv extends AppLocalizations { 'Exporterar repeater / roomserver med plats till GPX-fil.'; @override - String get settings_gpxExportContacts => 'Exportera följeslagare till GPX'; + String get settings_gpxExportContacts => 'Exportera följeslagare till GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Exporterar följeslagare med en plats till GPX-fil.'; + 'Exporterar följeslagare med en plats till GPX-fil.'; @override String get settings_gpxExportAll => 'Exportera alla kontakter till GPX'; @@ -3088,40 +3117,39 @@ class AppLocalizationsSv extends AppLocalizations { 'Exporterar alla kontakter med en plats till GPX-fil.'; @override - String get settings_gpxExportSuccess => - 'Har exporterat GPX-fil med framgÃ¥ng'; + String get settings_gpxExportSuccess => 'Har exporterat GPX-fil med framgång'; @override String get settings_gpxExportNoContacts => 'Inga kontakter att exportera.'; @override String get settings_gpxExportNotAvailable => - 'Stöds inte pÃ¥ din enhet/operativsystem'; + 'Stöds inte på din enhet/operativsystem'; @override String get settings_gpxExportError => - 'Det uppstod ett fel när data exporterades.'; + 'Det uppstod ett fel när data exporterades.'; @override String get settings_gpxExportRepeatersRoom => 'Repeater- och rumsserverplatser'; @override - String get settings_gpxExportChat => 'Medhjälparplatser'; + String get settings_gpxExportChat => 'Medhjälparplatser'; @override String get settings_gpxExportAllContacts => 'Alla kontakters platser'; @override String get settings_gpxExportShareText => - 'Kartdata exporterad frÃ¥n meshcore-open'; + 'Kartdata exporterad från meshcore-open'; @override String get settings_gpxExportShareSubject => 'meshcore-open export av GPX-kartdata'; @override - String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer'; + String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer'; @override String get snrIndicator_lastSeen => 'Senast sedd'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1a70aa2..fe6ca55 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -12,92 +12,92 @@ class AppLocalizationsUk extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => 'Контакти'; + String get nav_contacts => 'Контакти'; @override - String get nav_channels => 'Канали'; + String get nav_channels => 'Канали'; @override - String get nav_map => 'Карта'; + String get nav_map => 'Карта'; @override - String get common_cancel => 'Скасувати'; + String get common_cancel => 'Скасувати'; @override - String get common_ok => 'ОК'; + String get common_ok => 'ОК'; @override - String get common_connect => 'Підключити'; + String get common_connect => 'Підключити'; @override - String get common_unknownDevice => 'Невідомий пристрій'; + String get common_unknownDevice => 'Невідомий пристрій'; @override - String get common_save => 'Зберегти'; + String get common_save => 'Зберегти'; @override - String get common_delete => 'Видалити'; + String get common_delete => 'Видалити'; @override - String get common_close => 'Закрити'; + String get common_close => 'Закрити'; @override - String get common_edit => 'Редагувати'; + String get common_edit => 'Редагувати'; @override - String get common_add => 'Додати'; + String get common_add => 'Додати'; @override - String get common_settings => 'Налаштування'; + String get common_settings => 'Налаштування'; @override - String get common_disconnect => 'Відключити'; + String get common_disconnect => 'Відключити'; @override - String get common_connected => 'Підключено'; + String get common_connected => 'Підключено'; @override - String get common_disconnected => 'Відключено'; + String get common_disconnected => 'Відключено'; @override - String get common_create => 'Створити'; + String get common_create => 'Створити'; @override - String get common_continue => 'Продовжити'; + String get common_continue => 'Продовжити'; @override - String get common_share => 'Поділитися'; + String get common_share => 'Поділитися'; @override - String get common_copy => 'Копіювати'; + String get common_copy => 'Копіювати'; @override - String get common_retry => 'Повторити'; + String get common_retry => 'Повторити'; @override - String get common_hide => 'Приховати'; + String get common_hide => 'Приховати'; @override - String get common_remove => 'Прибрати'; + String get common_remove => 'Прибрати'; @override - String get common_enable => 'Увімкнути'; + String get common_enable => 'Увімкнути'; @override - String get common_disable => 'Вимкнути'; + String get common_disable => 'Вимкнути'; @override - String get common_reboot => 'Перезавантажити'; + String get common_reboot => 'Перезавантажити'; @override - String get common_loading => 'Завантаження...'; + String get common_loading => 'Завантаження...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { - return '$volts Ð’'; + return '$volts В'; } @override @@ -115,245 +115,273 @@ class AppLocalizationsUk extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Підключити через USB'; + String get usbScreenTitle => 'Підключити через USB'; @override String get usbScreenSubtitle => - 'Виберіть виявлене серійне пристрій Ñ– підключіть його безпосередньо до вашого вузла MeshCore.'; + 'Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.'; @override - String get usbScreenStatus => 'Виберіть пристрій USB'; + String get usbScreenStatus => 'Виберіть пристрій USB'; @override String get usbScreenNote => - 'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.'; + 'USB-серіальний порт активний на підтримуваних пристроях на базі Android та на десктопних платформах.'; @override String get usbScreenEmptyState => - 'Не знайдено жодних пристроїв USB. Підключіть один Ñ– перезавантажте.'; + 'Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.'; @override - String get scanner_scanning => 'Пошук пристроїв...'; + String get usbErrorPermissionDenied => + 'Було відмовлено у наданні дозволу на використання USB.'; @override - String get scanner_connecting => 'Підключення...'; + String get usbErrorDeviceMissing => 'Вибране USB-пристрій більше недоступне.'; @override - String get scanner_disconnecting => 'Відключення...'; + String get usbErrorInvalidPort => 'Виберіть дійсний USB-пристрій.'; @override - String get scanner_notConnected => 'Не підключено'; + String get usbErrorBusy => + 'Ще один запит на підключення через USB вже обробляється.'; + + @override + String get usbErrorNotConnected => 'Немає підключених пристроїв USB.'; + + @override + String get usbErrorOpenFailed => 'Не вдалося відкрити вибране USB-пристрій.'; + + @override + String get usbErrorConnectFailed => + 'Не вдалося підключитися до вибраного USB-пристрою.'; + + @override + String get usbErrorUnsupported => + 'Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.'; + + @override + String get usbErrorAlreadyActive => 'USB-з\'єднання вже встановлено.'; + + @override + String get usbErrorNoDeviceSelected => + 'Не було вибрано жодного пристрою USB.'; + + @override + String get usbErrorPortClosed => 'З\'єднання USB не встановлено.'; + + @override + String get usbErrorConnectTimedOut => + 'Час очікування закінчився, оскільки пристрій не відповів.'; + + @override + String get scanner_scanning => 'Пошук пристроїв...'; + + @override + String get scanner_connecting => 'Підключення...'; + + @override + String get scanner_disconnecting => 'Відключення...'; + + @override + String get scanner_notConnected => 'Не підключено'; @override String scanner_connectedTo(String deviceName) { - return 'Підключено до $deviceName'; + return 'Підключено до $deviceName'; } @override - String get scanner_searchingDevices => - 'Пошук пристроїв MeshCore...'; + String get scanner_searchingDevices => 'Пошук пристроїв MeshCore...'; @override String get scanner_tapToScan => - 'Натисніть «Сканувати», щоб знайти пристрої MeshCore'; + 'Натисніть «Сканувати», щоб знайти пристрої MeshCore'; @override String scanner_connectionFailed(String error) { - return 'Помилка підключення: $error'; + return 'Помилка підключення: $error'; } @override - String get scanner_stop => 'Стоп'; + String get scanner_stop => 'Стоп'; @override - String get scanner_scan => 'Сканувати'; + String get scanner_scan => 'Сканувати'; @override - String get scanner_bluetoothOff => 'Bluetooth вимкнено'; + String get scanner_bluetoothOff => 'Bluetooth вимкнено'; @override String get scanner_bluetoothOffMessage => - 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; + 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; @override - String get scanner_chromeRequired => 'Потрібен браузер Chrome'; + String get scanner_chromeRequired => 'Потрібен браузер Chrome'; @override String get scanner_chromeRequiredMessage => - 'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.'; + 'Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.'; @override - String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; + String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; @override - String get device_quickSwitch => 'Швидке перемикання'; + String get device_quickSwitch => 'Швидке перемикання'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => 'Налаштування'; + String get settings_title => 'Налаштування'; @override - String get settings_deviceInfo => - 'Інформація про пристрій'; + String get settings_deviceInfo => 'Інформація про пристрій'; @override - String get settings_appSettings => - 'Налаштування програми'; + String get settings_appSettings => 'Налаштування програми'; @override String get settings_appSettingsSubtitle => - 'Сповіщення, повідомлення та налаштування карти'; + 'Сповіщення, повідомлення та налаштування карти'; @override - String get settings_nodeSettings => 'Налаштування вузла'; + String get settings_nodeSettings => 'Налаштування вузла'; @override - String get settings_nodeName => 'Ім\'я вузла'; + String get settings_nodeName => 'Ім\'я вузла'; @override - String get settings_nodeNameNotSet => 'Не встановлено'; + String get settings_nodeNameNotSet => 'Не встановлено'; @override - String get settings_nodeNameHint => 'Введіть ім\'я вузла'; + String get settings_nodeNameHint => 'Введіть ім\'я вузла'; @override - String get settings_nodeNameUpdated => 'Ім\'я оновлено'; + String get settings_nodeNameUpdated => 'Ім\'я оновлено'; @override - String get settings_radioSettings => 'Налаштування радіо'; + String get settings_radioSettings => 'Налаштування радіо'; @override String get settings_radioSettingsSubtitle => - 'Частота, потужність, коефіцієнт розширення'; + 'Частота, потужність, коефіцієнт розширення'; @override - String get settings_radioSettingsUpdated => - 'Налаштування радіо оновлено'; + String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено'; @override - String get settings_location => 'Розташування'; + String get settings_location => 'Розташування'; @override - String get settings_locationSubtitle => 'GPS координати'; + String get settings_locationSubtitle => 'GPS координати'; @override - String get settings_locationUpdated => - 'Розташування оновлено'; + String get settings_locationUpdated => 'Розташування оновлено'; @override - String get settings_locationBothRequired => - 'Введіть широту та довготу.'; + String get settings_locationBothRequired => 'Введіть широту та довготу.'; @override - String get settings_locationInvalid => - 'Некоректна широта або довгота.'; + String get settings_locationInvalid => 'Некоректна широта або довгота.'; @override - String get settings_locationGPSEnable => 'Увімкнути GPS'; + String get settings_locationGPSEnable => 'Увімкнути GPS'; @override String get settings_locationGPSEnableSubtitle => - 'Вмикає автоматичне оновлення місцезнаходження через GPS.'; + 'Вмикає автоматичне оновлення місцезнаходження через GPS.'; @override - String get settings_locationIntervalSec => - 'Інтервал для GPS (Секунди)'; + String get settings_locationIntervalSec => 'Інтервал для GPS (Секунди)'; @override String get settings_locationIntervalInvalid => - 'Інтервал має бути не менше 60 секунд Ñ– менше 86400 секунд.'; + 'Інтервал має бути не менше 60 секунд і менше 86400 секунд.'; @override - String get settings_latitude => 'Широта'; + String get settings_latitude => 'Широта'; @override - String get settings_longitude => 'Довгота'; + String get settings_longitude => 'Довгота'; @override - String get settings_privacyMode => 'Режим приватності'; + String get settings_privacyMode => 'Режим приватності'; @override String get settings_privacyModeSubtitle => - 'Приховати ім\'я/розташування в оголошеннях'; + 'Приховати ім\'я/розташування в оголошеннях'; @override String get settings_privacyModeToggle => - 'Увімкніть режим приватності, щоб приховати своє ім\'я та місцезнаходження в оголошеннях.'; + 'Увімкніть режим приватності, щоб приховати своє ім\'я та місцезнаходження в оголошеннях.'; @override - String get settings_privacyModeEnabled => - 'Режим приватності увімкнено'; + String get settings_privacyModeEnabled => 'Режим приватності увімкнено'; @override - String get settings_privacyModeDisabled => - 'Режим приватності вимкнено'; + String get settings_privacyModeDisabled => 'Режим приватності вимкнено'; @override - String get settings_actions => 'Дії'; + String get settings_actions => 'Дії'; @override - String get settings_sendAdvertisement => 'Оголосити себе'; + String get settings_sendAdvertisement => 'Оголосити себе'; @override String get settings_sendAdvertisementSubtitle => - 'Транслювати присутність зараз'; + 'Транслювати присутність зараз'; @override - String get settings_advertisementSent => - 'Оголошення надіслано'; + String get settings_advertisementSent => 'Оголошення надіслано'; @override - String get settings_syncTime => 'Синхронізація часу'; + String get settings_syncTime => 'Синхронізація часу'; @override String get settings_syncTimeSubtitle => - 'Встановити час пристрою відповідно до часу телефону.'; + 'Встановити час пристрою відповідно до часу телефону.'; @override - String get settings_timeSynchronized => 'Час синхронізовано'; + String get settings_timeSynchronized => 'Час синхронізовано'; @override - String get settings_refreshContacts => 'Оновити контакти'; + String get settings_refreshContacts => 'Оновити контакти'; @override String get settings_refreshContactsSubtitle => - 'Перезавантажити список контактів з пристрою'; + 'Перезавантажити список контактів з пристрою'; @override - String get settings_rebootDevice => - 'Перезавантажити пристрій'; + String get settings_rebootDevice => 'Перезавантажити пристрій'; @override String get settings_rebootDeviceSubtitle => - 'Перезавантажити пристрій MeshCore'; + 'Перезавантажити пристрій MeshCore'; @override String get settings_rebootDeviceConfirm => - 'Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.'; + 'Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.'; @override - String get settings_debug => 'Налагодження'; + String get settings_debug => 'Налагодження'; @override - String get settings_bleDebugLog => - 'Журнал налагодження BLE'; + String get settings_bleDebugLog => 'Журнал налагодження BLE'; @override String get settings_bleDebugLogSubtitle => - 'Команди BLE, відповіді та необроблені дані'; + 'Команди BLE, відповіді та необроблені дані'; @override - String get settings_appDebugLog => - 'Журнал налагодження програми'; + String get settings_appDebugLog => 'Журнал налагодження програми'; @override String get settings_appDebugLogSubtitle => - 'Повідомлення налагодження програми'; + 'Повідомлення налагодження програми'; @override - String get settings_about => 'Про програму'; + String get settings_about => 'Про програму'; @override String settings_aboutVersion(String version) { @@ -361,119 +389,115 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get settings_aboutLegalese => 'Проєкт MeshCore Open Source 2026'; + String get settings_aboutLegalese => 'Проєкт MeshCore Open Source 2026'; @override String get settings_aboutDescription => - 'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.'; + 'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.'; @override String get settings_aboutOpenMeteoAttribution => - 'Дані про висоту LOS: Open-Meteo (CC BY 4.0)'; + 'Дані про висоту LOS: Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => 'Ім\'я'; + String get settings_infoName => 'Ім\'я'; @override String get settings_infoId => 'ID'; @override - String get settings_infoStatus => 'Статус'; + String get settings_infoStatus => 'Статус'; @override - String get settings_infoBattery => 'Батарея'; + String get settings_infoBattery => 'Батарея'; @override - String get settings_infoPublicKey => 'Відкритий ключ'; + String get settings_infoPublicKey => 'Відкритий ключ'; @override - String get settings_infoContactsCount => - 'Кількість контактів'; + String get settings_infoContactsCount => 'Кількість контактів'; @override - String get settings_infoChannelCount => 'Кількість каналів'; + String get settings_infoChannelCount => 'Кількість каналів'; @override - String get settings_presets => 'Попередні налаштування'; + String get settings_presets => 'Попередні налаштування'; @override - String get settings_frequency => 'Частота (МГц)'; + String get settings_frequency => 'Частота (МГц)'; @override String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => - 'Некоректна частота (300-2500 МГц)'; + String get settings_frequencyInvalid => 'Некоректна частота (300-2500 МГц)'; @override - String get settings_bandwidth => 'Смуга пропускання'; + String get settings_bandwidth => 'Смуга пропускання'; @override - String get settings_spreadingFactor => - 'Коефіцієнт розширення'; + String get settings_spreadingFactor => 'Коефіцієнт розширення'; @override - String get settings_codingRate => 'Швидкість кодування'; + String get settings_codingRate => 'Швидкість кодування'; @override - String get settings_txPower => 'Потужність TX (дБм)'; + String get settings_txPower => 'Потужність TX (дБм)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => - 'Некоректна потужність TX (0-22 дБм)'; + String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)'; @override - String get settings_clientRepeat => 'Автономна система'; + String get settings_clientRepeat => 'Автономна система'; @override String get settings_clientRepeatSubtitle => - 'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.'; + 'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.'; @override String get settings_clientRepeatFreqWarning => - 'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.'; + 'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.'; @override String settings_error(String message) { - return 'Помилка: $message'; + return 'Помилка: $message'; } @override - String get appSettings_title => 'Налаштування програми'; + String get appSettings_title => 'Налаштування програми'; @override - String get appSettings_appearance => 'Вигляд'; + String get appSettings_appearance => 'Вигляд'; @override - String get appSettings_theme => 'Тема'; + String get appSettings_theme => 'Тема'; @override - String get appSettings_themeSystem => 'Системна'; + String get appSettings_themeSystem => 'Системна'; @override - String get appSettings_themeLight => 'Світла'; + String get appSettings_themeLight => 'Світла'; @override - String get appSettings_themeDark => 'Темна'; + String get appSettings_themeDark => 'Темна'; @override - String get appSettings_language => 'Мова'; + String get appSettings_language => 'Мова'; @override - String get appSettings_languageSystem => 'Як у системі'; + String get appSettings_languageSystem => 'Як у системі'; @override String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -482,16 +506,16 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -500,1144 +524,1076 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Російська'; + String get appSettings_languageRu => 'Російська'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Українська'; @override String get appSettings_enableMessageTracing => - 'Увімкнути відстеження повідомлень'; + 'Увімкнути відстеження повідомлень'; @override String get appSettings_enableMessageTracingSubtitle => - 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; + 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; @override - String get appSettings_notifications => 'Сповіщення'; + String get appSettings_notifications => 'Сповіщення'; @override - String get appSettings_enableNotifications => - 'Увімкнути сповіщення'; + String get appSettings_enableNotifications => 'Увімкнути сповіщення'; @override String get appSettings_enableNotificationsSubtitle => - 'Отримувати сповіщення про повідомлення та оголошення'; + 'Отримувати сповіщення про повідомлення та оголошення'; @override String get appSettings_notificationPermissionDenied => - 'У доступі до сповіщень відмовлено'; + 'У доступі до сповіщень відмовлено'; @override - String get appSettings_notificationsEnabled => - 'Сповіщення увімкнено'; + String get appSettings_notificationsEnabled => 'Сповіщення увімкнено'; @override - String get appSettings_notificationsDisabled => - 'Сповіщення вимкнено'; + String get appSettings_notificationsDisabled => 'Сповіщення вимкнено'; @override - String get appSettings_messageNotifications => - 'Сповіщення про повідомлення'; + String get appSettings_messageNotifications => 'Сповіщення про повідомлення'; @override String get appSettings_messageNotificationsSubtitle => - 'Показувати сповіщення при отриманні нових повідомлень'; + 'Показувати сповіщення при отриманні нових повідомлень'; @override - String get appSettings_channelMessageNotifications => - 'Сповіщення каналів'; + String get appSettings_channelMessageNotifications => 'Сповіщення каналів'; @override String get appSettings_channelMessageNotificationsSubtitle => - 'Показувати сповіщення при отриманні повідомлень каналу'; + 'Показувати сповіщення при отриманні повідомлень каналу'; @override String get appSettings_advertisementNotifications => - 'Сповіщення про оголошення'; + 'Сповіщення про оголошення'; @override String get appSettings_advertisementNotificationsSubtitle => - 'Показувати сповіщення при виявленні нових вузлів'; + 'Показувати сповіщення при виявленні нових вузлів'; @override - String get appSettings_messaging => 'Обмін повідомленнями'; + String get appSettings_messaging => 'Обмін повідомленнями'; @override String get appSettings_clearPathOnMaxRetry => - 'Очищати шлях після макс. спроб'; + 'Очищати шлях після макс. спроб'; @override String get appSettings_clearPathOnMaxRetrySubtitle => - 'Скидати шлях до контакту після 5 невдалих спроб надсилання'; + 'Скидати шлях до контакту після 5 невдалих спроб надсилання'; @override String get appSettings_pathsWillBeCleared => - 'Шляхи будуть очищені після 5 невдалих спроб.'; + 'Шляхи будуть очищені після 5 невдалих спроб.'; @override String get appSettings_pathsWillNotBeCleared => - 'Шляхи не будуть очищатися автоматично.'; + 'Шляхи не будуть очищатися автоматично.'; @override - String get appSettings_autoRouteRotation => - 'Авторотація маршруту'; + String get appSettings_autoRouteRotation => 'Авторотація маршруту'; @override String get appSettings_autoRouteRotationSubtitle => - 'Чергувати найкращі шляхи та режим «на всю мережу» (flood)'; + 'Чергувати найкращі шляхи та режим «на всю мережу» (flood)'; @override String get appSettings_autoRouteRotationEnabled => - 'Авторотація маршрутизації увімкнена'; + 'Авторотація маршрутизації увімкнена'; @override String get appSettings_autoRouteRotationDisabled => - 'Авторотація маршрутизації вимкнена'; + 'Авторотація маршрутизації вимкнена'; @override - String get appSettings_battery => 'Батарея'; + String get appSettings_battery => 'Батарея'; @override - String get appSettings_batteryChemistry => 'Хімія батареї'; + String get appSettings_batteryChemistry => 'Хімія батареї'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return 'Встановити для пристрою ($deviceName)'; + return 'Встановити для пристрою ($deviceName)'; } @override String get appSettings_batteryChemistryConnectFirst => - 'Підключіть пристрій, щоб вибрати'; + 'Підключіть пристрій, щоб вибрати'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2Ð’)'; + String get appSettings_batteryNmc => '18650 NMC (3.0-4.2В)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65Ð’)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65В)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2Ð’)'; + String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)'; @override - String get appSettings_mapDisplay => 'Відображення карти'; + String get appSettings_mapDisplay => 'Відображення карти'; @override - String get appSettings_showRepeaters => - 'Показувати ретранслятори'; + String get appSettings_showRepeaters => 'Показувати ретранслятори'; @override String get appSettings_showRepeatersSubtitle => - 'Відображати вузли-ретранслятори на карті'; + 'Відображати вузли-ретранслятори на карті'; @override - String get appSettings_showChatNodes => - 'Показувати вузли чату'; + String get appSettings_showChatNodes => 'Показувати вузли чату'; @override String get appSettings_showChatNodesSubtitle => - 'Відображати вузли чату на карті'; + 'Відображати вузли чату на карті'; @override - String get appSettings_showOtherNodes => - 'Показувати інші вузли'; + String get appSettings_showOtherNodes => 'Показувати інші вузли'; @override String get appSettings_showOtherNodesSubtitle => - 'Відображати інші типи вузлів на карті'; + 'Відображати інші типи вузлів на карті'; @override - String get appSettings_timeFilter => 'Фільтр часу'; + String get appSettings_timeFilter => 'Фільтр часу'; @override - String get appSettings_timeFilterShowAll => - 'Показати всі вузли'; + String get appSettings_timeFilterShowAll => 'Показати всі вузли'; @override String appSettings_timeFilterShowLast(int hours) { - return 'Показати вузли за останні $hours год'; + return 'Показати вузли за останні $hours год'; } @override - String get appSettings_mapTimeFilter => 'Фільтр часу карти'; + String get appSettings_mapTimeFilter => 'Фільтр часу карти'; @override String get appSettings_showNodesDiscoveredWithin => - 'Показувати вузли, виявлені за:'; + 'Показувати вузли, виявлені за:'; @override - String get appSettings_allTime => 'Весь час'; + String get appSettings_allTime => 'Весь час'; @override - String get appSettings_lastHour => 'Останню годину'; + String get appSettings_lastHour => 'Останню годину'; @override - String get appSettings_last6Hours => 'Останні 6 годин'; + String get appSettings_last6Hours => 'Останні 6 годин'; @override - String get appSettings_last24Hours => 'Останні 24 години'; + String get appSettings_last24Hours => 'Останні 24 години'; @override - String get appSettings_lastWeek => 'Минулий тиждень'; + String get appSettings_lastWeek => 'Минулий тиждень'; @override - String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; + String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; @override - String get appSettings_unitsTitle => 'одиниці'; + String get appSettings_unitsTitle => 'одиниці'; @override - String get appSettings_unitsMetric => 'Метричний (м / км)'; + String get appSettings_unitsMetric => 'Метричний (м / км)'; @override - String get appSettings_unitsImperial => 'Імперська (ft / mi)'; + String get appSettings_unitsImperial => 'Імперська (ft / mi)'; @override - String get appSettings_noAreaSelected => 'Область не вибрано'; + String get appSettings_noAreaSelected => 'Область не вибрано'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return 'Вибрана область (зум $minZoom-$maxZoom)'; + return 'Вибрана область (зум $minZoom-$maxZoom)'; } @override - String get appSettings_debugCard => 'Налагодження'; + String get appSettings_debugCard => 'Налагодження'; @override - String get appSettings_appDebugLogging => - 'Логування налагодження програми'; + String get appSettings_appDebugLogging => 'Логування налагодження програми'; @override String get appSettings_appDebugLoggingSubtitle => - 'Записувати повідомлення налагодження програми в лог для усунення несправностей.'; + 'Записувати повідомлення налагодження програми в лог для усунення несправностей.'; @override String get appSettings_appDebugLoggingEnabled => - 'Логування налагодження програми увімкнено'; + 'Логування налагодження програми увімкнено'; @override String get appSettings_appDebugLoggingDisabled => - 'Налагодження програми вимкнено.'; + 'Налагодження програми вимкнено.'; @override - String get contacts_title => 'Контакти'; + String get contacts_title => 'Контакти'; @override - String get contacts_noContacts => 'Контактів не знайдено.'; + String get contacts_noContacts => 'Контактів не знайдено.'; @override String get contacts_contactsWillAppear => - 'Контакти з\'являться, коли пристрої надішлють оголошення.'; + 'Контакти з\'являться, коли пристрої надішлють оголошення.'; @override - String get contacts_unread => 'Непрочитане'; + String get contacts_unread => 'Непрочитане'; @override - String get contacts_searchContactsNoNumber => - 'Пошук контактів...'; + String get contacts_searchContactsNoNumber => 'Пошук контактів...'; @override String contacts_searchContacts(int number, String str) { - return 'Пошук контактів...'; + return 'Пошук контактів...'; } @override String contacts_searchFavorites(int number, String str) { - return 'Пошук $number$str улюблених...'; + return 'Пошук $number$str улюблених...'; } @override String contacts_searchUsers(int number, String str) { - return 'Пошук $number$str користувачів...'; + return 'Пошук $number$str користувачів...'; } @override String contacts_searchRepeaters(int number, String str) { - return 'Пошук $number$str ретрансляторів...'; + return 'Пошук $number$str ретрансляторів...'; } @override String contacts_searchRoomServers(int number, String str) { - return 'Пошук $number$str серверів кімнат...'; + return 'Пошук $number$str серверів кімнат...'; } @override - String get contacts_noUnreadContacts => - 'Немає непрочитаних контактів'; + String get contacts_noUnreadContacts => 'Немає непрочитаних контактів'; @override - String get contacts_noContactsFound => - 'Контактів або груп не знайдено.'; + String get contacts_noContactsFound => 'Контактів або груп не знайдено.'; @override - String get contacts_deleteContact => 'Видалити контакт'; + String get contacts_deleteContact => 'Видалити контакт'; @override String contacts_removeConfirm(String contactName) { - return 'Видалити $contactName з контактів?'; + return 'Видалити $contactName з контактів?'; } @override - String get contacts_manageRepeater => - 'Керувати ретранслятором'; + String get contacts_manageRepeater => 'Керувати ретранслятором'; @override - String get contacts_manageRoom => - 'Керувати сервером кімнати'; + String get contacts_manageRoom => 'Керувати сервером кімнати'; @override - String get contacts_roomLogin => 'Вхід у кімнату'; + String get contacts_roomLogin => 'Вхід у кімнату'; @override - String get contacts_openChat => 'Відкрити чат'; + String get contacts_openChat => 'Відкрити чат'; @override - String get contacts_editGroup => 'Редагувати групу'; + String get contacts_editGroup => 'Редагувати групу'; @override - String get contacts_deleteGroup => 'Видалити групу'; + String get contacts_deleteGroup => 'Видалити групу'; @override String contacts_deleteGroupConfirm(String groupName) { - return 'Видалити $groupName?'; + return 'Видалити $groupName?'; } @override - String get contacts_newGroup => 'Нова група'; + String get contacts_newGroup => 'Нова група'; @override - String get contacts_groupName => 'Назва групи'; + String get contacts_groupName => 'Назва групи'; @override - String get contacts_groupNameRequired => - 'Назва групи обов\'язкова.'; + String get contacts_groupNameRequired => 'Назва групи обов\'язкова.'; @override String contacts_groupAlreadyExists(String name) { - return 'Група «$name» вже існує.'; + return 'Група «$name» вже існує.'; } @override - String get contacts_filterContacts => - 'Фільтрувати контакти...'; + String get contacts_filterContacts => 'Фільтрувати контакти...'; @override String get contacts_noContactsMatchFilter => - 'Жоден контакт не відповідає фільтру.'; + 'Жоден контакт не відповідає фільтру.'; @override - String get contacts_noMembers => 'Немає учасників'; + String get contacts_noMembers => 'Немає учасників'; @override - String get contacts_lastSeenNow => 'Ð’ мережі'; + String get contacts_lastSeenNow => 'В мережі'; @override String contacts_lastSeenMinsAgo(int minutes) { - return 'Ð’ мережі $minutes хв. тому'; + return 'В мережі $minutes хв. тому'; } @override - String get contacts_lastSeenHourAgo => - 'Ð’ мережі 1 годину тому'; + String get contacts_lastSeenHourAgo => 'В мережі 1 годину тому'; @override String contacts_lastSeenHoursAgo(int hours) { - return 'Ð’ мережі $hours год. тому'; + return 'В мережі $hours год. тому'; } @override - String get contacts_lastSeenDayAgo => 'Ð’ мережі 1 день тому'; + String get contacts_lastSeenDayAgo => 'В мережі 1 день тому'; @override String contacts_lastSeenDaysAgo(int days) { - return 'Ð’ мережі $days дн. тому'; + return 'В мережі $days дн. тому'; } @override - String get channels_title => 'Канали'; + String get channels_title => 'Канали'; @override - String get channels_noChannelsConfigured => - 'Канали не налаштовані'; + String get channels_noChannelsConfigured => 'Канали не налаштовані'; @override - String get channels_addPublicChannel => - 'Додати публічний канал'; + String get channels_addPublicChannel => 'Додати публічний канал'; @override - String get channels_searchChannels => 'Пошук каналів...'; + String get channels_searchChannels => 'Пошук каналів...'; @override - String get channels_noChannelsFound => 'Каналів не знайдено'; + String get channels_noChannelsFound => 'Каналів не знайдено'; @override String channels_channelIndex(int index) { - return 'Канал $index'; + return 'Канал $index'; } @override - String get channels_hashtagChannel => 'Канал з хештегом'; + String get channels_hashtagChannel => 'Канал з хештегом'; @override - String get channels_public => 'Публічний'; + String get channels_public => 'Публічний'; @override - String get channels_private => 'Приватний'; + String get channels_private => 'Приватний'; @override - String get channels_publicChannel => 'Публічний канал'; + String get channels_publicChannel => 'Публічний канал'; @override - String get channels_privateChannel => 'Приватний канал'; + String get channels_privateChannel => 'Приватний канал'; @override - String get channels_editChannel => 'Редагувати канал'; + String get channels_editChannel => 'Редагувати канал'; @override - String get channels_muteChannel => - 'Вимкнути сповіщення каналу'; + String get channels_muteChannel => 'Вимкнути сповіщення каналу'; @override - String get channels_unmuteChannel => - 'Увімкнути сповіщення каналу'; + String get channels_unmuteChannel => 'Увімкнути сповіщення каналу'; @override - String get channels_deleteChannel => 'Видалити канал'; + String get channels_deleteChannel => 'Видалити канал'; @override String channels_deleteChannelConfirm(String name) { - return 'Видалити $name? Це не можна скасувати.'; + return 'Видалити $name? Це не можна скасувати.'; } @override String channels_channelDeleteFailed(String name) { - return 'Не вдалося видалити канал \"$name\"'; + return 'Не вдалося видалити канал \"$name\"'; } @override String channels_channelDeleted(String name) { - return 'Канал «$name» видалено'; + return 'Канал «$name» видалено'; } @override - String get channels_addChannel => 'Додати канал'; + String get channels_addChannel => 'Додати канал'; @override - String get channels_channelIndexLabel => 'Індекс каналу'; + String get channels_channelIndexLabel => 'Індекс каналу'; @override - String get channels_channelName => 'Назва каналу'; + String get channels_channelName => 'Назва каналу'; @override - String get channels_usePublicChannel => - 'Використовувати публічний канал'; + String get channels_usePublicChannel => 'Використовувати публічний канал'; @override - String get channels_standardPublicPsk => - 'Стандартний публічний PSK'; + String get channels_standardPublicPsk => 'Стандартний публічний PSK'; @override String get channels_pskHex => 'PSK (Hex)'; @override - String get channels_generateRandomPsk => - 'Згенерувати випадковий ключ PSK'; + String get channels_generateRandomPsk => 'Згенерувати випадковий ключ PSK'; @override - String get channels_enterChannelName => - 'Будь ласка, введіть назву каналу'; + String get channels_enterChannelName => 'Будь ласка, введіть назву каналу'; @override String get channels_pskMustBe32Hex => - 'PSK має складатися з 32 шістнадцяткових символів.'; + 'PSK має складатися з 32 шістнадцяткових символів.'; @override String channels_channelAdded(String name) { - return 'Канал «$name» додано'; + return 'Канал «$name» додано'; } @override String channels_editChannelTitle(int index) { - return 'Редагувати канал $index'; + return 'Редагувати канал $index'; } @override - String get channels_smazCompression => 'Стиснення SMAZ'; + String get channels_smazCompression => 'Стиснення SMAZ'; @override String channels_channelUpdated(String name) { - return 'Канал «$name» оновлено'; + return 'Канал «$name» оновлено'; } @override - String get channels_publicChannelAdded => - 'Публічний канал додано'; + String get channels_publicChannelAdded => 'Публічний канал додано'; @override - String get channels_sortBy => 'Сортувати за'; + String get channels_sortBy => 'Сортувати за'; @override - String get channels_sortManual => 'Вручну'; + String get channels_sortManual => 'Вручну'; @override - String get channels_sortAZ => 'А-Я'; + String get channels_sortAZ => 'А-Я'; @override - String get channels_sortLatestMessages => - 'Останні повідомлення'; + String get channels_sortLatestMessages => 'Останні повідомлення'; @override - String get channels_sortUnread => 'Непрочитані'; + String get channels_sortUnread => 'Непрочитані'; @override - String get channels_createPrivateChannel => - 'Створити приватний канал'; + String get channels_createPrivateChannel => 'Створити приватний канал'; @override - String get channels_createPrivateChannelDesc => - 'Захищено секретним ключем.'; + String get channels_createPrivateChannelDesc => 'Захищено секретним ключем.'; @override - String get channels_joinPrivateChannel => - 'Приєднатися до приватного каналу'; + String get channels_joinPrivateChannel => 'Приєднатися до приватного каналу'; @override - String get channels_joinPrivateChannelDesc => - 'Ввести секретний ключ вручну.'; + String get channels_joinPrivateChannelDesc => 'Ввести секретний ключ вручну.'; @override - String get channels_joinPublicChannel => - 'Приєднатися до публічного каналу'; + String get channels_joinPublicChannel => 'Приєднатися до публічного каналу'; @override String get channels_joinPublicChannelDesc => - 'Будь-хто може приєднатися до цього каналу.'; + 'Будь-хто може приєднатися до цього каналу.'; @override - String get channels_joinHashtagChannel => - 'Приєднатися до каналу з хештегом'; + String get channels_joinHashtagChannel => 'Приєднатися до каналу з хештегом'; @override String get channels_joinHashtagChannelDesc => - 'Будь-хто може приєднатися до каналів #hashtag.'; + 'Будь-хто може приєднатися до каналів #hashtag.'; @override - String get channels_scanQrCode => 'Сканувати QR-код'; + String get channels_scanQrCode => 'Сканувати QR-код'; @override - String get channels_scanQrCodeComingSoon => 'Скоро буде'; + String get channels_scanQrCodeComingSoon => 'Скоро буде'; @override - String get channels_enterHashtag => 'Введіть хештег'; + String get channels_enterHashtag => 'Введіть хештег'; @override - String get channels_hashtagHint => 'напр. #команда'; + String get channels_hashtagHint => 'напр. #команда'; @override - String get chat_noMessages => 'Поки немає повідомлень.'; + String get chat_noMessages => 'Поки немає повідомлень.'; @override - String get chat_sendMessageToStart => - 'Надішліть повідомлення, щоб почати'; + String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати'; @override String get chat_originalMessageNotFound => - 'Оригінальне повідомлення не знайдено'; + 'Оригінальне повідомлення не знайдено'; @override String chat_replyingTo(String name) { - return 'Відповідь $name'; + return 'Відповідь $name'; } @override String chat_replyTo(String name) { - return 'Відповісти $name'; + return 'Відповісти $name'; } @override - String get chat_location => 'Розташування'; + String get chat_location => 'Розташування'; @override String chat_sendMessageTo(String contactName) { - return 'Надіслати повідомлення $contactName'; + return 'Надіслати повідомлення $contactName'; } @override - String get chat_typeMessage => 'Введіть повідомлення...'; + String get chat_typeMessage => 'Введіть повідомлення...'; @override String chat_messageTooLong(int maxBytes) { - return 'Повідомлення занадто довге (макс. $maxBytes байт).'; + return 'Повідомлення занадто довге (макс. $maxBytes байт).'; } @override - String get chat_messageCopied => - 'Повідомлення скопійовано'; + String get chat_messageCopied => 'Повідомлення скопійовано'; @override - String get chat_messageDeleted => 'Повідомлення видалено'; + String get chat_messageDeleted => 'Повідомлення видалено'; @override - String get chat_retryingMessage => 'Спроба відновлення.'; + String get chat_retryingMessage => 'Спроба відновлення.'; @override String chat_retryCount(int current, int max) { - return 'Повторна спроба $current/$max'; + return 'Повторна спроба $current/$max'; } @override - String get chat_sendGif => 'Надіслати GIF'; + String get chat_sendGif => 'Надіслати GIF'; @override - String get chat_reply => 'Відповісти'; + String get chat_reply => 'Відповісти'; @override - String get chat_addReaction => 'Додати реакцію'; + String get chat_addReaction => 'Додати реакцію'; @override - String get chat_me => 'Я'; + String get chat_me => 'Я'; @override - String get emojiCategorySmileys => 'Емодзі'; + String get emojiCategorySmileys => 'Емодзі'; @override - String get emojiCategoryGestures => 'Жести'; + String get emojiCategoryGestures => 'Жести'; @override - String get emojiCategoryHearts => 'Серця'; + String get emojiCategoryHearts => 'Серця'; @override - String get emojiCategoryObjects => 'Об\'єкти'; + String get emojiCategoryObjects => 'Об\'єкти'; @override - String get gifPicker_title => 'Вибрати GIF'; + String get gifPicker_title => 'Вибрати GIF'; @override - String get gifPicker_searchHint => 'Пошук GIF...'; + String get gifPicker_searchHint => 'Пошук GIF...'; @override - String get gifPicker_poweredBy => 'На базі GIPHY'; + String get gifPicker_poweredBy => 'На базі GIPHY'; @override - String get gifPicker_noGifsFound => 'GIF не знайдено'; + String get gifPicker_noGifsFound => 'GIF не знайдено'; @override - String get gifPicker_failedLoad => - 'Не вдалося завантажити GIF-файли'; + String get gifPicker_failedLoad => 'Не вдалося завантажити GIF-файли'; @override - String get gifPicker_failedSearch => 'Пошук GIF не вдався'; + String get gifPicker_failedSearch => 'Пошук GIF не вдався'; @override - String get gifPicker_noInternet => - 'Немає інтернет-з\'єднання'; + String get gifPicker_noInternet => 'Немає інтернет-з\'єднання'; @override - String get debugLog_appTitle => - 'Журнал налагодження програми'; + String get debugLog_appTitle => 'Журнал налагодження програми'; @override - String get debugLog_bleTitle => 'Журнал налагодження BLE'; + String get debugLog_bleTitle => 'Журнал налагодження BLE'; @override - String get debugLog_copyLog => 'Копіювати журнал'; + String get debugLog_copyLog => 'Копіювати журнал'; @override - String get debugLog_clearLog => 'Очистити журнал'; + String get debugLog_clearLog => 'Очистити журнал'; @override - String get debugLog_copied => - 'Журнал налагодження скопійовано'; + String get debugLog_copied => 'Журнал налагодження скопійовано'; @override - String get debugLog_bleCopied => 'Журнал BLE скопійовано'; + String get debugLog_bleCopied => 'Журнал BLE скопійовано'; @override String get debugLog_noEntries => - 'Поки що немає записів журналу налагодження.'; + 'Поки що немає записів журналу налагодження.'; @override String get debugLog_enableInSettings => - 'Увімкніть налагодження програми в налаштуваннях'; + 'Увімкніть налагодження програми в налаштуваннях'; @override - String get debugLog_frames => 'Кадри'; + String get debugLog_frames => 'Кадри'; @override - String get debugLog_rawLogRx => 'Необроблений лог - RX'; + String get debugLog_rawLogRx => 'Необроблений лог - RX'; @override - String get debugLog_noBleActivity => - 'Поки що немає активності BLE.'; + String get debugLog_noBleActivity => 'Поки що немає активності BLE.'; @override String debugFrame_length(int count) { - return 'Довжина кадру: $count байт'; + return 'Довжина кадру: $count байт'; } @override String debugFrame_command(String value) { - return 'Команда: 0x$value'; + return 'Команда: 0x$value'; } @override - String get debugFrame_textMessageHeader => 'Повідомлення:'; + String get debugFrame_textMessageHeader => 'Повідомлення:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- PubKey призначення: $pubKey'; + return '- PubKey призначення: $pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- Мітка часу: $timestamp'; + return '- Мітка часу: $timestamp'; } @override String debugFrame_flags(String value) { - return '- Прапорці: 0x$value'; + return '- Прапорці: 0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- Тип тексту: $type ($label)'; + return '- Тип тексту: $type ($label)'; } @override String get debugFrame_textTypeCli => 'CLI'; @override - String get debugFrame_textTypePlain => 'Звичайний'; + String get debugFrame_textTypePlain => 'Звичайний'; @override String debugFrame_text(String text) { - return '- Текст: \"$text\"'; + return '- Текст: \"$text\"'; } @override - String get debugFrame_hexDump => 'Дамп Hex:'; + String get debugFrame_hexDump => 'Дамп Hex:'; @override - String get chat_pathManagement => 'Керування шляхами'; + String get chat_pathManagement => 'Керування шляхами'; @override - String get chat_ShowAllPaths => 'Показати всі шляхи'; + String get chat_ShowAllPaths => 'Показати всі шляхи'; @override - String get chat_routingMode => 'Режим маршрутизації'; + String get chat_routingMode => 'Режим маршрутизації'; @override - String get chat_autoUseSavedPath => - 'Авто (використовувати збережений шлях)'; + String get chat_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; @override - String get chat_forceFloodMode => - 'Примусово на всю мережу'; + String get chat_forceFloodMode => 'Примусово на всю мережу'; @override String get chat_recentAckPaths => - 'Недавні шляхи ACK (натисніть, щоб використати):'; + 'Недавні шляхи ACK (натисніть, щоб використати):'; @override String get chat_pathHistoryFull => - 'Історія шляхів заповнена. Видаліть записи, щоб додати нові.'; + 'Історія шляхів заповнена. Видаліть записи, щоб додати нові.'; @override - String get chat_hopSingular => 'Стрибок'; + String get chat_hopSingular => 'Стрибок'; @override - String get chat_hopPlural => 'стрибків'; + String get chat_hopPlural => 'стрибків'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'стрибків', - many: 'стрибків', - few: 'стрибки', - one: 'стрибок', + other: 'стрибків', + many: 'стрибків', + few: 'стрибки', + one: 'стрибок', ); return '$count $_temp0'; } @override - String get chat_successes => 'Успішно'; + String get chat_successes => 'Успішно'; @override - String get chat_removePath => 'Видалити шлях'; + String get chat_removePath => 'Видалити шлях'; @override String get chat_noPathHistoryYet => - 'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.'; + 'Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.'; @override - String get chat_pathActions => 'Дії зі шляхом:'; + String get chat_pathActions => 'Дії зі шляхом:'; @override - String get chat_setCustomPath => - 'Встановити власний шлях'; + String get chat_setCustomPath => 'Встановити власний шлях'; @override - String get chat_setCustomPathSubtitle => - 'Вказати шлях маршрутизації вручну'; + String get chat_setCustomPathSubtitle => 'Вказати шлях маршрутизації вручну'; @override - String get chat_clearPath => 'Очистити шлях'; + String get chat_clearPath => 'Очистити шлях'; @override String get chat_clearPathSubtitle => - 'Примусово повторити пошук при наступному надсиланні'; + 'Примусово повторити пошук при наступному надсиланні'; @override String get chat_pathCleared => - 'Шлях очищено. Наступне повідомлення оновить маршрут.'; + 'Шлях очищено. Наступне повідомлення оновить маршрут.'; @override String get chat_floodModeSubtitle => - 'Використовувати перемикач маршрутизації в панелі програми'; + 'Використовувати перемикач маршрутизації в панелі програми'; @override String get chat_floodModeEnabled => - 'Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; + 'Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.'; @override - String get chat_fullPath => 'Повний шлях'; + String get chat_fullPath => 'Повний шлях'; @override String get chat_pathDetailsNotAvailable => - 'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.'; + 'Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.'; @override String chat_pathSetHops(int hopCount, String status) { String _temp0 = intl.Intl.pluralLogic( hopCount, locale: localeName, - other: 'стрибків', - many: 'стрибків', - few: 'стрибки', - one: 'стрибок', + other: 'стрибків', + many: 'стрибків', + few: 'стрибки', + one: 'стрибок', ); - return 'Шлях встановлено: $hopCount $_temp0 - $status'; + return 'Шлях встановлено: $hopCount $_temp0 - $status'; } @override String get chat_pathSavedLocally => - 'Збережено локально. Підключіться для синхронізації.'; + 'Збережено локально. Підключіться для синхронізації.'; @override - String get chat_pathDeviceConfirmed => - 'Пристрій підтверджено.'; + String get chat_pathDeviceConfirmed => 'Пристрій підтверджено.'; @override - String get chat_pathDeviceNotConfirmed => - 'Пристрій ще не підтверджено.'; + String get chat_pathDeviceNotConfirmed => 'Пристрій ще не підтверджено.'; @override - String get chat_type => 'Ввід'; + String get chat_type => 'Ввід'; @override - String get chat_path => 'Шлях'; + String get chat_path => 'Шлях'; @override - String get chat_publicKey => 'Відкритий ключ'; + String get chat_publicKey => 'Відкритий ключ'; @override - String get chat_compressOutgoingMessages => - 'Стискати вихідні повідомлення'; + String get chat_compressOutgoingMessages => 'Стискати вихідні повідомлення'; @override - String get chat_floodForced => - 'На всю мережу (примусово)'; + String get chat_floodForced => 'На всю мережу (примусово)'; @override - String get chat_directForced => 'Прямий (примусово)'; + String get chat_directForced => 'Прямий (примусово)'; @override String chat_hopsForced(int count) { - return '$count стрибків (примусово)'; + return '$count стрибків (примусово)'; } @override - String get chat_floodAuto => 'На всю мережу (авто)'; + String get chat_floodAuto => 'На всю мережу (авто)'; @override - String get chat_direct => 'Прямий'; + String get chat_direct => 'Прямий'; @override - String get chat_poiShared => - 'Точкою інтересу поділилися'; + String get chat_poiShared => 'Точкою інтересу поділилися'; @override String chat_unread(int count) { - return 'Непрочитано: $count'; + return 'Непрочитано: $count'; } @override - String get chat_openLink => 'Відкрити посилання?'; + String get chat_openLink => 'Відкрити посилання?'; @override String get chat_openLinkConfirmation => - 'Ви хочете відкрити це посилання у браузері?'; + 'Ви хочете відкрити це посилання у браузері?'; @override - String get chat_open => 'Відкрити'; + String get chat_open => 'Відкрити'; @override String chat_couldNotOpenLink(String url) { - return 'Не вдалося відкрити посилання: $url'; + return 'Не вдалося відкрити посилання: $url'; } @override - String get chat_invalidLink => - 'Невірний формат посилання'; + String get chat_invalidLink => 'Невірний формат посилання'; @override - String get map_title => 'Карта вузлів'; + String get map_title => 'Карта вузлів'; @override - String get map_lineOfSight => 'Пряма видимість'; + String get map_lineOfSight => 'Пряма видимість'; @override - String get map_losScreenTitle => 'Пряма видимість'; + String get map_losScreenTitle => 'Пряма видимість'; @override String get map_noNodesWithLocation => - 'Немає вузлів з даними про розташування'; + 'Немає вузлів з даними про розташування'; @override String get map_nodesNeedGps => - 'Вузли повинні надавати свої GPS координати,\nщоб з\'явитися на карті.'; + 'Вузли повинні надавати свої GPS координати,\nщоб з\'явитися на карті.'; @override String map_nodesCount(int count) { - return 'Вузли: $count'; + return 'Вузли: $count'; } @override String map_pinsCount(int count) { - return 'Мітки: $count'; + return 'Мітки: $count'; } @override - String get map_chat => 'Чат'; + String get map_chat => 'Чат'; @override - String get map_repeater => 'Ретранслятор'; + String get map_repeater => 'Ретранслятор'; @override - String get map_room => 'Кімната'; + String get map_room => 'Кімната'; @override - String get map_sensor => 'Сенсор'; + String get map_sensor => 'Сенсор'; @override - String get map_pinDm => 'Ключ (DM)'; + String get map_pinDm => 'Ключ (DM)'; @override - String get map_pinPrivate => 'Замок (Приватний)'; + String get map_pinPrivate => 'Замок (Приватний)'; @override - String get map_pinPublic => 'Ключ (Публічний)'; + String get map_pinPublic => 'Ключ (Публічний)'; @override - String get map_lastSeen => 'Останній раз бачили'; + String get map_lastSeen => 'Останній раз бачили'; @override String get map_disconnectConfirm => - 'Ви впевнені, що хочете відключитися від цього пристрою?'; + 'Ви впевнені, що хочете відключитися від цього пристрою?'; @override - String get map_from => 'Від'; + String get map_from => 'Від'; @override - String get map_source => 'Джерело'; + String get map_source => 'Джерело'; @override - String get map_flags => 'Прапорці'; + String get map_flags => 'Прапорці'; @override - String get map_shareMarkerHere => - 'Поділитися маркером тут'; + String get map_shareMarkerHere => 'Поділитися маркером тут'; @override - String get map_pinLabel => 'Мітка піна'; + String get map_pinLabel => 'Мітка піна'; @override - String get map_label => 'Мітка'; + String get map_label => 'Мітка'; @override - String get map_pointOfInterest => 'Точка інтересу'; + String get map_pointOfInterest => 'Точка інтересу'; @override - String get map_sendToContact => 'Надіслати контакту'; + String get map_sendToContact => 'Надіслати контакту'; @override - String get map_sendToChannel => 'Надіслати в канал'; + String get map_sendToChannel => 'Надіслати в канал'; @override - String get map_noChannelsAvailable => - 'Немає доступних каналів'; + String get map_noChannelsAvailable => 'Немає доступних каналів'; @override - String get map_publicLocationShare => - 'Поділитися в публічному місці'; + String get map_publicLocationShare => 'Поділитися в публічному місці'; @override String map_publicLocationShareConfirm(String channelLabel) { - return 'Ви збираєтеся поділитися розташуванням у $channelLabel. Цей канал публічний, Ñ– кожен, хто має ключ PSK, може це побачити.'; + return 'Ви збираєтеся поділитися розташуванням у $channelLabel. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.'; } @override String get map_connectToShareMarkers => - 'Підключіться до пристрою, щоб поділитися маркерами'; + 'Підключіться до пристрою, щоб поділитися маркерами'; @override - String get map_filterNodes => 'Фільтрувати вузли'; + String get map_filterNodes => 'Фільтрувати вузли'; @override - String get map_nodeTypes => 'Типи вузлів'; + String get map_nodeTypes => 'Типи вузлів'; @override - String get map_chatNodes => 'Вузли чату'; + String get map_chatNodes => 'Вузли чату'; @override - String get map_repeaters => 'Ретранслятори'; + String get map_repeaters => 'Ретранслятори'; @override - String get map_otherNodes => 'Інші вузли'; + String get map_otherNodes => 'Інші вузли'; @override - String get map_keyPrefix => 'Префікс ключа'; + String get map_keyPrefix => 'Префікс ключа'; @override - String get map_filterByKeyPrefix => - 'Фільтрувати за префіксом ключа'; + String get map_filterByKeyPrefix => 'Фільтрувати за префіксом ключа'; @override - String get map_publicKeyPrefix => - 'Префікс відкритого ключа'; + String get map_publicKeyPrefix => 'Префікс відкритого ключа'; @override - String get map_markers => 'Маркери'; + String get map_markers => 'Маркери'; @override - String get map_showSharedMarkers => - 'Показувати спільні маркери'; + String get map_showSharedMarkers => 'Показувати спільні маркери'; @override - String get map_lastSeenTime => - 'Час останньої активності'; + String get map_lastSeenTime => 'Час останньої активності'; @override - String get map_sharedPin => 'Спільний пін'; + String get map_sharedPin => 'Спільний пін'; @override - String get map_joinRoom => 'Приєднатися до кімнати'; + String get map_joinRoom => 'Приєднатися до кімнати'; @override - String get map_manageRepeater => - 'Керувати ретранслятором'; + String get map_manageRepeater => 'Керувати ретранслятором'; @override - String get map_tapToAdd => - 'Натисніть на вузли, щоб додати Ñ—Ñ… до шляху'; + String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху'; @override - String get map_runTrace => 'Виконати трасування шляху'; + String get map_runTrace => 'Виконати трасування шляху'; @override - String get map_removeLast => 'Видалити останній'; + String get map_removeLast => 'Видалити останній'; @override - String get map_pathTraceCancelled => - 'Відмінується трасування шляху'; + String get map_pathTraceCancelled => 'Відмінується трасування шляху'; @override - String get mapCache_title => 'Офлайн-кеш карти'; + String get mapCache_title => 'Офлайн-кеш карти'; @override String get mapCache_selectAreaFirst => - 'Спершу виберіть область для кешування'; + 'Спершу виберіть область для кешування'; @override String get mapCache_noTilesToDownload => - 'Немає плиток для завантаження в цій області.'; + 'Немає плиток для завантаження в цій області.'; @override - String get mapCache_downloadTilesTitle => - 'Завантажити плитки'; + String get mapCache_downloadTilesTitle => 'Завантажити плитки'; @override String mapCache_downloadTilesPrompt(int count) { - return 'Завантажити $count плиток для використання офлайн?'; + return 'Завантажити $count плиток для використання офлайн?'; } @override - String get mapCache_downloadAction => 'Завантажити'; + String get mapCache_downloadAction => 'Завантажити'; @override String mapCache_cachedTiles(int count) { - return 'Закешовано $count плиток'; + return 'Закешовано $count плиток'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return 'Плитки в кеші ($downloaded) ($failed помилок)'; + return 'Плитки в кеші ($downloaded) ($failed помилок)'; } @override - String get mapCache_clearOfflineCacheTitle => - 'Очистити офлайн-кеш'; + String get mapCache_clearOfflineCacheTitle => 'Очистити офлайн-кеш'; @override String get mapCache_clearOfflineCachePrompt => - 'Видалити всі закешовані плитки карти?'; + 'Видалити всі закешовані плитки карти?'; @override - String get mapCache_offlineCacheCleared => - 'Офлайн-кеш очищено.'; + String get mapCache_offlineCacheCleared => 'Офлайн-кеш очищено.'; @override - String get mapCache_noAreaSelected => 'Область не вибрано'; + String get mapCache_noAreaSelected => 'Область не вибрано'; @override - String get mapCache_cacheArea => 'Область кешування'; + String get mapCache_cacheArea => 'Область кешування'; @override - String get mapCache_useCurrentView => - 'Використати поточний вигляд'; + String get mapCache_useCurrentView => 'Використати поточний вигляд'; @override - String get mapCache_zoomRange => - 'Діапазон масштабування'; + String get mapCache_zoomRange => 'Діапазон масштабування'; @override String mapCache_estimatedTiles(int count) { - return 'Оцінка плиток: $count'; + return 'Оцінка плиток: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return 'Завантажено $completed / $total'; + return 'Завантажено $completed / $total'; } @override - String get mapCache_downloadTilesButton => - 'Завантажити плитки'; + String get mapCache_downloadTilesButton => 'Завантажити плитки'; @override - String get mapCache_clearCacheButton => 'Очистити кеш'; + String get mapCache_clearCacheButton => 'Очистити кеш'; @override String mapCache_failedDownloads(int count) { - return 'Невдалі завантаження: $count'; + return 'Невдалі завантаження: $count'; } @override @@ -1647,134 +1603,132 @@ class AppLocalizationsUk extends AppLocalizations { String east, String west, ) { - return 'Пн $north, Пд $south, Сх $east, Зх $west'; + return 'Пн $north, Пд $south, Сх $east, Зх $west'; } @override - String get time_justNow => 'Тільки що'; + String get time_justNow => 'Тільки що'; @override String time_minutesAgo(int minutes) { - return '$minutes хв. тому'; + return '$minutes хв. тому'; } @override String time_hoursAgo(int hours) { - return '$hours год. тому'; + return '$hours год. тому'; } @override String time_daysAgo(int days) { - return '$days дн. тому'; + return '$days дн. тому'; } @override - String get time_hour => 'година'; + String get time_hour => 'година'; @override - String get time_hours => 'годин'; + String get time_hours => 'годин'; @override - String get time_day => 'день'; + String get time_day => 'день'; @override - String get time_days => 'днів'; + String get time_days => 'днів'; @override - String get time_week => 'тиждень'; + String get time_week => 'тиждень'; @override - String get time_weeks => 'тижнів'; + String get time_weeks => 'тижнів'; @override - String get time_month => 'місяць'; + String get time_month => 'місяць'; @override - String get time_months => 'місяців'; + String get time_months => 'місяців'; @override - String get time_minutes => 'хвилин'; + String get time_minutes => 'хвилин'; @override - String get time_allTime => 'Весь час'; + String get time_allTime => 'Весь час'; @override - String get dialog_disconnect => 'Відключити'; + String get dialog_disconnect => 'Відключити'; @override String get dialog_disconnectConfirm => - 'Ви впевнені, що хочете відключитися від цього пристрою?'; + 'Ви впевнені, що хочете відключитися від цього пристрою?'; @override - String get login_repeaterLogin => 'Вхід у ретранслятор'; + String get login_repeaterLogin => 'Вхід у ретранслятор'; @override - String get login_roomLogin => 'Вхід у кімнату'; + String get login_roomLogin => 'Вхід у кімнату'; @override - String get login_password => 'Пароль'; + String get login_password => 'Пароль'; @override - String get login_enterPassword => 'Введіть пароль'; + String get login_enterPassword => 'Введіть пароль'; @override - String get login_savePassword => 'Зберегти пароль'; + String get login_savePassword => 'Зберегти пароль'; @override String get login_savePasswordSubtitle => - 'Пароль буде надійно збережено на цьому пристрої.'; + 'Пароль буде надійно збережено на цьому пристрої.'; @override String get login_repeaterDescription => - 'Введіть пароль ретранслятора для доступу до налаштувань та статусу.'; + 'Введіть пароль ретранслятора для доступу до налаштувань та статусу.'; @override String get login_roomDescription => - 'Введіть пароль кімнати для доступу до налаштувань та статусу.'; + 'Введіть пароль кімнати для доступу до налаштувань та статусу.'; @override - String get login_routing => 'Маршрутизація'; + String get login_routing => 'Маршрутизація'; @override - String get login_routingMode => 'Режим маршрутизації'; + String get login_routingMode => 'Режим маршрутизації'; @override - String get login_autoUseSavedPath => - 'Авто (використовувати збережений шлях)'; + String get login_autoUseSavedPath => 'Авто (використовувати збережений шлях)'; @override - String get login_forceFloodMode => - 'Примусово на всю мережу'; + String get login_forceFloodMode => 'Примусово на всю мережу'; @override - String get login_managePaths => 'Керувати шляхами'; + String get login_managePaths => 'Керувати шляхами'; @override - String get login_login => 'Вхід'; + String get login_login => 'Вхід'; @override String login_attempt(int current, int max) { - return 'Спроба $current/$max'; + return 'Спроба $current/$max'; } @override String login_failed(String error) { - return 'Вхід не вдався: $error'; + return 'Вхід не вдався: $error'; } @override String get login_failedMessage => - 'Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.'; + 'Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.'; @override - String get common_reload => 'Перезавантажити'; + String get common_reload => 'Перезавантажити'; @override - String get common_clear => 'Очистити'; + String get common_clear => 'Очистити'; @override String path_currentPath(String path) { - return 'Поточний шлях: $path'; + return 'Поточний шлях: $path'; } @override @@ -1782,182 +1736,174 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'стрибками', - many: 'стрибками', - few: 'стрибками', - one: 'стрибком', + other: 'стрибками', + many: 'стрибками', + few: 'стрибками', + one: 'стрибком', ); - return 'Використання шляху з $count $_temp0'; + return 'Використання шляху з $count $_temp0'; } @override - String get path_enterCustomPath => 'Ввести власний шлях'; + String get path_enterCustomPath => 'Ввести власний шлях'; @override - String get path_currentPathLabel => 'Поточний шлях'; + String get path_currentPathLabel => 'Поточний шлях'; @override String get path_hexPrefixInstructions => - 'Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.'; + 'Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.'; @override String get path_hexPrefixExample => - 'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).'; + 'Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).'; @override - String get path_labelHexPrefixes => 'Hex-префікси'; + String get path_labelHexPrefixes => 'Hex-префікси'; @override String get path_helperMaxHops => - 'Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)'; + 'Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)'; @override - String get path_selectFromContacts => 'Вибрати з контактів:'; + String get path_selectFromContacts => 'Вибрати з контактів:'; @override String get path_noRepeatersFound => - 'Ретрансляторів або серверів кімнат не знайдено.'; + 'Ретрансляторів або серверів кімнат не знайдено.'; @override String get path_customPathsRequire => - 'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.'; + 'Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.'; @override String path_invalidHexPrefixes(String prefixes) { - return 'Некоректні hex-префікси: $prefixes'; + return 'Некоректні hex-префікси: $prefixes'; } @override - String get path_tooLong => - 'Шлях занадто довгий. Максимум 64 стрибки.'; + String get path_tooLong => 'Шлях занадто довгий. Максимум 64 стрибки.'; @override - String get path_setPath => 'Встановити шлях'; + String get path_setPath => 'Встановити шлях'; @override - String get repeater_management => - 'Керування ретранслятором'; + String get repeater_management => 'Керування ретранслятором'; @override - String get room_management => - 'Адміністрування сервера кімнати'; + String get room_management => 'Адміністрування сервера кімнати'; @override - String get repeater_managementTools => - 'Інструменти керування'; + String get repeater_managementTools => 'Інструменти керування'; @override - String get repeater_status => 'Статус'; + String get repeater_status => 'Статус'; @override String get repeater_statusSubtitle => - 'Показати статус, статистику та сусідів ретранслятора'; + 'Показати статус, статистику та сусідів ретранслятора'; @override - String get repeater_telemetry => 'Телеметрія'; + String get repeater_telemetry => 'Телеметрія'; @override String get repeater_telemetrySubtitle => - 'Показати телеметрію сенсорів та статистику системи'; + 'Показати телеметрію сенсорів та статистику системи'; @override String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => - 'Надіслати команди ретранслятору'; + String get repeater_cliSubtitle => 'Надіслати команди ретранслятору'; @override - String get repeater_neighbors => 'Сусіди'; + String get repeater_neighbors => 'Сусіди'; @override String get repeater_neighborsSubtitle => - 'Показати сусідів нульового стрибка.'; + 'Показати сусідів нульового стрибка.'; @override - String get repeater_settings => 'Налаштування'; + String get repeater_settings => 'Налаштування'; @override - String get repeater_settingsSubtitle => - 'Налаштувати параметри ретранслятора'; + String get repeater_settingsSubtitle => 'Налаштувати параметри ретранслятора'; @override - String get repeater_statusTitle => 'Статус ретранслятора'; + String get repeater_statusTitle => 'Статус ретранслятора'; @override - String get repeater_routingMode => 'Режим маршрутизації'; + String get repeater_routingMode => 'Режим маршрутизації'; @override String get repeater_autoUseSavedPath => - 'Авто (використовувати збережений шлях)'; + 'Авто (використовувати збережений шлях)'; @override - String get repeater_forceFloodMode => - 'Примусово на всю мережу'; + String get repeater_forceFloodMode => 'Примусово на всю мережу'; @override - String get repeater_pathManagement => 'Керування шляхами'; + String get repeater_pathManagement => 'Керування шляхами'; @override - String get repeater_refresh => 'Оновити'; + String get repeater_refresh => 'Оновити'; @override String get repeater_statusRequestTimeout => - 'Час очікування запиту статусу вичерпано.'; + 'Час очікування запиту статусу вичерпано.'; @override String repeater_errorLoadingStatus(String error) { - return 'Помилка завантаження статусу: $error'; + return 'Помилка завантаження статусу: $error'; } @override - String get repeater_systemInformation => - 'Системна інформація'; + String get repeater_systemInformation => 'Системна інформація'; @override - String get repeater_battery => 'Батарея'; + String get repeater_battery => 'Батарея'; @override - String get repeater_clockAtLogin => 'Годинник (при вході)'; + String get repeater_clockAtLogin => 'Годинник (при вході)'; @override - String get repeater_uptime => 'Час роботи'; + String get repeater_uptime => 'Час роботи'; @override - String get repeater_queueLength => 'Довжина черги'; + String get repeater_queueLength => 'Довжина черги'; @override - String get repeater_debugFlags => 'Прапорці налагодження'; + String get repeater_debugFlags => 'Прапорці налагодження'; @override - String get repeater_radioStatistics => 'Статистика радіо'; + String get repeater_radioStatistics => 'Статистика радіо'; @override - String get repeater_lastRssi => 'Останній RSSI'; + String get repeater_lastRssi => 'Останній RSSI'; @override - String get repeater_lastSnr => 'Останній SNR'; + String get repeater_lastSnr => 'Останній SNR'; @override - String get repeater_noiseFloor => 'Рівень шуму'; + String get repeater_noiseFloor => 'Рівень шуму'; @override - String get repeater_txAirtime => 'Ефірний час TX'; + String get repeater_txAirtime => 'Ефірний час TX'; @override - String get repeater_rxAirtime => 'Ефірний час RX'; + String get repeater_rxAirtime => 'Ефірний час RX'; @override - String get repeater_packetStatistics => 'Статистика пакетів'; + String get repeater_packetStatistics => 'Статистика пакетів'; @override - String get repeater_sent => 'Надіслано'; + String get repeater_sent => 'Надіслано'; @override - String get repeater_received => 'Отримано'; + String get repeater_received => 'Отримано'; @override - String get repeater_duplicates => 'Дублікати'; + String get repeater_duplicates => 'Дублікати'; @override String repeater_daysHoursMinsSecs( @@ -1966,712 +1912,674 @@ class AppLocalizationsUk extends AppLocalizations { int minutes, int seconds, ) { - return '$days дн. $hours год $minutes хв $seconds с'; + return '$days дн. $hours год $minutes хв $seconds с'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; + return 'Всього: $total, На всю мережу: $flood, Прямі: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'На всю мережу: $flood, Прямі: $direct'; + return 'На всю мережу: $flood, Прямі: $direct'; } @override String repeater_duplicatesTotal(int total) { - return 'Всього: $total'; + return 'Всього: $total'; } @override - String get repeater_settingsTitle => - 'Налаштування ретранслятора'; + String get repeater_settingsTitle => 'Налаштування ретранслятора'; @override - String get repeater_basicSettings => - 'Основні налаштування'; + String get repeater_basicSettings => 'Основні налаштування'; @override - String get repeater_repeaterName => 'Ім\'я ретранслятора'; + String get repeater_repeaterName => 'Ім\'я ретранслятора'; @override String get repeater_repeaterNameHelper => - 'Показати ім\'я цього ретранслятора'; + 'Показати ім\'я цього ретранслятора'; @override - String get repeater_adminPassword => - 'Пароль адміністратора'; + String get repeater_adminPassword => 'Пароль адміністратора'; @override - String get repeater_adminPasswordHelper => - 'Пароль повного доступу'; + String get repeater_adminPasswordHelper => 'Пароль повного доступу'; @override - String get repeater_guestPassword => 'Гостьовий пароль'; + String get repeater_guestPassword => 'Гостьовий пароль'; @override String get repeater_guestPasswordHelper => - 'Доступ лише для читання з паролем'; + 'Доступ лише для читання з паролем'; @override - String get repeater_radioSettings => 'Налаштування радіо'; + String get repeater_radioSettings => 'Налаштування радіо'; @override - String get repeater_frequencyMhz => 'Частота (МГц)'; + String get repeater_frequencyMhz => 'Частота (МГц)'; @override - String get repeater_frequencyHelper => '300-2500 МГц'; + String get repeater_frequencyHelper => '300-2500 МГц'; @override - String get repeater_txPower => 'Потужність TX'; + String get repeater_txPower => 'Потужність TX'; @override - String get repeater_txPowerHelper => '1-30 дБм'; + String get repeater_txPowerHelper => '1-30 дБм'; @override - String get repeater_bandwidth => 'Смуга пропускання'; + String get repeater_bandwidth => 'Смуга пропускання'; @override - String get repeater_spreadingFactor => - 'Коефіцієнт розширення'; + String get repeater_spreadingFactor => 'Коефіцієнт розширення'; @override - String get repeater_codingRate => 'Швидкість кодування'; + String get repeater_codingRate => 'Швидкість кодування'; @override - String get repeater_locationSettings => - 'Налаштування розташування'; + String get repeater_locationSettings => 'Налаштування розташування'; @override - String get repeater_latitude => 'Широта'; + String get repeater_latitude => 'Широта'; @override String get repeater_latitudeHelper => - 'Десяткові градуси (наприклад, 37.7749)'; + 'Десяткові градуси (наприклад, 37.7749)'; @override - String get repeater_longitude => 'Довгота'; + String get repeater_longitude => 'Довгота'; @override String get repeater_longitudeHelper => - 'Десяткові градуси (наприклад, -122.4194)'; + 'Десяткові градуси (наприклад, -122.4194)'; @override - String get repeater_features => 'Функції'; + String get repeater_features => 'Функції'; @override - String get repeater_packetForwarding => - 'Пересилання пакетів'; + String get repeater_packetForwarding => 'Пересилання пакетів'; @override String get repeater_packetForwardingSubtitle => - 'Дозволити ретранслятору пересилати пакети'; + 'Дозволити ретранслятору пересилати пакети'; @override - String get repeater_guestAccess => 'Гостьовий доступ'; + String get repeater_guestAccess => 'Гостьовий доступ'; @override String get repeater_guestAccessSubtitle => - 'Дозволити гостьовий доступ лише для читання'; + 'Дозволити гостьовий доступ лише для читання'; @override - String get repeater_privacyMode => 'Режим приватності'; + String get repeater_privacyMode => 'Режим приватності'; @override String get repeater_privacyModeSubtitle => - 'Приховати ім\'я/розташування в оголошеннях'; + 'Приховати ім\'я/розташування в оголошеннях'; @override - String get repeater_advertisementSettings => - 'Налаштування оголошень'; + String get repeater_advertisementSettings => 'Налаштування оголошень'; @override String get repeater_localAdvertInterval => - 'Інтервал локальних оголошень (0 стрибків)'; + 'Інтервал локальних оголошень (0 стрибків)'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes хвилин'; + return '$minutes хвилин'; } @override String get repeater_floodAdvertInterval => - 'Інтервал оголошень на всю мережу (flood)'; + 'Інтервал оголошень на всю мережу (flood)'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours годин'; + return '$hours годин'; } @override String get repeater_encryptedAdvertInterval => - 'Інтервал зашифрованих оголошень'; + 'Інтервал зашифрованих оголошень'; @override - String get repeater_dangerZone => 'Небезпечна зона'; + String get repeater_dangerZone => 'Небезпечна зона'; @override - String get repeater_rebootRepeater => - 'Перезавантажити ретранслятор'; + String get repeater_rebootRepeater => 'Перезавантажити ретранслятор'; @override String get repeater_rebootRepeaterSubtitle => - 'Скинути пристрій ретранслятора'; + 'Скинути пристрій ретранслятора'; @override String get repeater_rebootRepeaterConfirm => - 'Ви впевнені, що хочете перезавантажити цей ретранслятор?'; + 'Ви впевнені, що хочете перезавантажити цей ретранслятор?'; @override String get repeater_regenerateIdentityKey => - 'Перегенерувати ключ ідентичності'; + 'Перегенерувати ключ ідентичності'; @override String get repeater_regenerateIdentityKeySubtitle => - 'Згенерувати нову пару ключів (публічний/приватний)'; + 'Згенерувати нову пару ключів (публічний/приватний)'; @override String get repeater_regenerateIdentityKeyConfirm => - 'Це створить нову ідентичність для ретранслятора. Продовжити?'; + 'Це створить нову ідентичність для ретранслятора. Продовжити?'; @override - String get repeater_eraseFileSystem => - 'Очистити файлову систему'; + String get repeater_eraseFileSystem => 'Очистити файлову систему'; @override String get repeater_eraseFileSystemSubtitle => - 'Відформатувати файлову систему ретранслятора'; + 'Відформатувати файлову систему ретранслятора'; @override String get repeater_eraseFileSystemConfirm => - 'УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!'; + 'УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!'; @override String get repeater_eraseSerialOnly => - 'Очищення доступне лише через послідовну консоль.'; + 'Очищення доступне лише через послідовну консоль.'; @override String repeater_commandSent(String command) { - return 'Команда надіслана: $command'; + return 'Команда надіслана: $command'; } @override String repeater_errorSendingCommand(String error) { - return 'Помилка надсилання команди: $error'; + return 'Помилка надсилання команди: $error'; } @override - String get repeater_confirm => 'Підтвердити'; + String get repeater_confirm => 'Підтвердити'; @override - String get repeater_settingsSaved => - 'Налаштування успішно збережено.'; + String get repeater_settingsSaved => 'Налаштування успішно збережено.'; @override String repeater_errorSavingSettings(String error) { - return 'Помилка збереження налаштувань: $error'; + return 'Помилка збереження налаштувань: $error'; } @override - String get repeater_refreshBasicSettings => - 'Оновити основні налаштування'; + String get repeater_refreshBasicSettings => 'Оновити основні налаштування'; @override - String get repeater_refreshRadioSettings => - 'Оновити налаштування радіо'; + String get repeater_refreshRadioSettings => 'Оновити налаштування радіо'; @override - String get repeater_refreshTxPower => - 'Оновити потужність TX'; + String get repeater_refreshTxPower => 'Оновити потужність TX'; @override String get repeater_refreshLocationSettings => - 'Оновити налаштування розташування'; + 'Оновити налаштування розташування'; @override - String get repeater_refreshPacketForwarding => - 'Оновити пересилання пакетів'; + String get repeater_refreshPacketForwarding => 'Оновити пересилання пакетів'; @override - String get repeater_refreshGuestAccess => - 'Оновити гостьовий доступ'; + String get repeater_refreshGuestAccess => 'Оновити гостьовий доступ'; @override - String get repeater_refreshPrivacyMode => - 'Оновити режим приватності'; + String get repeater_refreshPrivacyMode => 'Оновити режим приватності'; @override String get repeater_refreshAdvertisementSettings => - 'Оновити налаштування оголошень'; + 'Оновити налаштування оголошень'; @override String repeater_refreshed(String label) { - return '$label оновлено'; + return '$label оновлено'; } @override String repeater_errorRefreshing(String label) { - return 'Помилка оновлення $label'; + return 'Помилка оновлення $label'; } @override - String get repeater_cliTitle => 'Ретранслятор CLI'; + String get repeater_cliTitle => 'Ретранслятор CLI'; @override - String get repeater_debugNextCommand => - 'Налагодити наступну команду'; + String get repeater_debugNextCommand => 'Налагодити наступну команду'; @override - String get repeater_commandHelp => 'Довідка'; + String get repeater_commandHelp => 'Довідка'; @override - String get repeater_clearHistory => 'Очистити історію'; + String get repeater_clearHistory => 'Очистити історію'; @override - String get repeater_noCommandsSent => - 'Команди ще не надсилалися.'; + String get repeater_noCommandsSent => 'Команди ще не надсилалися.'; @override String get repeater_typeCommandOrUseQuick => - 'Введіть команду нижче або використовуйте швидкі команди'; + 'Введіть команду нижче або використовуйте швидкі команди'; @override - String get repeater_enterCommandHint => 'Введіть команду...'; + String get repeater_enterCommandHint => 'Введіть команду...'; @override - String get repeater_previousCommand => 'Попередня команда'; + String get repeater_previousCommand => 'Попередня команда'; @override - String get repeater_nextCommand => 'Наступна команда'; + String get repeater_nextCommand => 'Наступна команда'; @override - String get repeater_enterCommandFirst => - 'Спершу введіть команду'; + String get repeater_enterCommandFirst => 'Спершу введіть команду'; @override - String get repeater_cliCommandFrameTitle => 'Фрейм команди CLI'; + String get repeater_cliCommandFrameTitle => 'Фрейм команди CLI'; @override String repeater_cliCommandError(String error) { - return 'Помилка: $error'; + return 'Помилка: $error'; } @override - String get repeater_cliQuickGetName => 'Отримати ім\'я'; + String get repeater_cliQuickGetName => 'Отримати ім\'я'; @override - String get repeater_cliQuickGetRadio => 'Отримати Радіо'; + String get repeater_cliQuickGetRadio => 'Отримати Радіо'; @override - String get repeater_cliQuickGetTx => 'Отримати TX'; + String get repeater_cliQuickGetTx => 'Отримати TX'; @override - String get repeater_cliQuickNeighbors => 'Сусіди'; + String get repeater_cliQuickNeighbors => 'Сусіди'; @override - String get repeater_cliQuickVersion => 'Версія'; + String get repeater_cliQuickVersion => 'Версія'; @override - String get repeater_cliQuickAdvertise => 'Оголосити'; + String get repeater_cliQuickAdvertise => 'Оголосити'; @override - String get repeater_cliQuickClock => 'Годинник'; + String get repeater_cliQuickClock => 'Годинник'; @override - String get repeater_cliHelpAdvert => - 'Надсилає пакет оголошення'; + String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення'; @override String get repeater_cliHelpReboot => - 'Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що Ñ” нормальним)'; + 'Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що є нормальним)'; @override String get repeater_cliHelpClock => - 'Відображає поточний час за годинником кожного пристрою.'; + 'Відображає поточний час за годинником кожного пристрою.'; @override String get repeater_cliHelpPassword => - 'Встановлює новий пароль адміністратора для пристрою.'; + 'Встановлює новий пароль адміністратора для пристрою.'; @override String get repeater_cliHelpVersion => - 'Відображає версію пристрою та дату збірки прошивки.'; + 'Відображає версію пристрою та дату збірки прошивки.'; @override String get repeater_cliHelpClearStats => - 'Скидає різні лічильники статистики до нуля.'; + 'Скидає різні лічильники статистики до нуля.'; @override - String get repeater_cliHelpSetAf => - 'Встановлює коефіцієнт ефірного часу.'; + String get repeater_cliHelpSetAf => 'Встановлює коефіцієнт ефірного часу.'; @override String get repeater_cliHelpSetTx => - 'Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).'; + 'Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).'; @override String get repeater_cliHelpSetRepeat => - 'Вмикає або вимикає роль ретранслятора для цього вузла.'; + 'Вмикає або вимикає роль ретранслятора для цього вузла.'; @override String get repeater_cliHelpSetAllowReadOnly => - '(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)'; + '(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)'; @override String get repeater_cliHelpSetFloodMax => - 'Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).'; + 'Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).'; @override String get repeater_cliHelpSetIntThresh => - 'Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.'; + 'Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.'; @override String get repeater_cliHelpSetAgcResetInterval => - 'Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.'; + 'Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetMultiAcks => - 'Вмикає або вимикає функціональність подвійних ACK.'; + 'Вмикає або вимикає функціональність подвійних ACK.'; @override String get repeater_cliHelpSetAdvertInterval => - 'Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.'; + 'Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetFloodAdvertInterval => - 'Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.'; + 'Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetGuestPassword => - 'Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)'; + 'Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)'; @override - String get repeater_cliHelpSetName => - 'Встановлює ім\'я для оголошення.'; + String get repeater_cliHelpSetName => 'Встановлює ім\'я для оголошення.'; @override String get repeater_cliHelpSetLat => - 'Встановлює широту для карти оголошень. (десяткові градуси)'; + 'Встановлює широту для карти оголошень. (десяткові градуси)'; @override String get repeater_cliHelpSetLon => - 'Встановлює довготу для карти оголошень. (десяткові градуси)'; + 'Встановлює довготу для карти оголошень. (десяткові градуси)'; @override String get repeater_cliHelpSetRadio => - 'Повністю встановлює нові параметри радіо та зберігає Ñ—Ñ… у налаштуваннях. Потребує команди «перезавантаження» для застосування.'; + 'Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.'; @override String get repeater_cliHelpSetRxDelay => - 'Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.'; + 'Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.'; @override String get repeater_cliHelpSetTxDelay => - 'Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).'; + 'Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).'; @override String get repeater_cliHelpSetDirectTxDelay => - 'Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.'; + 'Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.'; @override - String get repeater_cliHelpSetBridgeEnabled => - 'Увімкнути/Вимкнути міст.'; + String get repeater_cliHelpSetBridgeEnabled => 'Увімкнути/Вимкнути міст.'; @override String get repeater_cliHelpSetBridgeDelay => - 'Встановити затримку перед пересиланням пакетів.'; + 'Встановити затримку перед пересиланням пакетів.'; @override String get repeater_cliHelpSetBridgeSource => - 'Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.'; + 'Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.'; @override String get repeater_cliHelpSetBridgeBaud => - 'Встановити швидкість послідовного зв\'язку для мостів Rs232.'; + 'Встановити швидкість послідовного зв\'язку для мостів Rs232.'; @override String get repeater_cliHelpSetBridgeSecret => - 'Встановити секрет мосту для мостів espnow.'; + 'Встановити секрет мосту для мостів espnow.'; @override String get repeater_cliHelpSetAdcMultiplier => - 'Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).'; + 'Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).'; @override String get repeater_cliHelpTempRadio => - 'Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).'; + 'Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).'; @override String get repeater_cliHelpSetPerm => - 'Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний Ñ– його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).'; + 'Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний і його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).'; @override String get repeater_cliHelpGetBridgeType => - 'Отримати тип мосту: немає, rs232, espnow'; + 'Отримати тип мосту: немає, rs232, espnow'; @override String get repeater_cliHelpLogStart => - 'Починає запис пакетів у файлову систему.'; + 'Починає запис пакетів у файлову систему.'; @override String get repeater_cliHelpLogStop => - 'Зупиняє запис пакетів у файлову систему.'; + 'Зупиняє запис пакетів у файлову систему.'; @override String get repeater_cliHelpLogErase => - 'Видаляє журнали пакетів з файлової системи.'; + 'Видаляє журнали пакетів з файлової системи.'; @override String get repeater_cliHelpNeighbors => - 'Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4'; + 'Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4'; @override String get repeater_cliHelpNeighborRemove => - 'Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.'; + 'Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.'; @override String get repeater_cliHelpRegion => - '(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).'; + '(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).'; @override String get repeater_cliHelpRegionLoad => - 'ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.'; + 'ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.'; @override String get repeater_cliHelpRegionGet => - 'Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім\'я-регіону (ім\'я-батька) \'F\'»'; + 'Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім\'я-регіону (ім\'я-батька) \'F\'»'; @override String get repeater_cliHelpRegionPut => - 'Додає або оновлює визначення регіону з заданою назвою.'; + 'Додає або оновлює визначення регіону з заданою назвою.'; @override String get repeater_cliHelpRegionRemove => - 'Видаляє визначення регіону з заданою назвою.'; + 'Видаляє визначення регіону з заданою назвою.'; @override String get repeater_cliHelpRegionAllowf => - 'Встановлює дозвіл «Flood» для заданого регіону. (\'\' для глобальної/успадкованої області)'; + 'Встановлює дозвіл «Flood» для заданого регіону. (\'\' для глобальної/успадкованої області)'; @override String get repeater_cliHelpRegionDenyf => - 'Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )'; + 'Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )'; @override String get repeater_cliHelpRegionHome => - 'Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)'; + 'Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)'; @override - String get repeater_cliHelpRegionHomeSet => - 'Встановлює «домашній» регіон.'; + String get repeater_cliHelpRegionHomeSet => 'Встановлює «домашній» регіон.'; @override String get repeater_cliHelpRegionSave => - 'Зберігає список/карту регіонів у сховищі.'; + 'Зберігає список/карту регіонів у сховищі.'; @override String get repeater_cliHelpGps => - 'Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.'; + 'Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.'; @override - String get repeater_cliHelpGpsOnOff => - 'Увімкнути/вимкнути GPS.'; + String get repeater_cliHelpGpsOnOff => 'Увімкнути/вимкнути GPS.'; @override String get repeater_cliHelpGpsSync => - 'Синхронізує час вузла з годинником GPS.'; + 'Синхронізує час вузла з годинником GPS.'; @override String get repeater_cliHelpGpsSetLoc => - 'Встановлює позицію вузла за координатами GPS Ñ– зберігає в налаштуваннях.'; + 'Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.'; @override String get repeater_cliHelpGpsAdvert => - 'Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях'; + 'Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях'; @override String get repeater_cliHelpGpsAdvertSet => - 'Встановлює конфігурацію оголошення розташування.'; + 'Встановлює конфігурацію оголошення розташування.'; @override - String get repeater_commandsListTitle => 'Список команд'; + String get repeater_commandsListTitle => 'Список команд'; @override String get repeater_commandsListNote => - 'ПРИМІТКА: для різних команд «set»... також існує команда «get»...'; + 'ПРИМІТКА: для різних команд «set»... також існує команда «get»...'; @override - String get repeater_general => 'Загальні'; + String get repeater_general => 'Загальні'; @override - String get repeater_settingsCategory => 'Налаштування'; + String get repeater_settingsCategory => 'Налаштування'; @override - String get repeater_bridge => 'Міст'; + String get repeater_bridge => 'Міст'; @override - String get repeater_logging => 'Логування'; + String get repeater_logging => 'Логування'; @override - String get repeater_neighborsRepeaterOnly => - 'Сусіди (Тільки ретранслятор)'; + String get repeater_neighborsRepeaterOnly => 'Сусіди (Тільки ретранслятор)'; @override String get repeater_regionManagementRepeaterOnly => - 'Керування регіонами (Тільки ретранслятор)'; + 'Керування регіонами (Тільки ретранслятор)'; @override String get repeater_regionNote => - 'Команди регіонів були введені для керування визначеннями та дозволами регіонів.'; + 'Команди регіонів були введені для керування визначеннями та дозволами регіонів.'; @override - String get repeater_gpsManagement => 'Керування GPS'; + String get repeater_gpsManagement => 'Керування GPS'; @override String get repeater_gpsNote => - 'Команда GPS була введена для керування питаннями, пов\'язаними з локацією.'; + 'Команда GPS була введена для керування питаннями, пов\'язаними з локацією.'; @override - String get telemetry_receivedData => - 'Дані телеметрії отримано'; + String get telemetry_receivedData => 'Дані телеметрії отримано'; @override - String get telemetry_requestTimeout => - 'Час запиту телеметрії вичерпано.'; + String get telemetry_requestTimeout => 'Час запиту телеметрії вичерпано.'; @override String telemetry_errorLoading(String error) { - return 'Помилка завантаження телеметрії: $error'; + return 'Помилка завантаження телеметрії: $error'; } @override - String get telemetry_noData => - 'Дані телеметрії недоступні.'; + String get telemetry_noData => 'Дані телеметрії недоступні.'; @override String telemetry_channelTitle(int channel) { - return 'Канал $channel'; + return 'Канал $channel'; } @override - String get telemetry_batteryLabel => 'Батарея'; + String get telemetry_batteryLabel => 'Батарея'; @override - String get telemetry_voltageLabel => 'Напруга'; + String get telemetry_voltageLabel => 'Напруга'; @override - String get telemetry_mcuTemperatureLabel => 'Температура MCU'; + String get telemetry_mcuTemperatureLabel => 'Температура MCU'; @override - String get telemetry_temperatureLabel => 'Температура'; + String get telemetry_temperatureLabel => 'Температура'; @override - String get telemetry_currentLabel => 'Поточний струм'; + String get telemetry_currentLabel => 'Поточний струм'; @override String telemetry_batteryValue(int percent, String volts) { - return '$percent% / $voltsÐ’'; + return '$percent% / $voltsВ'; } @override String telemetry_voltageValue(String volts) { - return '$voltsÐ’'; + return '$voltsВ'; } @override String telemetry_currentValue(String amps) { - return '$ampsА'; + return '$ampsА'; } @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => - 'Дані сусідів отримано'; + String get neighbors_receivedData => 'Дані сусідів отримано'; @override - String get neighbors_requestTimedOut => - 'Час запиту сусідів вичерпано.'; + String get neighbors_requestTimedOut => 'Час запиту сусідів вичерпано.'; @override String neighbors_errorLoading(String error) { - return 'Помилка завантаження сусідів: $error'; + return 'Помилка завантаження сусідів: $error'; } @override - String get neighbors_repeatersNeighbors => - 'Ретранслятори-сусіди'; + String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди'; @override - String get neighbors_noData => - 'Дані про сусідів недоступні.'; + String get neighbors_noData => 'Дані про сусідів недоступні.'; @override String neighbors_unknownContact(String pubkey) { - return 'Невідомий відкритий ключ $pubkey'; + return 'Невідомий відкритий ключ $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Почуто: $time тому'; + return 'Почуто: $time тому'; } @override - String get channelPath_title => 'Шлях пакету'; + String get channelPath_title => 'Шлях пакету'; @override - String get channelPath_viewMap => 'Показати карту'; + String get channelPath_viewMap => 'Показати карту'; @override - String get channelPath_otherObservedPaths => - 'Інші спостережувані шляхи'; + String get channelPath_otherObservedPaths => 'Інші спостережувані шляхи'; @override - String get channelPath_repeaterHops => - 'Стрибки ретранслятора'; + String get channelPath_repeaterHops => 'Стрибки ретранслятора'; @override String get channelPath_noHopDetails => - 'Деталі відправки не надані для цього пакету.'; + 'Деталі відправки не надані для цього пакету.'; @override - String get channelPath_messageDetails => - 'Деталі повідомлення'; + String get channelPath_messageDetails => 'Деталі повідомлення'; @override - String get channelPath_senderLabel => 'Відправник'; + String get channelPath_senderLabel => 'Відправник'; @override - String get channelPath_timeLabel => 'Час'; + String get channelPath_timeLabel => 'Час'; @override - String get channelPath_repeatsLabel => 'Повторення'; + String get channelPath_repeatsLabel => 'Повторення'; @override String channelPath_pathLabel(int index) { - return 'Шлях $index'; + return 'Шлях $index'; } @override - String get channelPath_observedLabel => 'Спостережено'; + String get channelPath_observedLabel => 'Спостережено'; @override String channelPath_observedPathTitle(int index, String hops) { - return 'Спостережуваний шлях $index • $hops'; + return 'Спостережуваний шлях $index • $hops'; } @override - String get channelPath_noLocationData => - 'Немає даних про розташування'; + String get channelPath_noLocationData => 'Немає даних про розташування'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2684,161 +2592,153 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get channelPath_unknownPath => 'Невідомий'; + String get channelPath_unknownPath => 'Невідомий'; @override - String get channelPath_floodPath => 'На всю мережу'; + String get channelPath_floodPath => 'На всю мережу'; @override - String get channelPath_directPath => 'Прямий'; + String get channelPath_directPath => 'Прямий'; @override String channelPath_observedZeroOf(int total) { - return '0 з $total стрибків'; + return '0 з $total стрибків'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed з $total стрибків'; + return '$observed з $total стрибків'; } @override - String get channelPath_mapTitle => 'Карта шляху'; + String get channelPath_mapTitle => 'Карта шляху'; @override String get channelPath_noRepeaterLocations => - 'Позиції ретрансляторів недоступні для цього шляху.'; + 'Позиції ретрансляторів недоступні для цього шляху.'; @override String channelPath_primaryPath(int index) { - return 'Шлях $index (Основний)'; + return 'Шлях $index (Основний)'; } @override - String get channelPath_pathLabelTitle => 'Шлях'; + String get channelPath_pathLabelTitle => 'Шлях'; @override - String get channelPath_observedPathHeader => - 'Спостережуваний шлях'; + String get channelPath_observedPathHeader => 'Спостережуваний шлях'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override String get channelPath_noHopDetailsAvailable => - 'Деталі стрибків недоступні для цього пакету.'; + 'Деталі стрибків недоступні для цього пакету.'; @override - String get channelPath_unknownRepeater => - 'Невідомий ретранслятор'; + String get channelPath_unknownRepeater => 'Невідомий ретранслятор'; @override - String get community_title => 'Спільнота'; + String get community_title => 'Спільнота'; @override - String get community_create => 'Створити спільноту'; + String get community_create => 'Створити спільноту'; @override String get community_createDesc => - 'Створити нову спільноту та поділитися через QR-код.'; + 'Створити нову спільноту та поділитися через QR-код.'; @override - String get community_join => 'Приєднатися'; + String get community_join => 'Приєднатися'; @override - String get community_joinTitle => - 'Приєднатися до спільноти'; + String get community_joinTitle => 'Приєднатися до спільноти'; @override String community_joinConfirmation(String name) { - return 'Ви бажаєте приєднатися до спільноти «$name»?'; + return 'Ви бажаєте приєднатися до спільноти «$name»?'; } @override - String get community_scanQr => 'Сканувати QR спільноти'; + String get community_scanQr => 'Сканувати QR спільноти'; @override String get community_scanInstructions => - 'Наведіть камеру на QR-код спільноти.'; + 'Наведіть камеру на QR-код спільноти.'; @override - String get community_showQr => 'Показати QR-код'; + String get community_showQr => 'Показати QR-код'; @override - String get community_publicChannel => 'Публічна спільнота'; + String get community_publicChannel => 'Публічна спільнота'; @override - String get community_hashtagChannel => 'Хештег спільноти'; + String get community_hashtagChannel => 'Хештег спільноти'; @override - String get community_name => 'Назва спільноти'; + String get community_name => 'Назва спільноти'; @override - String get community_enterName => - 'Введіть назву спільноти'; + String get community_enterName => 'Введіть назву спільноти'; @override String community_created(String name) { - return 'Спільноту «$name» створено'; + return 'Спільноту «$name» створено'; } @override String community_joined(String name) { - return 'Приєднався до спільноти «$name»'; + return 'Приєднався до спільноти «$name»'; } @override - String get community_qrTitle => 'Поділитися спільнотою'; + String get community_qrTitle => 'Поділитися спільнотою'; @override String community_qrInstructions(String name) { - return 'Відскануйте цей QR-код, щоб приєднатися до $name'; + return 'Відскануйте цей QR-код, щоб приєднатися до $name'; } @override String get community_hashtagPrivacyHint => - 'Канали хештегів спільноти доступні лише членам спільноти'; + 'Канали хештегів спільноти доступні лише членам спільноти'; @override - String get community_invalidQrCode => - 'Недійсний QR-код спільноти'; + String get community_invalidQrCode => 'Недійсний QR-код спільноти'; @override - String get community_alreadyMember => 'Вже учасник'; + String get community_alreadyMember => 'Вже учасник'; @override String community_alreadyMemberMessage(String name) { - return 'Ви вже Ñ” учасником «$name».'; + return 'Ви вже є учасником «$name».'; } @override - String get community_addPublicChannel => - 'Додати публічний канал спільноти'; + String get community_addPublicChannel => 'Додати публічний канал спільноти'; @override String get community_addPublicChannelHint => - 'Автоматично додати публічний канал для цієї спільноти'; + 'Автоматично додати публічний канал для цієї спільноти'; @override - String get community_noCommunities => - 'Поки не приєднано до жодної групи.'; + String get community_noCommunities => 'Поки не приєднано до жодної групи.'; @override String get community_scanOrCreate => - 'Відскануйте QR-код або створіть спільноту, щоб почати'; + 'Відскануйте QR-код або створіть спільноту, щоб почати'; @override - String get community_manageCommunities => - 'Керувати спільнотами'; + String get community_manageCommunities => 'Керувати спільнотами'; @override - String get community_delete => 'Покинути спільноту'; + String get community_delete => 'Покинути спільноту'; @override String community_deleteConfirm(String name) { - return 'Покинути «$name»?'; + return 'Покинути «$name»?'; } @override @@ -2846,205 +2746,196 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'каналів', - many: 'каналів', - few: 'канали', - one: 'канал', + other: 'каналів', + many: 'каналів', + few: 'канали', + one: 'канал', ); - return 'Це також видалить $count $_temp0 та Ñ—Ñ… повідомлення.'; + return 'Це також видалить $count $_temp0 та їх повідомлення.'; } @override String community_deleted(String name) { - return 'Спільноту «$name» покинуто'; + return 'Спільноту «$name» покинуто'; } @override - String get community_regenerateSecret => - 'Перегенерувати секрет'; + String get community_regenerateSecret => 'Перегенерувати секрет'; @override String community_regenerateSecretConfirm(String name) { - return 'Перегенерувати секретний ключ для «$name»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; + return 'Перегенерувати секретний ключ для «$name»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.'; } @override - String get community_regenerate => 'Перегенерувати'; + String get community_regenerate => 'Перегенерувати'; @override String community_secretRegenerated(String name) { - return 'Секретний пароль для «$name» перегенеровано'; + return 'Секретний пароль для «$name» перегенеровано'; } @override - String get community_updateSecret => 'Оновити секрет'; + String get community_updateSecret => 'Оновити секрет'; @override String community_secretUpdated(String name) { - return 'Зміну секрету для «$name» оновлено'; + return 'Зміну секрету для «$name» оновлено'; } @override String community_scanToUpdateSecret(String name) { - return 'Відскануйте новий QR-код, щоб оновити пароль для «$name»'; + return 'Відскануйте новий QR-код, щоб оновити пароль для «$name»'; } @override - String get community_addHashtagChannel => - 'Додати хештег спільноти'; + String get community_addHashtagChannel => 'Додати хештег спільноти'; @override String get community_addHashtagChannelDesc => - 'Додати канал хештегу для цієї спільноти'; + 'Додати канал хештегу для цієї спільноти'; @override - String get community_selectCommunity => 'Вибрати спільноту'; + String get community_selectCommunity => 'Вибрати спільноту'; @override - String get community_regularHashtag => 'Звичайний хештег'; + String get community_regularHashtag => 'Звичайний хештег'; @override String get community_regularHashtagDesc => - 'Публічний хештег (будь-хто може приєднатися)'; + 'Публічний хештег (будь-хто може приєднатися)'; @override - String get community_communityHashtag => 'Хештег спільноти'; + String get community_communityHashtag => 'Хештег спільноти'; @override String get community_communityHashtagDesc => - 'Ексклюзивно для членів спільноти'; + 'Ексклюзивно для членів спільноти'; @override String community_forCommunity(String name) { - return 'Для $name'; + return 'Для $name'; } @override - String get listFilter_tooltip => 'Фільтр та сортування'; + String get listFilter_tooltip => 'Фільтр та сортування'; @override - String get listFilter_sortBy => 'Сортувати за'; + String get listFilter_sortBy => 'Сортувати за'; @override - String get listFilter_latestMessages => - 'Останні повідомлення'; + String get listFilter_latestMessages => 'Останні повідомлення'; @override - String get listFilter_heardRecently => 'Нещодавно чули'; + String get listFilter_heardRecently => 'Нещодавно чули'; @override - String get listFilter_az => 'А-Я'; + String get listFilter_az => 'А-Я'; @override - String get listFilter_filters => 'Фільтри'; + String get listFilter_filters => 'Фільтри'; @override - String get listFilter_all => 'Все'; + String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Улюблені'; + String get listFilter_favorites => 'Улюблені'; @override - String get listFilter_addToFavorites => - 'Додати до улюблених'; + String get listFilter_addToFavorites => 'Додати до улюблених'; @override - String get listFilter_removeFromFavorites => - 'Видалити зі списку улюблених'; + String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених'; @override - String get listFilter_users => 'Користувачі'; + String get listFilter_users => 'Користувачі'; @override - String get listFilter_repeaters => 'Ретранслятори'; + String get listFilter_repeaters => 'Ретранслятори'; @override - String get listFilter_roomServers => 'Сервери кімнат'; + String get listFilter_roomServers => 'Сервери кімнат'; @override - String get listFilter_unreadOnly => - 'Тільки непрочитані повідомлення'; + String get listFilter_unreadOnly => 'Тільки непрочитані повідомлення'; @override - String get listFilter_newGroup => 'Нова група'; + String get listFilter_newGroup => 'Нова група'; @override - String get pathTrace_you => 'Ви'; + String get pathTrace_you => 'Ви'; @override - String get pathTrace_failed => - 'Відстеження шляху не вдалося.'; + String get pathTrace_failed => 'Відстеження шляху не вдалося.'; @override - String get pathTrace_notAvailable => - 'Трасування шляху недоступне.'; + String get pathTrace_notAvailable => 'Трасування шляху недоступне.'; @override - String get pathTrace_refreshTooltip => 'Оновити Path Trace'; + String get pathTrace_refreshTooltip => 'Оновити Path Trace'; @override String get pathTrace_someHopsNoLocation => - 'Одне або більше хмелів відсутнє місце розташування!'; + 'Одне або більше хмелів відсутнє місце розташування!'; @override - String get pathTrace_clearTooltip => 'Очистити шлях'; + String get pathTrace_clearTooltip => 'Очистити шлях'; @override String get losSelectStartEnd => - 'Виберіть початковий Ñ– кінцевий вузли для LOS.'; + 'Виберіть початковий і кінцевий вузли для LOS.'; @override String losRunFailed(String error) { - return 'Помилка перевірки прямої видимості: $error'; + return 'Помилка перевірки прямої видимості: $error'; } @override - String get losClearAllPoints => 'Очистити всі пункти'; + String get losClearAllPoints => 'Очистити всі пункти'; @override String get losRunToViewElevationProfile => - 'Запустіть LOS, щоб переглянути профіль висоти'; + 'Запустіть LOS, щоб переглянути профіль висоти'; @override - String get losMenuTitle => 'Меню LOS'; + String get losMenuTitle => 'Меню LOS'; @override String get losMenuSubtitle => - 'Торкніться вузлів або утримуйте карту, щоб отримати власні точки'; + 'Торкніться вузлів або утримуйте карту, щоб отримати власні точки'; @override - String get losShowDisplayNodes => - 'Показати вузли відображення'; + String get losShowDisplayNodes => 'Показати вузли відображення'; @override - String get losCustomPoints => 'Користувальницькі точки'; + String get losCustomPoints => 'Користувальницькі точки'; @override String losCustomPointLabel(int index) { - return 'Спеціальний $index'; + return 'Спеціальний $index'; } @override - String get losPointA => 'Точка А'; + String get losPointA => 'Точка А'; @override - String get losPointB => 'Точка Б'; + String get losPointB => 'Точка Б'; @override String losAntennaA(String value, String unit) { - return 'Антена A: $value $unit'; + return 'Антена A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return 'Антена B: $value $unit'; + return 'Антена B: $value $unit'; } @override - String get losRun => 'Запустіть LOS'; + String get losRun => 'Запустіть LOS'; @override - String get losNoElevationData => 'Немає даних про висоту'; + String get losNoElevationData => 'Немає даних про висоту'; @override String losProfileClear( @@ -3053,7 +2944,7 @@ class AppLocalizationsUk extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit'; + return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit'; } @override @@ -3063,64 +2954,61 @@ class AppLocalizationsUk extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit, заблоковано $obstruction $heightUnit'; + return '$distance $distanceUnit, заблоковано $obstruction $heightUnit'; } @override - String get losStatusChecking => 'LOS: перевірка...'; + String get losStatusChecking => 'LOS: перевірка...'; @override - String get losStatusNoData => 'LOS: немає даних'; + String get losStatusNoData => 'LOS: немає даних'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо'; + return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо'; } @override String get losErrorElevationUnavailable => - 'Дані про висоту недоступні для одного чи кількох зразків.'; + 'Дані про висоту недоступні для одного чи кількох зразків.'; @override String get losErrorInvalidInput => - 'Недійсні дані про точки/висоту для розрахунку LOS.'; + 'Недійсні дані про точки/висоту для розрахунку LOS.'; @override - String get losRenameCustomPoint => - 'Перейменуйте спеціальну точку'; + String get losRenameCustomPoint => 'Перейменуйте спеціальну точку'; @override - String get losPointName => 'Назва точки'; + String get losPointName => 'Назва точки'; @override - String get losShowPanelTooltip => 'Показати панель LOS'; + String get losShowPanelTooltip => 'Показати панель LOS'; @override - String get losHidePanelTooltip => 'Приховати панель LOS'; + String get losHidePanelTooltip => 'Приховати панель LOS'; @override String get losElevationAttribution => - 'Дані про висоту: Open-Meteo (CC BY 4.0)'; + 'Дані про висоту: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Радіогоризонт'; + String get losLegendRadioHorizon => 'Радіогоризонт'; @override - String get losLegendLosBeam => 'Лінія прямої видимості'; + String get losLegendLosBeam => 'Лінія прямої видимості'; @override - String get losLegendTerrain => 'Рельєф'; + String get losLegendTerrain => 'Рельєф'; @override - String get losFrequencyLabel => 'Частота'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => - 'Переглянути деталі розрахунку'; + String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку'; @override - String get losFrequencyDialogTitle => - 'Розрахунок радіогоризонту'; + String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту'; @override String losFrequencyDialogDescription( @@ -3129,104 +3017,96 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; + return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; } @override - String get contacts_pathTrace => 'Трасування шляхів'; + String get contacts_pathTrace => 'Трасування шляхів'; @override - String get contacts_ping => 'Пінгувати'; + String get contacts_ping => 'Пінгувати'; @override - String get contacts_repeaterPathTrace => - 'Трасування шляху до повторювача'; + String get contacts_repeaterPathTrace => 'Трасування шляху до повторювача'; @override - String get contacts_repeaterPing => 'Пінгувати повторювач'; + String get contacts_repeaterPing => 'Пінгувати повторювач'; @override - String get contacts_roomPathTrace => - 'Трасування шляху до серверу кімнати'; + String get contacts_roomPathTrace => 'Трасування шляху до серверу кімнати'; @override - String get contacts_roomPing => 'Пінг сервера кімнати'; + String get contacts_roomPing => 'Пінг сервера кімнати'; @override - String get contacts_chatTraceRoute => 'Трасування шляху'; + String get contacts_chatTraceRoute => 'Трасування шляху'; @override String contacts_pathTraceTo(String name) { - return 'Відстежити маршрут до $name'; + return 'Відстежити маршрут до $name'; } @override - String get contacts_clipboardEmpty => - 'Буфер обміну порожній'; + String get contacts_clipboardEmpty => 'Буфер обміну порожній'; @override - String get contacts_invalidAdvertFormat => - 'Недійсні контактні дані'; + String get contacts_invalidAdvertFormat => 'Недійсні контактні дані'; @override - String get contacts_contactImported => - 'Контакт було імпортовано.'; + String get contacts_contactImported => 'Контакт було імпортовано.'; @override - String get contacts_contactImportFailed => - 'Контакт не вдалося імпортувати'; + String get contacts_contactImportFailed => 'Контакт не вдалося імпортувати'; @override - String get contacts_zeroHopAdvert => - 'Реклама без перехоплення'; + String get contacts_zeroHopAdvert => 'Реклама без перехоплення'; @override - String get contacts_floodAdvert => 'Залив реклами'; + String get contacts_floodAdvert => 'Залив реклами'; @override String get contacts_copyAdvertToClipboard => - 'Копіювати оголошення в буфер обміну'; + 'Копіювати оголошення в буфер обміну'; @override String get contacts_addContactFromClipboard => - 'Додати контакт з буфера обміну'; + 'Додати контакт з буфера обміну'; @override - String get contacts_ShareContact => - 'Копіювати контакт у буфер обміну'; + String get contacts_ShareContact => 'Копіювати контакт у буфер обміну'; @override String get contacts_ShareContactZeroHop => - 'Поділитися контактом за оголошенням'; + 'Поділитися контактом за оголошенням'; @override String get contacts_zeroHopContactAdvertSent => - 'Відправлено контакт за оголошенням'; + 'Відправлено контакт за оголошенням'; @override String get contacts_zeroHopContactAdvertFailed => - 'Не вдалося надіслати контакт.'; + 'Не вдалося надіслати контакт.'; @override String get contacts_contactAdvertCopied => - 'Рекламу скопійовано до буфера обміну.'; + 'Рекламу скопійовано до буфера обміну.'; @override String get contacts_contactAdvertCopyFailed => - 'Копіювання оголошення в буфер обміну завершилося невдало'; + 'Копіювання оголошення в буфер обміну завершилося невдало'; @override - String get notification_activityTitle => 'Активність MeshCore'; + String get notification_activityTitle => 'Активність MeshCore'; @override String notification_messagesCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'повідомлень', - many: 'повідомлень', - few: 'повідомлення', - one: 'повідомлення', + other: 'повідомлень', + many: 'повідомлень', + few: 'повідомлення', + one: 'повідомлення', ); return '$count $_temp0'; } @@ -3236,10 +3116,10 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'повідомлень каналу', - many: 'повідомлень каналу', - few: 'повідомлення каналу', - one: 'повідомлення каналу', + other: 'повідомлень каналу', + many: 'повідомлень каналу', + few: 'повідомлення каналу', + one: 'повідомлення каналу', ); return '$count $_temp0'; } @@ -3249,86 +3129,78 @@ class AppLocalizationsUk extends AppLocalizations { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'нових вузлів', - many: 'нових вузлів', - few: 'нових вузли', - one: 'новий вузол', + other: 'нових вузлів', + many: 'нових вузлів', + few: 'нових вузли', + one: 'новий вузол', ); return '$count $_temp0'; } @override String notification_newTypeDiscovered(String contactType) { - return 'Виявлено новий $contactType'; + return 'Виявлено новий $contactType'; } @override - String get notification_receivedNewMessage => - 'Отримано нове повідомлення'; + String get notification_receivedNewMessage => 'Отримано нове повідомлення'; @override String get settings_gpxExportRepeaters => - 'Експортувати ретранслятори / сервер кімнати до GPX'; + 'Експортувати ретранслятори / сервер кімнати до GPX'; @override String get settings_gpxExportRepeatersSubtitle => - 'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.'; + 'Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.'; @override - String get settings_gpxExportContacts => - 'Експортувати супутників до GPX'; + String get settings_gpxExportContacts => 'Експортувати супутників до GPX'; @override String get settings_gpxExportContactsSubtitle => - 'Експортує супутників з місцезнаходженням у файл GPX.'; + 'Експортує супутників з місцезнаходженням у файл GPX.'; @override - String get settings_gpxExportAll => - 'Експортувати всі контакти до GPX'; + String get settings_gpxExportAll => 'Експортувати всі контакти до GPX'; @override String get settings_gpxExportAllSubtitle => - 'Експортує всі контакти з місцем розташування у файл GPX.'; + 'Експортує всі контакти з місцем розташування у файл GPX.'; @override - String get settings_gpxExportSuccess => - 'Успішно експортовано файл GPX.'; + String get settings_gpxExportSuccess => 'Успішно експортовано файл GPX.'; @override - String get settings_gpxExportNoContacts => - 'Немає контактів для експорту.'; + String get settings_gpxExportNoContacts => 'Немає контактів для експорту.'; @override String get settings_gpxExportNotAvailable => - 'Не підтримується на вашому пристрої/операційній системі'; + 'Не підтримується на вашому пристрої/операційній системі'; @override - String get settings_gpxExportError => - 'Сталася помилка під час експорту.'; + String get settings_gpxExportError => 'Сталася помилка під час експорту.'; @override String get settings_gpxExportRepeatersRoom => - 'Місцезнаходження повторювача та сервера кімнати'; + 'Місцезнаходження повторювача та сервера кімнати'; @override - String get settings_gpxExportChat => 'Місця супутників'; + String get settings_gpxExportChat => 'Місця супутників'; @override - String get settings_gpxExportAllContacts => - 'Усі місця контактів'; + String get settings_gpxExportAllContacts => 'Усі місця контактів'; @override String get settings_gpxExportShareText => - 'Дані карти експортовані з meshcore-open'; + 'Дані карти експортовані з meshcore-open'; @override String get settings_gpxExportShareSubject => - 'експорт даних карти meshcore-open у форматі GPX'; + 'експорт даних карти meshcore-open у форматі GPX'; @override - String get snrIndicator_nearByRepeaters => - 'Ближні ретранслятори'; + String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори'; @override - String get snrIndicator_lastSeen => 'Останній раз бачили'; + String get snrIndicator_lastSeen => 'Останній раз бачили'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ef3bafa..9b32209 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -12,88 +12,88 @@ class AppLocalizationsZh extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => '联系人'; + String get nav_contacts => '联系人'; @override - String get nav_channels => '频道'; + String get nav_channels => '频道'; @override - String get nav_map => '地图'; + String get nav_map => '地图'; @override - String get common_cancel => '取消'; + String get common_cancel => '取消'; @override - String get common_ok => '确定'; + String get common_ok => '确定'; @override - String get common_connect => '连接'; + String get common_connect => '连接'; @override - String get common_unknownDevice => '未知设备'; + String get common_unknownDevice => '未知设备'; @override - String get common_save => '保存'; + String get common_save => '保存'; @override - String get common_delete => '删除'; + String get common_delete => '删除'; @override - String get common_close => '关闭'; + String get common_close => '关闭'; @override - String get common_edit => '编辑'; + String get common_edit => '编辑'; @override - String get common_add => '添加'; + String get common_add => '添加'; @override - String get common_settings => '设置'; + String get common_settings => '设置'; @override - String get common_disconnect => 'æ–­å¼€'; + String get common_disconnect => '断开'; @override - String get common_connected => '已连接'; + String get common_connected => '已连接'; @override - String get common_disconnected => '已断开'; + String get common_disconnected => '已断开'; @override - String get common_create => '创建'; + String get common_create => '创建'; @override - String get common_continue => 'ç»§ç»­'; + String get common_continue => '继续'; @override - String get common_share => '分享'; + String get common_share => '分享'; @override - String get common_copy => '复制'; + String get common_copy => '复制'; @override - String get common_retry => '重试'; + String get common_retry => '重试'; @override - String get common_hide => '隐藏'; + String get common_hide => '隐藏'; @override - String get common_remove => '移除'; + String get common_remove => '移除'; @override - String get common_enable => '启用'; + String get common_enable => '启用'; @override - String get common_disable => '禁用'; + String get common_disable => '禁用'; @override - String get common_reboot => '重启'; + String get common_reboot => '重启'; @override - String get common_loading => '正在加载...'; + String get common_loading => '正在加载...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -106,233 +106,258 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get scanner_title => '连接设备'; + String get scanner_title => '连接设备'; @override String get connectionChoiceUsbLabel => 'USB'; @override - String get connectionChoiceBluetoothLabel => '蓝牙'; + String get connectionChoiceBluetoothLabel => '蓝牙'; @override - String get usbScreenTitle => '通过USB连接'; + String get usbScreenTitle => '通过USB连接'; @override - String get usbScreenSubtitle => - '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。'; + String get usbScreenSubtitle => '选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。'; @override - String get usbScreenStatus => '选择一个 USB 设备'; + String get usbScreenStatus => '选择一个 USB 设备'; @override - String get usbScreenNote => - '在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。'; + String get usbScreenNote => '在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。'; @override - String get usbScreenEmptyState => - '未找到任何 USB 设备。请插入一个,然后刷新。'; + String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。'; @override - String get scanner_scanning => '正在搜索设备...'; + String get usbErrorPermissionDenied => '拒绝了USB权限。'; @override - String get scanner_connecting => '正在连接...'; + String get usbErrorDeviceMissing => '所选的USB设备已不再可用。'; @override - String get scanner_disconnecting => '断开连接...'; + String get usbErrorInvalidPort => '选择一个有效的USB设备。'; @override - String get scanner_notConnected => '未连接'; + String get usbErrorBusy => '还有一个 USB 连接请求正在进行中。'; + + @override + String get usbErrorNotConnected => '没有连接任何USB设备。'; + + @override + String get usbErrorOpenFailed => '未能打开所选的USB设备。'; + + @override + String get usbErrorConnectFailed => '未能连接到所选的USB设备。'; + + @override + String get usbErrorUnsupported => '此平台不支持USB串行通信。'; + + @override + String get usbErrorAlreadyActive => 'USB 连接已建立。'; + + @override + String get usbErrorNoDeviceSelected => '未选择任何 USB 设备。'; + + @override + String get usbErrorPortClosed => 'USB 连接未建立。'; + + @override + String get usbErrorConnectTimedOut => '等待设备响应超时。'; + + @override + String get scanner_scanning => '正在搜索设备...'; + + @override + String get scanner_connecting => '正在连接...'; + + @override + String get scanner_disconnecting => '断开连接...'; + + @override + String get scanner_notConnected => '未连接'; @override String scanner_connectedTo(String deviceName) { - return '已连接到 $deviceName'; + return '已连接到 $deviceName'; } @override - String get scanner_searchingDevices => '正在搜索 MeshCore 设备...'; + String get scanner_searchingDevices => '正在搜索 MeshCore 设备...'; @override - String get scanner_tapToScan => - '点击“扫描”按钮以查找 MeshCore 设备。'; + String get scanner_tapToScan => '点击“扫描”按钮以查找 MeshCore 设备。'; @override String scanner_connectionFailed(String error) { - return '连接失败:$error'; + return '连接失败:$error'; } @override - String get scanner_stop => '停止'; + String get scanner_stop => '停止'; @override - String get scanner_scan => '扫描'; + String get scanner_scan => '扫描'; @override - String get scanner_bluetoothOff => '蓝牙已关闭'; + String get scanner_bluetoothOff => '蓝牙已关闭'; @override - String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备'; + String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备'; @override - String get scanner_chromeRequired => '需要 Chrome 浏览器'; + String get scanner_chromeRequired => '需要 Chrome 浏览器'; @override String get scanner_chromeRequiredMessage => - 'æ­¤ Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。'; + '此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。'; @override - String get scanner_enableBluetooth => '启用蓝牙'; + String get scanner_enableBluetooth => '启用蓝牙'; @override - String get device_quickSwitch => '快速切换'; + String get device_quickSwitch => '快速切换'; @override String get device_meshcore => 'MeshCore'; @override - String get settings_title => '设置'; + String get settings_title => '设置'; @override - String get settings_deviceInfo => '设备信息'; + String get settings_deviceInfo => '设备信息'; @override - String get settings_appSettings => '应用设置'; + String get settings_appSettings => '应用设置'; @override - String get settings_appSettingsSubtitle => '通知、消息和地图偏好'; + String get settings_appSettingsSubtitle => '通知、消息和地图偏好'; @override - String get settings_nodeSettings => '节点设置'; + String get settings_nodeSettings => '节点设置'; @override - String get settings_nodeName => '节点名称'; + String get settings_nodeName => '节点名称'; @override - String get settings_nodeNameNotSet => '未设置'; + String get settings_nodeNameNotSet => '未设置'; @override - String get settings_nodeNameHint => '请输入节点名称'; + String get settings_nodeNameHint => '请输入节点名称'; @override - String get settings_nodeNameUpdated => '节点名称已更新'; + String get settings_nodeNameUpdated => '节点名称已更新'; @override - String get settings_radioSettings => '无线电设置'; + String get settings_radioSettings => '无线电设置'; @override - String get settings_radioSettingsSubtitle => '频率、功率、扩频因子'; + String get settings_radioSettingsSubtitle => '频率、功率、扩频因子'; @override - String get settings_radioSettingsUpdated => '无线电设置已更新'; + String get settings_radioSettingsUpdated => '无线电设置已更新'; @override - String get settings_location => '位置'; + String get settings_location => '位置'; @override - String get settings_locationSubtitle => 'GPS 坐标'; + String get settings_locationSubtitle => 'GPS 坐标'; @override - String get settings_locationUpdated => '位置和 GPS 设置已更新'; + String get settings_locationUpdated => '位置和 GPS 设置已更新'; @override - String get settings_locationBothRequired => '请输入经度和纬度'; + String get settings_locationBothRequired => '请输入经度和纬度'; @override - String get settings_locationInvalid => '无效的经度和纬度'; + String get settings_locationInvalid => '无效的经度和纬度'; @override - String get settings_locationGPSEnable => '启用 GPS'; + String get settings_locationGPSEnable => '启用 GPS'; @override - String get settings_locationGPSEnableSubtitle => - '启用 GPS 以自动更新位置。'; + String get settings_locationGPSEnableSubtitle => '启用 GPS 以自动更新位置。'; @override - String get settings_locationIntervalSec => 'GPS 间隔(秒)'; + String get settings_locationIntervalSec => 'GPS 间隔(秒)'; @override - String get settings_locationIntervalInvalid => - '间隔时间必须至少为 60 秒,但不超过 86400 秒。'; + String get settings_locationIntervalInvalid => '间隔时间必须至少为 60 秒,但不超过 86400 秒。'; @override - String get settings_latitude => '纬度'; + String get settings_latitude => '纬度'; @override - String get settings_longitude => '经度'; + String get settings_longitude => '经度'; @override - String get settings_privacyMode => '隐私模式'; + String get settings_privacyMode => '隐私模式'; @override - String get settings_privacyModeSubtitle => '在广告中隐藏姓名/位置'; + String get settings_privacyModeSubtitle => '在广告中隐藏姓名/位置'; @override - String get settings_privacyModeToggle => - '切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。'; + String get settings_privacyModeToggle => '切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。'; @override - String get settings_privacyModeEnabled => '隐私模式已启用'; + String get settings_privacyModeEnabled => '隐私模式已启用'; @override - String get settings_privacyModeDisabled => '隐私模式已关闭'; + String get settings_privacyModeDisabled => '隐私模式已关闭'; @override - String get settings_actions => '操作'; + String get settings_actions => '操作'; @override - String get settings_sendAdvertisement => '发送广播'; + String get settings_sendAdvertisement => '发送广播'; @override - String get settings_sendAdvertisementSubtitle => '立即发送广播'; + String get settings_sendAdvertisementSubtitle => '立即发送广播'; @override - String get settings_advertisementSent => '已发送广播'; + String get settings_advertisementSent => '已发送广播'; @override - String get settings_syncTime => '同步时间'; + String get settings_syncTime => '同步时间'; @override - String get settings_syncTimeSubtitle => - '将设备时钟设置为与手机时间一致'; + String get settings_syncTimeSubtitle => '将设备时钟设置为与手机时间一致'; @override - String get settings_timeSynchronized => '时间已同步'; + String get settings_timeSynchronized => '时间已同步'; @override - String get settings_refreshContacts => '刷新联系人'; + String get settings_refreshContacts => '刷新联系人'; @override - String get settings_refreshContactsSubtitle => - '从设备重新加载联系人列表'; + String get settings_refreshContactsSubtitle => '从设备重新加载联系人列表'; @override - String get settings_rebootDevice => '重启设备'; + String get settings_rebootDevice => '重启设备'; @override - String get settings_rebootDeviceSubtitle => '重启 MeshCore 设备'; + String get settings_rebootDeviceSubtitle => '重启 MeshCore 设备'; @override - String get settings_rebootDeviceConfirm => - '确定要重启设备吗?这将断开与设备的连接。'; + String get settings_rebootDeviceConfirm => '确定要重启设备吗?这将断开与设备的连接。'; @override - String get settings_debug => '调试'; + String get settings_debug => '调试'; @override - String get settings_bleDebugLog => 'BLE 调试日志'; + String get settings_bleDebugLog => 'BLE 调试日志'; @override - String get settings_bleDebugLogSubtitle => - 'BLE 命令、响应和原始数据'; + String get settings_bleDebugLogSubtitle => 'BLE 命令、响应和原始数据'; @override - String get settings_appDebugLog => '应用调试日志'; + String get settings_appDebugLog => '应用调试日志'; @override - String get settings_appDebugLogSubtitle => '应用调试消息'; + String get settings_appDebugLogSubtitle => '应用调试消息'; @override - String get settings_about => '关于'; + String get settings_about => '关于'; @override String settings_aboutVersion(String version) { @@ -340,1180 +365,1146 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get settings_aboutLegalese => '2026 MeshCore 开源项目'; + String get settings_aboutLegalese => '2026 MeshCore 开源项目'; @override String get settings_aboutDescription => - '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; + '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; @override String get settings_aboutOpenMeteoAttribution => - 'LOS 高程数据:Open-Meteo (CC BY 4.0)'; + 'LOS 高程数据:Open-Meteo (CC BY 4.0)'; @override - String get settings_infoName => '名称'; + String get settings_infoName => '名称'; @override String get settings_infoId => 'MAC ID'; @override - String get settings_infoStatus => '状态'; + String get settings_infoStatus => '状态'; @override - String get settings_infoBattery => '电池'; + String get settings_infoBattery => '电池'; @override - String get settings_infoPublicKey => '公钥'; + String get settings_infoPublicKey => '公钥'; @override - String get settings_infoContactsCount => '联系人数量'; + String get settings_infoContactsCount => '联系人数量'; @override - String get settings_infoChannelCount => '频道数量'; + String get settings_infoChannelCount => '频道数量'; @override - String get settings_presets => '预设'; + String get settings_presets => '预设'; @override - String get settings_frequency => '频率 (MHz)'; + String get settings_frequency => '频率 (MHz)'; @override String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => - '无效频率范围(300-2500 MHz)'; + String get settings_frequencyInvalid => '无效频率范围(300-2500 MHz)'; @override - String get settings_bandwidth => '带宽'; + String get settings_bandwidth => '带宽'; @override - String get settings_spreadingFactor => '扩频因子'; + String get settings_spreadingFactor => '扩频因子'; @override - String get settings_codingRate => '编码速率'; + String get settings_codingRate => '编码速率'; @override - String get settings_txPower => 'TX 功率 (dBm)'; + String get settings_txPower => 'TX 功率 (dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; + String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; @override - String get settings_clientRepeat => '离网重复'; + String get settings_clientRepeat => '离网重复'; @override - String get settings_clientRepeatSubtitle => - '允许此设备重复发送网状数据包给其他设备'; + String get settings_clientRepeatSubtitle => '允许此设备重复发送网状数据包给其他设备'; @override String get settings_clientRepeatFreqWarning => - '离网重复通信需要使用 433、869 或 918 兆赫兹的频率。'; + '离网重复通信需要使用 433、869 或 918 兆赫兹的频率。'; @override String settings_error(String message) { - return '错误:$message'; + return '错误:$message'; } @override - String get appSettings_title => '应用设置'; + String get appSettings_title => '应用设置'; @override - String get appSettings_appearance => '外观'; + String get appSettings_appearance => '外观'; @override - String get appSettings_theme => '主题'; + String get appSettings_theme => '主题'; @override - String get appSettings_themeSystem => '跟随系统'; + String get appSettings_themeSystem => '跟随系统'; @override - String get appSettings_themeLight => '浅色'; + String get appSettings_themeLight => '浅色'; @override - String get appSettings_themeDark => '深色'; + String get appSettings_themeDark => '深色'; @override - String get appSettings_language => '语言'; + String get appSettings_language => '语言'; @override - String get appSettings_languageSystem => '跟随系统'; + String get appSettings_languageSystem => '跟随系统'; @override - String get appSettings_languageEn => '英语'; + String get appSettings_languageEn => '英语'; @override - String get appSettings_languageFr => '法语'; + String get appSettings_languageFr => '法语'; @override - String get appSettings_languageEs => '西班牙语'; + String get appSettings_languageEs => '西班牙语'; @override - String get appSettings_languageDe => '德语'; + String get appSettings_languageDe => '德语'; @override - String get appSettings_languagePl => '波兰语'; + String get appSettings_languagePl => '波兰语'; @override - String get appSettings_languageSl => '斯洛文尼亚语'; + String get appSettings_languageSl => '斯洛文尼亚语'; @override - String get appSettings_languagePt => '葡萄牙语'; + String get appSettings_languagePt => '葡萄牙语'; @override - String get appSettings_languageIt => '意大利语'; + String get appSettings_languageIt => '意大利语'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override - String get appSettings_languageSv => '瑞典语'; + String get appSettings_languageSv => '瑞典语'; @override - String get appSettings_languageNl => '荷兰语'; + String get appSettings_languageNl => '荷兰语'; @override - String get appSettings_languageSk => '斯洛伐克语'; + String get appSettings_languageSk => '斯洛伐克语'; @override - String get appSettings_languageBg => '保加利亚语'; + String get appSettings_languageBg => '保加利亚语'; @override - String get appSettings_languageRu => '俄语'; + String get appSettings_languageRu => '俄语'; @override - String get appSettings_languageUk => '乌克兰语'; + String get appSettings_languageUk => '乌克兰语'; @override - String get appSettings_enableMessageTracing => '启用消息追踪'; + String get appSettings_enableMessageTracing => '启用消息追踪'; @override - String get appSettings_enableMessageTracingSubtitle => - '显示消息的详细路由和时间元数据'; + String get appSettings_enableMessageTracingSubtitle => '显示消息的详细路由和时间元数据'; @override - String get appSettings_notifications => '通知'; + String get appSettings_notifications => '通知'; @override - String get appSettings_enableNotifications => '启用通知'; + String get appSettings_enableNotifications => '启用通知'; @override - String get appSettings_enableNotificationsSubtitle => - '接收消息和广播的通知'; + String get appSettings_enableNotificationsSubtitle => '接收消息和广播的通知'; @override - String get appSettings_notificationPermissionDenied => '权限被拒绝'; + String get appSettings_notificationPermissionDenied => '权限被拒绝'; @override - String get appSettings_notificationsEnabled => '通知已启用'; + String get appSettings_notificationsEnabled => '通知已启用'; @override - String get appSettings_notificationsDisabled => '通知已关闭'; + String get appSettings_notificationsDisabled => '通知已关闭'; @override - String get appSettings_messageNotifications => '消息通知'; + String get appSettings_messageNotifications => '消息通知'; @override - String get appSettings_messageNotificationsSubtitle => - '收到新消息时显示通知'; + String get appSettings_messageNotificationsSubtitle => '收到新消息时显示通知'; @override - String get appSettings_channelMessageNotifications => '频道消息通知'; + String get appSettings_channelMessageNotifications => '频道消息通知'; @override - String get appSettings_channelMessageNotificationsSubtitle => - '收到频道消息时显示通知'; + String get appSettings_channelMessageNotificationsSubtitle => '收到频道消息时显示通知'; @override - String get appSettings_advertisementNotifications => '广播通知'; + String get appSettings_advertisementNotifications => '广播通知'; @override - String get appSettings_advertisementNotificationsSubtitle => - '发现新节点时显示通知'; + String get appSettings_advertisementNotificationsSubtitle => '发现新节点时显示通知'; @override - String get appSettings_messaging => '消息'; + String get appSettings_messaging => '消息'; @override - String get appSettings_clearPathOnMaxRetry => - '达到最大重试次数时清除路径'; + String get appSettings_clearPathOnMaxRetry => '达到最大重试次数时清除路径'; @override - String get appSettings_clearPathOnMaxRetrySubtitle => - '在5次发送失败后重置联系路径。'; + String get appSettings_clearPathOnMaxRetrySubtitle => '在5次发送失败后重置联系路径。'; @override - String get appSettings_pathsWillBeCleared => '5次失败后将重新路由'; + String get appSettings_pathsWillBeCleared => '5次失败后将重新路由'; @override - String get appSettings_pathsWillNotBeCleared => '路径不会自动清除'; + String get appSettings_pathsWillNotBeCleared => '路径不会自动清除'; @override - String get appSettings_autoRouteRotation => '自动路径轮换'; + String get appSettings_autoRouteRotation => '自动路径轮换'; @override - String get appSettings_autoRouteRotationSubtitle => - '在最佳路径和泛洪模式之间切换'; + String get appSettings_autoRouteRotationSubtitle => '在最佳路径和泛洪模式之间切换'; @override - String get appSettings_autoRouteRotationEnabled => - '自动路径轮换已启用'; + String get appSettings_autoRouteRotationEnabled => '自动路径轮换已启用'; @override - String get appSettings_autoRouteRotationDisabled => - '自动路径轮换已禁用'; + String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用'; @override - String get appSettings_battery => '电池'; + String get appSettings_battery => '电池'; @override - String get appSettings_batteryChemistry => '电池类型'; + String get appSettings_batteryChemistry => '电池类型'; @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return '为每个设备设置 ($deviceName)'; + return '为每个设备设置 ($deviceName)'; } @override - String get appSettings_batteryChemistryConnectFirst => '请先连接设备'; + String get appSettings_batteryChemistryConnectFirst => '请先连接设备'; @override - String get appSettings_batteryNmc => '18650 NMC 电池 (3.0-4.2V)'; + String get appSettings_batteryNmc => '18650 NMC 电池 (3.0-4.2V)'; @override - String get appSettings_batteryLifepo4 => '磷酸铁锂 (2.6-3.65V)'; + String get appSettings_batteryLifepo4 => '磷酸铁锂 (2.6-3.65V)'; @override - String get appSettings_batteryLipo => '锂聚合物电池 (3.0-4.2V)'; + String get appSettings_batteryLipo => '锂聚合物电池 (3.0-4.2V)'; @override - String get appSettings_mapDisplay => '地图显示'; + String get appSettings_mapDisplay => '地图显示'; @override - String get appSettings_showRepeaters => '显示转发节点'; + String get appSettings_showRepeaters => '显示转发节点'; @override - String get appSettings_showRepeatersSubtitle => - '在地图上显示转发节点'; + String get appSettings_showRepeatersSubtitle => '在地图上显示转发节点'; @override - String get appSettings_showChatNodes => '显示聊天节点'; + String get appSettings_showChatNodes => '显示聊天节点'; @override - String get appSettings_showChatNodesSubtitle => - '在地图上显示聊天节点'; + String get appSettings_showChatNodesSubtitle => '在地图上显示聊天节点'; @override - String get appSettings_showOtherNodes => '显示其他节点'; + String get appSettings_showOtherNodes => '显示其他节点'; @override - String get appSettings_showOtherNodesSubtitle => - '在地图上显示其他节点类型'; + String get appSettings_showOtherNodesSubtitle => '在地图上显示其他节点类型'; @override - String get appSettings_timeFilter => '时间过滤器'; + String get appSettings_timeFilter => '时间过滤器'; @override - String get appSettings_timeFilterShowAll => '显示所有节点'; + String get appSettings_timeFilterShowAll => '显示所有节点'; @override String appSettings_timeFilterShowLast(int hours) { - return '显示过去 $hours 小时内的节点'; + return '显示过去 $hours 小时内的节点'; } @override - String get appSettings_mapTimeFilter => '地图时间筛选'; + String get appSettings_mapTimeFilter => '地图时间筛选'; @override - String get appSettings_showNodesDiscoveredWithin => - '显示在此时间段内发现的节点:'; + String get appSettings_showNodesDiscoveredWithin => '显示在此时间段内发现的节点:'; @override - String get appSettings_allTime => '所有时间'; + String get appSettings_allTime => '所有时间'; @override - String get appSettings_lastHour => '过去一小时'; + String get appSettings_lastHour => '过去一小时'; @override - String get appSettings_last6Hours => '过去6小时'; + String get appSettings_last6Hours => '过去6小时'; @override - String get appSettings_last24Hours => '过去24小时'; + String get appSettings_last24Hours => '过去24小时'; @override - String get appSettings_lastWeek => '上周'; + String get appSettings_lastWeek => '上周'; @override - String get appSettings_offlineMapCache => '离线地图缓存'; + String get appSettings_offlineMapCache => '离线地图缓存'; @override - String get appSettings_unitsTitle => '单位'; + String get appSettings_unitsTitle => '单位'; @override - String get appSettings_unitsMetric => '公制(米/公里)'; + String get appSettings_unitsMetric => '公制(米/公里)'; @override - String get appSettings_unitsImperial => '英制 (ft / mi)'; + String get appSettings_unitsImperial => '英制 (ft / mi)'; @override - String get appSettings_noAreaSelected => '未选择任何区域'; + String get appSettings_noAreaSelected => '未选择任何区域'; @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return '已选择区域(缩放 $minZoom - $maxZoom)'; + return '已选择区域(缩放 $minZoom - $maxZoom)'; } @override - String get appSettings_debugCard => '调试'; + String get appSettings_debugCard => '调试'; @override - String get appSettings_appDebugLogging => '应用调试日志'; + String get appSettings_appDebugLogging => '应用调试日志'; @override - String get appSettings_appDebugLoggingSubtitle => - '记录应用调试消息以进行故障排除。'; + String get appSettings_appDebugLoggingSubtitle => '记录应用调试消息以进行故障排除。'; @override - String get appSettings_appDebugLoggingEnabled => '调试日志已启用'; + String get appSettings_appDebugLoggingEnabled => '调试日志已启用'; @override - String get appSettings_appDebugLoggingDisabled => - '应用调试日志已禁用'; + String get appSettings_appDebugLoggingDisabled => '应用调试日志已禁用'; @override - String get contacts_title => '联系人'; + String get contacts_title => '联系人'; @override - String get contacts_noContacts => '暂无联系人'; + String get contacts_noContacts => '暂无联系人'; @override - String get contacts_contactsWillAppear => - '当设备发送广播时,联系人将显示。'; + String get contacts_contactsWillAppear => '当设备发送广播时,联系人将显示。'; @override - String get contacts_unread => '未读'; + String get contacts_unread => '未读'; @override - String get contacts_searchContactsNoNumber => '搜索联系人...'; + String get contacts_searchContactsNoNumber => '搜索联系人...'; @override String contacts_searchContacts(int number, String str) { - return '搜索联系人...'; + return '搜索联系人...'; } @override String contacts_searchFavorites(int number, String str) { - return '搜索 $number$str 收藏...'; + return '搜索 $number$str 收藏...'; } @override String contacts_searchUsers(int number, String str) { - return '搜索 $number$str 位用户...'; + return '搜索 $number$str 位用户...'; } @override String contacts_searchRepeaters(int number, String str) { - return '搜索 $number$str 重复器...'; + return '搜索 $number$str 重复器...'; } @override String contacts_searchRoomServers(int number, String str) { - return '搜索 $number$str 房间服务器...'; + return '搜索 $number$str 房间服务器...'; } @override - String get contacts_noUnreadContacts => '没有未读内容'; + String get contacts_noUnreadContacts => '没有未读内容'; @override - String get contacts_noContactsFound => '未找到任何联系人或群聊'; + String get contacts_noContactsFound => '未找到任何联系人或群聊'; @override - String get contacts_deleteContact => '删除联系人'; + String get contacts_deleteContact => '删除联系人'; @override String contacts_removeConfirm(String contactName) { - return '从联系人中移除 $contactName?'; + return '从联系人中移除 $contactName?'; } @override - String get contacts_manageRepeater => '管理转发节点'; + String get contacts_manageRepeater => '管理转发节点'; @override - String get contacts_manageRoom => '管理房间服务器'; + String get contacts_manageRoom => '管理房间服务器'; @override - String get contacts_roomLogin => '服务器登录'; + String get contacts_roomLogin => '服务器登录'; @override - String get contacts_openChat => '打开聊天'; + String get contacts_openChat => '打开聊天'; @override - String get contacts_editGroup => '编辑群聊'; + String get contacts_editGroup => '编辑群聊'; @override - String get contacts_deleteGroup => '删除群聊'; + String get contacts_deleteGroup => '删除群聊'; @override String contacts_deleteGroupConfirm(String groupName) { - return '删除群聊 \"$groupName\"?'; + return '删除群聊 \"$groupName\"?'; } @override - String get contacts_newGroup => '新建群聊'; + String get contacts_newGroup => '新建群聊'; @override - String get contacts_groupName => '群聊名称'; + String get contacts_groupName => '群聊名称'; @override - String get contacts_groupNameRequired => '请输入群聊名称'; + String get contacts_groupNameRequired => '请输入群聊名称'; @override String contacts_groupAlreadyExists(String name) { - return '名为 \"$name\" 的群聊已存在'; + return '名为 \"$name\" 的群聊已存在'; } @override - String get contacts_filterContacts => '筛选联系人...'; + String get contacts_filterContacts => '筛选联系人...'; @override - String get contacts_noContactsMatchFilter => '没有符合条件的联系人'; + String get contacts_noContactsMatchFilter => '没有符合条件的联系人'; @override - String get contacts_noMembers => '暂无成员'; + String get contacts_noMembers => '暂无成员'; @override - String get contacts_lastSeenNow => '刚刚'; + String get contacts_lastSeenNow => '刚刚'; @override String contacts_lastSeenMinsAgo(int minutes) { - return '最后在线 $minutes 分钟前'; + return '最后在线 $minutes 分钟前'; } @override - String get contacts_lastSeenHourAgo => '最后在线 1小时前'; + String get contacts_lastSeenHourAgo => '最后在线 1小时前'; @override String contacts_lastSeenHoursAgo(int hours) { - return '最后在线 $hours 小时前'; + return '最后在线 $hours 小时前'; } @override - String get contacts_lastSeenDayAgo => '最后在线 1天前'; + String get contacts_lastSeenDayAgo => '最后在线 1天前'; @override String contacts_lastSeenDaysAgo(int days) { - return '最后在线 $days 天前'; + return '最后在线 $days 天前'; } @override - String get channels_title => '频道'; + String get channels_title => '频道'; @override - String get channels_noChannelsConfigured => '未配置任何频道'; + String get channels_noChannelsConfigured => '未配置任何频道'; @override - String get channels_addPublicChannel => '添加公共频道'; + String get channels_addPublicChannel => '添加公共频道'; @override - String get channels_searchChannels => '搜索频道...'; + String get channels_searchChannels => '搜索频道...'; @override - String get channels_noChannelsFound => '未找到任何频道'; + String get channels_noChannelsFound => '未找到任何频道'; @override String channels_channelIndex(int index) { - return '频道 $index'; + return '频道 $index'; } @override - String get channels_hashtagChannel => '标签频道'; + String get channels_hashtagChannel => '标签频道'; @override - String get channels_public => '公共'; + String get channels_public => '公共'; @override - String get channels_private => '私有'; + String get channels_private => '私有'; @override - String get channels_publicChannel => '公共频道'; + String get channels_publicChannel => '公共频道'; @override - String get channels_privateChannel => '私有频道'; + String get channels_privateChannel => '私有频道'; @override - String get channels_editChannel => '编辑频道'; + String get channels_editChannel => '编辑频道'; @override - String get channels_muteChannel => '静音频道'; + String get channels_muteChannel => '静音频道'; @override - String get channels_unmuteChannel => '取消静音频道'; + String get channels_unmuteChannel => '取消静音频道'; @override - String get channels_deleteChannel => '删除频道'; + String get channels_deleteChannel => '删除频道'; @override String channels_deleteChannelConfirm(String name) { - return '删除频道 \"$name\"?此操作不可撤销。'; + return '删除频道 \"$name\"?此操作不可撤销。'; } @override String channels_channelDeleteFailed(String name) { - return '无法删除频道 \"$name\"'; + return '无法删除频道 \"$name\"'; } @override String channels_channelDeleted(String name) { - return '已删除频道 \"$name\"'; + return '已删除频道 \"$name\"'; } @override - String get channels_addChannel => '添加频道'; + String get channels_addChannel => '添加频道'; @override - String get channels_channelIndexLabel => '频道索引'; + String get channels_channelIndexLabel => '频道索引'; @override - String get channels_channelName => '频道名称'; + String get channels_channelName => '频道名称'; @override - String get channels_usePublicChannel => '使用公共频道'; + String get channels_usePublicChannel => '使用公共频道'; @override - String get channels_standardPublicPsk => '标准公共 PSK'; + String get channels_standardPublicPsk => '标准公共 PSK'; @override - String get channels_pskHex => 'PSK (十六进制)'; + String get channels_pskHex => 'PSK (十六进制)'; @override - String get channels_generateRandomPsk => '生成随机 PSK'; + String get channels_generateRandomPsk => '生成随机 PSK'; @override - String get channels_enterChannelName => '请输入频道名称'; + String get channels_enterChannelName => '请输入频道名称'; @override - String get channels_pskMustBe32Hex => - 'PSK 必须为 32 个十六进制字符'; + String get channels_pskMustBe32Hex => 'PSK 必须为 32 个十六进制字符'; @override String channels_channelAdded(String name) { - return '已添加频道 \"$name\"'; + return '已添加频道 \"$name\"'; } @override String channels_editChannelTitle(int index) { - return '编辑频道 $index'; + return '编辑频道 $index'; } @override - String get channels_smazCompression => 'SMAZ 压缩'; + String get channels_smazCompression => 'SMAZ 压缩'; @override String channels_channelUpdated(String name) { - return '频道 \"$name\" 已更新'; + return '频道 \"$name\" 已更新'; } @override - String get channels_publicChannelAdded => '已添加公共频道'; + String get channels_publicChannelAdded => '已添加公共频道'; @override - String get channels_sortBy => '排序方式'; + String get channels_sortBy => '排序方式'; @override - String get channels_sortManual => '手动'; + String get channels_sortManual => '手动'; @override String get channels_sortAZ => 'A-Z'; @override - String get channels_sortLatestMessages => '最新消息'; + String get channels_sortLatestMessages => '最新消息'; @override - String get channels_sortUnread => '未读'; + String get channels_sortUnread => '未读'; @override - String get channels_createPrivateChannel => '创建私有频道'; + String get channels_createPrivateChannel => '创建私有频道'; @override - String get channels_createPrivateChannelDesc => '使用密钥保护。'; + String get channels_createPrivateChannelDesc => '使用密钥保护。'; @override - String get channels_joinPrivateChannel => '加入私有频道'; + String get channels_joinPrivateChannel => '加入私有频道'; @override - String get channels_joinPrivateChannelDesc => '手动输入密钥。'; + String get channels_joinPrivateChannelDesc => '手动输入密钥。'; @override - String get channels_joinPublicChannel => '加入公共频道'; + String get channels_joinPublicChannel => '加入公共频道'; @override - String get channels_joinPublicChannelDesc => '任何人都可以加入。'; + String get channels_joinPublicChannelDesc => '任何人都可以加入。'; @override - String get channels_joinHashtagChannel => '加入标签频道'; + String get channels_joinHashtagChannel => '加入标签频道'; @override - String get channels_joinHashtagChannelDesc => - '任何人都可以加入标签频道。'; + String get channels_joinHashtagChannelDesc => '任何人都可以加入标签频道。'; @override - String get channels_scanQrCode => '扫描二维码'; + String get channels_scanQrCode => '扫描二维码'; @override - String get channels_scanQrCodeComingSoon => '即将推出'; + String get channels_scanQrCodeComingSoon => '即将推出'; @override - String get channels_enterHashtag => '输入标签'; + String get channels_enterHashtag => '输入标签'; @override - String get channels_hashtagHint => '例如:#团队'; + String get channels_hashtagHint => '例如:#团队'; @override - String get chat_noMessages => '暂无消息'; + String get chat_noMessages => '暂无消息'; @override - String get chat_sendMessageToStart => '发送消息开始对话'; + String get chat_sendMessageToStart => '发送消息开始对话'; @override - String get chat_originalMessageNotFound => '找不到原始消息'; + String get chat_originalMessageNotFound => '找不到原始消息'; @override String chat_replyingTo(String name) { - return '正在回复 $name'; + return '正在回复 $name'; } @override String chat_replyTo(String name) { - return '回复 $name'; + return '回复 $name'; } @override - String get chat_location => '位置'; + String get chat_location => '位置'; @override String chat_sendMessageTo(String contactName) { - return '发送消息给 $contactName'; + return '发送消息给 $contactName'; } @override - String get chat_typeMessage => '输入消息...'; + String get chat_typeMessage => '输入消息...'; @override String chat_messageTooLong(int maxBytes) { - return '消息过长(最多 $maxBytes 字节)'; + return '消息过长(最多 $maxBytes 字节)'; } @override - String get chat_messageCopied => '消息已复制'; + String get chat_messageCopied => '消息已复制'; @override - String get chat_messageDeleted => '消息已删除'; + String get chat_messageDeleted => '消息已删除'; @override - String get chat_retryingMessage => '正在重试消息'; + String get chat_retryingMessage => '正在重试消息'; @override String chat_retryCount(int current, int max) { - return '重试 $current/$max'; + return '重试 $current/$max'; } @override - String get chat_sendGif => '发送 GIF'; + String get chat_sendGif => '发送 GIF'; @override - String get chat_reply => '回复'; + String get chat_reply => '回复'; @override - String get chat_addReaction => '添加表情'; + String get chat_addReaction => '添加表情'; @override - String get chat_me => '我'; + String get chat_me => '我'; @override - String get emojiCategorySmileys => '表情'; + String get emojiCategorySmileys => '表情'; @override - String get emojiCategoryGestures => '手势'; + String get emojiCategoryGestures => '手势'; @override - String get emojiCategoryHearts => '爱心'; + String get emojiCategoryHearts => '爱心'; @override - String get emojiCategoryObjects => '物品'; + String get emojiCategoryObjects => '物品'; @override - String get gifPicker_title => '选择 GIF'; + String get gifPicker_title => '选择 GIF'; @override - String get gifPicker_searchHint => '搜索 GIF...'; + String get gifPicker_searchHint => '搜索 GIF...'; @override - String get gifPicker_poweredBy => 'ç”± GIPHY 提供'; + String get gifPicker_poweredBy => '由 GIPHY 提供'; @override - String get gifPicker_noGifsFound => '未找到 GIF'; + String get gifPicker_noGifsFound => '未找到 GIF'; @override - String get gifPicker_failedLoad => '加载 GIF 失败'; + String get gifPicker_failedLoad => '加载 GIF 失败'; @override - String get gifPicker_failedSearch => '搜索 GIF 失败'; + String get gifPicker_failedSearch => '搜索 GIF 失败'; @override - String get gifPicker_noInternet => '无网络连接'; + String get gifPicker_noInternet => '无网络连接'; @override - String get debugLog_appTitle => '应用调试日志'; + String get debugLog_appTitle => '应用调试日志'; @override - String get debugLog_bleTitle => 'BLE 调试日志'; + String get debugLog_bleTitle => 'BLE 调试日志'; @override - String get debugLog_copyLog => '复制日志'; + String get debugLog_copyLog => '复制日志'; @override - String get debugLog_clearLog => '清除日志'; + String get debugLog_clearLog => '清除日志'; @override - String get debugLog_copied => '调试日志已复制'; + String get debugLog_copied => '调试日志已复制'; @override - String get debugLog_bleCopied => 'BLE 日志已复制'; + String get debugLog_bleCopied => 'BLE 日志已复制'; @override - String get debugLog_noEntries => '暂无调试日志'; + String get debugLog_noEntries => '暂无调试日志'; @override - String get debugLog_enableInSettings => - '请在设置中启用应用调试日志。'; + String get debugLog_enableInSettings => '请在设置中启用应用调试日志。'; @override - String get debugLog_frames => '帧'; + String get debugLog_frames => '帧'; @override - String get debugLog_rawLogRx => '原始日志 RX'; + String get debugLog_rawLogRx => '原始日志 RX'; @override - String get debugLog_noBleActivity => '暂无 BLE 活动'; + String get debugLog_noBleActivity => '暂无 BLE 活动'; @override String debugFrame_length(int count) { - return '帧长度:$count 字节'; + return '帧长度:$count 字节'; } @override String debugFrame_command(String value) { - return '命令:0x$value'; + return '命令:0x$value'; } @override - String get debugFrame_textMessageHeader => '文本消息:'; + String get debugFrame_textMessageHeader => '文本消息:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- 目标公钥:$pubKey'; + return '- 目标公钥:$pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- 时间戳:$timestamp'; + return '- 时间戳:$timestamp'; } @override String debugFrame_flags(String value) { - return '- 标志:0x$value'; + return '- 标志:0x$value'; } @override String debugFrame_textType(int type, String label) { - return '- 文本类型:$type ($label)'; + return '- 文本类型:$type ($label)'; } @override - String get debugFrame_textTypeCli => '命令行'; + String get debugFrame_textTypeCli => '命令行'; @override - String get debugFrame_textTypePlain => '纯文本'; + String get debugFrame_textTypePlain => '纯文本'; @override String debugFrame_text(String text) { - return '- 文本:“$text”'; + return '- 文本:“$text”'; } @override - String get debugFrame_hexDump => '十六进制数据:'; + String get debugFrame_hexDump => '十六进制数据:'; @override - String get chat_pathManagement => '路径管理'; + String get chat_pathManagement => '路径管理'; @override - String get chat_ShowAllPaths => '显示所有路径'; + String get chat_ShowAllPaths => '显示所有路径'; @override - String get chat_routingMode => '路由模式'; + String get chat_routingMode => '路由模式'; @override - String get chat_autoUseSavedPath => '自动(使用保存的路径)'; + String get chat_autoUseSavedPath => '自动(使用保存的路径)'; @override - String get chat_forceFloodMode => '强制泛洪模式'; + String get chat_forceFloodMode => '强制泛洪模式'; @override - String get chat_recentAckPaths => - '最近使用的 ACK 路径(点击使用):'; + String get chat_recentAckPaths => '最近使用的 ACK 路径(点击使用):'; @override - String get chat_pathHistoryFull => - '路径历史已满,请删除后再添加。'; + String get chat_pathHistoryFull => '路径历史已满,请删除后再添加。'; @override - String get chat_hopSingular => 'è·³'; + String get chat_hopSingular => '跳'; @override - String get chat_hopPlural => 'è·³'; + String get chat_hopPlural => '跳'; @override String chat_hopsCount(int count) { - return '$count è·³'; + return '$count 跳'; } @override - String get chat_successes => '成功'; + String get chat_successes => '成功'; @override - String get chat_removePath => '移除路径'; + String get chat_removePath => '移除路径'; @override - String get chat_noPathHistoryYet => - '暂无路径历史。\n发送消息以探索路径。'; + String get chat_noPathHistoryYet => '暂无路径历史。\n发送消息以探索路径。'; @override - String get chat_pathActions => '路径操作:'; + String get chat_pathActions => '路径操作:'; @override - String get chat_setCustomPath => '设置自定义路径'; + String get chat_setCustomPath => '设置自定义路径'; @override - String get chat_setCustomPathSubtitle => '手动指定路由路径'; + String get chat_setCustomPathSubtitle => '手动指定路由路径'; @override - String get chat_clearPath => '清除路径'; + String get chat_clearPath => '清除路径'; @override - String get chat_clearPathSubtitle => - '清除当前路径,下次发送将重新尝试。'; + String get chat_clearPathSubtitle => '清除当前路径,下次发送将重新尝试。'; @override - String get chat_pathCleared => - '路径已清除。下一条消息将重新路由。'; + String get chat_pathCleared => '路径已清除。下一条消息将重新路由。'; @override - String get chat_floodModeSubtitle => '在应用栏中切换路由模式。'; + String get chat_floodModeSubtitle => '在应用栏中切换路由模式。'; @override - String get chat_floodModeEnabled => - '泛洪模式已启用。可通过应用栏的路由图标切换。'; + String get chat_floodModeEnabled => '泛洪模式已启用。可通过应用栏的路由图标切换。'; @override - String get chat_fullPath => '完整路径'; + String get chat_fullPath => '完整路径'; @override - String get chat_pathDetailsNotAvailable => - '路径信息暂不可用,请尝试发送消息刷新。'; + String get chat_pathDetailsNotAvailable => '路径信息暂不可用,请尝试发送消息刷新。'; @override String chat_pathSetHops(int hopCount, String status) { - return '路径设置:$hopCount è·³ - $status'; + return '路径设置:$hopCount 跳 - $status'; } @override - String get chat_pathSavedLocally => - '已本地保存,连接设备后可同步。'; + String get chat_pathSavedLocally => '已本地保存,连接设备后可同步。'; @override - String get chat_pathDeviceConfirmed => '设备已确认。'; + String get chat_pathDeviceConfirmed => '设备已确认。'; @override - String get chat_pathDeviceNotConfirmed => '设备尚未确认。'; + String get chat_pathDeviceNotConfirmed => '设备尚未确认。'; @override - String get chat_type => '类型'; + String get chat_type => '类型'; @override - String get chat_path => '路径'; + String get chat_path => '路径'; @override - String get chat_publicKey => '公钥'; + String get chat_publicKey => '公钥'; @override - String get chat_compressOutgoingMessages => '压缩发送的消息'; + String get chat_compressOutgoingMessages => '压缩发送的消息'; @override - String get chat_floodForced => '泛洪(强制)'; + String get chat_floodForced => '泛洪(强制)'; @override - String get chat_directForced => '直连(强制)'; + String get chat_directForced => '直连(强制)'; @override String chat_hopsForced(int count) { - return '$count 跳(强制)'; + return '$count 跳(强制)'; } @override - String get chat_floodAuto => '自动泛洪'; + String get chat_floodAuto => '自动泛洪'; @override - String get chat_direct => '直连'; + String get chat_direct => '直连'; @override - String get chat_poiShared => '共享位置'; + String get chat_poiShared => '共享位置'; @override String chat_unread(int count) { - return '未读:$count'; + return '未读:$count'; } @override - String get chat_openLink => '打开链接?'; + String get chat_openLink => '打开链接?'; @override - String get chat_openLinkConfirmation => - '是否使用浏览器打开此链接?'; + String get chat_openLinkConfirmation => '是否使用浏览器打开此链接?'; @override - String get chat_open => '打开'; + String get chat_open => '打开'; @override String chat_couldNotOpenLink(String url) { - return '无法打开链接:$url'; + return '无法打开链接:$url'; } @override - String get chat_invalidLink => '无效的链接格式'; + String get chat_invalidLink => '无效的链接格式'; @override - String get map_title => '节点地图'; + String get map_title => '节点地图'; @override - String get map_lineOfSight => '视线'; + String get map_lineOfSight => '视线'; @override - String get map_losScreenTitle => '视线'; + String get map_losScreenTitle => '视线'; @override - String get map_noNodesWithLocation => '没有包含位置信息的节点'; + String get map_noNodesWithLocation => '没有包含位置信息的节点'; @override - String get map_nodesNeedGps => - '节点需要共享 GPS 坐标才能在地图上显示'; + String get map_nodesNeedGps => '节点需要共享 GPS 坐标才能在地图上显示'; @override String map_nodesCount(int count) { - return '节点:$count'; + return '节点:$count'; } @override String map_pinsCount(int count) { - return '标记:$count'; + return '标记:$count'; } @override - String get map_chat => '聊天'; + String get map_chat => '聊天'; @override - String get map_repeater => '转发节点'; + String get map_repeater => '转发节点'; @override - String get map_room => '房间'; + String get map_room => '房间'; @override - String get map_sensor => '传感器'; + String get map_sensor => '传感器'; @override - String get map_pinDm => '标记(私信)'; + String get map_pinDm => '标记(私信)'; @override - String get map_pinPrivate => '私有'; + String get map_pinPrivate => '私有'; @override - String get map_pinPublic => '公共'; + String get map_pinPublic => '公共'; @override - String get map_lastSeen => '最后在线'; + String get map_lastSeen => '最后在线'; @override - String get map_disconnectConfirm => - '确定要断开与此设备的连接吗?'; + String get map_disconnectConfirm => '确定要断开与此设备的连接吗?'; @override - String get map_from => '来自'; + String get map_from => '来自'; @override - String get map_source => '来源'; + String get map_source => '来源'; @override - String get map_flags => '标志'; + String get map_flags => '标志'; @override - String get map_shareMarkerHere => '在此分享标记'; + String get map_shareMarkerHere => '在此分享标记'; @override - String get map_pinLabel => '标签'; + String get map_pinLabel => '标签'; @override - String get map_label => '标签'; + String get map_label => '标签'; @override - String get map_pointOfInterest => '兴趣点'; + String get map_pointOfInterest => '兴趣点'; @override - String get map_sendToContact => '发送给联系人'; + String get map_sendToContact => '发送给联系人'; @override - String get map_sendToChannel => '发送到频道'; + String get map_sendToChannel => '发送到频道'; @override - String get map_noChannelsAvailable => '没有可用的频道'; + String get map_noChannelsAvailable => '没有可用的频道'; @override - String get map_publicLocationShare => '公共位置共享'; + String get map_publicLocationShare => '公共位置共享'; @override String map_publicLocationShareConfirm(String channelLabel) { - return '您即将在 $channelLabel 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。'; + return '您即将在 $channelLabel 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。'; } @override - String get map_connectToShareMarkers => '连接设备以共享标记'; + String get map_connectToShareMarkers => '连接设备以共享标记'; @override - String get map_filterNodes => '过滤节点'; + String get map_filterNodes => '过滤节点'; @override - String get map_nodeTypes => '节点类型'; + String get map_nodeTypes => '节点类型'; @override - String get map_chatNodes => '聊天节点'; + String get map_chatNodes => '聊天节点'; @override - String get map_repeaters => '转发节点'; + String get map_repeaters => '转发节点'; @override - String get map_otherNodes => '其他节点'; + String get map_otherNodes => '其他节点'; @override - String get map_keyPrefix => '关键字前缀'; + String get map_keyPrefix => '关键字前缀'; @override - String get map_filterByKeyPrefix => '按关键字前缀筛选'; + String get map_filterByKeyPrefix => '按关键字前缀筛选'; @override - String get map_publicKeyPrefix => '关键字前缀'; + String get map_publicKeyPrefix => '关键字前缀'; @override - String get map_markers => '标记'; + String get map_markers => '标记'; @override - String get map_showSharedMarkers => '显示共享标记'; + String get map_showSharedMarkers => '显示共享标记'; @override - String get map_lastSeenTime => '最后在线时间'; + String get map_lastSeenTime => '最后在线时间'; @override - String get map_sharedPin => '共享标记'; + String get map_sharedPin => '共享标记'; @override - String get map_joinRoom => '加入房间'; + String get map_joinRoom => '加入房间'; @override - String get map_manageRepeater => '管理转发节点'; + String get map_manageRepeater => '管理转发节点'; @override - String get map_tapToAdd => '点击节点以添加到路径'; + String get map_tapToAdd => '点击节点以添加到路径'; @override - String get map_runTrace => '运行路径追踪'; + String get map_runTrace => '运行路径追踪'; @override - String get map_removeLast => '移除最后一个'; + String get map_removeLast => '移除最后一个'; @override - String get map_pathTraceCancelled => '路径追踪已取消'; + String get map_pathTraceCancelled => '路径追踪已取消'; @override - String get mapCache_title => '离线地图缓存'; + String get mapCache_title => '离线地图缓存'; @override - String get mapCache_selectAreaFirst => '请先选择要缓存的区域'; + String get mapCache_selectAreaFirst => '请先选择要缓存的区域'; @override - String get mapCache_noTilesToDownload => '此区域没有可下载的瓦片'; + String get mapCache_noTilesToDownload => '此区域没有可下载的瓦片'; @override - String get mapCache_downloadTilesTitle => '下载瓦片'; + String get mapCache_downloadTilesTitle => '下载瓦片'; @override String mapCache_downloadTilesPrompt(int count) { - return '这需要下载 $count 个瓦片'; + return '这需要下载 $count 个瓦片'; } @override - String get mapCache_downloadAction => '下载'; + String get mapCache_downloadAction => '下载'; @override String mapCache_cachedTiles(int count) { - return '已缓存 $count 个瓦片'; + return '已缓存 $count 个瓦片'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return '已缓存 $downloaded 个瓦片($failed 个失败)'; + return '已缓存 $downloaded 个瓦片($failed 个失败)'; } @override - String get mapCache_clearOfflineCacheTitle => '清除离线缓存'; + String get mapCache_clearOfflineCacheTitle => '清除离线缓存'; @override - String get mapCache_clearOfflineCachePrompt => - '清除所有缓存的地图瓦片'; + String get mapCache_clearOfflineCachePrompt => '清除所有缓存的地图瓦片'; @override - String get mapCache_offlineCacheCleared => '离线缓存已清除'; + String get mapCache_offlineCacheCleared => '离线缓存已清除'; @override - String get mapCache_noAreaSelected => '未选择区域'; + String get mapCache_noAreaSelected => '未选择区域'; @override - String get mapCache_cacheArea => '缓存区域'; + String get mapCache_cacheArea => '缓存区域'; @override - String get mapCache_useCurrentView => '使用当前视图'; + String get mapCache_useCurrentView => '使用当前视图'; @override - String get mapCache_zoomRange => '缩放范围'; + String get mapCache_zoomRange => '缩放范围'; @override String mapCache_estimatedTiles(int count) { - return '估计瓦片数:$count'; + return '估计瓦片数:$count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return '已下载 $completed/$total'; + return '已下载 $completed/$total'; } @override - String get mapCache_downloadTilesButton => '下载瓦片'; + String get mapCache_downloadTilesButton => '下载瓦片'; @override - String get mapCache_clearCacheButton => '清除缓存'; + String get mapCache_clearCacheButton => '清除缓存'; @override String mapCache_failedDownloads(int count) { - return '下载失败:$count'; + return '下载失败:$count'; } @override @@ -1523,296 +1514,284 @@ class AppLocalizationsZh extends AppLocalizations { String east, String west, ) { - return '北 $north, 南 $south, 东 $east, 西 $west'; + return '北 $north, 南 $south, 东 $east, 西 $west'; } @override - String get time_justNow => '刚才'; + String get time_justNow => '刚才'; @override String time_minutesAgo(int minutes) { - return '$minutes分钟前'; + return '$minutes分钟前'; } @override String time_hoursAgo(int hours) { - return '$hours小时前'; + return '$hours小时前'; } @override String time_daysAgo(int days) { - return '$days天前'; + return '$days天前'; } @override - String get time_hour => '小时'; + String get time_hour => '小时'; @override - String get time_hours => '小时'; + String get time_hours => '小时'; @override - String get time_day => '天'; + String get time_day => '天'; @override - String get time_days => '天'; + String get time_days => '天'; @override - String get time_week => '周'; + String get time_week => '周'; @override - String get time_weeks => '周'; + String get time_weeks => '周'; @override - String get time_month => '月'; + String get time_month => '月'; @override - String get time_months => '月'; + String get time_months => '月'; @override - String get time_minutes => '分钟'; + String get time_minutes => '分钟'; @override - String get time_allTime => '所有时间'; + String get time_allTime => '所有时间'; @override - String get dialog_disconnect => 'æ–­å¼€'; + String get dialog_disconnect => '断开'; @override - String get dialog_disconnectConfirm => - '确定要断开与此设备的连接吗?'; + String get dialog_disconnectConfirm => '确定要断开与此设备的连接吗?'; @override - String get login_repeaterLogin => '转发节点登录'; + String get login_repeaterLogin => '转发节点登录'; @override - String get login_roomLogin => '房间服务器登录'; + String get login_roomLogin => '房间服务器登录'; @override - String get login_password => '密码'; + String get login_password => '密码'; @override - String get login_enterPassword => '请输入密码'; + String get login_enterPassword => '请输入密码'; @override - String get login_savePassword => '保存密码'; + String get login_savePassword => '保存密码'; @override - String get login_savePasswordSubtitle => - '密码将安全地存储在此设备上'; + String get login_savePasswordSubtitle => '密码将安全地存储在此设备上'; @override - String get login_repeaterDescription => - '输入转发节点密码以访问设置和状态。'; + String get login_repeaterDescription => '输入转发节点密码以访问设置和状态。'; @override - String get login_roomDescription => - '输入房间服务器密码以访问设置和状态。'; + String get login_roomDescription => '输入房间服务器密码以访问设置和状态。'; @override - String get login_routing => '路由'; + String get login_routing => '路由'; @override - String get login_routingMode => '路由模式'; + String get login_routingMode => '路由模式'; @override - String get login_autoUseSavedPath => '自动(使用保存的路径)'; + String get login_autoUseSavedPath => '自动(使用保存的路径)'; @override - String get login_forceFloodMode => '强制泛洪模式'; + String get login_forceFloodMode => '强制泛洪模式'; @override - String get login_managePaths => '管理路径'; + String get login_managePaths => '管理路径'; @override - String get login_login => '登录'; + String get login_login => '登录'; @override String login_attempt(int current, int max) { - return '尝试 $current/$max'; + return '尝试 $current/$max'; } @override String login_failed(String error) { - return '登录失败:$error'; + return '登录失败:$error'; } @override - String get login_failedMessage => - '登录失败。可能是密码错误或无法连接到服务器。'; + String get login_failedMessage => '登录失败。可能是密码错误或无法连接到服务器。'; @override - String get common_reload => '重新加载'; + String get common_reload => '重新加载'; @override - String get common_clear => '清除'; + String get common_clear => '清除'; @override String path_currentPath(String path) { - return '当前路径:$path'; + return '当前路径:$path'; } @override String path_usingHopsPath(int count) { - return '使用 $count 跳路径'; + return '使用 $count 跳路径'; } @override - String get path_enterCustomPath => '输入自定义路径'; + String get path_enterCustomPath => '输入自定义路径'; @override - String get path_currentPathLabel => '当前路径'; + String get path_currentPathLabel => '当前路径'; @override - String get path_hexPrefixInstructions => - '请输入每个中继节点的2字符十六进制前缀,用逗号分隔。'; + String get path_hexPrefixInstructions => '请输入每个中继节点的2字符十六进制前缀,用逗号分隔。'; @override - String get path_hexPrefixExample => - '例如:A1, F2, 3C(每个节点使用其公钥的第一字节)'; + String get path_hexPrefixExample => '例如:A1, F2, 3C(每个节点使用其公钥的第一字节)'; @override - String get path_labelHexPrefixes => '路径(十六进制前缀)'; + String get path_labelHexPrefixes => '路径(十六进制前缀)'; @override - String get path_helperMaxHops => - '最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。'; + String get path_helperMaxHops => '最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。'; @override - String get path_selectFromContacts => '或从联系人列表中选择:'; + String get path_selectFromContacts => '或从联系人列表中选择:'; @override - String get path_noRepeatersFound => - '未找到任何转发节点或房间服务器。'; + String get path_noRepeatersFound => '未找到任何转发节点或房间服务器。'; @override - String get path_customPathsRequire => - '自定义路径需要中间节点转发消息。'; + String get path_customPathsRequire => '自定义路径需要中间节点转发消息。'; @override String path_invalidHexPrefixes(String prefixes) { - return '无效的十六进制前缀:$prefixes'; + return '无效的十六进制前缀:$prefixes'; } @override - String get path_tooLong => '路径过长,最多允许 64 跳。'; + String get path_tooLong => '路径过长,最多允许 64 跳。'; @override - String get path_setPath => '设置路径'; + String get path_setPath => '设置路径'; @override - String get repeater_management => '转发节点管理'; + String get repeater_management => '转发节点管理'; @override - String get room_management => '房间服务器管理'; + String get room_management => '房间服务器管理'; @override - String get repeater_managementTools => '管理工具'; + String get repeater_managementTools => '管理工具'; @override - String get repeater_status => '状态'; + String get repeater_status => '状态'; @override - String get repeater_statusSubtitle => - '查看转发节点状态、统计和邻居'; + String get repeater_statusSubtitle => '查看转发节点状态、统计和邻居'; @override - String get repeater_telemetry => '遥测'; + String get repeater_telemetry => '遥测'; @override - String get repeater_telemetrySubtitle => - '查看传感器和系统状态数据'; + String get repeater_telemetrySubtitle => '查看传感器和系统状态数据'; @override - String get repeater_cli => '命令行'; + String get repeater_cli => '命令行'; @override - String get repeater_cliSubtitle => '向转发节点发送命令'; + String get repeater_cliSubtitle => '向转发节点发送命令'; @override - String get repeater_neighbors => '邻居'; + String get repeater_neighbors => '邻居'; @override - String get repeater_neighborsSubtitle => '查看邻居节点(零跳)'; + String get repeater_neighborsSubtitle => '查看邻居节点(零跳)'; @override - String get repeater_settings => '设置'; + String get repeater_settings => '设置'; @override - String get repeater_settingsSubtitle => '配置转发节点参数'; + String get repeater_settingsSubtitle => '配置转发节点参数'; @override - String get repeater_statusTitle => '转发节点状态'; + String get repeater_statusTitle => '转发节点状态'; @override - String get repeater_routingMode => '路由模式'; + String get repeater_routingMode => '路由模式'; @override - String get repeater_autoUseSavedPath => '自动(使用保存的路径)'; + String get repeater_autoUseSavedPath => '自动(使用保存的路径)'; @override - String get repeater_forceFloodMode => '强制泛洪模式'; + String get repeater_forceFloodMode => '强制泛洪模式'; @override - String get repeater_pathManagement => '路径管理'; + String get repeater_pathManagement => '路径管理'; @override - String get repeater_refresh => '刷新'; + String get repeater_refresh => '刷新'; @override - String get repeater_statusRequestTimeout => '状态请求超时'; + String get repeater_statusRequestTimeout => '状态请求超时'; @override String repeater_errorLoadingStatus(String error) { - return '加载状态时出错:$error'; + return '加载状态时出错:$error'; } @override - String get repeater_systemInformation => '系统信息'; + String get repeater_systemInformation => '系统信息'; @override - String get repeater_battery => '电池'; + String get repeater_battery => '电池'; @override - String get repeater_clockAtLogin => '登录时的时钟'; + String get repeater_clockAtLogin => '登录时的时钟'; @override - String get repeater_uptime => '运行时间'; + String get repeater_uptime => '运行时间'; @override - String get repeater_queueLength => '队列长度'; + String get repeater_queueLength => '队列长度'; @override - String get repeater_debugFlags => '调试标志'; + String get repeater_debugFlags => '调试标志'; @override - String get repeater_radioStatistics => '无线电统计'; + String get repeater_radioStatistics => '无线电统计'; @override - String get repeater_lastRssi => '上次 RSSI'; + String get repeater_lastRssi => '上次 RSSI'; @override - String get repeater_lastSnr => '上次 SNR'; + String get repeater_lastSnr => '上次 SNR'; @override - String get repeater_noiseFloor => '底噪'; + String get repeater_noiseFloor => '底噪'; @override - String get repeater_txAirtime => '发送空中时间'; + String get repeater_txAirtime => '发送空中时间'; @override - String get repeater_rxAirtime => '接收空中时间'; + String get repeater_rxAirtime => '接收空中时间'; @override - String get repeater_packetStatistics => '数据包统计'; + String get repeater_packetStatistics => '数据包统计'; @override - String get repeater_sent => '发送'; + String get repeater_sent => '发送'; @override - String get repeater_received => '接收'; + String get repeater_received => '接收'; @override - String get repeater_duplicates => '重复'; + String get repeater_duplicates => '重复'; @override String repeater_daysHoursMinsSecs( @@ -1821,550 +1800,511 @@ class AppLocalizationsZh extends AppLocalizations { int minutes, int seconds, ) { - return '$days天 $hours小时 $minutes分 $secondsç§’'; + return '$days天 $hours小时 $minutes分 $seconds秒'; } @override String repeater_packetTxTotal(int total, String flood, String direct) { - return '总计:$total,泛洪:$flood,直连:$direct'; + return '总计:$total,泛洪:$flood,直连:$direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return '总计:$total,泛洪:$flood,直连:$direct'; + return '总计:$total,泛洪:$flood,直连:$direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return '泛洪:$flood,直连:$direct'; + return '泛洪:$flood,直连:$direct'; } @override String repeater_duplicatesTotal(int total) { - return '总计:$total'; + return '总计:$total'; } @override - String get repeater_settingsTitle => '转发节点设置'; + String get repeater_settingsTitle => '转发节点设置'; @override - String get repeater_basicSettings => '基本设置'; + String get repeater_basicSettings => '基本设置'; @override - String get repeater_repeaterName => '转发节点名称'; + String get repeater_repeaterName => '转发节点名称'; @override - String get repeater_repeaterNameHelper => '此转发节点的显示名称'; + String get repeater_repeaterNameHelper => '此转发节点的显示名称'; @override - String get repeater_adminPassword => '管理员密码'; + String get repeater_adminPassword => '管理员密码'; @override - String get repeater_adminPasswordHelper => '完整访问密码'; + String get repeater_adminPasswordHelper => '完整访问密码'; @override - String get repeater_guestPassword => '访客密码'; + String get repeater_guestPassword => '访客密码'; @override - String get repeater_guestPasswordHelper => '只读访问密码'; + String get repeater_guestPasswordHelper => '只读访问密码'; @override - String get repeater_radioSettings => '无线电设置'; + String get repeater_radioSettings => '无线电设置'; @override - String get repeater_frequencyMhz => '频率 (MHz)'; + String get repeater_frequencyMhz => '频率 (MHz)'; @override String get repeater_frequencyHelper => '300-2500 MHz'; @override - String get repeater_txPower => 'TX 功率'; + String get repeater_txPower => 'TX 功率'; @override String get repeater_txPowerHelper => '1-30 dBm'; @override - String get repeater_bandwidth => '带宽'; + String get repeater_bandwidth => '带宽'; @override - String get repeater_spreadingFactor => '扩频因子'; + String get repeater_spreadingFactor => '扩频因子'; @override - String get repeater_codingRate => '编码速率'; + String get repeater_codingRate => '编码速率'; @override - String get repeater_locationSettings => '位置设置'; + String get repeater_locationSettings => '位置设置'; @override - String get repeater_latitude => '纬度'; + String get repeater_latitude => '纬度'; @override - String get repeater_latitudeHelper => '十进制,例如 37.7749'; + String get repeater_latitudeHelper => '十进制,例如 37.7749'; @override - String get repeater_longitude => '经度'; + String get repeater_longitude => '经度'; @override - String get repeater_longitudeHelper => '十进制,例如 -122.4194'; + String get repeater_longitudeHelper => '十进制,例如 -122.4194'; @override - String get repeater_features => '功能'; + String get repeater_features => '功能'; @override - String get repeater_packetForwarding => '数据包转发'; + String get repeater_packetForwarding => '数据包转发'; @override - String get repeater_packetForwardingSubtitle => - '启用转发节点转发数据包'; + String get repeater_packetForwardingSubtitle => '启用转发节点转发数据包'; @override - String get repeater_guestAccess => '访客访问'; + String get repeater_guestAccess => '访客访问'; @override - String get repeater_guestAccessSubtitle => '允许访客只读权限'; + String get repeater_guestAccessSubtitle => '允许访客只读权限'; @override - String get repeater_privacyMode => '隐私模式'; + String get repeater_privacyMode => '隐私模式'; @override - String get repeater_privacyModeSubtitle => '在广播中隐藏姓名/位置'; + String get repeater_privacyModeSubtitle => '在广播中隐藏姓名/位置'; @override - String get repeater_advertisementSettings => '广播设置'; + String get repeater_advertisementSettings => '广播设置'; @override - String get repeater_localAdvertInterval => '本地广播间隔'; + String get repeater_localAdvertInterval => '本地广播间隔'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes 分钟'; + return '$minutes 分钟'; } @override - String get repeater_floodAdvertInterval => '泛洪广播间隔'; + String get repeater_floodAdvertInterval => '泛洪广播间隔'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours 小时'; + return '$hours 小时'; } @override - String get repeater_encryptedAdvertInterval => '加密广播间隔'; + String get repeater_encryptedAdvertInterval => '加密广播间隔'; @override - String get repeater_dangerZone => '危险设置'; + String get repeater_dangerZone => '危险设置'; @override - String get repeater_rebootRepeater => '重启转发节点'; + String get repeater_rebootRepeater => '重启转发节点'; @override - String get repeater_rebootRepeaterSubtitle => '重启转发节点设备'; + String get repeater_rebootRepeaterSubtitle => '重启转发节点设备'; @override - String get repeater_rebootRepeaterConfirm => - '确定要重启此转发节点吗?'; + String get repeater_rebootRepeaterConfirm => '确定要重启此转发节点吗?'; @override - String get repeater_regenerateIdentityKey => '重新生成身份密钥'; + String get repeater_regenerateIdentityKey => '重新生成身份密钥'; @override - String get repeater_regenerateIdentityKeySubtitle => - '生成新的公钥/私钥对'; + String get repeater_regenerateIdentityKeySubtitle => '生成新的公钥/私钥对'; @override - String get repeater_regenerateIdentityKeyConfirm => - '这将为转发节点生成新身份,继续吗?'; + String get repeater_regenerateIdentityKeyConfirm => '这将为转发节点生成新身份,继续吗?'; @override - String get repeater_eraseFileSystem => '擦除文件系统'; + String get repeater_eraseFileSystem => '擦除文件系统'; @override - String get repeater_eraseFileSystemSubtitle => - '格式化转发节点文件系统'; + String get repeater_eraseFileSystemSubtitle => '格式化转发节点文件系统'; @override - String get repeater_eraseFileSystemConfirm => - '警告:此操作将清除转发节点上的所有数据,且无法恢复!'; + String get repeater_eraseFileSystemConfirm => '警告:此操作将清除转发节点上的所有数据,且无法恢复!'; @override - String get repeater_eraseSerialOnly => - '擦除功能仅可通过串行控制台使用。'; + String get repeater_eraseSerialOnly => '擦除功能仅可通过串行控制台使用。'; @override String repeater_commandSent(String command) { - return '命令已发送:$command'; + return '命令已发送:$command'; } @override String repeater_errorSendingCommand(String error) { - return '发送命令时出错:$error'; + return '发送命令时出错:$error'; } @override - String get repeater_confirm => '确认'; + String get repeater_confirm => '确认'; @override - String get repeater_settingsSaved => '设置保存成功'; + String get repeater_settingsSaved => '设置保存成功'; @override String repeater_errorSavingSettings(String error) { - return '保存设置时出错:$error'; + return '保存设置时出错:$error'; } @override - String get repeater_refreshBasicSettings => '刷新基本设置'; + String get repeater_refreshBasicSettings => '刷新基本设置'; @override - String get repeater_refreshRadioSettings => '刷新无线电设置'; + String get repeater_refreshRadioSettings => '刷新无线电设置'; @override - String get repeater_refreshTxPower => '刷新 TX 功率'; + String get repeater_refreshTxPower => '刷新 TX 功率'; @override - String get repeater_refreshLocationSettings => '刷新位置设置'; + String get repeater_refreshLocationSettings => '刷新位置设置'; @override - String get repeater_refreshPacketForwarding => '刷新包转发'; + String get repeater_refreshPacketForwarding => '刷新包转发'; @override - String get repeater_refreshGuestAccess => '刷新访客权限'; + String get repeater_refreshGuestAccess => '刷新访客权限'; @override - String get repeater_refreshPrivacyMode => '刷新隐私模式'; + String get repeater_refreshPrivacyMode => '刷新隐私模式'; @override - String get repeater_refreshAdvertisementSettings => '刷新广播设置'; + String get repeater_refreshAdvertisementSettings => '刷新广播设置'; @override String repeater_refreshed(String label) { - return '$label 已刷新'; + return '$label 已刷新'; } @override String repeater_errorRefreshing(String label) { - return '刷新 $label 时出错'; + return '刷新 $label 时出错'; } @override - String get repeater_cliTitle => '转发节点命令行'; + String get repeater_cliTitle => '转发节点命令行'; @override - String get repeater_debugNextCommand => '调试下一条命令'; + String get repeater_debugNextCommand => '调试下一条命令'; @override - String get repeater_commandHelp => '帮助'; + String get repeater_commandHelp => '帮助'; @override - String get repeater_clearHistory => '清除历史'; + String get repeater_clearHistory => '清除历史'; @override - String get repeater_noCommandsSent => '尚未发送命令'; + String get repeater_noCommandsSent => '尚未发送命令'; @override - String get repeater_typeCommandOrUseQuick => - '输入命令或使用快捷命令'; + String get repeater_typeCommandOrUseQuick => '输入命令或使用快捷命令'; @override - String get repeater_enterCommandHint => '输入命令...'; + String get repeater_enterCommandHint => '输入命令...'; @override - String get repeater_previousCommand => '上一条命令'; + String get repeater_previousCommand => '上一条命令'; @override - String get repeater_nextCommand => '下一条命令'; + String get repeater_nextCommand => '下一条命令'; @override - String get repeater_enterCommandFirst => '请先输入命令'; + String get repeater_enterCommandFirst => '请先输入命令'; @override - String get repeater_cliCommandFrameTitle => 'CLI 命令帧'; + String get repeater_cliCommandFrameTitle => 'CLI 命令帧'; @override String repeater_cliCommandError(String error) { - return '错误:$error'; + return '错误:$error'; } @override - String get repeater_cliQuickGetName => '获取名称'; + String get repeater_cliQuickGetName => '获取名称'; @override - String get repeater_cliQuickGetRadio => '获取无线电设置'; + String get repeater_cliQuickGetRadio => '获取无线电设置'; @override - String get repeater_cliQuickGetTx => '获取 TX'; + String get repeater_cliQuickGetTx => '获取 TX'; @override - String get repeater_cliQuickNeighbors => '邻居'; + String get repeater_cliQuickNeighbors => '邻居'; @override - String get repeater_cliQuickVersion => '版本'; + String get repeater_cliQuickVersion => '版本'; @override - String get repeater_cliQuickAdvertise => '发送广播'; + String get repeater_cliQuickAdvertise => '发送广播'; @override - String get repeater_cliQuickClock => 'æ—¶é’Ÿ'; + String get repeater_cliQuickClock => '时钟'; @override - String get repeater_cliHelpAdvert => '发送广播包'; + String get repeater_cliHelpAdvert => '发送广播包'; @override - String get repeater_cliHelpReboot => - '重启设备。(注意:可能会收到超时错误,属于正常现象)'; + String get repeater_cliHelpReboot => '重启设备。(注意:可能会收到超时错误,属于正常现象)'; @override - String get repeater_cliHelpClock => '显示设备当前时间'; + String get repeater_cliHelpClock => '显示设备当前时间'; @override - String get repeater_cliHelpPassword => '设置新的管理员密码'; + String get repeater_cliHelpPassword => '设置新的管理员密码'; @override - String get repeater_cliHelpVersion => - '显示设备版本和固件构建日期'; + String get repeater_cliHelpVersion => '显示设备版本和固件构建日期'; @override - String get repeater_cliHelpClearStats => '重置各种统计数据'; + String get repeater_cliHelpClearStats => '重置各种统计数据'; @override - String get repeater_cliHelpSetAf => '设置时间因子'; + String get repeater_cliHelpSetAf => '设置时间因子'; @override - String get repeater_cliHelpSetTx => - '设置 LoRa 发射功率 (dBm)(重启生效)'; + String get repeater_cliHelpSetTx => '设置 LoRa 发射功率 (dBm)(重启生效)'; @override - String get repeater_cliHelpSetRepeat => - '启用或禁用此节点的转发功能'; + String get repeater_cliHelpSetRepeat => '启用或禁用此节点的转发功能'; @override String get repeater_cliHelpSetAllowReadOnly => - '(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)'; + '(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)'; @override - String get repeater_cliHelpSetFloodMax => - '设置最大传入数据包跳数(≥该值则不转发)'; + String get repeater_cliHelpSetFloodMax => '设置最大传入数据包跳数(≥该值则不转发)'; @override - String get repeater_cliHelpSetIntThresh => - '设置干扰阈值 (dB),默认14,设为0禁用'; + String get repeater_cliHelpSetIntThresh => '设置干扰阈值 (dB),默认14,设为0禁用'; @override - String get repeater_cliHelpSetAgcResetInterval => - '设置 AGC 重置间隔(秒),设为0禁用'; + String get repeater_cliHelpSetAgcResetInterval => '设置 AGC 重置间隔(秒),设为0禁用'; @override - String get repeater_cliHelpSetMultiAcks => - '启用或禁用“多重确认”功能'; + String get repeater_cliHelpSetMultiAcks => '启用或禁用“多重确认”功能'; @override - String get repeater_cliHelpSetAdvertInterval => - '设置本地广播间隔(分钟),设为0禁用'; + String get repeater_cliHelpSetAdvertInterval => '设置本地广播间隔(分钟),设为0禁用'; @override - String get repeater_cliHelpSetFloodAdvertInterval => - '设置泛洪广播间隔(小时),设为0禁用'; + String get repeater_cliHelpSetFloodAdvertInterval => '设置泛洪广播间隔(小时),设为0禁用'; @override - String get repeater_cliHelpSetGuestPassword => '设置/更新访客密码'; + String get repeater_cliHelpSetGuestPassword => '设置/更新访客密码'; @override - String get repeater_cliHelpSetName => '设置广播名称'; + String get repeater_cliHelpSetName => '设置广播名称'; @override - String get repeater_cliHelpSetLat => '设置广播纬度(十进制)'; + String get repeater_cliHelpSetLat => '设置广播纬度(十进制)'; @override - String get repeater_cliHelpSetLon => '设置广播经度(十进制)'; + String get repeater_cliHelpSetLon => '设置广播经度(十进制)'; @override - String get repeater_cliHelpSetRadio => - '完全重设无线电参数并保存,需重启生效'; + String get repeater_cliHelpSetRadio => '完全重设无线电参数并保存,需重启生效'; @override - String get repeater_cliHelpSetRxDelay => - '(实验性)设置接收延迟基数,设为0禁用'; + String get repeater_cliHelpSetRxDelay => '(实验性)设置接收延迟基数,设为0禁用'; @override - String get repeater_cliHelpSetTxDelay => - '通过因子和随机时隙延迟泛洪数据包转发,降低冲突'; + String get repeater_cliHelpSetTxDelay => '通过因子和随机时隙延迟泛洪数据包转发,降低冲突'; @override - String get repeater_cliHelpSetDirectTxDelay => - '同 txdelay,用于直连模式数据包'; + String get repeater_cliHelpSetDirectTxDelay => '同 txdelay,用于直连模式数据包'; @override - String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥接'; + String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥接'; @override - String get repeater_cliHelpSetBridgeDelay => '设置桥接转发延迟'; + String get repeater_cliHelpSetBridgeDelay => '设置桥接转发延迟'; @override - String get repeater_cliHelpSetBridgeSource => - '选择桥接器转发接收或发送的数据包'; + String get repeater_cliHelpSetBridgeSource => '选择桥接器转发接收或发送的数据包'; @override - String get repeater_cliHelpSetBridgeBaud => - '设置 RS232 桥接串口波特率'; + String get repeater_cliHelpSetBridgeBaud => '设置 RS232 桥接串口波特率'; @override - String get repeater_cliHelpSetBridgeSecret => '设置 ESPNOW 桥接密钥'; + String get repeater_cliHelpSetBridgeSecret => '设置 ESPNOW 桥接密钥'; @override - String get repeater_cliHelpSetAdcMultiplier => - '设置电池电压校正系数(特定板支持)'; + String get repeater_cliHelpSetAdcMultiplier => '设置电池电压校正系数(特定板支持)'; @override - String get repeater_cliHelpTempRadio => - '临时设置无线电参数指定分钟,之后恢复(不保存)'; + String get repeater_cliHelpTempRadio => '临时设置无线电参数指定分钟,之后恢复(不保存)'; @override - String get repeater_cliHelpSetPerm => - '修改 ACL,权限位:0访客、1只读、2读写、3管理员'; + String get repeater_cliHelpSetPerm => '修改 ACL,权限位:0访客、1只读、2读写、3管理员'; @override - String get repeater_cliHelpGetBridgeType => - '支持桥接模式:RS232、ESPNOW'; + String get repeater_cliHelpGetBridgeType => '支持桥接模式:RS232、ESPNOW'; @override - String get repeater_cliHelpLogStart => '开始记录数据包到文件系统'; + String get repeater_cliHelpLogStart => '开始记录数据包到文件系统'; @override - String get repeater_cliHelpLogStop => '停止记录数据包'; + String get repeater_cliHelpLogStop => '停止记录数据包'; @override - String get repeater_cliHelpLogErase => '删除所有记录的数据包'; + String get repeater_cliHelpLogErase => '删除所有记录的数据包'; @override - String get repeater_cliHelpNeighbors => - '显示零跳广播收到的其他转发节点列表'; + String get repeater_cliHelpNeighbors => '显示零跳广播收到的其他转发节点列表'; @override - String get repeater_cliHelpNeighborRemove => - '从邻居列表删除第一个匹配项(通过公钥前缀)'; + String get repeater_cliHelpNeighborRemove => '从邻居列表删除第一个匹配项(通过公钥前缀)'; @override - String get repeater_cliHelpRegion => - '(仅串口)列出所有定义区域及当前泛洪权限'; + String get repeater_cliHelpRegion => '(仅串口)列出所有定义区域及当前泛洪权限'; @override - String get repeater_cliHelpRegionLoad => - '特殊多命令调用,以空行结束'; + String get repeater_cliHelpRegionLoad => '特殊多命令调用,以空行结束'; @override - String get repeater_cliHelpRegionGet => '搜索指定前缀的区域'; + String get repeater_cliHelpRegionGet => '搜索指定前缀的区域'; @override - String get repeater_cliHelpRegionPut => '添加或更新区域定义'; + String get repeater_cliHelpRegionPut => '添加或更新区域定义'; @override - String get repeater_cliHelpRegionRemove => '删除指定区域定义'; + String get repeater_cliHelpRegionRemove => '删除指定区域定义'; @override - String get repeater_cliHelpRegionAllowf => - '为区域设置“泛洪”权限'; + String get repeater_cliHelpRegionAllowf => '为区域设置“泛洪”权限'; @override - String get repeater_cliHelpRegionDenyf => '移除区域的“泛洪”权限'; + String get repeater_cliHelpRegionDenyf => '移除区域的“泛洪”权限'; @override - String get repeater_cliHelpRegionHome => - '返回当前“主区域”(预留)'; + String get repeater_cliHelpRegionHome => '返回当前“主区域”(预留)'; @override - String get repeater_cliHelpRegionHomeSet => '设置“主”区域'; + String get repeater_cliHelpRegionHomeSet => '设置“主”区域'; @override - String get repeater_cliHelpRegionSave => '保存区域列表到存储'; + String get repeater_cliHelpRegionSave => '保存区域列表到存储'; @override - String get repeater_cliHelpGps => '显示 GPS 状态'; + String get repeater_cliHelpGps => '显示 GPS 状态'; @override - String get repeater_cliHelpGpsOnOff => '切换 GPS 电源'; + String get repeater_cliHelpGpsOnOff => '切换 GPS 电源'; @override - String get repeater_cliHelpGpsSync => '将节点时间与 GPS 同步'; + String get repeater_cliHelpGpsSync => '将节点时间与 GPS 同步'; @override - String get repeater_cliHelpGpsSetLoc => - '将节点坐标设为 GPS 坐标并保存'; + String get repeater_cliHelpGpsSetLoc => '将节点坐标设为 GPS 坐标并保存'; @override - String get repeater_cliHelpGpsAdvert => - '设置位置广播配置:none/share/prefs'; + String get repeater_cliHelpGpsAdvert => '设置位置广播配置:none/share/prefs'; @override - String get repeater_cliHelpGpsAdvertSet => '设置广播位置配置'; + String get repeater_cliHelpGpsAdvertSet => '设置广播位置配置'; @override - String get repeater_commandsListTitle => '命令列表'; + String get repeater_commandsListTitle => '命令列表'; @override - String get repeater_commandsListNote => - '注意:多数 set 命令也有对应的 get 命令'; + String get repeater_commandsListNote => '注意:多数 set 命令也有对应的 get 命令'; @override - String get repeater_general => '通用'; + String get repeater_general => '通用'; @override - String get repeater_settingsCategory => '设置'; + String get repeater_settingsCategory => '设置'; @override - String get repeater_bridge => '桥接'; + String get repeater_bridge => '桥接'; @override - String get repeater_logging => '日志'; + String get repeater_logging => '日志'; @override - String get repeater_neighborsRepeaterOnly => '邻居(仅转发节点)'; + String get repeater_neighborsRepeaterOnly => '邻居(仅转发节点)'; @override - String get repeater_regionManagementRepeaterOnly => - '区域管理(仅转发节点)'; + String get repeater_regionManagementRepeaterOnly => '区域管理(仅转发节点)'; @override - String get repeater_regionNote => - '区域命令用于管理区域定义和权限'; + String get repeater_regionNote => '区域命令用于管理区域定义和权限'; @override - String get repeater_gpsManagement => 'GPS 管理'; + String get repeater_gpsManagement => 'GPS 管理'; @override - String get repeater_gpsNote => 'GPS 命令用于位置相关任务'; + String get repeater_gpsNote => 'GPS 命令用于位置相关任务'; @override - String get telemetry_receivedData => '接收到的遥测数据'; + String get telemetry_receivedData => '接收到的遥测数据'; @override - String get telemetry_requestTimeout => '遥测请求超时'; + String get telemetry_requestTimeout => '遥测请求超时'; @override String telemetry_errorLoading(String error) { - return '加载遥测数据时出错:$error'; + return '加载遥测数据时出错:$error'; } @override - String get telemetry_noData => '暂无遥测数据'; + String get telemetry_noData => '暂无遥测数据'; @override String telemetry_channelTitle(int channel) { - return '频道 $channel'; + return '频道 $channel'; } @override - String get telemetry_batteryLabel => '电池'; + String get telemetry_batteryLabel => '电池'; @override - String get telemetry_voltageLabel => '电压'; + String get telemetry_voltageLabel => '电压'; @override - String get telemetry_mcuTemperatureLabel => 'MCU 温度'; + String get telemetry_mcuTemperatureLabel => 'MCU 温度'; @override - String get telemetry_temperatureLabel => '温度'; + String get telemetry_temperatureLabel => '温度'; @override - String get telemetry_currentLabel => '电流'; + String get telemetry_currentLabel => '电流'; @override String telemetry_batteryValue(int percent, String volts) { @@ -2383,78 +2323,78 @@ class AppLocalizationsZh extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override - String get neighbors_receivedData => '已接收邻居信息'; + String get neighbors_receivedData => '已接收邻居信息'; @override - String get neighbors_requestTimedOut => '邻居请求超时'; + String get neighbors_requestTimedOut => '邻居请求超时'; @override String neighbors_errorLoading(String error) { - return '加载邻居时出错:$error'; + return '加载邻居时出错:$error'; } @override - String get neighbors_repeatersNeighbors => '转发节点的邻居'; + String get neighbors_repeatersNeighbors => '转发节点的邻居'; @override - String get neighbors_noData => '暂无邻居信息'; + String get neighbors_noData => '暂无邻居信息'; @override String neighbors_unknownContact(String pubkey) { - return '未知 $pubkey'; + return '未知 $pubkey'; } @override String neighbors_heardAgo(String time) { - return '听到:$time前'; + return '听到:$time前'; } @override - String get channelPath_title => '数据包路径'; + String get channelPath_title => '数据包路径'; @override - String get channelPath_viewMap => '查看地图'; + String get channelPath_viewMap => '查看地图'; @override - String get channelPath_otherObservedPaths => '其他观察到的路径'; + String get channelPath_otherObservedPaths => '其他观察到的路径'; @override - String get channelPath_repeaterHops => '转发节点跳数'; + String get channelPath_repeaterHops => '转发节点跳数'; @override - String get channelPath_noHopDetails => '此数据包未提供详细信息'; + String get channelPath_noHopDetails => '此数据包未提供详细信息'; @override - String get channelPath_messageDetails => '消息详情'; + String get channelPath_messageDetails => '消息详情'; @override - String get channelPath_senderLabel => '发送者'; + String get channelPath_senderLabel => '发送者'; @override - String get channelPath_timeLabel => 'æ—¶é—´'; + String get channelPath_timeLabel => '时间'; @override - String get channelPath_repeatsLabel => '重复'; + String get channelPath_repeatsLabel => '重复'; @override String channelPath_pathLabel(int index) { - return '路径 $index'; + return '路径 $index'; } @override - String get channelPath_observedLabel => '观察到的'; + String get channelPath_observedLabel => '观察到的'; @override String channelPath_observedPathTitle(int index, String hops) { - return '观察到的路径 $index • $hops'; + return '观察到的路径 $index • $hops'; } @override - String get channelPath_noLocationData => '无位置信息'; + String get channelPath_noLocationData => '无位置信息'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2467,339 +2407,328 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channelPath_unknownPath => '未知'; + String get channelPath_unknownPath => '未知'; @override - String get channelPath_floodPath => '泛洪'; + String get channelPath_floodPath => '泛洪'; @override - String get channelPath_directPath => '直连'; + String get channelPath_directPath => '直连'; @override String channelPath_observedZeroOf(int total) { - return '0 / $total è·³'; + return '0 / $total 跳'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '$observed / $total è·³'; + return '$observed / $total 跳'; } @override - String get channelPath_mapTitle => '路径地图'; + String get channelPath_mapTitle => '路径地图'; @override - String get channelPath_noRepeaterLocations => - '此路径上没有可用的转发节点位置信息'; + String get channelPath_noRepeaterLocations => '此路径上没有可用的转发节点位置信息'; @override String channelPath_primaryPath(int index) { - return '路径 $index(主要)'; + return '路径 $index(主要)'; } @override - String get channelPath_pathLabelTitle => '路径'; + String get channelPath_pathLabelTitle => '路径'; @override - String get channelPath_observedPathHeader => '观察到的路径'; + String get channelPath_observedPathHeader => '观察到的路径'; @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override - String get channelPath_noHopDetailsAvailable => - '此数据包暂无详细信息'; + String get channelPath_noHopDetailsAvailable => '此数据包暂无详细信息'; @override - String get channelPath_unknownRepeater => '未知转发节点'; + String get channelPath_unknownRepeater => '未知转发节点'; @override - String get community_title => '社区'; + String get community_title => '社区'; @override - String get community_create => '创建社区'; + String get community_create => '创建社区'; @override - String get community_createDesc => - '创建新社区并通过二维码分享。'; + String get community_createDesc => '创建新社区并通过二维码分享。'; @override - String get community_join => '加入'; + String get community_join => '加入'; @override - String get community_joinTitle => '加入社区'; + String get community_joinTitle => '加入社区'; @override String community_joinConfirmation(String name) { - return '是否加入社区 \"$name\"?'; + return '是否加入社区 \"$name\"?'; } @override - String get community_scanQr => '扫描社区二维码'; + String get community_scanQr => '扫描社区二维码'; @override - String get community_scanInstructions => - '将摄像头对准社区的二维码'; + String get community_scanInstructions => '将摄像头对准社区的二维码'; @override - String get community_showQr => '显示二维码'; + String get community_showQr => '显示二维码'; @override - String get community_publicChannel => '社区公共频道'; + String get community_publicChannel => '社区公共频道'; @override - String get community_hashtagChannel => '社区标签频道'; + String get community_hashtagChannel => '社区标签频道'; @override - String get community_name => '社区名称'; + String get community_name => '社区名称'; @override - String get community_enterName => '请输入社区名称'; + String get community_enterName => '请输入社区名称'; @override String community_created(String name) { - return '社区 \"$name\" 已创建'; + return '社区 \"$name\" 已创建'; } @override String community_joined(String name) { - return '已加入社区 \"$name\"'; + return '已加入社区 \"$name\"'; } @override - String get community_qrTitle => '分享社区'; + String get community_qrTitle => '分享社区'; @override String community_qrInstructions(String name) { - return '扫描此二维码加入 \"$name\"'; + return '扫描此二维码加入 \"$name\"'; } @override - String get community_hashtagPrivacyHint => - '仅社区成员可加入社区标签频道。'; + String get community_hashtagPrivacyHint => '仅社区成员可加入社区标签频道。'; @override - String get community_invalidQrCode => '无效的社区二维码'; + String get community_invalidQrCode => '无效的社区二维码'; @override - String get community_alreadyMember => '已是成员'; + String get community_alreadyMember => '已是成员'; @override String community_alreadyMemberMessage(String name) { - return '您已是 \"$name\" 的成员。'; + return '您已是 \"$name\" 的成员。'; } @override - String get community_addPublicChannel => '添加公共频道'; + String get community_addPublicChannel => '添加公共频道'; @override - String get community_addPublicChannelHint => - '自动添加此社区的公共频道'; + String get community_addPublicChannelHint => '自动添加此社区的公共频道'; @override - String get community_noCommunities => '尚未加入任何社区。'; + String get community_noCommunities => '尚未加入任何社区。'; @override - String get community_scanOrCreate => - '扫描二维码或创建社区以开始。'; + String get community_scanOrCreate => '扫描二维码或创建社区以开始。'; @override - String get community_manageCommunities => '管理社区'; + String get community_manageCommunities => '管理社区'; @override - String get community_delete => '退出社区'; + String get community_delete => '退出社区'; @override String community_deleteConfirm(String name) { - return '是否退出 \"$name\"?'; + return '是否退出 \"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return '这将同时删除 $count 个频道及其所有消息。'; + return '这将同时删除 $count 个频道及其所有消息。'; } @override String community_deleted(String name) { - return '已退出社区 \"$name\"'; + return '已退出社区 \"$name\"'; } @override - String get community_regenerateSecret => '重新生成密钥'; + String get community_regenerateSecret => '重新生成密钥'; @override String community_regenerateSecretConfirm(String name) { - return '是否为 \"$name\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。'; + return '是否为 \"$name\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。'; } @override - String get community_regenerate => '重新生成'; + String get community_regenerate => '重新生成'; @override String community_secretRegenerated(String name) { - return '已为 \"$name\" 重新生成密钥'; + return '已为 \"$name\" 重新生成密钥'; } @override - String get community_updateSecret => '更新密钥'; + String get community_updateSecret => '更新密钥'; @override String community_secretUpdated(String name) { - return '“$name”的密钥已更新'; + return '“$name”的密钥已更新'; } @override String community_scanToUpdateSecret(String name) { - return '扫描新二维码以更新 \"$name\" 的密钥'; + return '扫描新二维码以更新 \"$name\" 的密钥'; } @override - String get community_addHashtagChannel => '添加标签频道'; + String get community_addHashtagChannel => '添加标签频道'; @override - String get community_addHashtagChannelDesc => - '为此社区创建标签频道'; + String get community_addHashtagChannelDesc => '为此社区创建标签频道'; @override - String get community_selectCommunity => '选择社区'; + String get community_selectCommunity => '选择社区'; @override - String get community_regularHashtag => '普通标签'; + String get community_regularHashtag => '普通标签'; @override - String get community_regularHashtagDesc => - '公共标签频道(任何人都可参与)'; + String get community_regularHashtagDesc => '公共标签频道(任何人都可参与)'; @override - String get community_communityHashtag => '社区标签'; + String get community_communityHashtag => '社区标签'; @override - String get community_communityHashtagDesc => '仅限社区成员'; + String get community_communityHashtagDesc => '仅限社区成员'; @override String community_forCommunity(String name) { - return '为 $name'; + return '为 $name'; } @override - String get listFilter_tooltip => '筛选与排序'; + String get listFilter_tooltip => '筛选与排序'; @override - String get listFilter_sortBy => '排序方式'; + String get listFilter_sortBy => '排序方式'; @override - String get listFilter_latestMessages => '最新消息'; + String get listFilter_latestMessages => '最新消息'; @override - String get listFilter_heardRecently => '最近听到'; + String get listFilter_heardRecently => '最近听到'; @override String get listFilter_az => 'A-Z'; @override - String get listFilter_filters => '筛选'; + String get listFilter_filters => '筛选'; @override - String get listFilter_all => '全部'; + String get listFilter_all => '全部'; @override - String get listFilter_favorites => '收藏'; + String get listFilter_favorites => '收藏'; @override - String get listFilter_addToFavorites => '添加到收藏'; + String get listFilter_addToFavorites => '添加到收藏'; @override - String get listFilter_removeFromFavorites => '从收藏中移除'; + String get listFilter_removeFromFavorites => '从收藏中移除'; @override - String get listFilter_users => '用户'; + String get listFilter_users => '用户'; @override - String get listFilter_repeaters => '转发节点'; + String get listFilter_repeaters => '转发节点'; @override - String get listFilter_roomServers => '房间服务器'; + String get listFilter_roomServers => '房间服务器'; @override - String get listFilter_unreadOnly => '仅显示未读'; + String get listFilter_unreadOnly => '仅显示未读'; @override - String get listFilter_newGroup => '新建群聊'; + String get listFilter_newGroup => '新建群聊'; @override - String get pathTrace_you => '我自己'; + String get pathTrace_you => '我自己'; @override - String get pathTrace_failed => '路径追踪失败。'; + String get pathTrace_failed => '路径追踪失败。'; @override - String get pathTrace_notAvailable => '无法获取路径信息。'; + String get pathTrace_notAvailable => '无法获取路径信息。'; @override - String get pathTrace_refreshTooltip => '刷新路径追踪'; + String get pathTrace_refreshTooltip => '刷新路径追踪'; @override - String get pathTrace_someHopsNoLocation => '某些跳缺少位置信息!'; + String get pathTrace_someHopsNoLocation => '某些跳缺少位置信息!'; @override - String get pathTrace_clearTooltip => '清除路径'; + String get pathTrace_clearTooltip => '清除路径'; @override - String get losSelectStartEnd => - '选择 LOS 的起始节点和结束节点。'; + String get losSelectStartEnd => '选择 LOS 的起始节点和结束节点。'; @override String losRunFailed(String error) { - return '视线检查失败:$error'; + return '视线检查失败:$error'; } @override - String get losClearAllPoints => '清除所有点'; + String get losClearAllPoints => '清除所有点'; @override - String get losRunToViewElevationProfile => '运行 LOS 查看高程剖面'; + String get losRunToViewElevationProfile => '运行 LOS 查看高程剖面'; @override - String get losMenuTitle => '服务水平菜单'; + String get losMenuTitle => '服务水平菜单'; @override - String get losMenuSubtitle => - '点击节点或长按地图以获取自定义点'; + String get losMenuSubtitle => '点击节点或长按地图以获取自定义点'; @override - String get losShowDisplayNodes => '显示显示节点'; + String get losShowDisplayNodes => '显示显示节点'; @override - String get losCustomPoints => '自定义积分'; + String get losCustomPoints => '自定义积分'; @override String losCustomPointLabel(int index) { - return '自定义 $index'; + return '自定义 $index'; } @override - String get losPointA => 'A点'; + String get losPointA => 'A点'; @override - String get losPointB => 'B点'; + String get losPointB => 'B点'; @override String losAntennaA(String value, String unit) { - return '天线 A: $value $unit'; + return '天线 A: $value $unit'; } @override String losAntennaB(String value, String unit) { - return '天线 B:$value $unit'; + return '天线 B:$value $unit'; } @override - String get losRun => '运行视距'; + String get losRun => '运行视距'; @override - String get losNoElevationData => '无海拔数据'; + String get losNoElevationData => '无海拔数据'; @override String losProfileClear( @@ -2808,7 +2737,7 @@ class AppLocalizationsZh extends AppLocalizations { String clearance, String heightUnit, ) { - return '$distance $distanceUnit,清除 LOS,最小间隙 $clearance $heightUnit'; + return '$distance $distanceUnit,清除 LOS,最小间隙 $clearance $heightUnit'; } @override @@ -2818,60 +2747,58 @@ class AppLocalizationsZh extends AppLocalizations { String obstruction, String heightUnit, ) { - return '$distance $distanceUnit,被 $obstruction $heightUnit 阻止'; + return '$distance $distanceUnit,被 $obstruction $heightUnit 阻止'; } @override - String get losStatusChecking => '洛斯:正在检查...'; + String get losStatusChecking => '洛斯:正在检查...'; @override - String get losStatusNoData => 'LOS:无数据'; + String get losStatusNoData => 'LOS:无数据'; @override String losStatusSummary(int clear, int total, int blocked, int unknown) { - return 'LOS:$clear/$total 清除,$blocked 阻塞,$unknown 未知'; + return 'LOS:$clear/$total 清除,$blocked 阻塞,$unknown 未知'; } @override - String get losErrorElevationUnavailable => - '一个或多个样本的海拔数据不可用。'; + String get losErrorElevationUnavailable => '一个或多个样本的海拔数据不可用。'; @override - String get losErrorInvalidInput => - '用于 LOS 计算的点/高程数据无效。'; + String get losErrorInvalidInput => '用于 LOS 计算的点/高程数据无效。'; @override - String get losRenameCustomPoint => '重命名自定义点'; + String get losRenameCustomPoint => '重命名自定义点'; @override - String get losPointName => '点名称'; + String get losPointName => '点名称'; @override - String get losShowPanelTooltip => '显示 LOS 面板'; + String get losShowPanelTooltip => '显示 LOS 面板'; @override - String get losHidePanelTooltip => '隐藏 LOS 面板'; + String get losHidePanelTooltip => '隐藏 LOS 面板'; @override - String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; + String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => '无线电地平线'; + String get losLegendRadioHorizon => '无线电地平线'; @override - String get losLegendLosBeam => '视距波束'; + String get losLegendLosBeam => '视距波束'; @override - String get losLegendTerrain => '地形'; + String get losLegendTerrain => '地形'; @override - String get losFrequencyLabel => '频率'; + String get losFrequencyLabel => '频率'; @override - String get losFrequencyInfoTooltip => '查看计算详情'; + String get losFrequencyInfoTooltip => '查看计算详情'; @override - String get losFrequencyDialogTitle => '无线电地平线计算'; + String get losFrequencyDialogTitle => '无线电地平线计算'; @override String losFrequencyDialogDescription( @@ -2880,161 +2807,151 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return '从 $baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; + return '从 $baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; } @override - String get contacts_pathTrace => '路径追踪'; + String get contacts_pathTrace => '路径追踪'; @override String get contacts_ping => 'Ping'; @override - String get contacts_repeaterPathTrace => 'Trace 转发节点'; + String get contacts_repeaterPathTrace => 'Trace 转发节点'; @override - String get contacts_repeaterPing => 'Ping 转发节点'; + String get contacts_repeaterPing => 'Ping 转发节点'; @override - String get contacts_roomPathTrace => 'Trace 房间服务器'; + String get contacts_roomPathTrace => 'Trace 房间服务器'; @override - String get contacts_roomPing => 'Ping 房间服务器'; + String get contacts_roomPing => 'Ping 房间服务器'; @override - String get contacts_chatTraceRoute => '路由追踪'; + String get contacts_chatTraceRoute => '路由追踪'; @override String contacts_pathTraceTo(String name) { - return '追踪至 $name 的路径'; + return '追踪至 $name 的路径'; } @override - String get contacts_clipboardEmpty => '剪贴板为空'; + String get contacts_clipboardEmpty => '剪贴板为空'; @override - String get contacts_invalidAdvertFormat => '无效的联系人信息格式'; + String get contacts_invalidAdvertFormat => '无效的联系人信息格式'; @override - String get contacts_contactImported => '联系人已导入'; + String get contacts_contactImported => '联系人已导入'; @override - String get contacts_contactImportFailed => '导入联系人失败。'; + String get contacts_contactImportFailed => '导入联系人失败。'; @override - String get contacts_zeroHopAdvert => '发送零跳广播'; + String get contacts_zeroHopAdvert => '发送零跳广播'; @override - String get contacts_floodAdvert => '发送泛洪广播'; + String get contacts_floodAdvert => '发送泛洪广播'; @override - String get contacts_copyAdvertToClipboard => '复制广播到剪贴板'; + String get contacts_copyAdvertToClipboard => '复制广播到剪贴板'; @override - String get contacts_addContactFromClipboard => '从剪贴板添加联系人'; + String get contacts_addContactFromClipboard => '从剪贴板添加联系人'; @override - String get contacts_ShareContact => '复制联系人信息到剪贴板'; + String get contacts_ShareContact => '复制联系人信息到剪贴板'; @override - String get contacts_ShareContactZeroHop => '通过广播分享联系人'; + String get contacts_ShareContactZeroHop => '通过广播分享联系人'; @override - String get contacts_zeroHopContactAdvertSent => '零跳广播已发送'; + String get contacts_zeroHopContactAdvertSent => '零跳广播已发送'; @override - String get contacts_zeroHopContactAdvertFailed => - '发送联系人广播失败。'; + String get contacts_zeroHopContactAdvertFailed => '发送联系人广播失败。'; @override - String get contacts_contactAdvertCopied => '广播已复制到剪贴板。'; + String get contacts_contactAdvertCopied => '广播已复制到剪贴板。'; @override - String get contacts_contactAdvertCopyFailed => - '复制广播到剪贴板失败。'; + String get contacts_contactAdvertCopyFailed => '复制广播到剪贴板失败。'; @override - String get notification_activityTitle => 'MeshCore 活动'; + String get notification_activityTitle => 'MeshCore 活动'; @override String notification_messagesCount(int count) { - return '$count 条消息'; + return '$count 条消息'; } @override String notification_channelMessagesCount(int count) { - return '$count 条频道消息'; + return '$count 条频道消息'; } @override String notification_newNodesCount(int count) { - return '$count 个新节点'; + return '$count 个新节点'; } @override String notification_newTypeDiscovered(String contactType) { - return '发现新 $contactType'; + return '发现新 $contactType'; } @override - String get notification_receivedNewMessage => '收到新消息'; + String get notification_receivedNewMessage => '收到新消息'; @override - String get settings_gpxExportRepeaters => - '导出转发节点/房间服务器到 GPX'; + String get settings_gpxExportRepeaters => '导出转发节点/房间服务器到 GPX'; @override - String get settings_gpxExportRepeatersSubtitle => - '导出带位置的转发节点/房间服务器到 GPX 文件'; + String get settings_gpxExportRepeatersSubtitle => '导出带位置的转发节点/房间服务器到 GPX 文件'; @override - String get settings_gpxExportContacts => '导出伙伴到 GPX'; + String get settings_gpxExportContacts => '导出伙伴到 GPX'; @override - String get settings_gpxExportContactsSubtitle => - '导出带位置的伙伴到 GPX 文件'; + String get settings_gpxExportContactsSubtitle => '导出带位置的伙伴到 GPX 文件'; @override - String get settings_gpxExportAll => '导出所有联系人到 GPX'; + String get settings_gpxExportAll => '导出所有联系人到 GPX'; @override - String get settings_gpxExportAllSubtitle => - '导出所有带位置的联系人到 GPX 文件'; + String get settings_gpxExportAllSubtitle => '导出所有带位置的联系人到 GPX 文件'; @override - String get settings_gpxExportSuccess => 'GPX 文件导出成功'; + String get settings_gpxExportSuccess => 'GPX 文件导出成功'; @override - String get settings_gpxExportNoContacts => '没有可导出的联系人'; + String get settings_gpxExportNoContacts => '没有可导出的联系人'; @override - String get settings_gpxExportNotAvailable => - '您的设备/操作系统不支持'; + String get settings_gpxExportNotAvailable => '您的设备/操作系统不支持'; @override - String get settings_gpxExportError => '导出时出错'; + String get settings_gpxExportError => '导出时出错'; @override - String get settings_gpxExportRepeatersRoom => - '转发节点与房间服务器位置'; + String get settings_gpxExportRepeatersRoom => '转发节点与房间服务器位置'; @override - String get settings_gpxExportChat => '伙伴位置'; + String get settings_gpxExportChat => '伙伴位置'; @override - String get settings_gpxExportAllContacts => '所有联系人位置'; + String get settings_gpxExportAllContacts => '所有联系人位置'; @override - String get settings_gpxExportShareText => - '来自 MeshCore Open 的地图数据导出'; + String get settings_gpxExportShareText => '来自 MeshCore Open 的地图数据导出'; @override - String get settings_gpxExportShareSubject => - 'MeshCore Open GPX 地图数据导出'; + String get settings_gpxExportShareSubject => 'MeshCore Open GPX 地图数据导出'; @override - String get snrIndicator_nearByRepeaters => '附近的重复器'; + String get snrIndicator_nearByRepeaters => '附近的重复器'; @override - String get snrIndicator_lastSeen => '最近访问'; + String get snrIndicator_lastSeen => '最近访问'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index d325209..ba22eb2 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", "@channels_channelDeleteFailed": { "placeholders": { @@ -27,7 +27,7 @@ "common_create": "Maak", "common_continue": "Doorgaan", "common_share": "Delen", - "common_copy": "Kopiëren", + "common_copy": "Kopiëren", "common_retry": "Nogmaals proberen", "common_hide": "Verbergen", "common_remove": "Verwijderen", @@ -35,7 +35,7 @@ "common_disable": "Uitschakelen", "common_reboot": "Herstarten", "common_loading": "Laden...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -92,7 +92,7 @@ "settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor", "settings_radioSettingsUpdated": "Radio instellingen bijgewerkt", "settings_location": "Locatie", - "settings_locationSubtitle": "GPS coördinaten", + "settings_locationSubtitle": "GPS coördinaten", "settings_locationUpdated": "Locatie bijgewerkt", "settings_locationBothRequired": "Voer zowel breedte- als lengtegraad in.", "settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.", @@ -165,18 +165,18 @@ "appSettings_language": "Taal", "appSettings_languageSystem": "Standaardinstelling", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Notificaties", "appSettings_enableNotifications": "Notificaties inschakelen", "appSettings_enableNotificationsSubtitle": "Ontvang meldingen voor berichten en advertenties", @@ -338,7 +338,7 @@ }, "channels_hashtagChannel": "Hashtag kanaal", "channels_public": "Openbaar", - "channels_private": "Privé", + "channels_private": "Privé", "channels_publicChannel": "Open kanaal", "channels_privateChannel": "Private kanaal", "channels_editChannel": "Kanaal bewerken", @@ -623,7 +623,7 @@ "chat_invalidLink": "Ongeldig linkformaat", "map_title": "Node Map", "map_noNodesWithLocation": "Geen nodes met locatiegegevens", - "map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen", + "map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen", "map_nodesCount": "Nodes: {count}", "@map_nodesCount": { "placeholders": { @@ -645,7 +645,7 @@ "map_room": "Ruimte", "map_sensor": "Sensor", "map_pinDm": "Verzenden als bericht (DM)", - "map_pinPrivate": "Beveiligd (Privé)", + "map_pinPrivate": "Beveiligd (Privé)", "map_pinPublic": "Openbaar spikken", "map_lastSeen": "Laaste keer gezien", "map_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?", @@ -1039,7 +1039,7 @@ "repeater_eraseFileSystem": "Verwijder Besturingssysteem", "repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater", "repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!", - "repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.", + "repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.", "repeater_commandSent": "Commando verzonden: {command}", "@repeater_commandSent": { "placeholders": { @@ -1143,11 +1143,11 @@ "repeater_cliHelpSetBridgeEnabled": "Poort inschakelen/uitschakelen.", "repeater_cliHelpSetBridgeDelay": "Verzend vertraging instellen voor pakketten.", "repeater_cliHelpSetBridgeSource": "Kies of de brug ontvangen pakketten of verzonden pakketten opnieuw moet versturen.", - "repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.", + "repeater_cliHelpSetBridgeBaud": "Stel de seriële link baudrate in voor rs232 bruggen.", "repeater_cliHelpSetBridgeSecret": "Stel bridge-geheim in voor espnow bridges.", "repeater_cliHelpSetAdcMultiplier": "Stelt een aangepaste factor in om de gerapporteerde batterijspanning aan te passen (alleen ondersteund op selecte borden).", "repeater_cliHelpTempRadio": "Stelt tijdelijke radio parameters in voor het opgegeven aantal minuten, en keert daarna terug naar de originele radio parameters. (wordt niet opgeslagen in de voorkeuren).", - "repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)", + "repeater_cliHelpSetPerm": "Wijzigt de ACL. Verwijder de overeenkomstige entry (door pubkey prefix) als \"permissions\" 0 is. Voeg een nieuwe entry toe als pubkey-hex volledig is en niet momenteel in de ACL staat. Update de entry door matching pubkey prefix. Toestemming bits variëren per firmware rol, maar de onderste 2 bits zijn: 0 (Gast), 1 (Alleen lezen), 2 (Lezen/schrijven), 3 (Admin)", "repeater_cliHelpGetBridgeType": "Ontvang brugtype: geen, rs232, espnow", "repeater_cliHelpLogStart": "Start pakketlogging naar het bestandssysteem.", "repeater_cliHelpLogStop": "Stoppen met het loggen van pakketten naar het bestandssysteem.", @@ -1155,7 +1155,7 @@ "repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4", "repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.", "repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.", - "repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.", + "repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.", "repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"", "repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.", "repeater_cliHelpRegionRemove": "Verwijdert een regio-definitie met de gegeven naam. (moet exact overeenkomen en geen kindregio's hebben)", @@ -1167,7 +1167,7 @@ "repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.", "repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.", "repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.", - "repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.", + "repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.", "repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen", "repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.", "repeater_commandsListTitle": "Commandenlijst", @@ -1178,9 +1178,9 @@ "repeater_logging": "Logging", "repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)", "repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)", - "repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.", + "repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.", "repeater_gpsManagement": "Beheer GPS", - "repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.", + "repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.", "telemetry_receivedData": "Ontvangen Telemetriedata", "telemetry_requestTimeout": "Telemetryverzoek is uitgevallen.", "telemetry_errorLoading": "Fout bij het laden van de telemetrie: {error}", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1254,7 +1254,7 @@ "channelPath_repeatsLabel": "Repeats", "channelPath_pathLabel": "Pad {index}", "channelPath_observedLabel": "Waargenomen", - "channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}", + "channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Pad", "channelPath_observedPathHeader": "Waargenomen Pad", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1369,8 +1369,8 @@ "neighbors_repeatersNeighbors": "Herhalingen Buren", "neighbors_noData": "Geen gegevens van buren beschikbaar.", "channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.", - "channels_createPrivateChannel": "Maak een Privé Kanaal", - "channels_joinPrivateChannel": "Sluit een Privé Kanaal aan", + "channels_createPrivateChannel": "Maak een Privé Kanaal", + "channels_joinPrivateChannel": "Sluit een Privé Kanaal aan", "channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.", "channels_joinPublicChannel": "Sluit het Open Kanaal", "channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.", @@ -1558,22 +1558,22 @@ "contacts_roomPing": "Ping kamer server", "contacts_chatTraceRoute": "Route traceren", "contacts_pathTraceTo": "Trace route to {name}", - "appSettings_languageUk": "Oekraïens", + "appSettings_languageUk": "Oekraïens", "contacts_invalidAdvertFormat": "Ongeldige contactgegevens", - "contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.", + "contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.", "contacts_zeroHopAdvert": "Zero Hop Reclame", "contacts_floodAdvert": "Overstromingsadvertentie", - "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", + "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", "appSettings_languageRu": "Russisch", "appSettings_enableMessageTracing": "Berichttracking inschakelen", "appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven", "contacts_clipboardEmpty": "Knipbord is leeg.", "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", - "contacts_contactImported": "Contact is geïmporteerd.", + "contacts_contactImported": "Contact is geïmporteerd.", "contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie", "contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.", - "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", - "contacts_ShareContact": "Kontakt naar Klembord kopiëren", + "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", + "contacts_ShareContact": "Kontakt naar Klembord kopiëren", "contacts_ShareContactZeroHop": "Contact delen via advertentie", "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", "notification_activityTitle": "MeshCore Activiteit", @@ -1584,7 +1584,7 @@ "notification_receivedNewMessage": "Nieuw bericht ontvangen", "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", - "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", + "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", "settings_gpxExportNoContacts": "Geen contacten om te exporteren.", "settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem", "settings_gpxExportError": "Er was een fout bij het exporteren.", @@ -1595,7 +1595,7 @@ "settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties", "settings_gpxExportChat": "Locaties van metgezellen", "settings_gpxExportAllContacts": "Alle contactlocaties", - "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", + "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren", "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!", "map_removeLast": "Verwijder Laatste", @@ -1802,11 +1802,23 @@ "contacts_searchUsers": "Zoek {number}{str} gebruikers...", "contacts_searchFavorites": "Zoek {number}{str} favorieten...", "contacts_searchRoomServers": "Zoek {number}{str} Room servers...", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", + "usbScreenTitle": "Verbind via USB", "usbScreenStatus": "Selecteer een USB-apparaat", "usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.", - "usbScreenTitle": "Verbind via USB", - "usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad." + "usbScreenSubtitle": "Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", + "usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.", + "usbErrorPermissionDenied": "Toegang via USB is geweigerd.", + "usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.", + "usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.", + "usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.", + "usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.", + "usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.", + "usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.", + "usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.", + "usbErrorAlreadyActive": "Een USB-verbinding is al actief.", + "usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.", + "usbErrorPortClosed": "De USB-verbinding is niet actief.", + "usbErrorConnectTimedOut": "Wachtperiode is verlopen, er is geen reactie ontvangen van het apparaat.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index dd7b133..97e3321 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Nie udaÅ‚o siÄ™ usunąć kanaÅ‚u \"{name}\"", +{ + "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -10,32 +10,32 @@ "@@locale": "pl", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", - "nav_channels": "KanaÅ‚y", + "nav_channels": "Kanały", "nav_map": "Mapa", "common_cancel": "Anuluj", - "common_connect": "Połącz", - "common_unknownDevice": "Nieznane urzÄ…dzenie", + "common_connect": "Połącz", + "common_unknownDevice": "Nieznane urządzenie", "common_save": "Zapisz", - "common_delete": "UsuÅ„", - "common_close": "Zamknąć", + "common_delete": "Usuń", + "common_close": "Zamknąć", "common_edit": "Edytuj", "common_add": "Dodaj", "common_settings": "Ustawienia", - "common_disconnect": "Odłącz", - "common_connected": "Połączono", - "common_disconnected": "Odłączony", - "common_create": "Utwórz", + "common_disconnect": "Odłącz", + "common_connected": "Połączono", + "common_disconnected": "Odłączony", + "common_create": "Utwórz", "common_continue": "Kontynuuj", - "common_share": "UdostÄ™pnij", + "common_share": "Udostępnij", "common_copy": "Kopiuj", - "common_retry": "Spróbować", + "common_retry": "Spróbować", "common_hide": "Ukryj", - "common_remove": "UsuÅ„", - "common_enable": "Włącz", - "common_disable": "Wyłączyć", - "common_reboot": "Zrestartować", - "common_loading": "Ładowanie...", - "common_notAvailable": "—", + "common_remove": "Usuń", + "common_enable": "Włącz", + "common_disable": "Wyłączyć", + "common_reboot": "Zrestartować", + "common_loading": "Ładowanie...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Skanowanie urzÄ…dzeÅ„...", - "scanner_connecting": "Łączenie...", - "scanner_disconnecting": "Odłączanie...", - "scanner_notConnected": "Niepołączony", - "scanner_connectedTo": "Połączono z {deviceName}", + "scanner_scanning": "Skanowanie urządzeń...", + "scanner_connecting": "Łączenie...", + "scanner_disconnecting": "Odłączanie...", + "scanner_notConnected": "Niepołączony", + "scanner_connectedTo": "Połączono z {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,9 +65,9 @@ } } }, - "scanner_searchingDevices": "Wyszukiwanie urzÄ…dzeÅ„ MeshCore...", - "scanner_tapToScan": "NaciÅ›nij Skan, aby znaleźć urzÄ…dzenia MeshCore", - "scanner_connectionFailed": "Połączenie nieudane: {error}", + "scanner_searchingDevices": "Wyszukiwanie urządzeń MeshCore...", + "scanner_tapToScan": "Naciśnij Skan, aby znaleźć urządzenia MeshCore", + "scanner_connectionFailed": "Połączenie nieudane: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -80,43 +80,43 @@ "device_quickSwitch": "Szybka zmiana", "device_meshcore": "MeshCore", "settings_title": "Ustawienia", - "settings_deviceInfo": "Informacje o urzÄ…dzeniu", + "settings_deviceInfo": "Informacje o urządzeniu", "settings_appSettings": "Ustawienia aplikacji", - "settings_appSettingsSubtitle": "Powiadomienia, wiadomoÅ›ci i preferencje mapy", - "settings_nodeSettings": "Ustawienia wÄ™zÅ‚a", - "settings_nodeName": "Nazwa wÄ™zÅ‚a", + "settings_appSettingsSubtitle": "Powiadomienia, wiadomości i preferencje mapy", + "settings_nodeSettings": "Ustawienia węzła", + "settings_nodeName": "Nazwa węzła", "settings_nodeNameNotSet": "Nie ustawione", - "settings_nodeNameHint": "Wprowadź nazwÄ™ wÄ™zÅ‚a", - "settings_nodeNameUpdated": "ImiÄ™ zaktualizowane", + "settings_nodeNameHint": "Wprowadź nazwę węzła", + "settings_nodeNameUpdated": "Imię zaktualizowane", "settings_radioSettings": "Ustawienia radia", - "settings_radioSettingsSubtitle": "CzÄ™stotliwość, moc, współczynnik rozpraszania", - "settings_radioSettingsUpdated": "Ustawienia radia zostaÅ‚y zaktualizowane", + "settings_radioSettingsSubtitle": "Częstotliwość, moc, współczynnik rozpraszania", + "settings_radioSettingsUpdated": "Ustawienia radia zostały zaktualizowane", "settings_location": "Lokalizacja", "settings_locationSubtitle": "Koordynaty GPS", "settings_locationUpdated": "Lokalizacja zaktualizowana", - "settings_locationBothRequired": "Wprowadź zarówno szerokość, jak i dÅ‚ugość geograficznÄ….", - "settings_locationInvalid": "NieprawidÅ‚owa szerokość geograficzna lub dÅ‚ugość geograficzna.", - "settings_latitude": "Szerokość", - "settings_longitude": "DÅ‚ugość", + "settings_locationBothRequired": "Wprowadź zarówno szerokość, jak i długość geograficzną.", + "settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.", + "settings_latitude": "Szerokość", + "settings_longitude": "Długość", "settings_privacyMode": "Tryb Prywatny", - "settings_privacyModeSubtitle": "Ukryj imiÄ™/lokalizacjÄ™ w reklamach", - "settings_privacyModeToggle": "Włącz tryb prywatnoÅ›ci, aby ukryć swoje imiÄ™ i lokalizacjÄ™ w reklamach.", - "settings_privacyModeEnabled": "Tryb prywatnoÅ›ci włączony", - "settings_privacyModeDisabled": "Tryb prywatnoÅ›ci wyłączony", - "settings_actions": "DziaÅ‚ania", - "settings_sendAdvertisement": "WyÅ›lij ReklamÄ™", - "settings_sendAdvertisementSubtitle": "Obecność transmisji jest teraz", - "settings_advertisementSent": "Reklama wysÅ‚ana", + "settings_privacyModeSubtitle": "Ukryj imię/lokalizację w reklamach", + "settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w reklamach.", + "settings_privacyModeEnabled": "Tryb prywatności włączony", + "settings_privacyModeDisabled": "Tryb prywatności wyłączony", + "settings_actions": "Działania", + "settings_sendAdvertisement": "Wyślij Reklamę", + "settings_sendAdvertisementSubtitle": "Obecność transmisji jest teraz", + "settings_advertisementSent": "Reklama wysłana", "settings_syncTime": "Czas synchronizacji", - "settings_syncTimeSubtitle": "Ustaw zegar urzÄ…dzenia na czas telefonu.", + "settings_syncTimeSubtitle": "Ustaw zegar urządzenia na czas telefonu.", "settings_timeSynchronized": "Synchronizacja czasu", - "settings_refreshContacts": "OdÅ›wież Kontakty", - "settings_refreshContactsSubtitle": "OdÅ›wież listÄ™ kontaktów z urzÄ…dzenia", - "settings_rebootDevice": "Zrestartuj UrzÄ…dzenie", - "settings_rebootDeviceSubtitle": "Zrestartuj urzÄ…dzenie MeshCore", - "settings_rebootDeviceConfirm": "Czy na pewno chcesz zrestartować urzÄ…dzenie? BÄ™dziesz odłączony.", + "settings_refreshContacts": "Odśwież Kontakty", + "settings_refreshContactsSubtitle": "Odśwież listę kontaktów z urządzenia", + "settings_rebootDevice": "Zrestartuj Urządzenie", + "settings_rebootDeviceSubtitle": "Zrestartuj urządzenie MeshCore", + "settings_rebootDeviceConfirm": "Czy na pewno chcesz zrestartować urządzenie? Będziesz odłączony.", "settings_debug": "Debug", - "settings_bleDebugLog": "Log błędów BLE", + "settings_bleDebugLog": "Log błędów BLE", "settings_bleDebugLogSubtitle": "Polecenia BLE, odpowiedzi i surowe dane", "settings_appDebugLog": "Log Wykonywania Aplikacji", "settings_appDebugLogSubtitle": "Komunikaty debugowania aplikacji", @@ -130,25 +130,25 @@ } }, "settings_aboutLegalese": "Projekt MeshCore Open Source 2026", - "settings_aboutDescription": "Otwarty kod źródÅ‚owy klient Flutter dla urzÄ…dzeÅ„ do sieci mesh LoRa MeshCore.", - "settings_infoName": "ImiÄ™", + "settings_aboutDescription": "Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.", + "settings_infoName": "Imię", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Bateria", "settings_infoPublicKey": "Klucz Publiczny", - "settings_infoContactsCount": "Liczba kontaktów", - "settings_infoChannelCount": "Liczba kanałów", + "settings_infoContactsCount": "Liczba kontaktów", + "settings_infoChannelCount": "Liczba kanałów", "settings_presets": "Preset", - "settings_frequency": "CzÄ™stotliwość (MHz)", + "settings_frequency": "Częstotliwość (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", - "settings_frequencyInvalid": "NieprawidÅ‚owa czÄ™stotliwość (300-2500 MHz)", - "settings_bandwidth": "Przepustowość", - "settings_spreadingFactor": "RozkÅ‚ad Czynnika", + "settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)", + "settings_bandwidth": "Przepustowość", + "settings_spreadingFactor": "Rozkład Czynnika", "settings_codingRate": "Stawka Kodowania", "settings_txPower": "TX Moc (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "NieprawidÅ‚owa moc TX (0-22 dBm)", - "settings_error": "Błąd: {message}", + "settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)", + "settings_error": "Błąd: {message}", "@settings_error": { "placeholders": { "message": { @@ -157,50 +157,50 @@ } }, "appSettings_title": "Ustawienia aplikacji", - "appSettings_appearance": "WyglÄ…d", + "appSettings_appearance": "Wygląd", "appSettings_theme": "Motyw", - "appSettings_themeSystem": "DomyÅ›lne ustawienia systemu", + "appSettings_themeSystem": "Domyślne ustawienia systemu", "appSettings_themeLight": "Jasne", "appSettings_themeDark": "Ciemny", - "appSettings_language": "JÄ™zyk", - "appSettings_languageSystem": "DomyÅ›lny systemowy", + "appSettings_language": "Język", + "appSettings_languageSystem": "Domyślny systemowy", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Powiadomienia", - "appSettings_enableNotifications": "Włącz Powiadomienia", - "appSettings_enableNotificationsSubtitle": "Otrzymuj powiadomienia o wiadomoÅ›ciach i reklamach.", + "appSettings_enableNotifications": "Włącz Powiadomienia", + "appSettings_enableNotificationsSubtitle": "Otrzymuj powiadomienia o wiadomościach i reklamach.", "appSettings_notificationPermissionDenied": "Odmowa zezwolenia na powiadomienia", - "appSettings_notificationsEnabled": "Powiadomienia włączone", - "appSettings_notificationsDisabled": "Powiadomienia wyłączone", - "appSettings_messageNotifications": "Powiadomienia o wiadomoÅ›ciach", - "appSettings_messageNotificationsSubtitle": "Pokaż powiadomienie przy otrzymywaniu nowych wiadomoÅ›ci", - "appSettings_channelMessageNotifications": "Powiadomienia o WiadomoÅ›ciach na KanaÅ‚ach", - "appSettings_channelMessageNotificationsSubtitle": "Pokaż powiadomienie przy odbieraniu wiadomoÅ›ci z kanaÅ‚u", + "appSettings_notificationsEnabled": "Powiadomienia włączone", + "appSettings_notificationsDisabled": "Powiadomienia wyłączone", + "appSettings_messageNotifications": "Powiadomienia o wiadomościach", + "appSettings_messageNotificationsSubtitle": "Pokaż powiadomienie przy otrzymywaniu nowych wiadomości", + "appSettings_channelMessageNotifications": "Powiadomienia o Wiadomościach na Kanałach", + "appSettings_channelMessageNotificationsSubtitle": "Pokaż powiadomienie przy odbieraniu wiadomości z kanału", "appSettings_advertisementNotifications": "Powiadomienia Reklamowe", - "appSettings_advertisementNotificationsSubtitle": "WyÅ›wietl powiadomienie, gdy zostanÄ… odkryte nowe wÄ™zÅ‚y.", - "appSettings_messaging": "WiadomoÅ›ci", - "appSettings_clearPathOnMaxRetry": "Wyczyść ÅšcieżkÄ™ na Maksymalnej Próbie", - "appSettings_clearPathOnMaxRetrySubtitle": "Resetuj Å›cieżkÄ™ kontaktu po 5 nieudanych próbach wysÅ‚ania", - "appSettings_pathsWillBeCleared": "Droga bÄ™dzie wyczyszczona po 5 nieudanych próbach.", + "appSettings_advertisementNotificationsSubtitle": "Wyświetl powiadomienie, gdy zostaną odkryte nowe węzły.", + "appSettings_messaging": "Wiadomości", + "appSettings_clearPathOnMaxRetry": "Wyczyść Ścieżkę na Maksymalnej Próbie", + "appSettings_clearPathOnMaxRetrySubtitle": "Resetuj ścieżkę kontaktu po 5 nieudanych próbach wysłania", + "appSettings_pathsWillBeCleared": "Droga będzie wyczyszczona po 5 nieudanych próbach.", "appSettings_pathsWillNotBeCleared": "Droga nie zostanie automatycznie wyczyszczona.", "appSettings_autoRouteRotation": "Automatyczne Rotowanie Trasy", - "appSettings_autoRouteRotationSubtitle": "Przełączaj siÄ™ miÄ™dzy najlepszymi Å›cieżkami a trybem zalewowym.", - "appSettings_autoRouteRotationEnabled": "Automatyczne obracanie tras włączone", - "appSettings_autoRouteRotationDisabled": "Automatyczne obracanie tras wyłączone", + "appSettings_autoRouteRotationSubtitle": "Przełączaj się między najlepszymi ścieżkami a trybem zalewowym.", + "appSettings_autoRouteRotationEnabled": "Automatyczne obracanie tras włączone", + "appSettings_autoRouteRotationDisabled": "Automatyczne obracanie tras wyłączone", "appSettings_battery": "Bateria", "appSettings_batteryChemistry": "Chemia Baterii", - "appSettings_batteryChemistryPerDevice": "Ustawione na urzÄ…dzenie ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Ustawione na urządzenie ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Połącz siÄ™ z urzÄ…dzeniem, aby wybrać", + "appSettings_batteryChemistryConnectFirst": "Połącz się z urządzeniem, aby wybrać", "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65 V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", - "appSettings_mapDisplay": "WyÅ›wietlanie mapy", - "appSettings_showRepeaters": "Pokaż Powtórniki", - "appSettings_showRepeatersSubtitle": "WyÅ›wietl wÄ™zÅ‚y powtarzajÄ…ce siÄ™ na mapie", - "appSettings_showChatNodes": "Pokaż WÄ™zÅ‚y Rozmowy", - "appSettings_showChatNodesSubtitle": "WyÅ›wietl wÄ™zÅ‚y czatu na mapie", - "appSettings_showOtherNodes": "Pokaż inne wÄ™zÅ‚y", - "appSettings_showOtherNodesSubtitle": "WyÅ›wietl inne typy wÄ™złów na mapie", + "appSettings_mapDisplay": "Wyświetlanie mapy", + "appSettings_showRepeaters": "Pokaż Powtórniki", + "appSettings_showRepeatersSubtitle": "Wyświetl węzły powtarzające się na mapie", + "appSettings_showChatNodes": "Pokaż Węzły Rozmowy", + "appSettings_showChatNodesSubtitle": "Wyświetl węzły czatu na mapie", + "appSettings_showOtherNodes": "Pokaż inne węzły", + "appSettings_showOtherNodesSubtitle": "Wyświetl inne typy węzłów na mapie", "appSettings_timeFilter": "Filtrowanie Czasu", - "appSettings_timeFilterShowAll": "Pokaż wszystkie wÄ™zÅ‚y", - "appSettings_timeFilterShowLast": "Pokaż wÄ™zÅ‚y z ostatnich {hours} godzin", + "appSettings_timeFilterShowAll": "Pokaż wszystkie węzły", + "appSettings_timeFilterShowLast": "Pokaż węzły z ostatnich {hours} godzin", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,14 +230,14 @@ } }, "appSettings_mapTimeFilter": "Filtrowanie Czasu Mapy", - "appSettings_showNodesDiscoveredWithin": "Pokaż wÄ™zÅ‚y odkryte w:", + "appSettings_showNodesDiscoveredWithin": "Pokaż węzły odkryte w:", "appSettings_allTime": "Wszystko czasowo", "appSettings_lastHour": "Ostatnia godzina", "appSettings_last6Hours": "Ostatnie 6 godzin", "appSettings_last24Hours": "Ostatnie 24 godziny", - "appSettings_lastWeek": "TydzieÅ„ temu", + "appSettings_lastWeek": "Tydzień temu", "appSettings_offlineMapCache": "Bufor Map Offline", - "appSettings_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", + "appSettings_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", "appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { @@ -251,17 +251,17 @@ }, "appSettings_debugCard": "Debug", "appSettings_appDebugLogging": "Logowanie Debugowania Aplikacji", - "appSettings_appDebugLoggingSubtitle": "Loguj wiadomoÅ›ci debugowania aplikacji w celu rozwiÄ…zywania problemów.", - "appSettings_appDebugLoggingEnabled": "Zdebugowanie aplikacji włączone", - "appSettings_appDebugLoggingDisabled": "Zasubskrybowane logi debugowania aplikacji wyłączone.", + "appSettings_appDebugLoggingSubtitle": "Loguj wiadomości debugowania aplikacji w celu rozwiązywania problemów.", + "appSettings_appDebugLoggingEnabled": "Zdebugowanie aplikacji włączone", + "appSettings_appDebugLoggingDisabled": "Zasubskrybowane logi debugowania aplikacji wyłączone.", "contacts_title": "Kontakty", - "contacts_noContacts": "Brak jeszcze kontaktów.", - "contacts_contactsWillAppear": "Kontakty bÄ™dÄ… wyÅ›wietlane, gdy urzÄ…dzenia reklamujÄ… siÄ™.", + "contacts_noContacts": "Brak jeszcze kontaktów.", + "contacts_contactsWillAppear": "Kontakty będą wyświetlane, gdy urządzenia reklamują się.", "contacts_searchContacts": "Wyszukaj kontakty...", - "contacts_noUnreadContacts": "Brak nieprzeczytanych kontaktów", - "contacts_noContactsFound": "Brak znalezionych kontaktów ani grup.", - "contacts_deleteContact": "UsuÅ„ Kontakt", - "contacts_removeConfirm": "UsuÅ„ {contactName} z kontaktów?", + "contacts_noUnreadContacts": "Brak nieprzeczytanych kontaktów", + "contacts_noContactsFound": "Brak znalezionych kontaktów ani grup.", + "contacts_deleteContact": "Usuń Kontakt", + "contacts_removeConfirm": "Usuń {contactName} z kontaktów?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -269,12 +269,12 @@ } } }, - "contacts_manageRepeater": "ZarzÄ…dzaj Powtórzami", + "contacts_manageRepeater": "Zarządzaj Powtórzami", "contacts_roomLogin": "Logowanie do pokoju", - "contacts_openChat": "Otwórz czat", - "contacts_editGroup": "Edytuj GrupÄ™", - "contacts_deleteGroup": "UsuÅ„ GrupÄ™", - "contacts_deleteGroupConfirm": "UsuÅ„ \"{groupName}\"?", + "contacts_openChat": "Otwórz czat", + "contacts_editGroup": "Edytuj Grupę", + "contacts_deleteGroup": "Usuń Grupę", + "contacts_deleteGroupConfirm": "Usuń \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -285,7 +285,7 @@ "contacts_newGroup": "Nowa Grupa", "contacts_groupName": "Nazwa grupy", "contacts_groupNameRequired": "Nazwa grupy jest wymagana", - "contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje", + "contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,10 +294,10 @@ } }, "contacts_filterContacts": "Filtruj kontakty...", - "contacts_noContactsMatchFilter": "Brak pasujÄ…cych kontaktów do Twojego filtra", - "contacts_noMembers": "Brak czÅ‚onków", - "contacts_lastSeenNow": "Ostatnie połączenie", - "contacts_lastSeenMinsAgo": "Ostatnie połączenie {minutes} min temu", + "contacts_noContactsMatchFilter": "Brak pasujących kontaktów do Twojego filtra", + "contacts_noMembers": "Brak członków", + "contacts_lastSeenNow": "Ostatnie połączenie", + "contacts_lastSeenMinsAgo": "Ostatnie połączenie {minutes} min temu", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Ostatni raz widziany 1 godzinÄ™ temu", - "contacts_lastSeenHoursAgo": "Ostatnie połączenie {hours} godzin temu", + "contacts_lastSeenHourAgo": "Ostatni raz widziany 1 godzinę temu", + "contacts_lastSeenHoursAgo": "Ostatnie połączenie {hours} godzin temu", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Ostatni raz widziany 1 dzieÅ„ temu", - "contacts_lastSeenDaysAgo": "Ostatnie połączenie {days} dni temu", + "contacts_lastSeenDayAgo": "Ostatni raz widziany 1 dzień temu", + "contacts_lastSeenDaysAgo": "Ostatnie połączenie {days} dni temu", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -323,12 +323,12 @@ } } }, - "channels_title": "KanaÅ‚y", - "channels_noChannelsConfigured": "Brak skonfigurowanych kanałów", - "channels_addPublicChannel": "Dodaj kanaÅ‚ publiczny", - "channels_searchChannels": "Wyszukaj kanaÅ‚y...", - "channels_noChannelsFound": "Brak znalezionych kanałów", - "channels_channelIndex": "KanaÅ‚ {index}", + "channels_title": "Kanały", + "channels_noChannelsConfigured": "Brak skonfigurowanych kanałów", + "channels_addPublicChannel": "Dodaj kanał publiczny", + "channels_searchChannels": "Wyszukaj kanały...", + "channels_noChannelsFound": "Brak znalezionych kanałów", + "channels_channelIndex": "Kanał {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -336,16 +336,16 @@ } } }, - "channels_hashtagChannel": "KanaÅ‚ z hashtagami", + "channels_hashtagChannel": "Kanał z hashtagami", "channels_public": "Publiczny", "channels_private": "Prywatne", - "channels_publicChannel": "KanaÅ‚ publiczny", - "channels_privateChannel": "Prywatny kanaÅ‚", - "channels_editChannel": "Edytuj kanaÅ‚", - "channels_muteChannel": "Wycisz kanaÅ‚", - "channels_unmuteChannel": "Wyłącz wyciszenie kanaÅ‚u", - "channels_deleteChannel": "UsuÅ„ kanaÅ‚", - "channels_deleteChannelConfirm": "UsuÅ„ \"{name}\"? Nie można tego cofnąć.", + "channels_publicChannel": "Kanał publiczny", + "channels_privateChannel": "Prywatny kanał", + "channels_editChannel": "Edytuj kanał", + "channels_muteChannel": "Wycisz kanał", + "channels_unmuteChannel": "Wyłącz wyciszenie kanału", + "channels_deleteChannel": "Usuń kanał", + "channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "KanaÅ‚ \"{name}\" usuniÄ™to", + "channels_channelDeleted": "Kanał \"{name}\" usunięto", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "Dodaj KanaÅ‚", - "channels_channelIndexLabel": "Indeks kanaÅ‚u", - "channels_channelName": "Nazwa kanaÅ‚u", - "channels_usePublicChannel": "Użyj kanaÅ‚u publicznego", + "channels_addChannel": "Dodaj Kanał", + "channels_channelIndexLabel": "Indeks kanału", + "channels_channelName": "Nazwa kanału", + "channels_usePublicChannel": "Użyj kanału publicznego", "channels_standardPublicPsk": "Standard public PSK", "channels_pskHex": "PSK (Hex)", "channels_generateRandomPsk": "Wygeneruj losowy klucz PSK", - "channels_enterChannelName": "ProszÄ™ podać nazwÄ™ kanaÅ‚u.", - "channels_pskMustBe32Hex": "PSK musi mieć 32 znaki szesnastkowe.", - "channels_channelAdded": "KanaÅ‚ \"{name}\" dodany", + "channels_enterChannelName": "Proszę podać nazwę kanału.", + "channels_pskMustBe32Hex": "PSK musi mieć 32 znaki szesnastkowe.", + "channels_channelAdded": "Kanał \"{name}\" dodany", "@channels_channelAdded": { "placeholders": { "name": { @@ -378,7 +378,7 @@ } } }, - "channels_editChannelTitle": "Edytuj KanaÅ‚ {index}", + "channels_editChannelTitle": "Edytuj Kanał {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -387,7 +387,7 @@ } }, "channels_smazCompression": "Kompresja SMAZ", - "channels_channelUpdated": "KanaÅ‚ \"{name}\" zostaÅ‚ zaktualizowany", + "channels_channelUpdated": "Kanał \"{name}\" został zaktualizowany", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,15 +395,15 @@ } } }, - "channels_publicChannelAdded": "KanaÅ‚ publiczny dodany", + "channels_publicChannelAdded": "Kanał publiczny dodany", "channels_sortBy": "Sortuj po", - "channels_sortManual": "RÄ™czna", + "channels_sortManual": "Ręczna", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Najnowsze wiadomoÅ›ci", - "channels_sortUnread": "NiezgÅ‚oszone", - "chat_noMessages": "Brak jeszcze wiadomoÅ›ci", - "chat_sendMessageToStart": "WyÅ›lij wiadomość, aby rozpocząć.", - "chat_originalMessageNotFound": "Błąd: Nie znaleziono oryginalnego komunikatu", + "channels_sortLatestMessages": "Najnowsze wiadomości", + "channels_sortUnread": "Niezgłoszone", + "chat_noMessages": "Brak jeszcze wiadomości", + "chat_sendMessageToStart": "Wyślij wiadomość, aby rozpocząć.", + "chat_originalMessageNotFound": "Błąd: Nie znaleziono oryginalnego komunikatu", "chat_replyingTo": "Odpowiadanie na {name}", "@chat_replyingTo": { "placeholders": { @@ -421,7 +421,7 @@ } }, "chat_location": "Lokalizacja", - "chat_sendMessageTo": "WyÅ›lij wiadomość do {contactName}", + "chat_sendMessageTo": "Wyślij wiadomość do {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "Wpisz wiadomość...", - "chat_messageTooLong": "Wiadomość jest za dÅ‚uga (maksymalnie {maxBytes} bajtów).", + "chat_typeMessage": "Wpisz wiadomość...", + "chat_messageTooLong": "Wiadomość jest za długa (maksymalnie {maxBytes} bajtów).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,10 +438,10 @@ } } }, - "chat_messageCopied": "Wiadomość skopiowana", - "chat_messageDeleted": "Wiadomość usuniÄ™ta", - "chat_retryingMessage": "Próba ponowienia", - "chat_retryCount": "Spróbuj {current}/{max}", + "chat_messageCopied": "Wiadomość skopiowana", + "chat_messageDeleted": "Wiadomość usunięta", + "chat_retryingMessage": "Próba ponowienia", + "chat_retryCount": "Spróbuj {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -452,9 +452,9 @@ } } }, - "chat_sendGif": "WyÅ›lij GIF", + "chat_sendGif": "Wyślij GIF", "chat_reply": "Odpowiedz", - "chat_addReaction": "Dodaj ReakcjÄ™", + "chat_addReaction": "Dodaj Reakcję", "chat_me": "Ja", "emojiCategorySmileys": "Emoji", "emojiCategoryGestures": "Gestikulacje", @@ -463,22 +463,22 @@ "gifPicker_title": "Wybierz GIF", "gifPicker_searchHint": "Wyszukaj GIF-y...", "gifPicker_poweredBy": "Zasilane przez GIPHY", - "gifPicker_noGifsFound": "Nie znaleziono GIF-ów", - "gifPicker_failedLoad": "Nie udaÅ‚o siÄ™ zaÅ‚adować GIF-ów", - "gifPicker_failedSearch": "Nie udaÅ‚o siÄ™ znaleźć GIF-ów", - "gifPicker_noInternet": "Brak połączenia internetowego", + "gifPicker_noGifsFound": "Nie znaleziono GIF-ów", + "gifPicker_failedLoad": "Nie udało się załadować GIF-ów", + "gifPicker_failedSearch": "Nie udało się znaleźć GIF-ów", + "gifPicker_noInternet": "Brak połączenia internetowego", "debugLog_appTitle": "Log Wykonywania Aplikacji", - "debugLog_bleTitle": "Log błędów BLE", + "debugLog_bleTitle": "Log błędów BLE", "debugLog_copyLog": "Kopiuj log", - "debugLog_clearLog": "Wyczyść dziennik", + "debugLog_clearLog": "Wyczyść dziennik", "debugLog_copied": "Skopiowano dziennik debugowania", "debugLog_bleCopied": "Skopiowany log BLE", - "debugLog_noEntries": "Nie ma jeszcze żadnych logów debugowania.", - "debugLog_enableInSettings": "Włącz logowanie debugowania aplikacji w ustawieniach", + "debugLog_noEntries": "Nie ma jeszcze żadnych logów debugowania.", + "debugLog_enableInSettings": "Włącz logowanie debugowania aplikacji w ustawieniach", "debugLog_frames": "Ramy", "debugLog_rawLogRx": "Surowe Log-RX", - "debugLog_noBleActivity": "Brak aktywnoÅ›ci BLE jeszcze.", - "debugFrame_length": "DÅ‚ugość ramy: {count} bajtów", + "debugLog_noBleActivity": "Brak aktywności BLE jeszcze.", + "debugFrame_length": "Długość ramy: {count} bajtów", "@debugFrame_length": { "placeholders": { "count": { @@ -494,7 +494,7 @@ } } }, - "debugFrame_textMessageHeader": "Wiadomość tekstowa:", + "debugFrame_textMessageHeader": "Wiadomość tekstowa:", "debugFrame_destinationPubKey": "- Oznaczenie PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { @@ -540,13 +540,13 @@ } } }, - "debugFrame_hexDump": "WyjÅ›cie SzESZCZNULNE:", - "chat_pathManagement": "ZarzÄ…dzanie Å›cieżkami", + "debugFrame_hexDump": "Wyjście SzESZCZNULNE:", + "chat_pathManagement": "Zarządzanie ścieżkami", "chat_routingMode": "Tryb routingu", - "chat_autoUseSavedPath": "Automatyczne (użyj zapisanej Å›cieżki)", + "chat_autoUseSavedPath": "Automatyczne (użyj zapisanej ścieżki)", "chat_forceFloodMode": "Wymusz Tryb Powodowany", - "chat_recentAckPaths": "Ostatnie Å›cieżki ACK (naciÅ›nij, aby użyć):", - "chat_pathHistoryFull": "Historia Å›cieżek jest peÅ‚na. UsuÅ„ wpisy, aby dodać nowe.", + "chat_recentAckPaths": "Ostatnie ścieżki ACK (naciśnij, aby użyć):", + "chat_pathHistoryFull": "Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.", "chat_hopSingular": "Skacz", "chat_hopPlural": "skoczkowie", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -558,19 +558,19 @@ } }, "chat_successes": "Sukcesy", - "chat_removePath": "UsuÅ„ Å›cieżkÄ™", - "chat_noPathHistoryYet": "Brak jeszcze historii Å›cieżek.\nWyÅ›lij wiadomość, aby odkryć Å›cieżki.", - "chat_pathActions": "DziaÅ‚ania Å›cieżki:", - "chat_setCustomPath": "Ustaw ÅšcieżkÄ™ DostosowanÄ…", - "chat_setCustomPathSubtitle": "RÄ™cznie okreÅ›l trasÄ™.", - "chat_clearPath": "Wyczyść ÅšcieżkÄ™", - "chat_clearPathSubtitle": "Zmusz do ponownej identyfikacji przy nastÄ™pnym wysÅ‚aniu", - "chat_pathCleared": "Åšcieżka oczyszczona. Kolejne powiadomienie odnajdzie trasÄ™.", - "chat_floodModeSubtitle": "Użyj przełącznika routingu w pasku narzÄ™dzi.", - "chat_floodModeEnabled": "Tryb powodziowy włączony. Włącz ponownie za pomocÄ… ikony routingu w pasku narzÄ™dzi.", - "chat_fullPath": "PeÅ‚na Å›cieżka", - "chat_pathDetailsNotAvailable": "Szczegóły Å›cieżki jeszcze niedostÄ™pne. Spróbuj wysÅ‚ać wiadomość, aby odÅ›wieżyć.", - "chat_pathSetHops": "Åšcieżka ustawiona: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_removePath": "Usuń ścieżkę", + "chat_noPathHistoryYet": "Brak jeszcze historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.", + "chat_pathActions": "Działania ścieżki:", + "chat_setCustomPath": "Ustaw Ścieżkę Dostosowaną", + "chat_setCustomPathSubtitle": "Ręcznie określ trasę.", + "chat_clearPath": "Wyczyść Ścieżkę", + "chat_clearPathSubtitle": "Zmusz do ponownej identyfikacji przy następnym wysłaniu", + "chat_pathCleared": "Ścieżka oczyszczona. Kolejne powiadomienie odnajdzie trasę.", + "chat_floodModeSubtitle": "Użyj przełącznika routingu w pasku narzędzi.", + "chat_floodModeEnabled": "Tryb powodziowy włączony. Włącz ponownie za pomocą ikony routingu w pasku narzędzi.", + "chat_fullPath": "Pełna ścieżka", + "chat_pathDetailsNotAvailable": "Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.", + "chat_pathSetHops": "Ścieżka ustawiona: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Zapisano lokalnie. Połącz siÄ™, aby zsynchronizować.", - "chat_pathDeviceConfirmed": "UrzÄ…dzenie potwierdzone.", - "chat_pathDeviceNotConfirmed": "UrzÄ…dzenie nie zostaÅ‚o jeszcze potwierdzone.", - "chat_type": "Wprowadź", - "chat_path": "Åšcieżka", + "chat_pathSavedLocally": "Zapisano lokalnie. Połącz się, aby zsynchronizować.", + "chat_pathDeviceConfirmed": "Urządzenie potwierdzone.", + "chat_pathDeviceNotConfirmed": "Urządzenie nie zostało jeszcze potwierdzone.", + "chat_type": "Wprowadź", + "chat_path": "Ścieżka", "chat_publicKey": "Klucz Publiczny", - "chat_compressOutgoingMessages": "Kompresuj wychodzÄ…ce wiadomoÅ›ci", - "chat_floodForced": "Powodowana Powódź", - "chat_directForced": "BezpoÅ›rednio (wymuszono)", - "chat_hopsForced": "{count} skoków (wymuszonych)", + "chat_compressOutgoingMessages": "Kompresuj wychodzące wiadomości", + "chat_floodForced": "Powodowana Powódź", + "chat_directForced": "Bezpośrednio (wymuszono)", + "chat_hopsForced": "{count} skoków (wymuszonych)", "@chat_hopsForced": { "placeholders": { "count": { @@ -599,9 +599,9 @@ } }, "chat_floodAuto": "Powodzie (automatyczne)", - "chat_direct": "BezpoÅ›rednio", - "chat_poiShared": "Wspólny POI", - "chat_unread": "NiezgÅ‚oszone: {count}", + "chat_direct": "Bezpośrednio", + "chat_poiShared": "Wspólny POI", + "chat_unread": "Niezgłoszone: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Otworzyć link?", - "chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglÄ…darce?", - "chat_open": "Otwórz", - "chat_couldNotOpenLink": "Nie można otworzyć linku: {url}", + "chat_openLink": "Otworzyć link?", + "chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?", + "chat_open": "Otwórz", + "chat_couldNotOpenLink": "Nie można otworzyć linku: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,11 +620,11 @@ } } }, - "chat_invalidLink": "NieprawidÅ‚owy format linku", - "map_title": "Mapa wÄ™złów", - "map_noNodesWithLocation": "Brak wÄ™złów z danymi lokalizacyjnymi", - "map_nodesNeedGps": "WÄ™zÅ‚y muszÄ… udostÄ™pniać swoje współrzÄ™dne GPS,\naby pojawić siÄ™ na mapie.", - "map_nodesCount": "WÄ™zÅ‚y: {count}", + "chat_invalidLink": "Nieprawidłowy format linku", + "map_title": "Mapa węzłów", + "map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi", + "map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.", + "map_nodesCount": "Węzły: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -641,26 +641,26 @@ } }, "map_chat": "Rozmowa", - "map_repeater": "Powtórzacz", - "map_room": "Pokój", + "map_repeater": "Powtórzacz", + "map_room": "Pokój", "map_sensor": "Czujnik", "map_pinDm": "Zablokuj (DM)", "map_pinPrivate": "Zablokuj (Prywatnie)", "map_pinPublic": "Oznacz jako publiczne", "map_lastSeen": "Ostatni raz widziany", - "map_disconnectConfirm": "Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?", + "map_disconnectConfirm": "Czy na pewno chcesz się odłączyć od tego urządzenia?", "map_from": "Od", - "map_source": "ŹródÅ‚o", + "map_source": "Źródło", "map_flags": "Flagi", - "map_shareMarkerHere": "UdostÄ™pnij znacznik tutaj", - "map_pinLabel": "Oznacz etykietÄ™", + "map_shareMarkerHere": "Udostępnij znacznik tutaj", + "map_pinLabel": "Oznacz etykietę", "map_label": "Etykieta", - "map_pointOfInterest": "Punkt zainteresowaÅ„", - "map_sendToContact": "WyÅ›lij do kontaktu", - "map_sendToChannel": "WyÅ›lij do kanaÅ‚u", - "map_noChannelsAvailable": "Brak dostÄ™pnych kanałów", - "map_publicLocationShare": "UdostÄ™pnij lokalizacjÄ™ publicznie", - "map_publicLocationShareConfirm": "Wkrótce udostÄ™pnisz lokalizacjÄ™ w {channelLabel}. Ten kanaÅ‚ jest publiczny i każdy z PSK może go zobaczyć.", + "map_pointOfInterest": "Punkt zainteresowań", + "map_sendToContact": "Wyślij do kontaktu", + "map_sendToChannel": "Wyślij do kanału", + "map_noChannelsAvailable": "Brak dostępnych kanałów", + "map_publicLocationShare": "Udostępnij lokalizację publicznie", + "map_publicLocationShareConfirm": "Wkrótce udostępnisz lokalizację w {channelLabel}. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Połącz siÄ™ z urzÄ…dzeniem, aby udostÄ™pniać znacznik.", - "map_filterNodes": "Filtruj WÄ™zÅ‚y", - "map_nodeTypes": "Typy wÄ™złów", - "map_chatNodes": "WÄ™zÅ‚y czatu", + "map_connectToShareMarkers": "Połącz się z urządzeniem, aby udostępniać znacznik.", + "map_filterNodes": "Filtruj Węzły", + "map_nodeTypes": "Typy węzłów", + "map_chatNodes": "Węzły czatu", "map_repeaters": "Powtarzacze", - "map_otherNodes": "Inne wÄ™zÅ‚y", + "map_otherNodes": "Inne węzły", "map_keyPrefix": "Prefiks klucza", "map_filterByKeyPrefix": "Filtruj po prefiksie klucza", - "map_publicKeyPrefix": "Przewód klucza publicznego", + "map_publicKeyPrefix": "Przewód klucza publicznego", "map_markers": "Oznaczarki", - "map_showSharedMarkers": "Pokaż współdzielone znaki.", + "map_showSharedMarkers": "Pokaż współdzielone znaki.", "map_lastSeenTime": "Ostatni raz widiany", "map_sharedPin": "Podzielony PIN", - "map_joinRoom": "Dołącz do pokoju", - "map_manageRepeater": "ZarzÄ…dzaj Powtórzami", + "map_joinRoom": "Dołącz do pokoju", + "map_manageRepeater": "Zarządzaj Powtórzami", "mapCache_title": "Bufor Map Offline", - "mapCache_selectAreaFirst": "Wybierz obszar do wstÄ™pnego pobrania.", - "mapCache_noTilesToDownload": "Brak dostÄ™pnych pÅ‚ytek do pobrania dla tego obszaru.", - "mapCache_downloadTilesTitle": "Pobierz pÅ‚ytki", - "mapCache_downloadTilesPrompt": "Pobierz {count} pÅ‚ytek do użytku offline?", + "mapCache_selectAreaFirst": "Wybierz obszar do wstępnego pobrania.", + "mapCache_noTilesToDownload": "Brak dostępnych płytek do pobrania dla tego obszaru.", + "mapCache_downloadTilesTitle": "Pobierz płytki", + "mapCache_downloadTilesPrompt": "Pobierz {count} płytek do użytku offline?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -696,7 +696,7 @@ } }, "mapCache_downloadAction": "Pobierz", - "mapCache_cachedTiles": "PamiÄ™tanych {count} pÅ‚ytek", + "mapCache_cachedTiles": "Pamiętanych {count} płytek", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "PamiÄ™tane {downloaded} pÅ‚ytki ({failed} nieudane)", + "mapCache_cachedTilesWithFailed": "Pamiętane {downloaded} płytki ({failed} nieudane)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Wyczyść pamięć podrÄ™cznÄ… offline", - "mapCache_clearOfflineCachePrompt": "UsuÅ„ wszystkie tymczasowe kafelki mapy?", - "mapCache_offlineCacheCleared": "Pamięć podrÄ™czna offline zostaÅ‚a wyczyszczona", - "mapCache_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", - "mapCache_cacheArea": "Obszar pamiÄ™ci podrÄ™cznej", - "mapCache_useCurrentView": "Użyj aktualnego widoku", - "mapCache_zoomRange": "Zakres powiÄ™kszenia", - "mapCache_estimatedTiles": "Szacunkowa liczba pÅ‚ytek: {count}", + "mapCache_clearOfflineCacheTitle": "Wyczyść pamięć podręczną offline", + "mapCache_clearOfflineCachePrompt": "Usuń wszystkie tymczasowe kafelki mapy?", + "mapCache_offlineCacheCleared": "Pamięć podręczna offline została wyczyszczona", + "mapCache_noAreaSelected": "Nie zaznaczono żadnej powierzchni.", + "mapCache_cacheArea": "Obszar pamięci podręcznej", + "mapCache_useCurrentView": "Użyj aktualnego widoku", + "mapCache_zoomRange": "Zakres powiększenia", + "mapCache_estimatedTiles": "Szacunkowa liczba płytek: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -742,7 +742,7 @@ } }, "mapCache_downloadTilesButton": "Pobierz Paski", - "mapCache_clearCacheButton": "Wyczyść pamięć podrÄ™cznÄ…", + "mapCache_clearCacheButton": "Wyczyść pamięć podręczną", "mapCache_failedDownloads": "Nieudane pobrania: {count}", "@mapCache_failedDownloads": { "placeholders": { @@ -768,7 +768,7 @@ } } }, - "time_justNow": "WÅ‚aÅ›nie teraz", + "time_justNow": "Właśnie teraz", "time_minutesAgo": "{minutes} minut temu", "@time_minutesAgo": { "placeholders": { @@ -795,31 +795,31 @@ }, "time_hour": "godzina", "time_hours": "godziny", - "time_day": "dzieÅ„", + "time_day": "dzień", "time_days": "dni", - "time_week": "tydzieÅ„", + "time_week": "tydzień", "time_weeks": "tygodnie", - "time_month": "miesiÄ…c", + "time_month": "miesiąc", "time_months": "miesiace", "time_minutes": "minuty", "time_allTime": "Wszystko czasowo", - "dialog_disconnect": "Odłącz", - "dialog_disconnectConfirm": "Czy na pewno chcesz siÄ™ odłączyć od tego urzÄ…dzenia?", - "login_repeaterLogin": "Powtórz Logowanie", + "dialog_disconnect": "Odłącz", + "dialog_disconnectConfirm": "Czy na pewno chcesz się odłączyć od tego urządzenia?", + "login_repeaterLogin": "Powtórz Logowanie", "login_roomLogin": "Logowanie do pokoju", - "login_password": "HasÅ‚o", - "login_enterPassword": "Wprowadź hasÅ‚o", - "login_savePassword": "Zapisz hasÅ‚o", - "login_savePasswordSubtitle": "HasÅ‚o bÄ™dzie bezpiecznie przechowywane na tym urzÄ…dzeniu.", - "login_repeaterDescription": "Wprowadź hasÅ‚o do powtarzacza, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.", - "login_roomDescription": "Wprowadź hasÅ‚o do pokoju, aby uzyskać dostÄ™p do ustawieÅ„ i statusu.", + "login_password": "Hasło", + "login_enterPassword": "Wprowadź hasło", + "login_savePassword": "Zapisz hasło", + "login_savePasswordSubtitle": "Hasło będzie bezpiecznie przechowywane na tym urządzeniu.", + "login_repeaterDescription": "Wprowadź hasło do powtarzacza, aby uzyskać dostęp do ustawień i statusu.", + "login_roomDescription": "Wprowadź hasło do pokoju, aby uzyskać dostęp do ustawień i statusu.", "login_routing": "Przekierowanie", "login_routingMode": "Tryb routingu", - "login_autoUseSavedPath": "Automatycznie (użyj zapisanej Å›cieżki)", + "login_autoUseSavedPath": "Automatycznie (użyj zapisanej ścieżki)", "login_forceFloodMode": "Wymusz Tryb Powodowany", - "login_managePaths": "ZarzÄ…dzaj Åšcieżkami", - "login_login": "Zaloguj siÄ™", - "login_attempt": "Próba {current}/{max}", + "login_managePaths": "Zarządzaj Ścieżkami", + "login_login": "Zaloguj się", + "login_attempt": "Próba {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Zalogowanie siÄ™ nie powiodÅ‚o: {error}", + "login_failed": "Zalogowanie się nie powiodło: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Logowanie nie powiodÅ‚o siÄ™. HasÅ‚o jest nieprawidÅ‚owe albo repeater jest nieosiÄ…galny.", - "common_reload": "Ponownie zaÅ‚adować", - "common_clear": "Wyczyść", - "path_currentPath": "Aktualny Å›cieżka: {path}", + "login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.", + "common_reload": "Ponownie załadować", + "common_clear": "Wyczyść", + "path_currentPath": "Aktualny ścieżka: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Użyj Å›cieżki {count} {count, plural, =1{hop} other{hops}}.", + "path_usingHopsPath": "Użyj ścieżki {count} {count, plural, =1{hop} other{hops}}.", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Wprowadź wÅ‚asnÄ… Å›cieżkÄ™", - "path_currentPathLabel": "Aktualny Å›cieżka", - "path_hexPrefixInstructions": "Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.", - "path_hexPrefixExample": "A1,F2,3C (każedy wÄ™zeÅ‚ używa pierwszego bajtu swojego klucza publicznego)", - "path_labelHexPrefixes": "Åšcieżka (przesuniÄ™cia bitowe)", - "path_helperMaxHops": "Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).", - "path_selectFromContacts": "Albo wybierz z kontaktów:", - "path_noRepeatersFound": "Nie znaleziono repeaterów ani serwerów pokoi.", - "path_customPathsRequire": "Dostosowane Å›cieżki wymagajÄ… poÅ›rednich skoków, które mogÄ… przekazywać wiadomoÅ›ci.", - "path_invalidHexPrefixes": "NieprawidÅ‚owe prefiksy szesnastkowe: {prefixes}", + "path_enterCustomPath": "Wprowadź własną ścieżkę", + "path_currentPathLabel": "Aktualny ścieżka", + "path_hexPrefixInstructions": "Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.", + "path_hexPrefixExample": "A1,F2,3C (każedy węzeł używa pierwszego bajtu swojego klucza publicznego)", + "path_labelHexPrefixes": "Ścieżka (przesunięcia bitowe)", + "path_helperMaxHops": "Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).", + "path_selectFromContacts": "Albo wybierz z kontaktów:", + "path_noRepeatersFound": "Nie znaleziono repeaterów ani serwerów pokoi.", + "path_customPathsRequire": "Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.", + "path_invalidHexPrefixes": "Nieprawidłowe prefiksy szesnastkowe: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Åšcieżka jest zbyt dÅ‚uga. Dozwolonych skoków wynosi 64.", - "path_setPath": "Ustaw ÅšcieżkÄ™", - "repeater_management": "ZarzÄ…dzanie Powtórzami", - "repeater_managementTools": "NarzÄ™dzia ZarzÄ…dzania", + "path_tooLong": "Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.", + "path_setPath": "Ustaw Ścieżkę", + "repeater_management": "Zarządzanie Powtórzami", + "repeater_managementTools": "Narzędzia Zarządzania", "repeater_status": "Status", - "repeater_statusSubtitle": "WyÅ›wietl status powtarzacza, statystyki i sÄ…siadów.", + "repeater_statusSubtitle": "Wyświetl status powtarzacza, statystyki i sąsiadów.", "repeater_telemetry": "Telemetry", - "repeater_telemetrySubtitle": "WyÅ›wietl dane telemetryczne z czujników i statystyki systemu", + "repeater_telemetrySubtitle": "Wyświetl dane telemetryczne z czujników i statystyki systemu", "repeater_cli": "CLI", - "repeater_cliSubtitle": "WyÅ›lij polecenia do powielacza", + "repeater_cliSubtitle": "Wyślij polecenia do powielacza", "repeater_settings": "Ustawienia", "repeater_settingsSubtitle": "Skonfiguruj parametry powtarzacza", "repeater_statusTitle": "Status powtarzacza", "repeater_routingMode": "Tryb routingu", - "repeater_autoUseSavedPath": "Automatycznie (użyj zapisanej Å›cieżki)", + "repeater_autoUseSavedPath": "Automatycznie (użyj zapisanej ścieżki)", "repeater_forceFloodMode": "Wymusz Tryb Powodowany", - "repeater_pathManagement": "ZarzÄ…dzanie Å›cieżkami", - "repeater_refresh": "OdÅ›wież", - "repeater_statusRequestTimeout": "Å»yczenie statusu timed out.", - "repeater_errorLoadingStatus": "Błąd podczas Å‚adowania statusu: {error}", + "repeater_pathManagement": "Zarządzanie ścieżkami", + "repeater_refresh": "Odśwież", + "repeater_statusRequestTimeout": "Życzenie statusu timed out.", + "repeater_errorLoadingStatus": "Błąd podczas ładowania statusu: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -904,19 +904,19 @@ "repeater_systemInformation": "Informacje o systemie", "repeater_battery": "Bateria", "repeater_clockAtLogin": "Godzina (przy logowaniu)", - "repeater_uptime": "DostÄ™pność", - "repeater_queueLength": "DÅ‚ugość kolejki", + "repeater_uptime": "Dostępność", + "repeater_queueLength": "Długość kolejki", "repeater_debugFlags": "Opcje debugowania", "repeater_radioStatistics": "Statystyki Radia", "repeater_lastRssi": "Ostatni RSSI", "repeater_lastSnr": "Ostatnie SNR", - "repeater_noiseFloor": "Poziom Szumów", + "repeater_noiseFloor": "Poziom Szumów", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Statystyki pakietów", - "repeater_sent": "WysÅ‚ane", + "repeater_packetStatistics": "Statystyki pakietów", + "repeater_sent": "Wysłane", "repeater_received": "Otrzymano", - "repeater_duplicates": "Powtórzenia", + "repeater_duplicates": "Powtórzenia", "repeater_daysHoursMinsSecs": "{days} dni {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Razem: {total}, Powodzenie: {flood}, BezpoÅ›rednio: {direct}", + "repeater_packetTxTotal": "Razem: {total}, Powodzenie: {flood}, Bezpośrednio: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Razem: {total}, Powodzenie: {flood}, BezpoÅ›rednio: {direct}", + "repeater_packetRxTotal": "Razem: {total}, Powodzenie: {flood}, Bezpośrednio: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Powodzie: {flood}, BezpoÅ›rednie: {direct}", + "repeater_duplicatesFloodDirect": "Powodzie: {flood}, Bezpośrednie: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,36 +981,36 @@ } } }, - "repeater_settingsTitle": "Ustawienia Powtórki", + "repeater_settingsTitle": "Ustawienia Powtórki", "repeater_basicSettings": "Podstawowe Ustawienia", - "repeater_repeaterName": "Nazwa Powtórnika", - "repeater_repeaterNameHelper": "WyÅ›wietl nazwÄ™ tego powtarzacza", - "repeater_adminPassword": "HasÅ‚o Administracyjne", - "repeater_adminPasswordHelper": "PeÅ‚ny dostÄ™p hasÅ‚o", - "repeater_guestPassword": "HasÅ‚o goÅ›cia", - "repeater_guestPasswordHelper": "DostÄ™p tylko do odczytu hasÅ‚o", + "repeater_repeaterName": "Nazwa Powtórnika", + "repeater_repeaterNameHelper": "Wyświetl nazwę tego powtarzacza", + "repeater_adminPassword": "Hasło Administracyjne", + "repeater_adminPasswordHelper": "Pełny dostęp hasło", + "repeater_guestPassword": "Hasło gościa", + "repeater_guestPasswordHelper": "Dostęp tylko do odczytu hasło", "repeater_radioSettings": "Ustawienia radia", - "repeater_frequencyMhz": "CzÄ™stotliwość (MHz)", + "repeater_frequencyMhz": "Częstotliwość (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Przepustowość", - "repeater_spreadingFactor": "RozkÅ‚ad Czynnika", + "repeater_bandwidth": "Przepustowość", + "repeater_spreadingFactor": "Rozkład Czynnika", "repeater_codingRate": "Stawka kodowania", "repeater_locationSettings": "Ustawienia Lokalizacji", - "repeater_latitude": "Szerokość", - "repeater_latitudeHelper": "Stopnie dziesiÄ™tne (np. 37.7749)", - "repeater_longitude": "DÅ‚ugość", - "repeater_longitudeHelper": "Stopnie dziesiÄ™tne (np. -122,4194)", + "repeater_latitude": "Szerokość", + "repeater_latitudeHelper": "Stopnie dziesiętne (np. 37.7749)", + "repeater_longitude": "Długość", + "repeater_longitudeHelper": "Stopnie dziesiętne (np. -122,4194)", "repeater_features": "Funkcje", - "repeater_packetForwarding": "Przekierowanie pakietów", - "repeater_packetForwardingSubtitle": "Włącz repeater, aby przekazywać pakiety.", - "repeater_guestAccess": "DostÄ™p dla goÅ›ci", - "repeater_guestAccessSubtitle": "Umożliw dostÄ™p tylko do odczytu dla goÅ›ci.", - "repeater_privacyMode": "Tryb PrywatnoÅ›ci", - "repeater_privacyModeSubtitle": "Ukryj imiÄ™/lokalizacjÄ™ w reklamach", + "repeater_packetForwarding": "Przekierowanie pakietów", + "repeater_packetForwardingSubtitle": "Włącz repeater, aby przekazywać pakiety.", + "repeater_guestAccess": "Dostęp dla gości", + "repeater_guestAccessSubtitle": "Umożliw dostęp tylko do odczytu dla gości.", + "repeater_privacyMode": "Tryb Prywatności", + "repeater_privacyModeSubtitle": "Ukryj imię/lokalizację w reklamach", "repeater_advertisementSettings": "Ustawienia Reklam", - "repeater_localAdvertInterval": "InterwaÅ‚ Reklamy Lokalnej", + "repeater_localAdvertInterval": "Interwał Reklamy Lokalnej", "repeater_localAdvertIntervalMinutes": "{minutes} minut", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "InterwaÅ‚ Reklamy Powodziowej", + "repeater_floodAdvertInterval": "Interwał Reklamy Powodziowej", "repeater_floodAdvertIntervalHours": "{hours} godzin", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Zaszyfrowany InterwaÅ‚ Reklamowy", - "repeater_dangerZone": "Strefa ZagrożeÅ„", + "repeater_encryptedAdvertInterval": "Zaszyfrowany Interwał Reklamowy", + "repeater_dangerZone": "Strefa Zagrożeń", "repeater_rebootRepeater": "Zrestartuj Powtarzacz", - "repeater_rebootRepeaterSubtitle": "Zrestartuj urzÄ…dzenie powtarzajÄ…ce.", - "repeater_rebootRepeaterConfirm": "Czy na pewno chcesz zrestartować ten repeater?", - "repeater_regenerateIdentityKey": "Wygeneruj klucz tożsamoÅ›ci", - "repeater_regenerateIdentityKeySubtitle": "Wygeneruj nowÄ… parÄ™ kluczy publicznych/prywatnych", - "repeater_regenerateIdentityKeyConfirm": "To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?", - "repeater_eraseFileSystem": "Wyczyść System Plików", - "repeater_eraseFileSystemSubtitle": "Sformatuj system plików powielacza", - "repeater_eraseFileSystemConfirm": "OSTRZEÅ»ENIE: To spowoduje usuniÄ™cie wszystkich danych z powtarzacza. Nie da siÄ™ tego cofnąć!", - "repeater_eraseSerialOnly": "UsuniÄ™cie jest dostÄ™pne tylko przez konsolÄ™ szeregowÄ….", - "repeater_commandSent": "Polecenie wysÅ‚ane: {command}", + "repeater_rebootRepeaterSubtitle": "Zrestartuj urządzenie powtarzające.", + "repeater_rebootRepeaterConfirm": "Czy na pewno chcesz zrestartować ten repeater?", + "repeater_regenerateIdentityKey": "Wygeneruj klucz tożsamości", + "repeater_regenerateIdentityKeySubtitle": "Wygeneruj nową parę kluczy publicznych/prywatnych", + "repeater_regenerateIdentityKeyConfirm": "To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?", + "repeater_eraseFileSystem": "Wyczyść System Plików", + "repeater_eraseFileSystemSubtitle": "Sformatuj system plików powielacza", + "repeater_eraseFileSystemConfirm": "OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z powtarzacza. Nie da się tego cofnąć!", + "repeater_eraseSerialOnly": "Usunięcie jest dostępne tylko przez konsolę szeregową.", + "repeater_commandSent": "Polecenie wysłane: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Błąd podczas wysyÅ‚ania polecenia: {error}", + "repeater_errorSendingCommand": "Błąd podczas wysyłania polecenia: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "Potwierdź", - "repeater_settingsSaved": "Ustawienia zostaÅ‚y pomyÅ›lnie zapisane.", - "repeater_errorSavingSettings": "Błąd zapisu ustawieÅ„: {error}", + "repeater_confirm": "Potwierdź", + "repeater_settingsSaved": "Ustawienia zostały pomyślnie zapisane.", + "repeater_errorSavingSettings": "Błąd zapisu ustawień: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "OdÅ›wież Podstawowe Ustawienia", - "repeater_refreshRadioSettings": "OdÅ›wież Ustawienia Radio", - "repeater_refreshTxPower": "OdÅ›wież TX power", - "repeater_refreshLocationSettings": "OdÅ›wież Ustawienia Lokalizacji", - "repeater_refreshPacketForwarding": "OdÅ›wież trasowanie pakietów", - "repeater_refreshGuestAccess": "OdÅ›wież dostÄ™p goÅ›cia", - "repeater_refreshPrivacyMode": "OdÅ›wież Tryb PrywatnoÅ›ci", - "repeater_refreshAdvertisementSettings": "OdÅ›wież Ustawienia Reklamy", - "repeater_refreshed": "{label} odÅ›wieżone", + "repeater_refreshBasicSettings": "Odśwież Podstawowe Ustawienia", + "repeater_refreshRadioSettings": "Odśwież Ustawienia Radio", + "repeater_refreshTxPower": "Odśwież TX power", + "repeater_refreshLocationSettings": "Odśwież Ustawienia Lokalizacji", + "repeater_refreshPacketForwarding": "Odśwież trasowanie pakietów", + "repeater_refreshGuestAccess": "Odśwież dostęp gościa", + "repeater_refreshPrivacyMode": "Odśwież Tryb Prywatności", + "repeater_refreshAdvertisementSettings": "Odśwież Ustawienia Reklamy", + "repeater_refreshed": "{label} odświeżone", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Błąd podczas odÅ›wieżania {label}", + "repeater_errorRefreshing": "Błąd podczas odświeżania {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,17 +1091,17 @@ } }, "repeater_cliTitle": "Powtarzacz CLI", - "repeater_debugNextCommand": "Debug NastÄ™pnÄ… KomendÄ™", + "repeater_debugNextCommand": "Debug Następną Komendę", "repeater_commandHelp": "Pomoc", - "repeater_clearHistory": "Wyczyść historiÄ™", - "repeater_noCommandsSent": "Nie wysÅ‚ano jeszcze żadnych poleceÅ„", - "repeater_typeCommandOrUseQuick": "Wprowadź polecenie poniżej lub użyj szybkich poleceÅ„", - "repeater_enterCommandHint": "Wprowadź polecenie...", + "repeater_clearHistory": "Wyczyść historię", + "repeater_noCommandsSent": "Nie wysłano jeszcze żadnych poleceń", + "repeater_typeCommandOrUseQuick": "Wprowadź polecenie poniżej lub użyj szybkich poleceń", + "repeater_enterCommandHint": "Wprowadź polecenie...", "repeater_previousCommand": "Poprzednia komenda", - "repeater_nextCommand": "NastÄ™pna komenda", - "repeater_enterCommandFirst": "Wprowadź najpierw polecenie", - "repeater_cliCommandFrameTitle": "OkreÅ›lony Wyraz Polecenia CLI", - "repeater_cliCommandError": "Błąd: {error}", + "repeater_nextCommand": "Następna komenda", + "repeater_enterCommandFirst": "Wprowadź najpierw polecenie", + "repeater_cliCommandFrameTitle": "Określony Wyraz Polecenia CLI", + "repeater_cliCommandError": "Błąd: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1109,81 +1109,81 @@ } } }, - "repeater_cliQuickGetName": "Pobierz imiÄ™", + "repeater_cliQuickGetName": "Pobierz imię", "repeater_cliQuickGetRadio": "Uzyskaj Radio", "repeater_cliQuickGetTx": "Pobierz TX", - "repeater_cliQuickNeighbors": "SÄ…siedzi", + "repeater_cliQuickNeighbors": "Sąsiedzi", "repeater_cliQuickVersion": "Wersja", "repeater_cliQuickAdvertise": "Reklama", "repeater_cliQuickClock": "Godzina", - "repeater_cliHelpAdvert": "WysyÅ‚a pakiet reklamowy", - "repeater_cliHelpReboot": "Zresetuj urzÄ…dzenie. (Uwaga, może pojawić siÄ™ 'Timeout', co jest normalne)", - "repeater_cliHelpClock": "WyÅ›wietla aktualny czas zgodnie z zegarem urzÄ…dzenia.", - "repeater_cliHelpPassword": "Ustawia nowe hasÅ‚o administratora dla urzÄ…dzenia.", - "repeater_cliHelpVersion": "WyÅ›wietla wersjÄ™ urzÄ…dzenia i datÄ™ budowy oprogramowania.", - "repeater_cliHelpClearStats": "Resetuje różne wskaźniki statystyk do zera.", + "repeater_cliHelpAdvert": "Wysyła pakiet reklamowy", + "repeater_cliHelpReboot": "Zresetuj urządzenie. (Uwaga, może pojawić się 'Timeout', co jest normalne)", + "repeater_cliHelpClock": "Wyświetla aktualny czas zgodnie z zegarem urządzenia.", + "repeater_cliHelpPassword": "Ustawia nowe hasło administratora dla urządzenia.", + "repeater_cliHelpVersion": "Wyświetla wersję urządzenia i datę budowy oprogramowania.", + "repeater_cliHelpClearStats": "Resetuje różne wskaźniki statystyk do zera.", "repeater_cliHelpSetAf": "Ustawia czynnik czasu powietrznego.", - "repeater_cliHelpSetTx": "Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)", - "repeater_cliHelpSetRepeat": "Włącza lub wyłącza rolÄ™ powtarzacza dla tego wÄ™zÅ‚a.", - "repeater_cliHelpSetAllowReadOnly": "(Serwer pokoju) JeÅ›li 'włączone', to logowanie z pustym hasÅ‚em bÄ™dzie dozwolone, ale nie można publikować w pokoju (tylko czytać).", - "repeater_cliHelpSetFloodMax": "Ustawia maksymalnÄ… liczbÄ™ skoków pakietu powrotnego (jeÅ›li >= max, pakiet nie jest przekierowywany)", - "repeater_cliHelpSetIntThresh": "Ustawia Próg Interferencji (w dB). DomyÅ›lnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceÅ„ kanaÅ‚u.", - "repeater_cliHelpSetAgcResetInterval": "Ustawia interwaÅ‚ do zresetowania Automatycznego Sterownika GÅ‚oÅ›noÅ›ci. Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetMultiAcks": "Włącza lub wyłącza funkcjÄ™ 'podwójnych potwierdzeÅ„'.", - "repeater_cliHelpSetAdvertInterval": "Ustawia interwaÅ‚ timera w minutach do wysyÅ‚ania pakietu reklamy lokalnej (bezpoÅ›redniej). Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetFloodAdvertInterval": "Ustawia interwaÅ‚ timera w godzinach do wysÅ‚ania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetGuestPassword": "Ustawia/aktualizuje hasÅ‚o goÅ›cia. (dla repeaterów, loginy goÅ›ci mogÄ… wysyÅ‚ać żądanie \"Get Stats\")", - "repeater_cliHelpSetName": "Ustawia nazwÄ™ reklamy.", - "repeater_cliHelpSetLat": "Ustawia współrzÄ™dnÄ… geograficzne (w stopniach dziesiÄ™tnych) mapy reklam.", - "repeater_cliHelpSetLon": "Ustawia współrzÄ™dnÄ… dÅ‚ugoÅ›ciowÄ… mapy reklamy. (stopnie dziesiÄ™tne)", + "repeater_cliHelpSetTx": "Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)", + "repeater_cliHelpSetRepeat": "Włącza lub wyłącza rolę powtarzacza dla tego węzła.", + "repeater_cliHelpSetAllowReadOnly": "(Serwer pokoju) Jeśli 'włączone', to logowanie z pustym hasłem będzie dozwolone, ale nie można publikować w pokoju (tylko czytać).", + "repeater_cliHelpSetFloodMax": "Ustawia maksymalną liczbę skoków pakietu powrotnego (jeśli >= max, pakiet nie jest przekierowywany)", + "repeater_cliHelpSetIntThresh": "Ustawia Próg Interferencji (w dB). Domyślnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceń kanału.", + "repeater_cliHelpSetAgcResetInterval": "Ustawia interwał do zresetowania Automatycznego Sterownika Głośności. Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetMultiAcks": "Włącza lub wyłącza funkcję 'podwójnych potwierdzeń'.", + "repeater_cliHelpSetAdvertInterval": "Ustawia interwał timera w minutach do wysyłania pakietu reklamy lokalnej (bezpośredniej). Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetFloodAdvertInterval": "Ustawia interwał timera w godzinach do wysłania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetGuestPassword": "Ustawia/aktualizuje hasło gościa. (dla repeaterów, loginy gości mogą wysyłać żądanie \"Get Stats\")", + "repeater_cliHelpSetName": "Ustawia nazwę reklamy.", + "repeater_cliHelpSetLat": "Ustawia współrzędną geograficzne (w stopniach dziesiętnych) mapy reklam.", + "repeater_cliHelpSetLon": "Ustawia współrzędną długościową mapy reklamy. (stopnie dziesiętne)", "repeater_cliHelpSetRadio": "Ustawia nowe parametry radia i zapisuje je w preferencjach. Wymaga polecenia \"reboot\" do zastosowania.", - "repeater_cliHelpSetRxDelay": "Ustawienia (eksperymentalne) bazowe (muszÄ… być > 1, aby dziaÅ‚ać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnaÅ‚u/wynik. Ustaw na 0, aby wyłączyć.", - "repeater_cliHelpSetTxDelay": "Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyÅ‚anie (zmniejszajÄ…c prawdopodobieÅ„stwo kolizji).", - "repeater_cliHelpSetDirectTxDelay": "Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpoÅ›rednim.", - "repeater_cliHelpSetBridgeEnabled": "Włącz/Wyłącz mostek.", - "repeater_cliHelpSetBridgeDelay": "Ustaw czas opóźnienia przed ponownym wysyÅ‚aniem pakietów.", - "repeater_cliHelpSetBridgeSource": "Wybierz, czy most bÄ™dzie ponownie transmitowaÅ‚ otrzymywane pakiety, czy też wysyÅ‚ane.", - "repeater_cliHelpSetBridgeBaud": "Ustaw prÄ™dkość transmisji magistrali szeregowej dla mostów rs232.", - "repeater_cliHelpSetBridgeSecret": "Ustaw sekret dla mostów ESPNOW.", - "repeater_cliHelpSetAdcMultiplier": "Ustawia niestandardowy współczynnik do korekty zgÅ‚aszanego napiÄ™cia baterii (obsÅ‚uga tylko na wybranych pÅ‚ytach).", - "repeater_cliHelpTempRadio": "Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a nastÄ™pnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).", - "repeater_cliHelpSetPerm": "Modyfikuje ACL. Usuwa dopasowanÄ… wpis (z prefiksem pubkey), jeÅ›li \"permissions\" wynosi zero. Dodaje nowy wpis, jeÅ›li pubkey-hex ma peÅ‚nÄ… dÅ‚ugość i nie znajduje siÄ™ obecnie w ACL. Aktualizuje wpis, dopasowujÄ…c prefiks pubkey. Bit uprawnieÅ„ zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).", + "repeater_cliHelpSetRxDelay": "Ustawienia (eksperymentalne) bazowe (muszą być > 1, aby działać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnału/wynik. Ustaw na 0, aby wyłączyć.", + "repeater_cliHelpSetTxDelay": "Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyłanie (zmniejszając prawdopodobieństwo kolizji).", + "repeater_cliHelpSetDirectTxDelay": "Taki sam jak txdelay, ale dla stosowania losowej opóźnienia przy przekazywaniu pakietów w trybie bezpośrednim.", + "repeater_cliHelpSetBridgeEnabled": "Włącz/Wyłącz mostek.", + "repeater_cliHelpSetBridgeDelay": "Ustaw czas opóźnienia przed ponownym wysyłaniem pakietów.", + "repeater_cliHelpSetBridgeSource": "Wybierz, czy most będzie ponownie transmitował otrzymywane pakiety, czy też wysyłane.", + "repeater_cliHelpSetBridgeBaud": "Ustaw prędkość transmisji magistrali szeregowej dla mostów rs232.", + "repeater_cliHelpSetBridgeSecret": "Ustaw sekret dla mostów ESPNOW.", + "repeater_cliHelpSetAdcMultiplier": "Ustawia niestandardowy współczynnik do korekty zgłaszanego napięcia baterii (obsługa tylko na wybranych płytach).", + "repeater_cliHelpTempRadio": "Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a następnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).", + "repeater_cliHelpSetPerm": "Modyfikuje ACL. Usuwa dopasowaną wpis (z prefiksem pubkey), jeśli \"permissions\" wynosi zero. Dodaje nowy wpis, jeśli pubkey-hex ma pełną długość i nie znajduje się obecnie w ACL. Aktualizuje wpis, dopasowując prefiks pubkey. Bit uprawnień zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).", "repeater_cliHelpGetBridgeType": "Uzyskano typ mostu: brak, rs232, espnow", - "repeater_cliHelpLogStart": "Rozpoczyna siÄ™ logowanie pakietów do systemu plików.", - "repeater_cliHelpLogStop": "Zatrzymuje logowanie pakietów do systemu plików.", - "repeater_cliHelpLogErase": "Usuwa logi pakietów z systemu plików.", - "repeater_cliHelpNeighbors": "WyÅ›wietla listÄ™ innych wÄ™złów powtarzajÄ…cych siÄ™, które usÅ‚yszano dziÄ™ki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Usuwa pierwszy pasujÄ…cy wpis (z prefiksem pubkey (hex)) z listy sÄ…siadów.", - "repeater_cliHelpRegion": "(tylko seria) WyÅ›wietla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.", - "repeater_cliHelpRegionLoad": "ZAPOMNIJ: to jest specjalne wywoÅ‚anie wielokomendowe. Każda nastÄ™pna komenda jest nazwÄ… regionu (wciÄ™ta spacjami, aby wskazywać hierarchiÄ™ nadrzÄ™dnÄ…, z minimum jednÄ… spacjÄ…). ZakoÅ„czona wysÅ‚aniem pustej linii/komendy.", + "repeater_cliHelpLogStart": "Rozpoczyna się logowanie pakietów do systemu plików.", + "repeater_cliHelpLogStop": "Zatrzymuje logowanie pakietów do systemu plików.", + "repeater_cliHelpLogErase": "Usuwa logi pakietów z systemu plików.", + "repeater_cliHelpNeighbors": "Wyświetla listę innych węzłów powtarzających się, które usłyszano dzięki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Usuwa pierwszy pasujący wpis (z prefiksem pubkey (hex)) z listy sąsiadów.", + "repeater_cliHelpRegion": "(tylko seria) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.", + "repeater_cliHelpRegionLoad": "ZAPOMNIJ: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.", "repeater_cliHelpRegionGet": "Wyszukuje region o podanej nazwie prefiksu (lub \"\" dla zakresu globalnego). Odpowiada \"-> region-name (parent-name) 'F'\"", - "repeater_cliHelpRegionPut": "Dodaje lub aktualizuje definicjÄ™ regionu z podanÄ… nazwÄ….", - "repeater_cliHelpRegionRemove": "Usuwa definicjÄ™ regionu o podanej nazwie. (musi siÄ™ dokÅ‚adnie zgadzać i nie może mieć podregionów).", - "repeater_cliHelpRegionAllowf": "Ustawia uprawnienia 'P'Å‚ytkowe dla podanego regionu. ('' dla zakresu globalnego/starszego)", - "repeater_cliHelpRegionDenyf": "Usuwa uprawnienie 'PÅ‚ywajÄ…ce' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca siÄ™ używania tego na globalnym/starszym zakresie!!).", - "repeater_cliHelpRegionHome": "Odpowiada z aktualnej 'home' region. (Uwaga: nie zostaÅ‚o jeszcze zastosowane, zarezerwowane na przyszÅ‚ość).", + "repeater_cliHelpRegionPut": "Dodaje lub aktualizuje definicję regionu z podaną nazwą.", + "repeater_cliHelpRegionRemove": "Usuwa definicję regionu o podanej nazwie. (musi się dokładnie zgadzać i nie może mieć podregionów).", + "repeater_cliHelpRegionAllowf": "Ustawia uprawnienia 'P'łytkowe dla podanego regionu. ('' dla zakresu globalnego/starszego)", + "repeater_cliHelpRegionDenyf": "Usuwa uprawnienie 'Pływające' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).", + "repeater_cliHelpRegionHome": "Odpowiada z aktualnej 'home' region. (Uwaga: nie zostało jeszcze zastosowane, zarezerwowane na przyszłość).", "repeater_cliHelpRegionHomeSet": "Ustawia region 'domowe'.", - "repeater_cliHelpRegionSave": "Zapisuje listÄ™/mapÄ™ regionów do pamiÄ™ci.", - "repeater_cliHelpGps": "WyÅ›wietla status GPS. JeÅ›li GPS jest wyłączony, odpowiada tylko \"off\", jeÅ›li jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbÄ… satelitów.", - "repeater_cliHelpGpsOnOff": "Włącza/wyłącza nawigacjÄ™ GPS.", - "repeater_cliHelpGpsSync": "Synchronizuje czas wÄ™zÅ‚a z zegarem GPS.", - "repeater_cliHelpGpsSetLoc": "Ustawia pozycjÄ™ wÄ™zÅ‚a na współrzÄ™dne GPS i zapisuje preferencje.", - "repeater_cliHelpGpsAdvert": "UdostÄ™pnia konfiguracjÄ™ reklamy lokalizacji wÄ™zÅ‚a:\n- brak: nie uwzglÄ™dniaj lokalizacji w reklamach\n- udostÄ™pnia: udostÄ™pnia lokalizacjÄ™ GPS (z SensorManager)\n- ustawienia: reklamuj lokalizacjÄ™ przechowywanÄ… w ustawieniach", - "repeater_cliHelpGpsAdvertSet": "Ustawia konfiguracjÄ™ reklamy w lokalizacji.", - "repeater_commandsListTitle": "Lista poleceÅ„", - "repeater_commandsListNote": "ZAPAMIĘTAJ: dla różnych poleceÅ„ \"set ...\" istnieje również polecenie \"get ...\".", - "repeater_general": "Ogólne", + "repeater_cliHelpRegionSave": "Zapisuje listę/mapę regionów do pamięci.", + "repeater_cliHelpGps": "Wyświetla status GPS. Jeśli GPS jest wyłączony, odpowiada tylko \"off\", jeśli jest włączony, odpowiada z \"on\", \"status\", \"fix\", liczbą satelitów.", + "repeater_cliHelpGpsOnOff": "Włącza/wyłącza nawigację GPS.", + "repeater_cliHelpGpsSync": "Synchronizuje czas węzła z zegarem GPS.", + "repeater_cliHelpGpsSetLoc": "Ustawia pozycję węzła na współrzędne GPS i zapisuje preferencje.", + "repeater_cliHelpGpsAdvert": "Udostępnia konfigurację reklamy lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w reklamach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: reklamuj lokalizację przechowywaną w ustawieniach", + "repeater_cliHelpGpsAdvertSet": "Ustawia konfigurację reklamy w lokalizacji.", + "repeater_commandsListTitle": "Lista poleceń", + "repeater_commandsListNote": "ZAPAMIĘTAJ: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".", + "repeater_general": "Ogólne", "repeater_settingsCategory": "Ustawienia", "repeater_bridge": "Most", "repeater_logging": "Rejestrowanie", - "repeater_neighborsRepeaterOnly": "SÄ…siedzi (tylko powtarzacz)", - "repeater_regionManagementRepeaterOnly": "ZarzÄ…dzanie Regionem (tylko Powtarzacz)", - "repeater_regionNote": "Wprowadzono komendy regionalne w celu zarzÄ…dzania definicjami i uprawnieniami regionów.", - "repeater_gpsManagement": "ZarzÄ…dzanie GPS", - "repeater_gpsNote": "Polecenie GPS zostaÅ‚o wprowadzone w celu zarzÄ…dzania tematami zwiÄ…zanymi z lokalizacjÄ….", + "repeater_neighborsRepeaterOnly": "Sąsiedzi (tylko powtarzacz)", + "repeater_regionManagementRepeaterOnly": "Zarządzanie Regionem (tylko Powtarzacz)", + "repeater_regionNote": "Wprowadzono komendy regionalne w celu zarządzania definicjami i uprawnieniami regionów.", + "repeater_gpsManagement": "Zarządzanie GPS", + "repeater_gpsNote": "Polecenie GPS zostało wprowadzone w celu zarządzania tematami związanymi z lokalizacją.", "telemetry_receivedData": "Otrzymano Dane Telemetrii", - "telemetry_requestTimeout": "Å»yczenie o danych telemetrycznych nie udaÅ‚o siÄ™.", - "telemetry_errorLoading": "Błąd podczas Å‚adowania telemetry: {error}", + "telemetry_requestTimeout": "Życzenie o danych telemetrycznych nie udało się.", + "telemetry_errorLoading": "Błąd podczas ładowania telemetry: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,8 +1191,8 @@ } } }, - "telemetry_noData": "Brak dostÄ™pnych danych telemetrycznych.", - "telemetry_channelTitle": "KanaÅ‚ {channel}", + "telemetry_noData": "Brak dostępnych danych telemetrycznych.", + "telemetry_channelTitle": "Kanał {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1201,7 +1201,7 @@ } }, "telemetry_batteryLabel": "Bateria", - "telemetry_voltageLabel": "NapiÄ™cie", + "telemetry_voltageLabel": "Napięcie", "telemetry_mcuTemperatureLabel": "Temperatura MCU", "telemetry_temperatureLabel": "Temperatura", "telemetry_currentLabel": "Obecny", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Åšcieżka pakietu", - "channelPath_viewMap": "WyÅ›wietl mapÄ™", - "channelPath_otherObservedPaths": "Inne Zauważone Åšcieżki", - "channelPath_repeaterHops": "Skoki Powtórki", - "channelPath_noHopDetails": "Szczegóły dotyczÄ…ce tego pakietu nie zostaÅ‚y podane.", - "channelPath_messageDetails": "Szczegóły wiadomoÅ›ci", + "channelPath_title": "Ścieżka pakietu", + "channelPath_viewMap": "Wyświetl mapę", + "channelPath_otherObservedPaths": "Inne Zauważone Ścieżki", + "channelPath_repeaterHops": "Skoki Powtórki", + "channelPath_noHopDetails": "Szczegóły dotyczące tego pakietu nie zostały podane.", + "channelPath_messageDetails": "Szczegóły wiadomości", "channelPath_senderLabel": "Nadawca", "channelPath_timeLabel": "Czas", - "channelPath_repeatsLabel": "Powtórzenia", - "channelPath_pathLabel": "Åšcieżka {index}", + "channelPath_repeatsLabel": "Powtórzenia", + "channelPath_pathLabel": "Ścieżka {index}", "channelPath_observedLabel": "Obserwowane", - "channelPath_observedPathTitle": "Obserwowany Å›cieżka {index} • {hops}", + "channelPath_observedPathTitle": "Obserwowany ścieżka {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1290,8 +1290,8 @@ }, "channelPath_unknownPath": "Nieznane", "channelPath_floodPath": "Powodzenie", - "channelPath_directPath": "BezpoÅ›rednio", - "channelPath_observedZeroOf": "0 z {total} skoków", + "channelPath_directPath": "Bezpośrednio", + "channelPath_observedZeroOf": "0 z {total} skoków", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1299,7 +1299,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} z {total} skoków", + "channelPath_observedSomeOf": "{observed} z {total} skoków", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1310,9 +1310,9 @@ } } }, - "channelPath_mapTitle": "Mapa Å›cieżek", - "channelPath_noRepeaterLocations": "Brak dostÄ™pnych lokalizacji powtarzaczy dla tego Å›cieżki.", - "channelPath_primaryPath": "Åšcieżka {index} (Główna)", + "channelPath_mapTitle": "Mapa ścieżek", + "channelPath_noRepeaterLocations": "Brak dostępnych lokalizacji powtarzaczy dla tego ścieżki.", + "channelPath_primaryPath": "Ścieżka {index} (Główna)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1327,9 +1327,9 @@ } } }, - "channelPath_pathLabelTitle": "Åšcieżka", - "channelPath_observedPathHeader": "Obserwowana Å›cieżka", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Ścieżka", + "channelPath_observedPathHeader": "Obserwowana ścieżka", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,16 +1340,16 @@ } } }, - "channelPath_noHopDetailsAvailable": "Brak dostÄ™pnych szczegółów hopa dla tego pakietu.", + "channelPath_noHopDetailsAvailable": "Brak dostępnych szczegółów hopa dla tego pakietu.", "channelPath_unknownRepeater": "Nieznany Powtarzacz", "listFilter_tooltip": "Filtruj i sortuj", "listFilter_sortBy": "Sortuj po", - "listFilter_latestMessages": "Najnowsze wiadomoÅ›ci", - "listFilter_heardRecently": "SÅ‚yszano niedawno", + "listFilter_latestMessages": "Najnowsze wiadomości", + "listFilter_heardRecently": "Słyszano niedawno", "listFilter_az": "A-Z", "listFilter_filters": "Filtry", "listFilter_all": "Wszystko", - "listFilter_users": "Użytkownicy", + "listFilter_users": "Użytkownicy", "listFilter_repeaters": "Powtarzacze", "listFilter_roomServers": "Serwery pokoju", "listFilter_unreadOnly": "Tylko nieprzeczytane", @@ -1361,25 +1361,25 @@ } } }, - "repeater_neighbors": "SÄ…siedzi", - "repeater_neighborsSubtitle": "WyÅ›wietl sÄ…siedztwo zerowych hopów.", - "neighbors_receivedData": "Otrzymano dane sÄ…siedztwa", - "neighbors_requestTimedOut": "SÄ…siedzi proszÄ… o wyłączenie timingu.", - "neighbors_errorLoading": "Błąd podczas Å‚adowania sÄ…siadów: {error}", - "neighbors_repeatersNeighbors": "Powtarzacze SÄ…siedzi", - "neighbors_noData": "Brak danych dotyczÄ…cych sÄ…siadów.", - "channels_joinPrivateChannelDesc": "RÄ™cznie wprowadź klucz tajny.", - "channels_createPrivateChannel": "Utwórz Prywatny KanaÅ‚", + "repeater_neighbors": "Sąsiedzi", + "repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.", + "neighbors_receivedData": "Otrzymano dane sąsiedztwa", + "neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.", + "neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}", + "neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi", + "neighbors_noData": "Brak danych dotyczących sąsiadów.", + "channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.", + "channels_createPrivateChannel": "Utwórz Prywatny Kanał", "channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.", - "channels_joinPrivateChannel": "Dołącz do Prywatnego KanaÅ‚u", - "channels_joinPublicChannel": "Dołącz do kanaÅ‚u publicznego.", - "channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanaÅ‚u.", - "channels_joinHashtagChannel": "Dołącz do kanaÅ‚u oznaczanego hashtagiem", - "channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.", + "channels_joinPrivateChannel": "Dołącz do Prywatnego Kanału", + "channels_joinPublicChannel": "Dołącz do kanału publicznego.", + "channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanału.", + "channels_joinHashtagChannel": "Dołącz do kanału oznaczanego hashtagiem", + "channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.", "channels_scanQrCode": "Skanuj kod QR", - "channels_scanQrCodeComingSoon": "Wkrótce", - "channels_enterHashtag": "Wprowadź hashtag", - "channels_hashtagHint": "np. #zespół", + "channels_scanQrCodeComingSoon": "Wkrótce", + "channels_enterHashtag": "Wprowadź hashtag", + "channels_hashtagHint": "np. #zespół", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_heardAgo": "UsÅ‚yszano: {time} temu", + "neighbors_heardAgo": "Usłyszano: {time} temu", "neighbors_unknownContact": "Nieznana {pubkey}", - "settings_locationGPSEnable": "Włącz GPS", - "settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocÄ… GPS.", - "settings_locationIntervalSec": "InterwaÅ‚ dla GPS (Sekundy)", - "settings_locationIntervalInvalid": "InterwaÅ‚ musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.", - "contacts_manageRoom": "ZarzÄ…dzaj Serwerem Pokoju", - "room_management": "ZarzÄ…dzanie Serwerem Pokoju", + "settings_locationGPSEnable": "Włącz GPS", + "settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.", + "settings_locationIntervalSec": "Interwał dla GPS (Sekundy)", + "settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.", + "contacts_manageRoom": "Zarządzaj Serwerem Pokoju", + "room_management": "Zarządzanie Serwerem Pokoju", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1458,36 +1458,36 @@ } } }, - "community_createDesc": "Utwórz nowÄ… spoÅ‚eczność i udostÄ™pnij za pomocÄ… kodu QR.", - "community_title": "SpoÅ‚eczność", - "community_create": "Utwórz SpoÅ‚eczność", + "community_createDesc": "Utwórz nową społeczność i udostępnij za pomocą kodu QR.", + "community_title": "Społeczność", + "community_create": "Utwórz Społeczność", "common_ok": "OK", - "community_join": "Dołącz", - "community_joinTitle": "Dołącz do spoÅ‚ecznoÅ›ci", - "community_joinConfirmation": "Czy chcesz dołączyć do spoÅ‚ecznoÅ›ci \"{name}\"?", - "community_scanQr": "Skanuj QR kod spoÅ‚ecznoÅ›ci", - "community_scanInstructions": "Skieruj kamerÄ™ w kierunku kodu QR spoÅ‚ecznoÅ›ci.", - "community_showQr": "Pokaż kod QR", - "community_publicChannel": "SpoÅ‚eczność Publiczna", - "community_hashtagChannel": "Hashtag SpoÅ‚ecznoÅ›ci", - "community_name": "Nazwa SpoÅ‚ecznoÅ›ci", - "community_enterName": "Wprowadź nazwÄ™ spoÅ‚ecznoÅ›ci", - "community_created": "SpoÅ‚eczność \"{name}\" zostaÅ‚a utworzona", - "community_joined": "DołączyÅ‚ do spoÅ‚ecznoÅ›ci \"{name}\"", - "community_qrTitle": "Dziel siÄ™ SpoÅ‚ecznoÅ›ciÄ…", - "community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}", - "community_hashtagPrivacyHint": "KanaÅ‚y hashtagowe spoÅ‚ecznoÅ›ci sÄ… dostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci", - "community_invalidQrCode": "NieprawidÅ‚owy kod QR spoÅ‚ecznoÅ›ci.", - "community_alreadyMember": "Już jesteÅ› czÅ‚onkiem.", - "community_alreadyMemberMessage": "JesteÅ› już czÅ‚onkiem \"{name}\".", - "community_addPublicChannel": "Dodaj KanaÅ‚ Publiczny SpoÅ‚ecznoÅ›ci", - "community_addPublicChannelHint": "Automatycznie dodaj kanaÅ‚ publiczny dla tej spoÅ‚ecznoÅ›ci.", - "community_noCommunities": "Nie dołączono jeszcze żadnych spoÅ‚ecznoÅ›ci.", - "community_scanOrCreate": "Skanuj kod QR lub utwórz spoÅ‚eczność, aby zacząć.", - "community_manageCommunities": "ZarzÄ…dzaj Grupami", - "community_delete": "Opuszczenie SpoÅ‚ecznoÅ›ci", - "community_deleteConfirm": "OpuÅ›cić \"{name}\"?", - "community_deleteChannelsWarning": "Spowoduje to również usuniÄ™cie {count} kanaÅ‚u/kanałów i ich wiadomoÅ›ci.", + "community_join": "Dołącz", + "community_joinTitle": "Dołącz do społeczności", + "community_joinConfirmation": "Czy chcesz dołączyć do społeczności \"{name}\"?", + "community_scanQr": "Skanuj QR kod społeczności", + "community_scanInstructions": "Skieruj kamerę w kierunku kodu QR społeczności.", + "community_showQr": "Pokaż kod QR", + "community_publicChannel": "Społeczność Publiczna", + "community_hashtagChannel": "Hashtag Społeczności", + "community_name": "Nazwa Społeczności", + "community_enterName": "Wprowadź nazwę społeczności", + "community_created": "Społeczność \"{name}\" została utworzona", + "community_joined": "Dołączył do społeczności \"{name}\"", + "community_qrTitle": "Dziel się Społecznością", + "community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}", + "community_hashtagPrivacyHint": "Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności", + "community_invalidQrCode": "Nieprawidłowy kod QR społeczności.", + "community_alreadyMember": "Już jesteś członkiem.", + "community_alreadyMemberMessage": "Jesteś już członkiem \"{name}\".", + "community_addPublicChannel": "Dodaj Kanał Publiczny Społeczności", + "community_addPublicChannelHint": "Automatycznie dodaj kanał publiczny dla tej społeczności.", + "community_noCommunities": "Nie dołączono jeszcze żadnych społeczności.", + "community_scanOrCreate": "Skanuj kod QR lub utwórz społeczność, aby zacząć.", + "community_manageCommunities": "Zarządzaj Grupami", + "community_delete": "Opuszczenie Społeczności", + "community_deleteConfirm": "Opuścić \"{name}\"?", + "community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Opuszczono spoÅ‚eczność \"{name}\"", - "community_addHashtagChannel": "Dodaj hashtag spoÅ‚ecznoÅ›ci", - "community_addHashtagChannelDesc": "Dodaj kanaÅ‚ z hashtagiem dla tej spoÅ‚ecznoÅ›ci", - "community_selectCommunity": "Wybierz spoÅ‚eczność", + "community_deleted": "Opuszczono społeczność \"{name}\"", + "community_addHashtagChannel": "Dodaj hashtag społeczności", + "community_addHashtagChannelDesc": "Dodaj kanał z hashtagiem dla tej społeczności", + "community_selectCommunity": "Wybierz społeczność", "community_regularHashtag": "Hashtag regular", - "community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)", - "community_communityHashtag": "Hashtag SpoÅ‚ecznoÅ›ci", - "community_communityHashtagDesc": "DostÄ™pne tylko dla czÅ‚onków spoÅ‚ecznoÅ›ci", + "community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)", + "community_communityHashtag": "Hashtag Społeczności", + "community_communityHashtagDesc": "Dostępne tylko dla członków społeczności", "community_forCommunity": "Dla {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1533,11 +1533,11 @@ } }, "community_regenerate": "Zregeneruj", - "community_secretRegenerated": "HasÅ‚o ponownie wygenerowane dla \"{name}\"", + "community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"", "community_regenerateSecret": "Zregeneruj sekret", - "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy czÅ‚onkowie bÄ™dÄ… musieli zeskanować nowy kod QR, aby kontynuować komunikacjÄ™.", - "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"", - "community_secretUpdated": "HasÅ‚o zaktualizowane dla \"{name}\"", + "community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.", + "community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"", + "community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"", "community_updateSecret": "Zaktualizuj tajny klucz", "@contacts_pathTraceTo": { "placeholders": { @@ -1547,81 +1547,81 @@ } }, "pathTrace_you": "Ty", - "pathTrace_failed": "Åšledzenie Å›cieżki nie powiodÅ‚o siÄ™.", - "pathTrace_notAvailable": "Åšcieżka Å›ledzenia niedostÄ™pna.", - "contacts_pathTrace": "Åšledzenie Åšcieżek", - "contacts_ping": "Pingować", - "contacts_repeaterPathTrace": "Åšledzenie Å›cieżki do repeatera", - "contacts_roomPathTrace": "Åšledzenie Å›cieżki do serwera pokojowego", + "pathTrace_failed": "Śledzenie ścieżki nie powiodło się.", + "pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.", + "contacts_pathTrace": "Śledzenie Ścieżek", + "contacts_ping": "Pingować", + "contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera", + "contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego", "contacts_roomPing": "Pinguj serwer pokoju", - "pathTrace_refreshTooltip": "OdÅ›wież Å›cieżkÄ™.", + "pathTrace_refreshTooltip": "Odśwież ścieżkę.", "contacts_repeaterPing": "Repeater pingowy", - "contacts_pathTraceTo": "Åšledź trasÄ™ do {name}", - "contacts_chatTraceRoute": "Åšledź trasÄ™ promienia", + "contacts_pathTraceTo": "Śledź trasę do {name}", + "contacts_chatTraceRoute": "Śledź trasę promienia", "appSettings_languageRu": "Rosyjski", - "appSettings_languageUk": "UkraiÅ„ska", - "appSettings_enableMessageTracing": "Włącz Å›ledzenie wiadomoÅ›ci", - "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomoÅ›ci", - "contacts_contactImportFailed": "Kontakt nie zostaÅ‚ zaimportowany.", + "appSettings_languageUk": "Ukraińska", + "appSettings_enableMessageTracing": "Włącz śledzenie wiadomości", + "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości", + "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", "contacts_zeroHopAdvert": "Reklama Zero Hop", "contacts_floodAdvert": "Reklama powodziowa", - "contacts_copyAdvertToClipboard": "Kopiuj ogÅ‚oszenie do schowka", + "contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka", "contacts_clipboardEmpty": "Schowek jest pusty.", - "contacts_invalidAdvertFormat": "NieprawidÅ‚owe dane kontaktowe", + "contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe", "contacts_addContactFromClipboard": "Dodaj kontakt z schowka", - "contacts_contactImported": "Kontakt zostaÅ‚ zaimportowany.", - "contacts_zeroHopContactAdvertSent": "WysÅ‚ano kontakt przez ogÅ‚oszenie.", + "contacts_contactImported": "Kontakt został zaimportowany.", + "contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.", "contacts_contactAdvertCopied": "Reklama skopiowana do schowka.", - "contacts_contactAdvertCopyFailed": "Kopiowanie ogÅ‚oszenia do schowka nie powiodÅ‚o siÄ™.", - "contacts_ShareContactZeroHop": "UdostÄ™pnij kontakt przez ogÅ‚oszenie", + "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", + "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", "contacts_ShareContact": "Kopiuj kontakt do schowka", - "contacts_zeroHopContactAdvertFailed": "Nie udaÅ‚o siÄ™ wysÅ‚ać kontaktu.", - "notification_activityTitle": "Aktywność MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomoÅ›ci} many{wiadomoÅ›ci} other{wiadomoÅ›ci}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanaÅ‚u} few{wiadomoÅ›ci kanaÅ‚u} many{wiadomoÅ›ci kanaÅ‚u} other{wiadomoÅ›ci kanaÅ‚u}}", - "notification_newNodesCount": "{count} {count, plural, =1{nowy wÄ™zeÅ‚} few{nowe wÄ™zÅ‚y} many{nowych wÄ™złów} other{nowych wÄ™złów}}", + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", + "notification_activityTitle": "Aktywność MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}", + "notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}", "notification_newTypeDiscovered": "Nowy {contactType} wykryty", - "notification_receivedNewMessage": "Otrzymano nowÄ… wiadomość", + "notification_receivedNewMessage": "Otrzymano nową wiadomość", "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", - "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", - "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacjÄ… do pliku GPX.", - "settings_gpxExportSuccess": "PomyÅ›lnie wyeksportowano plik GPX.", - "settings_gpxExportNotAvailable": "Nie obsÅ‚ugiwane na Twoim urzÄ…dzeniu/systemie operacyjnym", - "settings_gpxExportError": "WystÄ…piÅ‚ błąd podczas eksportowania.", - "settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzajÄ…cych i pomieszczeÅ„", - "settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacjÄ… do pliku GPX.", + "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", + "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", + "settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.", + "settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym", + "settings_gpxExportError": "Wystąpił błąd podczas eksportowania.", + "settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzających i pomieszczeń", + "settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.", "settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX", - "settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacjÄ… do pliku GPX.", - "settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów", - "settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.", + "settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.", + "settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów", + "settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.", "settings_gpxExportChat": "Lokalizacje towarzyszy", "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open", - "pathTrace_someHopsNoLocation": "Jeden lub wiÄ™cej z chmieli nie ma okreÅ›lonej lokalizacji!", - "map_pathTraceCancelled": "Åšledzenie Å›cieżki anulowano.", - "map_runTrace": "Uruchom Å›lad Å›cieżki", - "pathTrace_clearTooltip": "Wyczyść Å›cieżkÄ™", - "map_removeLast": "UsuÅ„ ostatni", - "map_tapToAdd": "Kliknij na wÄ™zÅ‚y, aby dodać je do Å›cieżki.", - "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urzÄ…dzenia.", - "scanner_chromeRequired": "Wymagana przeglÄ…darka Chrome", - "scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglÄ…darki Google Chrome lub opartej na Chromium do obsÅ‚ugi Bluetooth.", - "scanner_bluetoothOff": "Bluetooth jest wyłączony", - "scanner_enableBluetooth": "Włącz Bluetooth", + "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!", + "map_pathTraceCancelled": "Śledzenie ścieżki anulowano.", + "map_runTrace": "Uruchom ślad ścieżki", + "pathTrace_clearTooltip": "Wyczyść ścieżkę", + "map_removeLast": "Usuń ostatni", + "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", + "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", + "scanner_chromeRequired": "Wymagana przeglądarka Chrome", + "scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.", + "scanner_bluetoothOff": "Bluetooth jest wyłączony", + "scanner_enableBluetooth": "Włącz Bluetooth", "snrIndicator_lastSeen": "Ostatnio widziany", - "snrIndicator_nearByRepeaters": "Nadajniki w pobliżu", - "chat_ShowAllPaths": "Pokaż wszystkie Å›cieżki", - "settings_clientRepeatSubtitle": "Pozwól temu urzÄ…dzeniu powtarzać pakiety danych dla innych urzÄ…dzeÅ„.", - "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", - "settings_clientRepeatFreqWarning": "Powtórka poza sieciÄ… wymaga czÄ™stotliwoÅ›ci 433, 869 lub 918 MHz.", - "settings_aboutOpenMeteoAttribution": "Dane wysokoÅ›ciowe LOS: Open-Meteo (CC BY 4.0)", + "snrIndicator_nearByRepeaters": "Nadajniki w pobliżu", + "chat_ShowAllPaths": "Pokaż wszystkie ścieżki", + "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", + "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", + "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.", + "settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Jednostki", "appSettings_unitsMetric": "Metryczne (m / km)", "appSettings_unitsImperial": "Imperialne (ft / mi)", "map_lineOfSight": "Linia wzroku", "map_losScreenTitle": "Linia wzroku", - "losSelectStartEnd": "Wybierz wÄ™zÅ‚y poczÄ…tkowe i koÅ„cowe dla LOS.", - "losRunFailed": "Sprawdzenie pola widzenia nie powiodÅ‚o siÄ™: {error}", + "losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.", + "losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,11 +1629,11 @@ } } }, - "losClearAllPoints": "Wyczyść wszystkie punkty", - "losRunToViewElevationProfile": "Uruchom LOS, aby wyÅ›wietlić profil wysokoÅ›ci", + "losClearAllPoints": "Wyczyść wszystkie punkty", + "losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Stuknij wÄ™zÅ‚y lub naciÅ›nij i przytrzymaj mapÄ™, aby uzyskać niestandardowe punkty", - "losShowDisplayNodes": "Pokaż wÄ™zÅ‚y wyÅ›wietlajÄ…ce", + "losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty", + "losShowDisplayNodes": "Pokaż węzły wyświetlające", "losCustomPoints": "Punkty niestandardowe", "losCustomPointLabel": "Niestandardowe {index}", "@losCustomPointLabel": { @@ -1668,8 +1668,8 @@ } }, "losRun": "Uruchom LOS-a", - "losNoElevationData": "Brak danych o wysokoÅ›ci", - "losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny przeÅ›wit {clearance} {heightUnit}", + "losNoElevationData": "Brak danych o wysokości", + "losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Dane dotyczÄ…ce wysokoÅ›ci sÄ… niedostÄ™pne dla jednej lub wiÄ™kszej liczby próbek.", - "losErrorInvalidInput": "NieprawidÅ‚owe dane punktów/wysokoÅ›ci do obliczenia LOS.", - "losRenameCustomPoint": "ZmieÅ„ nazwÄ™ punktu niestandardowego", + "losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.", + "losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.", + "losRenameCustomPoint": "Zmień nazwę punktu niestandardowego", "losPointName": "Nazwa punktu", - "losShowPanelTooltip": "Pokaż panel LOS", + "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", - "losElevationAttribution": "Dane dotyczÄ…ce wysokoÅ›ci: Open-Meteo (CC BY 4.0)", + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Horyzont radiowy", - "losLegendLosBeam": "Linia widocznoÅ›ci", + "losLegendLosBeam": "Linia widoczności", "losLegendTerrain": "Teren", - "losFrequencyLabel": "CzÄ™stotliwość", - "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", + "losFrequencyLabel": "Częstotliwość", + "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", - "losFrequencyDialogDescription": "ZaczynajÄ…c od k={baselineK} przy {baselineFreq} MHz, obliczenia korygujÄ… współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,7 +1753,7 @@ } } }, - "listFilter_removeFromFavorites": "UsuÅ„ z ulubionych", + "listFilter_removeFromFavorites": "Usuń z ulubionych", "listFilter_addToFavorites": "Dodaj do ulubionych", "listFilter_favorites": "Ulubione", "@contacts_searchFavorites": { @@ -1799,14 +1799,26 @@ "contacts_unread": "Nieprzeczytane", "contacts_searchContactsNoNumber": "Wyszukaj kontakty...", "contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...", - "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", - "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", - "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...", - "connectionChoiceBluetoothLabel": "Bluetooth", + "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", + "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", + "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...", + "usbScreenStatus": "Wybierz urządzenie USB", + "usbScreenTitle": "Połącz przez USB", + "usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.", + "usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.", + "usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.", + "usbErrorPermissionDenied": "Zostało odrzucone żądanie dostępu przez USB.", + "usbErrorDeviceMissing": "Wybór urządzenia USB już nie jest dostępny.", + "usbErrorInvalidPort": "Wybierz prawidłowe urządzenie USB.", + "usbErrorBusy": "Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.", + "usbErrorNotConnected": "Brak podłączonego urządzenia USB.", + "usbErrorOpenFailed": "Nie udało się otworzyć wybranego urządzenia USB.", + "usbErrorConnectFailed": "Nie udało się nawiązać połączenia z wybranym urządzeniem USB.", + "usbErrorUnsupported": "Port szeregowy USB nie jest obsługiwany na tym urządzeniu.", + "usbErrorAlreadyActive": "Połączenie USB jest już aktywne.", + "usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.", + "usbErrorPortClosed": "Połączenie USB nie jest aktywne.", + "usbErrorConnectTimedOut": "Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji.", "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "Połącz przez USB", - "usbScreenNote": "Port szeregowy USB jest aktywny na urzÄ…dzeniach z Androidem i platformach stacjonarnych, które obsÅ‚ugujÄ… tÄ™ funkcjÄ™.", - "usbScreenSubtitle": "Wybierz wykryty urzÄ…dzenie szeregowe i podłącz je bezpoÅ›rednio do swojego wÄ™zÅ‚a MeshCore.", - "usbScreenStatus": "Wybierz urzÄ…dzenie USB", - "usbScreenEmptyState": "Nie znaleziono żadnych urzÄ…dzeÅ„ USB. Podłącz jedno i zaktualizuj." + "connectionChoiceBluetoothLabel": "Bluetooth" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 883b222..6636e8f 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -20,7 +20,7 @@ "common_close": "Fechar", "common_edit": "Editar", "common_add": "Adicionar", - "common_settings": "Configurações", + "common_settings": "Configurações", "common_disconnect": "Desconectar", "common_connected": "Conectado", "common_disconnected": "Desconectado", @@ -35,7 +35,7 @@ "common_disable": "Desativar", "common_reboot": "Reiniciar", "common_loading": "Carregando...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -56,7 +56,7 @@ "scanner_scanning": "Procurando por dispositivos...", "scanner_connecting": "Conectando...", "scanner_disconnecting": "Desconectando...", - "scanner_notConnected": "Não está conectado", + "scanner_notConnected": "Não está conectado", "scanner_connectedTo": "Conectado a {deviceName}", "@scanner_connectedTo": { "placeholders": { @@ -67,7 +67,7 @@ }, "scanner_searchingDevices": "Procurando dispositivos MeshCore...", "scanner_tapToScan": "Toque em \"Escanear\" para encontrar dispositivos MeshCore", - "scanner_connectionFailed": "Falha na conexão: {error}", + "scanner_connectionFailed": "Falha na conexão: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -79,47 +79,47 @@ "scanner_scan": "Digitalizar", "device_quickSwitch": "Mudar rapidamente", "device_meshcore": "MeshCore", - "settings_title": "Configurações", - "settings_deviceInfo": "Informações do Dispositivo", - "settings_appSettings": "Configurações do App", - "settings_appSettingsSubtitle": "Notificações, mensagens e preferências de mapa", - "settings_nodeSettings": "Configurações do Nó", - "settings_nodeName": "Nome do Nó", - "settings_nodeNameNotSet": "Não definido", - "settings_nodeNameHint": "Insira o nome do nó", + "settings_title": "Configurações", + "settings_deviceInfo": "Informações do Dispositivo", + "settings_appSettings": "Configurações do App", + "settings_appSettingsSubtitle": "Notificações, mensagens e preferências de mapa", + "settings_nodeSettings": "Configurações do Nó", + "settings_nodeName": "Nome do Nó", + "settings_nodeNameNotSet": "Não definido", + "settings_nodeNameHint": "Insira o nome do nó", "settings_nodeNameUpdated": "Nome atualizado", - "settings_radioSettings": "Configurações de Rádio", - "settings_radioSettingsSubtitle": "Frequência, potência, fator de espalhamento", - "settings_radioSettingsUpdated": "Configurações de rádio atualizadas", - "settings_location": "Localização", + "settings_radioSettings": "Configurações de Rádio", + "settings_radioSettingsSubtitle": "Frequência, potência, fator de espalhamento", + "settings_radioSettingsUpdated": "Configurações de rádio atualizadas", + "settings_location": "Localização", "settings_locationSubtitle": "Coordenadas GPS", - "settings_locationUpdated": "Localização atualizada", + "settings_locationUpdated": "Localização atualizada", "settings_locationBothRequired": "Insira a latitude e a longitude.", - "settings_locationInvalid": "Latitude ou longitude inválidos.", + "settings_locationInvalid": "Latitude ou longitude inválidos.", "settings_latitude": "Latitude", "settings_longitude": "Longitude", "settings_privacyMode": "Modo de Privacidade", - "settings_privacyModeSubtitle": "Esconder nome/localização em anúncios", - "settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.", + "settings_privacyModeSubtitle": "Esconder nome/localização em anúncios", + "settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.", "settings_privacyModeEnabled": "Modo de privacidade ativado", "settings_privacyModeDisabled": "Modo de privacidade desativado", - "settings_actions": "Ações", + "settings_actions": "Ações", "settings_sendAdvertisement": "Enviar Publicidade", - "settings_sendAdvertisementSubtitle": "Presença de transmissão agora", - "settings_advertisementSent": "Anúncio enviado", - "settings_syncTime": "Tempo de Sincronização", - "settings_syncTimeSubtitle": "Definir o relógio do dispositivo para o horário do telefone", + "settings_sendAdvertisementSubtitle": "Presença de transmissão agora", + "settings_advertisementSent": "Anúncio enviado", + "settings_syncTime": "Tempo de Sincronização", + "settings_syncTimeSubtitle": "Definir o relógio do dispositivo para o horário do telefone", "settings_timeSynchronized": "Sincronizado com o tempo", "settings_refreshContacts": "Atualizar Contatos", "settings_refreshContactsSubtitle": "Recarregar a lista de contatos do dispositivo", "settings_rebootDevice": "Reiniciar Dispositivo", "settings_rebootDeviceSubtitle": "Reiniciar o dispositivo MeshCore", - "settings_rebootDeviceConfirm": "Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.", + "settings_rebootDeviceConfirm": "Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.", "settings_debug": "Depurar", - "settings_bleDebugLog": "Log de Depuração BLE", + "settings_bleDebugLog": "Log de Depuração BLE", "settings_bleDebugLogSubtitle": "Comandos, respostas e dados brutos do BLE", - "settings_appDebugLog": "Log de Depuração do Aplicativo", - "settings_appDebugLogSubtitle": "Mensagens de depuração do aplicativo", + "settings_appDebugLog": "Log de Depuração do Aplicativo", + "settings_appDebugLogSubtitle": "Mensagens de depuração do aplicativo", "settings_about": "Sobre", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -129,25 +129,25 @@ } } }, - "settings_aboutLegalese": "Projeto MeshCore de Código Aberto 2024", - "settings_aboutDescription": "Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.", + "settings_aboutLegalese": "Projeto MeshCore de Código Aberto 2024", + "settings_aboutDescription": "Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.", "settings_infoName": "Nome", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Bateria", - "settings_infoPublicKey": "Chave Pública", - "settings_infoContactsCount": "Número de Contatos", - "settings_infoChannelCount": "Número do Canal", + "settings_infoPublicKey": "Chave Pública", + "settings_infoContactsCount": "Número de Contatos", + "settings_infoChannelCount": "Número do Canal", "settings_presets": "Presets", - "settings_frequency": "Frequência (MHz)", + "settings_frequency": "Frequência (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", - "settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)", + "settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)", "settings_bandwidth": "Largura de banda", - "settings_spreadingFactor": "Fator de Dispersão", - "settings_codingRate": "Taxa de Codificação", - "settings_txPower": "TX Potência (dBm)", + "settings_spreadingFactor": "Fator de Dispersão", + "settings_codingRate": "Taxa de Codificação", + "settings_txPower": "TX Potência (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)", + "settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)", "settings_error": "Erro: {message}", "@settings_error": { "placeholders": { @@ -156,50 +156,50 @@ } } }, - "appSettings_title": "Configurações do App", - "appSettings_appearance": "Aparência", + "appSettings_title": "Configurações do App", + "appSettings_appearance": "Aparência", "appSettings_theme": "Tema", - "appSettings_themeSystem": "Padrão do sistema", + "appSettings_themeSystem": "Padrão do sistema", "appSettings_themeLight": "Luz", "appSettings_themeDark": "Escuro", "appSettings_language": "Idioma", - "appSettings_languageSystem": "Padrão do sistema", + "appSettings_languageSystem": "Padrão do sistema", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_notifications": "Notificações", - "appSettings_enableNotifications": "Ativar Notificações", - "appSettings_enableNotificationsSubtitle": "Receber notificações para mensagens e anúncios", - "appSettings_notificationPermissionDenied": "Permissão de notificação negada", - "appSettings_notificationsEnabled": "Notificações ativadas", - "appSettings_notificationsDisabled": "Notificações desativadas", - "appSettings_messageNotifications": "Notificações de Mensagem", - "appSettings_messageNotificationsSubtitle": "Mostrar notificação ao receber novas mensagens", - "appSettings_channelMessageNotifications": "Notificações de Mensagens do Canal", - "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificação ao receber mensagens do canal", - "appSettings_advertisementNotifications": "Notificações de Anúncios", - "appSettings_advertisementNotificationsSubtitle": "Mostrar notificação quando novos nós forem descobertos", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_notifications": "Notificações", + "appSettings_enableNotifications": "Ativar Notificações", + "appSettings_enableNotificationsSubtitle": "Receber notificações para mensagens e anúncios", + "appSettings_notificationPermissionDenied": "Permissão de notificação negada", + "appSettings_notificationsEnabled": "Notificações ativadas", + "appSettings_notificationsDisabled": "Notificações desativadas", + "appSettings_messageNotifications": "Notificações de Mensagem", + "appSettings_messageNotificationsSubtitle": "Mostrar notificação ao receber novas mensagens", + "appSettings_channelMessageNotifications": "Notificações de Mensagens do Canal", + "appSettings_channelMessageNotificationsSubtitle": "Mostrar notificação ao receber mensagens do canal", + "appSettings_advertisementNotifications": "Notificações de Anúncios", + "appSettings_advertisementNotificationsSubtitle": "Mostrar notificação quando novos nós forem descobertos", "appSettings_messaging": "Mensagens", - "appSettings_clearPathOnMaxRetry": "Limpar Caminho em Tentativas Máximas", - "appSettings_clearPathOnMaxRetrySubtitle": "Redefinir o caminho de contato após 5 tentativas de envio falhas", - "appSettings_pathsWillBeCleared": "Os caminhos serão limpos após 5 tentativas falhas.", - "appSettings_pathsWillNotBeCleared": "Os caminhos não serão limpos automaticamente.", - "appSettings_autoRouteRotation": "Rotação de Rota Automática", - "appSettings_autoRouteRotationSubtitle": "Alternar entre os melhores caminhos e o modo inundação", - "appSettings_autoRouteRotationEnabled": "Rotação de roteamento automático habilitada", - "appSettings_autoRouteRotationDisabled": "Rotação de roteamento automático desativada", + "appSettings_clearPathOnMaxRetry": "Limpar Caminho em Tentativas Máximas", + "appSettings_clearPathOnMaxRetrySubtitle": "Redefinir o caminho de contato após 5 tentativas de envio falhas", + "appSettings_pathsWillBeCleared": "Os caminhos serão limpos após 5 tentativas falhas.", + "appSettings_pathsWillNotBeCleared": "Os caminhos não serão limpos automaticamente.", + "appSettings_autoRouteRotation": "Rotação de Rota Automática", + "appSettings_autoRouteRotationSubtitle": "Alternar entre os melhores caminhos e o modo inundação", + "appSettings_autoRouteRotationEnabled": "Rotação de roteamento automático habilitada", + "appSettings_autoRouteRotationDisabled": "Rotação de roteamento automático desativada", "appSettings_battery": "Bateria", - "appSettings_batteryChemistry": "Química da Bateria", + "appSettings_batteryChemistry": "Química da Bateria", "appSettings_batteryChemistryPerDevice": "Definir por dispositivo ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { @@ -212,16 +212,16 @@ "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", - "appSettings_mapDisplay": "Exibição do Mapa", + "appSettings_mapDisplay": "Exibição do Mapa", "appSettings_showRepeaters": "Mostrar Repetidores", - "appSettings_showRepeatersSubtitle": "Exibir nós de repetidor no mapa", - "appSettings_showChatNodes": "Mostrar Nós de Chat", - "appSettings_showChatNodesSubtitle": "Exibir nós de chat no mapa", - "appSettings_showOtherNodes": "Mostrar Outros Nós", - "appSettings_showOtherNodesSubtitle": "Exibir outros tipos de nó no mapa", + "appSettings_showRepeatersSubtitle": "Exibir nós de repetidor no mapa", + "appSettings_showChatNodes": "Mostrar Nós de Chat", + "appSettings_showChatNodesSubtitle": "Exibir nós de chat no mapa", + "appSettings_showOtherNodes": "Mostrar Outros Nós", + "appSettings_showOtherNodesSubtitle": "Exibir outros tipos de nó no mapa", "appSettings_timeFilter": "Filtro de Tempo", - "appSettings_timeFilterShowAll": "Mostrar todos os nós", - "appSettings_timeFilterShowLast": "Mostrar nós das últimas {hours} horas", + "appSettings_timeFilterShowAll": "Mostrar todos os nós", + "appSettings_timeFilterShowLast": "Mostrar nós das últimas {hours} horas", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,15 +230,15 @@ } }, "appSettings_mapTimeFilter": "Filtro de Tempo do Mapa", - "appSettings_showNodesDiscoveredWithin": "Mostrar nós descobertos dentro de:", + "appSettings_showNodesDiscoveredWithin": "Mostrar nós descobertos dentro de:", "appSettings_allTime": "Todos os tempos", - "appSettings_lastHour": "Última hora", - "appSettings_last6Hours": "Últimos 6 horas", - "appSettings_last24Hours": "Últimas 24 horas", - "appSettings_lastWeek": "Da última semana", + "appSettings_lastHour": "Última hora", + "appSettings_last6Hours": "Últimos 6 horas", + "appSettings_last24Hours": "Últimas 24 horas", + "appSettings_lastWeek": "Da última semana", "appSettings_offlineMapCache": "Cache de Mapa Offline", - "appSettings_noAreaSelected": "Nenhuma área selecionada", - "appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Nenhuma área selecionada", + "appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,16 +250,16 @@ } }, "appSettings_debugCard": "Depurar", - "appSettings_appDebugLogging": "Rastreamento de Depuração do Aplicativo", - "appSettings_appDebugLoggingSubtitle": "Registrar mensagens de depuração do aplicativo Log para solucionar problemas", - "appSettings_appDebugLoggingEnabled": "Log de depuração do aplicativo habilitado", - "appSettings_appDebugLoggingDisabled": "O registro de depuração do aplicativo está desativado.", + "appSettings_appDebugLogging": "Rastreamento de Depuração do Aplicativo", + "appSettings_appDebugLoggingSubtitle": "Registrar mensagens de depuração do aplicativo Log para solucionar problemas", + "appSettings_appDebugLoggingEnabled": "Log de depuração do aplicativo habilitado", + "appSettings_appDebugLoggingDisabled": "O registro de depuração do aplicativo está desativado.", "contacts_title": "Contactos", - "contacts_noContacts": "Ainda não existem contatos.", - "contacts_contactsWillAppear": "Os contatos serão exibidos quando os dispositivos anunciarem.", + "contacts_noContacts": "Ainda não existem contatos.", + "contacts_contactsWillAppear": "Os contatos serão exibidos quando os dispositivos anunciarem.", "contacts_searchContacts": "Pesquisar contatos...", - "contacts_noUnreadContacts": "Sem contatos não lidos.", - "contacts_noContactsFound": "Não foram encontrados contatos ou grupos.", + "contacts_noUnreadContacts": "Sem contatos não lidos.", + "contacts_noContactsFound": "Não foram encontrados contatos ou grupos.", "contacts_deleteContact": "Excluir Contato", "contacts_removeConfirm": "Remover {contactName} dos contatos?", "@contacts_removeConfirm": { @@ -284,8 +284,8 @@ }, "contacts_newGroup": "Novo Grupo", "contacts_groupName": "Nome do grupo", - "contacts_groupNameRequired": "O nome do grupo é obrigatório.", - "contacts_groupAlreadyExists": "O grupo \"{name}\" já existe", + "contacts_groupNameRequired": "O nome do grupo é obrigatório.", + "contacts_groupAlreadyExists": "O grupo \"{name}\" já existe", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,10 +294,10 @@ } }, "contacts_filterContacts": "Filtrar contatos...", - "contacts_noContactsMatchFilter": "Não existem contatos que correspondam ao seu filtro", + "contacts_noContactsMatchFilter": "Não existem contatos que correspondam ao seu filtro", "contacts_noMembers": "Nenhum membro", - "contacts_lastSeenNow": "Última vez que foi visto agora", - "contacts_lastSeenMinsAgo": "Última vez que foi visto {minutes} minutos atrás", + "contacts_lastSeenNow": "Última vez que foi visto agora", + "contacts_lastSeenMinsAgo": "Última vez que foi visto {minutes} minutos atrás", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Última vez que foi visto há 1 hora.", - "contacts_lastSeenHoursAgo": "Última vez visto {hours} horas atrás", + "contacts_lastSeenHourAgo": "Última vez que foi visto há 1 hora.", + "contacts_lastSeenHoursAgo": "Última vez visto {hours} horas atrás", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Última vez que foi visto 1 dia atrás", - "contacts_lastSeenDaysAgo": "Última vez visto {days} dias atrás", + "contacts_lastSeenDayAgo": "Última vez que foi visto 1 dia atrás", + "contacts_lastSeenDaysAgo": "Última vez visto {days} dias atrás", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -324,8 +324,8 @@ } }, "channels_title": "Canais", - "channels_noChannelsConfigured": "Nenhuma canalização configurada", - "channels_addPublicChannel": "Adicionar Canal Público", + "channels_noChannelsConfigured": "Nenhuma canalização configurada", + "channels_addPublicChannel": "Adicionar Canal Público", "channels_searchChannels": "Pesquisar canais...", "channels_noChannelsFound": "Nenhum canal encontrado", "channels_channelIndex": "Canal {index}", @@ -337,15 +337,15 @@ } }, "channels_hashtagChannel": "Canal com hashtag", - "channels_public": "Público", + "channels_public": "Público", "channels_private": "Privado", - "channels_publicChannel": "Canal público", + "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", "channels_muteChannel": "Silenciar canal", "channels_unmuteChannel": "Ativar canal", "channels_deleteChannel": "Excluir canal", - "channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.", + "channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Canal \"{name}\" excluído", + "channels_channelDeleted": "Canal \"{name}\" excluído", "@channels_channelDeleted": { "placeholders": { "name": { @@ -362,12 +362,12 @@ } }, "channels_addChannel": "Adicionar Canal", - "channels_channelIndexLabel": "Índice do Canal", + "channels_channelIndexLabel": "Índice do Canal", "channels_channelName": "Nome do Canal", - "channels_usePublicChannel": "Usar Canal Público", - "channels_standardPublicPsk": "PSK público padrão", + "channels_usePublicChannel": "Usar Canal Público", + "channels_standardPublicPsk": "PSK público padrão", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Gerar PSK aleatório", + "channels_generateRandomPsk": "Gerar PSK aleatório", "channels_enterChannelName": "Por favor, insira um nome de canal", "channels_pskMustBe32Hex": "O PSK deve ter 32 caracteres hexadecimais.", "channels_channelAdded": "Canal \"{name}\" adicionado", @@ -386,7 +386,7 @@ } } }, - "channels_smazCompression": "Compressão SMAZ", + "channels_smazCompression": "Compressão SMAZ", "channels_channelUpdated": "Canal \"{name}\" atualizado", "@channels_channelUpdated": { "placeholders": { @@ -395,15 +395,15 @@ } } }, - "channels_publicChannelAdded": "Canal público adicionado", + "channels_publicChannelAdded": "Canal público adicionado", "channels_sortBy": "Ordenar por", "channels_sortManual": "Manual", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Últimas mensagens", - "channels_sortUnread": "Não lido", - "chat_noMessages": "Ainda não existem mensagens.", - "chat_sendMessageToStart": "Enviar uma mensagem para começar", - "chat_originalMessageNotFound": "Mensagem original não encontrada", + "channels_sortLatestMessages": "Últimas mensagens", + "channels_sortUnread": "Não lido", + "chat_noMessages": "Ainda não existem mensagens.", + "chat_sendMessageToStart": "Enviar uma mensagem para começar", + "chat_originalMessageNotFound": "Mensagem original não encontrada", "chat_replyingTo": "Responder a {name}", "@chat_replyingTo": { "placeholders": { @@ -420,7 +420,7 @@ } } }, - "chat_location": "Localização", + "chat_location": "Localização", "chat_sendMessageTo": "Enviar uma mensagem para {contactName}", "@chat_sendMessageTo": { "placeholders": { @@ -430,7 +430,7 @@ } }, "chat_typeMessage": "Digite uma mensagem...", - "chat_messageTooLong": "Mensagem muito longa (máximo {maxBytes} bytes).", + "chat_messageTooLong": "Mensagem muito longa (máximo {maxBytes} bytes).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -439,7 +439,7 @@ } }, "chat_messageCopied": "Mensagem copiada", - "chat_messageDeleted": "Mensagem excluída", + "chat_messageDeleted": "Mensagem excluída", "chat_retryingMessage": "Tentando novamente", "chat_retryCount": "Tentar {current}/{max}", "@chat_retryCount": { @@ -454,30 +454,30 @@ }, "chat_sendGif": "Enviar GIF", "chat_reply": "Responder", - "chat_addReaction": "Adicionar Reação", + "chat_addReaction": "Adicionar Reação", "chat_me": "Eu", "emojiCategorySmileys": "Emojis", "emojiCategoryGestures": "Gestos", - "emojiCategoryHearts": "Corações", + "emojiCategoryHearts": "Corações", "emojiCategoryObjects": "Objetos", "gifPicker_title": "Escolher um GIF", "gifPicker_searchHint": "Pesquisar GIFs...", "gifPicker_poweredBy": "Desenvolvido por GIPHY", "gifPicker_noGifsFound": "Nenhum GIF encontrado", - "gifPicker_failedLoad": "Não foi possível carregar os GIFs", + "gifPicker_failedLoad": "Não foi possível carregar os GIFs", "gifPicker_failedSearch": "Falha na pesquisa de GIFs", - "gifPicker_noInternet": "Sem conexão com a internet", - "debugLog_appTitle": "Log de Depuração do Aplicativo", - "debugLog_bleTitle": "Log de Depuração BLE", + "gifPicker_noInternet": "Sem conexão com a internet", + "debugLog_appTitle": "Log de Depuração do Aplicativo", + "debugLog_bleTitle": "Log de Depuração BLE", "debugLog_copyLog": "Copiar log", "debugLog_clearLog": "Limpar log", - "debugLog_copied": "Log de depuração copiado", + "debugLog_copied": "Log de depuração copiado", "debugLog_bleCopied": "Log BLE copiado", - "debugLog_noEntries": "Ainda não existem logs de depuração.", - "debugLog_enableInSettings": "Ativar o log de depuração do aplicativo nas configurações", + "debugLog_noEntries": "Ainda não existem logs de depuração.", + "debugLog_enableInSettings": "Ativar o log de depuração do aplicativo nas configurações", "debugLog_frames": "Estruturas", "debugLog_rawLogRx": "Log Raw-RX", - "debugLog_noBleActivity": "Ainda não há atividade BLE.", + "debugLog_noBleActivity": "Ainda não há atividade BLE.", "debugFrame_length": "Comprimento do Quadro: {count} bytes", "@debugFrame_length": { "placeholders": { @@ -540,13 +540,13 @@ } } }, - "debugFrame_hexDump": "Espaço Hexadecimal:", + "debugFrame_hexDump": "Espaço Hexadecimal:", "chat_pathManagement": "Gerenciamento de Caminhos", "chat_routingMode": "Modo de roteamento", "chat_autoUseSavedPath": "Auto (usar caminho salvo)", - "chat_forceFloodMode": "Modo de Inundação Forçado", + "chat_forceFloodMode": "Modo de Inundação Forçado", "chat_recentAckPaths": "Rotas de ACK Recentes (toque para usar):", - "chat_pathHistoryFull": "O histórico está cheio. Remova entradas para adicionar novas.", + "chat_pathHistoryFull": "O histórico está cheio. Remova entradas para adicionar novas.", "chat_hopSingular": "pule", "chat_hopPlural": "salta", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -559,17 +559,17 @@ }, "chat_successes": "Sucessos", "chat_removePath": "Remover caminho", - "chat_noPathHistoryYet": "Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.", - "chat_pathActions": "Ações do Caminho:", + "chat_noPathHistoryYet": "Ainda não há histórico de caminhos.\nEnvie uma mensagem para descobrir caminhos.", + "chat_pathActions": "Ações do Caminho:", "chat_setCustomPath": "Definir Caminho Personalizado", "chat_setCustomPathSubtitle": "Especifique manualmente o caminho de roteamento", "chat_clearPath": "Limpar Caminho", - "chat_clearPathSubtitle": "Forçar a descoberta na próxima transmissão", - "chat_pathCleared": "Caminho limpo. A próxima mensagem redescobrirá a rota.", + "chat_clearPathSubtitle": "Forçar a descoberta na próxima transmissão", + "chat_pathCleared": "Caminho limpo. A próxima mensagem redescobrirá a rota.", "chat_floodModeSubtitle": "Use a chave de roteamento na barra de ferramentas", - "chat_floodModeEnabled": "Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.", + "chat_floodModeEnabled": "Modo de inundação ativado. Desative-o novamente através do ícone de roteamento na barra de ferramentas.", "chat_fullPath": "Caminho Completo", - "chat_pathDetailsNotAvailable": "Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.", + "chat_pathDetailsNotAvailable": "Os detalhes do caminho ainda não estão disponíveis. Tente enviar uma mensagem para atualizar.", "chat_pathSetHops": "Caminho definido: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -583,14 +583,14 @@ }, "chat_pathSavedLocally": "Salvo localmente. Conectar para sincronizar.", "chat_pathDeviceConfirmed": "Dispositivo confirmado.", - "chat_pathDeviceNotConfirmed": "Dispositivo ainda não confirmado.", + "chat_pathDeviceNotConfirmed": "Dispositivo ainda não confirmado.", "chat_type": "Digite", "chat_path": "Caminho", - "chat_publicKey": "Chave Pública", + "chat_publicKey": "Chave Pública", "chat_compressOutgoingMessages": "Comprimir mensagens enviadas", - "chat_floodForced": "Inundação (forçada)", - "chat_directForced": "Direto (forçado)", - "chat_hopsForced": "{count} saltos (forçado)", + "chat_floodForced": "Inundação (forçada)", + "chat_directForced": "Direto (forçado)", + "chat_hopsForced": "{count} saltos (forçado)", "@chat_hopsForced": { "placeholders": { "count": { @@ -598,10 +598,10 @@ } } }, - "chat_floodAuto": "Inundação (automática)", + "chat_floodAuto": "Inundação (automática)", "chat_direct": "Salvar", "chat_poiShared": "Ponto de Interesse Compartilhado", - "chat_unread": "Não lido: {count}", + "chat_unread": "Não lido: {count}", "@chat_unread": { "placeholders": { "count": { @@ -612,7 +612,7 @@ "chat_openLink": "Abrir link?", "chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?", "chat_open": "Abrir", - "chat_couldNotOpenLink": "Não foi possível abrir o link: {url}", + "chat_couldNotOpenLink": "Não foi possível abrir o link: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,11 +620,11 @@ } } }, - "chat_invalidLink": "Formato de link inválido", - "map_title": "Mapa de Nós", - "map_noNodesWithLocation": "Não existem nós com dados de localização.", - "map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa", - "map_nodesCount": "Nós: {count}", + "chat_invalidLink": "Formato de link inválido", + "map_title": "Mapa de Nós", + "map_noNodesWithLocation": "Não existem nós com dados de localização.", + "map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa", + "map_nodesCount": "Nós: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -646,21 +646,21 @@ "map_sensor": "Sensor", "map_pinDm": "Gatilho (DM)", "map_pinPrivate": "Bloquear (Privado)", - "map_pinPublic": "Pin (Público)", - "map_lastSeen": "Última Visão", + "map_pinPublic": "Pin (Público)", + "map_lastSeen": "Última Visão", "map_disconnectConfirm": "Tem certeza de que deseja desconectar deste dispositivo?", "map_from": "De", "map_source": "Fonte", "map_flags": "Bandeiras", "map_shareMarkerHere": "Compartilhar marcador aqui", - "map_pinLabel": "Rótulo de marcador", - "map_label": "Rótulo", + "map_pinLabel": "Rótulo de marcador", + "map_label": "Rótulo", "map_pointOfInterest": "Ponto de interesse", "map_sendToContact": "Enviar para o contato", "map_sendToChannel": "Enviar para o canal", - "map_noChannelsAvailable": "Não existem canais disponíveis.", - "map_publicLocationShare": "Compartilhar local público", - "map_publicLocationShareConfirm": "Você está prestes a compartilhar uma localização em {channelLabel}. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.", + "map_noChannelsAvailable": "Não existem canais disponíveis.", + "map_publicLocationShare": "Compartilhar local público", + "map_publicLocationShareConfirm": "Você está prestes a compartilhar uma localização em {channelLabel}. Este canal é público e qualquer pessoa com a PSK pode visualizá-lo.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -669,23 +669,23 @@ } }, "map_connectToShareMarkers": "Conecte-se a um dispositivo para compartilhar marcadores", - "map_filterNodes": "Filtrar Nós", - "map_nodeTypes": "Tipos de Nó", - "map_chatNodes": "Nós de Chat", + "map_filterNodes": "Filtrar Nós", + "map_nodeTypes": "Tipos de Nó", + "map_chatNodes": "Nós de Chat", "map_repeaters": "Repetidores", - "map_otherNodes": "Outros Nós", + "map_otherNodes": "Outros Nós", "map_keyPrefix": "Prefixo Chave", "map_filterByKeyPrefix": "Filtrar por prefixo-chave", - "map_publicKeyPrefix": "Prefixo de chave pública", + "map_publicKeyPrefix": "Prefixo de chave pública", "map_markers": "Marcadores", "map_showSharedMarkers": "Mostrar marcadores compartilhados", - "map_lastSeenTime": "Último Tempo de Visualização", + "map_lastSeenTime": "Último Tempo de Visualização", "map_sharedPin": "Pin compartilhado", - "map_joinRoom": "Junte-se à Sala", + "map_joinRoom": "Junte-se à Sala", "map_manageRepeater": "Gerenciar Repetidor", "mapCache_title": "Cache de Mapa Offline", - "mapCache_selectAreaFirst": "Selecione uma área para armazenar em cache primeiro", - "mapCache_noTilesToDownload": "Não há tiles para baixar para esta área.", + "mapCache_selectAreaFirst": "Selecione uma área para armazenar em cache primeiro", + "mapCache_noTilesToDownload": "Não há tiles para baixar para esta área.", "mapCache_downloadTilesTitle": "Baixar tiles", "mapCache_downloadTilesPrompt": "Baixar {count} tiles para uso offline?", "@mapCache_downloadTilesPrompt": { @@ -718,9 +718,9 @@ "mapCache_clearOfflineCacheTitle": "Limpar cache offline", "mapCache_clearOfflineCachePrompt": "Remover todas as telhas de mapa em cache?", "mapCache_offlineCacheCleared": "Cache offline limpa", - "mapCache_noAreaSelected": "Nenhuma área selecionada", - "mapCache_cacheArea": "Área de Cache", - "mapCache_useCurrentView": "Usar a Visualização Atual", + "mapCache_noAreaSelected": "Nenhuma área selecionada", + "mapCache_cacheArea": "Área de Cache", + "mapCache_useCurrentView": "Usar a Visualização Atual", "mapCache_zoomRange": "Intervalo de Zoom", "mapCache_estimatedTiles": "Estimados azulejos: {count}", "@mapCache_estimatedTiles": { @@ -769,7 +769,7 @@ } }, "time_justNow": "Agora", - "time_minutesAgo": "{minutes} minutos atrás", + "time_minutesAgo": "{minutes} minutos atrás", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -777,7 +777,7 @@ } } }, - "time_hoursAgo": "{hours}h atrás", + "time_hoursAgo": "{hours}h atrás", "@time_hoursAgo": { "placeholders": { "hours": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} dias atrás", + "time_daysAgo": "{days} dias atrás", "@time_daysAgo": { "placeholders": { "days": { @@ -799,7 +799,7 @@ "time_days": "dias", "time_week": "semana", "time_weeks": "semanas", - "time_month": "mês", + "time_month": "mês", "time_months": "meses", "time_minutes": "minutos", "time_allTime": "Todos os tempos", @@ -810,13 +810,13 @@ "login_password": "Senha", "login_enterPassword": "Insira a senha", "login_savePassword": "Salvar senha", - "login_savePasswordSubtitle": "A senha será armazenada com segurança neste dispositivo.", - "login_repeaterDescription": "Insira a senha do repetidor para acessar as configurações e o status.", - "login_roomDescription": "Insira a senha da sala para acessar as configurações e o status.", + "login_savePasswordSubtitle": "A senha será armazenada com segurança neste dispositivo.", + "login_repeaterDescription": "Insira a senha do repetidor para acessar as configurações e o status.", + "login_roomDescription": "Insira a senha da sala para acessar as configurações e o status.", "login_routing": "Rotas", "login_routingMode": "Modo de roteamento", "login_autoUseSavedPath": "Auto (usar caminho salvo)", - "login_forceFloodMode": "Modo de Inundação Forçado", + "login_forceFloodMode": "Modo de Inundação Forçado", "login_managePaths": "Gerenciar Caminhos", "login_login": "Login", "login_attempt": "Tentar {current}/{max}", @@ -838,7 +838,7 @@ } } }, - "login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.", + "login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.", "common_reload": "Recarregar", "common_clear": "Limpar", "path_currentPath": "Caminho atual: {path}", @@ -859,14 +859,14 @@ }, "path_enterCustomPath": "Insira Caminho Personalizado", "path_currentPathLabel": "Caminho atual", - "path_hexPrefixInstructions": "Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.", - "path_hexPrefixExample": "A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)", + "path_hexPrefixInstructions": "Insira os prefixos hexadecimais de 2 caracteres para cada salto, separados por vírgulas.", + "path_hexPrefixExample": "A1,F2,3C (cada nó usa o primeiro byte de sua chave pública)", "path_labelHexPrefixes": "Prefixo Hexadecimal", - "path_helperMaxHops": "Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)", + "path_helperMaxHops": "Máximo de 64 saltos. Cada prefixo tem 2 caracteres hexadecimais (1 byte)", "path_selectFromContacts": "Ou selecione de contatos:", - "path_noRepeatersFound": "Não foram encontrados repetidores ou servidores de sala.", - "path_customPathsRequire": "Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.", - "path_invalidHexPrefixes": "Prefixos hexadecimais inválidos: {prefixes}", + "path_noRepeatersFound": "Não foram encontrados repetidores ou servidores de sala.", + "path_customPathsRequire": "Caminhos personalizados exigem saltos intermediários que podem transmitir mensagens.", + "path_invalidHexPrefixes": "Prefixos hexadecimais inválidos: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,25 +874,25 @@ } } }, - "path_tooLong": "Caminho muito longo. Máximo de 64 saltos permitidos.", + "path_tooLong": "Caminho muito longo. Máximo de 64 saltos permitidos.", "path_setPath": "Definir Caminho", "repeater_management": "Gerenciamento de Repetidor", "repeater_managementTools": "Ferramentas de Gerenciamento", "repeater_status": "Status", - "repeater_statusSubtitle": "Visualizar status do repetidor, estatísticas e vizinhos.", + "repeater_statusSubtitle": "Visualizar status do repetidor, estatísticas e vizinhos.", "repeater_telemetry": "Telemetria", - "repeater_telemetrySubtitle": "Visualizar telemetria de sensores e estatísticas do sistema", + "repeater_telemetrySubtitle": "Visualizar telemetria de sensores e estatísticas do sistema", "repeater_cli": "CLI", "repeater_cliSubtitle": "Enviar comandos ao repetidor", - "repeater_settings": "Configurações", - "repeater_settingsSubtitle": "Configurar parâmetros do repetidor", + "repeater_settings": "Configurações", + "repeater_settingsSubtitle": "Configurar parâmetros do repetidor", "repeater_statusTitle": "Status do Repetidor", "repeater_routingMode": "Modo de roteamento", "repeater_autoUseSavedPath": "Auto (usar caminho salvo)", - "repeater_forceFloodMode": "Modo de Inundação Forçado", + "repeater_forceFloodMode": "Modo de Inundação Forçado", "repeater_pathManagement": "Gerenciamento de caminhos", "repeater_refresh": "Atualizar", - "repeater_statusRequestTimeout": "Solicitação de status expirou.", + "repeater_statusRequestTimeout": "Solicitação de status expirou.", "repeater_errorLoadingStatus": "Erro ao carregar o status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -901,19 +901,19 @@ } } }, - "repeater_systemInformation": "Informações do Sistema", + "repeater_systemInformation": "Informações do Sistema", "repeater_battery": "Bateria", - "repeater_clockAtLogin": "Relógio (no login)", + "repeater_clockAtLogin": "Relógio (no login)", "repeater_uptime": "Disponibilidade", "repeater_queueLength": "Comprimento da Fila", - "repeater_debugFlags": "Marcar Flags de Depuração", - "repeater_radioStatistics": "Estatísticas de Rádio", - "repeater_lastRssi": "Último RSSI", - "repeater_lastSnr": "Último SNR", - "repeater_noiseFloor": "Nível de Ruído", + "repeater_debugFlags": "Marcar Flags de Depuração", + "repeater_radioStatistics": "Estatísticas de Rádio", + "repeater_lastRssi": "Último RSSI", + "repeater_lastSnr": "Último SNR", + "repeater_noiseFloor": "Nível de Ruído", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Estatísticas de Pacote", + "repeater_packetStatistics": "Estatísticas de Pacote", "repeater_sent": "Enviado", "repeater_received": "Recebido", "repeater_duplicates": "Duplicatas", @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", + "repeater_packetTxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", + "repeater_packetRxTotal": "Total: {total}, Inundação: {flood}, Direto: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Inundação: {flood}, Direto: {direct}", + "repeater_duplicatesFloodDirect": "Inundação: {flood}, Direto: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,23 +981,23 @@ } } }, - "repeater_settingsTitle": "Configurações do Repetidor", - "repeater_basicSettings": "Configurações Básicas", + "repeater_settingsTitle": "Configurações do Repetidor", + "repeater_basicSettings": "Configurações Básicas", "repeater_repeaterName": "Nome do Repetidor", "repeater_repeaterNameHelper": "Exibir nome para este repetidor", "repeater_adminPassword": "Senha de Administrador", "repeater_adminPasswordHelper": "Acesso completo de senha", "repeater_guestPassword": "Senha de convidado", "repeater_guestPasswordHelper": "Acesso com senha de leitura somente", - "repeater_radioSettings": "Configurações de Rádio", - "repeater_frequencyMhz": "Frequência (MHz)", + "repeater_radioSettings": "Configurações de Rádio", + "repeater_frequencyMhz": "Frequência (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "Largura de banda", - "repeater_spreadingFactor": "Fator de Dispersão", - "repeater_codingRate": "Taxa de Codificação", - "repeater_locationSettings": "Configurações de Localização", + "repeater_spreadingFactor": "Fator de Dispersão", + "repeater_codingRate": "Taxa de Codificação", + "repeater_locationSettings": "Configurações de Localização", "repeater_latitude": "Latitude", "repeater_latitudeHelper": "Graus decimais (por exemplo, 37,7749)", "repeater_longitude": "Longitude", @@ -1008,9 +1008,9 @@ "repeater_guestAccess": "Acesso de Convidado", "repeater_guestAccessSubtitle": "Permitir acesso de convidado somente leitura", "repeater_privacyMode": "Modo de Privacidade", - "repeater_privacyModeSubtitle": "Esconder nome/localização em anúncios", - "repeater_advertisementSettings": "Configurações de Anúncios", - "repeater_localAdvertInterval": "Intervalo de Anúncio Local", + "repeater_privacyModeSubtitle": "Esconder nome/localização em anúncios", + "repeater_advertisementSettings": "Configurações de Anúncios", + "repeater_localAdvertInterval": "Intervalo de Anúncio Local", "repeater_localAdvertIntervalMinutes": "{minutes} minutos", "@repeater_localAdvertIntervalMinutes": { "placeholders": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Intervalo de Anúncio de Inundação", + "repeater_floodAdvertInterval": "Intervalo de Anúncio de Inundação", "repeater_floodAdvertIntervalHours": "{hours} horas", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1028,18 +1028,18 @@ } } }, - "repeater_encryptedAdvertInterval": "Intervalo de Anúncio Criptografado", + "repeater_encryptedAdvertInterval": "Intervalo de Anúncio Criptografado", "repeater_dangerZone": "Zona de Perigo", "repeater_rebootRepeater": "Reiniciar Repetidor", "repeater_rebootRepeaterSubtitle": "Reiniciar o dispositivo repetidor", "repeater_rebootRepeaterConfirm": "Tem certeza de que deseja reiniciar este repetidor?", "repeater_regenerateIdentityKey": "Gerar Chave de Identidade", - "repeater_regenerateIdentityKeySubtitle": "Gerar nova chave pública/privada", - "repeater_regenerateIdentityKeyConfirm": "Isso gerará uma nova identidade para o repetidor. Continuar?", + "repeater_regenerateIdentityKeySubtitle": "Gerar nova chave pública/privada", + "repeater_regenerateIdentityKeyConfirm": "Isso gerará uma nova identidade para o repetidor. Continuar?", "repeater_eraseFileSystem": "Excluir Sistema de Arquivos", "repeater_eraseFileSystemSubtitle": "Formatar o sistema de arquivos do repetidor", - "repeater_eraseFileSystemConfirm": "AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!", - "repeater_eraseSerialOnly": "Apagar está disponível apenas via console serial.", + "repeater_eraseFileSystemConfirm": "AVISO: Isso apagará todos os dados no repetidor. Isso não pode ser desfeito!", + "repeater_eraseSerialOnly": "Apagar está disponível apenas via console serial.", "repeater_commandSent": "Comando enviado: {command}", "@repeater_commandSent": { "placeholders": { @@ -1057,8 +1057,8 @@ } }, "repeater_confirm": "Confirmar", - "repeater_settingsSaved": "Configurações salvas com sucesso", - "repeater_errorSavingSettings": "Erro ao salvar as configurações: {error}", + "repeater_settingsSaved": "Configurações salvas com sucesso", + "repeater_errorSavingSettings": "Erro ao salvar as configurações: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,14 +1066,14 @@ } } }, - "repeater_refreshBasicSettings": "Atualizar Configurações Básicas", - "repeater_refreshRadioSettings": "Atualizar Configurações de Rádio", + "repeater_refreshBasicSettings": "Atualizar Configurações Básicas", + "repeater_refreshRadioSettings": "Atualizar Configurações de Rádio", "repeater_refreshTxPower": "Atualizar TX de energia", - "repeater_refreshLocationSettings": "Atualizar Configurações de Localização", + "repeater_refreshLocationSettings": "Atualizar Configurações de Localização", "repeater_refreshPacketForwarding": "Atualizar Roteamento de Pacotes", "repeater_refreshGuestAccess": "Atualizar Acesso de Convidados", "repeater_refreshPrivacyMode": "Atualizar Modo Privacidade", - "repeater_refreshAdvertisementSettings": "Atualizar Configurações do Anúncio", + "repeater_refreshAdvertisementSettings": "Atualizar Configurações do Anúncio", "repeater_refreshed": "{label} atualizado", "@repeater_refreshed": { "placeholders": { @@ -1091,14 +1091,14 @@ } }, "repeater_cliTitle": "Repetidor CLI", - "repeater_debugNextCommand": "Depurar Próximo Comando", + "repeater_debugNextCommand": "Depurar Próximo Comando", "repeater_commandHelp": "Ajuda", - "repeater_clearHistory": "Limpar Histórico", - "repeater_noCommandsSent": "Ainda não foram enviadas comandos.", - "repeater_typeCommandOrUseQuick": "Digite um comando abaixo ou use comandos rápidos", + "repeater_clearHistory": "Limpar Histórico", + "repeater_noCommandsSent": "Ainda não foram enviadas comandos.", + "repeater_typeCommandOrUseQuick": "Digite um comando abaixo ou use comandos rápidos", "repeater_enterCommandHint": "Insira o comando...", "repeater_previousCommand": "Comando anterior", - "repeater_nextCommand": "Próxima ação", + "repeater_nextCommand": "Próxima ação", "repeater_enterCommandFirst": "Insira um comando primeiro", "repeater_cliCommandFrameTitle": "Frame de Comando CLI", "repeater_cliCommandError": "Erro: {error}", @@ -1110,79 +1110,79 @@ } }, "repeater_cliQuickGetName": "Obter Nome", - "repeater_cliQuickGetRadio": "Obter Rádio", + "repeater_cliQuickGetRadio": "Obter Rádio", "repeater_cliQuickGetTx": "Obter TX", "repeater_cliQuickNeighbors": "Vizinhos", - "repeater_cliQuickVersion": "Versão", + "repeater_cliQuickVersion": "Versão", "repeater_cliQuickAdvertise": "Anunciar", - "repeater_cliQuickClock": "Relógio", - "repeater_cliHelpAdvert": "Envia um pacote de anúncios", - "repeater_cliHelpReboot": "Reinicia o dispositivo. (note, você pode obter 'Timeout' que é normal)", - "repeater_cliHelpClock": "Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.", + "repeater_cliQuickClock": "Relógio", + "repeater_cliHelpAdvert": "Envia um pacote de anúncios", + "repeater_cliHelpReboot": "Reinicia o dispositivo. (note, você pode obter 'Timeout' que é normal)", + "repeater_cliHelpClock": "Exibe a hora atual de cada dispositivo, de acordo com o relógio do dispositivo.", "repeater_cliHelpPassword": "Define uma nova senha de administrador para o dispositivo.", - "repeater_cliHelpVersion": "Mostra a versão do dispositivo e a data de construção do firmware.", - "repeater_cliHelpClearStats": "Reseta vários contadores de estatísticas para zero.", + "repeater_cliHelpVersion": "Mostra a versão do dispositivo e a data de construção do firmware.", + "repeater_cliHelpClearStats": "Reseta vários contadores de estatísticas para zero.", "repeater_cliHelpSetAf": "Define o fator de tempo de ar.", - "repeater_cliHelpSetTx": "Define a potência de transmissão LoRa em dBm (redefinir para aplicar).", - "repeater_cliHelpSetRepeat": "Habilita ou desabilita o papel do repetidor para este nó.", - "repeater_cliHelpSetAllowReadOnly": "(Servidor de sala) Se 'ligado', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).", - "repeater_cliHelpSetFloodMax": "Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)", - "repeater_cliHelpSetIntThresh": "Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.", - "repeater_cliHelpSetAgcResetInterval": "Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.", + "repeater_cliHelpSetTx": "Define a potência de transmissão LoRa em dBm (redefinir para aplicar).", + "repeater_cliHelpSetRepeat": "Habilita ou desabilita o papel do repetidor para este nó.", + "repeater_cliHelpSetAllowReadOnly": "(Servidor de sala) Se 'ligado', então o login com senha em branco será permitido, mas não poderá Postar na sala. (apenas ler).", + "repeater_cliHelpSetFloodMax": "Define o número máximo de saltos de pacotes de inundação de entrada (se for >= máximo, o pacote não é encaminhado)", + "repeater_cliHelpSetIntThresh": "Define o Limite de Interferência (em dB). O valor padrão é 14. Defina como 0 para desativar a detecção de interferência de canal.", + "repeater_cliHelpSetAgcResetInterval": "Define o intervalo para resetar o Controlador de Ganho Automático. Defina como 0 para desativar.", "repeater_cliHelpSetMultiAcks": "Habilita ou desabilita a funcionalidade de \"double ACKs\".", - "repeater_cliHelpSetAdvertInterval": "Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.", - "repeater_cliHelpSetFloodAdvertInterval": "Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.", - "repeater_cliHelpSetGuestPassword": "Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")", - "repeater_cliHelpSetName": "Define o nome do anúncio.", - "repeater_cliHelpSetLat": "Define a latitude do mapa de anúncios. (graus decimais)", - "repeater_cliHelpSetLon": "Define a longitude do mapa de anúncios. (graus decimais)", - "repeater_cliHelpSetRadio": "Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.", - "repeater_cliHelpSetRxDelay": "Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.", - "repeater_cliHelpSetTxDelay": "Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)", - "repeater_cliHelpSetDirectTxDelay": "Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.", + "repeater_cliHelpSetAdvertInterval": "Define o intervalo do timer em minutos para enviar um pacote de anúncio local (sem salto). Defina como 0 para desativar.", + "repeater_cliHelpSetFloodAdvertInterval": "Define o intervalo do timer em horas para enviar um pacote de anúncio em massa. Defina como 0 para desativar.", + "repeater_cliHelpSetGuestPassword": "Define/atualiza a senha do convidado. (para repetidores, os logins de convidados podem enviar a solicitação \"Obter Estatísticas\")", + "repeater_cliHelpSetName": "Define o nome do anúncio.", + "repeater_cliHelpSetLat": "Define a latitude do mapa de anúncios. (graus decimais)", + "repeater_cliHelpSetLon": "Define a longitude do mapa de anúncios. (graus decimais)", + "repeater_cliHelpSetRadio": "Define completamente novos parâmetros de rádio e salva nas preferências. Requer um comando \"reboot\" para aplicar.", + "repeater_cliHelpSetRxDelay": "Configurações (experimental) base (deve ser > 1 para efeito) para aplicar um pequeno atraso aos pacotes recebidos, com base na força do sinal/pontuação. Defina como 0 para desativar.", + "repeater_cliHelpSetTxDelay": "Define um fator multiplicado com o tempo-em-ar para um pacote de modo de inundação e com um sistema de slot aleatório, para atrasar seu encaminhamento. (para diminuir a probabilidade de colisões)", + "repeater_cliHelpSetDirectTxDelay": "Igual a txdelay, mas para aplicar um atraso aleatório à encaminhamento de pacotes em modo direto.", "repeater_cliHelpSetBridgeEnabled": "Ativar/Desativar ponte.", "repeater_cliHelpSetBridgeDelay": "Definir atraso antes de retransmitir pacotes.", - "repeater_cliHelpSetBridgeSource": "Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.", + "repeater_cliHelpSetBridgeSource": "Escolha se a ponte retransmitirá pacotes recebidos ou pacotes transmitidos.", "repeater_cliHelpSetBridgeBaud": "Definir a taxa de baud para as pontes rs232.", "repeater_cliHelpSetBridgeSecret": "Definir segredo de ponte para pontes espnow.", "repeater_cliHelpSetAdcMultiplier": "Define um fator personalizado para ajustar a voltagem de bateria relatada (apenas suportado em placas selecionadas).", - "repeater_cliHelpTempRadio": "Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).", - "repeater_cliHelpSetPerm": "Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)", - "repeater_cliHelpGetBridgeType": "Obtém tipo de ponte nenhum, rs232, espnow", + "repeater_cliHelpTempRadio": "Define parâmetros de rádio temporários para o número especificado de minutos, revertendo para os parâmetros de rádio originais posteriormente. (não salva nas preferências).", + "repeater_cliHelpSetPerm": "Modifica o ACL. Remove a entrada correspondente (pelo prefixo de pubkey) se \"permissions\" for zero. Adiciona uma nova entrada se o pubkey-hex for de comprimento total e não estiver atualmente no ACL. Atualiza a entrada por correspondência de prefixo de pubkey. Os bits de permissão variam conforme o papel do firmware, mas os 2 bits inferiores são: 0 (Guest), 1 (Read only), 2 (Read write), 3 (Admin)", + "repeater_cliHelpGetBridgeType": "Obtém tipo de ponte nenhum, rs232, espnow", "repeater_cliHelpLogStart": "Inicia o registro de pacotes no sistema de arquivos.", "repeater_cliHelpLogStop": "Para interromper o registro de pacotes no sistema de arquivos.", "repeater_cliHelpLogErase": "Apaga os logs do pacote do sistema de arquivos.", - "repeater_cliHelpNeighbors": "Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4", - "repeater_cliHelpNeighborRemove": "Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.", - "repeater_cliHelpRegion": "(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.", - "repeater_cliHelpRegionLoad": "NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.", - "repeater_cliHelpRegionGet": "Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) 'F'\"", - "repeater_cliHelpRegionPut": "Adiciona ou atualiza uma definição de região com o nome fornecido.", - "repeater_cliHelpRegionRemove": "Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)", - "repeater_cliHelpRegionAllowf": "Define a permissão de 'F'luido para a região especificada. ('' para o escopo global/legado)", - "repeater_cliHelpRegionDenyf": "Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)", - "repeater_cliHelpRegionHome": "Responde com a região 'home' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)", - "repeater_cliHelpRegionHomeSet": "Define a região 'casa'.", - "repeater_cliHelpRegionSave": "Persiste a lista/mapa de regiões para o armazenamento.", - "repeater_cliHelpGps": "Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.", + "repeater_cliHelpNeighbors": "Mostra uma lista de outros nós de repetição ouvidos através de anúncios zero-hop. Cada linha é id-prefixo-hexadecimal:timestamp:snr-vezes-4", + "repeater_cliHelpNeighborRemove": "Remove a primeira entrada correspondente (por prefixo de chave pública (hexadecimal)) da lista de vizinhos.", + "repeater_cliHelpRegion": "(série apenas) Lista todas as regiões definidas e as permissões de inundação atuais.", + "repeater_cliHelpRegionLoad": "NOTA: isto é uma invocação multi-comando especial. Cada comando subsequente é um nome de região (indentado com espaços para indicar a hierarquia pai, com um espaço mínimo). Terminado enviando uma linha em branco/comando.", + "repeater_cliHelpRegionGet": "Procura região com o prefixo de nome dado (ou \"\\\" para o âmbito global). Responde com \"-> nome-região (nome-pai) 'F'\"", + "repeater_cliHelpRegionPut": "Adiciona ou atualiza uma definição de região com o nome fornecido.", + "repeater_cliHelpRegionRemove": "Remove uma definição de região com o nome fornecido. (deve corresponder exatamente e não ter regiões filhas)", + "repeater_cliHelpRegionAllowf": "Define a permissão de 'F'luido para a região especificada. ('' para o escopo global/legado)", + "repeater_cliHelpRegionDenyf": "Remove a permissão de \"F\"luido para a região especificada. (NOTA: neste momento NÃO é aconselhável usar isso no escopo global/legado!!)", + "repeater_cliHelpRegionHome": "Responde com a região 'home' atual. (Observação aplicada em nenhum lugar ainda, reservado para o futuro)", + "repeater_cliHelpRegionHomeSet": "Define a região 'casa'.", + "repeater_cliHelpRegionSave": "Persiste a lista/mapa de regiões para o armazenamento.", + "repeater_cliHelpGps": "Mostra o status do GPS. Quando o GPS estiver desligado, responde apenas com \"off\", se estiver ligado, responde com \"on\", status, fix, contagem de satélites.", "repeater_cliHelpGpsOnOff": "Alterna o estado de energia do GPS.", - "repeater_cliHelpGpsSync": "Sincroniza o tempo do nó com o relógio GPS.", - "repeater_cliHelpGpsSetLoc": "Define a posição do nó para coordenadas GPS e salvar preferências.", - "repeater_cliHelpGpsAdvert": "Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências", - "repeater_cliHelpGpsAdvertSet": "Define a configuração do anúncio de localização.", + "repeater_cliHelpGpsSync": "Sincroniza o tempo do nó com o relógio GPS.", + "repeater_cliHelpGpsSetLoc": "Define a posição do nó para coordenadas GPS e salvar preferências.", + "repeater_cliHelpGpsAdvert": "Define a configuração de anúncio da localização do nó:\n- nenhum: não incluir a localização nos anúncios\n- compartilhar: compartilhar a localização GPS (do SensorManager)\n- preferências: anunciar a localização armazenada nas preferências", + "repeater_cliHelpGpsAdvertSet": "Define a configuração do anúncio de localização.", "repeater_commandsListTitle": "Lista de Comandos", - "repeater_commandsListNote": "NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".", + "repeater_commandsListNote": "NOTA: para os diversos comandos \"set...\", também existe um comando \"get...\".", "repeater_general": "Geral", - "repeater_settingsCategory": "Configurações", + "repeater_settingsCategory": "Configurações", "repeater_bridge": "Ponte", "repeater_logging": "Registrar", "repeater_neighborsRepeaterOnly": "Vizinhos (apenas repetidor)", - "repeater_regionManagementRepeaterOnly": "Gerenciamento de Região (Apenas Repetidor)", - "repeater_regionNote": "Os comandos de região foram introduzidos para gerenciar definições e permissões de região.", + "repeater_regionManagementRepeaterOnly": "Gerenciamento de Região (Apenas Repetidor)", + "repeater_regionNote": "Os comandos de região foram introduzidos para gerenciar definições e permissões de região.", "repeater_gpsManagement": "Gerenciamento GPS", - "repeater_gpsNote": "O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.", + "repeater_gpsNote": "O comando GPS foi introduzido para gerenciar tópicos relacionados à localização.", "telemetry_receivedData": "Dados de Telemetria Recebidos", - "telemetry_requestTimeout": "Solicitação de telemetria expirou o tempo.", + "telemetry_requestTimeout": "Solicitação de telemetria expirou o tempo.", "telemetry_errorLoading": "Erro ao carregar a telemetria: {error}", "@telemetry_errorLoading": { "placeholders": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Não estão disponíveis dados de telemetria.", + "telemetry_noData": "Não estão disponíveis dados de telemetria.", "telemetry_channelTitle": "Canal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1201,7 +1201,7 @@ } }, "telemetry_batteryLabel": "Bateria", - "telemetry_voltageLabel": "Tensão", + "telemetry_voltageLabel": "Tensão", "telemetry_mcuTemperatureLabel": "Temperatura do MCU", "telemetry_temperatureLabel": "Temperatura", "telemetry_currentLabel": "Atual", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Rótulo de Caminho de Pacote", + "channelPath_title": "Rótulo de Caminho de Pacote", "channelPath_viewMap": "Ver mapa", "channelPath_otherObservedPaths": "Outros Caminhos Observados", "channelPath_repeaterHops": "Saltos do Repetidor", - "channelPath_noHopDetails": "Os detalhes do pacote não estão disponíveis.", + "channelPath_noHopDetails": "Os detalhes do pacote não estão disponíveis.", "channelPath_messageDetails": "Detalhes da Mensagem", "channelPath_senderLabel": "Remetente", "channelPath_timeLabel": "Tempo", "channelPath_repeatsLabel": "Repete", "channelPath_pathLabel": "Caminho {index}", "channelPath_observedLabel": "Observado", - "channelPath_observedPathTitle": "Rastreamento observado {index} • {hops}", + "channelPath_observedPathTitle": "Rastreamento observado {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Não há dados de localização.", + "channelPath_noLocationData": "Não há dados de localização.", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,7 +1289,7 @@ } }, "channelPath_unknownPath": "Desconhecido", - "channelPath_floodPath": "Inundação", + "channelPath_floodPath": "Inundação", "channelPath_directPath": "Salvar", "channelPath_observedZeroOf": "0 de {total} saltos", "@channelPath_observedZeroOf": { @@ -1311,8 +1311,8 @@ } }, "channelPath_mapTitle": "Mapa de Caminhos", - "channelPath_noRepeaterLocations": "Não estão disponíveis localizações de repetidores para este caminho.", - "channelPath_primaryPath": "Caminho {index} (Primário)", + "channelPath_noRepeaterLocations": "Não estão disponíveis localizações de repetidores para este caminho.", + "channelPath_primaryPath": "Caminho {index} (Primário)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Caminho", "channelPath_observedPathHeader": "Rastreamento Observado", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,19 +1340,19 @@ } } }, - "channelPath_noHopDetailsAvailable": "Não estão disponíveis detalhes de voo para este pacote.", + "channelPath_noHopDetailsAvailable": "Não estão disponíveis detalhes de voo para este pacote.", "channelPath_unknownRepeater": "Repetidor Desconhecido", "listFilter_tooltip": "Filtrar e ordenar", "listFilter_sortBy": "Ordenar por", - "listFilter_latestMessages": "Últimas mensagens", + "listFilter_latestMessages": "Últimas mensagens", "listFilter_heardRecently": "Ouvido recentemente", "listFilter_az": "A-Z", "listFilter_filters": "Filtros", "listFilter_all": "Tudo", - "listFilter_users": "Usuários", + "listFilter_users": "Usuários", "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de sala", - "listFilter_unreadOnly": "Apenas não lido", + "listFilter_unreadOnly": "Apenas não lido", "listFilter_newGroup": "Novo grupo", "@neighbors_errorLoading": { "placeholders": { @@ -1367,16 +1367,16 @@ "neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.", "neighbors_errorLoading": "Erro ao carregar vizinhos: {error}", "neighbors_repeatersNeighbors": "Repetidores Vizinhos", - "neighbors_noData": "Não estão disponíveis dados de vizinhos.", + "neighbors_noData": "Não estão disponíveis dados de vizinhos.", "channels_createPrivateChannelDesc": "Protegido com uma chave secreta.", "channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.", "channels_createPrivateChannel": "Criar um Canal Privado", "channels_joinPrivateChannel": "Junte-se a um Canal Privado", - "channels_joinPublicChannel": "Junte-se ao Canal Público", + "channels_joinPublicChannel": "Junte-se ao Canal Público", "channels_joinPublicChannelDesc": "Qualquer pessoa pode entrar neste canal.", "channels_joinHashtagChannel": "Junte-se a um Canal com Hashtag", "channels_joinHashtagChannelDesc": "Qualquer pessoa pode participar de canais com hashtag.", - "channels_scanQrCode": "Digitalizar um Código QR", + "channels_scanQrCode": "Digitalizar um Código QR", "channels_scanQrCodeComingSoon": "Em breve", "channels_enterHashtag": "Insira hashtag", "channels_hashtagHint": "ex. #equipe", @@ -1394,10 +1394,10 @@ } } }, - "neighbors_heardAgo": "Ouvido: {time} atrás", + "neighbors_heardAgo": "Ouvido: {time} atrás", "neighbors_unknownContact": "{pubkey} Desconhecido", "settings_locationGPSEnable": "Ativar GPS", - "settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.", + "settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.", "settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.", "settings_locationIntervalSec": "Intervalo para GPS (Segundos)", "contacts_manageRoom": "Gerenciar Servidor de Sala", @@ -1459,35 +1459,35 @@ } }, "community_title": "Comunidade", - "community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.", + "community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.", "common_ok": "OK", "community_create": "Criar Comunidade", "community_join": "Junte-se", - "community_joinTitle": "Junte-se à Comunidade", - "community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?", + "community_joinTitle": "Junte-se à Comunidade", + "community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?", "community_scanQr": "Digitalizar a QR Code da Comunidade", - "community_scanInstructions": "Aponte a câmera para um código QR da comunidade", - "community_showQr": "Mostrar Código QR", - "community_publicChannel": "Comunidade Pública", + "community_scanInstructions": "Aponte a câmera para um código QR da comunidade", + "community_showQr": "Mostrar Código QR", + "community_publicChannel": "Comunidade Pública", "community_hashtagChannel": "Hashtag da Comunidade", "community_name": "Nome da Comunidade", "community_enterName": "Insira o nome da comunidade", "community_created": "Comunidade \"{name}\" criada", - "community_joined": "Juntou-se à comunidade \"{name}\"", + "community_joined": "Juntou-se à comunidade \"{name}\"", "community_qrTitle": "Partilhar Comunidade", - "community_qrInstructions": "Escanear este código QR para juntar-se a {name}", - "community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade", - "community_invalidQrCode": "Código QR da comunidade inválido", - "community_alreadyMember": "Já é Membro", - "community_alreadyMemberMessage": "Você já é membro de \"{name}\".", - "community_addPublicChannel": "Adicionar Canal Público da Comunidade", - "community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade", - "community_noCommunities": "Ainda não foram adicionadas comunidades.", - "community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.", + "community_qrInstructions": "Escanear este código QR para juntar-se a {name}", + "community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade", + "community_invalidQrCode": "Código QR da comunidade inválido", + "community_alreadyMember": "Já é Membro", + "community_alreadyMemberMessage": "Você já é membro de \"{name}\".", + "community_addPublicChannel": "Adicionar Canal Público da Comunidade", + "community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade", + "community_noCommunities": "Ainda não foram adicionadas comunidades.", + "community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.", "community_manageCommunities": "Gerenciar Comunidades", "community_delete": "Deixar Comunidade", "community_deleteConfirm": "Sair de \"{name}\"?", - "community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.", + "community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1500,7 +1500,7 @@ "community_addHashtagChannelDesc": "Adicionar um canal de hashtag para esta comunidade", "community_selectCommunity": "Selecione Comunidade", "community_regularHashtag": "Hashtag Regular", - "community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)", + "community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)", "community_communityHashtag": "Hashtag da Comunidade", "community_communityHashtagDesc": "Apenas para membros da comunidade", "community_forCommunity": "Para {name}", @@ -1532,12 +1532,12 @@ } } }, - "community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.", + "community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.", "community_regenerateSecret": "Regenerar Senha Segura", "community_secretRegenerated": "Senha secreta regenerada para \"{name}\"", "community_regenerate": "Regenerar", "community_secretUpdated": "Segredo atualizado para \"{name}\"", - "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++", + "community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++", "community_updateSecret": "Atualizar Segredo", "@contacts_pathTraceTo": { "placeholders": { @@ -1546,82 +1546,82 @@ } } }, - "pathTrace_you": "Você", + "pathTrace_you": "Você", "pathTrace_failed": "Falha no rastreamento de caminho.", - "pathTrace_notAvailable": "Traçado de caminho não disponível.", + "pathTrace_notAvailable": "Traçado de caminho não disponível.", "pathTrace_refreshTooltip": "Atualizar Path Trace.", - "contacts_pathTrace": "Traçado de Caminho", + "contacts_pathTrace": "Traçado de Caminho", "contacts_ping": "Pingar", - "contacts_repeaterPathTrace": "Traçar caminho para repetidor", + "contacts_repeaterPathTrace": "Traçar caminho para repetidor", "contacts_repeaterPing": "Pingar repetidor", - "contacts_roomPathTrace": "Traçar caminho para o servidor da sala", + "contacts_roomPathTrace": "Traçar caminho para o servidor da sala", "contacts_roomPing": "Pingar servidor da sala", "contacts_chatTraceRoute": "Rastrear rota do caminho", "contacts_pathTraceTo": "Rastrear rota para {name}", - "contacts_invalidAdvertFormat": "Dados de Contato Inválidos", - "contacts_clipboardEmpty": "Área de Transferência Está Vazia.", + "contacts_invalidAdvertFormat": "Dados de Contato Inválidos", + "contacts_clipboardEmpty": "Área de Transferência Está Vazia.", "appSettings_languageUk": "Ucraniano", "contacts_contactImported": "Contato foi importado.", - "contacts_zeroHopAdvert": "Anúncio Zero Hop", - "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", - "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", + "contacts_zeroHopAdvert": "Anúncio Zero Hop", + "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", + "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", "appSettings_languageRu": "Russo", "appSettings_enableMessageTracing": "Ativar rastreamento de mensagens", "appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens", - "contacts_ShareContact": "Copiar contato para Área de Transferência", + "contacts_ShareContact": "Copiar contato para Área de Transferência", "contacts_contactImportFailed": "Contato falhou ao ser importado.", - "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", - "contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.", - "contacts_floodAdvert": "Anúncio de Inundação", - "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", - "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", + "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", + "contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.", + "contacts_floodAdvert": "Anúncio de Inundação", + "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", + "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", "notification_activityTitle": "Atividade MeshCore", "notification_messagesCount": "{count} {count, plural, =1{mensagem} other{mensagens}}", "notification_channelMessagesCount": "{count} {count, plural, =1{mensagem de canal} other{mensagens de canal}}", - "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", "notification_newTypeDiscovered": "Novo {contactType} descoberto", "notification_receivedNewMessage": "Nova mensagem recebida", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", - "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", + "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", - "settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.", - "settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO", + "settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.", + "settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO", "settings_gpxExportError": "Ocorreu um erro ao exportar.", "settings_gpxExportAll": "Exportar todos os contatos para GPX", "settings_gpxExportContacts": "Exportar companheiros para GPX", - "settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.", - "settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala", - "settings_gpxExportChat": "Localizações de companheiros", + "settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.", + "settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala", + "settings_gpxExportChat": "Localizações de companheiros", "settings_gpxExportNoContacts": "Nenhum contato para exportar.", "settings_gpxExportAllContacts": "Todos os locais de contatos", "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", - "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!", - "map_runTrace": "Executar Traçado de Caminho", + "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", + "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!", + "map_runTrace": "Executar Traçado de Caminho", "map_pathTraceCancelled": "Rastreamento de caminho cancelado.", "pathTrace_clearTooltip": "Limpar caminho", - "map_removeLast": "Remover Último", - "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", + "map_removeLast": "Remover Último", + "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", "scanner_enableBluetooth": "Ative o Bluetooth", - "scanner_bluetoothOff": "Bluetooth está desativado", + "scanner_bluetoothOff": "Bluetooth está desativado", "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", - "scanner_chromeRequired": "Navegador Chrome necessário", - "scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.", - "snrIndicator_nearByRepeaters": "Repetidores Próximos", - "snrIndicator_lastSeen": "Visto pela última vez", + "scanner_chromeRequired": "Navegador Chrome necessário", + "scanner_chromeRequiredMessage": "Esta aplicação web requer o Google Chrome ou um navegador baseado no Chromium para suporte de Bluetooth.", + "snrIndicator_nearByRepeaters": "Repetidores Próximos", + "snrIndicator_lastSeen": "Visto pela última vez", "chat_ShowAllPaths": "Mostrar todos os caminhos", - "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", - "settings_clientRepeat": "Repetição sem rede", + "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", + "settings_clientRepeat": "Repetição sem rede", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.", - "settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)", + "settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Unidades", - "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsMetric": "Métrico (m/km)", "appSettings_unitsImperial": "Imperial (ft/mi)", - "map_lineOfSight": "Linha de visão", - "map_losScreenTitle": "Linha de visão", - "losSelectStartEnd": "Selecione nós iniciais e finais para LOS.", - "losRunFailed": "Falha na verificação da linha de visão: {error}", + "map_lineOfSight": "Linha de visão", + "map_losScreenTitle": "Linha de visão", + "losSelectStartEnd": "Selecione nós iniciais e finais para LOS.", + "losRunFailed": "Falha na verificação da linha de visão: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1630,10 +1630,10 @@ } }, "losClearAllPoints": "Limpe todos os pontos", - "losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação", + "losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados", - "losShowDisplayNodes": "Mostrar nós de exibição", + "losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados", + "losShowDisplayNodes": "Mostrar nós de exibição", "losCustomPoints": "Pontos personalizados", "losCustomPointLabel": "{index} personalizado", "@losCustomPointLabel": { @@ -1668,8 +1668,8 @@ } }, "losRun": "Executar LOS", - "losNoElevationData": "Sem dados de elevação", - "losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}", + "losNoElevationData": "Sem dados de elevação", + "losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.", - "losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.", + "losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.", + "losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.", "losRenameCustomPoint": "Renomear ponto personalizado", "losPointName": "Nome do ponto", "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", - "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Horizonte de rádio", + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte de rádio", "losLegendLosBeam": "Linha de visada", "losLegendTerrain": "Terreno", - "losFrequencyLabel": "Frequência", - "losFrequencyInfoTooltip": "Ver detalhes do cálculo", - "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", - "losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", + "losFrequencyLabel": "Frequência", + "losFrequencyInfoTooltip": "Ver detalhes do cálculo", + "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", + "losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1798,15 +1798,27 @@ }, "contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...", "contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...", - "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", + "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", "contacts_searchContactsNoNumber": "Pesquisar Contatos...", - "contacts_unread": "Não lido", + "contacts_unread": "Não lido", "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala...", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenNote": "A comunicação serial USB está ativa em dispositivos Android e plataformas de desktop compatíveis.", - "usbScreenSubtitle": "Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.", - "usbScreenStatus": "Selecione um dispositivo USB", "usbScreenTitle": "Conecte via USB", - "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize." + "usbScreenSubtitle": "Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.", + "usbScreenStatus": "Selecione um dispositivo USB", + "usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.", + "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.", + "usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.", + "usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.", + "usbErrorInvalidPort": "Selecione um dispositivo USB válido.", + "usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.", + "usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.", + "usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.", + "usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.", + "usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.", + "usbErrorAlreadyActive": "A conexão USB já está ativa.", + "usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.", + "usbErrorPortClosed": "A conexão USB não está ativa.", + "usbErrorConnectTimedOut": "Tempo limite aguardando a resposta do dispositivo.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b0d7d50..bfbee95 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", +{ + "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,757 +9,757 @@ }, "@@locale": "ru", "appTitle": "MeshCore Open", - "nav_contacts": "Контакты", - "nav_channels": "Каналы", - "nav_map": "Карта", - "common_cancel": "Отмена", + "nav_contacts": "Контакты", + "nav_channels": "Каналы", + "nav_map": "Карта", + "common_cancel": "Отмена", "common_ok": "OK", - "common_connect": "Коннект", - "common_unknownDevice": "Неизвестное устройство", - "common_save": "Сохранить", - "common_delete": "Удалить", - "common_close": "Закрыть", - "common_edit": "Изменить", - "common_add": "Добавить", - "common_settings": "Настройки", - "common_disconnect": "Отключить", - "common_connected": "Подключено", - "common_disconnected": "Отключено", - "common_create": "Создать", - "common_continue": "Продолжить", - "common_share": "Поделиться", - "common_copy": "Копировать", - "common_retry": "Повторить", - "common_hide": "Скрыть", - "common_remove": "Убрать", - "common_enable": "Включить", - "common_disable": "Выключить", - "common_reboot": "Перезагрузить", - "common_loading": "Загрузка...", - "common_notAvailable": "—", - "common_voltageValue": "{volts} Ð’", + "common_connect": "Коннект", + "common_unknownDevice": "Неизвестное устройство", + "common_save": "Сохранить", + "common_delete": "Удалить", + "common_close": "Закрыть", + "common_edit": "Изменить", + "common_add": "Добавить", + "common_settings": "Настройки", + "common_disconnect": "Отключить", + "common_connected": "Подключено", + "common_disconnected": "Отключено", + "common_create": "Создать", + "common_continue": "Продолжить", + "common_share": "Поделиться", + "common_copy": "Копировать", + "common_retry": "Повторить", + "common_hide": "Скрыть", + "common_remove": "Убрать", + "common_enable": "Включить", + "common_disable": "Выключить", + "common_reboot": "Перезагрузить", + "common_loading": "Загрузка...", + "common_notAvailable": "—", + "common_voltageValue": "{volts} В", "common_percentValue": "{percent}%", "scanner_title": "MeshCore Open", - "scanner_scanning": "Поиск устройств...", - "scanner_connecting": "Подключение...", - "scanner_disconnecting": "Отключение...", - "scanner_notConnected": "Не подключено", - "scanner_connectedTo": "Подключено к {deviceName}", - "scanner_searchingDevices": "Поиск устройств MeshCore...", - "scanner_tapToScan": "Нажмите для поиска MeshCore устройств", - "scanner_connectionFailed": "Подключение не удалось: {error}", - "scanner_stop": "Стоп", - "scanner_scan": "Сканирование", - "device_quickSwitch": "Быстрое переключение", + "scanner_scanning": "Поиск устройств...", + "scanner_connecting": "Подключение...", + "scanner_disconnecting": "Отключение...", + "scanner_notConnected": "Не подключено", + "scanner_connectedTo": "Подключено к {deviceName}", + "scanner_searchingDevices": "Поиск устройств MeshCore...", + "scanner_tapToScan": "Нажмите для поиска MeshCore устройств", + "scanner_connectionFailed": "Подключение не удалось: {error}", + "scanner_stop": "Стоп", + "scanner_scan": "Сканирование", + "device_quickSwitch": "Быстрое переключение", "device_meshcore": "MeshCore", - "settings_title": "Настройки", - "settings_deviceInfo": "Информация об устройстве", - "settings_appSettings": "Настройки приложения", - "settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты", - "settings_nodeSettings": "Настройки ноды", - "settings_nodeName": "Имя ноды", - "settings_nodeNameNotSet": "Не установлено", - "settings_nodeNameHint": "Введите имя ноды", - "settings_nodeNameUpdated": "Имя обновлено", - "settings_radioSettings": "Настройки радио", - "settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения", - "settings_radioSettingsUpdated": "Настройки радио обновлены", - "settings_location": "Позиция", - "settings_locationSubtitle": "Координаты GPS", - "settings_locationUpdated": "Позиция и настройки GPS обновлены", - "settings_locationBothRequired": "Введите широту и долготу.", - "settings_locationInvalid": "Неверная широта или долгота.", - "settings_locationGPSEnable": "Включить GPS", - "settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.", - "settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)", - "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", - "settings_latitude": "Широта", - "settings_longitude": "Долгота", - "settings_privacyMode": "Режим конфиденциальности", - "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", - "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", - "settings_privacyModeEnabled": "Режим конфиденциальности включен", - "settings_privacyModeDisabled": "Режим конфиденциальности выключен", - "settings_actions": "Действия", - "settings_sendAdvertisement": "Отправить анонсирование", - "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", - "settings_advertisementSent": "Анонсирование отправлено", - "settings_syncTime": "Синхронизация времени", - "settings_syncTimeSubtitle": "Синхронизировать время с телефоном", - "settings_timeSynchronized": "Время синхронизировано", - "settings_refreshContacts": "Обновить контакты", - "settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства", - "settings_rebootDevice": "Перезагрузить устройство", - "settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore", - "settings_rebootDeviceConfirm": "Ð’Ñ‹ уверены, что хотите перезагрузить устройство? Ð’Ñ‹ будете отключены.", - "settings_debug": "Отладка", - "settings_bleDebugLog": "Журнал отладки BLE", - "settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные", - "settings_appDebugLog": "Журнал отладки приложения", - "settings_appDebugLogSubtitle": "Сообщения отладки приложения", - "settings_about": "О программе", + "settings_title": "Настройки", + "settings_deviceInfo": "Информация об устройстве", + "settings_appSettings": "Настройки приложения", + "settings_appSettingsSubtitle": "Уведомления, сообщения и настройки карты", + "settings_nodeSettings": "Настройки ноды", + "settings_nodeName": "Имя ноды", + "settings_nodeNameNotSet": "Не установлено", + "settings_nodeNameHint": "Введите имя ноды", + "settings_nodeNameUpdated": "Имя обновлено", + "settings_radioSettings": "Настройки радио", + "settings_radioSettingsSubtitle": "Частота, мощность и коэффициент распространения", + "settings_radioSettingsUpdated": "Настройки радио обновлены", + "settings_location": "Позиция", + "settings_locationSubtitle": "Координаты GPS", + "settings_locationUpdated": "Позиция и настройки GPS обновлены", + "settings_locationBothRequired": "Введите широту и долготу.", + "settings_locationInvalid": "Неверная широта или долгота.", + "settings_locationGPSEnable": "Включить GPS", + "settings_locationGPSEnableSubtitle": "Включение GPS для автоматического обновления позиции.", + "settings_locationIntervalSec": "Интервал для позиционирования GPS (секунды)", + "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", + "settings_latitude": "Широта", + "settings_longitude": "Долгота", + "settings_privacyMode": "Режим конфиденциальности", + "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", + "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", + "settings_privacyModeEnabled": "Режим конфиденциальности включен", + "settings_privacyModeDisabled": "Режим конфиденциальности выключен", + "settings_actions": "Действия", + "settings_sendAdvertisement": "Отправить анонсирование", + "settings_sendAdvertisementSubtitle": "Отправить анонсирование о присутствии сейчас", + "settings_advertisementSent": "Анонсирование отправлено", + "settings_syncTime": "Синхронизация времени", + "settings_syncTimeSubtitle": "Синхронизировать время с телефоном", + "settings_timeSynchronized": "Время синхронизировано", + "settings_refreshContacts": "Обновить контакты", + "settings_refreshContactsSubtitle": "Перезагрузить список контактов с устройства", + "settings_rebootDevice": "Перезагрузить устройство", + "settings_rebootDeviceSubtitle": "Перезапустить устройство MeshCore", + "settings_rebootDeviceConfirm": "Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.", + "settings_debug": "Отладка", + "settings_bleDebugLog": "Журнал отладки BLE", + "settings_bleDebugLogSubtitle": "Команды BLE, ответы и сырые данные", + "settings_appDebugLog": "Журнал отладки приложения", + "settings_appDebugLogSubtitle": "Сообщения отладки приложения", + "settings_about": "О программе", "settings_aboutVersion": "MeshCore Open v{version}", "settings_aboutLegalese": "2026 MeshCore Open Source Project", - "settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.", - "settings_infoName": "Имя", + "settings_aboutDescription": "Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.", + "settings_infoName": "Имя", "settings_infoId": "ID", - "settings_infoStatus": "Статус", - "settings_infoBattery": "Батарея", - "settings_infoPublicKey": "Публичный ключ", - "settings_infoContactsCount": "Количество контактов", - "settings_infoChannelCount": "Количество каналов", - "settings_presets": "Пресеты", - "settings_frequency": "Частота (МГц)", - "settings_frequencyHelper": "300.0 – 2500.0", - "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", - "settings_bandwidth": "Полоса пропускания", - "settings_spreadingFactor": "Коэффициент расширения", - "settings_codingRate": "Коэффициент кодирования", - "settings_txPower": "Мощность передачи (дБм)", - "settings_txPowerHelper": "0 – 22", - "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", - "settings_error": "Ошибка: {message}", - "appSettings_title": "Настройки приложения", - "appSettings_appearance": "Внешний вид", - "appSettings_theme": "Тема", - "appSettings_themeSystem": "Как в системе", - "appSettings_themeLight": "Светлая", - "appSettings_themeDark": "Тёмная", - "appSettings_language": "Язык", - "appSettings_languageSystem": "Как в системе", - "appSettings_languageEn": "Английский", - "appSettings_languageFr": "Французский", - "appSettings_languageEs": "Испанский", - "appSettings_languageDe": "Немецкий", - "appSettings_languagePl": "Польский", - "appSettings_languageSl": "Словенский", - "appSettings_languagePt": "Португальский", - "appSettings_languageIt": "Итальянский", - "appSettings_languageZh": "Китайский", - "appSettings_languageSv": "Шведский", - "appSettings_languageNl": "Нидерландский", - "appSettings_languageSk": "Словацкий", - "appSettings_languageBg": "Болгарский", - "appSettings_languageRu": "Русский", - "appSettings_notifications": "Уведомления", - "appSettings_enableNotifications": "Включить уведомления", - "appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях", - "appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено", - "appSettings_notificationsEnabled": "Уведомления включены", - "appSettings_notificationsDisabled": "Уведомления отключены", - "appSettings_messageNotifications": "Уведомления о сообщениях", - "appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений", - "appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах", - "appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах", - "appSettings_advertisementNotifications": "Уведомления об анонсированиях", - "appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод", - "appSettings_messaging": "Обмен сообщениями", - "appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток", - "appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки", - "appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток", - "appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться", - "appSettings_autoRouteRotation": "Автоматическое переключение маршрутов", - "appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки", - "appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено", - "appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено", - "appSettings_battery": "Батарея", - "appSettings_batteryChemistry": "Химия батареи", - "appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})", - "appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать", - "appSettings_batteryNmc": "18650 NMC (3.0–4.2 Ð’)", - "appSettings_batteryLifepo4": "LiFePO4 (2.6–3.65 Ð’)", - "appSettings_batteryLipo": "LiPo (3.0–4.2 Ð’)", - "appSettings_mapDisplay": "Отображение карты", - "appSettings_showRepeaters": "Показывать репитеры", - "appSettings_showRepeatersSubtitle": "Отображать репитеры на карте", - "appSettings_showChatNodes": "Показывать чат-ноды", - "appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте", - "appSettings_showOtherNodes": "Показывать другие ноды", - "appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте", - "appSettings_timeFilter": "Фильтр по времени", - "appSettings_timeFilterShowAll": "Показывать все ноды", - "appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч", - "appSettings_mapTimeFilter": "Временной фильтр карты", - "appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:", - "appSettings_allTime": "Всё время", - "appSettings_lastHour": "Последний час", - "appSettings_last6Hours": "Последние 6 часов", - "appSettings_last24Hours": "Последние 24 часа", - "appSettings_lastWeek": "Последнюю неделю", - "appSettings_offlineMapCache": "Кэш офлайн-карты", - "appSettings_noAreaSelected": "Область не выбрана", - "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", - "appSettings_debugCard": "Отладка", - "appSettings_appDebugLogging": "Журнал отладки приложения", - "appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики", - "appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён", - "appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён", - "contacts_title": "Контакты", - "contacts_noContacts": "Контактов пока нет", - "contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения", - "contacts_searchContacts": "Поиск контактов...", - "contacts_noUnreadContacts": "Нет непрочитанных контактов", - "contacts_noContactsFound": "Контакты или группы не найдены", - "contacts_deleteContact": "Удалить контакт", - "contacts_removeConfirm": "Удалить {contactName} из контактов?", - "contacts_manageRepeater": "Управление репитером", - "contacts_manageRoom": "Управление сервером комнат", - "contacts_roomLogin": "Вход на сервер комнат", - "contacts_openChat": "Открыть чат", - "contacts_editGroup": "Изменить группу", - "contacts_deleteGroup": "Удалить группу", - "contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?", - "contacts_newGroup": "Новая группа", - "contacts_groupName": "Имя группы", - "contacts_groupNameRequired": "Имя группы обязательно", - "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", - "contacts_filterContacts": "Фильтр контактов...", - "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", - "contacts_noMembers": "Нет участников", - "contacts_lastSeenNow": "Видели только что", - "contacts_lastSeenMinsAgo": "Видели {minutes} мин назад", - "contacts_lastSeenHourAgo": "Видели 1 час назад", - "contacts_lastSeenHoursAgo": "Видели {hours} ч назад", - "contacts_lastSeenDayAgo": "Видели 1 день назад", - "contacts_lastSeenDaysAgo": "Видели {days} дн. назад", - "channels_title": "Каналы", - "channels_noChannelsConfigured": "Каналы не настроены", - "channels_addPublicChannel": "Добавить публичный канал", - "channels_searchChannels": "Поиск каналов...", - "channels_noChannelsFound": "Каналы не найдены", - "channels_channelIndex": "Канал {index}", - "channels_hashtagChannel": "Хэштег-канал", - "channels_public": "Публичный", - "channels_private": "Приватный", - "channels_publicChannel": "Публичный канал", - "channels_privateChannel": "Приватный канал", - "channels_editChannel": "Изменить канал", - "channels_muteChannel": "Отключить уведомления канала", - "channels_unmuteChannel": "Включить уведомления канала", - "channels_deleteChannel": "Удалить канал", - "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", - "channels_channelDeleted": "Канал \"{name}\" удалён", - "channels_addChannel": "Добавить канал", - "channels_channelIndexLabel": "Индекс канала", - "channels_channelName": "Имя канала", - "channels_usePublicChannel": "Использовать публичный канал", - "channels_standardPublicPsk": "Стандартный публичный PSK", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батарея", + "settings_infoPublicKey": "Публичный ключ", + "settings_infoContactsCount": "Количество контактов", + "settings_infoChannelCount": "Количество каналов", + "settings_presets": "Пресеты", + "settings_frequency": "Частота (МГц)", + "settings_frequencyHelper": "300.0 – 2500.0", + "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", + "settings_bandwidth": "Полоса пропускания", + "settings_spreadingFactor": "Коэффициент расширения", + "settings_codingRate": "Коэффициент кодирования", + "settings_txPower": "Мощность передачи (дБм)", + "settings_txPowerHelper": "0 – 22", + "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", + "settings_error": "Ошибка: {message}", + "appSettings_title": "Настройки приложения", + "appSettings_appearance": "Внешний вид", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Как в системе", + "appSettings_themeLight": "Светлая", + "appSettings_themeDark": "Тёмная", + "appSettings_language": "Язык", + "appSettings_languageSystem": "Как в системе", + "appSettings_languageEn": "Английский", + "appSettings_languageFr": "Французский", + "appSettings_languageEs": "Испанский", + "appSettings_languageDe": "Немецкий", + "appSettings_languagePl": "Польский", + "appSettings_languageSl": "Словенский", + "appSettings_languagePt": "Португальский", + "appSettings_languageIt": "Итальянский", + "appSettings_languageZh": "Китайский", + "appSettings_languageSv": "Шведский", + "appSettings_languageNl": "Нидерландский", + "appSettings_languageSk": "Словацкий", + "appSettings_languageBg": "Болгарский", + "appSettings_languageRu": "Русский", + "appSettings_notifications": "Уведомления", + "appSettings_enableNotifications": "Включить уведомления", + "appSettings_enableNotificationsSubtitle": "Получать уведомления о сообщениях и оповещениях", + "appSettings_notificationPermissionDenied": "Разрешение на уведомления отклонено", + "appSettings_notificationsEnabled": "Уведомления включены", + "appSettings_notificationsDisabled": "Уведомления отключены", + "appSettings_messageNotifications": "Уведомления о сообщениях", + "appSettings_messageNotificationsSubtitle": "Показывать уведомление при получении новых сообщений", + "appSettings_channelMessageNotifications": "Уведомления о сообщениях в каналах", + "appSettings_channelMessageNotificationsSubtitle": "Показывать уведомление при получении сообщений в каналах", + "appSettings_advertisementNotifications": "Уведомления об анонсированиях", + "appSettings_advertisementNotificationsSubtitle": "Показывать уведомление при обнаружении новых нод", + "appSettings_messaging": "Обмен сообщениями", + "appSettings_clearPathOnMaxRetry": "Сбросить маршрут после максимального числа попыток", + "appSettings_clearPathOnMaxRetrySubtitle": "Сбросить маршрут контакта после 5 неудачных попыток отправки", + "appSettings_pathsWillBeCleared": "Маршруты будут сброшены после 5 неудачных попыток", + "appSettings_pathsWillNotBeCleared": "Маршруты не будут автоматически сбрасываться", + "appSettings_autoRouteRotation": "Автоматическое переключение маршрутов", + "appSettings_autoRouteRotationSubtitle": "Циклически переключаться между лучшими маршрутами и режимом рассылки", + "appSettings_autoRouteRotationEnabled": "Автоматическое переключение маршрутов включено", + "appSettings_autoRouteRotationDisabled": "Автоматическое переключение маршрутов отключено", + "appSettings_battery": "Батарея", + "appSettings_batteryChemistry": "Химия батареи", + "appSettings_batteryChemistryPerDevice": "Установить для устройства ({deviceName})", + "appSettings_batteryChemistryConnectFirst": "Подключитесь к устройству, чтобы выбрать", + "appSettings_batteryNmc": "18650 NMC (3.0–4.2 В)", + "appSettings_batteryLifepo4": "LiFePO4 (2.6–3.65 В)", + "appSettings_batteryLipo": "LiPo (3.0–4.2 В)", + "appSettings_mapDisplay": "Отображение карты", + "appSettings_showRepeaters": "Показывать репитеры", + "appSettings_showRepeatersSubtitle": "Отображать репитеры на карте", + "appSettings_showChatNodes": "Показывать чат-ноды", + "appSettings_showChatNodesSubtitle": "Отображать чат-ноды на карте", + "appSettings_showOtherNodes": "Показывать другие ноды", + "appSettings_showOtherNodesSubtitle": "Отображать другие типы нод на карте", + "appSettings_timeFilter": "Фильтр по времени", + "appSettings_timeFilterShowAll": "Показывать все ноды", + "appSettings_timeFilterShowLast": "Показывать ноды за последние {hours} ч", + "appSettings_mapTimeFilter": "Временной фильтр карты", + "appSettings_showNodesDiscoveredWithin": "Показывать ноды, обнаруженные за:", + "appSettings_allTime": "Всё время", + "appSettings_lastHour": "Последний час", + "appSettings_last6Hours": "Последние 6 часов", + "appSettings_last24Hours": "Последние 24 часа", + "appSettings_lastWeek": "Последнюю неделю", + "appSettings_offlineMapCache": "Кэш офлайн-карты", + "appSettings_noAreaSelected": "Область не выбрана", + "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})", + "appSettings_debugCard": "Отладка", + "appSettings_appDebugLogging": "Журнал отладки приложения", + "appSettings_appDebugLoggingSubtitle": "Записывать отладочные сообщения приложения для диагностики", + "appSettings_appDebugLoggingEnabled": "Журнал отладки приложения включён", + "appSettings_appDebugLoggingDisabled": "Журнал отладки приложения отключён", + "contacts_title": "Контакты", + "contacts_noContacts": "Контактов пока нет", + "contacts_contactsWillAppear": "Контакты появятся, когда устройства начнут рассылать оповещения", + "contacts_searchContacts": "Поиск контактов...", + "contacts_noUnreadContacts": "Нет непрочитанных контактов", + "contacts_noContactsFound": "Контакты или группы не найдены", + "contacts_deleteContact": "Удалить контакт", + "contacts_removeConfirm": "Удалить {contactName} из контактов?", + "contacts_manageRepeater": "Управление репитером", + "contacts_manageRoom": "Управление сервером комнат", + "contacts_roomLogin": "Вход на сервер комнат", + "contacts_openChat": "Открыть чат", + "contacts_editGroup": "Изменить группу", + "contacts_deleteGroup": "Удалить группу", + "contacts_deleteGroupConfirm": "Удалить \"{groupName}\"?", + "contacts_newGroup": "Новая группа", + "contacts_groupName": "Имя группы", + "contacts_groupNameRequired": "Имя группы обязательно", + "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", + "contacts_filterContacts": "Фильтр контактов...", + "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", + "contacts_noMembers": "Нет участников", + "contacts_lastSeenNow": "Видели только что", + "contacts_lastSeenMinsAgo": "Видели {minutes} мин назад", + "contacts_lastSeenHourAgo": "Видели 1 час назад", + "contacts_lastSeenHoursAgo": "Видели {hours} ч назад", + "contacts_lastSeenDayAgo": "Видели 1 день назад", + "contacts_lastSeenDaysAgo": "Видели {days} дн. назад", + "channels_title": "Каналы", + "channels_noChannelsConfigured": "Каналы не настроены", + "channels_addPublicChannel": "Добавить публичный канал", + "channels_searchChannels": "Поиск каналов...", + "channels_noChannelsFound": "Каналы не найдены", + "channels_channelIndex": "Канал {index}", + "channels_hashtagChannel": "Хэштег-канал", + "channels_public": "Публичный", + "channels_private": "Приватный", + "channels_publicChannel": "Публичный канал", + "channels_privateChannel": "Приватный канал", + "channels_editChannel": "Изменить канал", + "channels_muteChannel": "Отключить уведомления канала", + "channels_unmuteChannel": "Включить уведомления канала", + "channels_deleteChannel": "Удалить канал", + "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", + "channels_channelDeleted": "Канал \"{name}\" удалён", + "channels_addChannel": "Добавить канал", + "channels_channelIndexLabel": "Индекс канала", + "channels_channelName": "Имя канала", + "channels_usePublicChannel": "Использовать публичный канал", + "channels_standardPublicPsk": "Стандартный публичный PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Сгенерировать случайный PSK", - "channels_enterChannelName": "Введите имя канала", - "channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа", - "channels_channelAdded": "Канал \"{name}\" добавлен", - "channels_editChannelTitle": "Изменить канал {index}", - "channels_smazCompression": "Сжатие SMAZ", - "channels_channelUpdated": "Канал \"{name}\" обновлён", - "channels_publicChannelAdded": "Публичный канал добавлен", - "channels_sortBy": "Сортировка", - "channels_sortManual": "Вручную", - "channels_sortAZ": "По алфавиту", - "channels_sortLatestMessages": "По последним сообщениям", - "channels_sortUnread": "По непрочитанным", - "channels_createPrivateChannel": "Создать приватный канал", - "channels_createPrivateChannelDesc": "Защищён секретным ключом.", - "channels_joinPrivateChannel": "Присоединиться к приватному каналу", - "channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.", - "channels_joinPublicChannel": "Присоединиться к публичному каналу", - "channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.", - "channels_joinHashtagChannel": "Присоединиться к хэштег-каналу", - "channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.", - "channels_scanQrCode": "Сканировать QR-код", - "channels_scanQrCodeComingSoon": "Скоро будет", - "channels_enterHashtag": "Введите хэштег", - "channels_hashtagHint": "например, #команда", - "chat_noMessages": "Сообщений пока нет", - "chat_sendMessageToStart": "Отправьте сообщение, чтобы начать", - "chat_originalMessageNotFound": "Исходное сообщение не найдено", - "chat_replyingTo": "Ответ для {name}", - "chat_replyTo": "Ответить {name}", - "chat_location": "Местоположение", - "chat_sendMessageTo": "Отправить сообщение {contactName}", - "chat_typeMessage": "Напишите сообщение...", - "chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).", - "chat_messageCopied": "Сообщение скопировано", - "chat_messageDeleted": "Сообщение удалено", - "chat_retryingMessage": "Повтор отправки сообщения", - "chat_retryCount": "Попытка {current}/{max}", - "chat_sendGif": "Отправить GIF", - "chat_reply": "Ответить", - "chat_addReaction": "Добавить реакцию", - "chat_me": "Я", - "emojiCategorySmileys": "Смайлы", - "emojiCategoryGestures": "Жесты", - "emojiCategoryHearts": "Сердечки", - "emojiCategoryObjects": "Предметы", - "gifPicker_title": "Выберите GIF", - "gifPicker_searchHint": "Поиск GIF...", - "gifPicker_poweredBy": "Работает на GIPHY", - "gifPicker_noGifsFound": "GIF не найдены", - "gifPicker_failedLoad": "Не удалось загрузить GIF", - "gifPicker_failedSearch": "Не удалось выполнить поиск GIF", - "gifPicker_noInternet": "Нет подключения к интернету", - "debugLog_appTitle": "Журнал отладки приложения", - "debugLog_bleTitle": "Журнал отладки BLE", - "debugLog_copyLog": "Копировать журнал", - "debugLog_clearLog": "Очистить журнал", - "debugLog_copied": "Журнал отладки скопирован", - "debugLog_bleCopied": "Журнал BLE скопирован", - "debugLog_noEntries": "Журнал отладки пока пуст", - "debugLog_enableInSettings": "Включите запись журнала отладки в настройках", - "debugLog_frames": "Фреймы", - "debugLog_rawLogRx": "Сырой журнал приёма", - "debugLog_noBleActivity": "Активность BLE пока отсутствует", - "debugFrame_length": "Длина фрейма: {count} байт", - "debugFrame_command": "Команда: 0x{value}", - "debugFrame_textMessageHeader": "Фрейм текстового сообщения:", - "debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}", - "debugFrame_timestamp": "- Временная метка: {timestamp}", - "debugFrame_flags": "- Флаги: 0x{value}", - "debugFrame_textType": "- Тип текста: {type} ({label})", + "channels_generateRandomPsk": "Сгенерировать случайный PSK", + "channels_enterChannelName": "Введите имя канала", + "channels_pskMustBe32Hex": "PSK должен содержать 32 шестнадцатеричных символа", + "channels_channelAdded": "Канал \"{name}\" добавлен", + "channels_editChannelTitle": "Изменить канал {index}", + "channels_smazCompression": "Сжатие SMAZ", + "channels_channelUpdated": "Канал \"{name}\" обновлён", + "channels_publicChannelAdded": "Публичный канал добавлен", + "channels_sortBy": "Сортировка", + "channels_sortManual": "Вручную", + "channels_sortAZ": "По алфавиту", + "channels_sortLatestMessages": "По последним сообщениям", + "channels_sortUnread": "По непрочитанным", + "channels_createPrivateChannel": "Создать приватный канал", + "channels_createPrivateChannelDesc": "Защищён секретным ключом.", + "channels_joinPrivateChannel": "Присоединиться к приватному каналу", + "channels_joinPrivateChannelDesc": "Введите секретный ключ вручную.", + "channels_joinPublicChannel": "Присоединиться к публичному каналу", + "channels_joinPublicChannelDesc": "К этому каналу может присоединиться любой.", + "channels_joinHashtagChannel": "Присоединиться к хэштег-каналу", + "channels_joinHashtagChannelDesc": "К хэштег-каналам может присоединиться любой.", + "channels_scanQrCode": "Сканировать QR-код", + "channels_scanQrCodeComingSoon": "Скоро будет", + "channels_enterHashtag": "Введите хэштег", + "channels_hashtagHint": "например, #команда", + "chat_noMessages": "Сообщений пока нет", + "chat_sendMessageToStart": "Отправьте сообщение, чтобы начать", + "chat_originalMessageNotFound": "Исходное сообщение не найдено", + "chat_replyingTo": "Ответ для {name}", + "chat_replyTo": "Ответить {name}", + "chat_location": "Местоположение", + "chat_sendMessageTo": "Отправить сообщение {contactName}", + "chat_typeMessage": "Напишите сообщение...", + "chat_messageTooLong": "Сообщение слишком длинное (макс. {maxBytes} байт).", + "chat_messageCopied": "Сообщение скопировано", + "chat_messageDeleted": "Сообщение удалено", + "chat_retryingMessage": "Повтор отправки сообщения", + "chat_retryCount": "Попытка {current}/{max}", + "chat_sendGif": "Отправить GIF", + "chat_reply": "Ответить", + "chat_addReaction": "Добавить реакцию", + "chat_me": "Я", + "emojiCategorySmileys": "Смайлы", + "emojiCategoryGestures": "Жесты", + "emojiCategoryHearts": "Сердечки", + "emojiCategoryObjects": "Предметы", + "gifPicker_title": "Выберите GIF", + "gifPicker_searchHint": "Поиск GIF...", + "gifPicker_poweredBy": "Работает на GIPHY", + "gifPicker_noGifsFound": "GIF не найдены", + "gifPicker_failedLoad": "Не удалось загрузить GIF", + "gifPicker_failedSearch": "Не удалось выполнить поиск GIF", + "gifPicker_noInternet": "Нет подключения к интернету", + "debugLog_appTitle": "Журнал отладки приложения", + "debugLog_bleTitle": "Журнал отладки BLE", + "debugLog_copyLog": "Копировать журнал", + "debugLog_clearLog": "Очистить журнал", + "debugLog_copied": "Журнал отладки скопирован", + "debugLog_bleCopied": "Журнал BLE скопирован", + "debugLog_noEntries": "Журнал отладки пока пуст", + "debugLog_enableInSettings": "Включите запись журнала отладки в настройках", + "debugLog_frames": "Фреймы", + "debugLog_rawLogRx": "Сырой журнал приёма", + "debugLog_noBleActivity": "Активность BLE пока отсутствует", + "debugFrame_length": "Длина фрейма: {count} байт", + "debugFrame_command": "Команда: 0x{value}", + "debugFrame_textMessageHeader": "Фрейм текстового сообщения:", + "debugFrame_destinationPubKey": "- Публичный ключ получателя: {pubKey}", + "debugFrame_timestamp": "- Временная метка: {timestamp}", + "debugFrame_flags": "- Флаги: 0x{value}", + "debugFrame_textType": "- Тип текста: {type} ({label})", "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Обычный", - "debugFrame_text": "- Текст: \"{text}\"", - "debugFrame_hexDump": "Шестнадцатеричный дамп:", - "chat_pathManagement": "Управление маршрутами", - "chat_routingMode": "Режим маршрутизации", - "chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", - "chat_forceFloodMode": "Принудительный режим рассылки", - "chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):", - "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", - "chat_hopSingular": "хоп", - "chat_hopPlural": "хопов", - "chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", - "chat_successes": "успешно", - "chat_removePath": "Удалить маршрут", - "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", - "chat_pathActions": "Действия с маршрутом:", - "chat_setCustomPath": "Указать маршрут вручную", - "chat_setCustomPathSubtitle": "Вручную задать маршрут передачи", - "chat_clearPath": "Очистить маршрут", - "chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке", - "chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.", - "chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения", - "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", - "chat_fullPath": "Полный маршрут", - "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", - "chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}", - "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", - "chat_pathDeviceConfirmed": "Подтверждено устройством.", - "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", - "chat_type": "Тип", - "chat_path": "Маршрут", - "chat_publicKey": "Публичный ключ", - "chat_compressOutgoingMessages": "Сжимать исходящие сообщения", - "chat_floodForced": "Рассылка (принудительно)", - "chat_directForced": "Прямой (принудительно)", - "chat_hopsForced": "{count} хоп(ов) (принудительно)", - "chat_floodAuto": "Рассылка (авто)", - "chat_direct": "Прямой", - "chat_poiShared": "Точка интереса отправлена", - "chat_unread": "Непрочитанных: {count}", - "map_title": "Карта нод", - "map_noNodesWithLocation": "Нет нод с данными о местоположении", - "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте", - "map_nodesCount": "Нод: {count}", - "map_pinsCount": "Меток: {count}", - "map_chat": "Чат", - "map_repeater": "Репитер", - "map_room": "Комната", - "map_sensor": "Сенсор", - "map_pinDm": "Метка (ЛС)", - "map_pinPrivate": "Метка (Приватная)", - "map_pinPublic": "Метка (Публичная)", - "map_lastSeen": "Последнее появление", - "map_disconnectConfirm": "Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?", - "map_from": "От", - "map_source": "Источник", - "map_flags": "Флаги", - "map_shareMarkerHere": "Поделиться меткой здесь", - "map_pinLabel": "Метка", - "map_label": "Подпись", - "map_pointOfInterest": "Точка интереса", - "map_sendToContact": "Отправить контакту", - "map_sendToChannel": "Отправить в канал", - "map_noChannelsAvailable": "Нет доступных каналов", - "map_publicLocationShare": "Публичная передача местоположения", - "map_publicLocationShareConfirm": "Ð’Ñ‹ собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.", - "map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками", - "map_filterNodes": "Фильтр нод", - "map_nodeTypes": "Типы нод", - "map_chatNodes": "Чат-ноды", - "map_repeaters": "Репитеры", - "map_otherNodes": "Другие ноды", - "map_keyPrefix": "Префикс ключа", - "map_filterByKeyPrefix": "Фильтр по префиксу ключа", - "map_publicKeyPrefix": "Префикс публичного ключа", - "map_markers": "Метки", - "map_showSharedMarkers": "Показывать общие метки", - "map_lastSeenTime": "Время последнего появления", - "map_sharedPin": "Общая метка", - "map_joinRoom": "Присоединиться к комнате", - "map_manageRepeater": "Управление репитером", - "mapCache_title": "Кэш офлайн-карты", - "mapCache_selectAreaFirst": "Сначала выберите область для кэширования", - "mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области", - "mapCache_downloadTilesTitle": "Загрузить плитки", - "mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?", - "mapCache_downloadAction": "Загрузить", - "mapCache_cachedTiles": "Закэшировано {count} плиток", - "mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)", - "mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш", - "mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?", - "mapCache_offlineCacheCleared": "Офлайн-кэш очищен", - "mapCache_noAreaSelected": "Область не выбрана", - "mapCache_cacheArea": "Область кэширования", - "mapCache_useCurrentView": "Использовать текущий вид", - "mapCache_zoomRange": "Диапазон масштаба", - "mapCache_estimatedTiles": "Оценочное количество плиток: {count}", - "mapCache_downloadedTiles": "Загружено {completed} из {total}", - "mapCache_downloadTilesButton": "Загрузить плитки", - "mapCache_clearCacheButton": "Очистить кэш", - "mapCache_failedDownloads": "Неудачных загрузок: {count}", - "mapCache_boundsLabel": "С {north}, Ю {south}, Ð’ {east}, З {west}", - "time_justNow": "Только что", - "time_minutesAgo": "{minutes} мин назад", - "time_hoursAgo": "{hours} ч назад", - "time_daysAgo": "{days} дн. назад", - "time_hour": "час", - "time_hours": "часов", - "time_day": "день", - "time_days": "дней", - "time_week": "неделя", - "time_weeks": "недель", - "time_month": "месяц", - "time_months": "месяцев", - "time_minutes": "минут", - "time_allTime": "Всё время", - "dialog_disconnect": "Отключиться", - "dialog_disconnectConfirm": "Ð’Ñ‹ уверены, что хотите отключиться от этого устройства?", - "login_repeaterLogin": "Вход в репитер", - "login_roomLogin": "Вход на сервер комнат", - "login_password": "Пароль", - "login_enterPassword": "Введите пароль", - "login_savePassword": "Сохранить пароль", - "login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве", - "login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.", - "login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.", - "login_routing": "Маршрутизация", - "login_routingMode": "Режим маршрутизации", - "login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", - "login_forceFloodMode": "Принудительный режим рассылки", - "login_managePaths": "Управление маршрутами", - "login_login": "Войти", - "login_attempt": "Попытка {current}/{max}", - "login_failed": "Ошибка входа: {error}", - "login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.", - "common_reload": "Обновить", - "common_clear": "Очистить", - "path_currentPath": "Текущий маршрут: {path}", - "path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", - "path_enterCustomPath": "Введите маршрут вручную", - "path_currentPathLabel": "Текущий маршрут", - "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", - "path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)", - "path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)", - "path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)", - "path_selectFromContacts": "Или выберите из контактов:", - "path_noRepeatersFound": "Репитеры или серверы комнат не найдены.", - "path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.", - "path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}", - "path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.", - "path_setPath": "Установить маршрут", - "repeater_management": "Управление репитером", - "room_management": "Управление сервером комнат", - "repeater_managementTools": "Инструменты управления", - "repeater_status": "Статус", - "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера", - "repeater_telemetry": "Телеметрия", - "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", + "debugFrame_textTypePlain": "Обычный", + "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_hexDump": "Шестнадцатеричный дамп:", + "chat_pathManagement": "Управление маршрутами", + "chat_routingMode": "Режим маршрутизации", + "chat_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "chat_forceFloodMode": "Принудительный режим рассылки", + "chat_recentAckPaths": "Недавние подтверждённые маршруты (нажмите, чтобы использовать):", + "chat_pathHistoryFull": "История маршрутов заполнена. Удалите записи, чтобы добавить новые.", + "chat_hopSingular": "хоп", + "chat_hopPlural": "хопов", + "chat_hopsCount": "{count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", + "chat_successes": "успешно", + "chat_removePath": "Удалить маршрут", + "chat_noPathHistoryYet": "История маршрутов пока пуста.\nОтправьте сообщение, чтобы обнаружить маршруты.", + "chat_pathActions": "Действия с маршрутом:", + "chat_setCustomPath": "Указать маршрут вручную", + "chat_setCustomPathSubtitle": "Вручную задать маршрут передачи", + "chat_clearPath": "Очистить маршрут", + "chat_clearPathSubtitle": "Принудительно обновить маршрут при следующей отправке", + "chat_pathCleared": "Маршрут очищен. Следующее сообщение обновит маршрут.", + "chat_floodModeSubtitle": "Используйте переключатель маршрутизации в панели приложения", + "chat_floodModeEnabled": "Режим рассылки включён. Отключите через значок маршрутизации в панели приложения.", + "chat_fullPath": "Полный маршрут", + "chat_pathDetailsNotAvailable": "Детали маршрута ещё недоступны. Попробуйте отправить сообщение для обновления.", + "chat_pathSetHops": "Маршрут установлен: {hopCount} {hopCount, plural, one{хоп} few{хопа} many{хопов} other{хопов}} — {status}", + "chat_pathSavedLocally": "Сохранено локально. Подключитесь для синхронизации.", + "chat_pathDeviceConfirmed": "Подтверждено устройством.", + "chat_pathDeviceNotConfirmed": "Ещё не подтверждено устройством.", + "chat_type": "Тип", + "chat_path": "Маршрут", + "chat_publicKey": "Публичный ключ", + "chat_compressOutgoingMessages": "Сжимать исходящие сообщения", + "chat_floodForced": "Рассылка (принудительно)", + "chat_directForced": "Прямой (принудительно)", + "chat_hopsForced": "{count} хоп(ов) (принудительно)", + "chat_floodAuto": "Рассылка (авто)", + "chat_direct": "Прямой", + "chat_poiShared": "Точка интереса отправлена", + "chat_unread": "Непрочитанных: {count}", + "map_title": "Карта нод", + "map_noNodesWithLocation": "Нет нод с данными о местоположении", + "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте", + "map_nodesCount": "Нод: {count}", + "map_pinsCount": "Меток: {count}", + "map_chat": "Чат", + "map_repeater": "Репитер", + "map_room": "Комната", + "map_sensor": "Сенсор", + "map_pinDm": "Метка (ЛС)", + "map_pinPrivate": "Метка (Приватная)", + "map_pinPublic": "Метка (Публичная)", + "map_lastSeen": "Последнее появление", + "map_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", + "map_from": "От", + "map_source": "Источник", + "map_flags": "Флаги", + "map_shareMarkerHere": "Поделиться меткой здесь", + "map_pinLabel": "Метка", + "map_label": "Подпись", + "map_pointOfInterest": "Точка интереса", + "map_sendToContact": "Отправить контакту", + "map_sendToChannel": "Отправить в канал", + "map_noChannelsAvailable": "Нет доступных каналов", + "map_publicLocationShare": "Публичная передача местоположения", + "map_publicLocationShareConfirm": "Вы собираетесь поделиться местоположением в {channelLabel}. Этот канал публичный, и любой, у кого есть PSK, сможет его увидеть.", + "map_connectToShareMarkers": "Подключитесь к устройству, чтобы делиться метками", + "map_filterNodes": "Фильтр нод", + "map_nodeTypes": "Типы нод", + "map_chatNodes": "Чат-ноды", + "map_repeaters": "Репитеры", + "map_otherNodes": "Другие ноды", + "map_keyPrefix": "Префикс ключа", + "map_filterByKeyPrefix": "Фильтр по префиксу ключа", + "map_publicKeyPrefix": "Префикс публичного ключа", + "map_markers": "Метки", + "map_showSharedMarkers": "Показывать общие метки", + "map_lastSeenTime": "Время последнего появления", + "map_sharedPin": "Общая метка", + "map_joinRoom": "Присоединиться к комнате", + "map_manageRepeater": "Управление репитером", + "mapCache_title": "Кэш офлайн-карты", + "mapCache_selectAreaFirst": "Сначала выберите область для кэширования", + "mapCache_noTilesToDownload": "Нет плиток для загрузки в этой области", + "mapCache_downloadTilesTitle": "Загрузить плитки", + "mapCache_downloadTilesPrompt": "Загрузить {count} плиток для офлайн-использования?", + "mapCache_downloadAction": "Загрузить", + "mapCache_cachedTiles": "Закэшировано {count} плиток", + "mapCache_cachedTilesWithFailed": "Закэшировано {downloaded} плиток ({failed} не загружено)", + "mapCache_clearOfflineCacheTitle": "Очистить офлайн-кэш", + "mapCache_clearOfflineCachePrompt": "Удалить все закэшированные плитки карты?", + "mapCache_offlineCacheCleared": "Офлайн-кэш очищен", + "mapCache_noAreaSelected": "Область не выбрана", + "mapCache_cacheArea": "Область кэширования", + "mapCache_useCurrentView": "Использовать текущий вид", + "mapCache_zoomRange": "Диапазон масштаба", + "mapCache_estimatedTiles": "Оценочное количество плиток: {count}", + "mapCache_downloadedTiles": "Загружено {completed} из {total}", + "mapCache_downloadTilesButton": "Загрузить плитки", + "mapCache_clearCacheButton": "Очистить кэш", + "mapCache_failedDownloads": "Неудачных загрузок: {count}", + "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}", + "time_justNow": "Только что", + "time_minutesAgo": "{minutes} мин назад", + "time_hoursAgo": "{hours} ч назад", + "time_daysAgo": "{days} дн. назад", + "time_hour": "час", + "time_hours": "часов", + "time_day": "день", + "time_days": "дней", + "time_week": "неделя", + "time_weeks": "недель", + "time_month": "месяц", + "time_months": "месяцев", + "time_minutes": "минут", + "time_allTime": "Всё время", + "dialog_disconnect": "Отключиться", + "dialog_disconnectConfirm": "Вы уверены, что хотите отключиться от этого устройства?", + "login_repeaterLogin": "Вход в репитер", + "login_roomLogin": "Вход на сервер комнат", + "login_password": "Пароль", + "login_enterPassword": "Введите пароль", + "login_savePassword": "Сохранить пароль", + "login_savePasswordSubtitle": "Пароль будет надёжно сохранён на этом устройстве", + "login_repeaterDescription": "Введите пароль репитера для доступа к настройкам и статусу.", + "login_roomDescription": "Введите пароль комнаты для доступа к настройкам и статусу.", + "login_routing": "Маршрутизация", + "login_routingMode": "Режим маршрутизации", + "login_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "login_forceFloodMode": "Принудительный режим рассылки", + "login_managePaths": "Управление маршрутами", + "login_login": "Войти", + "login_attempt": "Попытка {current}/{max}", + "login_failed": "Ошибка входа: {error}", + "login_failedMessage": "Не удалось войти. Либо пароль неверен, либо репитер недоступен.", + "common_reload": "Обновить", + "common_clear": "Очистить", + "path_currentPath": "Текущий маршрут: {path}", + "path_usingHopsPath": "Используется маршрут из {count} {count, plural, one{хоп} few{хопа} many{хопов} other{хопов}}", + "path_enterCustomPath": "Введите маршрут вручную", + "path_currentPathLabel": "Текущий маршрут", + "path_hexPrefixInstructions": "Введите 2-символьные шестнадцатеричные префиксы для каждого хопа, разделённые запятыми.", + "path_hexPrefixExample": "Пример: A1,F2,3C (каждый узел использует первый байт своего публичного ключа)", + "path_labelHexPrefixes": "Маршрут (шестнадцатеричные префиксы)", + "path_helperMaxHops": "Максимум 64 хопа. Каждый префикс — 2 шестнадцатеричных символа (1 байт)", + "path_selectFromContacts": "Или выберите из контактов:", + "path_noRepeatersFound": "Репитеры или серверы комнат не найдены.", + "path_customPathsRequire": "Пользовательские маршруты требуют промежуточных узлов, способных ретранслировать сообщения.", + "path_invalidHexPrefixes": "Недопустимые шестнадцатеричные префиксы: {prefixes}", + "path_tooLong": "Маршрут слишком длинный. Максимум 64 хопа.", + "path_setPath": "Установить маршрут", + "repeater_management": "Управление репитером", + "room_management": "Управление сервером комнат", + "repeater_managementTools": "Инструменты управления", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Просмотр статуса, статистики и соседей репитера", + "repeater_telemetry": "Телеметрия", + "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Отправка команд репитеру", - "repeater_neighbors": "Соседи", - "repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.", - "repeater_settings": "Настройки", - "repeater_settingsSubtitle": "Настройка параметров репитера", - "repeater_statusTitle": "Статус репитера", - "repeater_routingMode": "Режим маршрутизации", - "repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", - "repeater_forceFloodMode": "Принудительный режим рассылки", - "repeater_pathManagement": "Управление маршрутами", - "repeater_refresh": "Обновить", - "repeater_statusRequestTimeout": "Время ожидания статуса истекло.", - "repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}", - "repeater_systemInformation": "Системная информация", - "repeater_battery": "Батарея", - "repeater_clockAtLogin": "Время (при входе)", - "repeater_uptime": "Время работы", - "repeater_queueLength": "Длина очереди", - "repeater_debugFlags": "Флаги отладки", - "repeater_radioStatistics": "Радиостатистика", - "repeater_lastRssi": "Последний RSSI", - "repeater_lastSnr": "Последний SNR", - "repeater_noiseFloor": "Уровень шума", - "repeater_txAirtime": "Время эфира (передача)", - "repeater_rxAirtime": "Время эфира (приём)", - "repeater_packetStatistics": "Статистика пакетов", - "repeater_sent": "Отправлено", - "repeater_received": "Получено", - "repeater_duplicates": "Дубликаты", - "repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с", - "repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", - "repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", - "repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}", - "repeater_duplicatesTotal": "Всего: {total}", - "repeater_settingsTitle": "Настройки репитера", - "repeater_basicSettings": "Основные настройки", - "repeater_repeaterName": "Имя репитера", - "repeater_repeaterNameHelper": "Отображаемое имя этого репитера", - "repeater_adminPassword": "Пароль администратора", - "repeater_adminPasswordHelper": "Пароль с полным доступом", - "repeater_guestPassword": "Гостевой пароль", - "repeater_guestPasswordHelper": "Пароль для доступа только для чтения", - "repeater_radioSettings": "Настройки радио", - "repeater_frequencyMhz": "Частота (МГц)", - "repeater_frequencyHelper": "300–2500 МГц", - "repeater_txPower": "Мощность передачи", - "repeater_txPowerHelper": "1–30 дБм", - "repeater_bandwidth": "Полоса пропускания", - "repeater_spreadingFactor": "Коэффициент расширения", - "repeater_codingRate": "Коэффициент кодирования", - "repeater_locationSettings": "Настройки местоположения", - "repeater_latitude": "Широта", - "repeater_latitudeHelper": "Ð’ десятичных градусах (напр., 37.7749)", - "repeater_longitude": "Долгота", - "repeater_longitudeHelper": "Ð’ десятичных градусах (напр., -122.4194)", - "repeater_features": "Функции", - "repeater_packetForwarding": "Пересылка пакетов", - "repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты", - "repeater_guestAccess": "Гостевой доступ", - "repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения", - "repeater_privacyMode": "Режим конфиденциальности", - "repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях", - "repeater_advertisementSettings": "Настройки анонсирования", - "repeater_localAdvertInterval": "Интервал локальных анонсирований", - "repeater_localAdvertIntervalMinutes": "{minutes} минут", - "repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)", - "repeater_floodAdvertIntervalHours": "{hours} часов", - "repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований", - "repeater_dangerZone": "Опасная зона", - "repeater_rebootRepeater": "Перезагрузить репитер", - "repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера", - "repeater_rebootRepeaterConfirm": "Ð’Ñ‹ уверены, что хотите перезагрузить этот репитер?", - "repeater_regenerateIdentityKey": "Пересоздать ключ идентификации", - "repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей", - "repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?", - "repeater_eraseFileSystem": "Стереть файловую систему", - "repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера", - "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!", - "repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.", - "repeater_commandSent": "Команда отправлена: {command}", - "repeater_errorSendingCommand": "Ошибка отправки команды: {error}", - "repeater_confirm": "Подтвердить", - "repeater_settingsSaved": "Настройки успешно сохранены", - "repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}", - "repeater_refreshBasicSettings": "Обновить основные настройки", - "repeater_refreshRadioSettings": "Обновить настройки радио", - "repeater_refreshTxPower": "Обновить мощность передачи", - "repeater_refreshLocationSettings": "Обновить настройки местоположения", - "repeater_refreshPacketForwarding": "Обновить пересылку пакетов", - "repeater_refreshGuestAccess": "Обновить гостевой доступ", - "repeater_refreshPrivacyMode": "Обновить режим конфиденциальности", - "repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований", - "repeater_refreshed": "{label} обновлён", - "repeater_errorRefreshing": "Ошибка обновления {label}", - "repeater_cliTitle": "CLI репитера", - "repeater_debugNextCommand": "Отладка следующей команды", - "repeater_commandHelp": "Справка по командам", - "repeater_clearHistory": "Очистить историю", - "repeater_noCommandsSent": "Команды ещё не отправлялись", - "repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды", - "repeater_enterCommandHint": "Введите команду...", - "repeater_previousCommand": "Предыдущая команда", - "repeater_nextCommand": "Следующая команда", - "repeater_enterCommandFirst": "Сначала введите команду", - "repeater_cliCommandFrameTitle": "Фрейм CLI-команды", - "repeater_cliCommandError": "Ошибка: {error}", - "repeater_cliQuickGetName": "Получить имя", - "repeater_cliQuickGetRadio": "Получить радио", - "repeater_cliQuickGetTx": "Получить TX", - "repeater_cliQuickNeighbors": "Соседи", - "repeater_cliQuickVersion": "Версия", - "repeater_cliQuickAdvertise": "Анонсировать", - "repeater_cliQuickClock": "Время", - "repeater_cliHelpAdvert": "Отправляет пакет анонсирования", - "repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)", - "repeater_cliHelpClock": "Показывает текущее время по часам устройства.", - "repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.", - "repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.", - "repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.", - "repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.", - "repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)", - "repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.", - "repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)", - "repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)", - "repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.", - "repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.", - "repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».", - "repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.", - "repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.", - "repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)", - "repeater_cliHelpSetName": "Устанавливает имя в оповещениях.", - "repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)", - "repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)", - "repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.", - "repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.", - "repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).", - "repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.", - "repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.", - "repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.", - "repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.", - "repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.", - "repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.", - "repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).", - "repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).", - "repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)", - "repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow", - "repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.", - "repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.", - "repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.", - "repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4", - "repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.", - "repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.", - "repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.", - "repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»", - "repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.", - "repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)", - "repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)", - "repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)", - "repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)", - "repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.", - "repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.", - "repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.", - "repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.", - "repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.", - "repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.", - "repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек", - "repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.", - "repeater_commandsListTitle": "Список команд", - "repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».", - "repeater_general": "Общие", - "repeater_settingsCategory": "Настройки", - "repeater_bridge": "Мост", - "repeater_logging": "Журналирование", - "repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)", - "repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)", - "repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.", - "repeater_gpsManagement": "Управление GPS", - "repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.", - "telemetry_receivedData": "Полученные телеметрические данные", - "telemetry_requestTimeout": "Время ожидания телеметрии истекло.", - "telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}", - "telemetry_noData": "Данные телеметрии недоступны.", - "telemetry_channelTitle": "Канал {channel}", - "telemetry_batteryLabel": "Батарея", - "telemetry_voltageLabel": "Напряжение", - "telemetry_mcuTemperatureLabel": "Температура МК", - "telemetry_temperatureLabel": "Температура", - "telemetry_currentLabel": "Ток", - "telemetry_batteryValue": "{percent}% / {volts}Ð’", - "telemetry_voltageValue": "{volts}Ð’", - "telemetry_currentValue": "{amps}А", - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", - "neighbors_receivedData": "Полученные данные о соседях", - "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", - "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", - "neighbors_repeatersNeighbors": "Соседи репитеров", - "neighbors_noData": "Данные о соседях недоступны.", - "neighbors_unknownContact": "Неизвестный {pubkey}", - "neighbors_heardA ago": "Слышали: {time} назад", - "channelPath_title": "Путь пакета", - "channelPath_viewMap": "Посмотреть на карте", - "channelPath_otherObservedPaths": "Другие наблюдаемые пути", - "channelPath_repeaterHops": "Хопы через репитеры", - "channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.", - "channelPath_messageDetails": "Детали сообщения", - "channelPath_senderLabel": "Отправитель", - "channelPath_timeLabel": "Время", - "channelPath_repeatsLabel": "Повторы", - "channelPath_pathLabel": "Путь {index}", - "channelPath_observedLabel": "Наблюдаемый", - "channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}", - "channelPath_noLocationData": "Нет данных о местоположении", + "repeater_cliSubtitle": "Отправка команд репитеру", + "repeater_neighbors": "Соседи", + "repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.", + "repeater_settings": "Настройки", + "repeater_settingsSubtitle": "Настройка параметров репитера", + "repeater_statusTitle": "Статус репитера", + "repeater_routingMode": "Режим маршрутизации", + "repeater_autoUseSavedPath": "Авто (использовать сохранённый маршрут)", + "repeater_forceFloodMode": "Принудительный режим рассылки", + "repeater_pathManagement": "Управление маршрутами", + "repeater_refresh": "Обновить", + "repeater_statusRequestTimeout": "Время ожидания статуса истекло.", + "repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}", + "repeater_systemInformation": "Системная информация", + "repeater_battery": "Батарея", + "repeater_clockAtLogin": "Время (при входе)", + "repeater_uptime": "Время работы", + "repeater_queueLength": "Длина очереди", + "repeater_debugFlags": "Флаги отладки", + "repeater_radioStatistics": "Радиостатистика", + "repeater_lastRssi": "Последний RSSI", + "repeater_lastSnr": "Последний SNR", + "repeater_noiseFloor": "Уровень шума", + "repeater_txAirtime": "Время эфира (передача)", + "repeater_rxAirtime": "Время эфира (приём)", + "repeater_packetStatistics": "Статистика пакетов", + "repeater_sent": "Отправлено", + "repeater_received": "Получено", + "repeater_duplicates": "Дубликаты", + "repeater_daysHoursMinsSecs": "{days} дн. {hours}ч {minutes}м {seconds}с", + "repeater_packetTxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_packetRxTotal": "Всего: {total}, Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesFloodDirect": "Рассылка: {flood}, Прямые: {direct}", + "repeater_duplicatesTotal": "Всего: {total}", + "repeater_settingsTitle": "Настройки репитера", + "repeater_basicSettings": "Основные настройки", + "repeater_repeaterName": "Имя репитера", + "repeater_repeaterNameHelper": "Отображаемое имя этого репитера", + "repeater_adminPassword": "Пароль администратора", + "repeater_adminPasswordHelper": "Пароль с полным доступом", + "repeater_guestPassword": "Гостевой пароль", + "repeater_guestPasswordHelper": "Пароль для доступа только для чтения", + "repeater_radioSettings": "Настройки радио", + "repeater_frequencyMhz": "Частота (МГц)", + "repeater_frequencyHelper": "300–2500 МГц", + "repeater_txPower": "Мощность передачи", + "repeater_txPowerHelper": "1–30 дБм", + "repeater_bandwidth": "Полоса пропускания", + "repeater_spreadingFactor": "Коэффициент расширения", + "repeater_codingRate": "Коэффициент кодирования", + "repeater_locationSettings": "Настройки местоположения", + "repeater_latitude": "Широта", + "repeater_latitudeHelper": "В десятичных градусах (напр., 37.7749)", + "repeater_longitude": "Долгота", + "repeater_longitudeHelper": "В десятичных градусах (напр., -122.4194)", + "repeater_features": "Функции", + "repeater_packetForwarding": "Пересылка пакетов", + "repeater_packetForwardingSubtitle": "Разрешить репитеру пересылать пакеты", + "repeater_guestAccess": "Гостевой доступ", + "repeater_guestAccessSubtitle": "Разрешить гостевой доступ только для чтения", + "repeater_privacyMode": "Режим конфиденциальности", + "repeater_privacyModeSubtitle": "Скрывать имя/местоположение в оповещениях", + "repeater_advertisementSettings": "Настройки анонсирования", + "repeater_localAdvertInterval": "Интервал локальных анонсирований", + "repeater_localAdvertIntervalMinutes": "{minutes} минут", + "repeater_floodAdvertInterval": "Интервал анонсирований рассылкой (flood)", + "repeater_floodAdvertIntervalHours": "{hours} часов", + "repeater_encryptedAdvertInterval": "Интервал зашифрованных анонсирований", + "repeater_dangerZone": "Опасная зона", + "repeater_rebootRepeater": "Перезагрузить репитер", + "repeater_rebootRepeaterSubtitle": "Перезапустить устройство репитера", + "repeater_rebootRepeaterConfirm": "Вы уверены, что хотите перезагрузить этот репитер?", + "repeater_regenerateIdentityKey": "Пересоздать ключ идентификации", + "repeater_regenerateIdentityKeySubtitle": "Сгенерировать новую пару публичного/приватного ключей", + "repeater_regenerateIdentityKeyConfirm": "Это создаст новую идентичность для репитера. Продолжить?", + "repeater_eraseFileSystem": "Стереть файловую систему", + "repeater_eraseFileSystemSubtitle": "Отформатировать файловую систему репитера", + "repeater_eraseFileSystemConfirm": "ВНИМАНИЕ: это удалит все данные на репитере. Действие нельзя отменить!", + "repeater_eraseSerialOnly": "Очистка доступна только через последовательную консоль.", + "repeater_commandSent": "Команда отправлена: {command}", + "repeater_errorSendingCommand": "Ошибка отправки команды: {error}", + "repeater_confirm": "Подтвердить", + "repeater_settingsSaved": "Настройки успешно сохранены", + "repeater_errorSavingSettings": "Ошибка сохранения настроек: {error}", + "repeater_refreshBasicSettings": "Обновить основные настройки", + "repeater_refreshRadioSettings": "Обновить настройки радио", + "repeater_refreshTxPower": "Обновить мощность передачи", + "repeater_refreshLocationSettings": "Обновить настройки местоположения", + "repeater_refreshPacketForwarding": "Обновить пересылку пакетов", + "repeater_refreshGuestAccess": "Обновить гостевой доступ", + "repeater_refreshPrivacyMode": "Обновить режим конфиденциальности", + "repeater_refreshAdvertisementSettings": "Обновить настройки анонсирований", + "repeater_refreshed": "{label} обновлён", + "repeater_errorRefreshing": "Ошибка обновления {label}", + "repeater_cliTitle": "CLI репитера", + "repeater_debugNextCommand": "Отладка следующей команды", + "repeater_commandHelp": "Справка по командам", + "repeater_clearHistory": "Очистить историю", + "repeater_noCommandsSent": "Команды ещё не отправлялись", + "repeater_typeCommandOrUseQuick": "Введите команду ниже или используйте быстрые команды", + "repeater_enterCommandHint": "Введите команду...", + "repeater_previousCommand": "Предыдущая команда", + "repeater_nextCommand": "Следующая команда", + "repeater_enterCommandFirst": "Сначала введите команду", + "repeater_cliCommandFrameTitle": "Фрейм CLI-команды", + "repeater_cliCommandError": "Ошибка: {error}", + "repeater_cliQuickGetName": "Получить имя", + "repeater_cliQuickGetRadio": "Получить радио", + "repeater_cliQuickGetTx": "Получить TX", + "repeater_cliQuickNeighbors": "Соседи", + "repeater_cliQuickVersion": "Версия", + "repeater_cliQuickAdvertise": "Анонсировать", + "repeater_cliQuickClock": "Время", + "repeater_cliHelpAdvert": "Отправляет пакет анонсирования", + "repeater_cliHelpReboot": "Перезагружает устройство. (обычно вы получите «Тайм-аут» — это нормально)", + "repeater_cliHelpClock": "Показывает текущее время по часам устройства.", + "repeater_cliHelpPassword": "Устанавливает новый пароль администратора для устройства.", + "repeater_cliHelpVersion": "Показывает версию устройства и дату сборки прошивки.", + "repeater_cliHelpClearStats": "Сбрасывает различные счётчики статистики в ноль.", + "repeater_cliHelpSetAf": "Устанавливает коэффициент времени в эфире.", + "repeater_cliHelpSetTx": "Устанавливает мощность передачи LoRa в дБм. (требуется перезагрузка)", + "repeater_cliHelpSetRepeat": "Включает или отключает роль репитера для этой ноды.", + "repeater_cliHelpSetAllowReadOnly": "(Сервер комнат) Если «on», то вход без пароля разрешён, но публиковать в комнату нельзя (только чтение)", + "repeater_cliHelpSetFloodMax": "Устанавливает максимальное число хопов для входящих пакетов в режиме рассылки (если >= макс., пакет не пересылается)", + "repeater_cliHelpSetIntThresh": "Устанавливает порог интерференции (в дБ). По умолчанию 14. Установите 0, чтобы отключить обнаружение помех.", + "repeater_cliHelpSetAgcResetInterval": "Устанавливает интервал сброса автоматической регулировки усиления. Установите 0, чтобы отключить.", + "repeater_cliHelpSetMultiAcks": "Включает или отключает функцию «двойных ACK».", + "repeater_cliHelpSetAdvertInterval": "Устанавливает интервал (в минутах) отправки локального (нулевой хоп) анонсирования. Установите 0, чтобы отключить.", + "repeater_cliHelpSetFloodAdvertInterval": "Устанавливает интервал (в часах) отправки анонсирований рассылкой. Установите 0, чтобы отключить.", + "repeater_cliHelpSetGuestPassword": "Устанавливает/обновляет гостевой пароль. (для репитеров гости могут отправлять запрос «Get Stats»)", + "repeater_cliHelpSetName": "Устанавливает имя в оповещениях.", + "repeater_cliHelpSetLat": "Устанавливает широту для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetLon": "Устанавливает долготу для карты в оповещениях. (десятичные градусы)", + "repeater_cliHelpSetRadio": "Устанавливает полностью новые параметры радио и сохраняет их в настройки. Требуется команда «reboot» для применения.", + "repeater_cliHelpSetRxDelay": "Устанавливает (экспериментально) базовую задержку (>1 для эффекта) для принятых пакетов на основе качества сигнала. Установите 0, чтобы отключить.", + "repeater_cliHelpSetTxDelay": "Устанавливает множитель времени в эфире для пакета в режиме рассылки и применяет случайную задержку перед пересылкой (чтобы уменьшить коллизии).", + "repeater_cliHelpSetDirectTxDelay": "То же, что txdelay, но для случайной задержки пересылки пакетов в прямом режиме.", + "repeater_cliHelpSetBridgeEnabled": "Включить/выключить мост.", + "repeater_cliHelpSetBridgeDelay": "Установить задержку перед ретрансляцией пакетов.", + "repeater_cliHelpSetBridgeSource": "Выбрать, будет ли мост ретранслировать полученные или отправленные пакеты.", + "repeater_cliHelpSetBridgeBaud": "Установить скорость последовательного соединения для мостов RS232.", + "repeater_cliHelpSetBridgeSecret": "Установить секрет моста для мостов ESP-NOW.", + "repeater_cliHelpSetAdcMultiplier": "Устанавливает пользовательский коэффициент коррекции напряжения батареи (поддерживается только на некоторых платах).", + "repeater_cliHelpTempRadio": "Устанавливает временные параметры радио на заданное число минут, затем возвращает исходные. (НЕ сохраняется в настройки).", + "repeater_cliHelpSetPerm": "Изменяет ACL. Удаляет запись (по префиксу публичного ключа), если «permissions» равен нулю. Добавляет новую запись, если указан полный ключ и он отсутствует в ACL. Обновляет запись по совпадению префикса. Биты прав зависят от роли прошивки, но младшие 2 бита: 0 (Гость), 1 (Только чтение), 2 (Чтение/запись), 3 (Админ)", + "repeater_cliHelpGetBridgeType": "Получает тип моста: none, rs232, espnow", + "repeater_cliHelpLogStart": "Начинает запись пакетов в файловую систему.", + "repeater_cliHelpLogStop": "Останавливает запись пакетов в файловую систему.", + "repeater_cliHelpLogErase": "Удаляет журналы пакетов из файловой системы.", + "repeater_cliHelpNeighbors": "Показывает список других репитеров, услышанных через оповещения нулевого хопа. Каждая строка: префикс-id-в-hex:временная-метка:snr×4", + "repeater_cliHelpNeighborRemove": "Удаляет первую подходящую запись (по префиксу публичного ключа в hex) из списка соседей.", + "repeater_cliHelpRegion": "(только через последовательный порт) Показывает все определённые регионы и текущие права на рассылку.", + "repeater_cliHelpRegionLoad": "ПРИМЕЧАНИЕ: это специальная многострочная команда. Каждая следующая строка — имя региона (с отступом пробелами для указания иерархии, минимум один пробел). Завершается пустой строкой.", + "repeater_cliHelpRegionGet": "Ищет регион по префиксу имени (или «*» для глобальной области). Отвечает: «-> имя-региона (родитель) 'F'»", + "repeater_cliHelpRegionPut": "Добавляет или обновляет определение региона с заданным именем.", + "repeater_cliHelpRegionRemove": "Удаляет определение региона с заданным именем. (должно точно совпадать и не иметь дочерних регионов)", + "repeater_cliHelpRegionAllowf": "Разрешает рассылку («F»lood) для заданного региона. («*» для глобальной/устаревшей области)", + "repeater_cliHelpRegionDenyf": "Запрещает рассылку («F»lood) для заданного региона. (НЕ рекомендуется для глобальной области!)", + "repeater_cliHelpRegionHome": "Показывает текущий «домашний» регион. (Пока не используется, зарезервировано на будущее)", + "repeater_cliHelpRegionHomeSet": "Устанавливает «домашний» регион.", + "repeater_cliHelpRegionSave": "Сохраняет список/карту регионов в память.", + "repeater_cliHelpGps": "Показывает статус GPS. Если GPS выключен — отвечает только «off». Если включён — показывает статус, фиксацию, количество спутников.", + "repeater_cliHelpGpsOnOff": "Переключает состояние питания GPS.", + "repeater_cliHelpGpsSync": "Синхронизирует время ноды с часами GPS.", + "repeater_cliHelpGpsSetLoc": "Устанавливает позицию ноды по координатам GPS и сохраняет в настройки.", + "repeater_cliHelpGpsAdvert": "Показывает конфигурацию передачи местоположения в анонсированиях:\n- none: не включать местоположение\n- share: передавать GPS-координаты (из SensorManager)\n- prefs: передавать координаты из настроек", + "repeater_cliHelpGpsAdvertSet": "Устанавливает конфигурацию передачи местоположения.", + "repeater_commandsListTitle": "Список команд", + "repeater_commandsListNote": "ПРИМЕЧАНИЕ: для большинства команд «set ...» существуют соответствующие команды «get ...».", + "repeater_general": "Общие", + "repeater_settingsCategory": "Настройки", + "repeater_bridge": "Мост", + "repeater_logging": "Журналирование", + "repeater_neighborsRepeaterOnly": "Соседи (только для репитеров)", + "repeater_regionManagementRepeaterOnly": "Управление регионами (только для репитеров)", + "repeater_regionNote": "Команды регионов введены для управления определениями регионов и правами доступа.", + "repeater_gpsManagement": "Управление GPS", + "repeater_gpsNote": "Команда gps введена для управления параметрами, связанными с местоположением.", + "telemetry_receivedData": "Полученные телеметрические данные", + "telemetry_requestTimeout": "Время ожидания телеметрии истекло.", + "telemetry_errorLoading": "Ошибка загрузки телеметрии: {error}", + "telemetry_noData": "Данные телеметрии недоступны.", + "telemetry_channelTitle": "Канал {channel}", + "telemetry_batteryLabel": "Батарея", + "telemetry_voltageLabel": "Напряжение", + "telemetry_mcuTemperatureLabel": "Температура МК", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Ток", + "telemetry_batteryValue": "{percent}% / {volts}В", + "telemetry_voltageValue": "{volts}В", + "telemetry_currentValue": "{amps}А", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "neighbors_receivedData": "Полученные данные о соседях", + "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", + "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", + "neighbors_repeatersNeighbors": "Соседи репитеров", + "neighbors_noData": "Данные о соседях недоступны.", + "neighbors_unknownContact": "Неизвестный {pubkey}", + "neighbors_heardA ago": "Слышали: {time} назад", + "channelPath_title": "Путь пакета", + "channelPath_viewMap": "Посмотреть на карте", + "channelPath_otherObservedPaths": "Другие наблюдаемые пути", + "channelPath_repeaterHops": "Хопы через репитеры", + "channelPath_noHopDetails": "Детали хопов для этого пакета не предоставлены.", + "channelPath_messageDetails": "Детали сообщения", + "channelPath_senderLabel": "Отправитель", + "channelPath_timeLabel": "Время", + "channelPath_repeatsLabel": "Повторы", + "channelPath_pathLabel": "Путь {index}", + "channelPath_observedLabel": "Наблюдаемый", + "channelPath_observedPathTitle": "Наблюдаемый путь {index} • {hops}", + "channelPath_noLocationData": "Нет данных о местоположении", "channelPath_timeWithDate": "{day}/{month} {time}", "channelPath_timeOnly": "{time}", - "channelPath_unknownPath": "Неизвестный", - "channelPath_floodPath": "Рассылка", - "channelPath_directPath": "Прямой", - "channelPath_observedZeroOf": "0 из {total} хопов", - "channelPath_observedSomeOf": "{observed} из {total} хопов", - "channelPath_mapTitle": "Карта пути", - "channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.", - "channelPath_primaryPath": "Путь {index} (Основной)", - "channelPath_pathLabelTitle": "Путь", - "channelPath_observedPathHeader": "Наблюдаемый путь", - "channelPath_selectedPathLabel": "{label} • {prefixes}", - "channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.", - "channelPath_unknownRepeater": "Неизвестный репитер", - "community_title": "Сообщество", - "community_create": "Создать сообщество", - "community_createDesc": "Создать новое сообщество и поделиться через QR-код.", - "community_join": "Присоединиться", - "community_joinTitle": "Присоединиться к сообществу", - "community_joinConfirmation": "Ð’Ñ‹ хотите присоединиться к сообществу \"{name}\"?", - "community_scanQr": "Сканировать QR-код сообщества", - "community_scanInstructions": "Наведите камеру на QR-код сообщества", - "community_showQr": "Показать QR-код", - "community_publicChannel": "Публичный канал сообщества", - "community_hashtagChannel": "Хэштег-канал сообщества", - "community_name": "Имя сообщества", - "community_enterName": "Введите имя сообщества", - "community_created": "Сообщество \"{name}\" создано", - "community_joined": "Присоединились к сообществу \"{name}\"", - "community_qrTitle": "Поделиться сообществом", - "community_qrInstructions": "Отсканируйте этот QR-код, чтобы присоединиться к \"{name}\"", - "community_hashtagPrivacyHint": "Хэштег-каналы сообщества доступны только его участникам", - "community_invalidQrCode": "Недопустимый QR-код сообщества", - "community_alreadyMember": "Уже участник", - "community_alreadyMemberMessage": "Ð’Ñ‹ уже участник сообщества \"{name}\".", - "community_addPublicChannel": "Добавить публичный канал сообщества", - "community_addPublicChannelHint": "Автоматически добавить публичный канал для этого сообщества", - "community_noCommunities": "Ð’Ñ‹ ещё не присоединились ни к одному сообществу", - "community_scanOrCreate": "Отсканируйте QR-код или создайте сообщество, чтобы начать", - "community_manageCommunities": "Управление сообществами", - "community_delete": "Покинуть сообщество", - "community_deleteConfirm": "Покинуть \"{name}\"?", - "community_deleteChannelsWarning": "Это также удалит {count} канал(ов) и их сообщения.", - "community_deleted": "Покинули сообщество \"{name}\"", - "community_regenerateSecret": "Пересоздать секрет", - "community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.", - "community_regenerate": "Пересоздать", - "community_secretRegenerated": "Секрет пересоздан для \"{name}\"", - "community_updateSecret": "Обновить секрет", - "community_secretUpdated": "Секрет обновлён для \"{name}\"", - "community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"", - "community_addHashtagChannel": "Добавить хэштег-канал сообщества", - "community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества", - "community_selectCommunity": "Выбрать сообщество", - "community_regularHashtag": "Обычный хэштег", - "community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)", - "community_communityHashtag": "Хэштег сообщества", - "community_communityHashtagDesc": "Доступен только участникам сообщества", - "community_forCommunity": "Для {name}", - "listFilter_tooltip": "Фильтр и сортировка", - "listFilter_sortBy": "Сортировка по", - "listFilter_latestMessages": "Последние сообщения", - "listFilter_heardRecently": "Слышали недавно", - "listFilter_az": "По алфавиту", - "listFilter_filters": "Фильтры", - "listFilter_all": "Все", - "listFilter_users": "Пользователи", - "listFilter_repeaters": "Репитеры", - "listFilter_roomServers": "Серверы комнат", - "listFilter_unreadOnly": "Только непрочитанные", - "listFilter_newGroup": "Новая группа", + "channelPath_unknownPath": "Неизвестный", + "channelPath_floodPath": "Рассылка", + "channelPath_directPath": "Прямой", + "channelPath_observedZeroOf": "0 из {total} хопов", + "channelPath_observedSomeOf": "{observed} из {total} хопов", + "channelPath_mapTitle": "Карта пути", + "channelPath_noRepeaterLocations": "Нет данных о местоположении репитеров для этого пути.", + "channelPath_primaryPath": "Путь {index} (Основной)", + "channelPath_pathLabelTitle": "Путь", + "channelPath_observedPathHeader": "Наблюдаемый путь", + "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_noHopDetailsAvailable": "Детали хопов для этого пакета недоступны.", + "channelPath_unknownRepeater": "Неизвестный репитер", + "community_title": "Сообщество", + "community_create": "Создать сообщество", + "community_createDesc": "Создать новое сообщество и поделиться через QR-код.", + "community_join": "Присоединиться", + "community_joinTitle": "Присоединиться к сообществу", + "community_joinConfirmation": "Вы хотите присоединиться к сообществу \"{name}\"?", + "community_scanQr": "Сканировать QR-код сообщества", + "community_scanInstructions": "Наведите камеру на QR-код сообщества", + "community_showQr": "Показать QR-код", + "community_publicChannel": "Публичный канал сообщества", + "community_hashtagChannel": "Хэштег-канал сообщества", + "community_name": "Имя сообщества", + "community_enterName": "Введите имя сообщества", + "community_created": "Сообщество \"{name}\" создано", + "community_joined": "Присоединились к сообществу \"{name}\"", + "community_qrTitle": "Поделиться сообществом", + "community_qrInstructions": "Отсканируйте этот QR-код, чтобы присоединиться к \"{name}\"", + "community_hashtagPrivacyHint": "Хэштег-каналы сообщества доступны только его участникам", + "community_invalidQrCode": "Недопустимый QR-код сообщества", + "community_alreadyMember": "Уже участник", + "community_alreadyMemberMessage": "Вы уже участник сообщества \"{name}\".", + "community_addPublicChannel": "Добавить публичный канал сообщества", + "community_addPublicChannelHint": "Автоматически добавить публичный канал для этого сообщества", + "community_noCommunities": "Вы ещё не присоединились ни к одному сообществу", + "community_scanOrCreate": "Отсканируйте QR-код или создайте сообщество, чтобы начать", + "community_manageCommunities": "Управление сообществами", + "community_delete": "Покинуть сообщество", + "community_deleteConfirm": "Покинуть \"{name}\"?", + "community_deleteChannelsWarning": "Это также удалит {count} канал(ов) и их сообщения.", + "community_deleted": "Покинули сообщество \"{name}\"", + "community_regenerateSecret": "Пересоздать секрет", + "community_regenerateSecretConfirm": "Пересоздать секретный ключ для \"{name}\"? Все участники должны будут отсканировать новый QR-код для продолжения общения.", + "community_regenerate": "Пересоздать", + "community_secretRegenerated": "Секрет пересоздан для \"{name}\"", + "community_updateSecret": "Обновить секрет", + "community_secretUpdated": "Секрет обновлён для \"{name}\"", + "community_scanToUpdateSecret": "Отсканируйте новый QR-код, чтобы обновить секрет для \"{name}\"", + "community_addHashtagChannel": "Добавить хэштег-канал сообщества", + "community_addHashtagChannelDesc": "Добавить хэштег-канал для этого сообщества", + "community_selectCommunity": "Выбрать сообщество", + "community_regularHashtag": "Обычный хэштег", + "community_regularHashtagDesc": "Публичный хэштег (любой может присоединиться)", + "community_communityHashtag": "Хэштег сообщества", + "community_communityHashtagDesc": "Доступен только участникам сообщества", + "community_forCommunity": "Для {name}", + "listFilter_tooltip": "Фильтр и сортировка", + "listFilter_sortBy": "Сортировка по", + "listFilter_latestMessages": "Последние сообщения", + "listFilter_heardRecently": "Слышали недавно", + "listFilter_az": "По алфавиту", + "listFilter_filters": "Фильтры", + "listFilter_all": "Все", + "listFilter_users": "Пользователи", + "listFilter_repeaters": "Репитеры", + "listFilter_roomServers": "Серверы комнат", + "listFilter_unreadOnly": "Только непрочитанные", + "listFilter_newGroup": "Новая группа", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -774,12 +774,12 @@ } } }, - "chat_open": "Открыть", - "chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}", - "chat_openLink": "Открыть ссылку?", - "chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?", - "neighbors_heardAgo": "Слушал(а): {time} назад", - "chat_invalidLink": "Неправильный формат ссылки", + "chat_open": "Открыть", + "chat_couldNotOpenLink": "Не удалось открыть ссылку: {url}", + "chat_openLink": "Открыть ссылку?", + "chat_openLinkConfirmation": "Хотите открыть эту ссылку в вашем браузере?", + "neighbors_heardAgo": "Слушал(а): {time} назад", + "chat_invalidLink": "Неправильный формат ссылки", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -787,81 +787,81 @@ } } }, - "pathTrace_you": "Ð’Ñ‹", - "pathTrace_failed": "Путь трассировки не выполнен.", - "pathTrace_notAvailable": "Трассировка пути недоступна.", - "pathTrace_refreshTooltip": "Обновить Path Trace", - "contacts_pathTrace": "Трассировка пути", - "contacts_ping": "Пинговать", - "contacts_repeaterPathTrace": "Отследить путь к ретранслятору", - "contacts_repeaterPing": "Пинговать повторитель", - "contacts_roomPathTrace": "Трассировка пути к серверу комнаты", - "contacts_roomPing": "Пинговать сервер комнаты", - "contacts_chatTraceRoute": "Трассировка маршрута", - "contacts_pathTraceTo": "Показать маршрут к {name}", - "contacts_contactImported": "Контакт был импортирован", - "contacts_contactImportFailed": "Контакт не удалось импортировать", - "contacts_invalidAdvertFormat": "Недействительные контактные данные", - "contacts_zeroHopAdvert": "Реклама Zero Hop", - "appSettings_languageUk": "Українська", - "appSettings_enableMessageTracing": "Включить трассировку сообщений", - "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", - "contacts_floodAdvert": "Рекламный поток", - "contacts_clipboardEmpty": "Буфер обмена пуст.", - "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", - "contacts_ShareContact": "Копировать контакт в буфер обмена", - "contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.", - "contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.", - "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", - "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", - "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", - "notification_activityTitle": "Активность MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}", - "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", - "notification_newTypeDiscovered": "Обнаружен новый {contactType}", - "notification_receivedNewMessage": "Получено новое сообщение", - "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", - "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", - "settings_gpxExportContacts": "Экспортировать спутников в GPX", - "settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС", - "settings_gpxExportError": "Произошла ошибка при экспорте.", - "settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат", - "settings_gpxExportChat": "Местоположения спутников", - "settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.", - "settings_gpxExportAll": "Экспортировать все контакты в GPX", - "settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.", - "settings_gpxExportAllContacts": "Все местоположения контактов", - "settings_gpxExportSuccess": "Успешно экспортирован файл GPX.", - "settings_gpxExportNoContacts": "Нет контактов для экспорта.", - "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", - "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!", - "map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.", - "map_removeLast": "Удалить последний", - "map_pathTraceCancelled": "Отмена трассировки пути", - "pathTrace_clearTooltip": "Очистить путь", - "map_runTrace": "Запустить трассировку пути", - "scanner_enableBluetooth": "Включите Bluetooth", - "scanner_bluetoothOff": "Bluetooth выключен", - "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", - "scanner_chromeRequired": "Требуется браузер Chrome", - "scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.", - "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", - "snrIndicator_lastSeen": "Последний раз видели", - "chat_ShowAllPaths": "Показать все пути", - "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", - "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", - "settings_clientRepeat": "Повторение \"вне сети\"", - "settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "Единицы", - "appSettings_unitsMetric": "Метрическая (м/км)", - "appSettings_unitsImperial": "Имперская (ft / mi)", - "map_lineOfSight": "Линия видимости", - "map_losScreenTitle": "Линия видимости", - "losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.", - "losRunFailed": "Проверка прямой видимости не удалась: {error}", + "pathTrace_you": "Вы", + "pathTrace_failed": "Путь трассировки не выполнен.", + "pathTrace_notAvailable": "Трассировка пути недоступна.", + "pathTrace_refreshTooltip": "Обновить Path Trace", + "contacts_pathTrace": "Трассировка пути", + "contacts_ping": "Пинговать", + "contacts_repeaterPathTrace": "Отследить путь к ретранслятору", + "contacts_repeaterPing": "Пинговать повторитель", + "contacts_roomPathTrace": "Трассировка пути к серверу комнаты", + "contacts_roomPing": "Пинговать сервер комнаты", + "contacts_chatTraceRoute": "Трассировка маршрута", + "contacts_pathTraceTo": "Показать маршрут к {name}", + "contacts_contactImported": "Контакт был импортирован", + "contacts_contactImportFailed": "Контакт не удалось импортировать", + "contacts_invalidAdvertFormat": "Недействительные контактные данные", + "contacts_zeroHopAdvert": "Реклама Zero Hop", + "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Включить трассировку сообщений", + "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", + "contacts_floodAdvert": "Рекламный поток", + "contacts_clipboardEmpty": "Буфер обмена пуст.", + "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", + "contacts_ShareContact": "Копировать контакт в буфер обмена", + "contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.", + "contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.", + "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", + "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", + "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", + "notification_activityTitle": "Активность MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{сообщение} few{сообщения} many{сообщений} other{сообщений}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{сообщение канала} few{сообщения канала} many{сообщений канала} other{сообщений канала}}", + "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", + "notification_newTypeDiscovered": "Обнаружен новый {contactType}", + "notification_receivedNewMessage": "Получено новое сообщение", + "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", + "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", + "settings_gpxExportContacts": "Экспортировать спутников в GPX", + "settings_gpxExportNotAvailable": "Не поддерживается на вашем устройстве/ОС", + "settings_gpxExportError": "Произошла ошибка при экспорте.", + "settings_gpxExportRepeatersRoom": "Местоположения повторителей и серверов комнат", + "settings_gpxExportChat": "Местоположения спутников", + "settings_gpxExportContactsSubtitle": "Экспортирует спутников с местоположением в файл GPX.", + "settings_gpxExportAll": "Экспортировать все контакты в GPX", + "settings_gpxExportAllSubtitle": "Экспортирует все контакты с местоположением в файл GPX.", + "settings_gpxExportAllContacts": "Все местоположения контактов", + "settings_gpxExportSuccess": "Успешно экспортирован файл GPX.", + "settings_gpxExportNoContacts": "Нет контактов для экспорта.", + "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", + "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!", + "map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.", + "map_removeLast": "Удалить последний", + "map_pathTraceCancelled": "Отмена трассировки пути", + "pathTrace_clearTooltip": "Очистить путь", + "map_runTrace": "Запустить трассировку пути", + "scanner_enableBluetooth": "Включите Bluetooth", + "scanner_bluetoothOff": "Bluetooth выключен", + "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "scanner_chromeRequired": "Требуется браузер Chrome", + "scanner_chromeRequiredMessage": "Для поддержки Bluetooth в этом веб-приложении требуется Google Chrome или браузер на базе Chromium.", + "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", + "snrIndicator_lastSeen": "Последний раз видели", + "chat_ShowAllPaths": "Показать все пути", + "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", + "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", + "settings_clientRepeat": "Повторение \"вне сети\"", + "settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Единицы", + "appSettings_unitsMetric": "Метрическая (м/км)", + "appSettings_unitsImperial": "Имперская (ft / mi)", + "map_lineOfSight": "Линия видимости", + "map_losScreenTitle": "Линия видимости", + "losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.", + "losRunFailed": "Проверка прямой видимости не удалась: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -869,13 +869,13 @@ } } }, - "losClearAllPoints": "Очистить все точки", - "losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.", - "losMenuTitle": "ЛОС Меню", - "losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.", - "losShowDisplayNodes": "Показать узлы отображения", - "losCustomPoints": "Пользовательские точки", - "losCustomPointLabel": "Пользовательский {index}", + "losClearAllPoints": "Очистить все точки", + "losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.", + "losMenuTitle": "ЛОС Меню", + "losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.", + "losShowDisplayNodes": "Показать узлы отображения", + "losCustomPoints": "Пользовательские точки", + "losCustomPointLabel": "Пользовательский {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -883,9 +883,9 @@ } } }, - "losPointA": "Точка А", - "losPointB": "Точка Б", - "losAntennaA": "Антенна А: {value} {unit}", + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антенна А: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -896,7 +896,7 @@ } } }, - "losAntennaB": "Антенна Б: {value} {unit}", + "losAntennaB": "Антенна Б: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -907,9 +907,9 @@ } } }, - "losRun": "Запустить ЛОС", - "losNoElevationData": "Нет данных о высоте", - "losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}", + "losRun": "Запустить ЛОС", + "losNoElevationData": "Нет данных о высоте", + "losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -926,7 +926,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -943,9 +943,9 @@ } } }, - "losStatusChecking": "ЛОС: проверяю...", - "losStatusNoData": "ЛОС: нет данных", - "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.", + "losStatusChecking": "ЛОС: проверяю...", + "losStatusNoData": "ЛОС: нет данных", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.", "@losStatusSummary": { "placeholders": { "clear": { @@ -962,20 +962,20 @@ } } }, - "losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.", - "losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.", - "losRenameCustomPoint": "Переименовать пользовательскую точку", - "losPointName": "Имя точки", - "losShowPanelTooltip": "Показать панель LOS", - "losHidePanelTooltip": "Скрыть панель LOS", - "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Радиогоризонт", - "losLegendLosBeam": "Линия прямой видимости", - "losLegendTerrain": "Рельеф", - "losFrequencyLabel": "Частота", - "losFrequencyInfoTooltip": "Просмотреть детали расчёта", - "losFrequencyDialogTitle": "Расчёт радиогоризонта", - "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", + "losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.", + "losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.", + "losRenameCustomPoint": "Переименовать пользовательскую точку", + "losPointName": "Имя точки", + "losShowPanelTooltip": "Показать панель LOS", + "losHidePanelTooltip": "Скрыть панель LOS", + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиогоризонт", + "losLegendLosBeam": "Линия прямой видимости", + "losLegendTerrain": "Рельеф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Просмотреть детали расчёта", + "losFrequencyDialogTitle": "Расчёт радиогоризонта", + "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -993,9 +993,9 @@ } } }, - "listFilter_addToFavorites": "Добавить в избранное", - "listFilter_favorites": "Избранное", - "listFilter_removeFromFavorites": "Удалить из избранного", + "listFilter_addToFavorites": "Добавить в избранное", + "listFilter_favorites": "Избранное", + "listFilter_removeFromFavorites": "Удалить из избранного", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1036,17 +1036,29 @@ } } }, - "contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...", - "contacts_searchContactsNoNumber": "Поиск контактов...", - "contacts_unread": "Непрочитанное", - "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", - "contacts_searchFavorites": "Поиск {number}{str} избранного...", - "contacts_searchUsers": "Поиск {number}{str} пользователей...", - "connectionChoiceUsbLabel": "USB", + "contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...", + "contacts_searchContactsNoNumber": "Поиск контактов...", + "contacts_unread": "Непрочитанное", + "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", + "contacts_searchFavorites": "Поиск {number}{str} избранного...", + "contacts_searchUsers": "Поиск {number}{str} пользователей...", + "usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.", + "usbScreenTitle": "Подключение через USB", + "usbScreenStatus": "Выберите USB-устройство", + "usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.", + "usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.", + "usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.", + "usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.", + "usbErrorInvalidPort": "Выберите действительное USB-устройство.", + "usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.", + "usbErrorNotConnected": "Ни одно устройство USB не подключено.", + "usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.", + "usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.", + "usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.", + "usbErrorAlreadyActive": "USB-соединение уже установлено и работает.", + "usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.", + "usbErrorPortClosed": "USB-соединение не установлено.", + "usbErrorConnectTimedOut": "Ожидание ответа от устройства превысило установленное время.", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.", - "usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.", - "usbScreenStatus": "Выберите USB-устройство", - "usbScreenTitle": "Подключение через USB", - "usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список." + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 183f2fc..1269fe1 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániÅ¥", +{ + "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -10,32 +10,32 @@ "@@locale": "sk", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", - "nav_channels": "Kanály", + "nav_channels": "Kanály", "nav_map": "Mapa", - "common_cancel": "ZruÅ¡iÅ¥", - "common_connect": "PripojiÅ¥", - "common_unknownDevice": "Neznáme zariadenie", - "common_save": "UložiÅ¥", - "common_delete": "OdstrániÅ¥", - "common_close": "ZavrieÅ¥", - "common_edit": "UpraviÅ¥", - "common_add": "PridaÅ¥", + "common_cancel": "Zrušiť", + "common_connect": "Pripojiť", + "common_unknownDevice": "Neznáme zariadenie", + "common_save": "Uložiť", + "common_delete": "Odstrániť", + "common_close": "Zavrieť", + "common_edit": "Upraviť", + "common_add": "Pridať", "common_settings": "Nastavenia", - "common_disconnect": "OdpojiÅ¥", - "common_connected": "Pripojené", - "common_disconnected": "Odpojené", - "common_create": "VytvoriÅ¥", - "common_continue": "PokračovaÅ¥", - "common_share": "ZdieľaÅ¥", - "common_copy": "KopírovaÅ¥", - "common_retry": "PokusÅ¥ znova", - "common_hide": "SkryÅ¥", - "common_remove": "OdstrániÅ¥", + "common_disconnect": "Odpojiť", + "common_connected": "Pripojené", + "common_disconnected": "Odpojené", + "common_create": "Vytvoriť", + "common_continue": "Pokračovať", + "common_share": "Zdieľať", + "common_copy": "Kopírovať", + "common_retry": "Pokusť znova", + "common_hide": "Skryť", + "common_remove": "Odstrániť", "common_enable": "Povolit", - "common_disable": "ZakázaÅ¥", - "common_reboot": "RestartovaÅ¥", - "common_loading": "Načítavanie...", - "common_notAvailable": "—", + "common_disable": "Zakázať", + "common_reboot": "Restartovať", + "common_loading": "Načítavanie...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Skrívania zariadení...", + "scanner_scanning": "Skrívania zariadení...", "scanner_connecting": "Pripojujem sa...", "scanner_disconnecting": "Odpojuje sa...", - "scanner_notConnected": "Nezriadené", - "scanner_connectedTo": "Pripojené k {deviceName}", + "scanner_notConnected": "Nezriadené", + "scanner_connectedTo": "Pripojené k {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,8 +65,8 @@ } } }, - "scanner_searchingDevices": "Hľadám zariadenia MeshCore...", - "scanner_tapToScan": "Stlač skenovanie na nájdenie zariadení MeshCore.", + "scanner_searchingDevices": "Hľadám zariadenia MeshCore...", + "scanner_tapToScan": "Stlač skenovanie na nájdenie zariadení MeshCore.", "scanner_connectionFailed": "Pripojenie zlyhalo: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -76,51 +76,51 @@ } }, "scanner_stop": "Zastavte", - "scanner_scan": "SkončiÅ¥", - "device_quickSwitch": "Rýchle prepínač", + "scanner_scan": "Skončiť", + "device_quickSwitch": "Rýchle prepínač", "device_meshcore": "MeshCore", "settings_title": "Nastavenia", - "settings_deviceInfo": "Informácie o zariadení", - "settings_appSettings": "Nastavenia aplikácie", - "settings_appSettingsSubtitle": "Upozornenia, správy a nastavenia mapy", + "settings_deviceInfo": "Informácie o zariadení", + "settings_appSettings": "Nastavenia aplikácie", + "settings_appSettingsSubtitle": "Upozornenia, správy a nastavenia mapy", "settings_nodeSettings": "Nastavenia uzla", - "settings_nodeName": "Názov uzla", - "settings_nodeNameNotSet": "Nezriadené", - "settings_nodeNameHint": "Zadajte názov uzla", - "settings_nodeNameUpdated": "Meno aktualizované", - "settings_radioSettings": "Nastavenia rádia", - "settings_radioSettingsSubtitle": "Frekvencia, výkon, rozptylovací faktor", - "settings_radioSettingsUpdated": "Nastavenia rádia aktualizované", + "settings_nodeName": "Názov uzla", + "settings_nodeNameNotSet": "Nezriadené", + "settings_nodeNameHint": "Zadajte názov uzla", + "settings_nodeNameUpdated": "Meno aktualizované", + "settings_radioSettings": "Nastavenia rádia", + "settings_radioSettingsSubtitle": "Frekvencia, výkon, rozptylovací faktor", + "settings_radioSettingsUpdated": "Nastavenia rádia aktualizované", "settings_location": "Lokalita", - "settings_locationSubtitle": "GPS súradnice", - "settings_locationUpdated": "Lokalita aktualizovaná", - "settings_locationBothRequired": "Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.", - "settings_locationInvalid": "Neplatná šírka alebo dĺžka.", - "settings_latitude": "Súradnica", - "settings_longitude": "Dĺžka", - "settings_privacyMode": "Režim ochrany súkromia", - "settings_privacyModeSubtitle": "SkryÅ¥ meno/poloha v reklamách", - "settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaÅ¡e meno a polohu v reklamách.", - "settings_privacyModeEnabled": "Ochranný režim je povolený.", - "settings_privacyModeDisabled": "Ochranný režim je vypnutý", - "settings_actions": "Možné akcie", - "settings_sendAdvertisement": "OdoslaÅ¥ reklamu", - "settings_sendAdvertisementSubtitle": "Momentálne priezornejÅ¡ie.", - "settings_advertisementSent": "Reklama odeslaná", - "settings_syncTime": "ÄŒas synchronizácie", - "settings_syncTimeSubtitle": "NastaviÅ¥ hodiny zariadenia na čas telefónu", - "settings_timeSynchronized": "ÄŒas synchronizovaný", - "settings_refreshContacts": "NačítaÅ¥ Kontakty", - "settings_refreshContactsSubtitle": "NačítaÅ¥ zoznam kontaktov z zariadenia", - "settings_rebootDevice": "RestartovaÅ¥ zariadenie", + "settings_locationSubtitle": "GPS súradnice", + "settings_locationUpdated": "Lokalita aktualizovaná", + "settings_locationBothRequired": "Zadajte obidve zložky zemyslenia a zložky meracieho kruhu.", + "settings_locationInvalid": "Neplatná šírka alebo dĺžka.", + "settings_latitude": "Súradnica", + "settings_longitude": "Dĺžka", + "settings_privacyMode": "Režim ochrany súkromia", + "settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách", + "settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.", + "settings_privacyModeEnabled": "Ochranný režim je povolený.", + "settings_privacyModeDisabled": "Ochranný režim je vypnutý", + "settings_actions": "Možné akcie", + "settings_sendAdvertisement": "Odoslať reklamu", + "settings_sendAdvertisementSubtitle": "Momentálne priezornejšie.", + "settings_advertisementSent": "Reklama odeslaná", + "settings_syncTime": "Čas synchronizácie", + "settings_syncTimeSubtitle": "Nastaviť hodiny zariadenia na čas telefónu", + "settings_timeSynchronized": "Čas synchronizovaný", + "settings_refreshContacts": "Načítať Kontakty", + "settings_refreshContactsSubtitle": "Načítať zoznam kontaktov z zariadenia", + "settings_rebootDevice": "Restartovať zariadenie", "settings_rebootDeviceSubtitle": "Restartujte zariadenie MeshCore.", - "settings_rebootDeviceConfirm": "Ste si istý, že chcete zariadenie reÅ¡tartovaÅ¥? Budete odpojení.", + "settings_rebootDeviceConfirm": "Ste si istý, že chcete zariadenie reštartovať? Budete odpojení.", "settings_debug": "Ladenie", "settings_bleDebugLog": "Log BLE Debug", - "settings_bleDebugLogSubtitle": "Príkazy BLE, odpovede a surové dáta", - "settings_appDebugLog": "Záznam ladenia aplikácie", - "settings_appDebugLogSubtitle": "Správy z ladenia aplikácie", - "settings_about": "O nás", + "settings_bleDebugLogSubtitle": "Príkazy BLE, odpovede a surové dáta", + "settings_appDebugLog": "Záznam ladenia aplikácie", + "settings_appDebugLogSubtitle": "Správy z ladenia aplikácie", + "settings_about": "O nás", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "MeshCore Open Source Projekt 2024", - "settings_aboutDescription": "Otvorený zdrojový Flutter klient pre MeshCore LoRa sieÅ¥ové zariadenia.", + "settings_aboutDescription": "Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.", "settings_infoName": "Meno", "settings_infoId": "ID", "settings_infoStatus": "Status", - "settings_infoBattery": "Batéria", - "settings_infoPublicKey": "Verejný kľúč", - "settings_infoContactsCount": "Počet kontaktov", - "settings_infoChannelCount": "Počet kanálov", + "settings_infoBattery": "Batéria", + "settings_infoPublicKey": "Verejný kľúč", + "settings_infoContactsCount": "Počet kontaktov", + "settings_infoChannelCount": "Počet kanálov", "settings_presets": "Prednastavenia", "settings_frequency": "Frekvencia (MHz)", - "settings_frequencyHelper": "300,0 – 2500,0", - "settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)", - "settings_bandwidth": "Šírka pásma", - "settings_spreadingFactor": "Rozptýľovací faktor", - "settings_codingRate": "Cenový kurz pre programovanie", - "settings_txPower": "TX Výkon (dBm)", + "settings_frequencyHelper": "300,0 – 2500,0", + "settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)", + "settings_bandwidth": "Šírka pásma", + "settings_spreadingFactor": "Rozptýľovací faktor", + "settings_codingRate": "Cenový kurz pre programovanie", + "settings_txPower": "TX Výkon (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)", + "settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)", "settings_error": "Chyba: {message}", "@settings_error": { "placeholders": { @@ -156,50 +156,50 @@ } } }, - "appSettings_title": "Nastavenia aplikácie", - "appSettings_appearance": "Vzhľad", - "appSettings_theme": "Téma", - "appSettings_themeSystem": "Predvolený systém", + "appSettings_title": "Nastavenia aplikácie", + "appSettings_appearance": "Vzhľad", + "appSettings_theme": "Téma", + "appSettings_themeSystem": "Predvolený systém", "appSettings_themeLight": "Svetlo", - "appSettings_themeDark": "Tmavé", + "appSettings_themeDark": "Tmavé", "appSettings_language": "Jazyk", - "appSettings_languageSystem": "Predvolený systém", + "appSettings_languageSystem": "Predvolený systém", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Upozornenia", - "appSettings_enableNotifications": "Povolte Notifikácie", - "appSettings_enableNotificationsSubtitle": "ZísÅ¥ o upozornenia na správy a inzeráty", - "appSettings_notificationPermissionDenied": "Odmietená povolenie notifikácií", - "appSettings_notificationsEnabled": "Upozornenia povolené", - "appSettings_notificationsDisabled": "Upozornenia sú vypnuté", - "appSettings_messageNotifications": "Správy od upozornení", - "appSettings_messageNotificationsSubtitle": "ZobraziÅ¥ upozornenie pri prijímaní nových správ", - "appSettings_channelMessageNotifications": "Notifikácie z kanálov", - "appSettings_channelMessageNotificationsSubtitle": "ZobraziÅ¥ upozornenie pri prijímaní správ z kanálu", + "appSettings_enableNotifications": "Povolte Notifikácie", + "appSettings_enableNotificationsSubtitle": "Zísť o upozornenia na správy a inzeráty", + "appSettings_notificationPermissionDenied": "Odmietená povolenie notifikácií", + "appSettings_notificationsEnabled": "Upozornenia povolené", + "appSettings_notificationsDisabled": "Upozornenia sú vypnuté", + "appSettings_messageNotifications": "Správy od upozornení", + "appSettings_messageNotificationsSubtitle": "Zobraziť upozornenie pri prijímaní nových správ", + "appSettings_channelMessageNotifications": "Notifikácie z kanálov", + "appSettings_channelMessageNotificationsSubtitle": "Zobraziť upozornenie pri prijímaní správ z kanálu", "appSettings_advertisementNotifications": "Upozornenia na reklamy", - "appSettings_advertisementNotificationsSubtitle": "ZobraziÅ¥ upozornenie, keď sa objavia nové uzly.", - "appSettings_messaging": "Správy", - "appSettings_clearPathOnMaxRetry": "Vyčisti cestu na Max Retry", - "appSettings_clearPathOnMaxRetrySubtitle": "ResetovaÅ¥ kontaktný priebeh po 5 neúspeÅ¡ných pokusoch o doručenie", - "appSettings_pathsWillBeCleared": "Cesty budú vymazané po 5 neúspeÅ¡ných pokusoch.", - "appSettings_pathsWillNotBeCleared": "Cesty sa automaticky nevymazávajú.", - "appSettings_autoRouteRotation": "Automatické prechodové trasy", - "appSettings_autoRouteRotationSubtitle": "Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.", - "appSettings_autoRouteRotationEnabled": "Automatické otáčanie trasy povolené", - "appSettings_autoRouteRotationDisabled": "Automatické prekladanie trás pozastavené", - "appSettings_battery": "Batéria", - "appSettings_batteryChemistry": "Chemická zloženie batérie", + "appSettings_advertisementNotificationsSubtitle": "Zobraziť upozornenie, keď sa objavia nové uzly.", + "appSettings_messaging": "Správy", + "appSettings_clearPathOnMaxRetry": "Vyčisti cestu na Max Retry", + "appSettings_clearPathOnMaxRetrySubtitle": "Resetovať kontaktný priebeh po 5 neúspešných pokusoch o doručenie", + "appSettings_pathsWillBeCleared": "Cesty budú vymazané po 5 neúspešných pokusoch.", + "appSettings_pathsWillNotBeCleared": "Cesty sa automaticky nevymazávajú.", + "appSettings_autoRouteRotation": "Automatické prechodové trasy", + "appSettings_autoRouteRotationSubtitle": "Striedajte sa medzi najlepšími trasami a režimom povodňovej analýzy.", + "appSettings_autoRouteRotationEnabled": "Automatické otáčanie trasy povolené", + "appSettings_autoRouteRotationDisabled": "Automatické prekladanie trás pozastavené", + "appSettings_battery": "Batéria", + "appSettings_batteryChemistry": "Chemická zloženie batérie", "appSettings_batteryChemistryPerDevice": "Nastavenie pre {deviceName}", "@appSettings_batteryChemistryPerDevice": { "placeholders": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Pripojte sa k zariadeniu na výber", + "appSettings_batteryChemistryConnectFirst": "Pripojte sa k zariadeniu na výber", "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Zobrazenie mapy", - "appSettings_showRepeaters": "ZobraziÅ¥ opakovače", - "appSettings_showRepeatersSubtitle": "ZobraziÅ¥ opakujúce sa uzly na mape", - "appSettings_showChatNodes": "ZobraziÅ¥ uzly chatových správ", - "appSettings_showChatNodesSubtitle": "ZobraziÅ¥ chatové uzly na mape", - "appSettings_showOtherNodes": "ZobraziÅ¥ ďalÅ¡ie uzly", - "appSettings_showOtherNodesSubtitle": "ZobraziÅ¥ ostatné typy uzlov na mape", - "appSettings_timeFilter": "Filtrovacie ÄŒasové Obdoby", - "appSettings_timeFilterShowAll": "ZobraziÅ¥ vÅ¡etky uzly", - "appSettings_timeFilterShowLast": "ZobraziÅ¥ uzly z posledných {hours} hodín", + "appSettings_showRepeaters": "Zobraziť opakovače", + "appSettings_showRepeatersSubtitle": "Zobraziť opakujúce sa uzly na mape", + "appSettings_showChatNodes": "Zobraziť uzly chatových správ", + "appSettings_showChatNodesSubtitle": "Zobraziť chatové uzly na mape", + "appSettings_showOtherNodes": "Zobraziť ďalšie uzly", + "appSettings_showOtherNodesSubtitle": "Zobraziť ostatné typy uzlov na mape", + "appSettings_timeFilter": "Filtrovacie Časové Obdoby", + "appSettings_timeFilterShowAll": "Zobraziť všetky uzly", + "appSettings_timeFilterShowLast": "Zobraziť uzly z posledných {hours} hodín", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -229,16 +229,16 @@ } } }, - "appSettings_mapTimeFilter": "Filtračný čas mapy", - "appSettings_showNodesDiscoveredWithin": "ZobraziÅ¥ uzly objavené v:", - "appSettings_allTime": "VÅ¡etky časy", - "appSettings_lastHour": "Posledná hodina", - "appSettings_last6Hours": "Posledné 6 hodín", - "appSettings_last24Hours": "Posledných 24 hodín", - "appSettings_lastWeek": "Minul týždeň", - "appSettings_offlineMapCache": "Offline Mapa Pamäť", - "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasÅ¥", - "appSettings_areaSelectedZoom": "Vyberená oblasÅ¥ (zoom {minZoom}-{maxZoom})", + "appSettings_mapTimeFilter": "Filtračný čas mapy", + "appSettings_showNodesDiscoveredWithin": "Zobraziť uzly objavené v:", + "appSettings_allTime": "Všetky časy", + "appSettings_lastHour": "Posledná hodina", + "appSettings_last6Hours": "Posledné 6 hodín", + "appSettings_last24Hours": "Posledných 24 hodín", + "appSettings_lastWeek": "Minul týždeň", + "appSettings_offlineMapCache": "Offline Mapa Pamäť", + "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť", + "appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,18 +250,18 @@ } }, "appSettings_debugCard": "Ladenie", - "appSettings_appDebugLogging": "Záznamy ladenia aplikácie", - "appSettings_appDebugLoggingSubtitle": "Logovací správy aplikácie pre ladenie", - "appSettings_appDebugLoggingEnabled": "Aplikácia povolila ladenie protokolmi", - "appSettings_appDebugLoggingDisabled": "Zabudované ladenie aplikácie je vypnuté.", + "appSettings_appDebugLogging": "Záznamy ladenia aplikácie", + "appSettings_appDebugLoggingSubtitle": "Logovací správy aplikácie pre ladenie", + "appSettings_appDebugLoggingEnabled": "Aplikácia povolila ladenie protokolmi", + "appSettings_appDebugLoggingDisabled": "Zabudované ladenie aplikácie je vypnuté.", "contacts_title": "Kontakty", - "contacts_noContacts": "Zatiaľ žiadne kontakty.", - "contacts_contactsWillAppear": "Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.", - "contacts_searchContacts": "Vyhľadávajte kontakty...", - "contacts_noUnreadContacts": "Žiadne neprečítané kontakty", - "contacts_noContactsFound": "Neboli nájdených žiadnych kontaktov ani skupiny.", - "contacts_deleteContact": "OdstrániÅ¥ kontakt", - "contacts_removeConfirm": "OdstrániÅ¥ {contactName} z kontaktov?", + "contacts_noContacts": "Zatiaľ žiadne kontakty.", + "contacts_contactsWillAppear": "Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.", + "contacts_searchContacts": "Vyhľadávajte kontakty...", + "contacts_noUnreadContacts": "Žiadne neprečítané kontakty", + "contacts_noContactsFound": "Neboli nájdených žiadnych kontaktov ani skupiny.", + "contacts_deleteContact": "Odstrániť kontakt", + "contacts_removeConfirm": "Odstrániť {contactName} z kontaktov?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -269,12 +269,12 @@ } } }, - "contacts_manageRepeater": "SpravovaÅ¥ opakované zoznamy", - "contacts_roomLogin": "Prihlásenie do miestnosti", - "contacts_openChat": "Otvorené Chat", - "contacts_editGroup": "UpraviÅ¥ skupinu", - "contacts_deleteGroup": "Vymažť skupinu", - "contacts_deleteGroupConfirm": "OdstrániÅ¥ \"{groupName}\"?", + "contacts_manageRepeater": "Spravovať opakované zoznamy", + "contacts_roomLogin": "Prihlásenie do miestnosti", + "contacts_openChat": "Otvorené Chat", + "contacts_editGroup": "Upraviť skupinu", + "contacts_deleteGroup": "Vymažť skupinu", + "contacts_deleteGroupConfirm": "Odstrániť \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -282,10 +282,10 @@ } } }, - "contacts_newGroup": "Nová skupina", - "contacts_groupName": "Názov skupiny", - "contacts_groupNameRequired": "Skupina musí maÅ¥ názov.", - "contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje", + "contacts_newGroup": "Nová skupina", + "contacts_groupName": "Názov skupiny", + "contacts_groupNameRequired": "Skupina musí mať názov.", + "contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -293,11 +293,11 @@ } } }, - "contacts_filterContacts": "FiltrovaÅ¥ kontakty...", - "contacts_noContactsMatchFilter": "Žiadne kontakty neodídu vášmu filtru.", - "contacts_noMembers": "Žiadni členovia", - "contacts_lastSeenNow": "Posledné zreteľné zobrazenie teraz", - "contacts_lastSeenMinsAgo": "Posledné zobrazenie {minutes} min. dozadu", + "contacts_filterContacts": "Filtrovať kontakty...", + "contacts_noContactsMatchFilter": "Žiadne kontakty neodídu vášmu filtru.", + "contacts_noMembers": "Žiadni členovia", + "contacts_lastSeenNow": "Posledné zreteľné zobrazenie teraz", + "contacts_lastSeenMinsAgo": "Posledné zobrazenie {minutes} min. dozadu", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Zobral/Zabral poslednýkrát pred hodinou.", - "contacts_lastSeenHoursAgo": "Posledné zobrazenie {hours} hodín dozadu", + "contacts_lastSeenHourAgo": "Zobral/Zabral poslednýkrát pred hodinou.", + "contacts_lastSeenHoursAgo": "Posledné zobrazenie {hours} hodín dozadu", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Zobral/Zabral posledný raz pred 1 dňom.", - "contacts_lastSeenDaysAgo": "Posledné zobrazenie {days} dní dozadu", + "contacts_lastSeenDayAgo": "Zobral/Zabral posledný raz pred 1 dňom.", + "contacts_lastSeenDaysAgo": "Posledné zobrazenie {days} dní dozadu", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -323,12 +323,12 @@ } } }, - "channels_title": "Kanály", - "channels_noChannelsConfigured": "Neobsiahnuté žiadne kanály", - "channels_addPublicChannel": "PridaÅ¥ verejný kanál", - "channels_searchChannels": "Vyhľadávajte kanály...", - "channels_noChannelsFound": "Neobsiahlo sa žiadnych kanálov.", - "channels_channelIndex": "Kanál {index}", + "channels_title": "Kanály", + "channels_noChannelsConfigured": "Neobsiahnuté žiadne kanály", + "channels_addPublicChannel": "Pridať verejný kanál", + "channels_searchChannels": "Vyhľadávajte kanály...", + "channels_noChannelsFound": "Neobsiahlo sa žiadnych kanálov.", + "channels_channelIndex": "Kanál {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -336,16 +336,16 @@ } } }, - "channels_hashtagChannel": "Kanál s hashtagom", - "channels_public": "Veľké verejné", - "channels_private": "Osobné", - "channels_publicChannel": "Veľké verejne kanály", - "channels_privateChannel": "Osobné kanál", - "channels_editChannel": "UpraviÅ¥ kanál", - "channels_muteChannel": "StlmiÅ¥ kanál", - "channels_unmuteChannel": "ZruÅ¡iÅ¥ stlmenie kanála", - "channels_deleteChannel": "OdstrániÅ¥ kanál", - "channels_deleteChannelConfirm": "OdstrániÅ¥ \"{name}\"? To sa nedá zruÅ¡iÅ¥.", + "channels_hashtagChannel": "Kanál s hashtagom", + "channels_public": "Veľké verejné", + "channels_private": "Osobné", + "channels_publicChannel": "Veľké verejne kanály", + "channels_privateChannel": "Osobné kanál", + "channels_editChannel": "Upraviť kanál", + "channels_muteChannel": "Stlmiť kanál", + "channels_unmuteChannel": "Zrušiť stlmenie kanála", + "channels_deleteChannel": "Odstrániť kanál", + "channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -353,7 +353,7 @@ } } }, - "channels_channelDeleted": "Kanál \"{name}\" bol odstránený", + "channels_channelDeleted": "Kanál \"{name}\" bol odstránený", "@channels_channelDeleted": { "placeholders": { "name": { @@ -361,16 +361,16 @@ } } }, - "channels_addChannel": "PridaÅ¥ kanál", - "channels_channelIndexLabel": "Index kanála", - "channels_channelName": "Názov kanálu", - "channels_usePublicChannel": "Použite verejný kanál", - "channels_standardPublicPsk": "Å tandardný verejný PSK", - "channels_pskHex": "PSK (Å ifrovacia kľúčik)", - "channels_generateRandomPsk": "GenerovaÅ¥ náhodný PSK", - "channels_enterChannelName": "Prosím, zadajte názov kanála.", - "channels_pskMustBe32Hex": "PSK musí maÅ¥ 32 hexadecimálových znakov.", - "channels_channelAdded": "Kanál \"{name}\" pridaný", + "channels_addChannel": "Pridať kanál", + "channels_channelIndexLabel": "Index kanála", + "channels_channelName": "Názov kanálu", + "channels_usePublicChannel": "Použite verejný kanál", + "channels_standardPublicPsk": "Štandardný verejný PSK", + "channels_pskHex": "PSK (Šifrovacia kľúčik)", + "channels_generateRandomPsk": "Generovať náhodný PSK", + "channels_enterChannelName": "Prosím, zadajte názov kanála.", + "channels_pskMustBe32Hex": "PSK musí mať 32 hexadecimálových znakov.", + "channels_channelAdded": "Kanál \"{name}\" pridaný", "@channels_channelAdded": { "placeholders": { "name": { @@ -378,7 +378,7 @@ } } }, - "channels_editChannelTitle": "UpraviÅ¥ kanál {index}", + "channels_editChannelTitle": "Upraviť kanál {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -386,8 +386,8 @@ } } }, - "channels_smazCompression": "Odstránenie kompresie SMAZ", - "channels_channelUpdated": "Kanál \"{name}\" bol aktualizovaný", + "channels_smazCompression": "Odstránenie kompresie SMAZ", + "channels_channelUpdated": "Kanál \"{name}\" bol aktualizovaný", "@channels_channelUpdated": { "placeholders": { "name": { @@ -395,16 +395,16 @@ } } }, - "channels_publicChannelAdded": "Veľký kanál pridaný", - "channels_sortBy": "TriediÅ¥ podľa", - "channels_sortManual": "Ručne", + "channels_publicChannelAdded": "Veľký kanál pridaný", + "channels_sortBy": "Triediť podľa", + "channels_sortManual": "Ručne", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "Posledné správy", - "channels_sortUnread": "Nezriadené", - "chat_noMessages": "Zatiaľ žiadne správy.", - "chat_sendMessageToStart": "PoÅ¡lite správu na začiatok", - "chat_originalMessageNotFound": "Neznámy pôvodný odkaz.", - "chat_replyingTo": "Odpovedám {name}", + "channels_sortLatestMessages": "Posledné správy", + "channels_sortUnread": "Nezriadené", + "chat_noMessages": "Zatiaľ žiadne správy.", + "chat_sendMessageToStart": "Pošlite správu na začiatok", + "chat_originalMessageNotFound": "Neznámy pôvodný odkaz.", + "chat_replyingTo": "Odpovedám {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "OdpovedaÅ¥ {name}", + "chat_replyTo": "Odpovedať {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,7 +421,7 @@ } }, "chat_location": "Lokalita", - "chat_sendMessageTo": "PoÅ¡li správu {contactName}", + "chat_sendMessageTo": "Pošli správu {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "NapiÅ¡te správu...", - "chat_messageTooLong": "Správa je príliÅ¡ dlhá (max {maxBytes} bytov).", + "chat_typeMessage": "Napište správu...", + "chat_messageTooLong": "Správa je príliš dlhá (max {maxBytes} bytov).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,10 +438,10 @@ } } }, - "chat_messageCopied": "Správa skopírovaná", - "chat_messageDeleted": "Posolstvo odstránené", + "chat_messageCopied": "Správa skopírovaná", + "chat_messageDeleted": "Posolstvo odstránené", "chat_retryingMessage": "Pokus o obnovenie", - "chat_retryCount": "SkúsiÅ¥ {current}/{max}", + "chat_retryCount": "Skúsiť {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -452,33 +452,33 @@ } } }, - "chat_sendGif": "OdoslaÅ¥ GIF", - "chat_reply": "OdpovedaÅ¥", - "chat_addReaction": "PridaÅ¥ Reakciu", + "chat_sendGif": "Odoslať GIF", + "chat_reply": "Odpovedať", + "chat_addReaction": "Pridať Reakciu", "chat_me": "Mne", "emojiCategorySmileys": "Emoji", - "emojiCategoryGestures": "Gestá", + "emojiCategoryGestures": "Gestá", "emojiCategoryHearts": "Srdcia", "emojiCategoryObjects": "Objekty", "gifPicker_title": "Vyberte GIF", - "gifPicker_searchHint": "Vyhľadávajte GIFy...", - "gifPicker_poweredBy": "Napájané spoločnosÅ¥ou GIPHY", - "gifPicker_noGifsFound": "Neboli nájdené žiadne GIFy.", - "gifPicker_failedLoad": "Nepodarilo sa načítaÅ¥ GIFy", - "gifPicker_failedSearch": "Nepodarilo sa vyhľadaÅ¥ GIFy", - "gifPicker_noInternet": "Žiadna internetová konektivita", - "debugLog_appTitle": "Záznam ladenia aplikácie", + "gifPicker_searchHint": "Vyhľadávajte GIFy...", + "gifPicker_poweredBy": "Napájané spoločnosťou GIPHY", + "gifPicker_noGifsFound": "Neboli nájdené žiadne GIFy.", + "gifPicker_failedLoad": "Nepodarilo sa načítať GIFy", + "gifPicker_failedSearch": "Nepodarilo sa vyhľadať GIFy", + "gifPicker_noInternet": "Žiadna internetová konektivita", + "debugLog_appTitle": "Záznam ladenia aplikácie", "debugLog_bleTitle": "Log BLE Debug", - "debugLog_copyLog": "KopírovaÅ¥ záznam", - "debugLog_clearLog": "VymažaÅ¥ záznam", - "debugLog_copied": "Záznam ladenia skopírovaný", - "debugLog_bleCopied": "Kopírovaný záznam z BLE.", - "debugLog_noEntries": "Zatiaľ neboli zaznamenané žiadne debug logy.", - "debugLog_enableInSettings": "Povolte ladicové logy v nastaveniach", - "debugLog_frames": "Rámce", + "debugLog_copyLog": "Kopírovať záznam", + "debugLog_clearLog": "Vymažať záznam", + "debugLog_copied": "Záznam ladenia skopírovaný", + "debugLog_bleCopied": "Kopírovaný záznam z BLE.", + "debugLog_noEntries": "Zatiaľ neboli zaznamenané žiadne debug logy.", + "debugLog_enableInSettings": "Povolte ladicové logy v nastaveniach", + "debugLog_frames": "Rámce", "debugLog_rawLogRx": "Raw Log-RX", - "debugLog_noBleActivity": "Zatiaľ žiadna aktivita BLE.", - "debugFrame_length": "Dĺžka rámca: {count} bajtov", + "debugLog_noBleActivity": "Zatiaľ žiadna aktivita BLE.", + "debugFrame_length": "Dĺžka rámca: {count} bajtov", "@debugFrame_length": { "placeholders": { "count": { @@ -486,7 +486,7 @@ } } }, - "debugFrame_command": "PrikázÌŒ: 0x{value}", + "debugFrame_command": "Prikáž: 0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -494,8 +494,8 @@ } } }, - "debugFrame_textMessageHeader": "Textová zvesÅ¥:", - "debugFrame_destinationPubKey": "- Cieľový PubKey: {pubKey}", + "debugFrame_textMessageHeader": "Textová zvesť:", + "debugFrame_destinationPubKey": "- Cieľový PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- ÄŒasové označenie: {timestamp}", + "debugFrame_timestamp": "- Časové označenie: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -511,7 +511,7 @@ } } }, - "debugFrame_flags": "- Žiadne vlajky: 0x{value}", + "debugFrame_flags": "- Žiadne vlajky: 0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -531,7 +531,7 @@ } }, "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Jednoduché", + "debugFrame_textTypePlain": "Jednoduché", "debugFrame_text": "- Text: \"{text}\"", "@debugFrame_text": { "placeholders": { @@ -541,14 +541,14 @@ } }, "debugFrame_hexDump": "Hex Dump:", - "chat_pathManagement": "Správa ciest", - "chat_routingMode": "Režim trasy", - "chat_autoUseSavedPath": "PoužiÅ¥ uloženú cestu", - "chat_forceFloodMode": "ZavrieÅ¥ režim núdzového povodňového režimu", - "chat_recentAckPaths": "Nedávne cesty ACK (klepni na použitie):", - "chat_pathHistoryFull": "História ciest je plná. Odstráňte záznamy, aby ste mohli pridaÅ¥ nové.", + "chat_pathManagement": "Správa ciest", + "chat_routingMode": "Režim trasy", + "chat_autoUseSavedPath": "Použiť uloženú cestu", + "chat_forceFloodMode": "Zavrieť režim núdzového povodňového režimu", + "chat_recentAckPaths": "Nedávne cesty ACK (klepni na použitie):", + "chat_pathHistoryFull": "História ciest je plná. Odstráňte záznamy, aby ste mohli pridať nové.", "chat_hopSingular": "Skok", - "chat_hopPlural": "SkákaÅ¥", + "chat_hopPlural": "Skákať", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { @@ -557,20 +557,20 @@ } } }, - "chat_successes": "Úspechy", - "chat_removePath": "OdstrániÅ¥ cestu", - "chat_noPathHistoryYet": "Zatiaľ žiadna história trás.\nPoÅ¡lite správu a objavte trasy.", + "chat_successes": "Úspechy", + "chat_removePath": "Odstrániť cestu", + "chat_noPathHistoryYet": "Zatiaľ žiadna história trás.\nPošlite správu a objavte trasy.", "chat_pathActions": "Cesty:", - "chat_setCustomPath": "NastaviÅ¥ vlastnú cestu", - "chat_setCustomPathSubtitle": "Ručne zadajte trasu.", - "chat_clearPath": "VyčistiÅ¡ cestu", - "chat_clearPathSubtitle": "Znovu nájsÅ¥ vynútene pri nasledujúcej poÅ¡lite", - "chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.", - "chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.", - "chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.", - "chat_fullPath": "Celá cesta", - "chat_pathDetailsNotAvailable": "Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslaÅ¥ správu na obnovenie.", - "chat_pathSetHops": "Cesta nastavená: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_setCustomPath": "Nastaviť vlastnú cestu", + "chat_setCustomPathSubtitle": "Ručne zadajte trasu.", + "chat_clearPath": "Vyčistiš cestu", + "chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite", + "chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.", + "chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.", + "chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.", + "chat_fullPath": "Celá cesta", + "chat_pathDetailsNotAvailable": "Podrobnosti o ceste zatiaľ dostupné nie sú. Skúste poslať správu na obnovenie.", + "chat_pathSetHops": "Cesta nastavená: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,16 +581,16 @@ } } }, - "chat_pathSavedLocally": "Uložené lokálne. Spojte sa na synchronizáciu.", - "chat_pathDeviceConfirmed": "Zariadenie potvrdené.", - "chat_pathDeviceNotConfirmed": "Zariadenie zatiaľ nebolo potvrdené.", - "chat_type": "NapiÅ¡te", + "chat_pathSavedLocally": "Uložené lokálne. Spojte sa na synchronizáciu.", + "chat_pathDeviceConfirmed": "Zariadenie potvrdené.", + "chat_pathDeviceNotConfirmed": "Zariadenie zatiaľ nebolo potvrdené.", + "chat_type": "Napište", "chat_path": "Cesta", - "chat_publicKey": "Verejný kľúč", - "chat_compressOutgoingMessages": "KomprimovaÅ¥ odoslané správy", - "chat_floodForced": "Povodňová (nutená)", - "chat_directForced": "Priame (donútené)", - "chat_hopsForced": "{count} skokov (nutené)", + "chat_publicKey": "Verejný kľúč", + "chat_compressOutgoingMessages": "Komprimovať odoslané správy", + "chat_floodForced": "Povodňová (nutená)", + "chat_directForced": "Priame (donútené)", + "chat_hopsForced": "{count} skokov (nutené)", "@chat_hopsForced": { "placeholders": { "count": { @@ -600,8 +600,8 @@ }, "chat_floodAuto": "Povod (automaticky)", "chat_direct": "Priamo", - "chat_poiShared": "Zdieľané body záujmu", - "chat_unread": "Nezriadené: {count}", + "chat_poiShared": "Zdieľané body záujmu", + "chat_unread": "Nezriadené: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "OtvoriÅ¥ odkaz?", - "chat_openLinkConfirmation": "Chcete otvoriÅ¥ tento odkaz v prehliadači?", - "chat_open": "OtvoriÅ¥", - "chat_couldNotOpenLink": "Nepodarilo sa otvoriÅ¥ odkaz: {url}", + "chat_openLink": "Otvoriť odkaz?", + "chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?", + "chat_open": "Otvoriť", + "chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,10 +620,10 @@ } } }, - "chat_invalidLink": "Neplatný formát odkazu", + "chat_invalidLink": "Neplatný formát odkazu", "map_title": "Mapa uzlov", - "map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe", - "map_nodesNeedGps": "Uholníky musia zdieľaÅ¥ svoje GPS súradnice, aby sa zobrazili na mape.", + "map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe", + "map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.", "map_nodesCount": "Uzly: {count}", "@map_nodesCount": { "placeholders": { @@ -632,7 +632,7 @@ } } }, - "map_pinsCount": "Krúžky: {count}", + "map_pinsCount": "Krúžky: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -645,22 +645,22 @@ "map_room": "Izba", "map_sensor": "Senzor", "map_pinDm": "Zabudka (DM)", - "map_pinPrivate": "Zabudka (Osobná)", - "map_pinPublic": "ZablokovaÅ¥ (verejne)", - "map_lastSeen": "Posledné zreteľné zobrazenie", - "map_disconnectConfirm": "Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?", + "map_pinPrivate": "Zabudka (Osobná)", + "map_pinPublic": "Zablokovať (verejne)", + "map_lastSeen": "Posledné zreteľné zobrazenie", + "map_disconnectConfirm": "Ste si istý/á, že chcete odpojiť od tohto zariadenia?", "map_from": "Od", "map_source": "Zdroj", - "map_flags": "Zástavy", - "map_shareMarkerHere": "Zdieľte značku tu", - "map_pinLabel": "Označka upozornenia", - "map_label": "Značka", - "map_pointOfInterest": "Bod záujmu", - "map_sendToContact": "PoÅ¡leÅ¥ na kontakt", - "map_sendToChannel": "PoslaÅ¥ do kanálu", - "map_noChannelsAvailable": "Неexistujú žiadne kanály.", - "map_publicLocationShare": "ZdieľiÅ¥ verejnú lokalitu", - "map_publicLocationShareConfirm": "ÄŒoskoro budete zdieľaÅ¥ polohu v {channelLabel}. Tento kanál je verejný a môže ho vidieÅ¥ každý s PSK.", + "map_flags": "Zástavy", + "map_shareMarkerHere": "Zdieľte značku tu", + "map_pinLabel": "Označka upozornenia", + "map_label": "Značka", + "map_pointOfInterest": "Bod záujmu", + "map_sendToContact": "Pošleť na kontakt", + "map_sendToChannel": "Poslať do kanálu", + "map_noChannelsAvailable": "Неexistujú žiadne kanály.", + "map_publicLocationShare": "Zdieľiť verejnú lokalitu", + "map_publicLocationShareConfirm": "Čoskoro budete zdieľať polohu v {channelLabel}. Tento kanál je verejný a môže ho vidieť každý s PSK.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Pripojte sa k zariadeniu na zdieľanie značiek", - "map_filterNodes": "FiltrovaÅ¥ uzly", + "map_connectToShareMarkers": "Pripojte sa k zariadeniu na zdieľanie značiek", + "map_filterNodes": "Filtrovať uzly", "map_nodeTypes": "Typy uzlov", - "map_chatNodes": "Chatové uzly", - "map_repeaters": "Opakovadlá", - "map_otherNodes": "Ostatné uzly", - "map_keyPrefix": "Päťciferné predpona", - "map_filterByKeyPrefix": "FiltrovaÅ¥ podľa predponového kľúča", - "map_publicKeyPrefix": "Prefix verejného kľúča", - "map_markers": "Označkovače", - "map_showSharedMarkers": "ZobraziÅ¥ zdieľané značky", - "map_lastSeenTime": "Posledný čas sledovania", - "map_sharedPin": "Zdieľaný PIN", - "map_joinRoom": "PripojiÅ¥ miestnosÅ¥", - "map_manageRepeater": "SpravovaÅ¥ Opakovanie", - "mapCache_title": "Offline Mapa Pamäť", - "mapCache_selectAreaFirst": "Vyberte si oblasÅ¥ na predprerúčenie.", - "mapCache_noTilesToDownload": "Žiadne dlaždice na stiahnutie pre toto zóna", - "mapCache_downloadTilesTitle": "StiahnuÅ¥ dlaždice", - "mapCache_downloadTilesPrompt": "StiahnuÅ¥ {count} dlaždíc na offline použitie?", + "map_chatNodes": "Chatové uzly", + "map_repeaters": "Opakovadlá", + "map_otherNodes": "Ostatné uzly", + "map_keyPrefix": "Päťciferné predpona", + "map_filterByKeyPrefix": "Filtrovať podľa predponového kľúča", + "map_publicKeyPrefix": "Prefix verejného kľúča", + "map_markers": "Označkovače", + "map_showSharedMarkers": "Zobraziť zdieľané značky", + "map_lastSeenTime": "Posledný čas sledovania", + "map_sharedPin": "Zdieľaný PIN", + "map_joinRoom": "Pripojiť miestnosť", + "map_manageRepeater": "Spravovať Opakovanie", + "mapCache_title": "Offline Mapa Pamäť", + "mapCache_selectAreaFirst": "Vyberte si oblasť na predprerúčenie.", + "mapCache_noTilesToDownload": "Žiadne dlaždice na stiahnutie pre toto zóna", + "mapCache_downloadTilesTitle": "Stiahnuť dlaždice", + "mapCache_downloadTilesPrompt": "Stiahnuť {count} dlaždíc na offline použitie?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,8 +695,8 @@ } } }, - "mapCache_downloadAction": "StiahnuÅ¥", - "mapCache_cachedTiles": "Zabudené {count} dlaždíc", + "mapCache_downloadAction": "Stiahnuť", + "mapCache_cachedTiles": "Zabudené {count} dlaždíc", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Uložené {downloaded} dlaždice ({failed} neúspeÅ¡né)", + "mapCache_cachedTilesWithFailed": "Uložené {downloaded} dlaždice ({failed} neúspešné)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "VymazaÅ¥ offline uloženie", - "mapCache_clearOfflineCachePrompt": "OdstrániÅ¥ vÅ¡etky uložené mapové dlaždice?", - "mapCache_offlineCacheCleared": "Offline polia vymazaná", - "mapCache_noAreaSelected": "Neoznačila sa žiadna oblasÅ¥", - "mapCache_cacheArea": "Obdĺžková oblasÅ¥", - "mapCache_useCurrentView": "Použite aktuálny zobrazenie", - "mapCache_zoomRange": "Rozsah zväčšenia", - "mapCache_estimatedTiles": "Odhadnuté dlaždice: {count}", + "mapCache_clearOfflineCacheTitle": "Vymazať offline uloženie", + "mapCache_clearOfflineCachePrompt": "Odstrániť všetky uložené mapové dlaždice?", + "mapCache_offlineCacheCleared": "Offline polia vymazaná", + "mapCache_noAreaSelected": "Neoznačila sa žiadna oblasť", + "mapCache_cacheArea": "Obdĺžková oblasť", + "mapCache_useCurrentView": "Použite aktuálny zobrazenie", + "mapCache_zoomRange": "Rozsah zväčšenia", + "mapCache_estimatedTiles": "Odhadnuté dlaždice: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Stiahnuté {completed} / {total}", + "mapCache_downloadedTiles": "Stiahnuté {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "StiahnuÅ¥ dlaždice", - "mapCache_clearCacheButton": "VyprázdniÅ¥ VädsÅ¥", - "mapCache_failedDownloads": "NeúspeÅ¡né stiahnutia: {count}", + "mapCache_downloadTilesButton": "Stiahnuť dlaždice", + "mapCache_clearCacheButton": "Vyprázdniť Vädsť", + "mapCache_failedDownloads": "Neúspešné stiahnutia: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -768,7 +768,7 @@ } } }, - "time_justNow": "Príbeh", + "time_justNow": "Príbeh", "time_minutesAgo": "{minutes} min dozadu", "@time_minutesAgo": { "placeholders": { @@ -785,7 +785,7 @@ } } }, - "time_daysAgo": "{days} dní dozadu", + "time_daysAgo": "{days} dní dozadu", "@time_daysAgo": { "placeholders": { "days": { @@ -795,31 +795,31 @@ }, "time_hour": "hodina", "time_hours": "hodiny", - "time_day": "deň", + "time_day": "deň", "time_days": "dni", - "time_week": "týždeň", - "time_weeks": "týždne", + "time_week": "týždeň", + "time_weeks": "týždne", "time_month": "mesiac", "time_months": "mesiace", - "time_minutes": "minúty", - "time_allTime": "VÅ¡etko ÄŒasom", - "dialog_disconnect": "OdpojiÅ¥", - "dialog_disconnectConfirm": "Ste si istý/á, že chcete odpojiÅ¥ od tohto zariadenia?", - "login_repeaterLogin": "Opätovné prihlásenie", - "login_roomLogin": "Prihlásenie do miestnosti", + "time_minutes": "minúty", + "time_allTime": "Všetko Časom", + "dialog_disconnect": "Odpojiť", + "dialog_disconnectConfirm": "Ste si istý/á, že chcete odpojiť od tohto zariadenia?", + "login_repeaterLogin": "Opätovné prihlásenie", + "login_roomLogin": "Prihlásenie do miestnosti", "login_password": "Heslo", "login_enterPassword": "Zadajte heslo", - "login_savePassword": "UložiÅ¥ heslo", - "login_savePasswordSubtitle": "Heslo bude bezpečne uložené na tomto zariadení.", - "login_repeaterDescription": "Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.", - "login_roomDescription": "Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.", - "login_routing": "Rútiace", - "login_routingMode": "Režim trasy", - "login_autoUseSavedPath": "PoužiÅ¥ uloženú cestu", - "login_forceFloodMode": "ZavrieÅ¥ režim núdzového povodňového režimu", - "login_managePaths": "SpravovaÅ¥ Cesty", - "login_login": "PrihlásiÅ¥", - "login_attempt": "Skúšaj {current}/{max}", + "login_savePassword": "Uložiť heslo", + "login_savePasswordSubtitle": "Heslo bude bezpečne uložené na tomto zariadení.", + "login_repeaterDescription": "Zadajte heslo opakovača, aby ste získali prístup k nastaveniam a stavu.", + "login_roomDescription": "Zadajte heslo do miestnosti na prístup k nastaveniam a stavu.", + "login_routing": "Rútiace", + "login_routingMode": "Režim trasy", + "login_autoUseSavedPath": "Použiť uloženú cestu", + "login_forceFloodMode": "Zavrieť režim núdzového povodňového režimu", + "login_managePaths": "Spravovať Cesty", + "login_login": "Prihlásiť", + "login_attempt": "Skúšaj {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Prihlásenie zlyhalo: {error}", + "login_failed": "Prihlásenie zlyhalo: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.", - "common_reload": "NačítaÅ¥", - "common_clear": "ZmazaÅ¥", - "path_currentPath": "Aktívna cesta: {path}", + "login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.", + "common_reload": "Načítať", + "common_clear": "Zmazať", + "path_currentPath": "Aktívna cesta: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Používa {count} {count, plural, =1{hop} other{hops}} cestu", + "path_usingHopsPath": "Používa {count} {count, plural, =1{hop} other{hops}} cestu", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,16 +857,16 @@ } } }, - "path_enterCustomPath": "Zadajte vlastný priebeh", - "path_currentPathLabel": "Aktuálny priebeh", - "path_hexPrefixInstructions": "Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.", - "path_hexPrefixExample": "A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)", - "path_labelHexPrefixes": "Cesty (hexové predpony)", - "path_helperMaxHops": "Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).", + "path_enterCustomPath": "Zadajte vlastný priebeh", + "path_currentPathLabel": "Aktuálny priebeh", + "path_hexPrefixInstructions": "Zadajte 2-miestne hexové predpony pre každú fázu, oddelené čiarkami.", + "path_hexPrefixExample": "A1,F2,3C (každý uzel používa prvý bajt svojho verejného kľúča)", + "path_labelHexPrefixes": "Cesty (hexové predpony)", + "path_helperMaxHops": "Max 64 skokov. Každý prefix je 2 hexadecimálne znaky (1 bajt).", "path_selectFromContacts": "Vyberte sa z kontaktov:", - "path_noRepeatersFound": "NenaÅ¡li sa žiadne opakovače ani serverové miestnosti.", - "path_customPathsRequire": "Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášaÅ¥ správky.", - "path_invalidHexPrefixes": "Neplatné hexové predpony: {prefixes}", + "path_noRepeatersFound": "Nenašli sa žiadne opakovače ani serverové miestnosti.", + "path_customPathsRequire": "Vlastné cesty vyžadujú medziletoch, ktoré môžu prenášať správky.", + "path_invalidHexPrefixes": "Neplatné hexové predpony: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Cesta je príliÅ¡ dlhá. Umožnené je maximum 64 skokov.", - "path_setPath": "NastaviÅ¥ cestu", - "repeater_management": "Správa opakérov", - "repeater_managementTools": "Nástroje na správu", + "path_tooLong": "Cesta je príliš dlhá. Umožnené je maximum 64 skokov.", + "path_setPath": "Nastaviť cestu", + "repeater_management": "Správa opakérov", + "repeater_managementTools": "Nástroje na správu", "repeater_status": "Status", - "repeater_statusSubtitle": "ZobraziÅ¥ stav, Å¡tatistiky a susedov repeatera", + "repeater_statusSubtitle": "Zobraziť stav, štatistiky a susedov repeatera", "repeater_telemetry": "Telemetria", - "repeater_telemetrySubtitle": "ZobraziÅ¥ telemetriu senzorov a systémových Å¡tatistík", + "repeater_telemetrySubtitle": "Zobraziť telemetriu senzorov a systémových štatistík", "repeater_cli": "CLI", - "repeater_cliSubtitle": "PoÅ¡lite príkazy opakovaču", + "repeater_cliSubtitle": "Pošlite príkazy opakovaču", "repeater_settings": "Nastavenia", - "repeater_settingsSubtitle": "Konfigurujte parametre opakovača", - "repeater_statusTitle": "Status opakého zboru", - "repeater_routingMode": "Režim trasy", - "repeater_autoUseSavedPath": "PoužiÅ¥ uloženú cestu", - "repeater_forceFloodMode": "ZavrieÅ¥ režim núdzového povodňového režimu", - "repeater_pathManagement": "Správa trás", - "repeater_refresh": "ObnoviÅ¥", - "repeater_statusRequestTimeout": "Požiadavka stavu zlyhala.", - "repeater_errorLoadingStatus": "Chyba pri načítaní stavu: {error}", + "repeater_settingsSubtitle": "Konfigurujte parametre opakovača", + "repeater_statusTitle": "Status opakého zboru", + "repeater_routingMode": "Režim trasy", + "repeater_autoUseSavedPath": "Použiť uloženú cestu", + "repeater_forceFloodMode": "Zavrieť režim núdzového povodňového režimu", + "repeater_pathManagement": "Správa trás", + "repeater_refresh": "Obnoviť", + "repeater_statusRequestTimeout": "Požiadavka stavu zlyhala.", + "repeater_errorLoadingStatus": "Chyba pri načítaní stavu: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -901,23 +901,23 @@ } } }, - "repeater_systemInformation": "Informácie o systéme", - "repeater_battery": "Batéria", - "repeater_clockAtLogin": "ÄŒas (pÅ™i pÅ™ihlášení)", - "repeater_uptime": "DostupnosÅ¥", - "repeater_queueLength": "Dĺžka fronty", - "repeater_debugFlags": "Kontrolné značky", - "repeater_radioStatistics": "Rádio Å tatistiky", - "repeater_lastRssi": "Posledná RSSI", - "repeater_lastSnr": "Posledný SNR", - "repeater_noiseFloor": "Hladina Å¡umu", + "repeater_systemInformation": "Informácie o systéme", + "repeater_battery": "Batéria", + "repeater_clockAtLogin": "Čas (při přihlášení)", + "repeater_uptime": "Dostupnosť", + "repeater_queueLength": "Dĺžka fronty", + "repeater_debugFlags": "Kontrolné značky", + "repeater_radioStatistics": "Rádio Štatistiky", + "repeater_lastRssi": "Posledná RSSI", + "repeater_lastSnr": "Posledný SNR", + "repeater_noiseFloor": "Hladina šumu", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", - "repeater_packetStatistics": "Statistiky balíka", - "repeater_sent": "Odoslané", - "repeater_received": "PriÅ¡lo", - "repeater_duplicates": "Duplikáty", - "repeater_daysHoursMinsSecs": "{days} dní {hours}h {minutes}m {seconds}s", + "repeater_packetStatistics": "Statistiky balíka", + "repeater_sent": "Odoslané", + "repeater_received": "Prišlo", + "repeater_duplicates": "Duplikáty", + "repeater_daysHoursMinsSecs": "{days} dní {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", + "repeater_packetTxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", + "repeater_packetRxTotal": "Celkem: {total}, Povodňový režim: {flood}, Priamy: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -981,37 +981,37 @@ } } }, - "repeater_settingsTitle": "Nastavenia OpakovacÌŒa", - "repeater_basicSettings": "Základné nastavenia", - "repeater_repeaterName": "Opakovacia názov", - "repeater_repeaterNameHelper": "Zobrazenie názvu tohto opakovača", - "repeater_adminPassword": "Heslo administrátora", - "repeater_adminPasswordHelper": "Celý prístupový heslo", - "repeater_guestPassword": "Heslo hosÅ¥a", - "repeater_guestPasswordHelper": "Prístupový heslo iba na čítanie", - "repeater_radioSettings": "Nastavenia rádia", + "repeater_settingsTitle": "Nastavenia Opakovača", + "repeater_basicSettings": "Základné nastavenia", + "repeater_repeaterName": "Opakovacia názov", + "repeater_repeaterNameHelper": "Zobrazenie názvu tohto opakovača", + "repeater_adminPassword": "Heslo administrátora", + "repeater_adminPasswordHelper": "Celý prístupový heslo", + "repeater_guestPassword": "Heslo hosťa", + "repeater_guestPasswordHelper": "Prístupový heslo iba na čítanie", + "repeater_radioSettings": "Nastavenia rádia", "repeater_frequencyMhz": "Frekvencia (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Power", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Šírka pásma", - "repeater_spreadingFactor": "Šírenie faktoru", - "repeater_codingRate": "RýchlosÅ¥ kódovania", + "repeater_bandwidth": "Šírka pásma", + "repeater_spreadingFactor": "Šírenie faktoru", + "repeater_codingRate": "Rýchlosť kódovania", "repeater_locationSettings": "Nastavenia polohy", - "repeater_latitude": "Súradnica", - "repeater_latitudeHelper": "Desatinné zložky (napr. 37.7749)", - "repeater_longitude": "Dĺžka", - "repeater_longitudeHelper": "Desatinné zložky (napr. -122.4194)", + "repeater_latitude": "Súradnica", + "repeater_latitudeHelper": "Desatinné zložky (napr. 37.7749)", + "repeater_longitude": "Dĺžka", + "repeater_longitudeHelper": "Desatinné zložky (napr. -122.4194)", "repeater_features": "Funkcie", "repeater_packetForwarding": "Riadenie prienikov", - "repeater_packetForwardingSubtitle": "Povolte opakovač na smerovanie paketov.", - "repeater_guestAccess": "Prístup pre hostí", - "repeater_guestAccessSubtitle": "UmožniÅ¥ prístup hosta iba na čítanie.", - "repeater_privacyMode": "Režim ochrany súkromia", - "repeater_privacyModeSubtitle": "SkryÅ¥ meno/poloha v reklamách", + "repeater_packetForwardingSubtitle": "Povolte opakovač na smerovanie paketov.", + "repeater_guestAccess": "Prístup pre hostí", + "repeater_guestAccessSubtitle": "Umožniť prístup hosta iba na čítanie.", + "repeater_privacyMode": "Režim ochrany súkromia", + "repeater_privacyModeSubtitle": "Skryť meno/poloha v reklamách", "repeater_advertisementSettings": "Nastavenia reklamy", - "repeater_localAdvertInterval": "Lokálna reklamná časová obdoba", - "repeater_localAdvertIntervalMinutes": "{minutes} minút", + "repeater_localAdvertInterval": "Lokálna reklamná časová obdoba", + "repeater_localAdvertIntervalMinutes": "{minutes} minút", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1019,8 +1019,8 @@ } } }, - "repeater_floodAdvertInterval": "Interval reklamnej povodňovej reklamy", - "repeater_floodAdvertIntervalHours": "{hours} hodín", + "repeater_floodAdvertInterval": "Interval reklamnej povodňovej reklamy", + "repeater_floodAdvertIntervalHours": "{hours} hodín", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1028,19 +1028,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Å ifrovaný reklamný interval", - "repeater_dangerZone": "Nebezpečná zóna", - "repeater_rebootRepeater": "Restart Repetér", - "repeater_rebootRepeaterSubtitle": "ResetovaÅ¥ vysielací prístroj", - "repeater_rebootRepeaterConfirm": "Ste si istý, že chcete tento opakovač restartovaÅ¥?", - "repeater_regenerateIdentityKey": "GenerovaÅ¥ kľúč identity", - "repeater_regenerateIdentityKeySubtitle": "GenerovaÅ¥ nový pár verejných/privátnych kľúčov", - "repeater_regenerateIdentityKeyConfirm": "Toto vytvorí nový identitu pre opakovač. PokračovaÅ¥?", - "repeater_eraseFileSystem": "VymažaÅ¥ Systémový ReÅ¥azec", - "repeater_eraseFileSystemSubtitle": "FormátovaÅ¥ systém opakujúcich sa súborov", - "repeater_eraseFileSystemConfirm": "VAROVANIE: Toto zmaže vÅ¡etky dáta na opakovači. To sa nedá zruÅ¡iÅ¥!", - "repeater_eraseSerialOnly": "Odstránenie je dostupné len cez sériové rozhranie.", - "repeater_commandSent": "Poforovaný príkaz: {command}", + "repeater_encryptedAdvertInterval": "Šifrovaný reklamný interval", + "repeater_dangerZone": "Nebezpečná zóna", + "repeater_rebootRepeater": "Restart Repetér", + "repeater_rebootRepeaterSubtitle": "Resetovať vysielací prístroj", + "repeater_rebootRepeaterConfirm": "Ste si istý, že chcete tento opakovač restartovať?", + "repeater_regenerateIdentityKey": "Generovať kľúč identity", + "repeater_regenerateIdentityKeySubtitle": "Generovať nový pár verejných/privátnych kľúčov", + "repeater_regenerateIdentityKeyConfirm": "Toto vytvorí nový identitu pre opakovač. Pokračovať?", + "repeater_eraseFileSystem": "Vymažať Systémový Reťazec", + "repeater_eraseFileSystemSubtitle": "Formátovať systém opakujúcich sa súborov", + "repeater_eraseFileSystemConfirm": "VAROVANIE: Toto zmaže všetky dáta na opakovači. To sa nedá zrušiť!", + "repeater_eraseSerialOnly": "Odstránenie je dostupné len cez sériové rozhranie.", + "repeater_commandSent": "Poforovaný príkaz: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Chyba pri odeslaní príkazu: {error}", + "repeater_errorSendingCommand": "Chyba pri odeslaní príkazu: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "PotvrdiÅ¥", - "repeater_settingsSaved": "Nastavenia boli uložené úspeÅ¡ne.", - "repeater_errorSavingSettings": "Chyba pri ukladaní nastavení: {error}", + "repeater_confirm": "Potvrdiť", + "repeater_settingsSaved": "Nastavenia boli uložené úspešne.", + "repeater_errorSavingSettings": "Chyba pri ukladaní nastavení: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,15 +1066,15 @@ } } }, - "repeater_refreshBasicSettings": "ObnoviÅ¥ základné nastavenia", - "repeater_refreshRadioSettings": "ObnoviÅ¥ Nastavenia Rádií", - "repeater_refreshTxPower": "ObnoviÅ¥ TX napájanie", - "repeater_refreshLocationSettings": "ObnoviÅ¥ Nastavenia Miesta", - "repeater_refreshPacketForwarding": "ObnoviÅ¥ smerovanie paketov", - "repeater_refreshGuestAccess": "ObnoviÅ¥ prístup hosÅ¥a", - "repeater_refreshPrivacyMode": "ObnoviÅ¥ Ochranný režim", - "repeater_refreshAdvertisementSettings": "ObnoviÅ¥ nastavenia reklamy", - "repeater_refreshed": "{label} sa znova načítalo", + "repeater_refreshBasicSettings": "Obnoviť základné nastavenia", + "repeater_refreshRadioSettings": "Obnoviť Nastavenia Rádií", + "repeater_refreshTxPower": "Obnoviť TX napájanie", + "repeater_refreshLocationSettings": "Obnoviť Nastavenia Miesta", + "repeater_refreshPacketForwarding": "Obnoviť smerovanie paketov", + "repeater_refreshGuestAccess": "Obnoviť prístup hosťa", + "repeater_refreshPrivacyMode": "Obnoviť Ochranný režim", + "repeater_refreshAdvertisementSettings": "Obnoviť nastavenia reklamy", + "repeater_refreshed": "{label} sa znova načítalo", "@repeater_refreshed": { "placeholders": { "label": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Chyba pri obnovení {label}", + "repeater_errorRefreshing": "Chyba pri obnovení {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,16 +1091,16 @@ } }, "repeater_cliTitle": "Opakovacia CLI", - "repeater_debugNextCommand": "Oprava Nasledujúceho Príkaz", + "repeater_debugNextCommand": "Oprava Nasledujúceho Príkaz", "repeater_commandHelp": "Pomoc", - "repeater_clearHistory": "VymazaÅ¥ históriu", - "repeater_noCommandsSent": "Zatiaľ neboli odeslané žiadne príkazy.", - "repeater_typeCommandOrUseQuick": "Zadajte príkaz nižšie alebo použite rýchle príkazy", - "repeater_enterCommandHint": "Zadajte príkaz...", - "repeater_previousCommand": "Predchádzajúci príkaz", - "repeater_nextCommand": "Nasledujúci príkaz", - "repeater_enterCommandFirst": "Zadajte najprv príkaz", - "repeater_cliCommandFrameTitle": "Rámok Príkaz CLI", + "repeater_clearHistory": "Vymazať históriu", + "repeater_noCommandsSent": "Zatiaľ neboli odeslané žiadne príkazy.", + "repeater_typeCommandOrUseQuick": "Zadajte príkaz nižšie alebo použite rýchle príkazy", + "repeater_enterCommandHint": "Zadajte príkaz...", + "repeater_previousCommand": "Predchádzajúci príkaz", + "repeater_nextCommand": "Nasledujúci príkaz", + "repeater_enterCommandFirst": "Zadajte najprv príkaz", + "repeater_cliCommandFrameTitle": "Rámok Príkaz CLI", "repeater_cliCommandError": "Chyba: {error}", "@repeater_cliCommandError": { "placeholders": { @@ -1109,81 +1109,81 @@ } } }, - "repeater_cliQuickGetName": "ZísÅ¥ meno", - "repeater_cliQuickGetRadio": "ZísÅ¥ po rádiu", - "repeater_cliQuickGetTx": "ZísÅ¥ TX", - "repeater_cliQuickNeighbors": "Súsezný", + "repeater_cliQuickGetName": "Zísť meno", + "repeater_cliQuickGetRadio": "Zísť po rádiu", + "repeater_cliQuickGetTx": "Zísť TX", + "repeater_cliQuickNeighbors": "Súsezný", "repeater_cliQuickVersion": "Verzia", "repeater_cliQuickAdvertise": "Reklama", "repeater_cliQuickClock": "Hodiny", - "repeater_cliHelpAdvert": "Odosiela reklamnú balíček.", - "repeater_cliHelpReboot": "Resetuje zariadenie. (pozor, môže dôjsÅ¥ k 'Timeoutu', čo je normálne)", - "repeater_cliHelpClock": "Zobrazuje aktuálny čas podľa hodiniek zariadenia.", - "repeater_cliHelpPassword": "Nastaví nový administrátorský prístupový údaj pre zariadenie.", - "repeater_cliHelpVersion": "Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.", - "repeater_cliHelpClearStats": "Resetuje rôzne Å¡tatistické počítadlá na nulu.", - "repeater_cliHelpSetAf": "Nastavuje časový faktor.", - "repeater_cliHelpSetTx": "Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reÅ¡tart na aplikáciu)", - "repeater_cliHelpSetRepeat": "Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.", - "repeater_cliHelpSetAllowReadOnly": "(Server miestnosti) Ak je 'zapnuté', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielaÅ¥ správu do miestnosti. (iba čítaÅ¥).", - "repeater_cliHelpSetFloodMax": "Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)", - "repeater_cliHelpSetIntThresh": "Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.", - "repeater_cliHelpSetAgcResetInterval": "Nastavuje interval na reÅ¡tartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.", - "repeater_cliHelpSetMultiAcks": "Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".", - "repeater_cliHelpSetAdvertInterval": "Nastavuje interval časovača v minútach na odoÅ¡le miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.", - "repeater_cliHelpSetFloodAdvertInterval": "Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.", - "repeater_cliHelpSetGuestPassword": "Nastavuje/aktualizuje heslo hosÅ¥a. (pre opakované pripojenia môžu hosÅ¥ovské prihlásenia posielaÅ¥ požadanie \"Get Stats\")", - "repeater_cliHelpSetName": "Nastaví názov reklamy.", - "repeater_cliHelpSetLat": "Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)", - "repeater_cliHelpSetLon": "Nastavuje longitudinu reklamnej mapy. (desatinné stupne)", - "repeater_cliHelpSetRadio": "Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.", - "repeater_cliHelpSetRxDelay": "Nastavenia (experimentálne) základné (musi byÅ¥ > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.", - "repeater_cliHelpSetTxDelay": "Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiÅ¥ pravdepodobnosÅ¥ kolízii).", - "repeater_cliHelpSetDirectTxDelay": "Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.", - "repeater_cliHelpSetBridgeEnabled": "AktivovaÅ¥/ZatváraÅ¥ most.", - "repeater_cliHelpSetBridgeDelay": "NastaviÅ¥ odklad pred retransmisiou paketov.", - "repeater_cliHelpSetBridgeSource": "Zvolte, či bude most retransmitovaÅ¥ prijaté alebo vysielané balíčky.", - "repeater_cliHelpSetBridgeBaud": "Nastavte sériový link baudrate pre rs232 mosty.", - "repeater_cliHelpSetBridgeSecret": "NastaviÅ¥ tajomstvo mosta pre eshnow mosty.", - "repeater_cliHelpSetAdcMultiplier": "Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).", - "repeater_cliHelpTempRadio": "Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).", - "repeater_cliHelpSetPerm": "Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).", - "repeater_cliHelpGetBridgeType": "ZísÅ¥ typ mosta: žiadny, rs232, espnow", - "repeater_cliHelpLogStart": "Začína protokolovanie balíkov do systému súborov.", - "repeater_cliHelpLogStop": "Zastaví protokolovanie paketov do systémového súboru.", - "repeater_cliHelpLogErase": "Odstráni záznamy z balíkov z systému súborov.", - "repeater_cliHelpNeighbors": "Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.", - "repeater_cliHelpRegion": "(len sériál) Zobrazuje vÅ¡etky definované regióny a aktuálne povolenia pre povodňové situácie.", - "repeater_cliHelpRegionLoad": "Poznámka: toto je Å¡peciálna multi-príkázová inÅ¡tancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.", - "repeater_cliHelpRegionGet": "Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) 'F'\"", - "repeater_cliHelpRegionPut": "Pridá alebo aktualizuje definíciu regiónu s daným menom.", - "repeater_cliHelpRegionRemove": "Odstráni definíciu oblasti s daným názvom. (musí zodpovedaÅ¥ presne a nemala by maÅ¥ podoblasti)", - "repeater_cliHelpRegionAllowf": "Nastavuje povolenie 'P'lávu pre zadanú oblasÅ¥. ('' pre globálny/dedičský rozsah)", - "repeater_cliHelpRegionDenyf": "Odstráni povolenie 'F'lood' pre zadanú oblasÅ¥. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používaÅ¥ na globálnom/dedskom rozsahu!!).", - "repeater_cliHelpRegionHome": "Odpovedá s aktuálnou 'domovskou' oblasÅ¥ou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)", - "repeater_cliHelpRegionHomeSet": "Nastaví 'domovskú' oblasÅ¥.", - "repeater_cliHelpRegionSave": "Uloží zoznam/mapu regiónov do úložiska.", - "repeater_cliHelpGps": "Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.", - "repeater_cliHelpGpsOnOff": "Prepínač stavu GPS napájania.", - "repeater_cliHelpGpsSync": "Synchronizuje čas uzla s GPS hodinami.", - "repeater_cliHelpGpsSetLoc": "Nastaví polohu uzla na GPS súradnice a uloží preferencie.", - "repeater_cliHelpGpsAdvert": "Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľaÅ¥: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach", - "repeater_cliHelpGpsAdvertSet": "Nastavuje konfiguráciu reklamy na zadané miesto.", - "repeater_commandsListTitle": "Zoznam príkazov", - "repeater_commandsListNote": "Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".", - "repeater_general": "Obecné", + "repeater_cliHelpAdvert": "Odosiela reklamnú balíček.", + "repeater_cliHelpReboot": "Resetuje zariadenie. (pozor, môže dôjsť k 'Timeoutu', čo je normálne)", + "repeater_cliHelpClock": "Zobrazuje aktuálny čas podľa hodiniek zariadenia.", + "repeater_cliHelpPassword": "Nastaví nový administrátorský prístupový údaj pre zariadenie.", + "repeater_cliHelpVersion": "Zobrazuje verziu zariadenia a dátum zostavenia firmvéru.", + "repeater_cliHelpClearStats": "Resetuje rôzne štatistické počítadlá na nulu.", + "repeater_cliHelpSetAf": "Nastavuje časový faktor.", + "repeater_cliHelpSetTx": "Nastavenie vysielacej sily LoRa v dBm. (potrebuje sa reštart na aplikáciu)", + "repeater_cliHelpSetRepeat": "Umožňuje alebo vypína zopakovaný príspevok pre tento uzol.", + "repeater_cliHelpSetAllowReadOnly": "(Server miestnosti) Ak je 'zapnuté', potom bude povolený prístup s prázdnym heslom, ale nebude možné posielať správu do miestnosti. (iba čítať).", + "repeater_cliHelpSetFloodMax": "Nastavuje maximálny počet skokov pre vstupný povelový paket (ak je >= max, paket nie je preposlaný)", + "repeater_cliHelpSetIntThresh": "Nastavuje hranicu ruživeho ladenia (v dB). Predvolené je 14. Nastavením na 0 sa vypne detekcia ruživeho ladenia kanálu.", + "repeater_cliHelpSetAgcResetInterval": "Nastavuje interval na reštartovanie Auto Gain Controlleru. Nastavenie na 0 vypne funkciu.", + "repeater_cliHelpSetMultiAcks": "Povolí alebo pozastaví funkciiu \"dvojité potvrdenia\".", + "repeater_cliHelpSetAdvertInterval": "Nastavuje interval časovača v minútach na odošle miestny (bezprostredný) reklamný paket. Nastavenie na 0 vypne funkciu.", + "repeater_cliHelpSetFloodAdvertInterval": "Nastavuje interval časovača v hodinách na odeslanie reklamnej vlne. Nastavenie na 0 vypne.", + "repeater_cliHelpSetGuestPassword": "Nastavuje/aktualizuje heslo hosťa. (pre opakované pripojenia môžu hosťovské prihlásenia posielať požadanie \"Get Stats\")", + "repeater_cliHelpSetName": "Nastaví názov reklamy.", + "repeater_cliHelpSetLat": "Nastaví geografickú šírku reklamnej mapy. (desatinné stupne)", + "repeater_cliHelpSetLon": "Nastavuje longitudinu reklamnej mapy. (desatinné stupne)", + "repeater_cliHelpSetRadio": "Nastavuje úplne nové parametre rádia a uloží ich do preferencií. Požaduje príkaz \"reboot\" na aplikáciu.", + "repeater_cliHelpSetRxDelay": "Nastavenia (experimentálne) základné (musi byť > 1 pre účel) na aplikáciu mierneho onesenia prijatých paketov, na základe signálu/skóre. Nastavenie na 0 vypne.", + "repeater_cliHelpSetTxDelay": "Nastavuje faktor násobený časom na vzduchu pre paket v režime povodňovej vlny a s náhodným systémom slotov, aby sa oneskorene jeho prenosovanie (s cieľom znížiť pravdepodobnosť kolízii).", + "repeater_cliHelpSetDirectTxDelay": "Podobne ako txdelay, ale pre aplikáciu náhodného oneskorenia pri preposlaní paketov v režime priameho prenosu.", + "repeater_cliHelpSetBridgeEnabled": "Aktivovať/Zatvárať most.", + "repeater_cliHelpSetBridgeDelay": "Nastaviť odklad pred retransmisiou paketov.", + "repeater_cliHelpSetBridgeSource": "Zvolte, či bude most retransmitovať prijaté alebo vysielané balíčky.", + "repeater_cliHelpSetBridgeBaud": "Nastavte sériový link baudrate pre rs232 mosty.", + "repeater_cliHelpSetBridgeSecret": "Nastaviť tajomstvo mosta pre eshnow mosty.", + "repeater_cliHelpSetAdcMultiplier": "Nastavuje vlastný faktor na úpravu nahlásenej batériovej napätia (podporované len na vybraných doskách).", + "repeater_cliHelpTempRadio": "Nastaví dočasné rádiové parametre pre zadaný počet minút, po skončení sa vráti k pôvodným rádiovým parametrom. (nepočuva sa do preferencií).", + "repeater_cliHelpSetPerm": "Zmení ACL. Odstráni zodpovedný záznam (podľa prefixa pubkey), ak je \"permissions\" rovné 0. Pridá nový záznam, ak je pubkey-hex plnej dĺžky a momentálne sa nenachádza v ACL. Aktualizuje záznam podľa zodpovedajúceho prefixa pubkey. Bitové oprávnenia sa líšia podľa funkčnej roly, ale nízke 2 bity sú: 0 (Hostiteľ), 1 (Čítanie len), 2 (Čítanie a zápis), 3 (Správca).", + "repeater_cliHelpGetBridgeType": "Zísť typ mosta: žiadny, rs232, espnow", + "repeater_cliHelpLogStart": "Začína protokolovanie balíkov do systému súborov.", + "repeater_cliHelpLogStop": "Zastaví protokolovanie paketov do systémového súboru.", + "repeater_cliHelpLogErase": "Odstráni záznamy z balíkov z systému súborov.", + "repeater_cliHelpNeighbors": "Zobrazuje zoznam iných repeaterových uzlov zasielaných cez zero-hop reklamy. Každý riadok je id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Odstráni prvú zhodujúcu položku (podľa prefixu pubkey (hex)) z zoznamu susedov.", + "repeater_cliHelpRegion": "(len sériál) Zobrazuje všetky definované regióny a aktuálne povolenia pre povodňové situácie.", + "repeater_cliHelpRegionLoad": "Poznámka: toto je špeciálna multi-príkázová inštancia. Každé nasledujúce príkaza je názov oblasti (zapustený s medzerami na indikáciu hierarchického pomeru, s minimálne jednou medzerou). Ukončené odeslaním prázdnej platnej linky/príkazu.", + "repeater_cliHelpRegionGet": "Hľadá región s daným príponou názvu (alebo \"\\\" pre globálny rozsah). Odpovedá \"-> región-název (rodič-název) 'F'\"", + "repeater_cliHelpRegionPut": "Pridá alebo aktualizuje definíciu regiónu s daným menom.", + "repeater_cliHelpRegionRemove": "Odstráni definíciu oblasti s daným názvom. (musí zodpovedať presne a nemala by mať podoblasti)", + "repeater_cliHelpRegionAllowf": "Nastavuje povolenie 'P'lávu pre zadanú oblasť. ('' pre globálny/dedičský rozsah)", + "repeater_cliHelpRegionDenyf": "Odstráni povolenie 'F'lood' pre zadanú oblasť. (UPOZORNENIE: v tejto fáze nie je odporúčané ho používať na globálnom/dedskom rozsahu!!).", + "repeater_cliHelpRegionHome": "Odpovedá s aktuálnou 'domovskou' oblasťou. (Poznámka aplikovaná zatiaľ nikde, vyhradené na budúce)", + "repeater_cliHelpRegionHomeSet": "Nastaví 'domovskú' oblasť.", + "repeater_cliHelpRegionSave": "Uloží zoznam/mapu regiónov do úložiska.", + "repeater_cliHelpGps": "Zobrazuje stav GPS. Ak je GPS vypnutý, odpovedá len \"off\", ak je zapnutý, odpovedá s \"on\", stavom, fixom a počtom satelitov.", + "repeater_cliHelpGpsOnOff": "Prepínač stavu GPS napájania.", + "repeater_cliHelpGpsSync": "Synchronizuje čas uzla s GPS hodinami.", + "repeater_cliHelpGpsSetLoc": "Nastaví polohu uzla na GPS súradnice a uloží preferencie.", + "repeater_cliHelpGpsAdvert": "Poskytuje konfiguráciu reklamy pre uzol:\n- žiadna: nezahrňte polohu do reklám\n- zdieľať: zdieľajte GPS polohu (z SensorManager)\n- nastavenia: zobrazujte polohu uloženú v nastaveniach", + "repeater_cliHelpGpsAdvertSet": "Nastavuje konfiguráciu reklamy na zadané miesto.", + "repeater_commandsListTitle": "Zoznam príkazov", + "repeater_commandsListNote": "Poznámka: pre rôzne príkazy \"set ...\" existuje aj príkaz \"get ...\".", + "repeater_general": "Obecné", "repeater_settingsCategory": "Nastavenia", "repeater_bridge": "Most", - "repeater_logging": "Záznamy", - "repeater_neighborsRepeaterOnly": "Súseznýci (iba opakovač)", - "repeater_regionManagementRepeaterOnly": "Správa regiónov (iba opakovač)", - "repeater_regionNote": "Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.", - "repeater_gpsManagement": "Správa GPS", - "repeater_gpsNote": "GPS príkaz bol zavádzaný na riadenie lokalitných tém.", - "telemetry_receivedData": "Obdolené Telemetrické dáta", - "telemetry_requestTimeout": "Požiadavka telemetrie zlyhala.", - "telemetry_errorLoading": "Chyba pri načítaní telemetrie: {error}", + "repeater_logging": "Záznamy", + "repeater_neighborsRepeaterOnly": "Súseznýci (iba opakovač)", + "repeater_regionManagementRepeaterOnly": "Správa regiónov (iba opakovač)", + "repeater_regionNote": "Regionové príkazy boli zavádzané na správu regionálnych definícií a oprávnení.", + "repeater_gpsManagement": "Správa GPS", + "repeater_gpsNote": "GPS príkaz bol zavádzaný na riadenie lokalitných tém.", + "telemetry_receivedData": "Obdolené Telemetrické dáta", + "telemetry_requestTimeout": "Požiadavka telemetrie zlyhala.", + "telemetry_errorLoading": "Chyba pri načítaní telemetrie: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1191,8 +1191,8 @@ } } }, - "telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.", - "telemetry_channelTitle": "Kanál {channel}", + "telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.", + "telemetry_channelTitle": "Kanál {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1200,11 +1200,11 @@ } } }, - "telemetry_batteryLabel": "Batéria", - "telemetry_voltageLabel": "Napätie", + "telemetry_batteryLabel": "Batéria", + "telemetry_voltageLabel": "Napätie", "telemetry_mcuTemperatureLabel": "MCU teplota", "telemetry_temperatureLabel": "Teplota", - "telemetry_currentLabel": "Aktuálne", + "telemetry_currentLabel": "Aktuálne", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Cesta balíka", - "channelPath_viewMap": "ZobraziÅ¥ mapu", - "channelPath_otherObservedPaths": "Ostatné pozorovacie cesty", - "channelPath_repeaterHops": "Skoky opakovača", - "channelPath_noHopDetails": "Podrobnosti o balíčku zatiaľ nie sú dostupné.", - "channelPath_messageDetails": "Podrobnosti o zprávach", - "channelPath_senderLabel": "Posielateľ", - "channelPath_timeLabel": "ÄŒas", + "channelPath_title": "Cesta balíka", + "channelPath_viewMap": "Zobraziť mapu", + "channelPath_otherObservedPaths": "Ostatné pozorovacie cesty", + "channelPath_repeaterHops": "Skoky opakovača", + "channelPath_noHopDetails": "Podrobnosti o balíčku zatiaľ nie sú dostupné.", + "channelPath_messageDetails": "Podrobnosti o zprávach", + "channelPath_senderLabel": "Posielateľ", + "channelPath_timeLabel": "Čas", "channelPath_repeatsLabel": "Opakovanie", "channelPath_pathLabel": "Cesta {index}", - "channelPath_observedLabel": "Pozorované", - "channelPath_observedPathTitle": "Sledovaný postup {index} • {hops}", + "channelPath_observedLabel": "Pozorované", + "channelPath_observedPathTitle": "Sledovaný postup {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Žiadne údaje o polohe", + "channelPath_noLocationData": "Žiadne údaje o polohe", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1288,8 +1288,8 @@ } } }, - "channelPath_unknownPath": "Neznáme", - "channelPath_floodPath": "Povodňová", + "channelPath_unknownPath": "Neznáme", + "channelPath_floodPath": "Povodňová", "channelPath_directPath": "Priamo", "channelPath_observedZeroOf": "0 z {total} skokov", "@channelPath_observedZeroOf": { @@ -1311,8 +1311,8 @@ } }, "channelPath_mapTitle": "Mapa Trasy", - "channelPath_noRepeaterLocations": "Pre túto cestu nie je dostupných žiadne polohy opakovačov.", - "channelPath_primaryPath": "Cesta {index} (Hlavná)", + "channelPath_noRepeaterLocations": "Pre túto cestu nie je dostupných žiadne polohy opakovačov.", + "channelPath_primaryPath": "Cesta {index} (Hlavná)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1328,8 +1328,8 @@ } }, "channelPath_pathLabelTitle": "Cesta", - "channelPath_observedPathHeader": "Sledovaná cesta", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_observedPathHeader": "Sledovaná cesta", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,20 +1340,20 @@ } } }, - "channelPath_noHopDetailsAvailable": "Pre toto balíček nie sú dostupné údaje o skokoch.", - "channelPath_unknownRepeater": "Neznáme opakovače", - "listFilter_tooltip": "FiltrovaÅ¥ a triediÅ¥", - "listFilter_sortBy": "TriediÅ¥ podľa", - "listFilter_latestMessages": "Posledné správy", - "listFilter_heardRecently": "Nedávno počuli.", + "channelPath_noHopDetailsAvailable": "Pre toto balíček nie sú dostupné údaje o skokoch.", + "channelPath_unknownRepeater": "Neznáme opakovače", + "listFilter_tooltip": "Filtrovať a triediť", + "listFilter_sortBy": "Triediť podľa", + "listFilter_latestMessages": "Posledné správy", + "listFilter_heardRecently": "Nedávno počuli.", "listFilter_az": "A-Z", "listFilter_filters": "Filtre", - "listFilter_all": "VÅ¡etko", - "listFilter_users": "Používatelia", - "listFilter_repeaters": "Opakovadlá", - "listFilter_roomServers": "Servéry miestnosti", - "listFilter_unreadOnly": "Nezaregistrované len", - "listFilter_newGroup": "Nová skupina", + "listFilter_all": "Všetko", + "listFilter_users": "Používatelia", + "listFilter_repeaters": "Opakovadlá", + "listFilter_roomServers": "Servéry miestnosti", + "listFilter_unreadOnly": "Nezaregistrované len", + "listFilter_newGroup": "Nová skupina", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1361,25 +1361,25 @@ } } }, - "repeater_neighborsSubtitle": "ZobraziÅ¥ susedné body bez skokov.", - "neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.", - "neighbors_receivedData": "Obdielo dáta suseda", - "repeater_neighbors": "Súsezný", - "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", - "neighbors_repeatersNeighbors": "Opakovadlá Súsezná", - "neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.", - "channels_createPrivateChannel": "Vytvorte súkromný kanál", - "channels_joinPrivateChannel": "PripojiÅ¥ sa k súkromnému kanálu", - "channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.", - "channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.", - "channels_joinPublicChannel": "Pripojte sa k verejnému kanálu", - "channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.", - "channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu", - "channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridaÅ¥ do hashtag kanálov.", - "channels_scanQrCode": "Skenujte QR kód", - "channels_scanQrCodeComingSoon": "ÄŒoskoro", + "repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.", + "neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.", + "neighbors_receivedData": "Obdielo dáta suseda", + "repeater_neighbors": "Súsezný", + "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", + "neighbors_repeatersNeighbors": "Opakovadlá Súsezná", + "neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.", + "channels_createPrivateChannel": "Vytvorte súkromný kanál", + "channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu", + "channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.", + "channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.", + "channels_joinPublicChannel": "Pripojte sa k verejnému kanálu", + "channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.", + "channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu", + "channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridať do hashtag kanálov.", + "channels_scanQrCode": "Skenujte QR kód", + "channels_scanQrCodeComingSoon": "Čoskoro", "channels_enterHashtag": "Zadajte hashtag", - "channels_hashtagHint": "napr. #tím", + "channels_hashtagHint": "napr. #tím", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1394,14 +1394,14 @@ } } }, - "neighbors_heardAgo": "Počuli sme to: {time} dozadu", - "neighbors_unknownContact": "Neznáma {pubkey}", - "settings_locationGPSEnable": "AktivovaÅ¥ GPS", - "settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.", + "neighbors_heardAgo": "Počuli sme to: {time} dozadu", + "neighbors_unknownContact": "Neznáma {pubkey}", + "settings_locationGPSEnable": "Aktivovať GPS", + "settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.", "settings_locationIntervalSec": "Interval pre GPS (Sekundy)", - "settings_locationIntervalInvalid": "Interval musí byÅ¥ aspoň 60 sekúnd a menej ako 86400 sekúnd.", - "contacts_manageRoom": "SpravovaÅ¥ server miestnosti", - "room_management": "Správa servera miestnosti", + "settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.", + "contacts_manageRoom": "Spravovať server miestnosti", + "room_management": "Správa servera miestnosti", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1458,36 +1458,36 @@ } } }, - "community_create": "VytvoriÅ¥ komunitu", + "community_create": "Vytvoriť komunitu", "community_title": "Komunita", - "community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.", - "community_join": "PripojiÅ¥", - "community_joinTitle": "PripojiÅ¥ sa k spoločenstvu", - "community_joinConfirmation": "ChceÅ¡ sa pridaÅ¥ do komunity \"{name}\"?", - "community_scanQr": "Skontrolujte komunitný QR kód", - "community_scanInstructions": "Zamerte kameru na komunitný QR kód.", - "community_showQr": "ZobraziÅ¥ QR kód", + "community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.", + "community_join": "Pripojiť", + "community_joinTitle": "Pripojiť sa k spoločenstvu", + "community_joinConfirmation": "Chceš sa pridať do komunity \"{name}\"?", + "community_scanQr": "Skontrolujte komunitný QR kód", + "community_scanInstructions": "Zamerte kameru na komunitný QR kód.", + "community_showQr": "Zobraziť QR kód", "common_ok": "OK\nDobre", - "community_publicChannel": "Komunita verejná", - "community_hashtagChannel": "Komunitný Hashtag", + "community_publicChannel": "Komunita verejná", + "community_hashtagChannel": "Komunitný Hashtag", "community_name": "Komunita", - "community_enterName": "Zadajte názov komunity", - "community_created": "Komunita \"{name}\" vytvorená", - "community_joined": "Pripojená komunita \"{name}\"", - "community_qrTitle": "Zdieľť komunitu", - "community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.", - "community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity", - "community_invalidQrCode": "Neplatná QR kód komunity.", - "community_alreadyMember": "Už ste členom.", - "community_alreadyMemberMessage": "Vy ste už členom \"{name}\".", - "community_addPublicChannel": "PridaÅ¥ verejný komunikačný kanál", - "community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.", - "community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite", - "community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.", - "community_manageCommunities": "SpravovaÅ¥ komunity", + "community_enterName": "Zadajte názov komunity", + "community_created": "Komunita \"{name}\" vytvorená", + "community_joined": "Pripojená komunita \"{name}\"", + "community_qrTitle": "Zdieľť komunitu", + "community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.", + "community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity", + "community_invalidQrCode": "Neplatná QR kód komunity.", + "community_alreadyMember": "Už ste členom.", + "community_alreadyMemberMessage": "Vy ste už členom \"{name}\".", + "community_addPublicChannel": "Pridať verejný komunikačný kanál", + "community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.", + "community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite", + "community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.", + "community_manageCommunities": "Spravovať komunity", "community_delete": "Nechajte komunitu", - "community_deleteConfirm": "OpustiÅ¥ \"{name}\"?", - "community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.", + "community_deleteConfirm": "Opustiť \"{name}\"?", + "community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Opustená komunita \"{name}\"", - "community_addHashtagChannel": "PridaÅ¥ komunitný hashtag", - "community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.", + "community_deleted": "Opustená komunita \"{name}\"", + "community_addHashtagChannel": "Pridať komunitný hashtag", + "community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.", "community_selectCommunity": "Vyberte komunitu", - "community_regularHashtag": "Zvyčajný hashtag", - "community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridaÅ¥)", - "community_communityHashtag": "Komunitný Hashtag", - "community_communityHashtagDesc": "Å pecifické pre členov komunity", + "community_regularHashtag": "Zvyčajný hashtag", + "community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)", + "community_communityHashtag": "Komunitný Hashtag", + "community_communityHashtagDesc": "Špecifické pre členov komunity", "community_forCommunity": "Pre {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1532,12 +1532,12 @@ } } }, - "community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne", - "community_regenerateSecretConfirm": "Znovu vygenerovaÅ¥ tajný kľúč pre \"{name}\"? VÅ¡etci členovia budú musieÅ¥ skanovaÅ¥ nový QR kód, aby mohli nadviazaÅ¥ komunikáciu.", - "community_regenerate": "Znovu vygenerovaÅ¥", - "community_regenerateSecret": "ZobraziÅ¥ nový tajný kód", - "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"", - "community_updateSecret": "AktualizovaÅ¥ tajné heslo", + "community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne", + "community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.", + "community_regenerate": "Znovu vygenerovať", + "community_regenerateSecret": "Zobraziť nový tajný kód", + "community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"", + "community_updateSecret": "Aktualizovať tajné heslo", "community_secretUpdated": "Zmena tajnej slova pre \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { @@ -1548,80 +1548,80 @@ }, "pathTrace_you": "Vy", "pathTrace_failed": "Sledovanie cesty zlyhalo.", - "pathTrace_notAvailable": "Path trace nie je k dispozícii.", - "pathTrace_refreshTooltip": "ObnoviÅ¥ Path Trace.", - "contacts_pathTrace": "Sledovanie lúčov", - "contacts_ping": "PingovaÅ¥", - "contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču", - "contacts_repeaterPing": "PingovaÅ¥ opakovač", + "pathTrace_notAvailable": "Path trace nie je k dispozícii.", + "pathTrace_refreshTooltip": "Obnoviť Path Trace.", + "contacts_pathTrace": "Sledovanie lúčov", + "contacts_ping": "Pingovať", + "contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču", + "contacts_repeaterPing": "Pingovať opakovač", "contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti", "contacts_roomPing": "Ping server miestnosti", - "contacts_chatTraceRoute": "SledovaÅ¥ trasu lúča", - "contacts_pathTraceTo": "SledovaÅ¥ trasu k {name}", - "contacts_clipboardEmpty": "Schránka je prázdna.", - "appSettings_languageUk": "Ukrajinská", - "contacts_contactImportFailed": "Kontakt sa nepodarilo importovaÅ¥.", - "contacts_zeroHopAdvert": "Inzerát Zero Hop", - "contacts_floodAdvert": "Inzerát povodní", - "contacts_copyAdvertToClipboard": "KopírovaÅ¥ reklamu do schránky", - "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", - "appSettings_languageRu": "RuÅ¡tina", - "appSettings_enableMessageTracing": "PovoliÅ¥ sledovanie správ", - "appSettings_enableMessageTracingSubtitle": "ZobraziÅ¥ podrobné metadáta o smerovaní a časovaní správ", - "contacts_addContactFromClipboard": "PridaÅ¥ kontakt z schránky", - "contacts_contactImported": "Kontakt bol importovaný.", - "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", - "contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.", - "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", + "contacts_chatTraceRoute": "Sledovať trasu lúča", + "contacts_pathTraceTo": "Sledovať trasu k {name}", + "contacts_clipboardEmpty": "Schránka je prázdna.", + "appSettings_languageUk": "Ukrajinská", + "contacts_contactImportFailed": "Kontakt sa nepodarilo importovať.", + "contacts_zeroHopAdvert": "Inzerát Zero Hop", + "contacts_floodAdvert": "Inzerát povodní", + "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", + "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", + "appSettings_languageRu": "Ruština", + "appSettings_enableMessageTracing": "Povoliť sledovanie správ", + "appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ", + "contacts_addContactFromClipboard": "Pridať kontakt z schránky", + "contacts_contactImported": "Kontakt bol importovaný.", + "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", + "contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.", + "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", - "contacts_ShareContactZeroHop": "ZdieľaÅ¥ kontakt cez inzerát", - "contacts_ShareContact": "KopírovaÅ¥ kontakt do schránky", + "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", + "contacts_ShareContact": "Kopírovať kontakt do schránky", "notification_activityTitle": "Aktivita MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}", - "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", - "notification_newTypeDiscovered": "Nový {contactType} objavený", - "notification_receivedNewMessage": "Prijatá nová správa", - "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", + "notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}", + "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", + "notification_newTypeDiscovered": "Nový {contactType} objavený", + "notification_receivedNewMessage": "Prijatá nová správa", + "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", "settings_gpxExportContacts": "Export sprievodcov do GPX", - "settings_gpxExportSuccess": "ÚspeÅ¡ne exportovaný súbor GPX.", - "settings_gpxExportNoContacts": "Žiadne kontakty na export.", - "settings_gpxExportNotAvailable": "Nie je podporované na vaÅ¡om zariadení/operáciomnom systéme", - "settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností", - "settings_gpxExportError": "Vyskytol sa chyba počas exportu.", - "settings_gpxExportAllSubtitle": "Exportuje vÅ¡etky kontakty s lokalitou do súboru GPX.", - "settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.", - "settings_gpxExportRepeaters": "ExportovaÅ¥ repeater / server miestnosti do GPX", - "settings_gpxExportAll": "ExportovaÅ¥ vÅ¡etky kontakty do GPX", - "settings_gpxExportAllContacts": "VÅ¡etky kontaktné lokality", - "settings_gpxExportChat": "Lokácie sprievodcov", - "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", - "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!", - "pathTrace_clearTooltip": "ZmazaÅ¥ cestu", + "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", + "settings_gpxExportNoContacts": "Žiadne kontakty na export.", + "settings_gpxExportNotAvailable": "Nie je podporované na vašom zariadení/operáciomnom systéme", + "settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností", + "settings_gpxExportError": "Vyskytol sa chyba počas exportu.", + "settings_gpxExportAllSubtitle": "Exportuje všetky kontakty s lokalitou do súboru GPX.", + "settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.", + "settings_gpxExportRepeaters": "Exportovať repeater / server miestnosti do GPX", + "settings_gpxExportAll": "Exportovať všetky kontakty do GPX", + "settings_gpxExportAllContacts": "Všetky kontaktné lokality", + "settings_gpxExportChat": "Lokácie sprievodcov", + "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", + "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", + "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!", + "pathTrace_clearTooltip": "Zmazať cestu", "map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.", - "map_removeLast": "OdstrániÅ¥ posledný", - "map_runTrace": "SpustiÅ¥ trasovaním cesty", - "map_pathTraceCancelled": "ZruÅ¡enie stopáže cesty bolo zruÅ¡ené.", - "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovaÅ¥ pre zariadenia.", - "scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome", - "scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.", - "scanner_bluetoothOff": "Bluetooth je vypnutý", + "map_removeLast": "Odstrániť posledný", + "map_runTrace": "Spustiť trasovaním cesty", + "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", + "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", + "scanner_chromeRequired": "Vyžaduje sa prehliadač Chrome", + "scanner_chromeRequiredMessage": "Táto webová aplikácia vyžaduje Google Chrome alebo prehliadač založený na Chromium pre podporu Bluetooth.", + "scanner_bluetoothOff": "Bluetooth je vypnutý", "scanner_enableBluetooth": "Povolte Bluetooth", - "snrIndicator_lastSeen": "Naposledy videný", - "snrIndicator_nearByRepeaters": "Miestne opakovače", - "chat_ShowAllPaths": "ZobraziÅ¥ vÅ¡etky cesty", - "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", - "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", - "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.", - "settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)", + "snrIndicator_lastSeen": "Naposledy videný", + "snrIndicator_nearByRepeaters": "Miestne opakovače", + "chat_ShowAllPaths": "Zobraziť všetky cesty", + "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", + "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", + "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.", + "settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Jednotky", - "appSettings_unitsMetric": "Metrické (m / km)", - "appSettings_unitsImperial": "Imperiálne (ft / mi)", + "appSettings_unitsMetric": "Metrické (m / km)", + "appSettings_unitsImperial": "Imperiálne (ft / mi)", "map_lineOfSight": "Line of Sight", "map_losScreenTitle": "Line of Sight", - "losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.", - "losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}", + "losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.", + "losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,13 +1629,13 @@ } } }, - "losClearAllPoints": "VymazaÅ¥ vÅ¡etky body", - "losRunToViewElevationProfile": "Ak chcete zobraziÅ¥ výškový profil, spustite LOS", + "losClearAllPoints": "Vymazať všetky body", + "losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS", "losMenuTitle": "Menu LOS", - "losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body", - "losShowDisplayNodes": "ZobraziÅ¥ uzly zobrazenia", - "losCustomPoints": "Vlastné body", - "losCustomPointLabel": "Vlastné {index}", + "losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body", + "losShowDisplayNodes": "Zobraziť uzly zobrazenia", + "losCustomPoints": "Vlastné body", + "losCustomPointLabel": "Vlastné {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1645,7 +1645,7 @@ }, "losPointA": "Bod A", "losPointB": "Bod B", - "losAntennaA": "Anténa A: {value} {unit}", + "losAntennaA": "Anténa A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Anténa B: {value} {unit}", + "losAntennaB": "Anténa B: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1668,8 +1668,8 @@ } }, "losRun": "Spustite LOS", - "losNoElevationData": "Žiadne údaje o nadmorskej výške", - "losProfileClear": "{distance} {distanceUnit}, vymazaÅ¥ LOS, min. vôľa {clearance} {heightUnit}", + "losNoElevationData": "Žiadne údaje o nadmorskej výške", + "losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1704,8 +1704,8 @@ } }, "losStatusChecking": "LOS: kontrolujem...", - "losStatusNoData": "LOS: žiadne údaje", - "losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme", + "losStatusNoData": "LOS: žiadne údaje", + "losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.", - "losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.", - "losRenameCustomPoint": "PremenovaÅ¥ vlastný bod", - "losPointName": "Názov bodu", - "losShowPanelTooltip": "ZobraziÅ¥ panel LOS", - "losHidePanelTooltip": "SkryÅ¥ panel LOS", - "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Rádiový horizont", - "losLegendLosBeam": "Priama viditeľnosÅ¥", - "losLegendTerrain": "Terén", + "losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.", + "losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.", + "losRenameCustomPoint": "Premenovať vlastný bod", + "losPointName": "Názov bodu", + "losShowPanelTooltip": "Zobraziť panel LOS", + "losHidePanelTooltip": "Skryť panel LOS", + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádiový horizont", + "losLegendLosBeam": "Priama viditeľnosť", + "losLegendTerrain": "Terén", "losFrequencyLabel": "Frekvencia", - "losFrequencyInfoTooltip": "ZobraziÅ¥ podrobnosti výpočtu", - "losFrequencyDialogTitle": "Výpočet rádiového horizontu", - "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", + "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", + "losFrequencyDialogTitle": "Výpočet rádiového horizontu", + "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_removeFromFavorites": "OdstrániÅ¥ z označení", - "listFilter_addToFavorites": "Pridaj do obľúbených", - "listFilter_favorites": "Obľúbené", + "listFilter_removeFromFavorites": "Odstrániť z označení", + "listFilter_addToFavorites": "Pridaj do obľúbených", + "listFilter_favorites": "Obľúbené", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1796,17 +1796,29 @@ } } }, - "contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...", - "contacts_searchFavorites": "HľadaÅ¥ {number}{str} obľúbené...", - "contacts_searchRepeaters": "HľadaÅ¥ {number}{str} opakovače...", - "contacts_searchUsers": "HľadaÅ¥ {number}{str} používateľov...", - "contacts_searchContactsNoNumber": "HľadaÅ¥ kontakty...", - "contacts_unread": "Neprečítané", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB", + "contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...", + "contacts_searchFavorites": "Hľadať {number}{str} obľúbené...", + "contacts_searchRepeaters": "Hľadať {number}{str} opakovače...", + "contacts_searchUsers": "Hľadať {number}{str} používateľov...", + "contacts_searchContactsNoNumber": "Hľadať kontakty...", + "contacts_unread": "Neprečítané", "usbScreenStatus": "Vyberte USB zariadenie", - "usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vaÅ¡ej MeshCore uzlu.", - "usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.", "usbScreenTitle": "Pripojte cez USB", - "usbScreenEmptyState": "NenaÅ¡li sa žiadne USB zariadenia. Pripojte jedno a obnovte." + "usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.", + "usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.", + "usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.", + "usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.", + "usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.", + "usbErrorInvalidPort": "Vyberte platné USB zariadenie.", + "usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v prebiehajúcom procese.", + "usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.", + "usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.", + "usbErrorConnectFailed": "Nezvládlo sa pripojenie k vybranému USB zariadeniu.", + "usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.", + "usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.", + "usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.", + "usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.", + "usbErrorConnectTimedOut": "Čakal som, kým sa zariadenie neozvými, ale časový limit sa dosiahol.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index b9bc264..ec09ee1 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", +{ + "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -12,8 +12,8 @@ "nav_contacts": "Stiki", "nav_channels": "Kanali", "nav_map": "Karta", - "common_cancel": "Prekliči", - "common_connect": "Poveži se", + "common_cancel": "Prekliči", + "common_connect": "Poveži se", "common_unknownDevice": "Nepoznano naprave", "common_save": "Shrani", "common_delete": "Izbrisati", @@ -31,11 +31,11 @@ "common_retry": "Ponoviti", "common_hide": "Skrita", "common_remove": "Izbrisati", - "common_enable": "Omogoči", + "common_enable": "Omogoči", "common_disable": "Izklopiti", "common_reboot": "Ponoviti", - "common_loading": "Naložanje...", - "common_notAvailable": "—", + "common_loading": "Naložanje...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -66,8 +66,8 @@ } }, "scanner_searchingDevices": "Iskanje naprav MeshCore...", - "scanner_tapToScan": "NagneÅ¡ na skeniranje za najdene naprave MeshCore.", - "scanner_connectionFailed": "PoÅ¡lo je z povezavo: {error}", + "scanner_tapToScan": "Nagneš na skeniranje za najdene naprave MeshCore.", + "scanner_connectionFailed": "Pošlo je z povezavo: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,51 +75,51 @@ } } }, - "scanner_stop": "Prekliči", + "scanner_stop": "Prekliči", "scanner_scan": "Skeniraj", "device_quickSwitch": "Hitro preklop", "device_meshcore": "MeshCore", "settings_title": "Nastavitve", "settings_deviceInfo": "Informacije o napravei", "settings_appSettings": "Nastavitve aplikacije", - "settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.", - "settings_nodeSettings": "Nastavitev časa", + "settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.", + "settings_nodeSettings": "Nastavitev časa", "settings_nodeName": "Ime node-a", "settings_nodeNameNotSet": "Ni nastavljeno", "settings_nodeNameHint": "Vnesite ime node-a", "settings_nodeNameUpdated": "Ime posodobljeno", "settings_radioSettings": "Nastavitve radija", - "settings_radioSettingsSubtitle": "Frekvenca, moč, razÅ¡iritveni faktor", + "settings_radioSettingsSubtitle": "Frekvenca, moč, razširitveni faktor", "settings_radioSettingsUpdated": "Radio nastavitve posodobljene", "settings_location": "Lokacija", "settings_locationSubtitle": "GPS koordinate", "settings_locationUpdated": "Lokacija posodobljena", - "settings_locationBothRequired": "Vnesite Å¡irino in dolžino.", - "settings_locationInvalid": "Neveljavna zemeljska Å¡irina ali dolžina.", - "settings_latitude": "Å irina", - "settings_longitude": "Dolžina", + "settings_locationBothRequired": "Vnesite širino in dolžino.", + "settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.", + "settings_latitude": "Širina", + "settings_longitude": "Dolžina", "settings_privacyMode": "Zasebnost", "settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", - "settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.", - "settings_privacyModeEnabled": "Privatni način je omogočen.", - "settings_privacyModeDisabled": "Privatni način je onemogočen.", + "settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.", + "settings_privacyModeEnabled": "Privatni način je omogočen.", + "settings_privacyModeDisabled": "Privatni način je onemogočen.", "settings_actions": "Akcije", - "settings_sendAdvertisement": "PoÅ¡lji Oglas", + "settings_sendAdvertisement": "Pošlji Oglas", "settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah", "settings_advertisementSent": "Oglas poslan", "settings_syncTime": "Nastavi uro", - "settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona", + "settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona", "settings_timeSynchronized": "Ura sinhronizirana", - "settings_refreshContacts": "Ponovno obišči kontakte", - "settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi", + "settings_refreshContacts": "Ponovno obišči kontakte", + "settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi", "settings_rebootDevice": "Ponovni zagon naprave", - "settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo", - "settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.", + "settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo", + "settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.", "settings_debug": "Debug", - "settings_bleDebugLog": "BLE debug log (razhroščevanje)", + "settings_bleDebugLog": "BLE debug log (razhroščevanje)", "settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki", "settings_appDebugLog": "Logi aplikacije", - "settings_appDebugLogSubtitle": "Debug sporočila aplikacije", + "settings_appDebugLogSubtitle": "Debug sporočila aplikacije", "settings_about": "Oglejte si", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -130,24 +130,24 @@ } }, "settings_aboutLegalese": "Odprtokodni projekt MeshCore 2024", - "settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.", + "settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.", "settings_infoName": "Ime", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Baterija", - "settings_infoPublicKey": "Javni ključ", - "settings_infoContactsCount": "Å tevilo stikov", - "settings_infoChannelCount": "Å tevilo kanalov", + "settings_infoPublicKey": "Javni ključ", + "settings_infoContactsCount": "Število stikov", + "settings_infoChannelCount": "Število kanalov", "settings_presets": "Prednastavitve", "settings_frequency": "Frekvenca (MHz)", "settings_frequencyHelper": "300,00 - 2500,00", "settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)", - "settings_bandwidth": "Pasovna Å¡irina", - "settings_spreadingFactor": "RazÅ¡iritveni faktor", + "settings_bandwidth": "Pasovna širina", + "settings_spreadingFactor": "Razširitveni faktor", "settings_codingRate": "Programska hitrost", - "settings_txPower": "TX Moč (dBm)", + "settings_txPower": "TX Moč (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)", + "settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)", "settings_error": "Napaka: {message}", "@settings_error": { "placeholders": { @@ -157,7 +157,7 @@ } }, "appSettings_title": "Nastavitve aplikacije", - "appSettings_appearance": "Prikaži", + "appSettings_appearance": "Prikaži", "appSettings_theme": "Tema", "appSettings_themeSystem": "Sistemska tema", "appSettings_themeLight": "Svetlo", @@ -165,39 +165,39 @@ "appSettings_language": "Jezik", "appSettings_languageSystem": "Sistemska privzeta vrednost", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Obvestila", - "appSettings_enableNotifications": "Omogoči obvestila", - "appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih", + "appSettings_enableNotifications": "Omogoči obvestila", + "appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih", "appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena", - "appSettings_notificationsEnabled": "Obvestila omogočena", + "appSettings_notificationsEnabled": "Obvestila omogočena", "appSettings_notificationsDisabled": "Obvestila so izklopljena", "appSettings_messageNotifications": "Obvestila", - "appSettings_messageNotificationsSubtitle": "Pokaži obvestilo ob prejemu novih sporočil.", - "appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala", - "appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala", + "appSettings_messageNotificationsSubtitle": "Pokaži obvestilo ob prejemu novih sporočil.", + "appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala", + "appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala", "appSettings_advertisementNotifications": "Opozorila o oglasih", - "appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.", + "appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.", "appSettings_messaging": "Komuniciranje", - "appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem Å¡tetju", - "appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspeÅ¡nih poskusih poÅ¡iljanja", - "appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspeÅ¡nih poskusih.", - "appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.", + "appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju", + "appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja", + "appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspešnih poskusih.", + "appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.", "appSettings_autoRouteRotation": "Avtomatsko rotacija prenosne poti", - "appSettings_autoRouteRotationSubtitle": "Menjaj med boljÅ¡o potjo in flood načinom", - "appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno", - "appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno", + "appSettings_autoRouteRotationSubtitle": "Menjaj med boljšo potjo in flood načinom", + "appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno", + "appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno", "appSettings_battery": "Baterija", "appSettings_batteryChemistry": "Kemija baterije", "appSettings_batteryChemistryPerDevice": "Nastavitev za napravo ({deviceName})", @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo", + "appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo", "appSettings_batteryNmc": "18650 NMC (3,0-4,2V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Prikaz zemljevida", - "appSettings_showRepeaters": "Prikaži repetitorje", - "appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi", - "appSettings_showChatNodes": "Prikaži naprave za klepet", - "appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu", - "appSettings_showOtherNodes": "Pokaži druge naprave", - "appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.", - "appSettings_timeFilter": "Filter po času", - "appSettings_timeFilterShowAll": "Pokaži vse naprave", - "appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah", + "appSettings_showRepeaters": "Prikaži repetitorje", + "appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi", + "appSettings_showChatNodes": "Prikaži naprave za klepet", + "appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu", + "appSettings_showOtherNodes": "Pokaži druge naprave", + "appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.", + "appSettings_timeFilter": "Filter po času", + "appSettings_timeFilterShowAll": "Pokaži vse naprave", + "appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -229,16 +229,16 @@ } } }, - "appSettings_mapTimeFilter": "Filter časa na zemljevidu", - "appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:", + "appSettings_mapTimeFilter": "Filter časa na zemljevidu", + "appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:", "appSettings_allTime": "Brez omejitev", "appSettings_lastHour": "V zadnji uri", "appSettings_last6Hours": "Zadnjih 6 ur", "appSettings_last24Hours": "Zadnjih 24 ur", - "appSettings_lastWeek": "PrejÅ¡nji teden", + "appSettings_lastWeek": "Prejšnji teden", "appSettings_offlineMapCache": "Shramba zemljevidov brez povezave", - "appSettings_noAreaSelected": "Območje ni izbrano", - "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", + "appSettings_noAreaSelected": "Območje ni izbrano", + "appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,19 +249,19 @@ } } }, - "appSettings_debugCard": "Razhroščevanje", + "appSettings_debugCard": "Razhroščevanje", "appSettings_appDebugLogging": "Programski dnevnik", - "appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav", - "appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno", - "appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.", + "appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav", + "appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno", + "appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.", "contacts_title": "Stiki", "contacts_noContacts": "Ni stikov.", "contacts_contactsWillAppear": "Stiki se bodo prikazali, ko se naprave oglasijo.", "contacts_searchContacts": "Iskanje stikov...", "contacts_noUnreadContacts": "Ne prebrani stiki.", "contacts_noContactsFound": "Stiki niso najdeni.", - "contacts_deleteContact": "IzbriÅ¡i stik", - "contacts_removeConfirm": "IzbriÅ¡em {contactName} iz stikov?", + "contacts_deleteContact": "Izbriši stik", + "contacts_removeConfirm": "Izbrišem {contactName} iz stikov?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -273,8 +273,8 @@ "contacts_roomLogin": "Prijava v sobo", "contacts_openChat": "Odpri klepet", "contacts_editGroup": "Uredi skupino", - "contacts_deleteGroup": "IzbriÅ¡i skupino", - "contacts_deleteGroupConfirm": "IzbriÅ¡i {groupName}?", + "contacts_deleteGroup": "Izbriši skupino", + "contacts_deleteGroupConfirm": "Izbriši {groupName}?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -285,7 +285,7 @@ "contacts_newGroup": "Nova skupina", "contacts_groupName": "Ime skupine", "contacts_groupNameRequired": "Ime skupine je obvezno.", - "contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja", + "contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,10 +294,10 @@ } }, "contacts_filterContacts": "Filtriraj stik\\,...", - "contacts_noContactsMatchFilter": "Noben stik ne ustreza vaÅ¡emu kriteriju.", - "contacts_noMembers": "Ni članov.", + "contacts_noContactsMatchFilter": "Noben stik ne ustreza vašemu kriteriju.", + "contacts_noMembers": "Ni članov.", "contacts_lastSeenNow": "Nazadnje viden zdaj", - "contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami", + "contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -305,8 +305,8 @@ } } }, - "contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.", - "contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami", + "contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.", + "contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -314,8 +314,8 @@ } } }, - "contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem", - "contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem", + "contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem", + "contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -324,9 +324,9 @@ } }, "channels_title": "Kanali", - "channels_noChannelsConfigured": "Kanali Å¡e niso konfigurirani", + "channels_noChannelsConfigured": "Kanali še niso konfigurirani", "channels_addPublicChannel": "Dodaj javni kanal", - "channels_searchChannels": "Poišči kanale...", + "channels_searchChannels": "Poišči kanale...", "channels_noChannelsFound": "Ne najdem kanalov.", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { @@ -342,10 +342,10 @@ "channels_publicChannel": "Javni kanal", "channels_privateChannel": "Zasebni kanal", "channels_editChannel": "Uredi kanal", - "channels_muteChannel": "UtiÅ¡aj kanal", + "channels_muteChannel": "Utišaj kanal", "channels_unmuteChannel": "Vklopi obvestila kanala", - "channels_deleteChannel": "PoÅ¡lji kanal", - "channels_deleteChannelConfirm": "IzbriÅ¡em \"{name}\"? To se ne da povrniti.", + "channels_deleteChannel": "Pošlji kanal", + "channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -366,8 +366,8 @@ "channels_channelName": "Ime kanala", "channels_usePublicChannel": "Uporabi javni kanal", "channels_standardPublicPsk": "Standardni javni PSK", - "channels_pskHex": "PSK (Å estnajstbinska)", - "channels_generateRandomPsk": "Generiraj naključni PSK", + "channels_pskHex": "PSK (Šestnajstbinska)", + "channels_generateRandomPsk": "Generiraj naključni PSK", "channels_enterChannelName": "Vnesi ime kanala", "channels_pskMustBe32Hex": "PSK mora biti 32 heksadecimalnih znakov.", "channels_channelAdded": "Kanal \"{name}\" dodan", @@ -397,13 +397,13 @@ }, "channels_publicChannelAdded": "javna skupnost dodana", "channels_sortBy": "Sortiraj po", - "channels_sortManual": "Ročno", + "channels_sortManual": "Ročno", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "NajnovejÅ¡e sporočilo", - "channels_sortUnread": "NereÅ¡eno", - "chat_noMessages": "Å e ni sporočil.", - "chat_sendMessageToStart": "PoÅ¡lji sporočilo za začetek.", - "chat_originalMessageNotFound": "Opozorilo: Sporočilo ni bilo najdeno", + "channels_sortLatestMessages": "Najnovejše sporočilo", + "channels_sortUnread": "Nerešeno", + "chat_noMessages": "Še ni sporočil.", + "chat_sendMessageToStart": "Pošlji sporočilo za začetek.", + "chat_originalMessageNotFound": "Opozorilo: Sporočilo ni bilo najdeno", "chat_replyingTo": "Odgovarjanje {name}", "@chat_replyingTo": { "placeholders": { @@ -412,7 +412,7 @@ } } }, - "chat_replyTo": "OdpoÅ¡lji odgovor {name}", + "chat_replyTo": "Odpošlji odgovor {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,7 +421,7 @@ } }, "chat_location": "Lokacija", - "chat_sendMessageTo": "PoÅ¡lji sporočilo {contactName}", + "chat_sendMessageTo": "Pošlji sporočilo {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -429,8 +429,8 @@ } } }, - "chat_typeMessage": "Vnesi sporočilo...", - "chat_messageTooLong": "PoÅ¡iljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).", + "chat_typeMessage": "Vnesi sporočilo...", + "chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -438,8 +438,8 @@ } } }, - "chat_messageCopied": "Sporočilo poslano", - "chat_messageDeleted": "Sporočilo izbrisano", + "chat_messageCopied": "Sporočilo poslano", + "chat_messageDeleted": "Sporočilo izbrisano", "chat_retryingMessage": "Ponovni poskus.", "chat_retryCount": "Ponovit {current}/{max}", "@chat_retryCount": { @@ -452,7 +452,7 @@ } } }, - "chat_sendGif": "PoÅ¡lji GIF", + "chat_sendGif": "Pošlji GIF", "chat_reply": "Odgovori", "chat_addReaction": "Dodaj reakcijo", "chat_me": "jaz", @@ -461,22 +461,22 @@ "emojiCategoryHearts": "Srce", "emojiCategoryObjects": "Predmeti", "gifPicker_title": "Izberi GIF", - "gifPicker_searchHint": "Išči GIF-e...", + "gifPicker_searchHint": "Išči GIF-e...", "gifPicker_poweredBy": "Napredno z GIPHY", "gifPicker_noGifsFound": "Ne najdem GIF-ov.", - "gifPicker_failedLoad": "NeuspeÅ¡no nalaganje GIF-a", - "gifPicker_failedSearch": "Iskanje neuspeÅ¡no.", + "gifPicker_failedLoad": "Neuspešno nalaganje GIF-a", + "gifPicker_failedSearch": "Iskanje neuspešno.", "gifPicker_noInternet": "Ni internetne povezave", "debugLog_appTitle": "Log zapiske aplikacije", "debugLog_bleTitle": "Log zapis BLE", "debugLog_copyLog": "Kopiraj dnevnik", - "debugLog_clearLog": "BriÅ¡i log", - "debugLog_copied": "Beležka kopirana.", - "debugLog_bleCopied": "Kopirana beležka iz BLE", + "debugLog_clearLog": "Briši log", + "debugLog_copied": "Beležka kopirana.", + "debugLog_bleCopied": "Kopirana beležka iz BLE", "debugLog_noEntries": "Ni ustvarjenih debug zapisov.", - "debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije", + "debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije", "debugLog_frames": "Okvirji", - "debugLog_rawLogRx": "Svež Log-RX", + "debugLog_rawLogRx": "Svež Log-RX", "debugLog_noBleActivity": "Ni BLE aktivnosti.", "debugFrame_length": "Izhodni rob: {count} bajtov", "@debugFrame_length": { @@ -495,7 +495,7 @@ } }, "debugFrame_textMessageHeader": "Obvestilo:", - "debugFrame_destinationPubKey": "- Destinirano Ključno Besedilo: {pubKey}", + "debugFrame_destinationPubKey": "- Destinirano Ključno Besedilo: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- ÄŒasovnik: {timestamp}", + "debugFrame_timestamp": "- Časovnik: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -542,11 +542,11 @@ }, "debugFrame_hexDump": "Hex Dump:", "chat_pathManagement": "Upravljanje poti", - "chat_routingMode": "Navodilo za usmerjevalni način", + "chat_routingMode": "Navodilo za usmerjevalni način", "chat_autoUseSavedPath": "Avto (uporabi shranjeno pot)", - "chat_forceFloodMode": "Nasilje obvezati v način", + "chat_forceFloodMode": "Nasilje obvezati v način", "chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):", - "chat_pathHistoryFull": "Zapiske o poti so popolni. IzbriÅ¡i vnose, da dodaÅ¡ nove.", + "chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.", "chat_hopSingular": "skok", "chat_hopPlural": "skokov", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -557,19 +557,19 @@ } } }, - "chat_successes": "UspeÅ¡ni", - "chat_removePath": "IzbriÅ¡i pot", - "chat_noPathHistoryYet": "Ni shranjenih poti.\nPoÅ¡lji sporočilo za odkrivanje poti.", + "chat_successes": "Uspešni", + "chat_removePath": "Izbriši pot", + "chat_noPathHistoryYet": "Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.", "chat_pathActions": "Potni ukazi:", "chat_setCustomPath": "Nastavi Prilozeno Pot", - "chat_setCustomPathSubtitle": "Ročno določite potniÅ¡ko pot.", - "chat_clearPath": "Počisti pot", - "chat_clearPathSubtitle": "Ob naslednji poÅ¡iljanju znova zbrati.", - "chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.", + "chat_setCustomPathSubtitle": "Ročno določite potniško pot.", + "chat_clearPath": "Počisti pot", + "chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.", + "chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.", "chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.", - "chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.", + "chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.", "chat_fullPath": "Polna pot", - "chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.", + "chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.", "chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { @@ -581,15 +581,15 @@ } } }, - "chat_pathSavedLocally": "Shrano lokalno. Povežite se za sinhronizacijo.", + "chat_pathSavedLocally": "Shrano lokalno. Povežite se za sinhronizacijo.", "chat_pathDeviceConfirmed": "Naprave potrjeno.", - "chat_pathDeviceNotConfirmed": "Naprave Å¡e niso potrdile.", + "chat_pathDeviceNotConfirmed": "Naprave še niso potrdile.", "chat_type": "Vnesite", "chat_path": "Pot", - "chat_publicKey": "Ključ javnega tipa", - "chat_compressOutgoingMessages": "Stisnite izhodne sporočila", + "chat_publicKey": "Ključ javnega tipa", + "chat_compressOutgoingMessages": "Stisnite izhodne sporočila", "chat_floodForced": "Porolni (nasilje).", - "chat_directForced": "NezglaÅ¡en (nasilje)", + "chat_directForced": "Nezglašen (nasilje)", "chat_hopsForced": "{count} skoki (nasilje)", "@chat_hopsForced": { "placeholders": { @@ -600,8 +600,8 @@ }, "chat_floodAuto": "Preplavljenje (avtomatizirano)", "chat_direct": "Neposredni", - "chat_poiShared": "Deljeno točke MN", - "chat_unread": "NereÅ¡eno: {count}", + "chat_poiShared": "Deljeno točke MN", + "chat_unread": "Nerešeno: {count}", "@chat_unread": { "placeholders": { "count": { @@ -610,9 +610,9 @@ } }, "chat_openLink": "Odpreti povezavo?", - "chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?", + "chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?", "chat_open": "Odpri", - "chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}", + "chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -621,9 +621,9 @@ } }, "chat_invalidLink": "Neveljavna oblika povezave", - "map_title": "Mapa omrežja", - "map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.", - "map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.", + "map_title": "Mapa omrežja", + "map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.", + "map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.", "map_nodesCount": "Omize: {count}", "@map_nodesCount": { "placeholders": { @@ -632,7 +632,7 @@ } } }, - "map_pinsCount": "Žigovi: {count}", + "map_pinsCount": "Žigovi: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -640,25 +640,25 @@ } } }, - "map_chat": "ÄŒistemar", + "map_chat": "Čistemar", "map_repeater": "Ponovitelj", "map_room": "Soba", "map_sensor": "Senzor", - "map_pinDm": "Zavežite (DM)", - "map_pinPrivate": "Zasebno označit", + "map_pinDm": "Zavežite (DM)", + "map_pinPrivate": "Zasebno označit", "map_pinPublic": "Oznaka (javna)", - "map_lastSeen": "Zadnjič Zazet", - "map_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", + "map_lastSeen": "Zadnjič Zazet", + "map_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", "map_from": "Od", "map_source": "Vir", "map_flags": "Zapestnice", - "map_shareMarkerHere": "Delite točke tukaj.", + "map_shareMarkerHere": "Delite točke tukaj.", "map_pinLabel": "Oznaka za pritrditev", "map_label": "Oznaka", - "map_pointOfInterest": "Točka zanimivosti", - "map_sendToContact": "PoÅ¡lji v kontakt", - "map_sendToChannel": "PoÅ¡lji v kanal", - "map_noChannelsAvailable": "Nihče kanalov na voljo.", + "map_pointOfInterest": "Točka zanimivosti", + "map_sendToContact": "Pošlji v kontakt", + "map_sendToChannel": "Pošlji v kanal", + "map_noChannelsAvailable": "Nihče kanalov na voljo.", "map_publicLocationShare": "Deljenje javne lokacije", "map_publicLocationShareConfirm": "Kljubite boste delili lokacijo v {channelLabel}. Ta kanal je javno dostopen in vsak, ki ima PSK, ga lahko vidi.", "@map_publicLocationShareConfirm": { @@ -668,26 +668,26 @@ } } }, - "map_connectToShareMarkers": "Povežite se z napravo za deljenje oznak.", - "map_filterNodes": "Filtirirajte člene", + "map_connectToShareMarkers": "Povežite se z napravo za deljenje oznak.", + "map_filterNodes": "Filtirirajte člene", "map_nodeTypes": "Vrste knope", - "map_chatNodes": "ÄŒuti zvezde", + "map_chatNodes": "Čuti zvezde", "map_repeaters": "Ponovljalniki", - "map_otherNodes": "Druge vozlišča", - "map_keyPrefix": "Predpona ključa", - "map_filterByKeyPrefix": "Filtri po predpomniku ključa", - "map_publicKeyPrefix": "Predifika javnega ključa", - "map_markers": "Označitelji", - "map_showSharedMarkers": "Pokaži skupno označenja", + "map_otherNodes": "Druge vozlišča", + "map_keyPrefix": "Predpona ključa", + "map_filterByKeyPrefix": "Filtri po predpomniku ključa", + "map_publicKeyPrefix": "Predifika javnega ključa", + "map_markers": "Označitelji", + "map_showSharedMarkers": "Pokaži skupno označenja", "map_lastSeenTime": "Datum zadnjega vpogleda", "map_sharedPin": "Deljeno naslovno geslo", - "map_joinRoom": "Pridružiti sobo", + "map_joinRoom": "Pridružiti sobo", "map_manageRepeater": "Upravljajte Ponovitve", - "mapCache_title": "Omrezni predpomnilnik zemljeÅ¡kih zemljejevskih slik", - "mapCache_selectAreaFirst": "Izberite območje za prvo predpomnilnik.", - "mapCache_noTilesToDownload": "Nihče slik ne bo naložil za to območje.", - "mapCache_downloadTilesTitle": "Naloži ploščice", - "mapCache_downloadTilesPrompt": "NaložiÅ¥ {count} plošč za uporabo v režimu brez povezave?", + "mapCache_title": "Omrezni predpomnilnik zemljeških zemljejevskih slik", + "mapCache_selectAreaFirst": "Izberite območje za prvo predpomnilnik.", + "mapCache_noTilesToDownload": "Nihče slik ne bo naložil za to območje.", + "mapCache_downloadTilesTitle": "Naloži ploščice", + "mapCache_downloadTilesPrompt": "Naložiť {count} plošč za uporabo v režimu brez povezave?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -695,8 +695,8 @@ } } }, - "mapCache_downloadAction": "Naloži", - "mapCache_cachedTiles": "PospeÅ¡eno shranjeni {count} plošč", + "mapCache_downloadAction": "Naloži", + "mapCache_cachedTiles": "Pospešeno shranjeni {count} plošč", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -704,7 +704,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Shranjeni {downloaded} ploščad ({failed} neuspeÅ¡no)", + "mapCache_cachedTilesWithFailed": "Shranjeni {downloaded} ploščad ({failed} neuspešno)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -715,14 +715,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Ponovite arhiv za offline način", - "mapCache_clearOfflineCachePrompt": "IzbriÅ¡i vse predpomnilnikovane kartografske ploščice?", + "mapCache_clearOfflineCacheTitle": "Ponovite arhiv za offline način", + "mapCache_clearOfflineCachePrompt": "Izbriši vse predpomnilnikovane kartografske ploščice?", "mapCache_offlineCacheCleared": "Omrezni predpomnik je bil izbrisal.", - "mapCache_noAreaSelected": "Nizona označena povrÅ¡ina", + "mapCache_noAreaSelected": "Nizona označena površina", "mapCache_cacheArea": "Omanski prostor", "mapCache_useCurrentView": "Uporabi trenutni prikaz", - "mapCache_zoomRange": "Občutek razpona", - "mapCache_estimatedTiles": "Predvideni ploščadi: {count}", + "mapCache_zoomRange": "Občutek razpona", + "mapCache_estimatedTiles": "Predvideni ploščadi: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -730,7 +730,7 @@ } } }, - "mapCache_downloadedTiles": "Naloženo {completed} / {total}", + "mapCache_downloadedTiles": "Naloženo {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -741,9 +741,9 @@ } } }, - "mapCache_downloadTilesButton": "Naloži ploščice", + "mapCache_downloadTilesButton": "Naloži ploščice", "mapCache_clearCacheButton": "Ponoviti arhiv", - "mapCache_failedDownloads": "PoslovniÅ¡ki izniki: {count}", + "mapCache_failedDownloads": "Poslovniški izniki: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -802,9 +802,9 @@ "time_month": "mesec", "time_months": "mesi", "time_minutes": "minute", - "time_allTime": "Vse časovno obdobje", + "time_allTime": "Vse časovno obdobje", "dialog_disconnect": "Odklopiti", - "dialog_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", + "dialog_disconnectConfirm": "Ste prepričani, da želite se odklopiti s tega naprave?", "login_repeaterLogin": "Ponovni vnos", "login_roomLogin": "Vnos v sobo", "login_password": "Geslo", @@ -814,12 +814,12 @@ "login_repeaterDescription": "Vnesite geslo za ponovljalnik, da dostopite do nastavitev in statusa.", "login_roomDescription": "Vnesite geslo v sobo za dostop do nastavitev in statusa.", "login_routing": "Usmerjanje", - "login_routingMode": "Navodilo za usmerjevalni način", + "login_routingMode": "Navodilo za usmerjevalni način", "login_autoUseSavedPath": "Avto (uporabi shranjeno pot)", - "login_forceFloodMode": "Nasilje obvezati v način", - "login_managePaths": "Upravljajte PotniÅ¡ke Proti", + "login_forceFloodMode": "Nasilje obvezati v način", + "login_managePaths": "Upravljajte Potniške Proti", "login_login": "Prijava", - "login_attempt": "PoskuÅ¡ajo {current}/{max}", + "login_attempt": "Poskušajo {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -830,7 +830,7 @@ } } }, - "login_failed": "Prijava je bila neuspeÅ¡na: {error}", + "login_failed": "Prijava je bila neuspešna: {error}", "@login_failed": { "placeholders": { "error": { @@ -838,8 +838,8 @@ } } }, - "login_failedMessage": "Prijava je bila neuspeÅ¡na. Geslo je napačno ali pa je repetitor nedosegljiv.", - "common_reload": "Ponovno naloži", + "login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.", + "common_reload": "Ponovno naloži", "common_clear": "Ponoviti", "path_currentPath": "Trenutna pot: {path}", "@path_currentPath": { @@ -859,14 +859,14 @@ }, "path_enterCustomPath": "Vnesite prilagojeno pot", "path_currentPathLabel": "Trenutna pot", - "path_hexPrefixInstructions": "Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.", - "path_hexPrefixExample": "Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)", - "path_labelHexPrefixes": "Pot (heksafixne skrajÅ¡ave)", + "path_hexPrefixInstructions": "Vnesite 2-karakterne heksadecimalne prefixe za vsako skopo, ločeno z zvezekami.", + "path_hexPrefixExample": "Primer: A1,F2,3C (vsak notranji element uporablja prvi bajt svojega javnega ključa)", + "path_labelHexPrefixes": "Pot (heksafixne skrajšave)", "path_helperMaxHops": "Maksimalno 64 skokov. Vsak prefiks je 2 heksadecimalna znamenja (1 bajt).", "path_selectFromContacts": "Izberi iz kontaktov:", - "path_noRepeatersFound": "Ne najdenih ponoviteljev ali strežnikov sob.", - "path_customPathsRequire": "Prilojene poti zahtevajo medhodne prenose, ki lahko prenaÅ¡ajo sporočila.", - "path_invalidHexPrefixes": "Neveljačni Å¡esteročlenski prefiksi: {prefixes}", + "path_noRepeatersFound": "Ne najdenih ponoviteljev ali strežnikov sob.", + "path_customPathsRequire": "Prilojene poti zahtevajo medhodne prenose, ki lahko prenašajo sporočila.", + "path_invalidHexPrefixes": "Neveljačni šesteročlenski prefiksi: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -874,7 +874,7 @@ } } }, - "path_tooLong": "Pot je prevelika. Dovoljeno največ 64 skokov.", + "path_tooLong": "Pot je prevelika. Dovoljeno največ 64 skokov.", "path_setPath": "Nastavi Pot", "repeater_management": "Upravljanje ponovitve", "repeater_managementTools": "Upravne orodje", @@ -883,17 +883,17 @@ "repeater_telemetry": "Telemetrija", "repeater_telemetrySubtitle": "Pogledate telemetrijo senzorjev in sistemske statistike", "repeater_cli": "CLI", - "repeater_cliSubtitle": "PoÅ¡lji ukazne povelje na ponovitveno enoto.", + "repeater_cliSubtitle": "Pošlji ukazne povelje na ponovitveno enoto.", "repeater_settings": "Nastavitve", "repeater_settingsSubtitle": "Konfigurirajte parametre ponovitelja", "repeater_statusTitle": "Status ponovitelja", - "repeater_routingMode": "Navodilo za usmerjevalni način", + "repeater_routingMode": "Navodilo za usmerjevalni način", "repeater_autoUseSavedPath": "Avto (uporabi shranjeno pot)", - "repeater_forceFloodMode": "Nasilje obvezati v način", + "repeater_forceFloodMode": "Nasilje obvezati v način", "repeater_pathManagement": "Upravljanje poti", "repeater_refresh": "Ponovno obnavljati", "repeater_statusRequestTimeout": "Zahtev statusa je iztekla.", - "repeater_errorLoadingStatus": "Napaka pri obnaÅ¡anju: {error}", + "repeater_errorLoadingStatus": "Napaka pri obnašanju: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -904,17 +904,17 @@ "repeater_systemInformation": "Informacije o sistemu", "repeater_battery": "Baterija", "repeater_clockAtLogin": "Ure (pri prijavi)", - "repeater_uptime": "ÄŒas delovanja", - "repeater_queueLength": "Dolžina čakalne vrste", + "repeater_uptime": "Čas delovanja", + "repeater_queueLength": "Dolžina čakalne vrste", "repeater_debugFlags": "Nastavitve odpravilnosti", "repeater_radioStatistics": "Radio Statistika", "repeater_lastRssi": "Potredno RSSI", - "repeater_lastSnr": "Nazadnje zabeležena SNR", - "repeater_noiseFloor": "Å umovita raven", + "repeater_lastSnr": "Nazadnje zabeležena SNR", + "repeater_noiseFloor": "Šumovita raven", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", "repeater_packetStatistics": "Statistika paketa", - "repeater_sent": "PoÅ¡ljeno", + "repeater_sent": "Pošljeno", "repeater_received": "Prejeto", "repeater_duplicates": "Duplikati", "repeater_daysHoursMinsSecs": "{days} dni {hours}h {minutes}m {seconds}s", @@ -987,27 +987,27 @@ "repeater_repeaterNameHelper": "Prikaz imena za ta ponovitelj.", "repeater_adminPassword": "Admin geslo", "repeater_adminPasswordHelper": "Polni dostopno geslo", - "repeater_guestPassword": "Geslo gostača", + "repeater_guestPassword": "Geslo gostača", "repeater_guestPasswordHelper": "Odpovedni dostopni geslo", "repeater_radioSettings": "Nastavitve Radija", "repeater_frequencyMhz": "Frekvenca (MHz)", "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX Moč", + "repeater_txPower": "TX Moč", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "Pasovna Å¡irina", - "repeater_spreadingFactor": "RazÅ¡iritveni faktor", + "repeater_bandwidth": "Pasovna širina", + "repeater_spreadingFactor": "Razširitveni faktor", "repeater_codingRate": "Programska hitrost", "repeater_locationSettings": "Nastavitve lokacije", - "repeater_latitude": "Å irina", + "repeater_latitude": "Širina", "repeater_latitudeHelper": "Desetbinske protiure (npr. 37.7749)", - "repeater_longitude": "Dolžina", + "repeater_longitude": "Dolžina", "repeater_longitudeHelper": "Desetbinske protiure (npr. -122,4194)", - "repeater_features": "Značilnosti", + "repeater_features": "Značilnosti", "repeater_packetForwarding": "Usmerjanje paketa", - "repeater_packetForwardingSubtitle": "Omogoči ponovitelja za usmerjanje paketov.", + "repeater_packetForwardingSubtitle": "Omogoči ponovitelja za usmerjanje paketov.", "repeater_guestAccess": "Prijemnik", - "repeater_guestAccessSubtitle": "Omogoči dostop gostom v samo bralni načinu.", - "repeater_privacyMode": "Privatni način", + "repeater_guestAccessSubtitle": "Omogoči dostop gostom v samo bralni načinu.", + "repeater_privacyMode": "Privatni način", "repeater_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", "repeater_advertisementSettings": "Nastavitve oglasnika", "repeater_localAdvertInterval": "Lokalen Oglasovni Razpon", @@ -1028,17 +1028,17 @@ } } }, - "repeater_encryptedAdvertInterval": "Å ifrirana Oglasovalska Trajanje", + "repeater_encryptedAdvertInterval": "Šifrirana Oglasovalska Trajanje", "repeater_dangerZone": "Opozorilo", "repeater_rebootRepeater": "Ponovni zagon Repeaterja", "repeater_rebootRepeaterSubtitle": "Ponovni zagon ponovitelja.", - "repeater_rebootRepeaterConfirm": "Ste prepričani, da želite ponovno zagon tega ponovitelja?", - "repeater_regenerateIdentityKey": "Ponovite Ključ Identnosti", - "repeater_regenerateIdentityKeySubtitle": "Ustvarite novo par javnih/zasebnih ključev", + "repeater_rebootRepeaterConfirm": "Ste prepričani, da želite ponovno zagon tega ponovitelja?", + "repeater_regenerateIdentityKey": "Ponovite Ključ Identnosti", + "repeater_regenerateIdentityKeySubtitle": "Ustvarite novo par javnih/zasebnih ključev", "repeater_regenerateIdentityKeyConfirm": "To bo ustvaril novo identiteto za ponavljalnik. Prijavite se?", - "repeater_eraseFileSystem": "Počisti Sustav Vajah", + "repeater_eraseFileSystem": "Počisti Sustav Vajah", "repeater_eraseFileSystemSubtitle": "Oblikuj datoteko ponovitve sistema", - "repeater_eraseFileSystemConfirm": "OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!", + "repeater_eraseFileSystemConfirm": "OPOZORILO: To bo izbrisal/a vsa dejstva na ponovilu. To ni mogoče povzvrniti!", "repeater_eraseSerialOnly": "Brisanje je na voljo samo preko serijske konzole.", "repeater_commandSent": "Navodilo poslano: {command}", "@repeater_commandSent": { @@ -1048,7 +1048,7 @@ } } }, - "repeater_errorSendingCommand": "Napaka pri poÅ¡iljanju ukaznega: {error}", + "repeater_errorSendingCommand": "Napaka pri pošiljanju ukaznega: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1057,7 +1057,7 @@ } }, "repeater_confirm": "Potrdit", - "repeater_settingsSaved": "Nastavitve so shranjene uspeÅ¡no.", + "repeater_settingsSaved": "Nastavitve so shranjene uspešno.", "repeater_errorSavingSettings": "Napaka pri shranjevanju nastavitev: {error}", "@repeater_errorSavingSettings": { "placeholders": { @@ -1068,11 +1068,11 @@ }, "repeater_refreshBasicSettings": "Ponovno nastavi osnovne nastavitve", "repeater_refreshRadioSettings": "Ponovno Nastavitve Radija", - "repeater_refreshTxPower": "Ponovno nastavi TX moč", + "repeater_refreshTxPower": "Ponovno nastavi TX moč", "repeater_refreshLocationSettings": "Ponovno Nastavi Nastavitve Lokacije", "repeater_refreshPacketForwarding": "Ponovno nastavitve usmerjevanja paketa", "repeater_refreshGuestAccess": "Ponovno nastavitve dostopa gostov", - "repeater_refreshPrivacyMode": "Ponovno aktiviraj način zasebnosti", + "repeater_refreshPrivacyMode": "Ponovno aktiviraj način zasebnosti", "repeater_refreshAdvertisementSettings": "Ponovno nastavi Oglede Oglasi", "repeater_refreshed": "{label} je bil/a posodobljen/a", "@repeater_refreshed": { @@ -1082,7 +1082,7 @@ } } }, - "repeater_errorRefreshing": "Napaka pri osveževanju {label}", + "repeater_errorRefreshing": "Napaka pri osveževanju {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,13 +1091,13 @@ } }, "repeater_cliTitle": "Ponovitelj CLI", - "repeater_debugNextCommand": "Popravi naslednje ukazne možnosti", + "repeater_debugNextCommand": "Popravi naslednje ukazne možnosti", "repeater_commandHelp": "Pomoc", "repeater_clearHistory": "Ponovi zgodovino", - "repeater_noCommandsSent": "Niti ena ukazne povratne informacije Å¡e ni poslana.", + "repeater_noCommandsSent": "Niti ena ukazne povratne informacije še ni poslana.", "repeater_typeCommandOrUseQuick": "Vnesite ukaz spodaj ali uporabite hitre ukaze", "repeater_enterCommandHint": "Vnesite ukaz...", - "repeater_previousCommand": "PrejÅ¡nji ukaz", + "repeater_previousCommand": "Prejšnji ukaz", "repeater_nextCommand": "Naslednja ukazna", "repeater_enterCommandFirst": "Vnesite ukaz najprej", "repeater_cliCommandFrameTitle": "Okno ukazne vrstice", @@ -1113,66 +1113,66 @@ "repeater_cliQuickGetRadio": "Dobiti Radiopravo", "repeater_cliQuickGetTx": "Pridobi TX", "repeater_cliQuickNeighbors": "Sosedi", - "repeater_cliQuickVersion": "Različica", + "repeater_cliQuickVersion": "Različica", "repeater_cliQuickAdvertise": "Oglasite", "repeater_cliQuickClock": "Ura", - "repeater_cliHelpAdvert": "PoÅ¡lje paket oglasov", + "repeater_cliHelpAdvert": "Pošlje paket oglasov", "repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)", - "repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.", + "repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.", "repeater_cliHelpPassword": "Nastavi novo administracijsko geslo za naprave.", - "repeater_cliHelpVersion": "Prikaže različico naprave in datum izrabe strojne opreme.", - "repeater_cliHelpClearStats": "Ponastavi različne statistične Å¡tevke na nič.", - "repeater_cliHelpSetAf": "Nastavi časovni koeficient.", - "repeater_cliHelpSetTx": "Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)", - "repeater_cliHelpSetRepeat": "Omogoči ali onemogoči vlogo ponovitelja za tono.", - "repeater_cliHelpSetAllowReadOnly": "(Osebni strežnik) ÄŒe je 'vklopljeno', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).", - "repeater_cliHelpSetFloodMax": "Nastavi največjo Å¡tevilo skokov za vstopne poplave (če je >= maks, paket ni usmerjen)", - "repeater_cliHelpSetIntThresh": "Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.", - "repeater_cliHelpSetAgcResetInterval": "Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.", - "repeater_cliHelpSetMultiAcks": "Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".", - "repeater_cliHelpSetAdvertInterval": "Nastavi časovno obmesto v minutah za poÅ¡iljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.", - "repeater_cliHelpSetFloodAdvertInterval": "Nastavi časovno obmesto v urah za poÅ¡iljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.", - "repeater_cliHelpSetGuestPassword": "Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi poÅ¡iljajo zahtevo \"Get Stats\")", + "repeater_cliHelpVersion": "Prikaže različico naprave in datum izrabe strojne opreme.", + "repeater_cliHelpClearStats": "Ponastavi različne statistične števke na nič.", + "repeater_cliHelpSetAf": "Nastavi časovni koeficient.", + "repeater_cliHelpSetTx": "Nastavi moč LoRa oddajanja v dBm. (za ponovni zagon za uporabo)", + "repeater_cliHelpSetRepeat": "Omogoči ali onemogoči vlogo ponovitelja za tono.", + "repeater_cliHelpSetAllowReadOnly": "(Osebni strežnik) Če je 'vklopljeno', potem bo dovoljeno prijavo z praznim geslom, vendar ne bo mogoče objaviti v sobo. (samo branje).", + "repeater_cliHelpSetFloodMax": "Nastavi največjo število skokov za vstopne poplave (če je >= maks, paket ni usmerjen)", + "repeater_cliHelpSetIntThresh": "Nastavi Prag Interferencij (v dB). Privzeto je 14. Nastavi na 0 za onemogočitev zaznavanja interferenc kanalov.", + "repeater_cliHelpSetAgcResetInterval": "Nastavi časovno razdaljo za ponovni zagon nadzornika Avtomatske uteži. Nastavi na 0 za onemogočanje.", + "repeater_cliHelpSetMultiAcks": "Omogoči ali onemogoči funkcijo \"dvojakih potrdil\".", + "repeater_cliHelpSetAdvertInterval": "Nastavi časovno obmesto v minutah za pošiljanje lokalnega (brezposrednega) napovednega paketa. Nastavi na 0 za onemogočiti.", + "repeater_cliHelpSetFloodAdvertInterval": "Nastavi časovno obmesto v urah za pošiljanje plovilnega oglasnega paketa. Nastavi na 0 za onemogočanje.", + "repeater_cliHelpSetGuestPassword": "Nastavi/posodobi geslo gosta. (za ponovitve lahko gostov prijavi pošiljajo zahtevo \"Get Stats\")", "repeater_cliHelpSetName": "Nastavi ime oglasnika.", - "repeater_cliHelpSetLat": "Nastavi zemljepisno Å¡irino oglaÅ¡evalskega zemljevida (desetdeljne).", - "repeater_cliHelpSetLon": "Nastavi zemljevidno Å¡irino oglasnika. (desetdelne stopnje)", + "repeater_cliHelpSetLat": "Nastavi zemljepisno širino oglaševalskega zemljevida (desetdeljne).", + "repeater_cliHelpSetLon": "Nastavi zemljevidno širino oglasnika. (desetdelne stopnje)", "repeater_cliHelpSetRadio": "Nastavi popolnoma nove radijske parametre in jih shranjuje v nastavitve. Za uporabo je potrebna \"restart\" ukaz.", - "repeater_cliHelpSetRxDelay": "Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.", - "repeater_cliHelpSetTxDelay": "Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjÅ¡a verjetnost kolizij)", - "repeater_cliHelpSetDirectTxDelay": "Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.", - "repeater_cliHelpSetBridgeEnabled": "Omogoči/Preklopi most.", + "repeater_cliHelpSetRxDelay": "Nastavitve (eksperimentalne) osnova (mora biti > 1 za učinkovanje) za uporabo rahle zakasnitve prejetih paketov, glede na moč signala/rezultat. Nastavite na 0 za onemogočanje.", + "repeater_cliHelpSetTxDelay": "Nastavi faktor, ki se množi s časom delovanja za paket v načinu poplavnega režima in z randomiziranim sistemom slotov, da odvrne njegovo posredovanje. (da se zmanjša verjetnost kolizij)", + "repeater_cliHelpSetDirectTxDelay": "Ima podobno vrednost kot txdelay, vendar jo lahko uporabite za dodajanje naknadnega zamika pri posredovanju paketov v režimu neposredne prevodi.", + "repeater_cliHelpSetBridgeEnabled": "Omogoči/Preklopi most.", "repeater_cliHelpSetBridgeDelay": "Nastavi zamik pred ponovnim poslanjem paketov.", "repeater_cliHelpSetBridgeSource": "Izberite, ali bodo most ponavljali prejeto ali poslan paket.", "repeater_cliHelpSetBridgeBaud": "Nastavi hitrost serijske povezave za mostove rs232.", "repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.", - "repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).", - "repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).", - "repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).", - "repeater_cliHelpGetBridgeType": "DobrodoÅ¡li pri izbiri vrste mostu: brez, rs232, espnow", - "repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.", - "repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.", - "repeater_cliHelpLogErase": "IzbriÅ¡e pakete zapisov iz datotek sistema.", - "repeater_cliHelpNeighbors": "Prikaže seznam drugih ponovnih knopov, do katerih je priÅ¡lo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4", - "repeater_cliHelpNeighborRemove": "IzbriÅ¡e prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.", + "repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).", + "repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).", + "repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).", + "repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow", + "repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.", + "repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.", + "repeater_cliHelpLogErase": "Izbriše pakete zapisov iz datotek sistema.", + "repeater_cliHelpNeighbors": "Prikaže seznam drugih ponovnih knopov, do katerih je prišlo preko brezposrednih oglasov. Vsaka vrstica je id-prefix-hex:timestamp:snr-times-4", + "repeater_cliHelpNeighborRemove": "Izbriše prvo ustreznu postavko (po predpomnilku pubkey (heks),) iz seznama sosedov.", "repeater_cliHelpRegion": "(Serija samo) Navaja vse definirane regije in trenutne poplave dovolilnosti.", - "repeater_cliHelpRegionLoad": "Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s poÅ¡iljanjem praznega reda/ukaza.", - "repeater_cliHelpRegionGet": "Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) 'F'\"", + "repeater_cliHelpRegionLoad": "Opomba: to je posebna več ukazna pozivna operacija. Vsak naslednji ukaz je ime regije (z lezijami za prikaz hierarhije, z enim ustvarjenim razmislom). Zaključena s pošiljanjem praznega reda/ukaza.", + "repeater_cliHelpRegionGet": "Išče regijo s podanimi imenimi prefiksom (ali \"\\\" za globalni obseg). Odgovori se s \"-> regija-ime (rodič-ime) 'F'\"", "repeater_cliHelpRegionPut": "Dodaja ali posodobi regijsko definicijo s podanim imenom.", - "repeater_cliHelpRegionRemove": "IzbriÅ¡e definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)", + "repeater_cliHelpRegionRemove": "Izbriše definicijo regije s podanim imenom. (mora se popolnoma ujemati in ne sme imeti podregij)", "repeater_cliHelpRegionAllowf": "Nastavi dovoljenje 'Nere' za podano regijo. ('' za globalni/dedni obseg)", - "repeater_cliHelpRegionDenyf": "Odstrani dovoljenje 'F'lood' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)", - "repeater_cliHelpRegionHome": "Odgovori z trenutnim 'domovim' območjem. (Opomba je bila Å¡e nujno uporabljena, rezervirano za prihodnost)", + "repeater_cliHelpRegionDenyf": "Odstrani dovoljenje 'F'lood' za podano regijo. (OPOZORILO: na tem koraku ni priporočljivo ga uporabljati na globalnem/dednem obsegu!!)", + "repeater_cliHelpRegionHome": "Odgovori z trenutnim 'domovim' območjem. (Opomba je bila še nujno uporabljena, rezervirano za prihodnost)", "repeater_cliHelpRegionHomeSet": "Nastavi regijo 'domov'.", "repeater_cliHelpRegionSave": "Shrani seznam/ zemljevzemi regij v shranjevanje.", - "repeater_cliHelpGps": "Pokaže status GPS-ja. ÄŒe je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in Å¡tetjem satelitiv.", - "repeater_cliHelpGpsOnOff": "Omogoči/onameni GPS način delovanja.", - "repeater_cliHelpGpsSync": "Sinhronizira čas časa ničala z gps uro.", - "repeater_cliHelpGpsSetLoc": "Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.", - "repeater_cliHelpGpsAdvert": "Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaÅ¡evati lokacijo shranjeno v preferencah", - "repeater_cliHelpGpsAdvertSet": "Nastavi konfiguracijo oglasa na določenem mestu.", + "repeater_cliHelpGps": "Pokaže status GPS-ja. Če je GPS izklopljen, odgovarja samo \"off\", če je vklopljen, odgovarja z \"on\", statusom, \"fix\" in štetjem satelitiv.", + "repeater_cliHelpGpsOnOff": "Omogoči/onameni GPS način delovanja.", + "repeater_cliHelpGpsSync": "Sinhronizira čas časa ničala z gps uro.", + "repeater_cliHelpGpsSetLoc": "Nastavi položaj časa na GPS koordinate in shranjevanje preferencij.", + "repeater_cliHelpGpsAdvert": "Omogoča konfiguracijo oglasi za notranjost člana:\n- none: ne vključevati lokacije v oglasih\n- share: deliti gps lokacijo (iz SensorManager)\n- prefs: oglaševati lokacijo shranjeno v preferencah", + "repeater_cliHelpGpsAdvertSet": "Nastavi konfiguracijo oglasa na določenem mestu.", "repeater_commandsListTitle": "Seznam ukazov", - "repeater_commandsListNote": "Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".", - "repeater_general": "Općenito", + "repeater_commandsListNote": "Opomba: za različne ukaze \"nastavi ...\" obstaja tudi ukaz \"dobi ...\".", + "repeater_general": "Općenito", "repeater_settingsCategory": "Nastavitve", "repeater_bridge": "Most", "repeater_logging": "Logiranje", @@ -1180,10 +1180,10 @@ "repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za repetitorje)", "repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.", "repeater_gpsManagement": "Upravljanje GPS", - "repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vpraÅ¡anji, povezanimi z lokacijo.", - "telemetry_receivedData": "Prejeto Telemetrično podatke", + "repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.", + "telemetry_receivedData": "Prejeto Telemetrično podatke", "telemetry_requestTimeout": "Zahtev telemetrije je iztekla.", - "telemetry_errorLoading": "Napaka pri obnaÅ¡anju telemetrije: {error}", + "telemetry_errorLoading": "Napaka pri obnašanju telemetrije: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1244,17 +1244,17 @@ } }, "channelPath_title": "Pot do paketa", - "channelPath_viewMap": "Prikaži zemljeznico", + "channelPath_viewMap": "Prikaži zemljeznico", "channelPath_otherObservedPaths": "Drugi opazovani poti", "channelPath_repeaterHops": "Skoki ponovitelja", "channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.", - "channelPath_messageDetails": "Podrobnosti sporočila", - "channelPath_senderLabel": "PoÅ¡iljatelj", + "channelPath_messageDetails": "Podrobnosti sporočila", + "channelPath_senderLabel": "Pošiljatelj", "channelPath_timeLabel": "Ura", "channelPath_repeatsLabel": "Ponovitve", "channelPath_pathLabel": "Pot {index}", "channelPath_observedLabel": "Opazovani", - "channelPath_observedPathTitle": "Opazovana pot {index} • {hops}", + "channelPath_observedPathTitle": "Opazovana pot {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1265,7 +1265,7 @@ } } }, - "channelPath_noLocationData": "Nihče ni določil lokacije.", + "channelPath_noLocationData": "Nihče ni določil lokacije.", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1329,7 +1329,7 @@ }, "channelPath_pathLabelTitle": "Pot", "channelPath_observedPathHeader": "Opazovana pot", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1342,10 +1342,10 @@ }, "channelPath_noHopDetailsAvailable": "Niso na voljo podrobnosti o letu.", "channelPath_unknownRepeater": "Nepoznati ponovitelj", - "listFilter_tooltip": "Filtri in vrstiči", + "listFilter_tooltip": "Filtri in vrstiči", "listFilter_sortBy": "Sortiraj po", - "listFilter_latestMessages": "NajnovejÅ¡e sporočilo", - "listFilter_heardRecently": "Nedavno sliÅ¡an", + "listFilter_latestMessages": "Najnovejše sporočilo", + "listFilter_heardRecently": "Nedavno slišan", "listFilter_az": "A-Z", "listFilter_filters": "Filtri", "listFilter_all": "Vse", @@ -1361,23 +1361,23 @@ } } }, - "repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.", + "repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.", "repeater_neighbors": "Sosedi", "neighbors_receivedData": "Prejeto podatke o sosedih", "neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.", - "neighbors_errorLoading": "Napaka pri obnaÅ¡anju sosedov: {error}", + "neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}", "neighbors_repeatersNeighbors": "Ponovitve Sosedi", "neighbors_noData": "Niso na voljo podatki o sosedih.", - "channels_joinPrivateChannel": "Pridružite se zasebni skupini", - "channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.", - "channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.", + "channels_joinPrivateChannel": "Pridružite se zasebni skupini", + "channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.", + "channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.", "channels_createPrivateChannel": "Ustvari zasebno kanal.", - "channels_joinPublicChannel": "Pridružite se javnemu kanalu", - "channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.", - "channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom", - "channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.", + "channels_joinPublicChannel": "Pridružite se javnemu kanalu", + "channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.", + "channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom", + "channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.", "channels_scanQrCode": "Skeniraj QR kodo", - "channels_scanQrCodeComingSoon": "Prihajajoča", + "channels_scanQrCodeComingSoon": "Prihajajoča", "channels_enterHashtag": "Vnesite hashtag", "channels_hashtagHint": "npr. #ekipa", "@neighbors_unknownContact": { @@ -1395,13 +1395,13 @@ } }, "neighbors_unknownContact": "Nepoznano {pubkey}", - "neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.", - "settings_locationGPSEnable": "Omogoči GPS", - "settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.", + "neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.", + "settings_locationGPSEnable": "Omogoči GPS", + "settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.", "settings_locationIntervalSec": "Interval za GPS (Sekunde)", "settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.", - "contacts_manageRoom": "Upravljajte strežnik sobe", - "room_management": "Upravljanje stremlišča", + "contacts_manageRoom": "Upravljajte strežnik sobe", + "room_management": "Upravljanje stremlišča", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1462,32 +1462,32 @@ "community_title": "Skupnost", "common_ok": "V redu", "community_create": "Ustvari skupnost", - "community_joinTitle": "Pridružite se skupnosti", - "community_joinConfirmation": "ŽeliÅ¡ se pridružiti skupnosti \"{name}\"?", + "community_joinTitle": "Pridružite se skupnosti", + "community_joinConfirmation": "Želiš se pridružiti skupnosti \"{name}\"?", "community_scanQr": "Skeniraj QR kode skupnosti", "community_scanInstructions": "Nasmerite kamero s skupnostnim QR kodom.", - "community_showQr": "Pokaži QR kodo", + "community_showQr": "Pokaži QR kodo", "community_publicChannel": "Skupnostna javna", "community_hashtagChannel": "Skupnostni hashtag", "community_name": "Komunitarne ime", "community_enterName": "Vnesite ime skupnosti", - "community_join": "Pridružiti se", + "community_join": "Pridružiti se", "community_created": "Skupnost \"{name}\" je bila ustvarila.", "community_joined": "Prilojen k skupnosti \"{name}\"", "community_qrTitle": "Delite skupnost", - "community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.", - "community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti", + "community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.", + "community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti", "community_invalidQrCode": "Neveljaven QR koden skupnosti", - "community_alreadyMember": "Že član", - "community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.", + "community_alreadyMember": "Že član", + "community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.", "community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja", "community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.", - "community_noCommunities": "Å e nobena skupnost se ni pridružila.", - "community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.", + "community_noCommunities": "Še nobena skupnost se ni pridružila.", + "community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.", "community_manageCommunities": "Upravljanje skupnosti", "community_delete": "Opusti skupnost", "community_deleteConfirm": "Zapusti \"{name}\"?", - "community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.", + "community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1502,7 +1502,7 @@ "community_regularHashtag": "Oznaka s hashtagom", "community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)", "community_communityHashtag": "Skupnostni hashtag", - "community_communityHashtagDesc": "Izključeno za uporabnike skupnosti", + "community_communityHashtagDesc": "Izključeno za uporabnike skupnosti", "community_forCommunity": "Za {name}", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1534,10 +1534,10 @@ }, "community_secretRegenerated": "Geslo za \"{name}\" ponovno ustvarjeno", "community_regenerateSecret": "Ponovno ustvari geslo", - "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.", + "community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.", "community_regenerate": "Preberi znova", - "community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}", - "community_updateSecret": "Ažuriraj ključ", + "community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}", + "community_updateSecret": "Ažuriraj ključ", "community_secretUpdated": "Skrivnostno spremembo za \"{name}\"", "@contacts_pathTraceTo": { "placeholders": { @@ -1549,78 +1549,78 @@ "pathTrace_you": "Ti", "pathTrace_failed": "Sledenje poti ni uspelo.", "pathTrace_notAvailable": "Potni sled ni na voljo.", - "pathTrace_refreshTooltip": "Osveži Path Trace.", + "pathTrace_refreshTooltip": "Osveži Path Trace.", "contacts_pathTrace": "Sledenje poti", "contacts_ping": "Pingati", "contacts_repeaterPathTrace": "Sledi poti do ponavljalnika", "contacts_repeaterPing": "Pinguj ponavljalnik", - "contacts_roomPathTrace": "Sledenje poti do strežnika sobe", - "contacts_roomPing": "Ping strežnik sobe", - "contacts_chatTraceRoute": "Slediti poti žarkov", + "contacts_roomPathTrace": "Sledenje poti do strežnika sobe", + "contacts_roomPing": "Ping strežnik sobe", + "contacts_chatTraceRoute": "Slediti poti žarkov", "contacts_pathTraceTo": "Trace route to {name}", - "appSettings_languageRu": "Ruščina", + "appSettings_languageRu": "Ruščina", "appSettings_languageUk": "Ukrajinsko", - "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", - "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", - "contacts_contactImported": "Kontakt je bil uvožen.", - "contacts_contactImportFailed": "Kontakt ni bil uspeÅ¡no uvožen.", + "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", + "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", + "contacts_contactImported": "Kontakt je bil uvožen.", + "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", "contacts_zeroHopAdvert": "Reklama brez posrednikov", - "contacts_floodAdvert": "Poplavna oglás", + "contacts_floodAdvert": "Poplavna oglás", "contacts_invalidAdvertFormat": "Neveljavni kontaktne podatke", - "contacts_clipboardEmpty": "Odložišče je prazno.", - "contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče", - "contacts_addContactFromClipboard": "Dodaj stik iz odložišča", + "contacts_clipboardEmpty": "Odložišče je prazno.", + "contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče", + "contacts_addContactFromClipboard": "Dodaj stik iz odložišča", "contacts_zeroHopContactAdvertSent": "Poslano po oglasu.", - "contacts_zeroHopContactAdvertFailed": "PoÅ¡iljanje kontakta ni uspelo.", - "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", - "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", + "contacts_zeroHopContactAdvertFailed": "Pošiljanje kontakta ni uspelo.", + "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", + "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", - "contacts_ShareContact": "Kopiraj stik v Odložišče", + "contacts_ShareContact": "Kopiraj stik v Odložišče", "notification_activityTitle": "Aktivnost MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}", - "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", + "notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}", + "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", "notification_newTypeDiscovered": "Odkrito novo {contactType}", - "notification_receivedNewMessage": "Prejeto novo sporočilo", + "notification_receivedNewMessage": "Prejeto novo sporočilo", "settings_gpxExportAll": "Izvozi vse kontakte v GPX", "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", - "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", - "settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX", - "settings_gpxExportError": "Pri izvozu je priÅ¡lo do napake.", - "settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe", + "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", + "settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX", + "settings_gpxExportError": "Pri izvozu je prišlo do napake.", + "settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe", "settings_gpxExportChat": "Lokacije spremljevalcev", "settings_gpxExportAllContacts": "Lokacije vseh stikov", "settings_gpxExportContactsSubtitle": "Izvozi spremljevalce z lokacijo v datoteko GPX.", "settings_gpxExportAllSubtitle": "Izvozi vse kontakte z lokacijo v datoteko GPX.", - "settings_gpxExportSuccess": "UspeÅ¡no izvoz GPX datoteke.", - "settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open", + "settings_gpxExportSuccess": "Uspešno izvoz GPX datoteke.", + "settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open", "settings_gpxExportNoContacts": "Ni stikov za izvoz.", - "settings_gpxExportNotAvailable": "Ni podprto na vaÅ¡em napravi/operacijskem sistemu", + "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte", - "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!", - "map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.", + "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!", + "map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.", "map_removeLast": "Odstrani Zadnji", - "map_runTrace": "Zaženi sledenje poti", - "pathTrace_clearTooltip": "Počisti pot", + "map_runTrace": "Zaženi sledenje poti", + "pathTrace_clearTooltip": "Počisti pot", "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", - "scanner_enableBluetooth": "Omogočite Bluetooth", - "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", + "scanner_enableBluetooth": "Omogočite Bluetooth", + "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", "scanner_chromeRequired": "Zahtevan brskalnik Chrome", "scanner_chromeRequiredMessage": "Ta spletna aplikacija za podporo Bluetooth zahteva Google Chrome ali brskalnik na osnovi Chromiuma.", "scanner_bluetoothOff": "Bluetooth je izklopljen", - "snrIndicator_lastSeen": "Zadnjič videno", - "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", - "chat_ShowAllPaths": "Prikaži vse poti", - "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", - "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", + "snrIndicator_lastSeen": "Zadnjič videno", + "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", + "chat_ShowAllPaths": "Prikaži vse poti", + "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", + "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", "settings_clientRepeat": "Neovadno ponavljanje", - "settings_aboutOpenMeteoAttribution": "Podatki o viÅ¡ini LOS: Open-Meteo (CC BY 4.0)", + "settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Enote", - "appSettings_unitsMetric": "Metrična (m/km)", + "appSettings_unitsMetric": "Metrična (m/km)", "appSettings_unitsImperial": "Imperialno (ft / mi)", "map_lineOfSight": "Linija vida", "map_losScreenTitle": "Linija vida", - "losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.", + "losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.", "losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}", "@losRunFailed": { "placeholders": { @@ -1629,12 +1629,12 @@ } } }, - "losClearAllPoints": "Počisti vse točke", - "losRunToViewElevationProfile": "Zaženite LOS za ogled viÅ¡inskega profila", + "losClearAllPoints": "Počisti vse točke", + "losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila", "losMenuTitle": "LOS meni", - "losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri", - "losShowDisplayNodes": "Pokaži prikazna vozlišča", - "losCustomPoints": "Točke po meri", + "losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri", + "losShowDisplayNodes": "Pokaži prikazna vozlišča", + "losCustomPoints": "Točke po meri", "losCustomPointLabel": "Po meri {index}", "@losCustomPointLabel": { "placeholders": { @@ -1643,8 +1643,8 @@ } } }, - "losPointA": "Točka A", - "losPointB": "Točka B", + "losPointA": "Točka A", + "losPointB": "Točka B", "losAntennaA": "Antena A: {value} {unit}", "@losAntennaA": { "placeholders": { @@ -1667,9 +1667,9 @@ } } }, - "losRun": "Zaženi LOS", - "losNoElevationData": "Ni podatkov o viÅ¡ini", - "losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjÅ¡a razdalja {clearance} {heightUnit}", + "losRun": "Zaženi LOS", + "losNoElevationData": "Ni podatkov o višini", + "losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Podatki o nadmorski viÅ¡ini niso na voljo za enega ali več vzorcev.", - "losErrorInvalidInput": "Neveljavni podatki o točkah/viÅ¡ini za izračun LOS.", - "losRenameCustomPoint": "Preimenujte točko po meri", - "losPointName": "Ime točke", - "losShowPanelTooltip": "Pokaži ploščo LOS", - "losHidePanelTooltip": "Skrij ploščo LOS", - "losElevationAttribution": "Podatki o viÅ¡ini: Open-Meteo (CC BY 4.0)", + "losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.", + "losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.", + "losRenameCustomPoint": "Preimenujte točko po meri", + "losPointName": "Ime točke", + "losShowPanelTooltip": "Pokaži ploščo LOS", + "losHidePanelTooltip": "Skrij ploščo LOS", + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Radijski horizont", "losLegendLosBeam": "Linija vidnosti", "losLegendTerrain": "Teren", "losFrequencyLabel": "Frekvenca", - "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", - "losFrequencyDialogTitle": "Izračun radijskega horizonta", - "losFrequencyDialogDescription": "ZačenÅ¡i od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", + "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", + "losFrequencyDialogTitle": "Izračun radijskega horizonta", + "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1798,15 +1798,27 @@ }, "contacts_unread": "Neprebrano", "contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...", - "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", + "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", "contacts_searchContactsNoNumber": "Iskanje stikov...", - "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", - "contacts_searchUsers": "Išči {number}{str} uporabnikov...", - "connectionChoiceBluetoothLabel": "Bluetooth", - "connectionChoiceUsbLabel": "USB", - "usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vaÅ¡im MeshCore-om.", - "usbScreenTitle": "Povežite preko USB", + "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", + "contacts_searchUsers": "Išči {number}{str} uporabnikov...", "usbScreenStatus": "Izberite USB naprave.", + "usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.", + "usbScreenTitle": "Povežite preko USB", "usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.", - "usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite." + "usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.", + "usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.", + "usbErrorDeviceMissing": "Izbrani USB napravega več ni na voljo.", + "usbErrorInvalidPort": "Izberite veljavno USB naprave.", + "usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.", + "usbErrorNotConnected": "Ni priklopljenih USB naprav.", + "usbErrorOpenFailed": "Niso uspeli odkriti izbrane USB naprave.", + "usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.", + "usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.", + "usbErrorAlreadyActive": "USB povezava je že aktivirana.", + "usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.", + "usbErrorPortClosed": "USB povezava ni aktivirana.", + "usbErrorConnectTimedOut": "Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8a74087..025294a 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { @@ -14,28 +14,28 @@ "nav_map": "Karta", "common_cancel": "Avbryt", "common_connect": "Anslut", - "common_unknownDevice": "Okänd enhet", + "common_unknownDevice": "Okänd enhet", "common_save": "Spara", "common_delete": "Radera", - "common_close": "Stänga", + "common_close": "Stänga", "common_edit": "Redigera", - "common_add": "Lägg till", - "common_settings": "Inställningar", - "common_disconnect": "Koppla frÃ¥n", + "common_add": "Lägg till", + "common_settings": "Inställningar", + "common_disconnect": "Koppla från", "common_connected": "Ansluten", "common_disconnected": "Ansluten", "common_create": "Skapa", - "common_continue": "Fortsätt", + "common_continue": "Fortsätt", "common_share": "Dela", "common_copy": "Kopiera", - "common_retry": "Försök igen", - "common_hide": "Dölj", + "common_retry": "Försök igen", + "common_hide": "Dölj", "common_remove": "Ta bort", "common_enable": "Aktivera", "common_disable": "Inaktivera", "common_reboot": "Start om", "common_loading": "Laddar...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,7 +53,7 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Söker efter enheter...", + "scanner_scanning": "Söker efter enheter...", "scanner_connecting": "Anslutning...", "scanner_disconnecting": "Anslutning bryts...", "scanner_notConnected": "Inte ansluten", @@ -65,8 +65,8 @@ } } }, - "scanner_searchingDevices": "Söker efter MeshCore-enheter...", - "scanner_tapToScan": "Tryck Skanna för att hitta MeshCore-enheter", + "scanner_searchingDevices": "Söker efter MeshCore-enheter...", + "scanner_tapToScan": "Tryck Skanna för att hitta MeshCore-enheter", "scanner_connectionFailed": "Anslutning misslyckades: {error}", "@scanner_connectionFailed": { "placeholders": { @@ -77,49 +77,49 @@ }, "scanner_stop": "Stoppa", "scanner_scan": "Skanna", - "device_quickSwitch": "Snabb växling", + "device_quickSwitch": "Snabb växling", "device_meshcore": "MeshCore", - "settings_title": "Inställningar", + "settings_title": "Inställningar", "settings_deviceInfo": "Enhetens information", - "settings_appSettings": "Appinställningar", - "settings_appSettingsSubtitle": "Meddelanden, notiser och kartinställningar", - "settings_nodeSettings": "Nodinställningar", + "settings_appSettings": "Appinställningar", + "settings_appSettingsSubtitle": "Meddelanden, notiser och kartinställningar", + "settings_nodeSettings": "Nodinställningar", "settings_nodeName": "Nodnamn", "settings_nodeNameNotSet": "Inte angivet", "settings_nodeNameHint": "Ange nodnamn", "settings_nodeNameUpdated": "Namn uppdaterat", - "settings_radioSettings": "Radioinställningar", + "settings_radioSettings": "Radioinställningar", "settings_radioSettingsSubtitle": "Frekvens, effekt, spridningsfaktor", - "settings_radioSettingsUpdated": "Radioinställningarna har uppdaterats", + "settings_radioSettingsUpdated": "Radioinställningarna har uppdaterats", "settings_location": "Plats", "settings_locationSubtitle": "GPS koordinater", "settings_locationUpdated": "Plats uppdaterad", - "settings_locationBothRequired": "Ange bÃ¥de latitud och longitud.", + "settings_locationBothRequired": "Ange både latitud och longitud.", "settings_locationInvalid": "Ogiltig latitud eller longitud.", "settings_latitude": "Latitud", - "settings_longitude": "Längdgrad", - "settings_privacyMode": "Privatläge", - "settings_privacyModeSubtitle": "Dölj namn/plats i annonser", - "settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.", - "settings_privacyModeEnabled": "Privatläget är aktiverat", - "settings_privacyModeDisabled": "Privatläge är avstängt", - "settings_actions": "Ã…tgärder", + "settings_longitude": "Längdgrad", + "settings_privacyMode": "Privatläge", + "settings_privacyModeSubtitle": "Dölj namn/plats i annonser", + "settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.", + "settings_privacyModeEnabled": "Privatläget är aktiverat", + "settings_privacyModeDisabled": "Privatläge är avstängt", + "settings_actions": "Åtgärder", "settings_sendAdvertisement": "Skicka Annons", - "settings_sendAdvertisementSubtitle": "Sändning finns nu", + "settings_sendAdvertisementSubtitle": "Sändning finns nu", "settings_advertisementSent": "Annons skickad", "settings_syncTime": "Synkroniseringstid", - "settings_syncTimeSubtitle": "Ställ enheten till telefonens tid", + "settings_syncTimeSubtitle": "Ställ enheten till telefonens tid", "settings_timeSynchronized": "Tidssynkroniserat", "settings_refreshContacts": "Uppdatera Kontakter", - "settings_refreshContactsSubtitle": "Ladda om kontaktlistan frÃ¥n enheten", + "settings_refreshContactsSubtitle": "Ladda om kontaktlistan från enheten", "settings_rebootDevice": "Starta om enheten", "settings_rebootDeviceSubtitle": "Starta MeshCore-enheten", - "settings_rebootDeviceConfirm": "Är du säker pÃ¥ att du vill starta om enheten? Du kommer att bli avkopplad.", - "settings_debug": "Felsök", - "settings_bleDebugLog": "BLE-felsökning", - "settings_bleDebugLogSubtitle": "BLE-kommandon, svar och rÃ¥data", - "settings_appDebugLog": "Appfelsökning", - "settings_appDebugLogSubtitle": "Applikations felsökningsmeddelanden", + "settings_rebootDeviceConfirm": "Är du säker på att du vill starta om enheten? Du kommer att bli avkopplad.", + "settings_debug": "Felsök", + "settings_bleDebugLog": "BLE-felsökning", + "settings_bleDebugLogSubtitle": "BLE-kommandon, svar och rådata", + "settings_appDebugLog": "Appfelsökning", + "settings_appDebugLogSubtitle": "Applikations felsökningsmeddelanden", "settings_about": "Om", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -129,16 +129,16 @@ } } }, - "settings_aboutLegalese": "2024 MeshCore Öppen Källkodsprojekt", - "settings_aboutDescription": "En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.", + "settings_aboutLegalese": "2024 MeshCore Öppen Källkodsprojekt", + "settings_aboutDescription": "En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.", "settings_infoName": "Namn", "settings_infoId": "ID", "settings_infoStatus": "Status", "settings_infoBattery": "Batteri", - "settings_infoPublicKey": "Allmänt nyckel", + "settings_infoPublicKey": "Allmänt nyckel", "settings_infoContactsCount": "Kontakterantal", "settings_infoChannelCount": "Kanalantal", - "settings_presets": "Fördefinierade inställningar", + "settings_presets": "Fördefinierade inställningar", "settings_frequency": "Frekvens (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)", @@ -156,51 +156,51 @@ } } }, - "appSettings_title": "Appinställningar", + "appSettings_title": "Appinställningar", "appSettings_appearance": "Utseende", "appSettings_theme": "Tema", "appSettings_themeSystem": "Systemstandard", "appSettings_themeLight": "Ljus", - "appSettings_themeDark": "Mörk", - "appSettings_language": "SprÃ¥k", + "appSettings_themeDark": "Mörk", + "appSettings_language": "Språk", "appSettings_languageSystem": "Systemstandard", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", "appSettings_notifications": "Meddelanden", "appSettings_enableNotifications": "Aktivera Notifikationer", - "appSettings_enableNotificationsSubtitle": "Ta emot notiser för meddelanden och reklam", - "appSettings_notificationPermissionDenied": "TillÃ¥telse för notifikationer nekad", + "appSettings_enableNotificationsSubtitle": "Ta emot notiser för meddelanden och reklam", + "appSettings_notificationPermissionDenied": "Tillåtelse för notifikationer nekad", "appSettings_notificationsEnabled": "Notifikationer aktiverade", - "appSettings_notificationsDisabled": "Meddelanden är avstängda", + "appSettings_notificationsDisabled": "Meddelanden är avstängda", "appSettings_messageNotifications": "Meddelandekrav", - "appSettings_messageNotificationsSubtitle": "Visa notis när nya meddelanden tas emot", + "appSettings_messageNotificationsSubtitle": "Visa notis när nya meddelanden tas emot", "appSettings_channelMessageNotifications": "Kanalmeddelandena", - "appSettings_channelMessageNotificationsSubtitle": "Visa notis när meddelanden i kanal mottas", + "appSettings_channelMessageNotificationsSubtitle": "Visa notis när meddelanden i kanal mottas", "appSettings_advertisementNotifications": "Annonsmeddelanden", - "appSettings_advertisementNotificationsSubtitle": "Visa notis när nya noder upptäcks", + "appSettings_advertisementNotificationsSubtitle": "Visa notis när nya noder upptäcks", "appSettings_messaging": "Meddelanden", - "appSettings_clearPathOnMaxRetry": "Rensa Vägen pÃ¥ Max Försök", - "appSettings_clearPathOnMaxRetrySubtitle": "Ã…terställ kontaktväg efter 5 misslyckade försök att skicka", - "appSettings_pathsWillBeCleared": "Sökvägar kommer att tömmas efter 5 misslyckade försök.", - "appSettings_pathsWillNotBeCleared": "Sökvägar kommer inte att rensas automatiskt.", - "appSettings_autoRouteRotation": "Automatisk Rutväxling", - "appSettings_autoRouteRotationSubtitle": "Blixtra mellan bästa vägar och flödesläge", - "appSettings_autoRouteRotationEnabled": "Automatisk ruttrotation är aktiverad", - "appSettings_autoRouteRotationDisabled": "Automatisk ruttrotation är avstängd", + "appSettings_clearPathOnMaxRetry": "Rensa Vägen på Max Försök", + "appSettings_clearPathOnMaxRetrySubtitle": "Återställ kontaktväg efter 5 misslyckade försök att skicka", + "appSettings_pathsWillBeCleared": "Sökvägar kommer att tömmas efter 5 misslyckade försök.", + "appSettings_pathsWillNotBeCleared": "Sökvägar kommer inte att rensas automatiskt.", + "appSettings_autoRouteRotation": "Automatisk Rutväxling", + "appSettings_autoRouteRotationSubtitle": "Blixtra mellan bästa vägar och flödesläge", + "appSettings_autoRouteRotationEnabled": "Automatisk ruttrotation är aktiverad", + "appSettings_autoRouteRotationDisabled": "Automatisk ruttrotation är avstängd", "appSettings_battery": "Batteri", "appSettings_batteryChemistry": "Batterikemi", - "appSettings_batteryChemistryPerDevice": "Ställ in per enhet ({deviceName})", + "appSettings_batteryChemistryPerDevice": "Ställ in per enhet ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -208,20 +208,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Anslut till en enhet för att välja", + "appSettings_batteryChemistryConnectFirst": "Anslut till en enhet för att välja", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", - "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", + "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65V)", "appSettings_batteryLipo": "LiPo (3.0-4.2V)", "appSettings_mapDisplay": "Kartvisning", - "appSettings_showRepeaters": "Visa Ã¥teruppslag", - "appSettings_showRepeatersSubtitle": "Visa Ã¥terspelsnoder pÃ¥ kartan", + "appSettings_showRepeaters": "Visa återuppslag", + "appSettings_showRepeatersSubtitle": "Visa återspelsnoder på kartan", "appSettings_showChatNodes": "Visa Chattnoder", - "appSettings_showChatNodesSubtitle": "Visa chattnoder pÃ¥ kartan", + "appSettings_showChatNodesSubtitle": "Visa chattnoder på kartan", "appSettings_showOtherNodes": "Visa andra noder", - "appSettings_showOtherNodesSubtitle": "Visa andra nodtyper pÃ¥ kartan", + "appSettings_showOtherNodesSubtitle": "Visa andra nodtyper på kartan", "appSettings_timeFilter": "Tidsfilter", "appSettings_timeFilterShowAll": "Visa alla noder", - "appSettings_timeFilterShowLast": "Visa noder frÃ¥n de senaste {hours} timmarna", + "appSettings_timeFilterShowLast": "Visa noder från de senaste {hours} timmarna", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,15 +230,15 @@ } }, "appSettings_mapTimeFilter": "Karttid Filter", - "appSettings_showNodesDiscoveredWithin": "Visa noder som upptäckts inom:", + "appSettings_showNodesDiscoveredWithin": "Visa noder som upptäckts inom:", "appSettings_allTime": "Totalen", "appSettings_lastHour": "Sista timmen", "appSettings_last6Hours": "De senaste 6 timmarna", "appSettings_last24Hours": "De senaste 24 timmarna", - "appSettings_lastWeek": "Förra veckan", + "appSettings_lastWeek": "Förra veckan", "appSettings_offlineMapCache": "Offline Kartcache", "appSettings_noAreaSelected": "Ingen area markerad", - "appSettings_areaSelectedZoom": "OmrÃ¥de markerat (zoom {minZoom}-{maxZoom})", + "appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -249,19 +249,19 @@ } } }, - "appSettings_debugCard": "Felsök", - "appSettings_appDebugLogging": "App-felsökning och loggning", - "appSettings_appDebugLoggingSubtitle": "Logga appens felsökningsmeddelanden för felsökning", - "appSettings_appDebugLoggingEnabled": "App felsökning loggning aktiverad", - "appSettings_appDebugLoggingDisabled": "App felsökning är avstängd", + "appSettings_debugCard": "Felsök", + "appSettings_appDebugLogging": "App-felsökning och loggning", + "appSettings_appDebugLoggingSubtitle": "Logga appens felsökningsmeddelanden för felsökning", + "appSettings_appDebugLoggingEnabled": "App felsökning loggning aktiverad", + "appSettings_appDebugLoggingDisabled": "App felsökning är avstängd", "contacts_title": "Kontakter", - "contacts_noContacts": "Inga kontakter ännu", - "contacts_contactsWillAppear": "Kontakter kommer att visas när enheter annonserar.", - "contacts_searchContacts": "Sök kontakter...", - "contacts_noUnreadContacts": "Inga oinlästa kontakter", + "contacts_noContacts": "Inga kontakter ännu", + "contacts_contactsWillAppear": "Kontakter kommer att visas när enheter annonserar.", + "contacts_searchContacts": "Sök kontakter...", + "contacts_noUnreadContacts": "Inga oinlästa kontakter", "contacts_noContactsFound": "Inga kontakter eller grupper hittades.", "contacts_deleteContact": "Ta bort Kontakt", - "contacts_removeConfirm": "Ta bort {contactName} frÃ¥n kontakter?", + "contacts_removeConfirm": "Ta bort {contactName} från kontakter?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -271,7 +271,7 @@ }, "contacts_manageRepeater": "Hantera Upprepare", "contacts_roomLogin": "Rum Inloggning", - "contacts_openChat": "Öppna Chatt", + "contacts_openChat": "Öppna Chatt", "contacts_editGroup": "Redigera Grupp", "contacts_deleteGroup": "Ta bort Grupp", "contacts_deleteGroupConfirm": "Ta bort {groupName}?", @@ -284,7 +284,7 @@ }, "contacts_newGroup": "Ny grupp", "contacts_groupName": "Gruppnamn", - "contacts_groupNameRequired": "Gruppnamnet är obligatoriskt", + "contacts_groupNameRequired": "Gruppnamnet är obligatoriskt", "contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.", "@contacts_groupAlreadyExists": { "placeholders": { @@ -305,7 +305,7 @@ } } }, - "contacts_lastSeenHourAgo": "Senast sedd för 1 timme sedan", + "contacts_lastSeenHourAgo": "Senast sedd för 1 timme sedan", "contacts_lastSeenHoursAgo": "Senast sedd {hours} timmar sedan", "@contacts_lastSeenHoursAgo": { "placeholders": { @@ -314,7 +314,7 @@ } } }, - "contacts_lastSeenDayAgo": "Senast sedd för 1 dag sedan", + "contacts_lastSeenDayAgo": "Senast sedd för 1 dag sedan", "contacts_lastSeenDaysAgo": "Senast synlig {days} dagar sedan", "@contacts_lastSeenDaysAgo": { "placeholders": { @@ -325,8 +325,8 @@ }, "channels_title": "Kanaler", "channels_noChannelsConfigured": "Inga kanaler konfigurerade", - "channels_addPublicChannel": "Lägg till publik kanal", - "channels_searchChannels": "Sök kanaler...", + "channels_addPublicChannel": "Lägg till publik kanal", + "channels_searchChannels": "Sök kanaler...", "channels_noChannelsFound": "Inga kanaler hittades", "channels_channelIndex": "Kanal {index}", "@channels_channelIndex": { @@ -339,13 +339,13 @@ "channels_hashtagChannel": "Hashtagkanal", "channels_public": "Offentligt", "channels_private": "Privat", - "channels_publicChannel": "Allmänt kanal", + "channels_publicChannel": "Allmänt kanal", "channels_privateChannel": "Privat kanal", "channels_editChannel": "Redigera kanal", "channels_muteChannel": "Tysta kanal", - "channels_unmuteChannel": "SlÃ¥ pÃ¥ ljud för kanal", + "channels_unmuteChannel": "Slå på ljud för kanal", "channels_deleteChannel": "Ta bort kanal", - "channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte Ã¥ngras.", + "channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -361,15 +361,15 @@ } } }, - "channels_addChannel": "Lägg till kanal", + "channels_addChannel": "Lägg till kanal", "channels_channelIndexLabel": "Kanalindex", "channels_channelName": "Kanalnamn", - "channels_usePublicChannel": "Använd Publikkanal", - "channels_standardPublicPsk": "Standard allmän PSK", + "channels_usePublicChannel": "Använd Publikkanal", + "channels_standardPublicPsk": "Standard allmän PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Generera slumpmässig PSK", + "channels_generateRandomPsk": "Generera slumpmässig PSK", "channels_enterChannelName": "Ange en kanalnamn", - "channels_pskMustBe32Hex": "PSK mÃ¥ste vara 32 hexadecimala tecken", + "channels_pskMustBe32Hex": "PSK måste vara 32 hexadecimala tecken", "channels_channelAdded": "Kanalen \"{name}\" har lagts till", "@channels_channelAdded": { "placeholders": { @@ -395,14 +395,14 @@ } } }, - "channels_publicChannelAdded": "Allmänt kanal tillagd", + "channels_publicChannelAdded": "Allmänt kanal tillagd", "channels_sortBy": "Sortera efter", "channels_sortManual": "Manuell", "channels_sortAZ": "A-Z", "channels_sortLatestMessages": "Senaste meddelanden", - "channels_sortUnread": "Oläst", - "chat_noMessages": "Inga meddelanden ännu", - "chat_sendMessageToStart": "Skicka ett meddelande för att komma igÃ¥ng", + "channels_sortUnread": "Oläst", + "chat_noMessages": "Inga meddelanden ännu", + "chat_sendMessageToStart": "Skicka ett meddelande för att komma igång", "chat_originalMessageNotFound": "Originalt meddelande hittades inte", "chat_replyingTo": "Svara till {name}", "@chat_replyingTo": { @@ -430,7 +430,7 @@ } }, "chat_typeMessage": "Skriv ett meddelande...", - "chat_messageTooLong": "Meddelandet är för lÃ¥ngt (max {maxBytes} byte).", + "chat_messageTooLong": "Meddelandet är för långt (max {maxBytes} byte).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -440,8 +440,8 @@ }, "chat_messageCopied": "Meddelandet kopierades", "chat_messageDeleted": "Meddelandet raderat", - "chat_retryingMessage": "Försöker igen", - "chat_retryCount": "Försök igen {current}/{max}", + "chat_retryingMessage": "Försöker igen", + "chat_retryCount": "Försök igen {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -454,30 +454,30 @@ }, "chat_sendGif": "Skicka GIF", "chat_reply": "Svara", - "chat_addReaction": "Lägg till reaktion", + "chat_addReaction": "Lägg till reaktion", "chat_me": "Mig", "emojiCategorySmileys": "Emojis", "emojiCategoryGestures": "Gestikuleringar", - "emojiCategoryHearts": "Hjärtan", + "emojiCategoryHearts": "Hjärtan", "emojiCategoryObjects": "Objekt", - "gifPicker_title": "Välj en GIF", - "gifPicker_searchHint": "Sök GIF:ar...", + "gifPicker_title": "Välj en GIF", + "gifPicker_searchHint": "Sök GIF:ar...", "gifPicker_poweredBy": "Drivet av GIPHY", "gifPicker_noGifsFound": "Inga GIF-filer hittades", "gifPicker_failedLoad": "Kunde inte ladda GIF-filer", - "gifPicker_failedSearch": "Sökningen misslyckades.", + "gifPicker_failedSearch": "Sökningen misslyckades.", "gifPicker_noInternet": "Ingen internetanslutning", - "debugLog_appTitle": "Appfelsökning", - "debugLog_bleTitle": "BLE-felsökning", + "debugLog_appTitle": "Appfelsökning", + "debugLog_bleTitle": "BLE-felsökning", "debugLog_copyLog": "Kopiera logg", "debugLog_clearLog": "Rensa logg", - "debugLog_copied": "Felsökningslogg kopierad", + "debugLog_copied": "Felsökningslogg kopierad", "debugLog_bleCopied": "BLE-logg kopierad", - "debugLog_noEntries": "Inga felsökningsloggar ännu", - "debugLog_enableInSettings": "Aktivera appens felsökningsloggning i inställningarna", + "debugLog_noEntries": "Inga felsökningsloggar ännu", + "debugLog_enableInSettings": "Aktivera appens felsökningsloggning i inställningarna", "debugLog_frames": "Rammar", - "debugLog_rawLogRx": "RÃ¥ Log-RX", - "debugLog_noBleActivity": "Ingen BLE-aktivitet ännu", + "debugLog_rawLogRx": "Rå Log-RX", + "debugLog_noBleActivity": "Ingen BLE-aktivitet ännu", "debugFrame_length": "Ramstorlek: {count} byte", "@debugFrame_length": { "placeholders": { @@ -494,8 +494,8 @@ } } }, - "debugFrame_textMessageHeader": "Textmeddelandefält:", - "debugFrame_destinationPubKey": "– Destination PubKey: {pubKey}", + "debugFrame_textMessageHeader": "Textmeddelandefält:", + "debugFrame_destinationPubKey": "– Destination PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -503,7 +503,7 @@ } } }, - "debugFrame_timestamp": "- Tidsstämpel: {timestamp}", + "debugFrame_timestamp": "- Tidsstämpel: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -542,11 +542,11 @@ }, "debugFrame_hexDump": "Hexdump:", "chat_pathManagement": "Stigarhantering", - "chat_routingMode": "Ruttläge", - "chat_autoUseSavedPath": "Automatisk (använd sparad sökväg)", - "chat_forceFloodMode": "Tvinga Översvämningsläge", - "chat_recentAckPaths": "Nyligen Ack-vägar (tryck för att använda):", - "chat_pathHistoryFull": "Historisk sökväg är full. Ta bort poster för att lägga till nya.", + "chat_routingMode": "Ruttläge", + "chat_autoUseSavedPath": "Automatisk (använd sparad sökväg)", + "chat_forceFloodMode": "Tvinga Översvämningsläge", + "chat_recentAckPaths": "Nyligen Ack-vägar (tryck för att använda):", + "chat_pathHistoryFull": "Historisk sökväg är full. Ta bort poster för att lägga till nya.", "chat_hopSingular": "hoppa", "chat_hopPlural": "hoppar", "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", @@ -557,20 +557,20 @@ } } }, - "chat_successes": "framgÃ¥ngar", - "chat_removePath": "Ta bort sökväg", - "chat_noPathHistoryYet": "Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spÃ¥r.", + "chat_successes": "framgångar", + "chat_removePath": "Ta bort sökväg", + "chat_noPathHistoryYet": "Ingen historik ännu.\nSkicka ett meddelande för att upptäcka spår.", "chat_pathActions": "Stigar:", - "chat_setCustomPath": "Ange anpassad sökväg", - "chat_setCustomPathSubtitle": "Ange ruttväg manuellt", - "chat_clearPath": "Rensa Vägen", - "chat_clearPathSubtitle": "Tvinga fram omstart vid nästa sändning", - "chat_pathCleared": "Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.", - "chat_floodModeSubtitle": "Använd routningsomkopplaren i appraden", - "chat_floodModeEnabled": "Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.", - "chat_fullPath": "Fullständig sökväg", - "chat_pathDetailsNotAvailable": "Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.", - "chat_pathSetHops": "Sökväg inställd: {hopCount} {hopCount, plural, =1{hopp} other{hoppar}} - {status}", + "chat_setCustomPath": "Ange anpassad sökväg", + "chat_setCustomPathSubtitle": "Ange ruttväg manuellt", + "chat_clearPath": "Rensa Vägen", + "chat_clearPathSubtitle": "Tvinga fram omstart vid nästa sändning", + "chat_pathCleared": "Routen är nu fri. Nästa meddelande kommer att upptäcka rutten igen.", + "chat_floodModeSubtitle": "Använd routningsomkopplaren i appraden", + "chat_floodModeEnabled": "Översvämningsläge aktiverat. Stäng av via ruttikonen i appraden.", + "chat_fullPath": "Fullständig sökväg", + "chat_pathDetailsNotAvailable": "Stigaruppgifterna är ännu inte tillgängliga. Försök att skicka ett meddelande för att uppdatera.", + "chat_pathSetHops": "Sökväg inställd: {hopCount} {hopCount, plural, =1{hopp} other{hoppar}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -581,14 +581,14 @@ } } }, - "chat_pathSavedLocally": "Sparat lokalt. Anslut för att synkronisera.", - "chat_pathDeviceConfirmed": "Enheten bekräftad.", - "chat_pathDeviceNotConfirmed": "Enheten har inte bekräftats ännu.", + "chat_pathSavedLocally": "Sparat lokalt. Anslut för att synkronisera.", + "chat_pathDeviceConfirmed": "Enheten bekräftad.", + "chat_pathDeviceNotConfirmed": "Enheten har inte bekräftats ännu.", "chat_type": "Skriv", - "chat_path": "Sökväg", - "chat_publicKey": "Allmänt nyckel", - "chat_compressOutgoingMessages": "Kryptera utgÃ¥ende meddelanden", - "chat_floodForced": "Översvämning (tvingad)", + "chat_path": "Sökväg", + "chat_publicKey": "Allmänt nyckel", + "chat_compressOutgoingMessages": "Kryptera utgående meddelanden", + "chat_floodForced": "Översvämning (tvingad)", "chat_directForced": "Direkt (tvingad)", "chat_hopsForced": "{count} hopp (tvingat)", "@chat_hopsForced": { @@ -598,10 +598,10 @@ } } }, - "chat_floodAuto": "Översvämning (auto)", + "chat_floodAuto": "Översvämning (auto)", "chat_direct": "Direkt", "chat_poiShared": "Delad POI", - "chat_unread": "Olästa: {count}", + "chat_unread": "Olästa: {count}", "@chat_unread": { "placeholders": { "count": { @@ -609,10 +609,10 @@ } } }, - "chat_openLink": "Öppna länk?", - "chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?", - "chat_open": "Öppna", - "chat_couldNotOpenLink": "Kunde inte öppna länken: {url}", + "chat_openLink": "Öppna länk?", + "chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?", + "chat_open": "Öppna", + "chat_couldNotOpenLink": "Kunde inte öppna länken: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -620,10 +620,10 @@ } } }, - "chat_invalidLink": "Ogiltigt länkformat", + "chat_invalidLink": "Ogiltigt länkformat", "map_title": "Nodkarta", "map_noNodesWithLocation": "Inga noder med platsinformation", - "map_nodesNeedGps": "Noder mÃ¥ste dela sina GPS-koordinater\nför att visas pÃ¥ kartan", + "map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan", "map_nodesCount": "Noder: {count}", "@map_nodesCount": { "placeholders": { @@ -641,26 +641,26 @@ } }, "map_chat": "Chat", - "map_repeater": "Ã…teruppspelare", + "map_repeater": "Återuppspelare", "map_room": "Rum", "map_sensor": "Sensor", - "map_pinDm": "LÃ¥s (DM)", - "map_pinPrivate": "LÃ¥s (Privat)", - "map_pinPublic": "AnslÃ¥ (Offentligt)", + "map_pinDm": "Lås (DM)", + "map_pinPrivate": "Lås (Privat)", + "map_pinPublic": "Anslå (Offentligt)", "map_lastSeen": "Senast sedd", - "map_disconnectConfirm": "Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?", - "map_from": "FrÃ¥n", - "map_source": "Källa", + "map_disconnectConfirm": "Är du säker på att du vill koppla från enheten?", + "map_from": "Från", + "map_source": "Källa", "map_flags": "Flaggor", - "map_shareMarkerHere": "Dela markeringen här", - "map_pinLabel": "Fästetikett", + "map_shareMarkerHere": "Dela markeringen här", + "map_pinLabel": "Fästetikett", "map_label": "Etikett", "map_pointOfInterest": "Plats av intresse", "map_sendToContact": "Skicka till kontakt", "map_sendToChannel": "Skicka till kanal", - "map_noChannelsAvailable": "Inga kanaler tillgängliga", + "map_noChannelsAvailable": "Inga kanaler tillgängliga", "map_publicLocationShare": "Dela offentlig plats", - "map_publicLocationShareConfirm": "Du hÃ¥ller pÃ¥ att dela en plats i {channelLabel}. Denna kanal är offentlig och alla med PSK kan se den.", + "map_publicLocationShareConfirm": "Du håller på att dela en plats i {channelLabel}. Denna kanal är offentlig och alla med PSK kan se den.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -668,7 +668,7 @@ } } }, - "map_connectToShareMarkers": "Anslut till en enhet för att dela markörer", + "map_connectToShareMarkers": "Anslut till en enhet för att dela markörer", "map_filterNodes": "Filtrera noder", "map_nodeTypes": "Nodtyper", "map_chatNodes": "Chatnoder", @@ -676,18 +676,18 @@ "map_otherNodes": "Andra noder", "map_keyPrefix": "Nyckelprefix", "map_filterByKeyPrefix": "Filtrera efter nyckelprefix", - "map_publicKeyPrefix": "Allmänt nyckelprästegenskap", - "map_markers": "Markörer", - "map_showSharedMarkers": "Visa delade markörer", + "map_publicKeyPrefix": "Allmänt nyckelprästegenskap", + "map_markers": "Markörer", + "map_showSharedMarkers": "Visa delade markörer", "map_lastSeenTime": "Senaste Visats Tid", "map_sharedPin": "Delad PIN", - "map_joinRoom": "GÃ¥ med i rum", + "map_joinRoom": "Gå med i rum", "map_manageRepeater": "Hantera Upprepare", "mapCache_title": "Offline Kartcache", - "mapCache_selectAreaFirst": "Välj ett omrÃ¥de att cachera först", - "mapCache_noTilesToDownload": "Inga kuber att ladda ner för detta omrÃ¥de", + "mapCache_selectAreaFirst": "Välj ett område att cachera först", + "mapCache_noTilesToDownload": "Inga kuber att ladda ner för detta område", "mapCache_downloadTilesTitle": "Ladda ner klick", - "mapCache_downloadTilesPrompt": "Ladda ner {count} kuber för offlineanvändning?", + "mapCache_downloadTilesPrompt": "Ladda ner {count} kuber för offlineanvändning?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -719,9 +719,9 @@ "mapCache_clearOfflineCachePrompt": "Ta bort alla cachemapplaner?", "mapCache_offlineCacheCleared": "Offline-cache rensad", "mapCache_noAreaSelected": "Ingen area markerad", - "mapCache_cacheArea": "CacheomrÃ¥de", - "mapCache_useCurrentView": "Använd Aktuell Visning", - "mapCache_zoomRange": "Zoombegränsning", + "mapCache_cacheArea": "Cacheområde", + "mapCache_useCurrentView": "Använd Aktuell Visning", + "mapCache_zoomRange": "Zoombegränsning", "mapCache_estimatedTiles": "Uppskattat antal klick: {count}", "@mapCache_estimatedTiles": { "placeholders": { @@ -799,27 +799,27 @@ "time_days": "dagar", "time_week": "vecka", "time_weeks": "veckor", - "time_month": "mÃ¥nad", - "time_months": "mÃ¥nader", + "time_month": "månad", + "time_months": "månader", "time_minutes": "minuter", "time_allTime": "Alla tider", - "dialog_disconnect": "Koppla frÃ¥n", - "dialog_disconnectConfirm": "Är du säker pÃ¥ att du vill koppla frÃ¥n enheten?", - "login_repeaterLogin": "Ã…teruppta Inloggning", + "dialog_disconnect": "Koppla från", + "dialog_disconnectConfirm": "Är du säker på att du vill koppla från enheten?", + "login_repeaterLogin": "Återuppta Inloggning", "login_roomLogin": "Rum Inloggning", - "login_password": "Lösenord", - "login_enterPassword": "Ange lösenord", - "login_savePassword": "Spara lösenord", - "login_savePasswordSubtitle": "Lösenord kommer att lagras säkert pÃ¥ enheten.", - "login_repeaterDescription": "Ange Ã¥teruppspelarens lösenord för att komma Ã¥t inställningar och status.", - "login_roomDescription": "Ange rummets lösenord för att komma Ã¥t inställningar och status.", + "login_password": "Lösenord", + "login_enterPassword": "Ange lösenord", + "login_savePassword": "Spara lösenord", + "login_savePasswordSubtitle": "Lösenord kommer att lagras säkert på enheten.", + "login_repeaterDescription": "Ange återuppspelarens lösenord för att komma åt inställningar och status.", + "login_roomDescription": "Ange rummets lösenord för att komma åt inställningar och status.", "login_routing": "Ruttning", - "login_routingMode": "Ruttläge", - "login_autoUseSavedPath": "Automatisk (använd sparad sökväg)", - "login_forceFloodMode": "Tvinga Översvämningsläge", - "login_managePaths": "Hantera Sökvägar", + "login_routingMode": "Ruttläge", + "login_autoUseSavedPath": "Automatisk (använd sparad sökväg)", + "login_forceFloodMode": "Tvinga Översvämningsläge", + "login_managePaths": "Hantera Sökvägar", "login_login": "Logga in", - "login_attempt": "Försök {current}/{max}", + "login_attempt": "Försök {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -838,10 +838,10 @@ } } }, - "login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller sÃ¥ gÃ¥r det inte att nÃ¥ repeatern.", + "login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.", "common_reload": "Ladda om", "common_clear": "Rensa", - "path_currentPath": "Nuvarande sökväg: {path}", + "path_currentPath": "Nuvarande sökväg: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -849,7 +849,7 @@ } } }, - "path_usingHopsPath": "Använda {count} {count, plural, =1{hop} other{hops}} sökväg", + "path_usingHopsPath": "Använda {count} {count, plural, =1{hop} other{hops}} sökväg", "@path_usingHopsPath": { "placeholders": { "count": { @@ -857,15 +857,15 @@ } } }, - "path_enterCustomPath": "Ange anpassad sökväg", - "path_currentPathLabel": "Nuvarande sökväg", - "path_hexPrefixInstructions": "Ange 2-tecknets hex-prefett för varje hopp, Ã¥tskilda med komma.", - "path_hexPrefixExample": "Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)", + "path_enterCustomPath": "Ange anpassad sökväg", + "path_currentPathLabel": "Nuvarande sökväg", + "path_hexPrefixInstructions": "Ange 2-tecknets hex-prefett för varje hopp, åtskilda med komma.", + "path_hexPrefixExample": "Exempel: A1,F2,3C (varje nod använder det första bytet av sitt publika nyckel)", "path_labelHexPrefixes": "Hexprefixer", - "path_helperMaxHops": "Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)", - "path_selectFromContacts": "Välj istället frÃ¥n kontakter:", - "path_noRepeatersFound": "Inga Ã¥teruppspelare eller rumsservrar hittades.", - "path_customPathsRequire": "Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.", + "path_helperMaxHops": "Max 64 hopp. Varje prefix är 2 hex-tecken (1 byte)", + "path_selectFromContacts": "Välj istället från kontakter:", + "path_noRepeatersFound": "Inga återuppspelare eller rumsservrar hittades.", + "path_customPathsRequire": "Anpassade sökvägar kräver mellansteg som kan vidarebefordra meddelanden.", "path_invalidHexPrefixes": "Ogiltiga hex-prefikser: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { @@ -874,26 +874,26 @@ } } }, - "path_tooLong": "Sökvägen är för lÃ¥ng. Max 64 hopp tillÃ¥tna.", - "path_setPath": "Ange Sökväg", - "repeater_management": "Ã…teruppspelarens Hantering", + "path_tooLong": "Sökvägen är för lång. Max 64 hopp tillåtna.", + "path_setPath": "Ange Sökväg", + "repeater_management": "Återuppspelarens Hantering", "repeater_managementTools": "Administrationsverktyg", "repeater_status": "Status", - "repeater_statusSubtitle": "Visa Ã¥terspolningsstatus, statistik och grannar", + "repeater_statusSubtitle": "Visa återspolningsstatus, statistik och grannar", "repeater_telemetry": "Telemetry", - "repeater_telemetrySubtitle": "Visa telemetri för sensorer och systemstatistik", + "repeater_telemetrySubtitle": "Visa telemetri för sensorer och systemstatistik", "repeater_cli": "CLI", "repeater_cliSubtitle": "Skicka kommandon till repetitorn", - "repeater_settings": "Inställningar", - "repeater_settingsSubtitle": "Konfigurera Ã¥terspolarparametrar", - "repeater_statusTitle": "Ã…terspelsstatus", - "repeater_routingMode": "Ruttläge", - "repeater_autoUseSavedPath": "Automatisk (använd sparad sökväg)", - "repeater_forceFloodMode": "Tvinga Översvämningsläge", + "repeater_settings": "Inställningar", + "repeater_settingsSubtitle": "Konfigurera återspolarparametrar", + "repeater_statusTitle": "Återspelsstatus", + "repeater_routingMode": "Ruttläge", + "repeater_autoUseSavedPath": "Automatisk (använd sparad sökväg)", + "repeater_forceFloodMode": "Tvinga Översvämningsläge", "repeater_pathManagement": "Stigarhantering", "repeater_refresh": "Uppdatera", - "repeater_statusRequestTimeout": "StatusförfrÃ¥gan gick inte att hämta.", - "repeater_errorLoadingStatus": "Fel vid inläsning av status: {error}", + "repeater_statusRequestTimeout": "Statusförfrågan gick inte att hämta.", + "repeater_errorLoadingStatus": "Fel vid inläsning av status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -904,13 +904,13 @@ "repeater_systemInformation": "Systeminformation", "repeater_battery": "Batteri", "repeater_clockAtLogin": "Klocka (vid inloggning)", - "repeater_uptime": "Tillgänglighet", - "repeater_queueLength": "Köans längd", - "repeater_debugFlags": "Felsökningsflaggor", + "repeater_uptime": "Tillgänglighet", + "repeater_queueLength": "Köans längd", + "repeater_debugFlags": "Felsökningsflaggor", "repeater_radioStatistics": "Radiostatistik", "repeater_lastRssi": "Senaste RSSI", "repeater_lastSnr": "Sista SNR", - "repeater_noiseFloor": "LjudnivÃ¥", + "repeater_noiseFloor": "Ljudnivå", "repeater_txAirtime": "TX Airtime", "repeater_rxAirtime": "RX Airtime", "repeater_packetStatistics": "Paketstatistik", @@ -934,7 +934,7 @@ } } }, - "repeater_packetTxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", + "repeater_packetTxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -948,7 +948,7 @@ } } }, - "repeater_packetRxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", + "repeater_packetRxTotal": "Totalt: {total}, Översvämning: {flood}, Direkt: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -962,7 +962,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Översvämning: {flood}, Direkt: {direct}", + "repeater_duplicatesFloodDirect": "Översvämning: {flood}, Direkt: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -981,15 +981,15 @@ } } }, - "repeater_settingsTitle": "Ã…teruppspelarens Inställningar", - "repeater_basicSettings": "Grundinställningar", + "repeater_settingsTitle": "Återuppspelarens Inställningar", + "repeater_basicSettings": "Grundinställningar", "repeater_repeaterName": "Upprepare Namn", - "repeater_repeaterNameHelper": "Visa namn för denna Ã¥terupprepare", - "repeater_adminPassword": "Adminlösenord", - "repeater_adminPasswordHelper": "Fullständig Ã¥tkomstlösenord", - "repeater_guestPassword": "Gästlösenhet", - "repeater_guestPasswordHelper": "Läs-skyddspassord", - "repeater_radioSettings": "Radioinställningar", + "repeater_repeaterNameHelper": "Visa namn för denna återupprepare", + "repeater_adminPassword": "Adminlösenord", + "repeater_adminPasswordHelper": "Fullständig åtkomstlösenord", + "repeater_guestPassword": "Gästlösenhet", + "repeater_guestPasswordHelper": "Läs-skyddspassord", + "repeater_radioSettings": "Radioinställningar", "repeater_frequencyMhz": "Frekvens (MHz)", "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX Effekt", @@ -997,19 +997,19 @@ "repeater_bandwidth": "Bandbredd", "repeater_spreadingFactor": "Spreadingfaktor", "repeater_codingRate": "Kodningsgrad", - "repeater_locationSettings": "Platsinställningar", + "repeater_locationSettings": "Platsinställningar", "repeater_latitude": "Latitud", "repeater_latitudeHelper": "Decimalgrader (t.ex. 37.7749)", - "repeater_longitude": "Längdgrad", + "repeater_longitude": "Längdgrad", "repeater_longitudeHelper": "Decimalgrader (t.ex. -122.4194)", "repeater_features": "Funktioner", - "repeater_packetForwarding": "Paketväxling", - "repeater_packetForwardingSubtitle": "Aktivera Ã¥teruppspelaren för att vidarebefordra paket", - "repeater_guestAccess": "GästÃ¥tkomst", - "repeater_guestAccessSubtitle": "TillÃ¥t läsbehörigheter för gäster.", - "repeater_privacyMode": "Privatläge", - "repeater_privacyModeSubtitle": "Dölj namn/plats i annonser", - "repeater_advertisementSettings": "Annonsinställningar", + "repeater_packetForwarding": "Paketväxling", + "repeater_packetForwardingSubtitle": "Aktivera återuppspelaren för att vidarebefordra paket", + "repeater_guestAccess": "Gäståtkomst", + "repeater_guestAccessSubtitle": "Tillåt läsbehörigheter för gäster.", + "repeater_privacyMode": "Privatläge", + "repeater_privacyModeSubtitle": "Dölj namn/plats i annonser", + "repeater_advertisementSettings": "Annonsinställningar", "repeater_localAdvertInterval": "Lokalt Annonsintervall", "repeater_localAdvertIntervalMinutes": "{minutes} minuter", "@repeater_localAdvertIntervalMinutes": { @@ -1019,7 +1019,7 @@ } } }, - "repeater_floodAdvertInterval": "Översvämnadsannonsens tidsintervall", + "repeater_floodAdvertInterval": "Översvämnadsannonsens tidsintervall", "repeater_floodAdvertIntervalHours": "{hours} timmar", "@repeater_floodAdvertIntervalHours": { "placeholders": { @@ -1029,17 +1029,17 @@ } }, "repeater_encryptedAdvertInterval": "Krypterad Annonsintervall", - "repeater_dangerZone": "FaraomrÃ¥de", - "repeater_rebootRepeater": "Starta Ã…teruppspelaren", + "repeater_dangerZone": "Faraområde", + "repeater_rebootRepeater": "Starta Återuppspelaren", "repeater_rebootRepeaterSubtitle": "Starta om repeternheten", - "repeater_rebootRepeaterConfirm": "Är du säker pÃ¥ att du vill starta om denna repeater?", + "repeater_rebootRepeaterConfirm": "Är du säker på att du vill starta om denna repeater?", "repeater_regenerateIdentityKey": "Generera Identitetsknyckel", "repeater_regenerateIdentityKeySubtitle": "Generera ny publik/privat nyckelpar", - "repeater_regenerateIdentityKeyConfirm": "Detta kommer att generera en ny identitet för Ã¥terspelaren. Fortsätta?", + "repeater_regenerateIdentityKeyConfirm": "Detta kommer att generera en ny identitet för återspelaren. Fortsätta?", "repeater_eraseFileSystem": "Radera Filsystem", - "repeater_eraseFileSystemSubtitle": "Formatera Ã¥terspelsfilsystemet", - "repeater_eraseFileSystemConfirm": "VARNING: Detta kommer att radera all data pÃ¥ repeatern. Detta kan inte Ã¥ngras!", - "repeater_eraseSerialOnly": "Rensa är endast tillgängligt via seriell konsol.", + "repeater_eraseFileSystemSubtitle": "Formatera återspelsfilsystemet", + "repeater_eraseFileSystemConfirm": "VARNING: Detta kommer att radera all data på repeatern. Detta kan inte ångras!", + "repeater_eraseSerialOnly": "Rensa är endast tillgängligt via seriell konsol.", "repeater_commandSent": "Kommandot skickades: {command}", "@repeater_commandSent": { "placeholders": { @@ -1056,9 +1056,9 @@ } } }, - "repeater_confirm": "Bekräfta", - "repeater_settingsSaved": "Inställningarna sparades framgÃ¥ngsrikt.", - "repeater_errorSavingSettings": "Fel vid sparande av inställningar: {error}", + "repeater_confirm": "Bekräfta", + "repeater_settingsSaved": "Inställningarna sparades framgångsrikt.", + "repeater_errorSavingSettings": "Fel vid sparande av inställningar: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1066,14 +1066,14 @@ } } }, - "repeater_refreshBasicSettings": "Ã…terställ Grundläggande Inställningar", - "repeater_refreshRadioSettings": "Ã…terställ Radiosinställningar", - "repeater_refreshTxPower": "Ã…terställ TX-effekt", - "repeater_refreshLocationSettings": "Uppdatera Lokationsinställningar", - "repeater_refreshPacketForwarding": "Ã…terställ Paketväxling", - "repeater_refreshGuestAccess": "Ã…terställ GästÃ¥tkomst", - "repeater_refreshPrivacyMode": "Ã…terställ Sekretessläge", - "repeater_refreshAdvertisementSettings": "Ã…terställ Annonsinställningar", + "repeater_refreshBasicSettings": "Återställ Grundläggande Inställningar", + "repeater_refreshRadioSettings": "Återställ Radiosinställningar", + "repeater_refreshTxPower": "Återställ TX-effekt", + "repeater_refreshLocationSettings": "Uppdatera Lokationsinställningar", + "repeater_refreshPacketForwarding": "Återställ Paketväxling", + "repeater_refreshGuestAccess": "Återställ Gäståtkomst", + "repeater_refreshPrivacyMode": "Återställ Sekretessläge", + "repeater_refreshAdvertisementSettings": "Återställ Annonsinställningar", "repeater_refreshed": "{label} har uppdaterats", "@repeater_refreshed": { "placeholders": { @@ -1090,17 +1090,17 @@ } } }, - "repeater_cliTitle": "Ã…teruppspelaren CLI", - "repeater_debugNextCommand": "Felsök Nästa Kommando", - "repeater_commandHelp": "Hjälp", + "repeater_cliTitle": "Återuppspelaren CLI", + "repeater_debugNextCommand": "Felsök Nästa Kommando", + "repeater_commandHelp": "Hjälp", "repeater_clearHistory": "Rensa Historik", - "repeater_noCommandsSent": "Inga kommandon skickats ännu", - "repeater_typeCommandOrUseQuick": "Skriv en kommando nedan eller använd snabba kommandon", + "repeater_noCommandsSent": "Inga kommandon skickats ännu", + "repeater_typeCommandOrUseQuick": "Skriv en kommando nedan eller använd snabba kommandon", "repeater_enterCommandHint": "Ange kommando...", "repeater_previousCommand": "Tidigare kommando", - "repeater_nextCommand": "Nästa kommando", - "repeater_enterCommandFirst": "Ange en kommando först", - "repeater_cliCommandFrameTitle": "Kommandofönster", + "repeater_nextCommand": "Nästa kommando", + "repeater_enterCommandFirst": "Ange en kommando först", + "repeater_cliCommandFrameTitle": "Kommandofönster", "repeater_cliCommandError": "Fel: {error}", "@repeater_cliCommandError": { "placeholders": { @@ -1109,80 +1109,80 @@ } } }, - "repeater_cliQuickGetName": "Hämta namn", - "repeater_cliQuickGetRadio": "FÃ¥ Radio", - "repeater_cliQuickGetTx": "Hämta TX", + "repeater_cliQuickGetName": "Hämta namn", + "repeater_cliQuickGetRadio": "Få Radio", + "repeater_cliQuickGetTx": "Hämta TX", "repeater_cliQuickNeighbors": "Grannar", "repeater_cliQuickVersion": "Version", "repeater_cliQuickAdvertise": "Annonsera", "repeater_cliQuickClock": "Klocka", "repeater_cliHelpAdvert": "Skickar ett annonspaket", - "repeater_cliHelpReboot": "Startar om enheten. (notera, du fÃ¥r kanske 'Timeout' vilket är normalt)", + "repeater_cliHelpReboot": "Startar om enheten. (notera, du får kanske 'Timeout' vilket är normalt)", "repeater_cliHelpClock": "Visar aktuell tid per enhetens klocka.", - "repeater_cliHelpPassword": "Ställer in ett nytt administratörslösenord för enheten.", + "repeater_cliHelpPassword": "Ställer in ett nytt administratörslösenord för enheten.", "repeater_cliHelpVersion": "Visar enhetsversion och firmwarebyggnadsdatum.", - "repeater_cliHelpClearStats": "Ã…terställer olika statistikräknare till noll.", - "repeater_cliHelpSetAf": "Ställer in lufttidsfaktor.", - "repeater_cliHelpSetTx": "Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)", - "repeater_cliHelpSetRepeat": "Aktiverar eller inaktiverar Ã¥teruppspelarens roll för denna nod.", - "repeater_cliHelpSetAllowReadOnly": "(Rumserver) Om 'pÃ¥', sÃ¥ tillÃ¥ts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).", - "repeater_cliHelpSetFloodMax": "Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).", - "repeater_cliHelpSetIntThresh": "Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den pÃ¥ 0 för att inaktivera detektion av kanalinterferens.", - "repeater_cliHelpSetAgcResetInterval": "Ställer in intervallet för att Ã¥terställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.", + "repeater_cliHelpClearStats": "Återställer olika statistikräknare till noll.", + "repeater_cliHelpSetAf": "Ställer in lufttidsfaktor.", + "repeater_cliHelpSetTx": "Ställer LoRa-sändningseffekten i dBm. (starta om för att tillämpa)", + "repeater_cliHelpSetRepeat": "Aktiverar eller inaktiverar återuppspelarens roll för denna nod.", + "repeater_cliHelpSetAllowReadOnly": "(Rumserver) Om 'på', så tillåts login med tomt lösenord, men kan inte Posta till rummet. (bara läsa).", + "repeater_cliHelpSetFloodMax": "Ställer in det maximala antalet hopp för inkommande översvämning (om >= max, skickas inte paketet).", + "repeater_cliHelpSetIntThresh": "Ställer Interferensgränsen (i dB). Standardvärdet är 14. Ställ in den på 0 för att inaktivera detektion av kanalinterferens.", + "repeater_cliHelpSetAgcResetInterval": "Ställer in intervallet för att återställa Auto Gain-kontrollen. Ställ in till 0 för att inaktivera.", "repeater_cliHelpSetMultiAcks": "Aktiverar eller inaktiverar funktionen 'dubbla ACKs'.", - "repeater_cliHelpSetAdvertInterval": "Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.", - "repeater_cliHelpSetFloodAdvertInterval": "Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in pÃ¥ 0 för att inaktivera.", - "repeater_cliHelpSetGuestPassword": "Ställer in/uppdaterar gästlösenordet. (för Ã¥tervändare kan gästloggar skicka \"Get Stats\"-förfrÃ¥gan)", - "repeater_cliHelpSetName": "Ställer in annonstexterna namn.", - "repeater_cliHelpSetLat": "Ställer in annonskartans latitud. (decimalgrader)", - "repeater_cliHelpSetLon": "Ställer in annonskartans longitud (decimalgrader).", - "repeater_cliHelpSetRadio": "Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.", - "repeater_cliHelpSetRxDelay": "Ställer (experimentell) basvärde (mÃ¥ste vara > 1 för effekt) för att applicera en liten fördröjning pÃ¥ mottagna paket, baserat pÃ¥ signalstyrka/poäng. Ställ in pÃ¥ 0 för att inaktivera.", - "repeater_cliHelpSetTxDelay": "Ställer in en faktor som multipliceras med tid pÃ¥ luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).", - "repeater_cliHelpSetDirectTxDelay": "Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.", + "repeater_cliHelpSetAdvertInterval": "Ställer in tidsintervallen i minuter för att skicka ett lokalt (utan-hopp) annonseringspaket. Ställs till 0 för att inaktivera.", + "repeater_cliHelpSetFloodAdvertInterval": "Ställer in tidsintervallen i timmar för att skicka ett flödesannonspaket. Ställ in på 0 för att inaktivera.", + "repeater_cliHelpSetGuestPassword": "Ställer in/uppdaterar gästlösenordet. (för återvändare kan gästloggar skicka \"Get Stats\"-förfrågan)", + "repeater_cliHelpSetName": "Ställer in annonstexterna namn.", + "repeater_cliHelpSetLat": "Ställer in annonskartans latitud. (decimalgrader)", + "repeater_cliHelpSetLon": "Ställer in annonskartans longitud (decimalgrader).", + "repeater_cliHelpSetRadio": "Ställer helt nya radioparametrar och sparar dem i inställningar. Kräver en \"omstart\" för att tillämpa.", + "repeater_cliHelpSetRxDelay": "Ställer (experimentell) basvärde (måste vara > 1 för effekt) för att applicera en liten fördröjning på mottagna paket, baserat på signalstyrka/poäng. Ställ in på 0 för att inaktivera.", + "repeater_cliHelpSetTxDelay": "Ställer in en faktor som multipliceras med tid på luft för en översvämningsläge-paket och med ett slumpmässigt slot-system för att fördröja dess vidarebefordran (för att minska risken för kollisioner).", + "repeater_cliHelpSetDirectTxDelay": "Samma som txdelay, men för att applicera en slumpmässig fördröjning vid vidarebefordran av direktlägespaket.", "repeater_cliHelpSetBridgeEnabled": "Aktivera/Inaktivera brygga.", - "repeater_cliHelpSetBridgeDelay": "Ställ in fördröjning innan paket Ã¥ter sänder.", - "repeater_cliHelpSetBridgeSource": "Välj om bron ska Ã¥terända mottagna paket eller sända paket.", - "repeater_cliHelpSetBridgeBaud": "Ställ baudgränsen för rs232-bryggarna.", - "repeater_cliHelpSetBridgeSecret": "Ställ bro-hemlighet för espnow-broar.", - "repeater_cliHelpSetAdcMultiplier": "Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd pÃ¥ utvalda kort).", - "repeater_cliHelpTempRadio": "Ställer temporära radioparametrar för det angivna antalet minuter, vilket Ã¥tergÃ¥r till de ursprungliga radioparametrarna efterÃ¥t. (sparar inte i inställningar).", - "repeater_cliHelpSetPerm": "Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. TillstÃ¥ndsbiten varierar per firmware-roll, men de lÃ¥ga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).", - "repeater_cliHelpGetBridgeType": "FÃ¥r brotyperna ingen, rs232, espnow", + "repeater_cliHelpSetBridgeDelay": "Ställ in fördröjning innan paket åter sänder.", + "repeater_cliHelpSetBridgeSource": "Välj om bron ska återända mottagna paket eller sända paket.", + "repeater_cliHelpSetBridgeBaud": "Ställ baudgränsen för rs232-bryggarna.", + "repeater_cliHelpSetBridgeSecret": "Ställ bro-hemlighet för espnow-broar.", + "repeater_cliHelpSetAdcMultiplier": "Ställer in anpassad faktor för att justera rapporterad batterispänning (endast stödd på utvalda kort).", + "repeater_cliHelpTempRadio": "Ställer temporära radioparametrar för det angivna antalet minuter, vilket återgår till de ursprungliga radioparametrarna efteråt. (sparar inte i inställningar).", + "repeater_cliHelpSetPerm": "Modifierar ACL. Tar bort matchande post (genom pubkey-prefiks) om \"permissions\" är noll. Lägger till ny post om pubkey-hex är full längd och inte redan finns i ACL. Uppdaterar posten genom matchande pubkey-prefiks. Tillståndsbiten varierar per firmware-roll, men de låga 2 bitarna är: 0 (Gäst), 1 (endast läsa), 2 (läs- och skrivskydd), 3 (administratör).", + "repeater_cliHelpGetBridgeType": "Får brotyperna ingen, rs232, espnow", "repeater_cliHelpLogStart": "Starta paketloggning till filsystem.", "repeater_cliHelpLogStop": "Stoppar paketloggning till filsystem.", - "repeater_cliHelpLogErase": "Raderar pakets loggar frÃ¥n filsystemet.", - "repeater_cliHelpNeighbors": "Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-gæ’®-4", - "repeater_cliHelpNeighborRemove": "Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) frÃ¥n grannlistan.", - "repeater_cliHelpRegion": "(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.", - "repeater_cliHelpRegionLoad": "MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.", - "repeater_cliHelpRegionGet": "Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) 'F'\"", - "repeater_cliHelpRegionPut": "Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.", - "repeater_cliHelpRegionRemove": "Tar bort en regionsdefinition med det angivna namnet. (mÃ¥ste matcha exakt och inte ha nÃ¥gra barnregioner)", - "repeater_cliHelpRegionAllowf": "Ställer 'Flöde'-behörighet för det angivna omrÃ¥det. ('' för det globala/gamla scopet)", - "repeater_cliHelpRegionDenyf": "Tar bort 'F'lood-behörigheten för det angivna omrÃ¥det. (OBS: rekommenderas inte att använda detta i detta skede pÃ¥ den globala/gamla omfattningen!!).", - "repeater_cliHelpRegionHome": "Svarar med den aktuella 'hem'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).", - "repeater_cliHelpRegionHomeSet": "Ställer in 'hemregionen'.", + "repeater_cliHelpLogErase": "Raderar pakets loggar från filsystemet.", + "repeater_cliHelpNeighbors": "Visar en lista över andra repeaternoder som hörts via noll-hop-annonser. Varje rad är id-prefix-hex:tidsstämpel:snr-g撮-4", + "repeater_cliHelpNeighborRemove": "Tar bort det första matchande inlägget (genom pubkey-prefiks (hex)) från grannlistan.", + "repeater_cliHelpRegion": "(Serien endast) Listar alla definierade regioner och aktuella översvämningsbehörigheter.", + "repeater_cliHelpRegionLoad": "MEDDELANDE: detta är ett specialkommando med flera kommandon. Varje efterföljande kommando är ett regionsnamn (indenterat med blanksteg för att indikera en hierarkisk relation, med minst ett blanksteg). Avslutas genom att skicka en tom rad/kommando.", + "repeater_cliHelpRegionGet": "Söker efter region med given namnprefiks (eller \"\" för det globala scopet). Svarar med \"-> regionnamn (föräldernamn) 'F'\"", + "repeater_cliHelpRegionPut": "Lägger till eller uppdaterar en regionsdefinition med det angivna namnet.", + "repeater_cliHelpRegionRemove": "Tar bort en regionsdefinition med det angivna namnet. (måste matcha exakt och inte ha några barnregioner)", + "repeater_cliHelpRegionAllowf": "Ställer 'Flöde'-behörighet för det angivna området. ('' för det globala/gamla scopet)", + "repeater_cliHelpRegionDenyf": "Tar bort 'F'lood-behörigheten för det angivna området. (OBS: rekommenderas inte att använda detta i detta skede på den globala/gamla omfattningen!!).", + "repeater_cliHelpRegionHome": "Svarar med den aktuella 'hem'-regionen. (Notera att detta ännu inte har tillämpats, reserverat för framtida användning).", + "repeater_cliHelpRegionHomeSet": "Ställer in 'hemregionen'.", "repeater_cliHelpRegionSave": "Sparar regionlistan/kartan till lagring.", - "repeater_cliHelpGps": "Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"pÃ¥\", status, fix, antal satelliter.", - "repeater_cliHelpGpsOnOff": "Aktiverar/inaktiverar GPS-strömsättningen.", - "repeater_cliHelpGpsSync": "Synkroniserar nätverks tid med GPS-klockan.", - "repeater_cliHelpGpsSetLoc": "Ställer nodens position till GPS-koordinater och sparar inställningar.", - "repeater_cliHelpGpsAdvert": "Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (frÃ¥n SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar", - "repeater_cliHelpGpsAdvertSet": "Ställer in annonsplatskonfiguration.", - "repeater_commandsListTitle": "Inställningslista", - "repeater_commandsListNote": "OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.", - "repeater_general": "Allmänt", - "repeater_settingsCategory": "Inställningar", + "repeater_cliHelpGps": "Visar GPS-status. Om GPS är avstängd svarar den endast med \"av\", annars svarar den med \"på\", status, fix, antal satelliter.", + "repeater_cliHelpGpsOnOff": "Aktiverar/inaktiverar GPS-strömsättningen.", + "repeater_cliHelpGpsSync": "Synkroniserar nätverks tid med GPS-klockan.", + "repeater_cliHelpGpsSetLoc": "Ställer nodens position till GPS-koordinater och sparar inställningar.", + "repeater_cliHelpGpsAdvert": "Ger platsannonskonfigurationen för noden:\n- ingen: inkludera inte plats i annonser\n- dela: dela gps-plats (från SensorManager)\n- inställningar: annonsera platsen som sparats i inställningar", + "repeater_cliHelpGpsAdvertSet": "Ställer in annonsplatskonfiguration.", + "repeater_commandsListTitle": "Inställningslista", + "repeater_commandsListNote": "OBS: för de olika \"set ...\" -kommandon finns det även ett \"get ...\" -kommando.", + "repeater_general": "Allmänt", + "repeater_settingsCategory": "Inställningar", "repeater_bridge": "Bro", "repeater_logging": "Logga", - "repeater_neighborsRepeaterOnly": "Grannar (Endast Ã¥terspelare)", - "repeater_regionManagementRepeaterOnly": "Regionhantering (endast Ã¥teruppspelare)", - "repeater_regionNote": "Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.", + "repeater_neighborsRepeaterOnly": "Grannar (Endast återspelare)", + "repeater_regionManagementRepeaterOnly": "Regionhantering (endast återuppspelare)", + "repeater_regionNote": "Regionkommandon har införts för att hantera regiondefinitioner och behörigheter.", "repeater_gpsManagement": "GPS Hantering", - "repeater_gpsNote": "GPS-kommando har introducerats för att hantera platsrelaterade ämnen.", + "repeater_gpsNote": "GPS-kommando har introducerats för att hantera platsrelaterade ämnen.", "telemetry_receivedData": "Mottagen Telemetridata", - "telemetry_requestTimeout": "TelemetryförfrÃ¥gan gick ut.", + "telemetry_requestTimeout": "Telemetryförfrågan gick ut.", "telemetry_errorLoading": "Fel vid laddning av telemetri: {error}", "@telemetry_errorLoading": { "placeholders": { @@ -1191,7 +1191,7 @@ } } }, - "telemetry_noData": "Inga telemetridata tillgängliga.", + "telemetry_noData": "Inga telemetridata tillgängliga.", "telemetry_channelTitle": "Kanal {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1201,7 +1201,7 @@ } }, "telemetry_batteryLabel": "Batteri", - "telemetry_voltageLabel": "Spänning", + "telemetry_voltageLabel": "Spänning", "telemetry_mcuTemperatureLabel": "MCU Temperatur", "telemetry_temperatureLabel": "Temperatur", "telemetry_currentLabel": "Aktuell", @@ -1232,7 +1232,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1243,18 +1243,18 @@ } } }, - "channelPath_title": "Paketväg", + "channelPath_title": "Paketväg", "channelPath_viewMap": "Visa karta", - "channelPath_otherObservedPaths": "Övriga observerade stigar", - "channelPath_repeaterHops": "Ã…terupptagningssteg", - "channelPath_noHopDetails": "Detaljer för denna paket är inte angivna.", + "channelPath_otherObservedPaths": "Övriga observerade stigar", + "channelPath_repeaterHops": "Återupptagningssteg", + "channelPath_noHopDetails": "Detaljer för denna paket är inte angivna.", "channelPath_messageDetails": "Meddelandets detaljer", - "channelPath_senderLabel": "Avsändare", + "channelPath_senderLabel": "Avsändare", "channelPath_timeLabel": "Tid", "channelPath_repeatsLabel": "Upprepa", - "channelPath_pathLabel": "Sökväg {index}", + "channelPath_pathLabel": "Sökväg {index}", "channelPath_observedLabel": "Observerat", - "channelPath_observedPathTitle": "Observerad bana {index} • {hops}", + "channelPath_observedPathTitle": "Observerad bana {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1288,8 +1288,8 @@ } } }, - "channelPath_unknownPath": "Okänt", - "channelPath_floodPath": "Översvämning", + "channelPath_unknownPath": "Okänt", + "channelPath_floodPath": "Översvämning", "channelPath_directPath": "Direkt", "channelPath_observedZeroOf": "0 av {total} hopp", "@channelPath_observedZeroOf": { @@ -1310,9 +1310,9 @@ } } }, - "channelPath_mapTitle": "Sökvägskarta", - "channelPath_noRepeaterLocations": "Inga Ã¥terupprepningsplatser finns tillgängliga för denna väg.", - "channelPath_primaryPath": "Sökväg {index} (Primär)", + "channelPath_mapTitle": "Sökvägskarta", + "channelPath_noRepeaterLocations": "Inga återupprepningsplatser finns tillgängliga för denna väg.", + "channelPath_primaryPath": "Sökväg {index} (Primär)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1327,9 +1327,9 @@ } } }, - "channelPath_pathLabelTitle": "Sökväg", - "channelPath_observedPathHeader": "Observerad Sökväg", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Sökväg", + "channelPath_observedPathHeader": "Observerad Sökväg", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1340,19 +1340,19 @@ } } }, - "channelPath_noHopDetailsAvailable": "Inga hoppdetaljer finns tillgängliga för detta paket.", - "channelPath_unknownRepeater": "Okänt Upprepare", + "channelPath_noHopDetailsAvailable": "Inga hoppdetaljer finns tillgängliga för detta paket.", + "channelPath_unknownRepeater": "Okänt Upprepare", "listFilter_tooltip": "Filtrera och sortera", "listFilter_sortBy": "Sortera efter", "listFilter_latestMessages": "Senaste meddelanden", - "listFilter_heardRecently": "Hörts nyligen", + "listFilter_heardRecently": "Hörts nyligen", "listFilter_az": "A-Z", "listFilter_filters": "Filteralternativ", "listFilter_all": "Alla", - "listFilter_users": "Användare", + "listFilter_users": "Användare", "listFilter_repeaters": "Upprepare", "listFilter_roomServers": "Rumservrar", - "listFilter_unreadOnly": "Endast oinlästa", + "listFilter_unreadOnly": "Endast oinlästa", "listFilter_newGroup": "Ny grupp", "@neighbors_errorLoading": { "placeholders": { @@ -1364,18 +1364,18 @@ "repeater_neighbors": "Grannar", "repeater_neighborsSubtitle": "Visa noll hoppgrannar.", "neighbors_receivedData": "Mottagna grannars data", - "neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.", - "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", + "neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.", + "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", "neighbors_repeatersNeighbors": "Upprepar grannar", - "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", + "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", "channels_createPrivateChannel": "Skapa en privat kanal", - "channels_joinPrivateChannel": "GÃ¥ med i en Privat Kanal", + "channels_joinPrivateChannel": "Gå med i en Privat Kanal", "channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.", "channels_createPrivateChannelDesc": "Skyddat med en hemlig nyckel.", - "channels_joinPublicChannel": "GÃ¥ med i den Offentliga Kanalen", - "channels_joinPublicChannelDesc": "Vem som helst kan gÃ¥ med i denna kanal.", - "channels_joinHashtagChannel": "GÃ¥ med i en Hashtagkanal", - "channels_joinHashtagChannelDesc": "Väldigt enkelt att gÃ¥ med i hashtag-kanaler.", + "channels_joinPublicChannel": "Gå med i den Offentliga Kanalen", + "channels_joinPublicChannelDesc": "Vem som helst kan gå med i denna kanal.", + "channels_joinHashtagChannel": "Gå med i en Hashtagkanal", + "channels_joinHashtagChannelDesc": "Väldigt enkelt att gå med i hashtag-kanaler.", "channels_scanQrCode": "Skanna en QR-kod", "channels_scanQrCodeComingSoon": "Kommer snart", "channels_enterHashtag": "Ange hashtag", @@ -1394,12 +1394,12 @@ } } }, - "neighbors_heardAgo": "Hördes: {time} sedan", - "neighbors_unknownContact": "Okänd {pubkey}", + "neighbors_heardAgo": "Hördes: {time} sedan", + "neighbors_unknownContact": "Okänd {pubkey}", "settings_locationGPSEnable": "Aktivera GPS", - "settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.", - "settings_locationIntervalSec": "Interval för GPS (Sekunder)", - "settings_locationIntervalInvalid": "Intervalet mÃ¥ste vara minst 60 sekunder och mindre än 86400 sekunder.", + "settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.", + "settings_locationIntervalSec": "Interval för GPS (Sekunder)", + "settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.", "contacts_manageRoom": "Hantera Rumserver", "room_management": "Rumserverhantering", "@community_joinConfirmation": { @@ -1462,32 +1462,32 @@ "community_createDesc": "Skapa en ny gemenskap och dela via QR-kod.", "common_ok": "Okej", "community_title": "Gemenskap", - "community_join": "GÃ¥ med", - "community_joinTitle": "GÃ¥ med i gemenskapen", - "community_joinConfirmation": "Vill du gÃ¥ med i communityn \"{name}\"?", + "community_join": "Gå med", + "community_joinTitle": "Gå med i gemenskapen", + "community_joinConfirmation": "Vill du gå med i communityn \"{name}\"?", "community_scanQr": "Skanna Gemenskapens QR", "community_scanInstructions": "Rikta kameran mot en QR-kod i communityn", "community_showQr": "Visa QR-kod", - "community_publicChannel": "Föreningens Offentliga", + "community_publicChannel": "Föreningens Offentliga", "community_name": "Gemenskapens namn", "community_enterName": "Ange communities namn", "community_created": "Community \"{name}\" har skapats", "community_joined": "Medlem i communityn \"{name}\"", "community_qrTitle": "Dela Gemenskap", - "community_qrInstructions": "Skanna denna QR-kod för att gÃ¥ med i \"{name}\"", - "community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nÃ¥s av medlemmar i communityn", + "community_qrInstructions": "Skanna denna QR-kod för att gå med i \"{name}\"", + "community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nås av medlemmar i communityn", "community_hashtagChannel": "Community Hashtag", "community_invalidQrCode": "Ogiltig community QR-kod", - "community_alreadyMember": "Är redan medlem", - "community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".", - "community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)", - "community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community", - "community_noCommunities": "Inga gemenskaper har anslutats ännu", - "community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igÃ¥ng", + "community_alreadyMember": "Är redan medlem", + "community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".", + "community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)", + "community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community", + "community_noCommunities": "Inga gemenskaper har anslutats ännu", + "community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igång", "community_manageCommunities": "Hantera Gemenskaper", - "community_delete": "Lämna Gemenskap", - "community_deleteConfirm": "Lämna \"{name}\"?", - "community_deleteChannelsWarning": "Detta kommer ocksÃ¥ att radera {count} kanal/kanaler och deras meddelanden.", + "community_delete": "Lämna Gemenskap", + "community_deleteConfirm": "Lämna \"{name}\"?", + "community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1495,14 +1495,14 @@ } } }, - "community_deleted": "Lämnade community \"{name}\"", - "community_addHashtagChannel": "Lägg till Gemenskapens Hashtag", - "community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community", - "community_selectCommunity": "Välj Gemenskap", + "community_deleted": "Lämnade community \"{name}\"", + "community_addHashtagChannel": "Lägg till Gemenskapens Hashtag", + "community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community", + "community_selectCommunity": "Välj Gemenskap", "community_regularHashtag": "Vanlig Hash Tag", - "community_regularHashtagDesc": "Offentlig hashtag (alla kan gÃ¥ med)", - "community_communityHashtagDesc": "Endast för medlemmar", - "community_forCommunity": "För {name}", + "community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)", + "community_communityHashtagDesc": "Endast för medlemmar", + "community_forCommunity": "För {name}", "community_communityHashtag": "Community Hashtag", "@community_regenerateSecretConfirm": { "placeholders": { @@ -1533,11 +1533,11 @@ } }, "community_regenerate": "Regenerera", - "community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar mÃ¥ste scanna den nya QR-koden för att fortsätta kommunicera.", - "community_secretRegenerated": "Lösenord Ã¥terskapad för \"{name}\"", + "community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.", + "community_secretRegenerated": "Lösenord återskapad för \"{name}\"", "community_regenerateSecret": "Regenerera hemlig kod", - "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"", - "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"", + "community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"", + "community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"", "community_updateSecret": "Uppdatera hemlighet", "@contacts_pathTraceTo": { "placeholders": { @@ -1547,28 +1547,28 @@ } }, "pathTrace_you": "Du", - "pathTrace_failed": "Sökvägsföljning misslyckades.", - "pathTrace_notAvailable": "Path trace ej tillgänglig.", + "pathTrace_failed": "Sökvägsföljning misslyckades.", + "pathTrace_notAvailable": "Path trace ej tillgänglig.", "pathTrace_refreshTooltip": "Uppdatera Path Trace", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", - "contacts_repeaterPathTrace": "VägspÃ¥rning till repeater", + "contacts_repeaterPathTrace": "Vägspårning till repeater", "contacts_repeaterPing": "Ping-repeater", - "contacts_roomPathTrace": "VägspÃ¥rning till rumserver", + "contacts_roomPathTrace": "Vägspårning till rumserver", "contacts_roomPing": "Ping rumsserver", - "contacts_chatTraceRoute": "SpÃ¥ra rutt", - "contacts_pathTraceTo": "SpÃ¥ra rutt till {name}", - "contacts_clipboardEmpty": "Urklipp är tomt.", + "contacts_chatTraceRoute": "Spåra rutt", + "contacts_pathTraceTo": "Spåra rutt till {name}", + "contacts_clipboardEmpty": "Urklipp är tomt.", "appSettings_languageRu": "Ryska", "contacts_contactImportFailed": "Kontakt kunde inte importeras.", "contacts_zeroHopAdvert": "Reklam med nollhopp", - "contacts_floodAdvert": "Översvämningsannons", + "contacts_floodAdvert": "Översvämningsannons", "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", "appSettings_languageUk": "Ukrainska", - "appSettings_enableMessageTracing": "Aktivera meddelandespÃ¥rning", - "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", - "contacts_addContactFromClipboard": "Lägg till kontakt frÃ¥n urklipp", + "appSettings_enableMessageTracing": "Aktivera meddelandespårning", + "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", + "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", "contacts_contactImported": "Kontakt har importerats.", "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", "contacts_contactAdvertCopied": "Annons kopierad till Urklipp.", @@ -1580,47 +1580,47 @@ "notification_messagesCount": "{count} {count, plural, =1{meddelande} other{meddelanden}}", "notification_channelMessagesCount": "{count} {count, plural, =1{kanalmeddelande} other{kanalmeddelanden}}", "notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}", - "notification_newTypeDiscovered": "Ny {contactType} upptäckt", + "notification_newTypeDiscovered": "Ny {contactType} upptäckt", "notification_receivedNewMessage": "Nytt meddelande mottaget", "settings_gpxExportAll": "Exportera alla kontakter till GPX", "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", - "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgÃ¥ng", + "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", "settings_gpxExportNoContacts": "Inga kontakter att exportera.", - "settings_gpxExportNotAvailable": "Stöds inte pÃ¥ din enhet/operativsystem", + "settings_gpxExportNotAvailable": "Stöds inte på din enhet/operativsystem", "settings_gpxExportRepeatersRoom": "Repeater- och rumsserverplatser", "settings_gpxExportRepeaters": "Exportera repeater / rumsservrar till GPX", "settings_gpxExportAllSubtitle": "Exporterar alla kontakter med en plats till GPX-fil.", - "settings_gpxExportContacts": "Exportera följeslagare till GPX", - "settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.", - "settings_gpxExportChat": "Medhjälparplatser", - "settings_gpxExportError": "Det uppstod ett fel när data exporterades.", + "settings_gpxExportContacts": "Exportera följeslagare till GPX", + "settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.", + "settings_gpxExportChat": "Medhjälparplatser", + "settings_gpxExportError": "Det uppstod ett fel när data exporterades.", "settings_gpxExportAllContacts": "Alla kontakters platser", "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", - "settings_gpxExportShareText": "Kartdata exporterad frÃ¥n meshcore-open", + "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open", "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!", - "pathTrace_clearTooltip": "Rensa väg", - "map_pathTraceCancelled": "SökvägsspÃ¥rning avbruten.", - "map_runTrace": "Kör spÃ¥rsökning", - "map_tapToAdd": "Tryck pÃ¥ noder för att lägga till dem i banan.", + "pathTrace_clearTooltip": "Rensa väg", + "map_pathTraceCancelled": "Sökvägsspårning avbruten.", + "map_runTrace": "Kör spårsökning", + "map_tapToAdd": "Tryck på noder för att lägga till dem i banan.", "map_removeLast": "Ta bort sista", "scanner_enableBluetooth": "Aktivera Bluetooth", - "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", - "scanner_chromeRequired": "Chrome-webbläsare krävs", - "scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.", - "scanner_bluetoothOff": "Bluetooth är avstängt", + "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", + "scanner_chromeRequired": "Chrome-webbläsare krävs", + "scanner_chromeRequiredMessage": "Denna webbapplikation kräver Google Chrome oder en Chromium-baserader webbläsare för Bluetooth-stöd.", + "scanner_bluetoothOff": "Bluetooth är avstängt", "snrIndicator_lastSeen": "Senast sedd", - "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", - "chat_ShowAllPaths": "Visa alla vägar", - "settings_clientRepeatSubtitle": "LÃ¥t enheten repetera nätpaket för andra användare.", - "settings_clientRepeat": "Upprepa utan elnät", - "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.", - "settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)", + "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", + "chat_ShowAllPaths": "Visa alla vägar", + "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", + "settings_clientRepeat": "Upprepa utan elnät", + "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.", + "settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)", "appSettings_unitsTitle": "Enheter", "appSettings_unitsMetric": "Metriskt (m/km)", "appSettings_unitsImperial": "Imperialt (ft / mi)", "map_lineOfSight": "Synlinje", "map_losScreenTitle": "Synlinje", - "losSelectStartEnd": "Välj start- och slutnoder för LOS.", + "losSelectStartEnd": "Välj start- och slutnoder för LOS.", "losRunFailed": "Synlinjekontroll misslyckades: {error}", "@losRunFailed": { "placeholders": { @@ -1630,11 +1630,11 @@ } }, "losClearAllPoints": "Rensa alla punkter", - "losRunToViewElevationProfile": "Kör LOS för att se höjdprofil", + "losRunToViewElevationProfile": "Kör LOS för att se höjdprofil", "losMenuTitle": "LOS-menyn", - "losMenuSubtitle": "Tryck pÃ¥ noder eller tryck länge pÃ¥ kartan för anpassade punkter", + "losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter", "losShowDisplayNodes": "Visa displaynoder", - "losCustomPoints": "Anpassade poäng", + "losCustomPoints": "Anpassade poäng", "losCustomPointLabel": "Anpassad {index}", "@losCustomPointLabel": { "placeholders": { @@ -1667,8 +1667,8 @@ } } }, - "losRun": "Kör LOS", - "losNoElevationData": "Inga höjddata", + "losRun": "Kör LOS", + "losNoElevationData": "Inga höjddata", "losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { @@ -1705,7 +1705,7 @@ }, "losStatusChecking": "LOS: kollar...", "losStatusNoData": "LOS: inga data", - "losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd", + "losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.", - "losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.", - "losRenameCustomPoint": "Byt namn pÃ¥ anpassad punkt", + "losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.", + "losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.", + "losRenameCustomPoint": "Byt namn på anpassad punkt", "losPointName": "Punktnamn", "losShowPanelTooltip": "Visa LOS-panelen", - "losHidePanelTooltip": "Dölj LOS-panelen", - "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", + "losHidePanelTooltip": "Dölj LOS-panelen", + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", "losLegendRadioHorizon": "Radiohorisont", "losLegendLosBeam": "Siktlinje", - "losLegendTerrain": "Terräng", + "losLegendTerrain": "Terräng", "losFrequencyLabel": "Frekvens", - "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", - "losFrequencyDialogTitle": "Beräkning av radiohorisonten", - "losFrequencyDialogDescription": "Med start frÃ¥n k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", + "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", + "losFrequencyDialogTitle": "Beräkning av radiohorisonten", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,8 +1753,8 @@ } } }, - "listFilter_removeFromFavorites": "Ta bort frÃ¥n favoriter", - "listFilter_addToFavorites": "Lägg till i favoriter", + "listFilter_removeFromFavorites": "Ta bort från favoriter", + "listFilter_addToFavorites": "Lägg till i favoriter", "listFilter_favorites": "Favoriter", "@contacts_searchFavorites": { "placeholders": { @@ -1796,17 +1796,29 @@ } } }, - "contacts_unread": "Oläst", - "contacts_searchContactsNoNumber": "Sök kontakter...", - "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", - "contacts_searchFavorites": "Sök {number}{str} Favoriter...", - "contacts_searchUsers": "Sök {number}{str} användare...", - "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", - "connectionChoiceUsbLabel": "USB", - "connectionChoiceBluetoothLabel": "Bluetooth", + "contacts_unread": "Oläst", + "contacts_searchContactsNoNumber": "Sök kontakter...", + "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", + "contacts_searchFavorites": "Sök {number}{str} Favoriter...", + "contacts_searchUsers": "Sök {number}{str} användare...", + "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", + "usbScreenNote": "USB-seriell kommunikation är aktiv på stöderdade Android-enheter och skrivbordsplattformar.", + "usbScreenStatus": "Välj en USB-enhet", + "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", "usbScreenTitle": "Anslut via USB", - "usbScreenNote": "USB-seriell kommunikation är aktiv pÃ¥ kompatibla Android-enheter och datorplattformar.", - "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", - "usbScreenStatus": "Välj en USB-enhet", - "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera." + "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.", + "usbErrorPermissionDenied": "Tillgången via USB nekas.", + "usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.", + "usbErrorInvalidPort": "Välj en giltig USB-enhet.", + "usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.", + "usbErrorNotConnected": "Ingen USB-enhet är ansluten.", + "usbErrorOpenFailed": "Kunde inte öppna det valda USB-enheten.", + "usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.", + "usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.", + "usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.", + "usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.", + "usbErrorPortClosed": "USB-anslutningen är inte aktiv.", + "usbErrorConnectTimedOut": "Tiden har löpt ut medan vi väntade på att enheten skulle svara.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index cc02c86..66cdf64 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", +{ + "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,34 +9,34 @@ }, "@@locale": "uk", "appTitle": "MeshCore Open", - "nav_contacts": "Контакти", - "nav_channels": "Канали", - "nav_map": "Карта", - "common_cancel": "Скасувати", - "common_connect": "Підключити", - "common_unknownDevice": "Невідомий пристрій", - "common_save": "Зберегти", - "common_delete": "Видалити", - "common_close": "Закрити", - "common_edit": "Редагувати", - "common_add": "Додати", - "common_settings": "Налаштування", - "common_disconnect": "Відключити", - "common_connected": "Підключено", - "common_disconnected": "Відключено", - "common_create": "Створити", - "common_continue": "Продовжити", - "common_share": "Поділитися", - "common_copy": "Копіювати", - "common_retry": "Повторити", - "common_hide": "Приховати", - "common_remove": "Прибрати", - "common_enable": "Увімкнути", - "common_disable": "Вимкнути", - "common_reboot": "Перезавантажити", - "common_loading": "Завантаження...", - "common_notAvailable": "—", - "common_voltageValue": "{volts} Ð’", + "nav_contacts": "Контакти", + "nav_channels": "Канали", + "nav_map": "Карта", + "common_cancel": "Скасувати", + "common_connect": "Підключити", + "common_unknownDevice": "Невідомий пристрій", + "common_save": "Зберегти", + "common_delete": "Видалити", + "common_close": "Закрити", + "common_edit": "Редагувати", + "common_add": "Додати", + "common_settings": "Налаштування", + "common_disconnect": "Відключити", + "common_connected": "Підключено", + "common_disconnected": "Відключено", + "common_create": "Створити", + "common_continue": "Продовжити", + "common_share": "Поділитися", + "common_copy": "Копіювати", + "common_retry": "Повторити", + "common_hide": "Приховати", + "common_remove": "Прибрати", + "common_enable": "Увімкнути", + "common_disable": "Вимкнути", + "common_reboot": "Перезавантажити", + "common_loading": "Завантаження...", + "common_notAvailable": "—", + "common_voltageValue": "{volts} В", "@common_voltageValue": { "placeholders": { "volts": { @@ -53,11 +53,11 @@ } }, "scanner_title": "MeshCore Open", - "scanner_scanning": "Пошук пристроїв...", - "scanner_connecting": "Підключення...", - "scanner_disconnecting": "Відключення...", - "scanner_notConnected": "Не підключено", - "scanner_connectedTo": "Підключено до {deviceName}", + "scanner_scanning": "Пошук пристроїв...", + "scanner_connecting": "Підключення...", + "scanner_disconnecting": "Відключення...", + "scanner_notConnected": "Не підключено", + "scanner_connectedTo": "Підключено до {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -65,9 +65,9 @@ } } }, - "scanner_searchingDevices": "Пошук пристроїв MeshCore...", - "scanner_tapToScan": "Натисніть «Сканувати», щоб знайти пристрої MeshCore", - "scanner_connectionFailed": "Помилка підключення: {error}", + "scanner_searchingDevices": "Пошук пристроїв MeshCore...", + "scanner_tapToScan": "Натисніть «Сканувати», щоб знайти пристрої MeshCore", + "scanner_connectionFailed": "Помилка підключення: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -75,52 +75,52 @@ } } }, - "scanner_stop": "Стоп", - "scanner_scan": "Сканувати", - "device_quickSwitch": "Швидке перемикання", + "scanner_stop": "Стоп", + "scanner_scan": "Сканувати", + "device_quickSwitch": "Швидке перемикання", "device_meshcore": "MeshCore", - "settings_title": "Налаштування", - "settings_deviceInfo": "Інформація про пристрій", - "settings_appSettings": "Налаштування програми", - "settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти", - "settings_nodeSettings": "Налаштування вузла", - "settings_nodeName": "Ім'я вузла", - "settings_nodeNameNotSet": "Не встановлено", - "settings_nodeNameHint": "Введіть ім'я вузла", - "settings_nodeNameUpdated": "Ім'я оновлено", - "settings_radioSettings": "Налаштування радіо", - "settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення", - "settings_radioSettingsUpdated": "Налаштування радіо оновлено", - "settings_location": "Розташування", - "settings_locationSubtitle": "GPS координати", - "settings_locationUpdated": "Розташування оновлено", - "settings_locationBothRequired": "Введіть широту та довготу.", - "settings_locationInvalid": "Некоректна широта або довгота.", - "settings_latitude": "Широта", - "settings_longitude": "Довгота", - "settings_privacyMode": "Режим приватності", - "settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", - "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.", - "settings_privacyModeEnabled": "Режим приватності увімкнено", - "settings_privacyModeDisabled": "Режим приватності вимкнено", - "settings_actions": "Дії", - "settings_sendAdvertisement": "Оголосити себе", - "settings_sendAdvertisementSubtitle": "Транслювати присутність зараз", - "settings_advertisementSent": "Оголошення надіслано", - "settings_syncTime": "Синхронізація часу", - "settings_syncTimeSubtitle": "Встановити час пристрою відповідно до часу телефону.", - "settings_timeSynchronized": "Час синхронізовано", - "settings_refreshContacts": "Оновити контакти", - "settings_refreshContactsSubtitle": "Перезавантажити список контактів з пристрою", - "settings_rebootDevice": "Перезавантажити пристрій", - "settings_rebootDeviceSubtitle": "Перезавантажити пристрій MeshCore", - "settings_rebootDeviceConfirm": "Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.", - "settings_debug": "Налагодження", - "settings_bleDebugLog": "Журнал налагодження BLE", - "settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані", - "settings_appDebugLog": "Журнал налагодження програми", - "settings_appDebugLogSubtitle": "Повідомлення налагодження програми", - "settings_about": "Про програму", + "settings_title": "Налаштування", + "settings_deviceInfo": "Інформація про пристрій", + "settings_appSettings": "Налаштування програми", + "settings_appSettingsSubtitle": "Сповіщення, повідомлення та налаштування карти", + "settings_nodeSettings": "Налаштування вузла", + "settings_nodeName": "Ім'я вузла", + "settings_nodeNameNotSet": "Не встановлено", + "settings_nodeNameHint": "Введіть ім'я вузла", + "settings_nodeNameUpdated": "Ім'я оновлено", + "settings_radioSettings": "Налаштування радіо", + "settings_radioSettingsSubtitle": "Частота, потужність, коефіцієнт розширення", + "settings_radioSettingsUpdated": "Налаштування радіо оновлено", + "settings_location": "Розташування", + "settings_locationSubtitle": "GPS координати", + "settings_locationUpdated": "Розташування оновлено", + "settings_locationBothRequired": "Введіть широту та довготу.", + "settings_locationInvalid": "Некоректна широта або довгота.", + "settings_latitude": "Широта", + "settings_longitude": "Довгота", + "settings_privacyMode": "Режим приватності", + "settings_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", + "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та місцезнаходження в оголошеннях.", + "settings_privacyModeEnabled": "Режим приватності увімкнено", + "settings_privacyModeDisabled": "Режим приватності вимкнено", + "settings_actions": "Дії", + "settings_sendAdvertisement": "Оголосити себе", + "settings_sendAdvertisementSubtitle": "Транслювати присутність зараз", + "settings_advertisementSent": "Оголошення надіслано", + "settings_syncTime": "Синхронізація часу", + "settings_syncTimeSubtitle": "Встановити час пристрою відповідно до часу телефону.", + "settings_timeSynchronized": "Час синхронізовано", + "settings_refreshContacts": "Оновити контакти", + "settings_refreshContactsSubtitle": "Перезавантажити список контактів з пристрою", + "settings_rebootDevice": "Перезавантажити пристрій", + "settings_rebootDeviceSubtitle": "Перезавантажити пристрій MeshCore", + "settings_rebootDeviceConfirm": "Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.", + "settings_debug": "Налагодження", + "settings_bleDebugLog": "Журнал налагодження BLE", + "settings_bleDebugLogSubtitle": "Команди BLE, відповіді та необроблені дані", + "settings_appDebugLog": "Журнал налагодження програми", + "settings_appDebugLogSubtitle": "Повідомлення налагодження програми", + "settings_about": "Про програму", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -129,26 +129,26 @@ } } }, - "settings_aboutLegalese": "Проєкт MeshCore Open Source 2026", - "settings_aboutDescription": "Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.", - "settings_infoName": "Ім'я", + "settings_aboutLegalese": "Проєкт MeshCore Open Source 2026", + "settings_aboutDescription": "Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.", + "settings_infoName": "Ім'я", "settings_infoId": "ID", - "settings_infoStatus": "Статус", - "settings_infoBattery": "Батарея", - "settings_infoPublicKey": "Відкритий ключ", - "settings_infoContactsCount": "Кількість контактів", - "settings_infoChannelCount": "Кількість каналів", - "settings_presets": "Попередні налаштування", - "settings_frequency": "Частота (МГц)", + "settings_infoStatus": "Статус", + "settings_infoBattery": "Батарея", + "settings_infoPublicKey": "Відкритий ключ", + "settings_infoContactsCount": "Кількість контактів", + "settings_infoChannelCount": "Кількість каналів", + "settings_presets": "Попередні налаштування", + "settings_frequency": "Частота (МГц)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)", - "settings_bandwidth": "Смуга пропускання", - "settings_spreadingFactor": "Коефіцієнт розширення", - "settings_codingRate": "Швидкість кодування", - "settings_txPower": "Потужність TX (дБм)", + "settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)", + "settings_bandwidth": "Смуга пропускання", + "settings_spreadingFactor": "Коефіцієнт розширення", + "settings_codingRate": "Швидкість кодування", + "settings_txPower": "Потужність TX (дБм)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)", - "settings_error": "Помилка: {message}", + "settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)", + "settings_error": "Помилка: {message}", "@settings_error": { "placeholders": { "message": { @@ -156,52 +156,52 @@ } } }, - "appSettings_title": "Налаштування програми", - "appSettings_appearance": "Вигляд", - "appSettings_theme": "Тема", - "appSettings_themeSystem": "Системна", - "appSettings_themeLight": "Світла", - "appSettings_themeDark": "Темна", - "appSettings_language": "Мова", - "appSettings_languageSystem": "Як у системі", + "appSettings_title": "Налаштування програми", + "appSettings_appearance": "Вигляд", + "appSettings_theme": "Тема", + "appSettings_themeSystem": "Системна", + "appSettings_themeLight": "Світла", + "appSettings_themeDark": "Темна", + "appSettings_language": "Мова", + "appSettings_languageSystem": "Як у системі", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_languageUk": "Українська", - "appSettings_notifications": "Сповіщення", - "appSettings_enableNotifications": "Увімкнути сповіщення", - "appSettings_enableNotificationsSubtitle": "Отримувати сповіщення про повідомлення та оголошення", - "appSettings_notificationPermissionDenied": "У доступі до сповіщень відмовлено", - "appSettings_notificationsEnabled": "Сповіщення увімкнено", - "appSettings_notificationsDisabled": "Сповіщення вимкнено", - "appSettings_messageNotifications": "Сповіщення про повідомлення", - "appSettings_messageNotificationsSubtitle": "Показувати сповіщення при отриманні нових повідомлень", - "appSettings_channelMessageNotifications": "Сповіщення каналів", - "appSettings_channelMessageNotificationsSubtitle": "Показувати сповіщення при отриманні повідомлень каналу", - "appSettings_advertisementNotifications": "Сповіщення про оголошення", - "appSettings_advertisementNotificationsSubtitle": "Показувати сповіщення при виявленні нових вузлів", - "appSettings_messaging": "Обмін повідомленнями", - "appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб", - "appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання", - "appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.", - "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.", - "appSettings_autoRouteRotation": "Авторотація маршруту", - "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)", - "appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена", - "appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена", - "appSettings_battery": "Батарея", - "appSettings_batteryChemistry": "Хімія батареї", - "appSettings_batteryChemistryPerDevice": "Встановити для пристрою ({deviceName})", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_languageUk": "Українська", + "appSettings_notifications": "Сповіщення", + "appSettings_enableNotifications": "Увімкнути сповіщення", + "appSettings_enableNotificationsSubtitle": "Отримувати сповіщення про повідомлення та оголошення", + "appSettings_notificationPermissionDenied": "У доступі до сповіщень відмовлено", + "appSettings_notificationsEnabled": "Сповіщення увімкнено", + "appSettings_notificationsDisabled": "Сповіщення вимкнено", + "appSettings_messageNotifications": "Сповіщення про повідомлення", + "appSettings_messageNotificationsSubtitle": "Показувати сповіщення при отриманні нових повідомлень", + "appSettings_channelMessageNotifications": "Сповіщення каналів", + "appSettings_channelMessageNotificationsSubtitle": "Показувати сповіщення при отриманні повідомлень каналу", + "appSettings_advertisementNotifications": "Сповіщення про оголошення", + "appSettings_advertisementNotificationsSubtitle": "Показувати сповіщення при виявленні нових вузлів", + "appSettings_messaging": "Обмін повідомленнями", + "appSettings_clearPathOnMaxRetry": "Очищати шлях після макс. спроб", + "appSettings_clearPathOnMaxRetrySubtitle": "Скидати шлях до контакту після 5 невдалих спроб надсилання", + "appSettings_pathsWillBeCleared": "Шляхи будуть очищені після 5 невдалих спроб.", + "appSettings_pathsWillNotBeCleared": "Шляхи не будуть очищатися автоматично.", + "appSettings_autoRouteRotation": "Авторотація маршруту", + "appSettings_autoRouteRotationSubtitle": "Чергувати найкращі шляхи та режим «на всю мережу» (flood)", + "appSettings_autoRouteRotationEnabled": "Авторотація маршрутизації увімкнена", + "appSettings_autoRouteRotationDisabled": "Авторотація маршрутизації вимкнена", + "appSettings_battery": "Батарея", + "appSettings_batteryChemistry": "Хімія батареї", + "appSettings_batteryChemistryPerDevice": "Встановити для пристрою ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -209,20 +209,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "Підключіть пристрій, щоб вибрати", - "appSettings_batteryNmc": "18650 NMC (3.0-4.2Ð’)", - "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65Ð’)", - "appSettings_batteryLipo": "LiPo (3.0-4.2Ð’)", - "appSettings_mapDisplay": "Відображення карти", - "appSettings_showRepeaters": "Показувати ретранслятори", - "appSettings_showRepeatersSubtitle": "Відображати вузли-ретранслятори на карті", - "appSettings_showChatNodes": "Показувати вузли чату", - "appSettings_showChatNodesSubtitle": "Відображати вузли чату на карті", - "appSettings_showOtherNodes": "Показувати інші вузли", - "appSettings_showOtherNodesSubtitle": "Відображати інші типи вузлів на карті", - "appSettings_timeFilter": "Фільтр часу", - "appSettings_timeFilterShowAll": "Показати всі вузли", - "appSettings_timeFilterShowLast": "Показати вузли за останні {hours} год", + "appSettings_batteryChemistryConnectFirst": "Підключіть пристрій, щоб вибрати", + "appSettings_batteryNmc": "18650 NMC (3.0-4.2В)", + "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65В)", + "appSettings_batteryLipo": "LiPo (3.0-4.2В)", + "appSettings_mapDisplay": "Відображення карти", + "appSettings_showRepeaters": "Показувати ретранслятори", + "appSettings_showRepeatersSubtitle": "Відображати вузли-ретранслятори на карті", + "appSettings_showChatNodes": "Показувати вузли чату", + "appSettings_showChatNodesSubtitle": "Відображати вузли чату на карті", + "appSettings_showOtherNodes": "Показувати інші вузли", + "appSettings_showOtherNodesSubtitle": "Відображати інші типи вузлів на карті", + "appSettings_timeFilter": "Фільтр часу", + "appSettings_timeFilterShowAll": "Показати всі вузли", + "appSettings_timeFilterShowLast": "Показати вузли за останні {hours} год", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -230,16 +230,16 @@ } } }, - "appSettings_mapTimeFilter": "Фільтр часу карти", - "appSettings_showNodesDiscoveredWithin": "Показувати вузли, виявлені за:", - "appSettings_allTime": "Весь час", - "appSettings_lastHour": "Останню годину", - "appSettings_last6Hours": "Останні 6 годин", - "appSettings_last24Hours": "Останні 24 години", - "appSettings_lastWeek": "Минулий тиждень", - "appSettings_offlineMapCache": "Офлайн-кеш карти", - "appSettings_noAreaSelected": "Область не вибрано", - "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", + "appSettings_mapTimeFilter": "Фільтр часу карти", + "appSettings_showNodesDiscoveredWithin": "Показувати вузли, виявлені за:", + "appSettings_allTime": "Весь час", + "appSettings_lastHour": "Останню годину", + "appSettings_last6Hours": "Останні 6 годин", + "appSettings_last24Hours": "Останні 24 години", + "appSettings_lastWeek": "Минулий тиждень", + "appSettings_offlineMapCache": "Офлайн-кеш карти", + "appSettings_noAreaSelected": "Область не вибрано", + "appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -250,19 +250,19 @@ } } }, - "appSettings_debugCard": "Налагодження", - "appSettings_appDebugLogging": "Логування налагодження програми", - "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.", - "appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено", - "appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.", - "contacts_title": "Контакти", - "contacts_noContacts": "Контактів не знайдено.", - "contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.", - "contacts_searchContacts": "Пошук контактів...", - "contacts_noUnreadContacts": "Немає непрочитаних контактів", - "contacts_noContactsFound": "Контактів або груп не знайдено.", - "contacts_deleteContact": "Видалити контакт", - "contacts_removeConfirm": "Видалити {contactName} з контактів?", + "appSettings_debugCard": "Налагодження", + "appSettings_appDebugLogging": "Логування налагодження програми", + "appSettings_appDebugLoggingSubtitle": "Записувати повідомлення налагодження програми в лог для усунення несправностей.", + "appSettings_appDebugLoggingEnabled": "Логування налагодження програми увімкнено", + "appSettings_appDebugLoggingDisabled": "Налагодження програми вимкнено.", + "contacts_title": "Контакти", + "contacts_noContacts": "Контактів не знайдено.", + "contacts_contactsWillAppear": "Контакти з'являться, коли пристрої надішлють оголошення.", + "contacts_searchContacts": "Пошук контактів...", + "contacts_noUnreadContacts": "Немає непрочитаних контактів", + "contacts_noContactsFound": "Контактів або груп не знайдено.", + "contacts_deleteContact": "Видалити контакт", + "contacts_removeConfirm": "Видалити {contactName} з контактів?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -270,12 +270,12 @@ } } }, - "contacts_manageRepeater": "Керувати ретранслятором", - "contacts_roomLogin": "Вхід у кімнату", - "contacts_openChat": "Відкрити чат", - "contacts_editGroup": "Редагувати групу", - "contacts_deleteGroup": "Видалити групу", - "contacts_deleteGroupConfirm": "Видалити {groupName}?", + "contacts_manageRepeater": "Керувати ретранслятором", + "contacts_roomLogin": "Вхід у кімнату", + "contacts_openChat": "Відкрити чат", + "contacts_editGroup": "Редагувати групу", + "contacts_deleteGroup": "Видалити групу", + "contacts_deleteGroupConfirm": "Видалити {groupName}?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -283,10 +283,10 @@ } } }, - "contacts_newGroup": "Нова група", - "contacts_groupName": "Назва групи", - "contacts_groupNameRequired": "Назва групи обов'язкова.", - "contacts_groupAlreadyExists": "Група «{name}» вже існує.", + "contacts_newGroup": "Нова група", + "contacts_groupName": "Назва групи", + "contacts_groupNameRequired": "Назва групи обов'язкова.", + "contacts_groupAlreadyExists": "Група «{name}» вже існує.", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -294,11 +294,11 @@ } } }, - "contacts_filterContacts": "Фільтрувати контакти...", - "contacts_noContactsMatchFilter": "Жоден контакт не відповідає фільтру.", - "contacts_noMembers": "Немає учасників", - "contacts_lastSeenNow": "Ð’ мережі", - "contacts_lastSeenMinsAgo": "Ð’ мережі {minutes} хв. тому", + "contacts_filterContacts": "Фільтрувати контакти...", + "contacts_noContactsMatchFilter": "Жоден контакт не відповідає фільтру.", + "contacts_noMembers": "Немає учасників", + "contacts_lastSeenNow": "В мережі", + "contacts_lastSeenMinsAgo": "В мережі {minutes} хв. тому", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -306,8 +306,8 @@ } } }, - "contacts_lastSeenHourAgo": "Ð’ мережі 1 годину тому", - "contacts_lastSeenHoursAgo": "Ð’ мережі {hours} год. тому", + "contacts_lastSeenHourAgo": "В мережі 1 годину тому", + "contacts_lastSeenHoursAgo": "В мережі {hours} год. тому", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -315,8 +315,8 @@ } } }, - "contacts_lastSeenDayAgo": "Ð’ мережі 1 день тому", - "contacts_lastSeenDaysAgo": "Ð’ мережі {days} дн. тому", + "contacts_lastSeenDayAgo": "В мережі 1 день тому", + "contacts_lastSeenDaysAgo": "В мережі {days} дн. тому", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -324,12 +324,12 @@ } } }, - "channels_title": "Канали", - "channels_noChannelsConfigured": "Канали не налаштовані", - "channels_addPublicChannel": "Додати публічний канал", - "channels_searchChannels": "Пошук каналів...", - "channels_noChannelsFound": "Каналів не знайдено", - "channels_channelIndex": "Канал {index}", + "channels_title": "Канали", + "channels_noChannelsConfigured": "Канали не налаштовані", + "channels_addPublicChannel": "Додати публічний канал", + "channels_searchChannels": "Пошук каналів...", + "channels_noChannelsFound": "Каналів не знайдено", + "channels_channelIndex": "Канал {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -337,16 +337,16 @@ } } }, - "channels_hashtagChannel": "Канал з хештегом", - "channels_public": "Публічний", - "channels_private": "Приватний", - "channels_publicChannel": "Публічний канал", - "channels_privateChannel": "Приватний канал", - "channels_editChannel": "Редагувати канал", - "channels_muteChannel": "Вимкнути сповіщення каналу", - "channels_unmuteChannel": "Увімкнути сповіщення каналу", - "channels_deleteChannel": "Видалити канал", - "channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.", + "channels_hashtagChannel": "Канал з хештегом", + "channels_public": "Публічний", + "channels_private": "Приватний", + "channels_publicChannel": "Публічний канал", + "channels_privateChannel": "Приватний канал", + "channels_editChannel": "Редагувати канал", + "channels_muteChannel": "Вимкнути сповіщення каналу", + "channels_unmuteChannel": "Увімкнути сповіщення каналу", + "channels_deleteChannel": "Видалити канал", + "channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -354,7 +354,7 @@ } } }, - "channels_channelDeleted": "Канал «{name}» видалено", + "channels_channelDeleted": "Канал «{name}» видалено", "@channels_channelDeleted": { "placeholders": { "name": { @@ -362,16 +362,16 @@ } } }, - "channels_addChannel": "Додати канал", - "channels_channelIndexLabel": "Індекс каналу", - "channels_channelName": "Назва каналу", - "channels_usePublicChannel": "Використовувати публічний канал", - "channels_standardPublicPsk": "Стандартний публічний PSK", + "channels_addChannel": "Додати канал", + "channels_channelIndexLabel": "Індекс каналу", + "channels_channelName": "Назва каналу", + "channels_usePublicChannel": "Використовувати публічний канал", + "channels_standardPublicPsk": "Стандартний публічний PSK", "channels_pskHex": "PSK (Hex)", - "channels_generateRandomPsk": "Згенерувати випадковий ключ PSK", - "channels_enterChannelName": "Будь ласка, введіть назву каналу", - "channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.", - "channels_channelAdded": "Канал «{name}» додано", + "channels_generateRandomPsk": "Згенерувати випадковий ключ PSK", + "channels_enterChannelName": "Будь ласка, введіть назву каналу", + "channels_pskMustBe32Hex": "PSK має складатися з 32 шістнадцяткових символів.", + "channels_channelAdded": "Канал «{name}» додано", "@channels_channelAdded": { "placeholders": { "name": { @@ -379,7 +379,7 @@ } } }, - "channels_editChannelTitle": "Редагувати канал {index}", + "channels_editChannelTitle": "Редагувати канал {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -387,8 +387,8 @@ } } }, - "channels_smazCompression": "Стиснення SMAZ", - "channels_channelUpdated": "Канал «{name}» оновлено", + "channels_smazCompression": "Стиснення SMAZ", + "channels_channelUpdated": "Канал «{name}» оновлено", "@channels_channelUpdated": { "placeholders": { "name": { @@ -396,16 +396,16 @@ } } }, - "channels_publicChannelAdded": "Публічний канал додано", - "channels_sortBy": "Сортувати за", - "channels_sortManual": "Вручну", - "channels_sortAZ": "А-Я", - "channels_sortLatestMessages": "Останні повідомлення", - "channels_sortUnread": "Непрочитані", - "chat_noMessages": "Поки немає повідомлень.", - "chat_sendMessageToStart": "Надішліть повідомлення, щоб почати", - "chat_originalMessageNotFound": "Оригінальне повідомлення не знайдено", - "chat_replyingTo": "Відповідь {name}", + "channels_publicChannelAdded": "Публічний канал додано", + "channels_sortBy": "Сортувати за", + "channels_sortManual": "Вручну", + "channels_sortAZ": "А-Я", + "channels_sortLatestMessages": "Останні повідомлення", + "channels_sortUnread": "Непрочитані", + "chat_noMessages": "Поки немає повідомлень.", + "chat_sendMessageToStart": "Надішліть повідомлення, щоб почати", + "chat_originalMessageNotFound": "Оригінальне повідомлення не знайдено", + "chat_replyingTo": "Відповідь {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -413,7 +413,7 @@ } } }, - "chat_replyTo": "Відповісти {name}", + "chat_replyTo": "Відповісти {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -421,8 +421,8 @@ } } }, - "chat_location": "Розташування", - "chat_sendMessageTo": "Надіслати повідомлення {contactName}", + "chat_location": "Розташування", + "chat_sendMessageTo": "Надіслати повідомлення {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -430,8 +430,8 @@ } } }, - "chat_typeMessage": "Введіть повідомлення...", - "chat_messageTooLong": "Повідомлення занадто довге (макс. {maxBytes} байт).", + "chat_typeMessage": "Введіть повідомлення...", + "chat_messageTooLong": "Повідомлення занадто довге (макс. {maxBytes} байт).", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -439,10 +439,10 @@ } } }, - "chat_messageCopied": "Повідомлення скопійовано", - "chat_messageDeleted": "Повідомлення видалено", - "chat_retryingMessage": "Спроба відновлення.", - "chat_retryCount": "Повторна спроба {current}/{max}", + "chat_messageCopied": "Повідомлення скопійовано", + "chat_messageDeleted": "Повідомлення видалено", + "chat_retryingMessage": "Спроба відновлення.", + "chat_retryCount": "Повторна спроба {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -453,33 +453,33 @@ } } }, - "chat_sendGif": "Надіслати GIF", - "chat_reply": "Відповісти", - "chat_addReaction": "Додати реакцію", - "chat_me": "Я", - "emojiCategorySmileys": "Емодзі", - "emojiCategoryGestures": "Жести", - "emojiCategoryHearts": "Серця", - "emojiCategoryObjects": "Об'єкти", - "gifPicker_title": "Вибрати GIF", - "gifPicker_searchHint": "Пошук GIF...", - "gifPicker_poweredBy": "На базі GIPHY", - "gifPicker_noGifsFound": "GIF не знайдено", - "gifPicker_failedLoad": "Не вдалося завантажити GIF-файли", - "gifPicker_failedSearch": "Пошук GIF не вдався", - "gifPicker_noInternet": "Немає інтернет-з'єднання", - "debugLog_appTitle": "Журнал налагодження програми", - "debugLog_bleTitle": "Журнал налагодження BLE", - "debugLog_copyLog": "Копіювати журнал", - "debugLog_clearLog": "Очистити журнал", - "debugLog_copied": "Журнал налагодження скопійовано", - "debugLog_bleCopied": "Журнал BLE скопійовано", - "debugLog_noEntries": "Поки що немає записів журналу налагодження.", - "debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях", - "debugLog_frames": "Кадри", - "debugLog_rawLogRx": "Необроблений лог - RX", - "debugLog_noBleActivity": "Поки що немає активності BLE.", - "debugFrame_length": "Довжина кадру: {count} байт", + "chat_sendGif": "Надіслати GIF", + "chat_reply": "Відповісти", + "chat_addReaction": "Додати реакцію", + "chat_me": "Я", + "emojiCategorySmileys": "Емодзі", + "emojiCategoryGestures": "Жести", + "emojiCategoryHearts": "Серця", + "emojiCategoryObjects": "Об'єкти", + "gifPicker_title": "Вибрати GIF", + "gifPicker_searchHint": "Пошук GIF...", + "gifPicker_poweredBy": "На базі GIPHY", + "gifPicker_noGifsFound": "GIF не знайдено", + "gifPicker_failedLoad": "Не вдалося завантажити GIF-файли", + "gifPicker_failedSearch": "Пошук GIF не вдався", + "gifPicker_noInternet": "Немає інтернет-з'єднання", + "debugLog_appTitle": "Журнал налагодження програми", + "debugLog_bleTitle": "Журнал налагодження BLE", + "debugLog_copyLog": "Копіювати журнал", + "debugLog_clearLog": "Очистити журнал", + "debugLog_copied": "Журнал налагодження скопійовано", + "debugLog_bleCopied": "Журнал BLE скопійовано", + "debugLog_noEntries": "Поки що немає записів журналу налагодження.", + "debugLog_enableInSettings": "Увімкніть налагодження програми в налаштуваннях", + "debugLog_frames": "Кадри", + "debugLog_rawLogRx": "Необроблений лог - RX", + "debugLog_noBleActivity": "Поки що немає активності BLE.", + "debugFrame_length": "Довжина кадру: {count} байт", "@debugFrame_length": { "placeholders": { "count": { @@ -487,7 +487,7 @@ } } }, - "debugFrame_command": "Команда: 0x{value}", + "debugFrame_command": "Команда: 0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -495,8 +495,8 @@ } } }, - "debugFrame_textMessageHeader": "Повідомлення:", - "debugFrame_destinationPubKey": "- PubKey призначення: {pubKey}", + "debugFrame_textMessageHeader": "Повідомлення:", + "debugFrame_destinationPubKey": "- PubKey призначення: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -504,7 +504,7 @@ } } }, - "debugFrame_timestamp": "- Мітка часу: {timestamp}", + "debugFrame_timestamp": "- Мітка часу: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -512,7 +512,7 @@ } } }, - "debugFrame_flags": "- Прапорці: 0x{value}", + "debugFrame_flags": "- Прапорці: 0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -520,7 +520,7 @@ } } }, - "debugFrame_textType": "- Тип тексту: {type} ({label})", + "debugFrame_textType": "- Тип тексту: {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -532,8 +532,8 @@ } }, "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "Звичайний", - "debugFrame_text": "- Текст: \"{text}\"", + "debugFrame_textTypePlain": "Звичайний", + "debugFrame_text": "- Текст: \"{text}\"", "@debugFrame_text": { "placeholders": { "text": { @@ -541,16 +541,16 @@ } } }, - "debugFrame_hexDump": "Дамп Hex:", - "chat_pathManagement": "Керування шляхами", - "chat_routingMode": "Режим маршрутизації", - "chat_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "chat_forceFloodMode": "Примусово на всю мережу", - "chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):", - "chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.", - "chat_hopSingular": "Стрибок", - "chat_hopPlural": "стрибків", - "chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}", + "debugFrame_hexDump": "Дамп Hex:", + "chat_pathManagement": "Керування шляхами", + "chat_routingMode": "Режим маршрутизації", + "chat_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "chat_forceFloodMode": "Примусово на всю мережу", + "chat_recentAckPaths": "Недавні шляхи ACK (натисніть, щоб використати):", + "chat_pathHistoryFull": "Історія шляхів заповнена. Видаліть записи, щоб додати нові.", + "chat_hopSingular": "Стрибок", + "chat_hopPlural": "стрибків", + "chat_hopsCount": "{count} {count, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -558,20 +558,20 @@ } } }, - "chat_successes": "Успішно", - "chat_removePath": "Видалити шлях", - "chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.", - "chat_pathActions": "Дії зі шляхом:", - "chat_setCustomPath": "Встановити власний шлях", - "chat_setCustomPathSubtitle": "Вказати шлях маршрутизації вручну", - "chat_clearPath": "Очистити шлях", - "chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні", - "chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.", - "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми", - "chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", - "chat_fullPath": "Повний шлях", - "chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.", - "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}", + "chat_successes": "Успішно", + "chat_removePath": "Видалити шлях", + "chat_noPathHistoryYet": "Історія шляхів недоступна.\nНадішліть повідомлення, щоб виявити шляхи.", + "chat_pathActions": "Дії зі шляхом:", + "chat_setCustomPath": "Встановити власний шлях", + "chat_setCustomPathSubtitle": "Вказати шлях маршрутизації вручну", + "chat_clearPath": "Очистити шлях", + "chat_clearPathSubtitle": "Примусово повторити пошук при наступному надсиланні", + "chat_pathCleared": "Шлях очищено. Наступне повідомлення оновить маршрут.", + "chat_floodModeSubtitle": "Використовувати перемикач маршрутизації в панелі програми", + "chat_floodModeEnabled": "Увімкнено режим «на всю мережу». Перемикайте через іконку маршрутизації на панелі інструментів.", + "chat_fullPath": "Повний шлях", + "chat_pathDetailsNotAvailable": "Деталі шляху ще недоступні. Спробуйте надіслати повідомлення для оновлення.", + "chat_pathSetHops": "Шлях встановлено: {hopCount} {hopCount, plural, =1{стрибок} few{стрибки} many{стрибків} other{стрибків}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -582,16 +582,16 @@ } } }, - "chat_pathSavedLocally": "Збережено локально. Підключіться для синхронізації.", - "chat_pathDeviceConfirmed": "Пристрій підтверджено.", - "chat_pathDeviceNotConfirmed": "Пристрій ще не підтверджено.", - "chat_type": "Ввід", - "chat_path": "Шлях", - "chat_publicKey": "Відкритий ключ", - "chat_compressOutgoingMessages": "Стискати вихідні повідомлення", - "chat_floodForced": "На всю мережу (примусово)", - "chat_directForced": "Прямий (примусово)", - "chat_hopsForced": "{count} стрибків (примусово)", + "chat_pathSavedLocally": "Збережено локально. Підключіться для синхронізації.", + "chat_pathDeviceConfirmed": "Пристрій підтверджено.", + "chat_pathDeviceNotConfirmed": "Пристрій ще не підтверджено.", + "chat_type": "Ввід", + "chat_path": "Шлях", + "chat_publicKey": "Відкритий ключ", + "chat_compressOutgoingMessages": "Стискати вихідні повідомлення", + "chat_floodForced": "На всю мережу (примусово)", + "chat_directForced": "Прямий (примусово)", + "chat_hopsForced": "{count} стрибків (примусово)", "@chat_hopsForced": { "placeholders": { "count": { @@ -599,10 +599,10 @@ } } }, - "chat_floodAuto": "На всю мережу (авто)", - "chat_direct": "Прямий", - "chat_poiShared": "Точкою інтересу поділилися", - "chat_unread": "Непрочитано: {count}", + "chat_floodAuto": "На всю мережу (авто)", + "chat_direct": "Прямий", + "chat_poiShared": "Точкою інтересу поділилися", + "chat_unread": "Непрочитано: {count}", "@chat_unread": { "placeholders": { "count": { @@ -610,10 +610,10 @@ } } }, - "chat_openLink": "Відкрити посилання?", - "chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?", - "chat_open": "Відкрити", - "chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}", + "chat_openLink": "Відкрити посилання?", + "chat_openLinkConfirmation": "Ви хочете відкрити це посилання у браузері?", + "chat_open": "Відкрити", + "chat_couldNotOpenLink": "Не вдалося відкрити посилання: {url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -621,11 +621,11 @@ } } }, - "chat_invalidLink": "Невірний формат посилання", - "map_title": "Карта вузлів", - "map_noNodesWithLocation": "Немає вузлів з даними про розташування", - "map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.", - "map_nodesCount": "Вузли: {count}", + "chat_invalidLink": "Невірний формат посилання", + "map_title": "Карта вузлів", + "map_noNodesWithLocation": "Немає вузлів з даними про розташування", + "map_nodesNeedGps": "Вузли повинні надавати свої GPS координати,\nщоб з'явитися на карті.", + "map_nodesCount": "Вузли: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -633,7 +633,7 @@ } } }, - "map_pinsCount": "Мітки: {count}", + "map_pinsCount": "Мітки: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -641,27 +641,27 @@ } } }, - "map_chat": "Чат", - "map_repeater": "Ретранслятор", - "map_room": "Кімната", - "map_sensor": "Сенсор", - "map_pinDm": "Ключ (DM)", - "map_pinPrivate": "Замок (Приватний)", - "map_pinPublic": "Ключ (Публічний)", - "map_lastSeen": "Останній раз бачили", - "map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", - "map_from": "Від", - "map_source": "Джерело", - "map_flags": "Прапорці", - "map_shareMarkerHere": "Поділитися маркером тут", - "map_pinLabel": "Мітка піна", - "map_label": "Мітка", - "map_pointOfInterest": "Точка інтересу", - "map_sendToContact": "Надіслати контакту", - "map_sendToChannel": "Надіслати в канал", - "map_noChannelsAvailable": "Немає доступних каналів", - "map_publicLocationShare": "Поділитися в публічному місці", - "map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, Ñ– кожен, хто має ключ PSK, може це побачити.", + "map_chat": "Чат", + "map_repeater": "Ретранслятор", + "map_room": "Кімната", + "map_sensor": "Сенсор", + "map_pinDm": "Ключ (DM)", + "map_pinPrivate": "Замок (Приватний)", + "map_pinPublic": "Ключ (Публічний)", + "map_lastSeen": "Останній раз бачили", + "map_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "map_from": "Від", + "map_source": "Джерело", + "map_flags": "Прапорці", + "map_shareMarkerHere": "Поділитися маркером тут", + "map_pinLabel": "Мітка піна", + "map_label": "Мітка", + "map_pointOfInterest": "Точка інтересу", + "map_sendToContact": "Надіслати контакту", + "map_sendToChannel": "Надіслати в канал", + "map_noChannelsAvailable": "Немає доступних каналів", + "map_publicLocationShare": "Поділитися в публічному місці", + "map_publicLocationShareConfirm": "Ви збираєтеся поділитися розташуванням у {channelLabel}. Цей канал публічний, і кожен, хто має ключ PSK, може це побачити.", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -669,26 +669,26 @@ } } }, - "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами", - "map_filterNodes": "Фільтрувати вузли", - "map_nodeTypes": "Типи вузлів", - "map_chatNodes": "Вузли чату", - "map_repeaters": "Ретранслятори", - "map_otherNodes": "Інші вузли", - "map_keyPrefix": "Префікс ключа", - "map_filterByKeyPrefix": "Фільтрувати за префіксом ключа", - "map_publicKeyPrefix": "Префікс відкритого ключа", - "map_markers": "Маркери", - "map_showSharedMarkers": "Показувати спільні маркери", - "map_lastSeenTime": "Час останньої активності", - "map_sharedPin": "Спільний пін", - "map_joinRoom": "Приєднатися до кімнати", - "map_manageRepeater": "Керувати ретранслятором", - "mapCache_title": "Офлайн-кеш карти", - "mapCache_selectAreaFirst": "Спершу виберіть область для кешування", - "mapCache_noTilesToDownload": "Немає плиток для завантаження в цій області.", - "mapCache_downloadTilesTitle": "Завантажити плитки", - "mapCache_downloadTilesPrompt": "Завантажити {count} плиток для використання офлайн?", + "map_connectToShareMarkers": "Підключіться до пристрою, щоб поділитися маркерами", + "map_filterNodes": "Фільтрувати вузли", + "map_nodeTypes": "Типи вузлів", + "map_chatNodes": "Вузли чату", + "map_repeaters": "Ретранслятори", + "map_otherNodes": "Інші вузли", + "map_keyPrefix": "Префікс ключа", + "map_filterByKeyPrefix": "Фільтрувати за префіксом ключа", + "map_publicKeyPrefix": "Префікс відкритого ключа", + "map_markers": "Маркери", + "map_showSharedMarkers": "Показувати спільні маркери", + "map_lastSeenTime": "Час останньої активності", + "map_sharedPin": "Спільний пін", + "map_joinRoom": "Приєднатися до кімнати", + "map_manageRepeater": "Керувати ретранслятором", + "mapCache_title": "Офлайн-кеш карти", + "mapCache_selectAreaFirst": "Спершу виберіть область для кешування", + "mapCache_noTilesToDownload": "Немає плиток для завантаження в цій області.", + "mapCache_downloadTilesTitle": "Завантажити плитки", + "mapCache_downloadTilesPrompt": "Завантажити {count} плиток для використання офлайн?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -696,8 +696,8 @@ } } }, - "mapCache_downloadAction": "Завантажити", - "mapCache_cachedTiles": "Закешовано {count} плиток", + "mapCache_downloadAction": "Завантажити", + "mapCache_cachedTiles": "Закешовано {count} плиток", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -705,7 +705,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Плитки в кеші ({downloaded}) ({failed} помилок)", + "mapCache_cachedTilesWithFailed": "Плитки в кеші ({downloaded}) ({failed} помилок)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -716,14 +716,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "Очистити офлайн-кеш", - "mapCache_clearOfflineCachePrompt": "Видалити всі закешовані плитки карти?", - "mapCache_offlineCacheCleared": "Офлайн-кеш очищено.", - "mapCache_noAreaSelected": "Область не вибрано", - "mapCache_cacheArea": "Область кешування", - "mapCache_useCurrentView": "Використати поточний вигляд", - "mapCache_zoomRange": "Діапазон масштабування", - "mapCache_estimatedTiles": "Оцінка плиток: {count}", + "mapCache_clearOfflineCacheTitle": "Очистити офлайн-кеш", + "mapCache_clearOfflineCachePrompt": "Видалити всі закешовані плитки карти?", + "mapCache_offlineCacheCleared": "Офлайн-кеш очищено.", + "mapCache_noAreaSelected": "Область не вибрано", + "mapCache_cacheArea": "Область кешування", + "mapCache_useCurrentView": "Використати поточний вигляд", + "mapCache_zoomRange": "Діапазон масштабування", + "mapCache_estimatedTiles": "Оцінка плиток: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -731,7 +731,7 @@ } } }, - "mapCache_downloadedTiles": "Завантажено {completed} / {total}", + "mapCache_downloadedTiles": "Завантажено {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -742,9 +742,9 @@ } } }, - "mapCache_downloadTilesButton": "Завантажити плитки", - "mapCache_clearCacheButton": "Очистити кеш", - "mapCache_failedDownloads": "Невдалі завантаження: {count}", + "mapCache_downloadTilesButton": "Завантажити плитки", + "mapCache_clearCacheButton": "Очистити кеш", + "mapCache_failedDownloads": "Невдалі завантаження: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -752,7 +752,7 @@ } } }, - "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", + "mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -769,8 +769,8 @@ } } }, - "time_justNow": "Тільки що", - "time_minutesAgo": "{minutes} хв. тому", + "time_justNow": "Тільки що", + "time_minutesAgo": "{minutes} хв. тому", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -778,7 +778,7 @@ } } }, - "time_hoursAgo": "{hours} год. тому", + "time_hoursAgo": "{hours} год. тому", "@time_hoursAgo": { "placeholders": { "hours": { @@ -786,7 +786,7 @@ } } }, - "time_daysAgo": "{days} дн. тому", + "time_daysAgo": "{days} дн. тому", "@time_daysAgo": { "placeholders": { "days": { @@ -794,33 +794,33 @@ } } }, - "time_hour": "година", - "time_hours": "годин", - "time_day": "день", - "time_days": "днів", - "time_week": "тиждень", - "time_weeks": "тижнів", - "time_month": "місяць", - "time_months": "місяців", - "time_minutes": "хвилин", - "time_allTime": "Весь час", - "dialog_disconnect": "Відключити", - "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", - "login_repeaterLogin": "Вхід у ретранслятор", - "login_roomLogin": "Вхід у кімнату", - "login_password": "Пароль", - "login_enterPassword": "Введіть пароль", - "login_savePassword": "Зберегти пароль", - "login_savePasswordSubtitle": "Пароль буде надійно збережено на цьому пристрої.", - "login_repeaterDescription": "Введіть пароль ретранслятора для доступу до налаштувань та статусу.", - "login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.", - "login_routing": "Маршрутизація", - "login_routingMode": "Режим маршрутизації", - "login_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "login_forceFloodMode": "Примусово на всю мережу", - "login_managePaths": "Керувати шляхами", - "login_login": "Вхід", - "login_attempt": "Спроба {current}/{max}", + "time_hour": "година", + "time_hours": "годин", + "time_day": "день", + "time_days": "днів", + "time_week": "тиждень", + "time_weeks": "тижнів", + "time_month": "місяць", + "time_months": "місяців", + "time_minutes": "хвилин", + "time_allTime": "Весь час", + "dialog_disconnect": "Відключити", + "dialog_disconnectConfirm": "Ви впевнені, що хочете відключитися від цього пристрою?", + "login_repeaterLogin": "Вхід у ретранслятор", + "login_roomLogin": "Вхід у кімнату", + "login_password": "Пароль", + "login_enterPassword": "Введіть пароль", + "login_savePassword": "Зберегти пароль", + "login_savePasswordSubtitle": "Пароль буде надійно збережено на цьому пристрої.", + "login_repeaterDescription": "Введіть пароль ретранслятора для доступу до налаштувань та статусу.", + "login_roomDescription": "Введіть пароль кімнати для доступу до налаштувань та статусу.", + "login_routing": "Маршрутизація", + "login_routingMode": "Режим маршрутизації", + "login_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "login_forceFloodMode": "Примусово на всю мережу", + "login_managePaths": "Керувати шляхами", + "login_login": "Вхід", + "login_attempt": "Спроба {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -831,7 +831,7 @@ } } }, - "login_failed": "Вхід не вдався: {error}", + "login_failed": "Вхід не вдався: {error}", "@login_failed": { "placeholders": { "error": { @@ -839,10 +839,10 @@ } } }, - "login_failedMessage": "Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.", - "common_reload": "Перезавантажити", - "common_clear": "Очистити", - "path_currentPath": "Поточний шлях: {path}", + "login_failedMessage": "Вхід не вдався. Або пароль неправильний, або ретранслятор недосяжний.", + "common_reload": "Перезавантажити", + "common_clear": "Очистити", + "path_currentPath": "Поточний шлях: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -850,7 +850,7 @@ } } }, - "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}", + "path_usingHopsPath": "Використання шляху з {count} {count, plural, =1{стрибком} few{стрибками} many{стрибками} other{стрибками}}", "@path_usingHopsPath": { "placeholders": { "count": { @@ -858,16 +858,16 @@ } } }, - "path_enterCustomPath": "Ввести власний шлях", - "path_currentPathLabel": "Поточний шлях", - "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.", - "path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).", - "path_labelHexPrefixes": "Hex-префікси", - "path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)", - "path_selectFromContacts": "Вибрати з контактів:", - "path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.", - "path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.", - "path_invalidHexPrefixes": "Некоректні hex-префікси: {prefixes}", + "path_enterCustomPath": "Ввести власний шлях", + "path_currentPathLabel": "Поточний шлях", + "path_hexPrefixInstructions": "Введіть 2-символьні hex-префікси для кожного стрибка, розділені комами.", + "path_hexPrefixExample": "Приклад: A1,F2,3C (кожен вузол використовує перший байт свого відкритого ключа).", + "path_labelHexPrefixes": "Hex-префікси", + "path_helperMaxHops": "Макс. 64 стрибки. Кожен префікс - 2 шістнадцяткові символи (1 байт)", + "path_selectFromContacts": "Вибрати з контактів:", + "path_noRepeatersFound": "Ретрансляторів або серверів кімнат не знайдено.", + "path_customPathsRequire": "Власні шляхи вимагають проміжних вузлів, які можуть передавати повідомлення.", + "path_invalidHexPrefixes": "Некоректні hex-префікси: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -875,26 +875,26 @@ } } }, - "path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.", - "path_setPath": "Встановити шлях", - "repeater_management": "Керування ретранслятором", - "repeater_managementTools": "Інструменти керування", - "repeater_status": "Статус", - "repeater_statusSubtitle": "Показати статус, статистику та сусідів ретранслятора", - "repeater_telemetry": "Телеметрія", - "repeater_telemetrySubtitle": "Показати телеметрію сенсорів та статистику системи", + "path_tooLong": "Шлях занадто довгий. Максимум 64 стрибки.", + "path_setPath": "Встановити шлях", + "repeater_management": "Керування ретранслятором", + "repeater_managementTools": "Інструменти керування", + "repeater_status": "Статус", + "repeater_statusSubtitle": "Показати статус, статистику та сусідів ретранслятора", + "repeater_telemetry": "Телеметрія", + "repeater_telemetrySubtitle": "Показати телеметрію сенсорів та статистику системи", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Надіслати команди ретранслятору", - "repeater_settings": "Налаштування", - "repeater_settingsSubtitle": "Налаштувати параметри ретранслятора", - "repeater_statusTitle": "Статус ретранслятора", - "repeater_routingMode": "Режим маршрутизації", - "repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)", - "repeater_forceFloodMode": "Примусово на всю мережу", - "repeater_pathManagement": "Керування шляхами", - "repeater_refresh": "Оновити", - "repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.", - "repeater_errorLoadingStatus": "Помилка завантаження статусу: {error}", + "repeater_cliSubtitle": "Надіслати команди ретранслятору", + "repeater_settings": "Налаштування", + "repeater_settingsSubtitle": "Налаштувати параметри ретранслятора", + "repeater_statusTitle": "Статус ретранслятора", + "repeater_routingMode": "Режим маршрутизації", + "repeater_autoUseSavedPath": "Авто (використовувати збережений шлях)", + "repeater_forceFloodMode": "Примусово на всю мережу", + "repeater_pathManagement": "Керування шляхами", + "repeater_refresh": "Оновити", + "repeater_statusRequestTimeout": "Час очікування запиту статусу вичерпано.", + "repeater_errorLoadingStatus": "Помилка завантаження статусу: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -902,23 +902,23 @@ } } }, - "repeater_systemInformation": "Системна інформація", - "repeater_battery": "Батарея", - "repeater_clockAtLogin": "Годинник (при вході)", - "repeater_uptime": "Час роботи", - "repeater_queueLength": "Довжина черги", - "repeater_debugFlags": "Прапорці налагодження", - "repeater_radioStatistics": "Статистика радіо", - "repeater_lastRssi": "Останній RSSI", - "repeater_lastSnr": "Останній SNR", - "repeater_noiseFloor": "Рівень шуму", - "repeater_txAirtime": "Ефірний час TX", - "repeater_rxAirtime": "Ефірний час RX", - "repeater_packetStatistics": "Статистика пакетів", - "repeater_sent": "Надіслано", - "repeater_received": "Отримано", - "repeater_duplicates": "Дублікати", - "repeater_daysHoursMinsSecs": "{days} дн. {hours} год {minutes} хв {seconds} с", + "repeater_systemInformation": "Системна інформація", + "repeater_battery": "Батарея", + "repeater_clockAtLogin": "Годинник (при вході)", + "repeater_uptime": "Час роботи", + "repeater_queueLength": "Довжина черги", + "repeater_debugFlags": "Прапорці налагодження", + "repeater_radioStatistics": "Статистика радіо", + "repeater_lastRssi": "Останній RSSI", + "repeater_lastSnr": "Останній SNR", + "repeater_noiseFloor": "Рівень шуму", + "repeater_txAirtime": "Ефірний час TX", + "repeater_rxAirtime": "Ефірний час RX", + "repeater_packetStatistics": "Статистика пакетів", + "repeater_sent": "Надіслано", + "repeater_received": "Отримано", + "repeater_duplicates": "Дублікати", + "repeater_daysHoursMinsSecs": "{days} дн. {hours} год {minutes} хв {seconds} с", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -935,7 +935,7 @@ } } }, - "repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "repeater_packetTxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -949,7 +949,7 @@ } } }, - "repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", + "repeater_packetRxTotal": "Всього: {total}, На всю мережу: {flood}, Прямі: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -963,7 +963,7 @@ } } }, - "repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}", + "repeater_duplicatesFloodDirect": "На всю мережу: {flood}, Прямі: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -974,7 +974,7 @@ } } }, - "repeater_duplicatesTotal": "Всього: {total}", + "repeater_duplicatesTotal": "Всього: {total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -982,37 +982,37 @@ } } }, - "repeater_settingsTitle": "Налаштування ретранслятора", - "repeater_basicSettings": "Основні налаштування", - "repeater_repeaterName": "Ім'я ретранслятора", - "repeater_repeaterNameHelper": "Показати ім'я цього ретранслятора", - "repeater_adminPassword": "Пароль адміністратора", - "repeater_adminPasswordHelper": "Пароль повного доступу", - "repeater_guestPassword": "Гостьовий пароль", - "repeater_guestPasswordHelper": "Доступ лише для читання з паролем", - "repeater_radioSettings": "Налаштування радіо", - "repeater_frequencyMhz": "Частота (МГц)", - "repeater_frequencyHelper": "300-2500 МГц", - "repeater_txPower": "Потужність TX", - "repeater_txPowerHelper": "1-30 дБм", - "repeater_bandwidth": "Смуга пропускання", - "repeater_spreadingFactor": "Коефіцієнт розширення", - "repeater_codingRate": "Швидкість кодування", - "repeater_locationSettings": "Налаштування розташування", - "repeater_latitude": "Широта", - "repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)", - "repeater_longitude": "Довгота", - "repeater_longitudeHelper": "Десяткові градуси (наприклад, -122.4194)", - "repeater_features": "Функції", - "repeater_packetForwarding": "Пересилання пакетів", - "repeater_packetForwardingSubtitle": "Дозволити ретранслятору пересилати пакети", - "repeater_guestAccess": "Гостьовий доступ", - "repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання", - "repeater_privacyMode": "Режим приватності", - "repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", - "repeater_advertisementSettings": "Налаштування оголошень", - "repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)", - "repeater_localAdvertIntervalMinutes": "{minutes} хвилин", + "repeater_settingsTitle": "Налаштування ретранслятора", + "repeater_basicSettings": "Основні налаштування", + "repeater_repeaterName": "Ім'я ретранслятора", + "repeater_repeaterNameHelper": "Показати ім'я цього ретранслятора", + "repeater_adminPassword": "Пароль адміністратора", + "repeater_adminPasswordHelper": "Пароль повного доступу", + "repeater_guestPassword": "Гостьовий пароль", + "repeater_guestPasswordHelper": "Доступ лише для читання з паролем", + "repeater_radioSettings": "Налаштування радіо", + "repeater_frequencyMhz": "Частота (МГц)", + "repeater_frequencyHelper": "300-2500 МГц", + "repeater_txPower": "Потужність TX", + "repeater_txPowerHelper": "1-30 дБм", + "repeater_bandwidth": "Смуга пропускання", + "repeater_spreadingFactor": "Коефіцієнт розширення", + "repeater_codingRate": "Швидкість кодування", + "repeater_locationSettings": "Налаштування розташування", + "repeater_latitude": "Широта", + "repeater_latitudeHelper": "Десяткові градуси (наприклад, 37.7749)", + "repeater_longitude": "Довгота", + "repeater_longitudeHelper": "Десяткові градуси (наприклад, -122.4194)", + "repeater_features": "Функції", + "repeater_packetForwarding": "Пересилання пакетів", + "repeater_packetForwardingSubtitle": "Дозволити ретранслятору пересилати пакети", + "repeater_guestAccess": "Гостьовий доступ", + "repeater_guestAccessSubtitle": "Дозволити гостьовий доступ лише для читання", + "repeater_privacyMode": "Режим приватності", + "repeater_privacyModeSubtitle": "Приховати ім'я/розташування в оголошеннях", + "repeater_advertisementSettings": "Налаштування оголошень", + "repeater_localAdvertInterval": "Інтервал локальних оголошень (0 стрибків)", + "repeater_localAdvertIntervalMinutes": "{minutes} хвилин", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1020,8 +1020,8 @@ } } }, - "repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)", - "repeater_floodAdvertIntervalHours": "{hours} годин", + "repeater_floodAdvertInterval": "Інтервал оголошень на всю мережу (flood)", + "repeater_floodAdvertIntervalHours": "{hours} годин", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1029,19 +1029,19 @@ } } }, - "repeater_encryptedAdvertInterval": "Інтервал зашифрованих оголошень", - "repeater_dangerZone": "Небезпечна зона", - "repeater_rebootRepeater": "Перезавантажити ретранслятор", - "repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора", - "repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?", - "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності", - "repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)", - "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?", - "repeater_eraseFileSystem": "Очистити файлову систему", - "repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора", - "repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!", - "repeater_eraseSerialOnly": "Очищення доступне лише через послідовну консоль.", - "repeater_commandSent": "Команда надіслана: {command}", + "repeater_encryptedAdvertInterval": "Інтервал зашифрованих оголошень", + "repeater_dangerZone": "Небезпечна зона", + "repeater_rebootRepeater": "Перезавантажити ретранслятор", + "repeater_rebootRepeaterSubtitle": "Скинути пристрій ретранслятора", + "repeater_rebootRepeaterConfirm": "Ви впевнені, що хочете перезавантажити цей ретранслятор?", + "repeater_regenerateIdentityKey": "Перегенерувати ключ ідентичності", + "repeater_regenerateIdentityKeySubtitle": "Згенерувати нову пару ключів (публічний/приватний)", + "repeater_regenerateIdentityKeyConfirm": "Це створить нову ідентичність для ретранслятора. Продовжити?", + "repeater_eraseFileSystem": "Очистити файлову систему", + "repeater_eraseFileSystemSubtitle": "Відформатувати файлову систему ретранслятора", + "repeater_eraseFileSystemConfirm": "УВАГА: Це видалить всі дані з ретранслятора. Це не можна скасувати!", + "repeater_eraseSerialOnly": "Очищення доступне лише через послідовну консоль.", + "repeater_commandSent": "Команда надіслана: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1049,7 +1049,7 @@ } } }, - "repeater_errorSendingCommand": "Помилка надсилання команди: {error}", + "repeater_errorSendingCommand": "Помилка надсилання команди: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1057,9 +1057,9 @@ } } }, - "repeater_confirm": "Підтвердити", - "repeater_settingsSaved": "Налаштування успішно збережено.", - "repeater_errorSavingSettings": "Помилка збереження налаштувань: {error}", + "repeater_confirm": "Підтвердити", + "repeater_settingsSaved": "Налаштування успішно збережено.", + "repeater_errorSavingSettings": "Помилка збереження налаштувань: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1067,15 +1067,15 @@ } } }, - "repeater_refreshBasicSettings": "Оновити основні налаштування", - "repeater_refreshRadioSettings": "Оновити налаштування радіо", - "repeater_refreshTxPower": "Оновити потужність TX", - "repeater_refreshLocationSettings": "Оновити налаштування розташування", - "repeater_refreshPacketForwarding": "Оновити пересилання пакетів", - "repeater_refreshGuestAccess": "Оновити гостьовий доступ", - "repeater_refreshPrivacyMode": "Оновити режим приватності", - "repeater_refreshAdvertisementSettings": "Оновити налаштування оголошень", - "repeater_refreshed": "{label} оновлено", + "repeater_refreshBasicSettings": "Оновити основні налаштування", + "repeater_refreshRadioSettings": "Оновити налаштування радіо", + "repeater_refreshTxPower": "Оновити потужність TX", + "repeater_refreshLocationSettings": "Оновити налаштування розташування", + "repeater_refreshPacketForwarding": "Оновити пересилання пакетів", + "repeater_refreshGuestAccess": "Оновити гостьовий доступ", + "repeater_refreshPrivacyMode": "Оновити режим приватності", + "repeater_refreshAdvertisementSettings": "Оновити налаштування оголошень", + "repeater_refreshed": "{label} оновлено", "@repeater_refreshed": { "placeholders": { "label": { @@ -1083,7 +1083,7 @@ } } }, - "repeater_errorRefreshing": "Помилка оновлення {label}", + "repeater_errorRefreshing": "Помилка оновлення {label}", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1091,18 +1091,18 @@ } } }, - "repeater_cliTitle": "Ретранслятор CLI", - "repeater_debugNextCommand": "Налагодити наступну команду", - "repeater_commandHelp": "Довідка", - "repeater_clearHistory": "Очистити історію", - "repeater_noCommandsSent": "Команди ще не надсилалися.", - "repeater_typeCommandOrUseQuick": "Введіть команду нижче або використовуйте швидкі команди", - "repeater_enterCommandHint": "Введіть команду...", - "repeater_previousCommand": "Попередня команда", - "repeater_nextCommand": "Наступна команда", - "repeater_enterCommandFirst": "Спершу введіть команду", - "repeater_cliCommandFrameTitle": "Фрейм команди CLI", - "repeater_cliCommandError": "Помилка: {error}", + "repeater_cliTitle": "Ретранслятор CLI", + "repeater_debugNextCommand": "Налагодити наступну команду", + "repeater_commandHelp": "Довідка", + "repeater_clearHistory": "Очистити історію", + "repeater_noCommandsSent": "Команди ще не надсилалися.", + "repeater_typeCommandOrUseQuick": "Введіть команду нижче або використовуйте швидкі команди", + "repeater_enterCommandHint": "Введіть команду...", + "repeater_previousCommand": "Попередня команда", + "repeater_nextCommand": "Наступна команда", + "repeater_enterCommandFirst": "Спершу введіть команду", + "repeater_cliCommandFrameTitle": "Фрейм команди CLI", + "repeater_cliCommandError": "Помилка: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1110,81 +1110,81 @@ } } }, - "repeater_cliQuickGetName": "Отримати ім'я", - "repeater_cliQuickGetRadio": "Отримати Радіо", - "repeater_cliQuickGetTx": "Отримати TX", - "repeater_cliQuickNeighbors": "Сусіди", - "repeater_cliQuickVersion": "Версія", - "repeater_cliQuickAdvertise": "Оголосити", - "repeater_cliQuickClock": "Годинник", - "repeater_cliHelpAdvert": "Надсилає пакет оголошення", - "repeater_cliHelpReboot": "Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що Ñ” нормальним)", - "repeater_cliHelpClock": "Відображає поточний час за годинником кожного пристрою.", - "repeater_cliHelpPassword": "Встановлює новий пароль адміністратора для пристрою.", - "repeater_cliHelpVersion": "Відображає версію пристрою та дату збірки прошивки.", - "repeater_cliHelpClearStats": "Скидає різні лічильники статистики до нуля.", - "repeater_cliHelpSetAf": "Встановлює коефіцієнт ефірного часу.", - "repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).", - "repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.", - "repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)", - "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).", - "repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.", - "repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.", - "repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.", - "repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.", - "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.", - "repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)", - "repeater_cliHelpSetName": "Встановлює ім'я для оголошення.", - "repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)", - "repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)", - "repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає Ñ—Ñ… у налаштуваннях. Потребує команди «перезавантаження» для застосування.", - "repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.", - "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", - "repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.", - "repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.", - "repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.", - "repeater_cliHelpSetBridgeSource": "Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.", - "repeater_cliHelpSetBridgeBaud": "Встановити швидкість послідовного зв'язку для мостів Rs232.", - "repeater_cliHelpSetBridgeSecret": "Встановити секрет мосту для мостів espnow.", - "repeater_cliHelpSetAdcMultiplier": "Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).", - "repeater_cliHelpTempRadio": "Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).", - "repeater_cliHelpSetPerm": "Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний Ñ– його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).", - "repeater_cliHelpGetBridgeType": "Отримати тип мосту: немає, rs232, espnow", - "repeater_cliHelpLogStart": "Починає запис пакетів у файлову систему.", - "repeater_cliHelpLogStop": "Зупиняє запис пакетів у файлову систему.", - "repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.", - "repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4", - "repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.", - "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).", - "repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.", - "repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»", - "repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.", - "repeater_cliHelpRegionRemove": "Видаляє визначення регіону з заданою назвою.", - "repeater_cliHelpRegionAllowf": "Встановлює дозвіл «Flood» для заданого регіону. ('' для глобальної/успадкованої області)", - "repeater_cliHelpRegionDenyf": "Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )", - "repeater_cliHelpRegionHome": "Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)", - "repeater_cliHelpRegionHomeSet": "Встановлює «домашній» регіон.", - "repeater_cliHelpRegionSave": "Зберігає список/карту регіонів у сховищі.", - "repeater_cliHelpGps": "Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.", - "repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.", - "repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.", - "repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS Ñ– зберігає в налаштуваннях.", - "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях", - "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.", - "repeater_commandsListTitle": "Список команд", - "repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...", - "repeater_general": "Загальні", - "repeater_settingsCategory": "Налаштування", - "repeater_bridge": "Міст", - "repeater_logging": "Логування", - "repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)", - "repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)", - "repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.", - "repeater_gpsManagement": "Керування GPS", - "repeater_gpsNote": "Команда GPS була введена для керування питаннями, пов'язаними з локацією.", - "telemetry_receivedData": "Дані телеметрії отримано", - "telemetry_requestTimeout": "Час запиту телеметрії вичерпано.", - "telemetry_errorLoading": "Помилка завантаження телеметрії: {error}", + "repeater_cliQuickGetName": "Отримати ім'я", + "repeater_cliQuickGetRadio": "Отримати Радіо", + "repeater_cliQuickGetTx": "Отримати TX", + "repeater_cliQuickNeighbors": "Сусіди", + "repeater_cliQuickVersion": "Версія", + "repeater_cliQuickAdvertise": "Оголосити", + "repeater_cliQuickClock": "Годинник", + "repeater_cliHelpAdvert": "Надсилає пакет оголошення", + "repeater_cliHelpReboot": "Перезавантажує пристрій. (Зверніть увагу, ви можете отримати «Тайм-аут», що є нормальним)", + "repeater_cliHelpClock": "Відображає поточний час за годинником кожного пристрою.", + "repeater_cliHelpPassword": "Встановлює новий пароль адміністратора для пристрою.", + "repeater_cliHelpVersion": "Відображає версію пристрою та дату збірки прошивки.", + "repeater_cliHelpClearStats": "Скидає різні лічильники статистики до нуля.", + "repeater_cliHelpSetAf": "Встановлює коефіцієнт ефірного часу.", + "repeater_cliHelpSetTx": "Встановлює потужність передачі LoRa в дБм (для застосування потрібне перезавантаження).", + "repeater_cliHelpSetRepeat": "Вмикає або вимикає роль ретранслятора для цього вузла.", + "repeater_cliHelpSetAllowReadOnly": "(Сервер кімнати) Якщо «увімкнено», порожній пароль дозволить вхід, але не дозволить публікувати в кімнаті. (тільки читання)", + "repeater_cliHelpSetFloodMax": "Встановлює максимальну кількість стрибків для вхідних пакетів flood (якщо >= max, пакет не пересилається).", + "repeater_cliHelpSetIntThresh": "Встановлює поріг інтерференції (в дБ). Значення за замовчуванням — 14. Встановлення на 0 вимикає виявлення інтерференції каналу.", + "repeater_cliHelpSetAgcResetInterval": "Встановлює інтервал скидання автоматичного контролера посилення (AGC). Встановіть 0 для вимкнення.", + "repeater_cliHelpSetMultiAcks": "Вмикає або вимикає функціональність подвійних ACK.", + "repeater_cliHelpSetAdvertInterval": "Встановлює інтервал таймера для надсилання локального пакету оголошення (без ретрансляції). Встановіть 0 для вимкнення.", + "repeater_cliHelpSetFloodAdvertInterval": "Встановлює інтервал таймера в годинах для надсилання пакету оголошення на всю мережу. Встановіть 0 для вимкнення.", + "repeater_cliHelpSetGuestPassword": "Встановлює/оновлює гостьовий пароль. (для ретрансляторів гостьові підключення можуть надсилати запит «Get Stats»)", + "repeater_cliHelpSetName": "Встановлює ім'я для оголошення.", + "repeater_cliHelpSetLat": "Встановлює широту для карти оголошень. (десяткові градуси)", + "repeater_cliHelpSetLon": "Встановлює довготу для карти оголошень. (десяткові градуси)", + "repeater_cliHelpSetRadio": "Повністю встановлює нові параметри радіо та зберігає їх у налаштуваннях. Потребує команди «перезавантаження» для застосування.", + "repeater_cliHelpSetRxDelay": "Базові (експериментальні) параметри для застосування невеликої затримки до отриманих пакетів залежно від сили сигналу/оцінки. Встановіть 0 для вимкнення.", + "repeater_cliHelpSetTxDelay": "Встановлює множник для часу роботи в режимі «на всю мережу» (flood) для пакету та системи випадкових слотів, щоб затримати його відправку (для зменшення ймовірності колізій).", + "repeater_cliHelpSetDirectTxDelay": "Те саме, що й txdelay, але для застосування випадкової затримки при пересиланні пакетів у прямому режимі.", + "repeater_cliHelpSetBridgeEnabled": "Увімкнути/Вимкнути міст.", + "repeater_cliHelpSetBridgeDelay": "Встановити затримку перед пересиланням пакетів.", + "repeater_cliHelpSetBridgeSource": "Виберіть, чи буде міст ретранслювати отримані пакети або передані пакети.", + "repeater_cliHelpSetBridgeBaud": "Встановити швидкість послідовного зв'язку для мостів Rs232.", + "repeater_cliHelpSetBridgeSecret": "Встановити секрет мосту для мостів espnow.", + "repeater_cliHelpSetAdcMultiplier": "Встановлює власний множник для коригування повідомлюваної напруги батареї (підтримується лише на деяких платах).", + "repeater_cliHelpTempRadio": "Встановлює тимчасові параметри радіо на задану кількість хвилин, потім повертається до початкових налаштувань. (не зберігає в налаштуваннях).", + "repeater_cliHelpSetPerm": "Змінює ACL (список контролю доступу). Видаляє відповідний запис (за префіксом публічного ключа), якщо «permissions» дорівнює нулю. Додає новий запис, якщо hex публічного ключа повний і його немає в ACL. Оновлює запис на основі префікса публічного ключа. Біти дозволів залежать від ролі прошивки, але нижні 2 біти: 0 (Гість), 1 (Тільки читання), 2 (Читання/Запис), 3 (Адміністратор).", + "repeater_cliHelpGetBridgeType": "Отримати тип мосту: немає, rs232, espnow", + "repeater_cliHelpLogStart": "Починає запис пакетів у файлову систему.", + "repeater_cliHelpLogStop": "Зупиняє запис пакетів у файлову систему.", + "repeater_cliHelpLogErase": "Видаляє журнали пакетів з файлової системи.", + "repeater_cliHelpNeighbors": "Показує список інших вузлів-ретрансляторів, почутих через оголошення без ретрансляції. Кожен рядок — id-hex-префікс:timestamp:snr-помножено-на-4", + "repeater_cliHelpNeighborRemove": "Видаляє перший відповідний запис (за префіксом публічного ключа (hex)) зі списку сусідів.", + "repeater_cliHelpRegion": "(тільки серійний) Перелічує всі визначені регіони та поточні дозволи на оголошення «на всю мережу» (flood).", + "repeater_cliHelpRegionLoad": "ПРИМІТКА: це спеціальний виклик кількох команд. Кожна наступна команда — це назва регіону (з відступом пробілами для позначення ієрархії батьків, мінімум один пробіл). Завершується надсиланням порожнього рядка/команди.", + "repeater_cliHelpRegionGet": "Шукає регіон із заданим префіксом назви (або «» для глобальної області). Відповідає: «-> ім'я-регіону (ім'я-батька) 'F'»", + "repeater_cliHelpRegionPut": "Додає або оновлює визначення регіону з заданою назвою.", + "repeater_cliHelpRegionRemove": "Видаляє визначення регіону з заданою назвою.", + "repeater_cliHelpRegionAllowf": "Встановлює дозвіл «Flood» для заданого регіону. ('' для глобальної/успадкованої області)", + "repeater_cliHelpRegionDenyf": "Видаляє дозвіл «Flood» для заданого регіону. (ПРИМІТКА: на даному етапі не рекомендується використовувати для глобальної/успадкованої області!! )", + "repeater_cliHelpRegionHome": "Відповідає поточним «домашнім» регіоном. (Примітка: поки ніде не застосовується, зарезервовано для майбутнього використання)", + "repeater_cliHelpRegionHomeSet": "Встановлює «домашній» регіон.", + "repeater_cliHelpRegionSave": "Зберігає список/карту регіонів у сховищі.", + "repeater_cliHelpGps": "Показує статус GPS. Коли GPS вимкнено, відповідає лише «вимкнено», якщо увімкнено — відповідає «увімкнено», статус, корекція, кількість супутників.", + "repeater_cliHelpGpsOnOff": "Увімкнути/вимкнути GPS.", + "repeater_cliHelpGpsSync": "Синхронізує час вузла з годинником GPS.", + "repeater_cliHelpGpsSetLoc": "Встановлює позицію вузла за координатами GPS і зберігає в налаштуваннях.", + "repeater_cliHelpGpsAdvert": "Надає конфігурацію оголошення розташування вузла:\n- none : не включати розташування в оголошення\n- share : ділитися розташуванням GPS (з SensorManager)\n- prefs : оголошувати розташування, збережене в налаштуваннях", + "repeater_cliHelpGpsAdvertSet": "Встановлює конфігурацію оголошення розташування.", + "repeater_commandsListTitle": "Список команд", + "repeater_commandsListNote": "ПРИМІТКА: для різних команд «set»... також існує команда «get»...", + "repeater_general": "Загальні", + "repeater_settingsCategory": "Налаштування", + "repeater_bridge": "Міст", + "repeater_logging": "Логування", + "repeater_neighborsRepeaterOnly": "Сусіди (Тільки ретранслятор)", + "repeater_regionManagementRepeaterOnly": "Керування регіонами (Тільки ретранслятор)", + "repeater_regionNote": "Команди регіонів були введені для керування визначеннями та дозволами регіонів.", + "repeater_gpsManagement": "Керування GPS", + "repeater_gpsNote": "Команда GPS була введена для керування питаннями, пов'язаними з локацією.", + "telemetry_receivedData": "Дані телеметрії отримано", + "telemetry_requestTimeout": "Час запиту телеметрії вичерпано.", + "telemetry_errorLoading": "Помилка завантаження телеметрії: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1192,8 +1192,8 @@ } } }, - "telemetry_noData": "Дані телеметрії недоступні.", - "telemetry_channelTitle": "Канал {channel}", + "telemetry_noData": "Дані телеметрії недоступні.", + "telemetry_channelTitle": "Канал {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1201,12 +1201,12 @@ } } }, - "telemetry_batteryLabel": "Батарея", - "telemetry_voltageLabel": "Напруга", - "telemetry_mcuTemperatureLabel": "Температура MCU", - "telemetry_temperatureLabel": "Температура", - "telemetry_currentLabel": "Поточний струм", - "telemetry_batteryValue": "{percent}% / {volts}Ð’", + "telemetry_batteryLabel": "Батарея", + "telemetry_voltageLabel": "Напруга", + "telemetry_mcuTemperatureLabel": "Температура MCU", + "telemetry_temperatureLabel": "Температура", + "telemetry_currentLabel": "Поточний струм", + "telemetry_batteryValue": "{percent}% / {volts}В", "@telemetry_batteryValue": { "placeholders": { "percent": { @@ -1217,7 +1217,7 @@ } } }, - "telemetry_voltageValue": "{volts}Ð’", + "telemetry_voltageValue": "{volts}В", "@telemetry_voltageValue": { "placeholders": { "volts": { @@ -1225,7 +1225,7 @@ } } }, - "telemetry_currentValue": "{amps}А", + "telemetry_currentValue": "{amps}А", "@telemetry_currentValue": { "placeholders": { "amps": { @@ -1233,7 +1233,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1244,18 +1244,18 @@ } } }, - "channelPath_title": "Шлях пакету", - "channelPath_viewMap": "Показати карту", - "channelPath_otherObservedPaths": "Інші спостережувані шляхи", - "channelPath_repeaterHops": "Стрибки ретранслятора", - "channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.", - "channelPath_messageDetails": "Деталі повідомлення", - "channelPath_senderLabel": "Відправник", - "channelPath_timeLabel": "Час", - "channelPath_repeatsLabel": "Повторення", - "channelPath_pathLabel": "Шлях {index}", - "channelPath_observedLabel": "Спостережено", - "channelPath_observedPathTitle": "Спостережуваний шлях {index} • {hops}", + "channelPath_title": "Шлях пакету", + "channelPath_viewMap": "Показати карту", + "channelPath_otherObservedPaths": "Інші спостережувані шляхи", + "channelPath_repeaterHops": "Стрибки ретранслятора", + "channelPath_noHopDetails": "Деталі відправки не надані для цього пакету.", + "channelPath_messageDetails": "Деталі повідомлення", + "channelPath_senderLabel": "Відправник", + "channelPath_timeLabel": "Час", + "channelPath_repeatsLabel": "Повторення", + "channelPath_pathLabel": "Шлях {index}", + "channelPath_observedLabel": "Спостережено", + "channelPath_observedPathTitle": "Спостережуваний шлях {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1266,7 +1266,7 @@ } } }, - "channelPath_noLocationData": "Немає даних про розташування", + "channelPath_noLocationData": "Немає даних про розташування", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1289,10 +1289,10 @@ } } }, - "channelPath_unknownPath": "Невідомий", - "channelPath_floodPath": "На всю мережу", - "channelPath_directPath": "Прямий", - "channelPath_observedZeroOf": "0 з {total} стрибків", + "channelPath_unknownPath": "Невідомий", + "channelPath_floodPath": "На всю мережу", + "channelPath_directPath": "Прямий", + "channelPath_observedZeroOf": "0 з {total} стрибків", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1300,7 +1300,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} з {total} стрибків", + "channelPath_observedSomeOf": "{observed} з {total} стрибків", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1311,9 +1311,9 @@ } } }, - "channelPath_mapTitle": "Карта шляху", - "channelPath_noRepeaterLocations": "Позиції ретрансляторів недоступні для цього шляху.", - "channelPath_primaryPath": "Шлях {index} (Основний)", + "channelPath_mapTitle": "Карта шляху", + "channelPath_noRepeaterLocations": "Позиції ретрансляторів недоступні для цього шляху.", + "channelPath_primaryPath": "Шлях {index} (Основний)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1328,9 +1328,9 @@ } } }, - "channelPath_pathLabelTitle": "Шлях", - "channelPath_observedPathHeader": "Спостережуваний шлях", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "Шлях", + "channelPath_observedPathHeader": "Спостережуваний шлях", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1341,20 +1341,20 @@ } } }, - "channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.", - "channelPath_unknownRepeater": "Невідомий ретранслятор", - "listFilter_tooltip": "Фільтр та сортування", - "listFilter_sortBy": "Сортувати за", - "listFilter_latestMessages": "Останні повідомлення", - "listFilter_heardRecently": "Нещодавно чули", - "listFilter_az": "А-Я", - "listFilter_filters": "Фільтри", - "listFilter_all": "Все", - "listFilter_users": "Користувачі", - "listFilter_repeaters": "Ретранслятори", - "listFilter_roomServers": "Сервери кімнат", - "listFilter_unreadOnly": "Тільки непрочитані повідомлення", - "listFilter_newGroup": "Нова група", + "channelPath_noHopDetailsAvailable": "Деталі стрибків недоступні для цього пакету.", + "channelPath_unknownRepeater": "Невідомий ретранслятор", + "listFilter_tooltip": "Фільтр та сортування", + "listFilter_sortBy": "Сортувати за", + "listFilter_latestMessages": "Останні повідомлення", + "listFilter_heardRecently": "Нещодавно чули", + "listFilter_az": "А-Я", + "listFilter_filters": "Фільтри", + "listFilter_all": "Все", + "listFilter_users": "Користувачі", + "listFilter_repeaters": "Ретранслятори", + "listFilter_roomServers": "Сервери кімнат", + "listFilter_unreadOnly": "Тільки непрочитані повідомлення", + "listFilter_newGroup": "Нова група", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1362,25 +1362,25 @@ } } }, - "repeater_neighbors": "Сусіди", - "repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", - "neighbors_receivedData": "Дані сусідів отримано", - "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", - "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", - "neighbors_repeatersNeighbors": "Ретранслятори-сусіди", - "neighbors_noData": "Дані про сусідів недоступні.", - "channels_createPrivateChannelDesc": "Захищено секретним ключем.", - "channels_joinPrivateChannel": "Приєднатися до приватного каналу", - "channels_createPrivateChannel": "Створити приватний канал", - "channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.", - "channels_joinPublicChannel": "Приєднатися до публічного каналу", - "channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.", - "channels_joinHashtagChannel": "Приєднатися до каналу з хештегом", - "channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.", - "channels_scanQrCode": "Сканувати QR-код", - "channels_scanQrCodeComingSoon": "Скоро буде", - "channels_enterHashtag": "Введіть хештег", - "channels_hashtagHint": "напр. #команда", + "repeater_neighbors": "Сусіди", + "repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", + "neighbors_receivedData": "Дані сусідів отримано", + "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", + "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", + "neighbors_repeatersNeighbors": "Ретранслятори-сусіди", + "neighbors_noData": "Дані про сусідів недоступні.", + "channels_createPrivateChannelDesc": "Захищено секретним ключем.", + "channels_joinPrivateChannel": "Приєднатися до приватного каналу", + "channels_createPrivateChannel": "Створити приватний канал", + "channels_joinPrivateChannelDesc": "Ввести секретний ключ вручну.", + "channels_joinPublicChannel": "Приєднатися до публічного каналу", + "channels_joinPublicChannelDesc": "Будь-хто може приєднатися до цього каналу.", + "channels_joinHashtagChannel": "Приєднатися до каналу з хештегом", + "channels_joinHashtagChannelDesc": "Будь-хто може приєднатися до каналів #hashtag.", + "channels_scanQrCode": "Сканувати QR-код", + "channels_scanQrCodeComingSoon": "Скоро буде", + "channels_enterHashtag": "Введіть хештег", + "channels_hashtagHint": "напр. #команда", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1395,14 +1395,14 @@ } } }, - "neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}", - "neighbors_heardAgo": "Почуто: {time} тому", - "settings_locationGPSEnable": "Увімкнути GPS", - "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.", - "settings_locationIntervalSec": "Інтервал для GPS (Секунди)", - "settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд Ñ– менше 86400 секунд.", - "contacts_manageRoom": "Керувати сервером кімнати", - "room_management": "Адміністрування сервера кімнати", + "neighbors_unknownContact": "Невідомий відкритий ключ {pubkey}", + "neighbors_heardAgo": "Почуто: {time} тому", + "settings_locationGPSEnable": "Увімкнути GPS", + "settings_locationGPSEnableSubtitle": "Вмикає автоматичне оновлення місцезнаходження через GPS.", + "settings_locationIntervalSec": "Інтервал для GPS (Секунди)", + "settings_locationIntervalInvalid": "Інтервал має бути не менше 60 секунд і менше 86400 секунд.", + "contacts_manageRoom": "Керувати сервером кімнати", + "room_management": "Адміністрування сервера кімнати", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1459,36 +1459,36 @@ } } }, - "common_ok": "ОК", - "community_title": "Спільнота", - "community_create": "Створити спільноту", - "community_createDesc": "Створити нову спільноту та поділитися через QR-код.", - "community_join": "Приєднатися", - "community_joinTitle": "Приєднатися до спільноти", - "community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?", - "community_scanQr": "Сканувати QR спільноти", - "community_scanInstructions": "Наведіть камеру на QR-код спільноти.", - "community_showQr": "Показати QR-код", - "community_publicChannel": "Публічна спільнота", - "community_hashtagChannel": "Хештег спільноти", - "community_name": "Назва спільноти", - "community_enterName": "Введіть назву спільноти", - "community_created": "Спільноту «{name}» створено", - "community_joined": "Приєднався до спільноти «{name}»", - "community_qrTitle": "Поділитися спільнотою", - "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}", - "community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти", - "community_invalidQrCode": "Недійсний QR-код спільноти", - "community_alreadyMember": "Вже учасник", - "community_alreadyMemberMessage": "Ви вже Ñ” учасником «{name}».", - "community_addPublicChannel": "Додати публічний канал спільноти", - "community_addPublicChannelHint": "Автоматично додати публічний канал для цієї спільноти", - "community_noCommunities": "Поки не приєднано до жодної групи.", - "community_scanOrCreate": "Відскануйте QR-код або створіть спільноту, щоб почати", - "community_manageCommunities": "Керувати спільнотами", - "community_delete": "Покинути спільноту", - "community_deleteConfirm": "Покинути «{name}»?", - "community_deleteChannelsWarning": "Це також видалить {count} {count, plural, =1{канал} few{канали} many{каналів} other{каналів}} та Ñ—Ñ… повідомлення.", + "common_ok": "ОК", + "community_title": "Спільнота", + "community_create": "Створити спільноту", + "community_createDesc": "Створити нову спільноту та поділитися через QR-код.", + "community_join": "Приєднатися", + "community_joinTitle": "Приєднатися до спільноти", + "community_joinConfirmation": "Ви бажаєте приєднатися до спільноти «{name}»?", + "community_scanQr": "Сканувати QR спільноти", + "community_scanInstructions": "Наведіть камеру на QR-код спільноти.", + "community_showQr": "Показати QR-код", + "community_publicChannel": "Публічна спільнота", + "community_hashtagChannel": "Хештег спільноти", + "community_name": "Назва спільноти", + "community_enterName": "Введіть назву спільноти", + "community_created": "Спільноту «{name}» створено", + "community_joined": "Приєднався до спільноти «{name}»", + "community_qrTitle": "Поділитися спільнотою", + "community_qrInstructions": "Відскануйте цей QR-код, щоб приєднатися до {name}", + "community_hashtagPrivacyHint": "Канали хештегів спільноти доступні лише членам спільноти", + "community_invalidQrCode": "Недійсний QR-код спільноти", + "community_alreadyMember": "Вже учасник", + "community_alreadyMemberMessage": "Ви вже є учасником «{name}».", + "community_addPublicChannel": "Додати публічний канал спільноти", + "community_addPublicChannelHint": "Автоматично додати публічний канал для цієї спільноти", + "community_noCommunities": "Поки не приєднано до жодної групи.", + "community_scanOrCreate": "Відскануйте QR-код або створіть спільноту, щоб почати", + "community_manageCommunities": "Керувати спільнотами", + "community_delete": "Покинути спільноту", + "community_deleteConfirm": "Покинути «{name}»?", + "community_deleteChannelsWarning": "Це також видалить {count} {count, plural, =1{канал} few{канали} many{каналів} other{каналів}} та їх повідомлення.", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1496,15 +1496,15 @@ } } }, - "community_deleted": "Спільноту «{name}» покинуто", - "community_addHashtagChannel": "Додати хештег спільноти", - "community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти", - "community_selectCommunity": "Вибрати спільноту", - "community_regularHashtag": "Звичайний хештег", - "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)", - "community_communityHashtag": "Хештег спільноти", - "community_communityHashtagDesc": "Ексклюзивно для членів спільноти", - "community_forCommunity": "Для {name}", + "community_deleted": "Спільноту «{name}» покинуто", + "community_addHashtagChannel": "Додати хештег спільноти", + "community_addHashtagChannelDesc": "Додати канал хештегу для цієї спільноти", + "community_selectCommunity": "Вибрати спільноту", + "community_regularHashtag": "Звичайний хештег", + "community_regularHashtagDesc": "Публічний хештег (будь-хто може приєднатися)", + "community_communityHashtag": "Хештег спільноти", + "community_communityHashtagDesc": "Ексклюзивно для членів спільноти", + "community_forCommunity": "Для {name}", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1533,13 +1533,13 @@ } } }, - "community_regenerateSecret": "Перегенерувати секрет", - "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.", - "community_regenerate": "Перегенерувати", - "community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", - "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", - "community_updateSecret": "Оновити секрет", - "community_secretUpdated": "Зміну секрету для «{name}» оновлено", + "community_regenerateSecret": "Перегенерувати секрет", + "community_regenerateSecretConfirm": "Перегенерувати секретний ключ для «{name}»? Всі учасники повинні будуть відсканувати новий QR-код, щоб продовжити спілкування.", + "community_regenerate": "Перегенерувати", + "community_secretRegenerated": "Секретний пароль для «{name}» перегенеровано", + "community_scanToUpdateSecret": "Відскануйте новий QR-код, щоб оновити пароль для «{name}»", + "community_updateSecret": "Оновити секрет", + "community_secretUpdated": "Зміну секрету для «{name}» оновлено", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1547,81 +1547,81 @@ } } }, - "pathTrace_you": "Ви", - "pathTrace_failed": "Відстеження шляху не вдалося.", - "pathTrace_notAvailable": "Трасування шляху недоступне.", - "pathTrace_refreshTooltip": "Оновити Path Trace", - "contacts_pathTrace": "Трасування шляхів", - "contacts_ping": "Пінгувати", - "contacts_repeaterPathTrace": "Трасування шляху до повторювача", - "contacts_repeaterPing": "Пінгувати повторювач", - "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", - "contacts_roomPing": "Пінг сервера кімнати", - "contacts_chatTraceRoute": "Трасування шляху", - "contacts_pathTraceTo": "Відстежити маршрут до {name}", - "contacts_invalidAdvertFormat": "Недійсні контактні дані", - "contacts_contactImported": "Контакт було імпортовано.", - "contacts_contactImportFailed": "Контакт не вдалося імпортувати", - "contacts_zeroHopAdvert": "Реклама без перехоплення", - "contacts_floodAdvert": "Залив реклами", - "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", - "contacts_clipboardEmpty": "Буфер обміну порожній", - "appSettings_languageRu": "Російська", - "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", - "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", - "contacts_ShareContact": "Копіювати контакт у буфер обміну", - "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", - "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", - "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", - "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", - "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", - "notification_activityTitle": "Активність MeshCore", - "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", - "notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}", - "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", - "notification_newTypeDiscovered": "Виявлено новий {contactType}", - "notification_receivedNewMessage": "Отримано нове повідомлення", - "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", - "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", - "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", - "settings_gpxExportNoContacts": "Немає контактів для експорту.", - "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", - "settings_gpxExportError": "Сталася помилка під час експорту.", - "settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", - "settings_gpxExportAll": "Експортувати всі контакти до GPX", - "settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", - "settings_gpxExportContacts": "Експортувати супутників до GPX", - "settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", - "settings_gpxExportChat": "Місця супутників", - "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", - "settings_gpxExportAllContacts": "Усі місця контактів", - "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", - "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", - "map_tapToAdd": "Натисніть на вузли, щоб додати Ñ—Ñ… до шляху", - "map_runTrace": "Виконати трасування шляху", - "pathTrace_clearTooltip": "Очистити шлях", - "map_removeLast": "Видалити останній", - "map_pathTraceCancelled": "Відмінується трасування шляху", - "scanner_enableBluetooth": "Увімкніть Bluetooth", - "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", - "scanner_chromeRequired": "Потрібен браузер Chrome", - "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", - "scanner_bluetoothOff": "Bluetooth вимкнено", - "snrIndicator_lastSeen": "Останній раз бачили", - "snrIndicator_nearByRepeaters": "Ближні ретранслятори", - "chat_ShowAllPaths": "Показати всі шляхи", - "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", - "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", - "settings_clientRepeat": "Автономна система", - "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "одиниці", - "appSettings_unitsMetric": "Метричний (м / км)", - "appSettings_unitsImperial": "Імперська (ft / mi)", - "map_lineOfSight": "Пряма видимість", - "map_losScreenTitle": "Пряма видимість", - "losSelectStartEnd": "Виберіть початковий Ñ– кінцевий вузли для LOS.", - "losRunFailed": "Помилка перевірки прямої видимості: {error}", + "pathTrace_you": "Ви", + "pathTrace_failed": "Відстеження шляху не вдалося.", + "pathTrace_notAvailable": "Трасування шляху недоступне.", + "pathTrace_refreshTooltip": "Оновити Path Trace", + "contacts_pathTrace": "Трасування шляхів", + "contacts_ping": "Пінгувати", + "contacts_repeaterPathTrace": "Трасування шляху до повторювача", + "contacts_repeaterPing": "Пінгувати повторювач", + "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", + "contacts_roomPing": "Пінг сервера кімнати", + "contacts_chatTraceRoute": "Трасування шляху", + "contacts_pathTraceTo": "Відстежити маршрут до {name}", + "contacts_invalidAdvertFormat": "Недійсні контактні дані", + "contacts_contactImported": "Контакт було імпортовано.", + "contacts_contactImportFailed": "Контакт не вдалося імпортувати", + "contacts_zeroHopAdvert": "Реклама без перехоплення", + "contacts_floodAdvert": "Залив реклами", + "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", + "contacts_clipboardEmpty": "Буфер обміну порожній", + "appSettings_languageRu": "Російська", + "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", + "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", + "contacts_ShareContact": "Копіювати контакт у буфер обміну", + "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", + "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", + "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", + "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", + "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", + "notification_activityTitle": "Активність MeshCore", + "notification_messagesCount": "{count} {count, plural, =1{повідомлення} few{повідомлення} many{повідомлень} other{повідомлень}}", + "notification_channelMessagesCount": "{count} {count, plural, =1{повідомлення каналу} few{повідомлення каналу} many{повідомлень каналу} other{повідомлень каналу}}", + "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", + "notification_newTypeDiscovered": "Виявлено новий {contactType}", + "notification_receivedNewMessage": "Отримано нове повідомлення", + "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", + "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", + "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", + "settings_gpxExportNoContacts": "Немає контактів для експорту.", + "settings_gpxExportNotAvailable": "Не підтримується на вашому пристрої/операційній системі", + "settings_gpxExportError": "Сталася помилка під час експорту.", + "settings_gpxExportAllSubtitle": "Експортує всі контакти з місцем розташування у файл GPX.", + "settings_gpxExportAll": "Експортувати всі контакти до GPX", + "settings_gpxExportContactsSubtitle": "Експортує супутників з місцезнаходженням у файл GPX.", + "settings_gpxExportContacts": "Експортувати супутників до GPX", + "settings_gpxExportRepeatersRoom": "Місцезнаходження повторювача та сервера кімнати", + "settings_gpxExportChat": "Місця супутників", + "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", + "settings_gpxExportAllContacts": "Усі місця контактів", + "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", + "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", + "map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху", + "map_runTrace": "Виконати трасування шляху", + "pathTrace_clearTooltip": "Очистити шлях", + "map_removeLast": "Видалити останній", + "map_pathTraceCancelled": "Відмінується трасування шляху", + "scanner_enableBluetooth": "Увімкніть Bluetooth", + "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", + "scanner_chromeRequired": "Потрібен браузер Chrome", + "scanner_chromeRequiredMessage": "Для підтримки Bluetooth у цьому веб-додатку потрібен Google Chrome або браузер на базі Chromium.", + "scanner_bluetoothOff": "Bluetooth вимкнено", + "snrIndicator_lastSeen": "Останній раз бачили", + "snrIndicator_nearByRepeaters": "Ближні ретранслятори", + "chat_ShowAllPaths": "Показати всі шляхи", + "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", + "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", + "settings_clientRepeat": "Автономна система", + "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "одиниці", + "appSettings_unitsMetric": "Метричний (м / км)", + "appSettings_unitsImperial": "Імперська (ft / mi)", + "map_lineOfSight": "Пряма видимість", + "map_losScreenTitle": "Пряма видимість", + "losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.", + "losRunFailed": "Помилка перевірки прямої видимості: {error}", "@losRunFailed": { "placeholders": { "error": { @@ -1629,13 +1629,13 @@ } } }, - "losClearAllPoints": "Очистити всі пункти", - "losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти", - "losMenuTitle": "Меню LOS", - "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", - "losShowDisplayNodes": "Показати вузли відображення", - "losCustomPoints": "Користувальницькі точки", - "losCustomPointLabel": "Спеціальний {index}", + "losClearAllPoints": "Очистити всі пункти", + "losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти", + "losMenuTitle": "Меню LOS", + "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", + "losShowDisplayNodes": "Показати вузли відображення", + "losCustomPoints": "Користувальницькі точки", + "losCustomPointLabel": "Спеціальний {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1643,9 +1643,9 @@ } } }, - "losPointA": "Точка А", - "losPointB": "Точка Б", - "losAntennaA": "Антена A: {value} {unit}", + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1656,7 +1656,7 @@ } } }, - "losAntennaB": "Антена B: {value} {unit}", + "losAntennaB": "Антена B: {value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1667,9 +1667,9 @@ } } }, - "losRun": "Запустіть LOS", - "losNoElevationData": "Немає даних про висоту", - "losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}", + "losRun": "Запустіть LOS", + "losNoElevationData": "Немає даних про висоту", + "losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1686,7 +1686,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}", + "losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1703,9 +1703,9 @@ } } }, - "losStatusChecking": "LOS: перевірка...", - "losStatusNoData": "LOS: немає даних", - "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо", + "losStatusChecking": "LOS: перевірка...", + "losStatusNoData": "LOS: немає даних", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо", "@losStatusSummary": { "placeholders": { "clear": { @@ -1722,20 +1722,20 @@ } } }, - "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", - "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", - "losRenameCustomPoint": "Перейменуйте спеціальну точку", - "losPointName": "Назва точки", - "losShowPanelTooltip": "Показати панель LOS", - "losHidePanelTooltip": "Приховати панель LOS", - "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "Радіогоризонт", - "losLegendLosBeam": "Лінія прямої видимості", - "losLegendTerrain": "Рельєф", - "losFrequencyLabel": "Частота", - "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", - "losFrequencyDialogTitle": "Розрахунок радіогоризонту", - "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", + "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", + "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", + "losRenameCustomPoint": "Перейменуйте спеціальну точку", + "losPointName": "Назва точки", + "losShowPanelTooltip": "Показати панель LOS", + "losHidePanelTooltip": "Приховати панель LOS", + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радіогоризонт", + "losLegendLosBeam": "Лінія прямої видимості", + "losLegendTerrain": "Рельєф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", + "losFrequencyDialogTitle": "Розрахунок радіогоризонту", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1753,9 +1753,9 @@ } } }, - "listFilter_removeFromFavorites": "Видалити зі списку улюблених", - "listFilter_addToFavorites": "Додати до улюблених", - "listFilter_favorites": "Улюблені", + "listFilter_removeFromFavorites": "Видалити зі списку улюблених", + "listFilter_addToFavorites": "Додати до улюблених", + "listFilter_favorites": "Улюблені", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1796,17 +1796,29 @@ } } }, - "contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...", - "contacts_searchUsers": "Пошук {number}{str} користувачів...", - "contacts_searchFavorites": "Пошук {number}{str} улюблених...", - "contacts_searchContactsNoNumber": "Пошук контактів...", - "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", - "contacts_unread": "Непрочитане", - "connectionChoiceUsbLabel": "USB", + "contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...", + "contacts_searchUsers": "Пошук {number}{str} користувачів...", + "contacts_searchFavorites": "Пошук {number}{str} улюблених...", + "contacts_searchContactsNoNumber": "Пошук контактів...", + "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", + "contacts_unread": "Непрочитане", + "usbScreenNote": "USB-серіальний порт активний на підтримуваних пристроях на базі Android та на десктопних платформах.", + "usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", + "usbScreenStatus": "Виберіть пристрій USB", + "usbScreenTitle": "Підключити через USB", + "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.", + "usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.", + "usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.", + "usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.", + "usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.", + "usbErrorNotConnected": "Немає підключених пристроїв USB.", + "usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.", + "usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.", + "usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.", + "usbErrorAlreadyActive": "USB-з'єднання вже встановлено.", + "usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.", + "usbErrorPortClosed": "З'єднання USB не встановлено.", + "usbErrorConnectTimedOut": "Час очікування закінчився, оскільки пристрій не відповів.", "connectionChoiceBluetoothLabel": "Bluetooth", - "usbScreenSubtitle": "Виберіть виявлене серійне пристрій Ñ– підключіть його безпосередньо до вашого вузла MeshCore.", - "usbScreenTitle": "Підключити через USB", - "usbScreenStatus": "Виберіть пристрій USB", - "usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.", - "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один Ñ– перезавантажте." + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c3ea415..07af01e 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,5 +1,5 @@ -{ - "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", +{ + "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { @@ -9,34 +9,34 @@ }, "@@locale": "zh", "appTitle": "MeshCore Open", - "nav_contacts": "联系人", - "nav_channels": "频道", - "nav_map": "地图", - "common_cancel": "取消", - "common_ok": "确定", - "common_connect": "连接", - "common_unknownDevice": "未知设备", - "common_save": "保存", - "common_delete": "删除", - "common_close": "关闭", - "common_edit": "编辑", - "common_add": "添加", - "common_settings": "设置", - "common_disconnect": "æ–­å¼€", - "common_connected": "已连接", - "common_disconnected": "已断开", - "common_create": "创建", - "common_continue": "ç»§ç»­", - "common_share": "分享", - "common_copy": "复制", - "common_retry": "重试", - "common_hide": "隐藏", - "common_remove": "移除", - "common_enable": "启用", - "common_disable": "禁用", - "common_reboot": "重启", - "common_loading": "正在加载...", - "common_notAvailable": "—", + "nav_contacts": "联系人", + "nav_channels": "频道", + "nav_map": "地图", + "common_cancel": "取消", + "common_ok": "确定", + "common_connect": "连接", + "common_unknownDevice": "未知设备", + "common_save": "保存", + "common_delete": "删除", + "common_close": "关闭", + "common_edit": "编辑", + "common_add": "添加", + "common_settings": "设置", + "common_disconnect": "断开", + "common_connected": "已连接", + "common_disconnected": "已断开", + "common_create": "创建", + "common_continue": "继续", + "common_share": "分享", + "common_copy": "复制", + "common_retry": "重试", + "common_hide": "隐藏", + "common_remove": "移除", + "common_enable": "启用", + "common_disable": "禁用", + "common_reboot": "重启", + "common_loading": "正在加载...", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -53,12 +53,12 @@ } } }, - "scanner_title": "连接设备", - "scanner_scanning": "正在搜索设备...", - "scanner_connecting": "正在连接...", - "scanner_disconnecting": "断开连接...", - "scanner_notConnected": "未连接", - "scanner_connectedTo": "已连接到 {deviceName}", + "scanner_title": "连接设备", + "scanner_scanning": "正在搜索设备...", + "scanner_connecting": "正在连接...", + "scanner_disconnecting": "断开连接...", + "scanner_notConnected": "未连接", + "scanner_connectedTo": "已连接到 {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -66,9 +66,9 @@ } } }, - "scanner_searchingDevices": "正在搜索 MeshCore 设备...", - "scanner_tapToScan": "点击“扫描”按钮以查找 MeshCore 设备。", - "scanner_connectionFailed": "连接失败:{error}", + "scanner_searchingDevices": "正在搜索 MeshCore 设备...", + "scanner_tapToScan": "点击“扫描”按钮以查找 MeshCore 设备。", + "scanner_connectionFailed": "连接失败:{error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -76,56 +76,56 @@ } } }, - "scanner_stop": "停止", - "scanner_scan": "扫描", - "device_quickSwitch": "快速切换", + "scanner_stop": "停止", + "scanner_scan": "扫描", + "device_quickSwitch": "快速切换", "device_meshcore": "MeshCore", - "settings_title": "设置", - "settings_deviceInfo": "设备信息", - "settings_appSettings": "应用设置", - "settings_appSettingsSubtitle": "通知、消息和地图偏好", - "settings_nodeSettings": "节点设置", - "settings_nodeName": "节点名称", - "settings_nodeNameNotSet": "未设置", - "settings_nodeNameHint": "请输入节点名称", - "settings_nodeNameUpdated": "节点名称已更新", - "settings_radioSettings": "无线电设置", - "settings_radioSettingsSubtitle": "频率、功率、扩频因子", - "settings_radioSettingsUpdated": "无线电设置已更新", - "settings_location": "位置", - "settings_locationSubtitle": "GPS 坐标", - "settings_locationUpdated": "位置和 GPS 设置已更新", - "settings_locationBothRequired": "请输入经度和纬度", - "settings_locationInvalid": "无效的经度和纬度", - "settings_locationGPSEnable": "启用 GPS", - "settings_locationGPSEnableSubtitle": "启用 GPS 以自动更新位置。", - "settings_locationIntervalSec": "GPS 间隔(秒)", - "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", - "settings_latitude": "纬度", - "settings_longitude": "经度", - "settings_privacyMode": "隐私模式", - "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", - "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", - "settings_privacyModeEnabled": "隐私模式已启用", - "settings_privacyModeDisabled": "隐私模式已关闭", - "settings_actions": "操作", - "settings_sendAdvertisement": "发送广播", - "settings_sendAdvertisementSubtitle": "立即发送广播", - "settings_advertisementSent": "已发送广播", - "settings_syncTime": "同步时间", - "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", - "settings_timeSynchronized": "时间已同步", - "settings_refreshContacts": "刷新联系人", - "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", - "settings_rebootDevice": "重启设备", - "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", - "settings_rebootDeviceConfirm": "确定要重启设备吗?这将断开与设备的连接。", - "settings_debug": "调试", - "settings_bleDebugLog": "BLE 调试日志", - "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", - "settings_appDebugLog": "应用调试日志", - "settings_appDebugLogSubtitle": "应用调试消息", - "settings_about": "关于", + "settings_title": "设置", + "settings_deviceInfo": "设备信息", + "settings_appSettings": "应用设置", + "settings_appSettingsSubtitle": "通知、消息和地图偏好", + "settings_nodeSettings": "节点设置", + "settings_nodeName": "节点名称", + "settings_nodeNameNotSet": "未设置", + "settings_nodeNameHint": "请输入节点名称", + "settings_nodeNameUpdated": "节点名称已更新", + "settings_radioSettings": "无线电设置", + "settings_radioSettingsSubtitle": "频率、功率、扩频因子", + "settings_radioSettingsUpdated": "无线电设置已更新", + "settings_location": "位置", + "settings_locationSubtitle": "GPS 坐标", + "settings_locationUpdated": "位置和 GPS 设置已更新", + "settings_locationBothRequired": "请输入经度和纬度", + "settings_locationInvalid": "无效的经度和纬度", + "settings_locationGPSEnable": "启用 GPS", + "settings_locationGPSEnableSubtitle": "启用 GPS 以自动更新位置。", + "settings_locationIntervalSec": "GPS 间隔(秒)", + "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", + "settings_latitude": "纬度", + "settings_longitude": "经度", + "settings_privacyMode": "隐私模式", + "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", + "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", + "settings_privacyModeEnabled": "隐私模式已启用", + "settings_privacyModeDisabled": "隐私模式已关闭", + "settings_actions": "操作", + "settings_sendAdvertisement": "发送广播", + "settings_sendAdvertisementSubtitle": "立即发送广播", + "settings_advertisementSent": "已发送广播", + "settings_syncTime": "同步时间", + "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", + "settings_timeSynchronized": "时间已同步", + "settings_refreshContacts": "刷新联系人", + "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", + "settings_rebootDevice": "重启设备", + "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", + "settings_rebootDeviceConfirm": "确定要重启设备吗?这将断开与设备的连接。", + "settings_debug": "调试", + "settings_bleDebugLog": "BLE 调试日志", + "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", + "settings_appDebugLog": "应用调试日志", + "settings_appDebugLogSubtitle": "应用调试消息", + "settings_about": "关于", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { @@ -134,31 +134,31 @@ } } }, - "settings_aboutLegalese": "2026 MeshCore 开源项目", - "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", - "settings_infoName": "名称", + "settings_aboutLegalese": "2026 MeshCore 开源项目", + "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", + "settings_infoName": "名称", "settings_infoId": "MAC ID", - "settings_infoStatus": "状态", - "settings_infoBattery": "电池", - "settings_infoPublicKey": "公钥", - "settings_infoContactsCount": "联系人数量", - "settings_infoChannelCount": "频道数量", - "settings_presets": "预设", + "settings_infoStatus": "状态", + "settings_infoBattery": "电池", + "settings_infoPublicKey": "公钥", + "settings_infoContactsCount": "联系人数量", + "settings_infoChannelCount": "频道数量", + "settings_presets": "预设", "settings_preset915Mhz": "915 MHz", "settings_preset868Mhz": "868 MHz", "settings_preset433Mhz": "433 MHz", - "settings_frequency": "频率 (MHz)", + "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "无效频率范围(300-2500 MHz)", - "settings_bandwidth": "带宽", - "settings_spreadingFactor": "扩频因子", - "settings_codingRate": "编码速率", - "settings_txPower": "TX 功率 (dBm)", + "settings_frequencyInvalid": "无效频率范围(300-2500 MHz)", + "settings_bandwidth": "带宽", + "settings_spreadingFactor": "扩频因子", + "settings_codingRate": "编码速率", + "settings_txPower": "TX 功率 (dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", - "settings_longRange": "远距离", - "settings_fastSpeed": "高速", - "settings_error": "错误:{message}", + "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", + "settings_longRange": "远距离", + "settings_fastSpeed": "高速", + "settings_error": "错误:{message}", "@settings_error": { "placeholders": { "message": { @@ -166,55 +166,55 @@ } } }, - "appSettings_title": "应用设置", - "appSettings_appearance": "外观", - "appSettings_theme": "主题", - "appSettings_themeSystem": "跟随系统", - "appSettings_themeLight": "浅色", - "appSettings_themeDark": "深色", - "appSettings_language": "语言", - "appSettings_languageSystem": "跟随系统", - "appSettings_languageEn": "英语", - "appSettings_languageFr": "法语", - "appSettings_languageEs": "西班牙语", - "appSettings_languageDe": "德语", - "appSettings_languagePl": "波兰语", - "appSettings_languageSl": "斯洛文尼亚语", - "appSettings_languagePt": "葡萄牙语", - "appSettings_languageIt": "意大利语", - "appSettings_languageZh": "中文", - "appSettings_languageSv": "瑞典语", - "appSettings_languageNl": "荷兰语", - "appSettings_languageSk": "斯洛伐克语", - "appSettings_languageBg": "保加利亚语", - "appSettings_languageRu": "俄语", - "appSettings_languageUk": "乌克兰语", - "appSettings_enableMessageTracing": "启用消息追踪", - "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", - "appSettings_notifications": "通知", - "appSettings_enableNotifications": "启用通知", - "appSettings_enableNotificationsSubtitle": "接收消息和广播的通知", - "appSettings_notificationPermissionDenied": "权限被拒绝", - "appSettings_notificationsEnabled": "通知已启用", - "appSettings_notificationsDisabled": "通知已关闭", - "appSettings_messageNotifications": "消息通知", - "appSettings_messageNotificationsSubtitle": "收到新消息时显示通知", - "appSettings_channelMessageNotifications": "频道消息通知", - "appSettings_channelMessageNotificationsSubtitle": "收到频道消息时显示通知", - "appSettings_advertisementNotifications": "广播通知", - "appSettings_advertisementNotificationsSubtitle": "发现新节点时显示通知", - "appSettings_messaging": "消息", - "appSettings_clearPathOnMaxRetry": "达到最大重试次数时清除路径", - "appSettings_clearPathOnMaxRetrySubtitle": "在5次发送失败后重置联系路径。", - "appSettings_pathsWillBeCleared": "5次失败后将重新路由", - "appSettings_pathsWillNotBeCleared": "路径不会自动清除", - "appSettings_autoRouteRotation": "自动路径轮换", - "appSettings_autoRouteRotationSubtitle": "在最佳路径和泛洪模式之间切换", - "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", - "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", - "appSettings_battery": "电池", - "appSettings_batteryChemistry": "电池类型", - "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", + "appSettings_title": "应用设置", + "appSettings_appearance": "外观", + "appSettings_theme": "主题", + "appSettings_themeSystem": "跟随系统", + "appSettings_themeLight": "浅色", + "appSettings_themeDark": "深色", + "appSettings_language": "语言", + "appSettings_languageSystem": "跟随系统", + "appSettings_languageEn": "英语", + "appSettings_languageFr": "法语", + "appSettings_languageEs": "西班牙语", + "appSettings_languageDe": "德语", + "appSettings_languagePl": "波兰语", + "appSettings_languageSl": "斯洛文尼亚语", + "appSettings_languagePt": "葡萄牙语", + "appSettings_languageIt": "意大利语", + "appSettings_languageZh": "中文", + "appSettings_languageSv": "瑞典语", + "appSettings_languageNl": "荷兰语", + "appSettings_languageSk": "斯洛伐克语", + "appSettings_languageBg": "保加利亚语", + "appSettings_languageRu": "俄语", + "appSettings_languageUk": "乌克兰语", + "appSettings_enableMessageTracing": "启用消息追踪", + "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", + "appSettings_notifications": "通知", + "appSettings_enableNotifications": "启用通知", + "appSettings_enableNotificationsSubtitle": "接收消息和广播的通知", + "appSettings_notificationPermissionDenied": "权限被拒绝", + "appSettings_notificationsEnabled": "通知已启用", + "appSettings_notificationsDisabled": "通知已关闭", + "appSettings_messageNotifications": "消息通知", + "appSettings_messageNotificationsSubtitle": "收到新消息时显示通知", + "appSettings_channelMessageNotifications": "频道消息通知", + "appSettings_channelMessageNotificationsSubtitle": "收到频道消息时显示通知", + "appSettings_advertisementNotifications": "广播通知", + "appSettings_advertisementNotificationsSubtitle": "发现新节点时显示通知", + "appSettings_messaging": "消息", + "appSettings_clearPathOnMaxRetry": "达到最大重试次数时清除路径", + "appSettings_clearPathOnMaxRetrySubtitle": "在5次发送失败后重置联系路径。", + "appSettings_pathsWillBeCleared": "5次失败后将重新路由", + "appSettings_pathsWillNotBeCleared": "路径不会自动清除", + "appSettings_autoRouteRotation": "自动路径轮换", + "appSettings_autoRouteRotationSubtitle": "在最佳路径和泛洪模式之间切换", + "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", + "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", + "appSettings_battery": "电池", + "appSettings_batteryChemistry": "电池类型", + "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -222,20 +222,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "请先连接设备", - "appSettings_batteryNmc": "18650 NMC 电池 (3.0-4.2V)", - "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", - "appSettings_batteryLipo": "锂聚合物电池 (3.0-4.2V)", - "appSettings_mapDisplay": "地图显示", - "appSettings_showRepeaters": "显示转发节点", - "appSettings_showRepeatersSubtitle": "在地图上显示转发节点", - "appSettings_showChatNodes": "显示聊天节点", - "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", - "appSettings_showOtherNodes": "显示其他节点", - "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", - "appSettings_timeFilter": "时间过滤器", - "appSettings_timeFilterShowAll": "显示所有节点", - "appSettings_timeFilterShowLast": "显示过去 {hours} 小时内的节点", + "appSettings_batteryChemistryConnectFirst": "请先连接设备", + "appSettings_batteryNmc": "18650 NMC 电池 (3.0-4.2V)", + "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", + "appSettings_batteryLipo": "锂聚合物电池 (3.0-4.2V)", + "appSettings_mapDisplay": "地图显示", + "appSettings_showRepeaters": "显示转发节点", + "appSettings_showRepeatersSubtitle": "在地图上显示转发节点", + "appSettings_showChatNodes": "显示聊天节点", + "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", + "appSettings_showOtherNodes": "显示其他节点", + "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", + "appSettings_timeFilter": "时间过滤器", + "appSettings_timeFilterShowAll": "显示所有节点", + "appSettings_timeFilterShowLast": "显示过去 {hours} 小时内的节点", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -243,16 +243,16 @@ } } }, - "appSettings_mapTimeFilter": "地图时间筛选", - "appSettings_showNodesDiscoveredWithin": "显示在此时间段内发现的节点:", - "appSettings_allTime": "所有时间", - "appSettings_lastHour": "过去一小时", - "appSettings_last6Hours": "过去6小时", - "appSettings_last24Hours": "过去24小时", - "appSettings_lastWeek": "上周", - "appSettings_offlineMapCache": "离线地图缓存", - "appSettings_noAreaSelected": "未选择任何区域", - "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", + "appSettings_mapTimeFilter": "地图时间筛选", + "appSettings_showNodesDiscoveredWithin": "显示在此时间段内发现的节点:", + "appSettings_allTime": "所有时间", + "appSettings_lastHour": "过去一小时", + "appSettings_last6Hours": "过去6小时", + "appSettings_last24Hours": "过去24小时", + "appSettings_lastWeek": "上周", + "appSettings_offlineMapCache": "离线地图缓存", + "appSettings_noAreaSelected": "未选择任何区域", + "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -263,19 +263,19 @@ } } }, - "appSettings_debugCard": "调试", - "appSettings_appDebugLogging": "应用调试日志", - "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以进行故障排除。", - "appSettings_appDebugLoggingEnabled": "调试日志已启用", - "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", - "contacts_title": "联系人", - "contacts_noContacts": "暂无联系人", - "contacts_contactsWillAppear": "当设备发送广播时,联系人将显示。", - "contacts_searchContacts": "搜索联系人...", - "contacts_noUnreadContacts": "没有未读内容", - "contacts_noContactsFound": "未找到任何联系人或群聊", - "contacts_deleteContact": "删除联系人", - "contacts_removeConfirm": "从联系人中移除 {contactName}?", + "appSettings_debugCard": "调试", + "appSettings_appDebugLogging": "应用调试日志", + "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以进行故障排除。", + "appSettings_appDebugLoggingEnabled": "调试日志已启用", + "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", + "contacts_title": "联系人", + "contacts_noContacts": "暂无联系人", + "contacts_contactsWillAppear": "当设备发送广播时,联系人将显示。", + "contacts_searchContacts": "搜索联系人...", + "contacts_noUnreadContacts": "没有未读内容", + "contacts_noContactsFound": "未找到任何联系人或群聊", + "contacts_deleteContact": "删除联系人", + "contacts_removeConfirm": "从联系人中移除 {contactName}?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -283,13 +283,13 @@ } } }, - "contacts_manageRepeater": "管理转发节点", - "contacts_manageRoom": "管理房间服务器", - "contacts_roomLogin": "服务器登录", - "contacts_openChat": "打开聊天", - "contacts_editGroup": "编辑群聊", - "contacts_deleteGroup": "删除群聊", - "contacts_deleteGroupConfirm": "删除群聊 \"{groupName}\"?", + "contacts_manageRepeater": "管理转发节点", + "contacts_manageRoom": "管理房间服务器", + "contacts_roomLogin": "服务器登录", + "contacts_openChat": "打开聊天", + "contacts_editGroup": "编辑群聊", + "contacts_deleteGroup": "删除群聊", + "contacts_deleteGroupConfirm": "删除群聊 \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -297,10 +297,10 @@ } } }, - "contacts_newGroup": "新建群聊", - "contacts_groupName": "群聊名称", - "contacts_groupNameRequired": "请输入群聊名称", - "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", + "contacts_newGroup": "新建群聊", + "contacts_groupName": "群聊名称", + "contacts_groupNameRequired": "请输入群聊名称", + "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -308,11 +308,11 @@ } } }, - "contacts_filterContacts": "筛选联系人...", - "contacts_noContactsMatchFilter": "没有符合条件的联系人", - "contacts_noMembers": "暂无成员", - "contacts_lastSeenNow": "刚刚", - "contacts_lastSeenMinsAgo": "最后在线 {minutes} 分钟前", + "contacts_filterContacts": "筛选联系人...", + "contacts_noContactsMatchFilter": "没有符合条件的联系人", + "contacts_noMembers": "暂无成员", + "contacts_lastSeenNow": "刚刚", + "contacts_lastSeenMinsAgo": "最后在线 {minutes} 分钟前", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -320,8 +320,8 @@ } } }, - "contacts_lastSeenHourAgo": "最后在线 1小时前", - "contacts_lastSeenHoursAgo": "最后在线 {hours} 小时前", + "contacts_lastSeenHourAgo": "最后在线 1小时前", + "contacts_lastSeenHoursAgo": "最后在线 {hours} 小时前", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -329,8 +329,8 @@ } } }, - "contacts_lastSeenDayAgo": "最后在线 1天前", - "contacts_lastSeenDaysAgo": "最后在线 {days} 天前", + "contacts_lastSeenDayAgo": "最后在线 1天前", + "contacts_lastSeenDaysAgo": "最后在线 {days} 天前", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -338,12 +338,12 @@ } } }, - "channels_title": "频道", - "channels_noChannelsConfigured": "未配置任何频道", - "channels_addPublicChannel": "添加公共频道", - "channels_searchChannels": "搜索频道...", - "channels_noChannelsFound": "未找到任何频道", - "channels_channelIndex": "频道 {index}", + "channels_title": "频道", + "channels_noChannelsConfigured": "未配置任何频道", + "channels_addPublicChannel": "添加公共频道", + "channels_searchChannels": "搜索频道...", + "channels_noChannelsFound": "未找到任何频道", + "channels_channelIndex": "频道 {index}", "@channels_channelIndex": { "placeholders": { "index": { @@ -351,16 +351,16 @@ } } }, - "channels_hashtagChannel": "标签频道", - "channels_public": "公共", - "channels_private": "私有", - "channels_publicChannel": "公共频道", - "channels_privateChannel": "私有频道", - "channels_editChannel": "编辑频道", - "channels_muteChannel": "静音频道", - "channels_unmuteChannel": "取消静音频道", - "channels_deleteChannel": "删除频道", - "channels_deleteChannelConfirm": "删除频道 \"{name}\"?此操作不可撤销。", + "channels_hashtagChannel": "标签频道", + "channels_public": "公共", + "channels_private": "私有", + "channels_publicChannel": "公共频道", + "channels_privateChannel": "私有频道", + "channels_editChannel": "编辑频道", + "channels_muteChannel": "静音频道", + "channels_unmuteChannel": "取消静音频道", + "channels_deleteChannel": "删除频道", + "channels_deleteChannelConfirm": "删除频道 \"{name}\"?此操作不可撤销。", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -368,7 +368,7 @@ } } }, - "channels_channelDeleted": "已删除频道 \"{name}\"", + "channels_channelDeleted": "已删除频道 \"{name}\"", "@channels_channelDeleted": { "placeholders": { "name": { @@ -376,16 +376,16 @@ } } }, - "channels_addChannel": "添加频道", - "channels_channelIndexLabel": "频道索引", - "channels_channelName": "频道名称", - "channels_usePublicChannel": "使用公共频道", - "channels_standardPublicPsk": "标准公共 PSK", - "channels_pskHex": "PSK (十六进制)", - "channels_generateRandomPsk": "生成随机 PSK", - "channels_enterChannelName": "请输入频道名称", - "channels_pskMustBe32Hex": "PSK 必须为 32 个十六进制字符", - "channels_channelAdded": "已添加频道 \"{name}\"", + "channels_addChannel": "添加频道", + "channels_channelIndexLabel": "频道索引", + "channels_channelName": "频道名称", + "channels_usePublicChannel": "使用公共频道", + "channels_standardPublicPsk": "标准公共 PSK", + "channels_pskHex": "PSK (十六进制)", + "channels_generateRandomPsk": "生成随机 PSK", + "channels_enterChannelName": "请输入频道名称", + "channels_pskMustBe32Hex": "PSK 必须为 32 个十六进制字符", + "channels_channelAdded": "已添加频道 \"{name}\"", "@channels_channelAdded": { "placeholders": { "name": { @@ -393,7 +393,7 @@ } } }, - "channels_editChannelTitle": "编辑频道 {index}", + "channels_editChannelTitle": "编辑频道 {index}", "@channels_editChannelTitle": { "placeholders": { "index": { @@ -401,8 +401,8 @@ } } }, - "channels_smazCompression": "SMAZ 压缩", - "channels_channelUpdated": "频道 \"{name}\" 已更新", + "channels_smazCompression": "SMAZ 压缩", + "channels_channelUpdated": "频道 \"{name}\" 已更新", "@channels_channelUpdated": { "placeholders": { "name": { @@ -410,28 +410,28 @@ } } }, - "channels_publicChannelAdded": "已添加公共频道", - "channels_sortBy": "排序方式", - "channels_sortManual": "手动", + "channels_publicChannelAdded": "已添加公共频道", + "channels_sortBy": "排序方式", + "channels_sortManual": "手动", "channels_sortAZ": "A-Z", - "channels_sortLatestMessages": "最新消息", - "channels_sortUnread": "未读", - "channels_createPrivateChannel": "创建私有频道", - "channels_createPrivateChannelDesc": "使用密钥保护。", - "channels_joinPrivateChannel": "加入私有频道", - "channels_joinPrivateChannelDesc": "手动输入密钥。", - "channels_joinPublicChannel": "加入公共频道", - "channels_joinPublicChannelDesc": "任何人都可以加入。", - "channels_joinHashtagChannel": "加入标签频道", - "channels_joinHashtagChannelDesc": "任何人都可以加入标签频道。", - "channels_scanQrCode": "扫描二维码", - "channels_scanQrCodeComingSoon": "即将推出", - "channels_enterHashtag": "输入标签", - "channels_hashtagHint": "例如:#团队", - "chat_noMessages": "暂无消息", - "chat_sendMessageToStart": "发送消息开始对话", - "chat_originalMessageNotFound": "找不到原始消息", - "chat_replyingTo": "正在回复 {name}", + "channels_sortLatestMessages": "最新消息", + "channels_sortUnread": "未读", + "channels_createPrivateChannel": "创建私有频道", + "channels_createPrivateChannelDesc": "使用密钥保护。", + "channels_joinPrivateChannel": "加入私有频道", + "channels_joinPrivateChannelDesc": "手动输入密钥。", + "channels_joinPublicChannel": "加入公共频道", + "channels_joinPublicChannelDesc": "任何人都可以加入。", + "channels_joinHashtagChannel": "加入标签频道", + "channels_joinHashtagChannelDesc": "任何人都可以加入标签频道。", + "channels_scanQrCode": "扫描二维码", + "channels_scanQrCodeComingSoon": "即将推出", + "channels_enterHashtag": "输入标签", + "channels_hashtagHint": "例如:#团队", + "chat_noMessages": "暂无消息", + "chat_sendMessageToStart": "发送消息开始对话", + "chat_originalMessageNotFound": "找不到原始消息", + "chat_replyingTo": "正在回复 {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -439,7 +439,7 @@ } } }, - "chat_replyTo": "回复 {name}", + "chat_replyTo": "回复 {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -447,8 +447,8 @@ } } }, - "chat_location": "位置", - "chat_sendMessageTo": "发送消息给 {contactName}", + "chat_location": "位置", + "chat_sendMessageTo": "发送消息给 {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -456,8 +456,8 @@ } } }, - "chat_typeMessage": "输入消息...", - "chat_messageTooLong": "消息过长(最多 {maxBytes} 字节)", + "chat_typeMessage": "输入消息...", + "chat_messageTooLong": "消息过长(最多 {maxBytes} 字节)", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -465,10 +465,10 @@ } } }, - "chat_messageCopied": "消息已复制", - "chat_messageDeleted": "消息已删除", - "chat_retryingMessage": "正在重试消息", - "chat_retryCount": "重试 {current}/{max}", + "chat_messageCopied": "消息已复制", + "chat_messageDeleted": "消息已删除", + "chat_retryingMessage": "正在重试消息", + "chat_retryCount": "重试 {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -479,33 +479,33 @@ } } }, - "chat_sendGif": "发送 GIF", - "chat_reply": "回复", - "chat_addReaction": "添加表情", - "chat_me": "我", - "emojiCategorySmileys": "表情", - "emojiCategoryGestures": "手势", - "emojiCategoryHearts": "爱心", - "emojiCategoryObjects": "物品", - "gifPicker_title": "选择 GIF", - "gifPicker_searchHint": "搜索 GIF...", - "gifPicker_poweredBy": "ç”± GIPHY 提供", - "gifPicker_noGifsFound": "未找到 GIF", - "gifPicker_failedLoad": "加载 GIF 失败", - "gifPicker_failedSearch": "搜索 GIF 失败", - "gifPicker_noInternet": "无网络连接", - "debugLog_appTitle": "应用调试日志", - "debugLog_bleTitle": "BLE 调试日志", - "debugLog_copyLog": "复制日志", - "debugLog_clearLog": "清除日志", - "debugLog_copied": "调试日志已复制", - "debugLog_bleCopied": "BLE 日志已复制", - "debugLog_noEntries": "暂无调试日志", - "debugLog_enableInSettings": "请在设置中启用应用调试日志。", - "debugLog_frames": "帧", - "debugLog_rawLogRx": "原始日志 RX", - "debugLog_noBleActivity": "暂无 BLE 活动", - "debugFrame_length": "帧长度:{count} 字节", + "chat_sendGif": "发送 GIF", + "chat_reply": "回复", + "chat_addReaction": "添加表情", + "chat_me": "我", + "emojiCategorySmileys": "表情", + "emojiCategoryGestures": "手势", + "emojiCategoryHearts": "爱心", + "emojiCategoryObjects": "物品", + "gifPicker_title": "选择 GIF", + "gifPicker_searchHint": "搜索 GIF...", + "gifPicker_poweredBy": "由 GIPHY 提供", + "gifPicker_noGifsFound": "未找到 GIF", + "gifPicker_failedLoad": "加载 GIF 失败", + "gifPicker_failedSearch": "搜索 GIF 失败", + "gifPicker_noInternet": "无网络连接", + "debugLog_appTitle": "应用调试日志", + "debugLog_bleTitle": "BLE 调试日志", + "debugLog_copyLog": "复制日志", + "debugLog_clearLog": "清除日志", + "debugLog_copied": "调试日志已复制", + "debugLog_bleCopied": "BLE 日志已复制", + "debugLog_noEntries": "暂无调试日志", + "debugLog_enableInSettings": "请在设置中启用应用调试日志。", + "debugLog_frames": "帧", + "debugLog_rawLogRx": "原始日志 RX", + "debugLog_noBleActivity": "暂无 BLE 活动", + "debugFrame_length": "帧长度:{count} 字节", "@debugFrame_length": { "placeholders": { "count": { @@ -513,7 +513,7 @@ } } }, - "debugFrame_command": "命令:0x{value}", + "debugFrame_command": "命令:0x{value}", "@debugFrame_command": { "placeholders": { "value": { @@ -521,8 +521,8 @@ } } }, - "debugFrame_textMessageHeader": "文本消息:", - "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", + "debugFrame_textMessageHeader": "文本消息:", + "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -530,7 +530,7 @@ } } }, - "debugFrame_timestamp": "- 时间戳:{timestamp}", + "debugFrame_timestamp": "- 时间戳:{timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -538,7 +538,7 @@ } } }, - "debugFrame_flags": "- 标志:0x{value}", + "debugFrame_flags": "- 标志:0x{value}", "@debugFrame_flags": { "placeholders": { "value": { @@ -546,7 +546,7 @@ } } }, - "debugFrame_textType": "- 文本类型:{type} ({label})", + "debugFrame_textType": "- 文本类型:{type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -557,9 +557,9 @@ } } }, - "debugFrame_textTypeCli": "命令行", - "debugFrame_textTypePlain": "纯文本", - "debugFrame_text": "- 文本:“{text}”", + "debugFrame_textTypeCli": "命令行", + "debugFrame_textTypePlain": "纯文本", + "debugFrame_text": "- 文本:“{text}”", "@debugFrame_text": { "placeholders": { "text": { @@ -567,16 +567,16 @@ } } }, - "debugFrame_hexDump": "十六进制数据:", - "chat_pathManagement": "路径管理", - "chat_routingMode": "路由模式", - "chat_autoUseSavedPath": "自动(使用保存的路径)", - "chat_forceFloodMode": "强制泛洪模式", - "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", - "chat_pathHistoryFull": "路径历史已满,请删除后再添加。", - "chat_hopSingular": "è·³", - "chat_hopPlural": "è·³", - "chat_hopsCount": "{count} è·³", + "debugFrame_hexDump": "十六进制数据:", + "chat_pathManagement": "路径管理", + "chat_routingMode": "路由模式", + "chat_autoUseSavedPath": "自动(使用保存的路径)", + "chat_forceFloodMode": "强制泛洪模式", + "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", + "chat_pathHistoryFull": "路径历史已满,请删除后再添加。", + "chat_hopSingular": "跳", + "chat_hopPlural": "跳", + "chat_hopsCount": "{count} 跳", "@chat_hopsCount": { "placeholders": { "count": { @@ -584,20 +584,20 @@ } } }, - "chat_successes": "成功", - "chat_removePath": "移除路径", - "chat_noPathHistoryYet": "暂无路径历史。\n发送消息以探索路径。", - "chat_pathActions": "路径操作:", - "chat_setCustomPath": "设置自定义路径", - "chat_setCustomPathSubtitle": "手动指定路由路径", - "chat_clearPath": "清除路径", - "chat_clearPathSubtitle": "清除当前路径,下次发送将重新尝试。", - "chat_pathCleared": "路径已清除。下一条消息将重新路由。", - "chat_floodModeSubtitle": "在应用栏中切换路由模式。", - "chat_floodModeEnabled": "泛洪模式已启用。可通过应用栏的路由图标切换。", - "chat_fullPath": "完整路径", - "chat_pathDetailsNotAvailable": "路径信息暂不可用,请尝试发送消息刷新。", - "chat_pathSetHops": "路径设置:{hopCount} è·³ - {status}", + "chat_successes": "成功", + "chat_removePath": "移除路径", + "chat_noPathHistoryYet": "暂无路径历史。\n发送消息以探索路径。", + "chat_pathActions": "路径操作:", + "chat_setCustomPath": "设置自定义路径", + "chat_setCustomPathSubtitle": "手动指定路由路径", + "chat_clearPath": "清除路径", + "chat_clearPathSubtitle": "清除当前路径,下次发送将重新尝试。", + "chat_pathCleared": "路径已清除。下一条消息将重新路由。", + "chat_floodModeSubtitle": "在应用栏中切换路由模式。", + "chat_floodModeEnabled": "泛洪模式已启用。可通过应用栏的路由图标切换。", + "chat_fullPath": "完整路径", + "chat_pathDetailsNotAvailable": "路径信息暂不可用,请尝试发送消息刷新。", + "chat_pathSetHops": "路径设置:{hopCount} 跳 - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -608,16 +608,16 @@ } } }, - "chat_pathSavedLocally": "已本地保存,连接设备后可同步。", - "chat_pathDeviceConfirmed": "设备已确认。", - "chat_pathDeviceNotConfirmed": "设备尚未确认。", - "chat_type": "类型", - "chat_path": "路径", - "chat_publicKey": "公钥", - "chat_compressOutgoingMessages": "压缩发送的消息", - "chat_floodForced": "泛洪(强制)", - "chat_directForced": "直连(强制)", - "chat_hopsForced": "{count} 跳(强制)", + "chat_pathSavedLocally": "已本地保存,连接设备后可同步。", + "chat_pathDeviceConfirmed": "设备已确认。", + "chat_pathDeviceNotConfirmed": "设备尚未确认。", + "chat_type": "类型", + "chat_path": "路径", + "chat_publicKey": "公钥", + "chat_compressOutgoingMessages": "压缩发送的消息", + "chat_floodForced": "泛洪(强制)", + "chat_directForced": "直连(强制)", + "chat_hopsForced": "{count} 跳(强制)", "@chat_hopsForced": { "placeholders": { "count": { @@ -625,10 +625,10 @@ } } }, - "chat_floodAuto": "自动泛洪", - "chat_direct": "直连", - "chat_poiShared": "共享位置", - "chat_unread": "未读:{count}", + "chat_floodAuto": "自动泛洪", + "chat_direct": "直连", + "chat_poiShared": "共享位置", + "chat_unread": "未读:{count}", "@chat_unread": { "placeholders": { "count": { @@ -636,10 +636,10 @@ } } }, - "chat_openLink": "打开链接?", - "chat_openLinkConfirmation": "是否使用浏览器打开此链接?", - "chat_open": "打开", - "chat_couldNotOpenLink": "无法打开链接:{url}", + "chat_openLink": "打开链接?", + "chat_openLinkConfirmation": "是否使用浏览器打开此链接?", + "chat_open": "打开", + "chat_couldNotOpenLink": "无法打开链接:{url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -647,11 +647,11 @@ } } }, - "chat_invalidLink": "无效的链接格式", - "map_title": "节点地图", - "map_noNodesWithLocation": "没有包含位置信息的节点", - "map_nodesNeedGps": "节点需要共享 GPS 坐标才能在地图上显示", - "map_nodesCount": "节点:{count}", + "chat_invalidLink": "无效的链接格式", + "map_title": "节点地图", + "map_noNodesWithLocation": "没有包含位置信息的节点", + "map_nodesNeedGps": "节点需要共享 GPS 坐标才能在地图上显示", + "map_nodesCount": "节点:{count}", "@map_nodesCount": { "placeholders": { "count": { @@ -659,7 +659,7 @@ } } }, - "map_pinsCount": "标记:{count}", + "map_pinsCount": "标记:{count}", "@map_pinsCount": { "placeholders": { "count": { @@ -667,27 +667,27 @@ } } }, - "map_chat": "聊天", - "map_repeater": "转发节点", - "map_room": "房间", - "map_sensor": "传感器", - "map_pinDm": "标记(私信)", - "map_pinPrivate": "私有", - "map_pinPublic": "公共", - "map_lastSeen": "最后在线", - "map_disconnectConfirm": "确定要断开与此设备的连接吗?", - "map_from": "来自", - "map_source": "来源", - "map_flags": "标志", - "map_shareMarkerHere": "在此分享标记", - "map_pinLabel": "标签", - "map_label": "标签", - "map_pointOfInterest": "兴趣点", - "map_sendToContact": "发送给联系人", - "map_sendToChannel": "发送到频道", - "map_noChannelsAvailable": "没有可用的频道", - "map_publicLocationShare": "公共位置共享", - "map_publicLocationShareConfirm": "您即将在 {channelLabel} 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。", + "map_chat": "聊天", + "map_repeater": "转发节点", + "map_room": "房间", + "map_sensor": "传感器", + "map_pinDm": "标记(私信)", + "map_pinPrivate": "私有", + "map_pinPublic": "公共", + "map_lastSeen": "最后在线", + "map_disconnectConfirm": "确定要断开与此设备的连接吗?", + "map_from": "来自", + "map_source": "来源", + "map_flags": "标志", + "map_shareMarkerHere": "在此分享标记", + "map_pinLabel": "标签", + "map_label": "标签", + "map_pointOfInterest": "兴趣点", + "map_sendToContact": "发送给联系人", + "map_sendToChannel": "发送到频道", + "map_noChannelsAvailable": "没有可用的频道", + "map_publicLocationShare": "公共位置共享", + "map_publicLocationShareConfirm": "您即将在 {channelLabel} 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -695,26 +695,26 @@ } } }, - "map_connectToShareMarkers": "连接设备以共享标记", - "map_filterNodes": "过滤节点", - "map_nodeTypes": "节点类型", - "map_chatNodes": "聊天节点", - "map_repeaters": "转发节点", - "map_otherNodes": "其他节点", - "map_keyPrefix": "关键字前缀", - "map_filterByKeyPrefix": "按关键字前缀筛选", - "map_publicKeyPrefix": "关键字前缀", - "map_markers": "标记", - "map_showSharedMarkers": "显示共享标记", - "map_lastSeenTime": "最后在线时间", - "map_sharedPin": "共享标记", - "map_joinRoom": "加入房间", - "map_manageRepeater": "管理转发节点", - "mapCache_title": "离线地图缓存", - "mapCache_selectAreaFirst": "请先选择要缓存的区域", - "mapCache_noTilesToDownload": "此区域没有可下载的瓦片", - "mapCache_downloadTilesTitle": "下载瓦片", - "mapCache_downloadTilesPrompt": "这需要下载 {count} 个瓦片", + "map_connectToShareMarkers": "连接设备以共享标记", + "map_filterNodes": "过滤节点", + "map_nodeTypes": "节点类型", + "map_chatNodes": "聊天节点", + "map_repeaters": "转发节点", + "map_otherNodes": "其他节点", + "map_keyPrefix": "关键字前缀", + "map_filterByKeyPrefix": "按关键字前缀筛选", + "map_publicKeyPrefix": "关键字前缀", + "map_markers": "标记", + "map_showSharedMarkers": "显示共享标记", + "map_lastSeenTime": "最后在线时间", + "map_sharedPin": "共享标记", + "map_joinRoom": "加入房间", + "map_manageRepeater": "管理转发节点", + "mapCache_title": "离线地图缓存", + "mapCache_selectAreaFirst": "请先选择要缓存的区域", + "mapCache_noTilesToDownload": "此区域没有可下载的瓦片", + "mapCache_downloadTilesTitle": "下载瓦片", + "mapCache_downloadTilesPrompt": "这需要下载 {count} 个瓦片", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -722,8 +722,8 @@ } } }, - "mapCache_downloadAction": "下载", - "mapCache_cachedTiles": "已缓存 {count} 个瓦片", + "mapCache_downloadAction": "下载", + "mapCache_cachedTiles": "已缓存 {count} 个瓦片", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -731,7 +731,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片({failed} 个失败)", + "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片({failed} 个失败)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -742,14 +742,14 @@ } } }, - "mapCache_clearOfflineCacheTitle": "清除离线缓存", - "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", - "mapCache_offlineCacheCleared": "离线缓存已清除", - "mapCache_noAreaSelected": "未选择区域", - "mapCache_cacheArea": "缓存区域", - "mapCache_useCurrentView": "使用当前视图", - "mapCache_zoomRange": "缩放范围", - "mapCache_estimatedTiles": "估计瓦片数:{count}", + "mapCache_clearOfflineCacheTitle": "清除离线缓存", + "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", + "mapCache_offlineCacheCleared": "离线缓存已清除", + "mapCache_noAreaSelected": "未选择区域", + "mapCache_cacheArea": "缓存区域", + "mapCache_useCurrentView": "使用当前视图", + "mapCache_zoomRange": "缩放范围", + "mapCache_estimatedTiles": "估计瓦片数:{count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -757,7 +757,7 @@ } } }, - "mapCache_downloadedTiles": "已下载 {completed}/{total}", + "mapCache_downloadedTiles": "已下载 {completed}/{total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -768,9 +768,9 @@ } } }, - "mapCache_downloadTilesButton": "下载瓦片", - "mapCache_clearCacheButton": "清除缓存", - "mapCache_failedDownloads": "下载失败:{count}", + "mapCache_downloadTilesButton": "下载瓦片", + "mapCache_clearCacheButton": "清除缓存", + "mapCache_failedDownloads": "下载失败:{count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -778,7 +778,7 @@ } } }, - "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", + "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -795,8 +795,8 @@ } } }, - "time_justNow": "刚才", - "time_minutesAgo": "{minutes}分钟前", + "time_justNow": "刚才", + "time_minutesAgo": "{minutes}分钟前", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -804,7 +804,7 @@ } } }, - "time_hoursAgo": "{hours}小时前", + "time_hoursAgo": "{hours}小时前", "@time_hoursAgo": { "placeholders": { "hours": { @@ -812,7 +812,7 @@ } } }, - "time_daysAgo": "{days}天前", + "time_daysAgo": "{days}天前", "@time_daysAgo": { "placeholders": { "days": { @@ -820,33 +820,33 @@ } } }, - "time_hour": "小时", - "time_hours": "小时", - "time_day": "天", - "time_days": "天", - "time_week": "周", - "time_weeks": "周", - "time_month": "月", - "time_months": "月", - "time_minutes": "分钟", - "time_allTime": "所有时间", - "dialog_disconnect": "æ–­å¼€", - "dialog_disconnectConfirm": "确定要断开与此设备的连接吗?", - "login_repeaterLogin": "转发节点登录", - "login_roomLogin": "房间服务器登录", - "login_password": "密码", - "login_enterPassword": "请输入密码", - "login_savePassword": "保存密码", - "login_savePasswordSubtitle": "密码将安全地存储在此设备上", - "login_repeaterDescription": "输入转发节点密码以访问设置和状态。", - "login_roomDescription": "输入房间服务器密码以访问设置和状态。", - "login_routing": "路由", - "login_routingMode": "路由模式", - "login_autoUseSavedPath": "自动(使用保存的路径)", - "login_forceFloodMode": "强制泛洪模式", - "login_managePaths": "管理路径", - "login_login": "登录", - "login_attempt": "尝试 {current}/{max}", + "time_hour": "小时", + "time_hours": "小时", + "time_day": "天", + "time_days": "天", + "time_week": "周", + "time_weeks": "周", + "time_month": "月", + "time_months": "月", + "time_minutes": "分钟", + "time_allTime": "所有时间", + "dialog_disconnect": "断开", + "dialog_disconnectConfirm": "确定要断开与此设备的连接吗?", + "login_repeaterLogin": "转发节点登录", + "login_roomLogin": "房间服务器登录", + "login_password": "密码", + "login_enterPassword": "请输入密码", + "login_savePassword": "保存密码", + "login_savePasswordSubtitle": "密码将安全地存储在此设备上", + "login_repeaterDescription": "输入转发节点密码以访问设置和状态。", + "login_roomDescription": "输入房间服务器密码以访问设置和状态。", + "login_routing": "路由", + "login_routingMode": "路由模式", + "login_autoUseSavedPath": "自动(使用保存的路径)", + "login_forceFloodMode": "强制泛洪模式", + "login_managePaths": "管理路径", + "login_login": "登录", + "login_attempt": "尝试 {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -857,7 +857,7 @@ } } }, - "login_failed": "登录失败:{error}", + "login_failed": "登录失败:{error}", "@login_failed": { "placeholders": { "error": { @@ -865,10 +865,10 @@ } } }, - "login_failedMessage": "登录失败。可能是密码错误或无法连接到服务器。", - "common_reload": "重新加载", - "common_clear": "清除", - "path_currentPath": "当前路径:{path}", + "login_failedMessage": "登录失败。可能是密码错误或无法连接到服务器。", + "common_reload": "重新加载", + "common_clear": "清除", + "path_currentPath": "当前路径:{path}", "@path_currentPath": { "placeholders": { "path": { @@ -876,7 +876,7 @@ } } }, - "path_usingHopsPath": "使用 {count} 跳路径", + "path_usingHopsPath": "使用 {count} 跳路径", "@path_usingHopsPath": { "placeholders": { "count": { @@ -884,16 +884,16 @@ } } }, - "path_enterCustomPath": "输入自定义路径", - "path_currentPathLabel": "当前路径", - "path_hexPrefixInstructions": "请输入每个中继节点的2字符十六进制前缀,用逗号分隔。", - "path_hexPrefixExample": "例如:A1, F2, 3C(每个节点使用其公钥的第一字节)", - "path_labelHexPrefixes": "路径(十六进制前缀)", - "path_helperMaxHops": "最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。", - "path_selectFromContacts": "或从联系人列表中选择:", - "path_noRepeatersFound": "未找到任何转发节点或房间服务器。", - "path_customPathsRequire": "自定义路径需要中间节点转发消息。", - "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", + "path_enterCustomPath": "输入自定义路径", + "path_currentPathLabel": "当前路径", + "path_hexPrefixInstructions": "请输入每个中继节点的2字符十六进制前缀,用逗号分隔。", + "path_hexPrefixExample": "例如:A1, F2, 3C(每个节点使用其公钥的第一字节)", + "path_labelHexPrefixes": "路径(十六进制前缀)", + "path_helperMaxHops": "最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。", + "path_selectFromContacts": "或从联系人列表中选择:", + "path_noRepeatersFound": "未找到任何转发节点或房间服务器。", + "path_customPathsRequire": "自定义路径需要中间节点转发消息。", + "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -901,29 +901,29 @@ } } }, - "path_tooLong": "路径过长,最多允许 64 跳。", - "path_setPath": "设置路径", - "repeater_management": "转发节点管理", - "room_management": "房间服务器管理", - "repeater_managementTools": "管理工具", - "repeater_status": "状态", - "repeater_statusSubtitle": "查看转发节点状态、统计和邻居", - "repeater_telemetry": "遥测", - "repeater_telemetrySubtitle": "查看传感器和系统状态数据", - "repeater_cli": "命令行", - "repeater_cliSubtitle": "向转发节点发送命令", - "repeater_neighbors": "邻居", - "repeater_neighborsSubtitle": "查看邻居节点(零跳)", - "repeater_settings": "设置", - "repeater_settingsSubtitle": "配置转发节点参数", - "repeater_statusTitle": "转发节点状态", - "repeater_routingMode": "路由模式", - "repeater_autoUseSavedPath": "自动(使用保存的路径)", - "repeater_forceFloodMode": "强制泛洪模式", - "repeater_pathManagement": "路径管理", - "repeater_refresh": "刷新", - "repeater_statusRequestTimeout": "状态请求超时", - "repeater_errorLoadingStatus": "加载状态时出错:{error}", + "path_tooLong": "路径过长,最多允许 64 跳。", + "path_setPath": "设置路径", + "repeater_management": "转发节点管理", + "room_management": "房间服务器管理", + "repeater_managementTools": "管理工具", + "repeater_status": "状态", + "repeater_statusSubtitle": "查看转发节点状态、统计和邻居", + "repeater_telemetry": "遥测", + "repeater_telemetrySubtitle": "查看传感器和系统状态数据", + "repeater_cli": "命令行", + "repeater_cliSubtitle": "向转发节点发送命令", + "repeater_neighbors": "邻居", + "repeater_neighborsSubtitle": "查看邻居节点(零跳)", + "repeater_settings": "设置", + "repeater_settingsSubtitle": "配置转发节点参数", + "repeater_statusTitle": "转发节点状态", + "repeater_routingMode": "路由模式", + "repeater_autoUseSavedPath": "自动(使用保存的路径)", + "repeater_forceFloodMode": "强制泛洪模式", + "repeater_pathManagement": "路径管理", + "repeater_refresh": "刷新", + "repeater_statusRequestTimeout": "状态请求超时", + "repeater_errorLoadingStatus": "加载状态时出错:{error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -931,23 +931,23 @@ } } }, - "repeater_systemInformation": "系统信息", - "repeater_battery": "电池", - "repeater_clockAtLogin": "登录时的时钟", - "repeater_uptime": "运行时间", - "repeater_queueLength": "队列长度", - "repeater_debugFlags": "调试标志", - "repeater_radioStatistics": "无线电统计", - "repeater_lastRssi": "上次 RSSI", - "repeater_lastSnr": "上次 SNR", - "repeater_noiseFloor": "底噪", - "repeater_txAirtime": "发送空中时间", - "repeater_rxAirtime": "接收空中时间", - "repeater_packetStatistics": "数据包统计", - "repeater_sent": "发送", - "repeater_received": "接收", - "repeater_duplicates": "重复", - "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}ç§’", + "repeater_systemInformation": "系统信息", + "repeater_battery": "电池", + "repeater_clockAtLogin": "登录时的时钟", + "repeater_uptime": "运行时间", + "repeater_queueLength": "队列长度", + "repeater_debugFlags": "调试标志", + "repeater_radioStatistics": "无线电统计", + "repeater_lastRssi": "上次 RSSI", + "repeater_lastSnr": "上次 SNR", + "repeater_noiseFloor": "底噪", + "repeater_txAirtime": "发送空中时间", + "repeater_rxAirtime": "接收空中时间", + "repeater_packetStatistics": "数据包统计", + "repeater_sent": "发送", + "repeater_received": "接收", + "repeater_duplicates": "重复", + "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}秒", "@repeater_daysHoursMinsSecs": { "placeholders": { "days": { @@ -964,7 +964,7 @@ } } }, - "repeater_packetTxTotal": "总计:{total},泛洪:{flood},直连:{direct}", + "repeater_packetTxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -978,7 +978,7 @@ } } }, - "repeater_packetRxTotal": "总计:{total},泛洪:{flood},直连:{direct}", + "repeater_packetRxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -992,7 +992,7 @@ } } }, - "repeater_duplicatesFloodDirect": "泛洪:{flood},直连:{direct}", + "repeater_duplicatesFloodDirect": "泛洪:{flood},直连:{direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -1003,7 +1003,7 @@ } } }, - "repeater_duplicatesTotal": "总计:{total}", + "repeater_duplicatesTotal": "总计:{total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -1011,37 +1011,37 @@ } } }, - "repeater_settingsTitle": "转发节点设置", - "repeater_basicSettings": "基本设置", - "repeater_repeaterName": "转发节点名称", - "repeater_repeaterNameHelper": "此转发节点的显示名称", - "repeater_adminPassword": "管理员密码", - "repeater_adminPasswordHelper": "完整访问密码", - "repeater_guestPassword": "访客密码", - "repeater_guestPasswordHelper": "只读访问密码", - "repeater_radioSettings": "无线电设置", - "repeater_frequencyMhz": "频率 (MHz)", + "repeater_settingsTitle": "转发节点设置", + "repeater_basicSettings": "基本设置", + "repeater_repeaterName": "转发节点名称", + "repeater_repeaterNameHelper": "此转发节点的显示名称", + "repeater_adminPassword": "管理员密码", + "repeater_adminPasswordHelper": "完整访问密码", + "repeater_guestPassword": "访客密码", + "repeater_guestPasswordHelper": "只读访问密码", + "repeater_radioSettings": "无线电设置", + "repeater_frequencyMhz": "频率 (MHz)", "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX 功率", + "repeater_txPower": "TX 功率", "repeater_txPowerHelper": "1-30 dBm", - "repeater_bandwidth": "带宽", - "repeater_spreadingFactor": "扩频因子", - "repeater_codingRate": "编码速率", - "repeater_locationSettings": "位置设置", - "repeater_latitude": "纬度", - "repeater_latitudeHelper": "十进制,例如 37.7749", - "repeater_longitude": "经度", - "repeater_longitudeHelper": "十进制,例如 -122.4194", - "repeater_features": "功能", - "repeater_packetForwarding": "数据包转发", - "repeater_packetForwardingSubtitle": "启用转发节点转发数据包", - "repeater_guestAccess": "访客访问", - "repeater_guestAccessSubtitle": "允许访客只读权限", - "repeater_privacyMode": "隐私模式", - "repeater_privacyModeSubtitle": "在广播中隐藏姓名/位置", - "repeater_advertisementSettings": "广播设置", - "repeater_localAdvertInterval": "本地广播间隔", - "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", + "repeater_bandwidth": "带宽", + "repeater_spreadingFactor": "扩频因子", + "repeater_codingRate": "编码速率", + "repeater_locationSettings": "位置设置", + "repeater_latitude": "纬度", + "repeater_latitudeHelper": "十进制,例如 37.7749", + "repeater_longitude": "经度", + "repeater_longitudeHelper": "十进制,例如 -122.4194", + "repeater_features": "功能", + "repeater_packetForwarding": "数据包转发", + "repeater_packetForwardingSubtitle": "启用转发节点转发数据包", + "repeater_guestAccess": "访客访问", + "repeater_guestAccessSubtitle": "允许访客只读权限", + "repeater_privacyMode": "隐私模式", + "repeater_privacyModeSubtitle": "在广播中隐藏姓名/位置", + "repeater_advertisementSettings": "广播设置", + "repeater_localAdvertInterval": "本地广播间隔", + "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1049,8 +1049,8 @@ } } }, - "repeater_floodAdvertInterval": "泛洪广播间隔", - "repeater_floodAdvertIntervalHours": "{hours} 小时", + "repeater_floodAdvertInterval": "泛洪广播间隔", + "repeater_floodAdvertIntervalHours": "{hours} 小时", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1058,19 +1058,19 @@ } } }, - "repeater_encryptedAdvertInterval": "加密广播间隔", - "repeater_dangerZone": "危险设置", - "repeater_rebootRepeater": "重启转发节点", - "repeater_rebootRepeaterSubtitle": "重启转发节点设备", - "repeater_rebootRepeaterConfirm": "确定要重启此转发节点吗?", - "repeater_regenerateIdentityKey": "重新生成身份密钥", - "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", - "repeater_regenerateIdentityKeyConfirm": "这将为转发节点生成新身份,继续吗?", - "repeater_eraseFileSystem": "擦除文件系统", - "repeater_eraseFileSystemSubtitle": "格式化转发节点文件系统", - "repeater_eraseFileSystemConfirm": "警告:此操作将清除转发节点上的所有数据,且无法恢复!", - "repeater_eraseSerialOnly": "擦除功能仅可通过串行控制台使用。", - "repeater_commandSent": "命令已发送:{command}", + "repeater_encryptedAdvertInterval": "加密广播间隔", + "repeater_dangerZone": "危险设置", + "repeater_rebootRepeater": "重启转发节点", + "repeater_rebootRepeaterSubtitle": "重启转发节点设备", + "repeater_rebootRepeaterConfirm": "确定要重启此转发节点吗?", + "repeater_regenerateIdentityKey": "重新生成身份密钥", + "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", + "repeater_regenerateIdentityKeyConfirm": "这将为转发节点生成新身份,继续吗?", + "repeater_eraseFileSystem": "擦除文件系统", + "repeater_eraseFileSystemSubtitle": "格式化转发节点文件系统", + "repeater_eraseFileSystemConfirm": "警告:此操作将清除转发节点上的所有数据,且无法恢复!", + "repeater_eraseSerialOnly": "擦除功能仅可通过串行控制台使用。", + "repeater_commandSent": "命令已发送:{command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1078,7 +1078,7 @@ } } }, - "repeater_errorSendingCommand": "发送命令时出错:{error}", + "repeater_errorSendingCommand": "发送命令时出错:{error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1086,9 +1086,9 @@ } } }, - "repeater_confirm": "确认", - "repeater_settingsSaved": "设置保存成功", - "repeater_errorSavingSettings": "保存设置时出错:{error}", + "repeater_confirm": "确认", + "repeater_settingsSaved": "设置保存成功", + "repeater_errorSavingSettings": "保存设置时出错:{error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1096,15 +1096,15 @@ } } }, - "repeater_refreshBasicSettings": "刷新基本设置", - "repeater_refreshRadioSettings": "刷新无线电设置", - "repeater_refreshTxPower": "刷新 TX 功率", - "repeater_refreshLocationSettings": "刷新位置设置", - "repeater_refreshPacketForwarding": "刷新包转发", - "repeater_refreshGuestAccess": "刷新访客权限", - "repeater_refreshPrivacyMode": "刷新隐私模式", - "repeater_refreshAdvertisementSettings": "刷新广播设置", - "repeater_refreshed": "{label} 已刷新", + "repeater_refreshBasicSettings": "刷新基本设置", + "repeater_refreshRadioSettings": "刷新无线电设置", + "repeater_refreshTxPower": "刷新 TX 功率", + "repeater_refreshLocationSettings": "刷新位置设置", + "repeater_refreshPacketForwarding": "刷新包转发", + "repeater_refreshGuestAccess": "刷新访客权限", + "repeater_refreshPrivacyMode": "刷新隐私模式", + "repeater_refreshAdvertisementSettings": "刷新广播设置", + "repeater_refreshed": "{label} 已刷新", "@repeater_refreshed": { "placeholders": { "label": { @@ -1112,7 +1112,7 @@ } } }, - "repeater_errorRefreshing": "刷新 {label} 时出错", + "repeater_errorRefreshing": "刷新 {label} 时出错", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1120,18 +1120,18 @@ } } }, - "repeater_cliTitle": "转发节点命令行", - "repeater_debugNextCommand": "调试下一条命令", - "repeater_commandHelp": "帮助", - "repeater_clearHistory": "清除历史", - "repeater_noCommandsSent": "尚未发送命令", - "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", - "repeater_enterCommandHint": "输入命令...", - "repeater_previousCommand": "上一条命令", - "repeater_nextCommand": "下一条命令", - "repeater_enterCommandFirst": "请先输入命令", - "repeater_cliCommandFrameTitle": "CLI 命令帧", - "repeater_cliCommandError": "错误:{error}", + "repeater_cliTitle": "转发节点命令行", + "repeater_debugNextCommand": "调试下一条命令", + "repeater_commandHelp": "帮助", + "repeater_clearHistory": "清除历史", + "repeater_noCommandsSent": "尚未发送命令", + "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", + "repeater_enterCommandHint": "输入命令...", + "repeater_previousCommand": "上一条命令", + "repeater_nextCommand": "下一条命令", + "repeater_enterCommandFirst": "请先输入命令", + "repeater_cliCommandFrameTitle": "CLI 命令帧", + "repeater_cliCommandError": "错误:{error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1139,81 +1139,81 @@ } } }, - "repeater_cliQuickGetName": "获取名称", - "repeater_cliQuickGetRadio": "获取无线电设置", - "repeater_cliQuickGetTx": "获取 TX", - "repeater_cliQuickNeighbors": "邻居", - "repeater_cliQuickVersion": "版本", - "repeater_cliQuickAdvertise": "发送广播", - "repeater_cliQuickClock": "æ—¶é’Ÿ", - "repeater_cliHelpAdvert": "发送广播包", - "repeater_cliHelpReboot": "重启设备。(注意:可能会收到超时错误,属于正常现象)", - "repeater_cliHelpClock": "显示设备当前时间", - "repeater_cliHelpPassword": "设置新的管理员密码", - "repeater_cliHelpVersion": "显示设备版本和固件构建日期", - "repeater_cliHelpClearStats": "重置各种统计数据", - "repeater_cliHelpSetAf": "设置时间因子", - "repeater_cliHelpSetTx": "设置 LoRa 发射功率 (dBm)(重启生效)", - "repeater_cliHelpSetRepeat": "启用或禁用此节点的转发功能", - "repeater_cliHelpSetAllowReadOnly": "(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)", - "repeater_cliHelpSetFloodMax": "设置最大传入数据包跳数(≥该值则不转发)", - "repeater_cliHelpSetIntThresh": "设置干扰阈值 (dB),默认14,设为0禁用", - "repeater_cliHelpSetAgcResetInterval": "设置 AGC 重置间隔(秒),设为0禁用", - "repeater_cliHelpSetMultiAcks": "启用或禁用“多重确认”功能", - "repeater_cliHelpSetAdvertInterval": "设置本地广播间隔(分钟),设为0禁用", - "repeater_cliHelpSetFloodAdvertInterval": "设置泛洪广播间隔(小时),设为0禁用", - "repeater_cliHelpSetGuestPassword": "设置/更新访客密码", - "repeater_cliHelpSetName": "设置广播名称", - "repeater_cliHelpSetLat": "设置广播纬度(十进制)", - "repeater_cliHelpSetLon": "设置广播经度(十进制)", - "repeater_cliHelpSetRadio": "完全重设无线电参数并保存,需重启生效", - "repeater_cliHelpSetRxDelay": "(实验性)设置接收延迟基数,设为0禁用", - "repeater_cliHelpSetTxDelay": "通过因子和随机时隙延迟泛洪数据包转发,降低冲突", - "repeater_cliHelpSetDirectTxDelay": "同 txdelay,用于直连模式数据包", - "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接", - "repeater_cliHelpSetBridgeDelay": "设置桥接转发延迟", - "repeater_cliHelpSetBridgeSource": "选择桥接器转发接收或发送的数据包", - "repeater_cliHelpSetBridgeBaud": "设置 RS232 桥接串口波特率", - "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥接密钥", - "repeater_cliHelpSetAdcMultiplier": "设置电池电压校正系数(特定板支持)", - "repeater_cliHelpTempRadio": "临时设置无线电参数指定分钟,之后恢复(不保存)", - "repeater_cliHelpSetPerm": "修改 ACL,权限位:0访客、1只读、2读写、3管理员", - "repeater_cliHelpGetBridgeType": "支持桥接模式:RS232、ESPNOW", - "repeater_cliHelpLogStart": "开始记录数据包到文件系统", - "repeater_cliHelpLogStop": "停止记录数据包", - "repeater_cliHelpLogErase": "删除所有记录的数据包", - "repeater_cliHelpNeighbors": "显示零跳广播收到的其他转发节点列表", - "repeater_cliHelpNeighborRemove": "从邻居列表删除第一个匹配项(通过公钥前缀)", - "repeater_cliHelpRegion": "(仅串口)列出所有定义区域及当前泛洪权限", - "repeater_cliHelpRegionLoad": "特殊多命令调用,以空行结束", - "repeater_cliHelpRegionGet": "搜索指定前缀的区域", - "repeater_cliHelpRegionPut": "添加或更新区域定义", - "repeater_cliHelpRegionRemove": "删除指定区域定义", - "repeater_cliHelpRegionAllowf": "为区域设置“泛洪”权限", - "repeater_cliHelpRegionDenyf": "移除区域的“泛洪”权限", - "repeater_cliHelpRegionHome": "返回当前“主区域”(预留)", - "repeater_cliHelpRegionHomeSet": "设置“主”区域", - "repeater_cliHelpRegionSave": "保存区域列表到存储", - "repeater_cliHelpGps": "显示 GPS 状态", - "repeater_cliHelpGpsOnOff": "切换 GPS 电源", - "repeater_cliHelpGpsSync": "将节点时间与 GPS 同步", - "repeater_cliHelpGpsSetLoc": "将节点坐标设为 GPS 坐标并保存", - "repeater_cliHelpGpsAdvert": "设置位置广播配置:none/share/prefs", - "repeater_cliHelpGpsAdvertSet": "设置广播位置配置", - "repeater_commandsListTitle": "命令列表", - "repeater_commandsListNote": "注意:多数 set 命令也有对应的 get 命令", - "repeater_general": "通用", - "repeater_settingsCategory": "设置", - "repeater_bridge": "桥接", - "repeater_logging": "日志", - "repeater_neighborsRepeaterOnly": "邻居(仅转发节点)", - "repeater_regionManagementRepeaterOnly": "区域管理(仅转发节点)", - "repeater_regionNote": "区域命令用于管理区域定义和权限", - "repeater_gpsManagement": "GPS 管理", - "repeater_gpsNote": "GPS 命令用于位置相关任务", - "telemetry_receivedData": "接收到的遥测数据", - "telemetry_requestTimeout": "遥测请求超时", - "telemetry_errorLoading": "加载遥测数据时出错:{error}", + "repeater_cliQuickGetName": "获取名称", + "repeater_cliQuickGetRadio": "获取无线电设置", + "repeater_cliQuickGetTx": "获取 TX", + "repeater_cliQuickNeighbors": "邻居", + "repeater_cliQuickVersion": "版本", + "repeater_cliQuickAdvertise": "发送广播", + "repeater_cliQuickClock": "时钟", + "repeater_cliHelpAdvert": "发送广播包", + "repeater_cliHelpReboot": "重启设备。(注意:可能会收到超时错误,属于正常现象)", + "repeater_cliHelpClock": "显示设备当前时间", + "repeater_cliHelpPassword": "设置新的管理员密码", + "repeater_cliHelpVersion": "显示设备版本和固件构建日期", + "repeater_cliHelpClearStats": "重置各种统计数据", + "repeater_cliHelpSetAf": "设置时间因子", + "repeater_cliHelpSetTx": "设置 LoRa 发射功率 (dBm)(重启生效)", + "repeater_cliHelpSetRepeat": "启用或禁用此节点的转发功能", + "repeater_cliHelpSetAllowReadOnly": "(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)", + "repeater_cliHelpSetFloodMax": "设置最大传入数据包跳数(≥该值则不转发)", + "repeater_cliHelpSetIntThresh": "设置干扰阈值 (dB),默认14,设为0禁用", + "repeater_cliHelpSetAgcResetInterval": "设置 AGC 重置间隔(秒),设为0禁用", + "repeater_cliHelpSetMultiAcks": "启用或禁用“多重确认”功能", + "repeater_cliHelpSetAdvertInterval": "设置本地广播间隔(分钟),设为0禁用", + "repeater_cliHelpSetFloodAdvertInterval": "设置泛洪广播间隔(小时),设为0禁用", + "repeater_cliHelpSetGuestPassword": "设置/更新访客密码", + "repeater_cliHelpSetName": "设置广播名称", + "repeater_cliHelpSetLat": "设置广播纬度(十进制)", + "repeater_cliHelpSetLon": "设置广播经度(十进制)", + "repeater_cliHelpSetRadio": "完全重设无线电参数并保存,需重启生效", + "repeater_cliHelpSetRxDelay": "(实验性)设置接收延迟基数,设为0禁用", + "repeater_cliHelpSetTxDelay": "通过因子和随机时隙延迟泛洪数据包转发,降低冲突", + "repeater_cliHelpSetDirectTxDelay": "同 txdelay,用于直连模式数据包", + "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接", + "repeater_cliHelpSetBridgeDelay": "设置桥接转发延迟", + "repeater_cliHelpSetBridgeSource": "选择桥接器转发接收或发送的数据包", + "repeater_cliHelpSetBridgeBaud": "设置 RS232 桥接串口波特率", + "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥接密钥", + "repeater_cliHelpSetAdcMultiplier": "设置电池电压校正系数(特定板支持)", + "repeater_cliHelpTempRadio": "临时设置无线电参数指定分钟,之后恢复(不保存)", + "repeater_cliHelpSetPerm": "修改 ACL,权限位:0访客、1只读、2读写、3管理员", + "repeater_cliHelpGetBridgeType": "支持桥接模式:RS232、ESPNOW", + "repeater_cliHelpLogStart": "开始记录数据包到文件系统", + "repeater_cliHelpLogStop": "停止记录数据包", + "repeater_cliHelpLogErase": "删除所有记录的数据包", + "repeater_cliHelpNeighbors": "显示零跳广播收到的其他转发节点列表", + "repeater_cliHelpNeighborRemove": "从邻居列表删除第一个匹配项(通过公钥前缀)", + "repeater_cliHelpRegion": "(仅串口)列出所有定义区域及当前泛洪权限", + "repeater_cliHelpRegionLoad": "特殊多命令调用,以空行结束", + "repeater_cliHelpRegionGet": "搜索指定前缀的区域", + "repeater_cliHelpRegionPut": "添加或更新区域定义", + "repeater_cliHelpRegionRemove": "删除指定区域定义", + "repeater_cliHelpRegionAllowf": "为区域设置“泛洪”权限", + "repeater_cliHelpRegionDenyf": "移除区域的“泛洪”权限", + "repeater_cliHelpRegionHome": "返回当前“主区域”(预留)", + "repeater_cliHelpRegionHomeSet": "设置“主”区域", + "repeater_cliHelpRegionSave": "保存区域列表到存储", + "repeater_cliHelpGps": "显示 GPS 状态", + "repeater_cliHelpGpsOnOff": "切换 GPS 电源", + "repeater_cliHelpGpsSync": "将节点时间与 GPS 同步", + "repeater_cliHelpGpsSetLoc": "将节点坐标设为 GPS 坐标并保存", + "repeater_cliHelpGpsAdvert": "设置位置广播配置:none/share/prefs", + "repeater_cliHelpGpsAdvertSet": "设置广播位置配置", + "repeater_commandsListTitle": "命令列表", + "repeater_commandsListNote": "注意:多数 set 命令也有对应的 get 命令", + "repeater_general": "通用", + "repeater_settingsCategory": "设置", + "repeater_bridge": "桥接", + "repeater_logging": "日志", + "repeater_neighborsRepeaterOnly": "邻居(仅转发节点)", + "repeater_regionManagementRepeaterOnly": "区域管理(仅转发节点)", + "repeater_regionNote": "区域命令用于管理区域定义和权限", + "repeater_gpsManagement": "GPS 管理", + "repeater_gpsNote": "GPS 命令用于位置相关任务", + "telemetry_receivedData": "接收到的遥测数据", + "telemetry_requestTimeout": "遥测请求超时", + "telemetry_errorLoading": "加载遥测数据时出错:{error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1221,8 +1221,8 @@ } } }, - "telemetry_noData": "暂无遥测数据", - "telemetry_channelTitle": "频道 {channel}", + "telemetry_noData": "暂无遥测数据", + "telemetry_channelTitle": "频道 {channel}", "@telemetry_channelTitle": { "placeholders": { "channel": { @@ -1230,11 +1230,11 @@ } } }, - "telemetry_batteryLabel": "电池", - "telemetry_voltageLabel": "电压", - "telemetry_mcuTemperatureLabel": "MCU 温度", - "telemetry_temperatureLabel": "温度", - "telemetry_currentLabel": "电流", + "telemetry_batteryLabel": "电池", + "telemetry_voltageLabel": "电压", + "telemetry_mcuTemperatureLabel": "MCU 温度", + "telemetry_temperatureLabel": "温度", + "telemetry_currentLabel": "电流", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1262,7 +1262,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1273,9 +1273,9 @@ } } }, - "neighbors_receivedData": "已接收邻居信息", - "neighbors_requestTimedOut": "邻居请求超时", - "neighbors_errorLoading": "加载邻居时出错:{error}", + "neighbors_receivedData": "已接收邻居信息", + "neighbors_requestTimedOut": "邻居请求超时", + "neighbors_errorLoading": "加载邻居时出错:{error}", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1283,9 +1283,9 @@ } } }, - "neighbors_repeatersNeighbors": "转发节点的邻居", - "neighbors_noData": "暂无邻居信息", - "neighbors_unknownContact": "未知 {pubkey}", + "neighbors_repeatersNeighbors": "转发节点的邻居", + "neighbors_noData": "暂无邻居信息", + "neighbors_unknownContact": "未知 {pubkey}", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1293,7 +1293,7 @@ } } }, - "neighbors_heardAgo": "听到:{time}前", + "neighbors_heardAgo": "听到:{time}前", "@neighbors_heardAgo": { "placeholders": { "time": { @@ -1301,18 +1301,18 @@ } } }, - "channelPath_title": "数据包路径", - "channelPath_viewMap": "查看地图", - "channelPath_otherObservedPaths": "其他观察到的路径", - "channelPath_repeaterHops": "转发节点跳数", - "channelPath_noHopDetails": "此数据包未提供详细信息", - "channelPath_messageDetails": "消息详情", - "channelPath_senderLabel": "发送者", - "channelPath_timeLabel": "æ—¶é—´", - "channelPath_repeatsLabel": "重复", - "channelPath_pathLabel": "路径 {index}", - "channelPath_observedLabel": "观察到的", - "channelPath_observedPathTitle": "观察到的路径 {index} • {hops}", + "channelPath_title": "数据包路径", + "channelPath_viewMap": "查看地图", + "channelPath_otherObservedPaths": "其他观察到的路径", + "channelPath_repeaterHops": "转发节点跳数", + "channelPath_noHopDetails": "此数据包未提供详细信息", + "channelPath_messageDetails": "消息详情", + "channelPath_senderLabel": "发送者", + "channelPath_timeLabel": "时间", + "channelPath_repeatsLabel": "重复", + "channelPath_pathLabel": "路径 {index}", + "channelPath_observedLabel": "观察到的", + "channelPath_observedPathTitle": "观察到的路径 {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1323,7 +1323,7 @@ } } }, - "channelPath_noLocationData": "无位置信息", + "channelPath_noLocationData": "无位置信息", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1346,10 +1346,10 @@ } } }, - "channelPath_unknownPath": "未知", - "channelPath_floodPath": "泛洪", - "channelPath_directPath": "直连", - "channelPath_observedZeroOf": "0 / {total} è·³", + "channelPath_unknownPath": "未知", + "channelPath_floodPath": "泛洪", + "channelPath_directPath": "直连", + "channelPath_observedZeroOf": "0 / {total} 跳", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1357,7 +1357,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} / {total} è·³", + "channelPath_observedSomeOf": "{observed} / {total} 跳", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1368,9 +1368,9 @@ } } }, - "channelPath_mapTitle": "路径地图", - "channelPath_noRepeaterLocations": "此路径上没有可用的转发节点位置信息", - "channelPath_primaryPath": "路径 {index}(主要)", + "channelPath_mapTitle": "路径地图", + "channelPath_noRepeaterLocations": "此路径上没有可用的转发节点位置信息", + "channelPath_primaryPath": "路径 {index}(主要)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1385,9 +1385,9 @@ } } }, - "channelPath_pathLabelTitle": "路径", - "channelPath_observedPathHeader": "观察到的路径", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_pathLabelTitle": "路径", + "channelPath_observedPathHeader": "观察到的路径", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { @@ -1398,14 +1398,14 @@ } } }, - "channelPath_noHopDetailsAvailable": "此数据包暂无详细信息", - "channelPath_unknownRepeater": "未知转发节点", - "community_title": "社区", - "community_create": "创建社区", - "community_createDesc": "创建新社区并通过二维码分享。", - "community_join": "加入", - "community_joinTitle": "加入社区", - "community_joinConfirmation": "是否加入社区 \"{name}\"?", + "channelPath_noHopDetailsAvailable": "此数据包暂无详细信息", + "channelPath_unknownRepeater": "未知转发节点", + "community_title": "社区", + "community_create": "创建社区", + "community_createDesc": "创建新社区并通过二维码分享。", + "community_join": "加入", + "community_joinTitle": "加入社区", + "community_joinConfirmation": "是否加入社区 \"{name}\"?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1413,14 +1413,14 @@ } } }, - "community_scanQr": "扫描社区二维码", - "community_scanInstructions": "将摄像头对准社区的二维码", - "community_showQr": "显示二维码", - "community_publicChannel": "社区公共频道", - "community_hashtagChannel": "社区标签频道", - "community_name": "社区名称", - "community_enterName": "请输入社区名称", - "community_created": "社区 \"{name}\" 已创建", + "community_scanQr": "扫描社区二维码", + "community_scanInstructions": "将摄像头对准社区的二维码", + "community_showQr": "显示二维码", + "community_publicChannel": "社区公共频道", + "community_hashtagChannel": "社区标签频道", + "community_name": "社区名称", + "community_enterName": "请输入社区名称", + "community_created": "社区 \"{name}\" 已创建", "@community_created": { "placeholders": { "name": { @@ -1428,7 +1428,7 @@ } } }, - "community_joined": "已加入社区 \"{name}\"", + "community_joined": "已加入社区 \"{name}\"", "@community_joined": { "placeholders": { "name": { @@ -1436,8 +1436,8 @@ } } }, - "community_qrTitle": "分享社区", - "community_qrInstructions": "扫描此二维码加入 \"{name}\"", + "community_qrTitle": "分享社区", + "community_qrInstructions": "扫描此二维码加入 \"{name}\"", "@community_qrInstructions": { "placeholders": { "name": { @@ -1445,10 +1445,10 @@ } } }, - "community_hashtagPrivacyHint": "仅社区成员可加入社区标签频道。", - "community_invalidQrCode": "无效的社区二维码", - "community_alreadyMember": "已是成员", - "community_alreadyMemberMessage": "您已是 \"{name}\" 的成员。", + "community_hashtagPrivacyHint": "仅社区成员可加入社区标签频道。", + "community_invalidQrCode": "无效的社区二维码", + "community_alreadyMember": "已是成员", + "community_alreadyMemberMessage": "您已是 \"{name}\" 的成员。", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1456,13 +1456,13 @@ } } }, - "community_addPublicChannel": "添加公共频道", - "community_addPublicChannelHint": "自动添加此社区的公共频道", - "community_noCommunities": "尚未加入任何社区。", - "community_scanOrCreate": "扫描二维码或创建社区以开始。", - "community_manageCommunities": "管理社区", - "community_delete": "退出社区", - "community_deleteConfirm": "是否退出 \"{name}\"?", + "community_addPublicChannel": "添加公共频道", + "community_addPublicChannelHint": "自动添加此社区的公共频道", + "community_noCommunities": "尚未加入任何社区。", + "community_scanOrCreate": "扫描二维码或创建社区以开始。", + "community_manageCommunities": "管理社区", + "community_delete": "退出社区", + "community_deleteConfirm": "是否退出 \"{name}\"?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1470,7 +1470,7 @@ } } }, - "community_deleteChannelsWarning": "这将同时删除 {count} 个频道及其所有消息。", + "community_deleteChannelsWarning": "这将同时删除 {count} 个频道及其所有消息。", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1478,7 +1478,7 @@ } } }, - "community_deleted": "已退出社区 \"{name}\"", + "community_deleted": "已退出社区 \"{name}\"", "@community_deleted": { "placeholders": { "name": { @@ -1486,8 +1486,8 @@ } } }, - "community_regenerateSecret": "重新生成密钥", - "community_regenerateSecretConfirm": "是否为 \"{name}\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。", + "community_regenerateSecret": "重新生成密钥", + "community_regenerateSecretConfirm": "是否为 \"{name}\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1495,8 +1495,8 @@ } } }, - "community_regenerate": "重新生成", - "community_secretRegenerated": "已为 \"{name}\" 重新生成密钥", + "community_regenerate": "重新生成", + "community_secretRegenerated": "已为 \"{name}\" 重新生成密钥", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1504,8 +1504,8 @@ } } }, - "community_updateSecret": "更新密钥", - "community_secretUpdated": "“{name}”的密钥已更新", + "community_updateSecret": "更新密钥", + "community_secretUpdated": "“{name}”的密钥已更新", "@community_secretUpdated": { "placeholders": { "name": { @@ -1513,7 +1513,7 @@ } } }, - "community_scanToUpdateSecret": "扫描新二维码以更新 \"{name}\" 的密钥", + "community_scanToUpdateSecret": "扫描新二维码以更新 \"{name}\" 的密钥", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1521,14 +1521,14 @@ } } }, - "community_addHashtagChannel": "添加标签频道", - "community_addHashtagChannelDesc": "为此社区创建标签频道", - "community_selectCommunity": "选择社区", - "community_regularHashtag": "普通标签", - "community_regularHashtagDesc": "公共标签频道(任何人都可参与)", - "community_communityHashtag": "社区标签", - "community_communityHashtagDesc": "仅限社区成员", - "community_forCommunity": "为 {name}", + "community_addHashtagChannel": "添加标签频道", + "community_addHashtagChannelDesc": "为此社区创建标签频道", + "community_selectCommunity": "选择社区", + "community_regularHashtag": "普通标签", + "community_regularHashtagDesc": "公共标签频道(任何人都可参与)", + "community_communityHashtag": "社区标签", + "community_communityHashtagDesc": "仅限社区成员", + "community_forCommunity": "为 {name}", "@community_forCommunity": { "placeholders": { "name": { @@ -1536,30 +1536,30 @@ } } }, - "listFilter_tooltip": "筛选与排序", - "listFilter_sortBy": "排序方式", - "listFilter_latestMessages": "最新消息", - "listFilter_heardRecently": "最近听到", + "listFilter_tooltip": "筛选与排序", + "listFilter_sortBy": "排序方式", + "listFilter_latestMessages": "最新消息", + "listFilter_heardRecently": "最近听到", "listFilter_az": "A-Z", - "listFilter_filters": "筛选", - "listFilter_all": "全部", - "listFilter_users": "用户", - "listFilter_repeaters": "转发节点", - "listFilter_roomServers": "房间服务器", - "listFilter_unreadOnly": "仅显示未读", - "listFilter_newGroup": "新建群聊", - "pathTrace_you": "我自己", - "pathTrace_failed": "路径追踪失败。", - "pathTrace_notAvailable": "无法获取路径信息。", - "pathTrace_refreshTooltip": "刷新路径追踪", - "contacts_pathTrace": "路径追踪", + "listFilter_filters": "筛选", + "listFilter_all": "全部", + "listFilter_users": "用户", + "listFilter_repeaters": "转发节点", + "listFilter_roomServers": "房间服务器", + "listFilter_unreadOnly": "仅显示未读", + "listFilter_newGroup": "新建群聊", + "pathTrace_you": "我自己", + "pathTrace_failed": "路径追踪失败。", + "pathTrace_notAvailable": "无法获取路径信息。", + "pathTrace_refreshTooltip": "刷新路径追踪", + "contacts_pathTrace": "路径追踪", "contacts_ping": "Ping", - "contacts_repeaterPathTrace": "Trace 转发节点", - "contacts_repeaterPing": "Ping 转发节点", - "contacts_roomPathTrace": "Trace 房间服务器", - "contacts_roomPing": "Ping 房间服务器", - "contacts_chatTraceRoute": "路由追踪", - "contacts_pathTraceTo": "追踪至 {name} 的路径", + "contacts_repeaterPathTrace": "Trace 转发节点", + "contacts_repeaterPing": "Ping 转发节点", + "contacts_roomPathTrace": "Trace 房间服务器", + "contacts_roomPing": "Ping 房间服务器", + "contacts_chatTraceRoute": "路由追踪", + "contacts_pathTraceTo": "追踪至 {name} 的路径", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1567,66 +1567,66 @@ } } }, - "contacts_clipboardEmpty": "剪贴板为空", - "contacts_invalidAdvertFormat": "无效的联系人信息格式", - "contacts_contactImported": "联系人已导入", - "contacts_contactImportFailed": "导入联系人失败。", - "contacts_zeroHopAdvert": "发送零跳广播", - "contacts_floodAdvert": "发送泛洪广播", - "contacts_copyAdvertToClipboard": "复制广播到剪贴板", - "contacts_addContactFromClipboard": "从剪贴板添加联系人", - "contacts_ShareContact": "复制联系人信息到剪贴板", - "contacts_ShareContactZeroHop": "通过广播分享联系人", - "contacts_zeroHopContactAdvertSent": "零跳广播已发送", - "contacts_zeroHopContactAdvertFailed": "发送联系人广播失败。", - "contacts_contactAdvertCopied": "广播已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "复制广播到剪贴板失败。", - "notification_activityTitle": "MeshCore 活动", - "notification_messagesCount": "{count} 条消息", - "notification_channelMessagesCount": "{count} 条频道消息", - "notification_newNodesCount": "{count} 个新节点", - "notification_newTypeDiscovered": "发现新 {contactType}", - "notification_receivedNewMessage": "收到新消息", - "settings_gpxExportRepeaters": "导出转发节点/房间服务器到 GPX", - "settings_gpxExportRepeatersSubtitle": "导出带位置的转发节点/房间服务器到 GPX 文件", - "settings_gpxExportContactsSubtitle": "导出带位置的伙伴到 GPX 文件", - "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", - "settings_gpxExportSuccess": "GPX 文件导出成功", - "settings_gpxExportError": "导出时出错", - "settings_gpxExportRepeatersRoom": "转发节点与房间服务器位置", - "settings_gpxExportChat": "伙伴位置", - "settings_gpxExportAll": "导出所有联系人到 GPX", - "settings_gpxExportContacts": "导出伙伴到 GPX", - "settings_gpxExportAllSubtitle": "导出所有带位置的联系人到 GPX 文件", - "settings_gpxExportAllContacts": "所有联系人位置", - "settings_gpxExportNoContacts": "没有可导出的联系人", - "settings_gpxExportShareText": "来自 MeshCore Open 的地图数据导出", - "settings_gpxExportShareSubject": "MeshCore Open GPX 地图数据导出", - "pathTrace_someHopsNoLocation": "某些跳缺少位置信息!", - "map_tapToAdd": "点击节点以添加到路径", - "pathTrace_clearTooltip": "清除路径", - "map_pathTraceCancelled": "路径追踪已取消", - "map_removeLast": "移除最后一个", - "map_runTrace": "运行路径追踪", - "scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备", - "scanner_chromeRequired": "需要 Chrome 浏览器", - "scanner_chromeRequiredMessage": "æ­¤ Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。", - "scanner_bluetoothOff": "蓝牙已关闭", - "scanner_enableBluetooth": "启用蓝牙", - "snrIndicator_lastSeen": "最近访问", - "snrIndicator_nearByRepeaters": "附近的重复器", - "chat_ShowAllPaths": "显示所有路径", - "settings_clientRepeat": "离网重复", - "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", - "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。", - "settings_aboutOpenMeteoAttribution": "LOS 高程数据:Open-Meteo (CC BY 4.0)", - "appSettings_unitsTitle": "单位", - "appSettings_unitsMetric": "公制(米/公里)", - "appSettings_unitsImperial": "英制 (ft / mi)", - "map_lineOfSight": "视线", - "map_losScreenTitle": "视线", - "losSelectStartEnd": "选择 LOS 的起始节点和结束节点。", - "losRunFailed": "视线检查失败:{error}", + "contacts_clipboardEmpty": "剪贴板为空", + "contacts_invalidAdvertFormat": "无效的联系人信息格式", + "contacts_contactImported": "联系人已导入", + "contacts_contactImportFailed": "导入联系人失败。", + "contacts_zeroHopAdvert": "发送零跳广播", + "contacts_floodAdvert": "发送泛洪广播", + "contacts_copyAdvertToClipboard": "复制广播到剪贴板", + "contacts_addContactFromClipboard": "从剪贴板添加联系人", + "contacts_ShareContact": "复制联系人信息到剪贴板", + "contacts_ShareContactZeroHop": "通过广播分享联系人", + "contacts_zeroHopContactAdvertSent": "零跳广播已发送", + "contacts_zeroHopContactAdvertFailed": "发送联系人广播失败。", + "contacts_contactAdvertCopied": "广播已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "复制广播到剪贴板失败。", + "notification_activityTitle": "MeshCore 活动", + "notification_messagesCount": "{count} 条消息", + "notification_channelMessagesCount": "{count} 条频道消息", + "notification_newNodesCount": "{count} 个新节点", + "notification_newTypeDiscovered": "发现新 {contactType}", + "notification_receivedNewMessage": "收到新消息", + "settings_gpxExportRepeaters": "导出转发节点/房间服务器到 GPX", + "settings_gpxExportRepeatersSubtitle": "导出带位置的转发节点/房间服务器到 GPX 文件", + "settings_gpxExportContactsSubtitle": "导出带位置的伙伴到 GPX 文件", + "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", + "settings_gpxExportSuccess": "GPX 文件导出成功", + "settings_gpxExportError": "导出时出错", + "settings_gpxExportRepeatersRoom": "转发节点与房间服务器位置", + "settings_gpxExportChat": "伙伴位置", + "settings_gpxExportAll": "导出所有联系人到 GPX", + "settings_gpxExportContacts": "导出伙伴到 GPX", + "settings_gpxExportAllSubtitle": "导出所有带位置的联系人到 GPX 文件", + "settings_gpxExportAllContacts": "所有联系人位置", + "settings_gpxExportNoContacts": "没有可导出的联系人", + "settings_gpxExportShareText": "来自 MeshCore Open 的地图数据导出", + "settings_gpxExportShareSubject": "MeshCore Open GPX 地图数据导出", + "pathTrace_someHopsNoLocation": "某些跳缺少位置信息!", + "map_tapToAdd": "点击节点以添加到路径", + "pathTrace_clearTooltip": "清除路径", + "map_pathTraceCancelled": "路径追踪已取消", + "map_removeLast": "移除最后一个", + "map_runTrace": "运行路径追踪", + "scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备", + "scanner_chromeRequired": "需要 Chrome 浏览器", + "scanner_chromeRequiredMessage": "此 Web 应用程序需要 Google Chrome 或基于 Chromium 的浏览器以支持蓝牙。", + "scanner_bluetoothOff": "蓝牙已关闭", + "scanner_enableBluetooth": "启用蓝牙", + "snrIndicator_lastSeen": "最近访问", + "snrIndicator_nearByRepeaters": "附近的重复器", + "chat_ShowAllPaths": "显示所有路径", + "settings_clientRepeat": "离网重复", + "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", + "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。", + "settings_aboutOpenMeteoAttribution": "LOS 高程数据:Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "单位", + "appSettings_unitsMetric": "公制(米/公里)", + "appSettings_unitsImperial": "英制 (ft / mi)", + "map_lineOfSight": "视线", + "map_losScreenTitle": "视线", + "losSelectStartEnd": "选择 LOS 的起始节点和结束节点。", + "losRunFailed": "视线检查失败:{error}", "@losRunFailed": { "placeholders": { "error": { @@ -1634,13 +1634,13 @@ } } }, - "losClearAllPoints": "清除所有点", - "losRunToViewElevationProfile": "运行 LOS 查看高程剖面", - "losMenuTitle": "服务水平菜单", - "losMenuSubtitle": "点击节点或长按地图以获取自定义点", - "losShowDisplayNodes": "显示显示节点", - "losCustomPoints": "自定义积分", - "losCustomPointLabel": "自定义 {index}", + "losClearAllPoints": "清除所有点", + "losRunToViewElevationProfile": "运行 LOS 查看高程剖面", + "losMenuTitle": "服务水平菜单", + "losMenuSubtitle": "点击节点或长按地图以获取自定义点", + "losShowDisplayNodes": "显示显示节点", + "losCustomPoints": "自定义积分", + "losCustomPointLabel": "自定义 {index}", "@losCustomPointLabel": { "placeholders": { "index": { @@ -1648,9 +1648,9 @@ } } }, - "losPointA": "A点", - "losPointB": "B点", - "losAntennaA": "天线 A: {value} {unit}", + "losPointA": "A点", + "losPointB": "B点", + "losAntennaA": "天线 A: {value} {unit}", "@losAntennaA": { "placeholders": { "value": { @@ -1661,7 +1661,7 @@ } } }, - "losAntennaB": "天线 B:{value} {unit}", + "losAntennaB": "天线 B:{value} {unit}", "@losAntennaB": { "placeholders": { "value": { @@ -1672,9 +1672,9 @@ } } }, - "losRun": "运行视距", - "losNoElevationData": "无海拔数据", - "losProfileClear": "{distance} {distanceUnit},清除 LOS,最小间隙 {clearance} {heightUnit}", + "losRun": "运行视距", + "losNoElevationData": "无海拔数据", + "losProfileClear": "{distance} {distanceUnit},清除 LOS,最小间隙 {clearance} {heightUnit}", "@losProfileClear": { "placeholders": { "distance": { @@ -1691,7 +1691,7 @@ } } }, - "losProfileBlocked": "{distance} {distanceUnit},被 {obstruction} {heightUnit} 阻止", + "losProfileBlocked": "{distance} {distanceUnit},被 {obstruction} {heightUnit} 阻止", "@losProfileBlocked": { "placeholders": { "distance": { @@ -1708,9 +1708,9 @@ } } }, - "losStatusChecking": "洛斯:正在检查...", - "losStatusNoData": "LOS:无数据", - "losStatusSummary": "LOS:{clear}/{total} 清除,{blocked} 阻塞,{unknown} 未知", + "losStatusChecking": "洛斯:正在检查...", + "losStatusNoData": "LOS:无数据", + "losStatusSummary": "LOS:{clear}/{total} 清除,{blocked} 阻塞,{unknown} 未知", "@losStatusSummary": { "placeholders": { "clear": { @@ -1727,20 +1727,20 @@ } } }, - "losErrorElevationUnavailable": "一个或多个样本的海拔数据不可用。", - "losErrorInvalidInput": "用于 LOS 计算的点/高程数据无效。", - "losRenameCustomPoint": "重命名自定义点", - "losPointName": "点名称", - "losShowPanelTooltip": "显示 LOS 面板", - "losHidePanelTooltip": "隐藏 LOS 面板", - "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", - "losLegendRadioHorizon": "无线电地平线", - "losLegendLosBeam": "视距波束", - "losLegendTerrain": "地形", - "losFrequencyLabel": "频率", - "losFrequencyInfoTooltip": "查看计算详情", - "losFrequencyDialogTitle": "无线电地平线计算", - "losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", + "losErrorElevationUnavailable": "一个或多个样本的海拔数据不可用。", + "losErrorInvalidInput": "用于 LOS 计算的点/高程数据无效。", + "losRenameCustomPoint": "重命名自定义点", + "losPointName": "点名称", + "losShowPanelTooltip": "显示 LOS 面板", + "losHidePanelTooltip": "隐藏 LOS 面板", + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "无线电地平线", + "losLegendLosBeam": "视距波束", + "losLegendTerrain": "地形", + "losFrequencyLabel": "频率", + "losFrequencyInfoTooltip": "查看计算详情", + "losFrequencyDialogTitle": "无线电地平线计算", + "losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1758,9 +1758,9 @@ } } }, - "listFilter_favorites": "收藏", - "listFilter_addToFavorites": "添加到收藏", - "listFilter_removeFromFavorites": "从收藏中移除", + "listFilter_favorites": "收藏", + "listFilter_addToFavorites": "添加到收藏", + "listFilter_removeFromFavorites": "从收藏中移除", "@contacts_searchFavorites": { "placeholders": { "number": { @@ -1801,17 +1801,29 @@ } } }, - "contacts_searchUsers": "搜索 {number}{str} 位用户...", - "contacts_unread": "未读", - "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", - "contacts_searchContactsNoNumber": "搜索联系人...", - "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", - "contacts_searchFavorites": "搜索 {number}{str} 收藏...", - "connectionChoiceBluetoothLabel": "蓝牙", - "connectionChoiceUsbLabel": "USB", - "usbScreenTitle": "通过USB连接", - "usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。", - "usbScreenStatus": "选择一个 USB 设备", - "usbScreenNote": "在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。", - "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。" + "contacts_searchUsers": "搜索 {number}{str} 位用户...", + "contacts_unread": "未读", + "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", + "contacts_searchContactsNoNumber": "搜索联系人...", + "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", + "contacts_searchFavorites": "搜索 {number}{str} 收藏...", + "usbScreenTitle": "通过USB连接", + "usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。", + "usbScreenNote": "在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。", + "usbScreenStatus": "选择一个 USB 设备", + "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。", + "usbErrorPermissionDenied": "拒绝了USB权限。", + "usbErrorDeviceMissing": "所选的USB设备已不再可用。", + "usbErrorInvalidPort": "选择一个有效的USB设备。", + "usbErrorBusy": "还有一个 USB 连接请求正在进行中。", + "usbErrorNotConnected": "没有连接任何USB设备。", + "usbErrorOpenFailed": "未能打开所选的USB设备。", + "usbErrorConnectFailed": "未能连接到所选的USB设备。", + "usbErrorUnsupported": "此平台不支持USB串行通信。", + "usbErrorAlreadyActive": "USB 连接已建立。", + "usbErrorNoDeviceSelected": "未选择任何 USB 设备。", + "usbErrorPortClosed": "USB 连接未建立。", + "usbErrorConnectTimedOut": "等待设备响应超时。", + "connectionChoiceBluetoothLabel": "蓝牙", + "connectionChoiceUsbLabel": "USB" } diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index c703a64..8ee739b 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -481,7 +482,7 @@ class _UsbScreenState extends State { setState(() { _ports.clear(); _selectedPort = null; - _errorText = error.toString(); + _errorText = _friendlyErrorMessage(error); _isLoadingPorts = false; }); } @@ -523,16 +524,56 @@ class _UsbScreenState extends State { } } - /// Strips the Dart runtime prefix (e.g. "Bad state: ", "Exception: ") - /// from error messages before showing them in the UI. String _friendlyErrorMessage(Object error) { + final l10n = context.l10n; + if (error is PlatformException) { + switch (error.code) { + case 'usb_permission_denied': + return l10n.usbErrorPermissionDenied; + case 'usb_device_missing': + case 'usb_device_detached': + return l10n.usbErrorDeviceMissing; + case 'usb_invalid_port': + return l10n.usbErrorInvalidPort; + case 'usb_busy': + return l10n.usbErrorBusy; + case 'usb_not_connected': + return l10n.usbErrorNotConnected; + case 'usb_driver_missing': + case 'usb_open_failed': + return l10n.usbErrorOpenFailed; + case 'usb_connect_failed': + case 'usb_write_failed': + case 'usb_io_error': + return l10n.usbErrorConnectFailed; + } + } + var msg = error.toString(); - // StateError surfaces as "Bad state: " if (msg.startsWith('Bad state: ')) { msg = msg.substring('Bad state: '.length); } else if (msg.startsWith('Exception: ')) { msg = msg.substring('Exception: '.length); } + + switch (msg) { + case 'USB serial transport is already active': + return l10n.usbErrorAlreadyActive; + case 'No USB serial device selected': + return l10n.usbErrorNoDeviceSelected; + case 'USB serial port is not open': + return l10n.usbErrorPortClosed; + case 'USB serial is not supported on this platform.': + case 'Web Serial is not supported by this browser.': + return l10n.usbErrorUnsupported; + case 'Timed out waiting for SELF_INFO during connect': + return l10n.usbErrorConnectTimedOut; + } + + if (msg.startsWith('Failed to open USB port ')) { + return l10n.usbErrorOpenFailed; + } + return msg; } diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index ac73673..66de3ab 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -140,7 +140,7 @@ class UsbSerialService { 'Android connect failed: $msg', tag: 'USB Serial', ); - throw StateError(msg); + rethrow; } } else { // ── Hot-restart guard ───────────────────────────────────────────────── diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 0cf57c5..a932a57 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -105,7 +105,7 @@ void main() { ); testWidgets( - 'UsbScreen keeps raw selection while showing connector USB display label', + 'UsbScreen keeps raw selection when connector USB display label changes', (tester) async { final connector = _FakeMeshCoreConnector( ports: ['COM6 - USB Serial Device (COM6)'], @@ -121,8 +121,6 @@ void main() { connector.notifyListeners(); await tester.pump(); - expect(find.text('KD3CGK mesh-utility.org'), findsOneWidget); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); await tester.pump(); From 21ff765e41c796ce2413fce07e788d9078f48c91 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:08:05 -0500 Subject: [PATCH 261/421] Refactor USB permission handling and reset initial channel sync flag --- .../meshcore/meshcore_open/MeshcoreUsbFunctions.kt | 14 +++++++------- lib/connector/meshcore_connector.dart | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt index d6c09b7..b8ee645 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt @@ -82,13 +82,6 @@ class MeshcoreUsbFunctions( return } - val granted = - intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) - if (!granted) { - result.error("usb_permission_denied", "USB permission denied", null) - return - } - val device = findUsbDevice(portName) if (device == null) { result.error( @@ -99,6 +92,13 @@ class MeshcoreUsbFunctions( return } + val granted = + intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) + if (!granted || !usbManager.hasPermission(device)) { + result.error("usb_permission_denied", "USB permission denied", null) + return + } + openUsbDevice(device, pendingConnectBaudRate, result) } } diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2bdc033..606b870 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1169,7 +1169,6 @@ class MeshCoreConnector extends ChangeNotifier { _pendingInitialContactsSync = false; _bleInitialSyncStarted = false; _pendingDeferredChannelSyncAfterContacts = false; - _webInitialHandshakeRequestSent = false; } bool get _shouldAutoReconnect => @@ -2241,6 +2240,7 @@ class MeshCoreConnector extends ChangeNotifier { (_activeTransport == MeshCoreTransportType.bluetooth || _activeTransport == MeshCoreTransportType.usb)) { _pendingDeferredChannelSyncAfterContacts = false; + _pendingInitialChannelSync = false; unawaited(getChannels()); } break; From 367e47bb1ece1850469986182345fd5ae744796f Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:38:10 -0500 Subject: [PATCH 262/421] Fix USB device name matching and correct localization strings --- .../com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt | 9 ++++++++- lib/l10n/app_en.arb | 8 ++++---- lib/l10n/app_localizations.dart | 8 ++++---- lib/l10n/app_localizations_en.dart | 8 ++++---- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt index b8ee645..52e7650 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt @@ -235,7 +235,14 @@ class MeshcoreUsbFunctions( } private fun findUsbDevice(portName: String): UsbDevice? { - return usbManager.deviceList.values.firstOrNull { it.deviceName == portName } + val devices = usbManager.deviceList.values + val exactMatch = devices.firstOrNull { it.deviceName == portName } + if (exactMatch != null) { + return exactMatch + } + + val normalizedName = portName.substringBefore(" - ").trim() + return devices.firstOrNull { it.deviceName == normalizedName } } private fun openUsbDevice( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cbb0d0d..315c88c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -28,7 +28,7 @@ "common_disable": "Disable", "common_reboot": "Reboot", "common_loading": "Loading...", - "common_notAvailable": "—", + "common_notAvailable": "—", "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { @@ -1351,7 +1351,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1401,7 +1401,7 @@ "channelPath_repeatsLabel": "Repeats", "channelPath_pathLabel": "Path {index}", "channelPath_observedLabel": "Observed", - "channelPath_observedPathTitle": "Observed path {index} • {hops}", + "channelPath_observedPathTitle": "Observed path {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1476,7 +1476,7 @@ }, "channelPath_pathLabelTitle": "Path", "channelPath_observedPathHeader": "Observed Path", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index aeec38b..f29ef1d 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -295,7 +295,7 @@ abstract class AppLocalizations { /// No description provided for @common_notAvailable. /// /// In en, this message translates to: - /// **'—'** + /// **'—'** String get common_notAvailable; /// No description provided for @common_voltageValue. @@ -4409,7 +4409,7 @@ abstract class AppLocalizations { /// No description provided for @telemetry_temperatureValue. /// /// In en, this message translates to: - /// **'{celsius}°C / {fahrenheit}°F'** + /// **'{celsius}°C / {fahrenheit}°F'** String telemetry_temperatureValue(String celsius, String fahrenheit); /// No description provided for @neighbors_receivedData. @@ -4523,7 +4523,7 @@ abstract class AppLocalizations { /// No description provided for @channelPath_observedPathTitle. /// /// In en, this message translates to: - /// **'Observed path {index} • {hops}'** + /// **'Observed path {index} • {hops}'** String channelPath_observedPathTitle(int index, String hops); /// No description provided for @channelPath_noLocationData. @@ -4607,7 +4607,7 @@ abstract class AppLocalizations { /// No description provided for @channelPath_selectedPathLabel. /// /// In en, this message translates to: - /// **'{label} • {prefixes}'** + /// **'{label} • {prefixes}'** String channelPath_selectedPathLabel(String label, String prefixes); /// No description provided for @channelPath_noHopDetailsAvailable. diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0e4e55d..0ecff06 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -93,7 +93,7 @@ class AppLocalizationsEn extends AppLocalizations { String get common_loading => 'Loading...'; @override - String get common_notAvailable => '—'; + String get common_notAvailable => '—'; @override String common_voltageValue(String volts) { @@ -2465,7 +2465,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2533,7 +2533,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Observed path $index • $hops'; + return 'Observed path $index • $hops'; } @override @@ -2588,7 +2588,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override From 524558c511ae84844f84f604978ec5ce687e19ba Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:44:13 -0500 Subject: [PATCH 263/421] clean --- lib/connector/meshcore_connector.dart | 10 ++++-- lib/l10n/app_bg.arb | 2 +- lib/l10n/app_en.arb | 24 +++++++------- lib/l10n/app_localizations.dart | 24 +++++++------- lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_en.dart | 24 +++++++------- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_sv.arb | 2 +- lib/screens/scanner_screen.dart | 15 +++++---- lib/screens/usb_screen.dart | 35 ++++++++++++--------- lib/services/usb_serial_service_native.dart | 11 +------ 11 files changed, 79 insertions(+), 72 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 606b870..95bce3b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -748,7 +748,10 @@ class MeshCoreConnector extends ChangeNotifier { try { await FlutterBluePlus.stopScan(); } catch (e) { - debugPrint('[FBP] stopScan error in startScan (ignored): $e'); + _appDebugLogService?.warn( + 'stopScan error in startScan (ignored): $e', + tag: 'BLE Scan', + ); } } await _scanSubscription?.cancel(); @@ -808,7 +811,10 @@ class MeshCoreConnector extends ChangeNotifier { try { await FlutterBluePlus.stopScan(); } catch (e) { - debugPrint('[FBP] stopScan error (ignored): $e'); + _appDebugLogService?.warn( + 'stopScan error (ignored): $e', + tag: 'BLE Scan', + ); } } await _scanSubscription?.cancel(); diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index f71dfdd..379dd47 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1809,7 +1809,7 @@ "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.", "usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.", "usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.", - "usbErrorInvalidPort": "Изберете валитно USB устройство.", + "usbErrorInvalidPort": "Изберете валидно USB устройство.", "usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.", "usbErrorNotConnected": "Няма свързано USB устройство.", "usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 315c88c..87aa1e1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -190,20 +190,20 @@ "appSettings_language": "Language", "appSettings_languageSystem": "System default", "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", + "appSettings_languageFr": "Français", + "appSettings_languageEs": "Español", "appSettings_languageDe": "Deutsch", "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", + "appSettings_languageSl": "Slovenščina", + "appSettings_languagePt": "Português", "appSettings_languageIt": "Italiano", - "appSettings_languageZh": "中文", + "appSettings_languageZh": "中文", "appSettings_languageSv": "Svenska", "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", - "appSettings_languageRu": "Русский", - "appSettings_languageUk": "Українська", + "appSettings_languageSk": "Slovenčina", + "appSettings_languageBg": "Български", + "appSettings_languageRu": "Русский", + "appSettings_languageUk": "Українська", "appSettings_enableMessageTracing": "Enable Message Tracing", "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_notifications": "Notifications", @@ -1351,7 +1351,7 @@ } } }, - "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", + "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { "celsius": { @@ -1401,7 +1401,7 @@ "channelPath_repeatsLabel": "Repeats", "channelPath_pathLabel": "Path {index}", "channelPath_observedLabel": "Observed", - "channelPath_observedPathTitle": "Observed path {index} • {hops}", + "channelPath_observedPathTitle": "Observed path {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1476,7 +1476,7 @@ }, "channelPath_pathLabelTitle": "Path", "channelPath_observedPathHeader": "Observed Path", - "channelPath_selectedPathLabel": "{label} • {prefixes}", + "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { "label": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index f29ef1d..a679063 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1015,13 +1015,13 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageFr. /// /// In en, this message translates to: - /// **'Français'** + /// **'Français'** String get appSettings_languageFr; /// No description provided for @appSettings_languageEs. /// /// In en, this message translates to: - /// **'Español'** + /// **'Español'** String get appSettings_languageEs; /// No description provided for @appSettings_languageDe. @@ -1039,13 +1039,13 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageSl. /// /// In en, this message translates to: - /// **'Slovenščina'** + /// **'Slovenščina'** String get appSettings_languageSl; /// No description provided for @appSettings_languagePt. /// /// In en, this message translates to: - /// **'Português'** + /// **'Português'** String get appSettings_languagePt; /// No description provided for @appSettings_languageIt. @@ -1057,7 +1057,7 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageZh. /// /// In en, this message translates to: - /// **'中文'** + /// **'中文'** String get appSettings_languageZh; /// No description provided for @appSettings_languageSv. @@ -1075,25 +1075,25 @@ abstract class AppLocalizations { /// No description provided for @appSettings_languageSk. /// /// In en, this message translates to: - /// **'Slovenčina'** + /// **'Slovenčina'** String get appSettings_languageSk; /// No description provided for @appSettings_languageBg. /// /// In en, this message translates to: - /// **'Български'** + /// **'Български'** String get appSettings_languageBg; /// No description provided for @appSettings_languageRu. /// /// In en, this message translates to: - /// **'Русский'** + /// **'Русский'** String get appSettings_languageRu; /// No description provided for @appSettings_languageUk. /// /// In en, this message translates to: - /// **'Українська'** + /// **'Українська'** String get appSettings_languageUk; /// No description provided for @appSettings_enableMessageTracing. @@ -4409,7 +4409,7 @@ abstract class AppLocalizations { /// No description provided for @telemetry_temperatureValue. /// /// In en, this message translates to: - /// **'{celsius}°C / {fahrenheit}°F'** + /// **'{celsius}°C / {fahrenheit}°F'** String telemetry_temperatureValue(String celsius, String fahrenheit); /// No description provided for @neighbors_receivedData. @@ -4523,7 +4523,7 @@ abstract class AppLocalizations { /// No description provided for @channelPath_observedPathTitle. /// /// In en, this message translates to: - /// **'Observed path {index} • {hops}'** + /// **'Observed path {index} • {hops}'** String channelPath_observedPathTitle(int index, String hops); /// No description provided for @channelPath_noLocationData. @@ -4607,7 +4607,7 @@ abstract class AppLocalizations { /// No description provided for @channelPath_selectedPathLabel. /// /// In en, this message translates to: - /// **'{label} • {prefixes}'** + /// **'{label} • {prefixes}'** String channelPath_selectedPathLabel(String label, String prefixes); /// No description provided for @channelPath_noHopDetailsAvailable. diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 39f827b..b964966 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -140,7 +140,7 @@ class AppLocalizationsBg extends AppLocalizations { 'Избраното USB устройство вече не е налично.'; @override - String get usbErrorInvalidPort => 'Изберете валитно USB устройство.'; + String get usbErrorInvalidPort => 'Изберете валидно USB устройство.'; @override String get usbErrorBusy => diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0ecff06..05ed0b9 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -489,10 +489,10 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_languageEn => 'English'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => 'Français'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => 'Español'; @override String get appSettings_languageDe => 'Deutsch'; @@ -501,16 +501,16 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_languagePl => 'Polski'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => 'Slovenščina'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => 'Português'; @override String get appSettings_languageIt => 'Italiano'; @override - String get appSettings_languageZh => '中文'; + String get appSettings_languageZh => '中文'; @override String get appSettings_languageSv => 'Svenska'; @@ -519,16 +519,16 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_languageNl => 'Nederlands'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => 'Slovenčina'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Русский'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Українська'; @override String get appSettings_enableMessageTracing => 'Enable Message Tracing'; @@ -2465,7 +2465,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String telemetry_temperatureValue(String celsius, String fahrenheit) { - return '$celsius°C / $fahrenheit°F'; + return '$celsius°C / $fahrenheit°F'; } @override @@ -2533,7 +2533,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String channelPath_observedPathTitle(int index, String hops) { - return 'Observed path $index • $hops'; + return 'Observed path $index • $hops'; } @override @@ -2588,7 +2588,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String channelPath_selectedPathLabel(String label, String prefixes) { - return '$label • $prefixes'; + return '$label • $prefixes'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index c68faf5..e36fc2a 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -126,7 +126,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbScreenNote => - 'USB-seriell kommunikation är aktiv på stöderdade Android-enheter och skrivbordsplattformar.'; + 'USB-seriell kommunikation är aktiv på stödda Android-enheter och skrivbordsplattformar.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 025294a..836bef0 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1802,7 +1802,7 @@ "contacts_searchFavorites": "Sök {number}{str} Favoriter...", "contacts_searchUsers": "Sök {number}{str} användare...", "contacts_searchRoomServers": "Sök {number}{str} Room-servrar...", - "usbScreenNote": "USB-seriell kommunikation är aktiv på stöderdade Android-enheter och skrivbordsplattformar.", + "usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och skrivbordsplattformar.", "usbScreenStatus": "Välj en USB-enhet", "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", "usbScreenTitle": "Anslut via USB", diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index f7966e9..5d7695c 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 '../utils/app_logger.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; import 'contacts_screen.dart'; @@ -60,7 +61,7 @@ class _ScannerScreenState extends State { } }, onError: (Object e) { - debugPrint("Scanner adapterState stream error: $e"); + appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen'); }, ); } @@ -86,7 +87,7 @@ class _ScannerScreenState extends State { ? IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - debugPrint('ScannerScreen: back button pressed'); + appLogger.info('Back button pressed', tag: 'ScannerScreen'); Navigator.of(context).maybePop(); }, ) @@ -131,8 +132,9 @@ class _ScannerScreenState extends State { if (usbSupported) FloatingActionButton.extended( onPressed: () { - debugPrint( - 'ScannerScreen: USB selected, opening UsbScreen', + appLogger.info( + 'USB selected, opening UsbScreen', + tag: 'ScannerScreen', ); Navigator.of(context).push( MaterialPageRoute(builder: (_) => const UsbScreen()), @@ -153,8 +155,9 @@ class _ScannerScreenState extends State { } else { unawaited( connector.startScan().catchError((e) { - debugPrint( - "Scanner screen startScan error: $e", + appLogger.warn( + 'startScan error: $e', + tag: 'ScannerScreen', ); }), ); diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 8ee739b..5c4dfb1 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector_usb.dart'; import '../l10n/l10n.dart'; +import '../utils/app_logger.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; import 'contacts_screen.dart'; @@ -109,7 +110,7 @@ class _UsbScreenState extends State { leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { - debugPrint('UsbScreen: back button pressed'); + appLogger.info('Back button pressed', tag: 'UsbScreen'); Navigator.of(context).maybePop(); }, ), @@ -124,8 +125,9 @@ class _UsbScreenState extends State { PlatformInfo.isIOS) TextButton.icon( onPressed: () { - debugPrint( - 'UsbScreen: Bluetooth selected, opening ScannerScreen', + appLogger.info( + 'Bluetooth selected, opening ScannerScreen', + tag: 'UsbScreen', ); Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const ScannerScreen()), @@ -218,8 +220,9 @@ class _UsbScreenState extends State { onPressed: _isLoadingPorts || _isConnecting ? null : () { - debugPrint( - 'UsbScreen: refresh ports pressed', + appLogger.info( + 'Refresh ports pressed', + tag: 'UsbScreen', ); _loadPorts(); }, @@ -234,8 +237,9 @@ class _UsbScreenState extends State { final rawPortName = normalizeUsbPortName( _selectedPort!, ); - debugPrint( - 'UsbScreen: connect pressed for $_selectedPort (raw: $rawPortName)', + appLogger.info( + 'Connect pressed for $_selectedPort (raw: $rawPortName)', + tag: 'UsbScreen', ); _connectSelectedPort(); } @@ -262,8 +266,9 @@ class _UsbScreenState extends State { onPressed: _isLoadingPorts || _isConnecting ? null : () { - debugPrint( - 'UsbScreen: refresh ports pressed', + appLogger.info( + 'Refresh ports pressed', + tag: 'UsbScreen', ); _loadPorts(); }, @@ -280,8 +285,9 @@ class _UsbScreenState extends State { final rawPortName = normalizeUsbPortName( _selectedPort!, ); - debugPrint( - 'UsbScreen: connect pressed for $_selectedPort (raw: $rawPortName)', + appLogger.info( + 'Connect pressed for $_selectedPort (raw: $rawPortName)', + tag: 'UsbScreen', ); _connectSelectedPort(); } @@ -413,7 +419,7 @@ class _UsbScreenState extends State { _selectedPort = port; _errorText = null; }); - debugPrint('UsbScreen: selected port $port'); + appLogger.info('Selected port $port', tag: 'UsbScreen'); }, leading: Icon( Icons.usb, @@ -511,8 +517,9 @@ class _UsbScreenState extends State { try { await _usbConnector.connect(portName: rawPortName); } catch (error, stackTrace) { - debugPrint( - 'UsbScreen: connect failed for $rawPortName: $error\n$stackTrace', + appLogger.error( + 'Connect failed for $rawPortName: $error\n$stackTrace', + tag: 'UsbScreen', ); if (!mounted) return; setState(() { diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 66de3ab..f758a1f 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -135,7 +135,6 @@ class UsbSerialService { } on PlatformException catch (error) { _status = UsbSerialStatus.disconnected; final msg = error.message ?? error.code; - debugPrint('[USB Serial] Android connect failed: $msg'); _debugLogService?.error( 'Android connect failed: $msg', tag: 'USB Serial', @@ -180,7 +179,6 @@ class UsbSerialService { if (openStatus != FlOpenStatus.open) { final msg = 'Failed to open USB port $candidate (status: $openStatus)'; - debugPrint('[USB Serial] $msg'); _debugLogService?.error(msg, tag: 'USB Serial'); // Not a FlSerialException — treat as terminal failure _status = UsbSerialStatus.disconnected; @@ -204,9 +202,6 @@ class UsbSerialService { } on FlSerialException catch (error) { // The native fl_open() already called fl_close() on failure // internally, so no extra cleanup is needed here for this candidate. - debugPrint( - '[USB Serial] Failed to open $candidate: ${error.msg} (code ${error.error})', - ); _debugLogService?.warn( 'Failed to open $candidate: ${error.msg} (code ${error.error})', tag: 'USB Serial', @@ -215,11 +210,8 @@ class UsbSerialService { // Try next candidate } catch (error, stackTrace) { _status = UsbSerialStatus.disconnected; - debugPrint( - '[USB Serial] Unexpected error opening $candidate: $error\n$stackTrace', - ); _debugLogService?.error( - 'Unexpected error opening $candidate: $error', + 'Unexpected error opening $candidate: $error\n$stackTrace', tag: 'USB Serial', ); rethrow; @@ -232,7 +224,6 @@ class UsbSerialService { final msg = lastError != null ? 'Failed to open USB port $primary: ${lastError.msg} (code ${lastError.error})' : 'Failed to open USB port $primary'; - debugPrint('[USB Serial] $msg'); _debugLogService?.error(msg, tag: 'USB Serial'); throw StateError(msg); } From 25fc9454a859a6066d098f843341d63d452a2f5e Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 4 Mar 2026 14:17:01 -0500 Subject: [PATCH 264/421] Add error handling tests for USB connection and listing ports --- test/screens/usb_flow_test.dart | 59 ++++++++++++++++++- .../services/usb_serial_frame_codec_test.dart | 21 +++++++ test/utils/usb_port_labels_test.dart | 18 ++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index a932a57..8bbd961 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; @@ -23,6 +24,8 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { String? fakeActiveUsbPort; String? fakeActiveUsbPortDisplayLabel; bool fakeUsbTransportConnected = false; + Future> Function()? listUsbPortsImpl; + Future Function({required String portName})? connectUsbImpl; @override MeshCoreConnectionState get state => initialState; @@ -38,13 +41,21 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { bool get isUsbTransportConnected => fakeUsbTransportConnected; @override - Future> listUsbPorts() async => List.from(_ports); + Future> listUsbPorts() async { + if (listUsbPortsImpl != null) { + return listUsbPortsImpl!(); + } + return List.from(_ports); + } @override Future connectUsb({ required String portName, int baudRate = 115200, }) async { + if (connectUsbImpl != null) { + return connectUsbImpl!(portName: portName); + } connectUsbCalls += 1; lastConnectPortName = portName; } @@ -145,4 +156,50 @@ void main() { expect(find.widgetWithText(FloatingActionButton, 'USB'), findsNothing); } }); + + group('Error Handling', () { + testWidgets('shows error message when listing ports fails', (tester) async { + final connector = _FakeMeshCoreConnector(); + connector.listUsbPortsImpl = () async { + throw PlatformException( + code: 'usb_permission_denied', + message: 'Permission denied', + ); + }; + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + expect( + find.text('USB permission was denied.'), + findsOneWidget, + ); + }); + + testWidgets( + 'connection failure completes without leaving loading state', + (tester) async { + final connector = _FakeMeshCoreConnector( + ports: ['COM1'], + ); + var connectAttempted = false; + connector.connectUsbImpl = ({required String portName}) async { + connectAttempted = true; + throw PlatformException(code: 'usb_busy', message: 'Device is busy'); + }; + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pumpAndSettle(); + + expect(connectAttempted, isTrue); + expect(find.byType(CircularProgressIndicator), findsNothing); + }); + }); } diff --git a/test/services/usb_serial_frame_codec_test.dart b/test/services/usb_serial_frame_codec_test.dart index 54165de..32242bd 100644 --- a/test/services/usb_serial_frame_codec_test.dart +++ b/test/services/usb_serial_frame_codec_test.dart @@ -138,4 +138,25 @@ void main() { expect(packets, hasLength(1)); expect(packets.single.payload, orderedEquals([0x55])); }); + + test('recovers from invalid frame header', () { + final decoder = UsbSerialFrameDecoder(); + + final packets = decoder.ingest( + Uint8List.fromList([ + // First, a malformed frame (e.g. from a partial TX echo) + usbSerialRxFrameStart, + usbSerialTxFrameStart, + // Then, a valid frame + usbSerialRxFrameStart, + 0x01, + 0x00, + 0x88, + ]), + ); + + expect(packets, hasLength(1)); + expect(packets.single.isRxFrame, isTrue); + expect(packets.single.payload, orderedEquals([0x88])); + }); } diff --git a/test/utils/usb_port_labels_test.dart b/test/utils/usb_port_labels_test.dart index b2d88bd..8c9212b 100644 --- a/test/utils/usb_port_labels_test.dart +++ b/test/utils/usb_port_labels_test.dart @@ -35,6 +35,24 @@ void main() { }, ); + test('friendlyUsbPortName works for Linux-style label', () { + expect( + friendlyUsbPortName( + '/dev/ttyACM0 - RAK4631 - USB VID:PID=239A:8029 SER=xxxxxxxx', + ), + 'RAK4631', + ); + }); + + test('friendlyUsbPortName trims whitespace from label parts', () { + expect( + friendlyUsbPortName( + ' /dev/ttyS0 - My Serial Port - n/a ', + ), + 'My Serial Port', + ); + }); + test( 'friendlyUsbPortName falls back to port name when description is n/a', () { From 3452bdae8c95847c74c2a2ccdfd0f92391dbe5f4 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 4 Mar 2026 14:22:28 -0500 Subject: [PATCH 265/421] Refactor test cases for USB flow and port labels for improved readability --- test/screens/usb_flow_test.dart | 15 +++++---------- test/utils/usb_port_labels_test.dart | 4 +--- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 8bbd961..fdcb5ed 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -172,18 +172,13 @@ void main() { ); await tester.pumpAndSettle(); - expect( - find.text('USB permission was denied.'), - findsOneWidget, - ); + expect(find.text('USB permission was denied.'), findsOneWidget); }); - testWidgets( - 'connection failure completes without leaving loading state', - (tester) async { - final connector = _FakeMeshCoreConnector( - ports: ['COM1'], - ); + testWidgets('connection failure completes without leaving loading state', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector(ports: ['COM1']); var connectAttempted = false; connector.connectUsbImpl = ({required String portName}) async { connectAttempted = true; diff --git a/test/utils/usb_port_labels_test.dart b/test/utils/usb_port_labels_test.dart index 8c9212b..1f81c78 100644 --- a/test/utils/usb_port_labels_test.dart +++ b/test/utils/usb_port_labels_test.dart @@ -46,9 +46,7 @@ void main() { test('friendlyUsbPortName trims whitespace from label parts', () { expect( - friendlyUsbPortName( - ' /dev/ttyS0 - My Serial Port - n/a ', - ), + friendlyUsbPortName(' /dev/ttyS0 - My Serial Port - n/a '), 'My Serial Port', ); }); From b5b930646f34dfef0ad8b945c580dd6418b8efb4 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 4 Mar 2026 14:41:16 -0500 Subject: [PATCH 266/421] Update flserial dependency to a specific commit reference --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3656e1f..692326a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,7 +41,7 @@ dependencies: flserial: git: url: https://github.com/MeshEnvy/flserial.git - ref: master + ref: 48216310061efc8d5d217cc18014fc2cb501646e provider: ^6.1.5+1 shared_preferences: ^2.2.2 uuid: ^4.3.3 From f584c4fba0bbdea7b2cc1a2204d2f5037c563d88 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Wed, 4 Mar 2026 16:38:12 -0500 Subject: [PATCH 267/421] added linux notification service --- lib/services/notification_service.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 0b59bbc..b3df59f 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -63,12 +63,16 @@ class NotificationService { appUserModelId: 'org.meshcore.open.app', guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86', ); + const linuxSettings = LinuxInitializationSettings( + defaultActionName: 'Open notification', + ); const initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, macOS: macSettings, windows: windowsSettings, + linux: linuxSettings, ); try { From fb58a3262c78189b937ecbedc5eb3f0e12666bad Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Thu, 5 Mar 2026 02:50:38 -0500 Subject: [PATCH 268/421] addressed codex review cleanup --- lib/screens/scanner_screen.dart | 3 +++ lib/services/usb_serial_service_native.dart | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 5d7695c..45fd3fb 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -33,9 +33,12 @@ class _ScannerScreenState extends State { _connector = Provider.of(context, listen: false); _connectionListener = () { + final isCurrentRoute = ModalRoute.of(context)?.isCurrent ?? true; if (_connector.state == MeshCoreConnectionState.disconnected) { _changedNavigation = false; } else if (_connector.state == MeshCoreConnectionState.connected && + _connector.activeTransport == MeshCoreTransportType.bluetooth && + isCurrentRoute && !_changedNavigation) { _changedNavigation = true; if (mounted) { diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index f758a1f..69ed5b8 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -344,10 +344,6 @@ class UsbSerialService { // with a dangling NativeCallable pointer. if (_useDesktopFlSerial) { final serial = _serial; - _serial = null; - _status = UsbSerialStatus.disconnected; - _connectedPortKey = null; - _connectedPortLabel = null; try { if (serial?.isOpen() == FlOpenStatus.open) { serial?.closePort(); // synchronous C call — kills the SerialThread From 1f2dfc555b8b165336869a843b5b1170f5117174 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:02:37 -0700 Subject: [PATCH 269/421] Add guessed node location map keys and translations Adds map_showGuessedLocations and map_guessedLocation to app_en.arb and translates them across all 14 supported locales. Regenerates dart localizations. --- lib/l10n/app_bg.arb | 4 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 4 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 12 + lib/l10n/app_localizations_bg.dart | 7 + lib/l10n/app_localizations_de.dart | 7 + lib/l10n/app_localizations_en.dart | 6 + lib/l10n/app_localizations_es.dart | 7 + lib/l10n/app_localizations_fr.dart | 7 + lib/l10n/app_localizations_it.dart | 6 + lib/l10n/app_localizations_nl.dart | 7 + lib/l10n/app_localizations_pl.dart | 7 + lib/l10n/app_localizations_pt.dart | 7 + lib/l10n/app_localizations_ru.dart | 7 + lib/l10n/app_localizations_sk.dart | 7 + lib/l10n/app_localizations_sl.dart | 6 + lib/l10n/app_localizations_sv.dart | 7 + lib/l10n/app_localizations_uk.dart | 7 + lib/l10n/app_localizations_zh.dart | 6 + lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pl.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_sk.arb | 4 +- lib/l10n/app_sl.arb | 4 +- lib/l10n/app_sv.arb | 4 +- lib/l10n/app_uk.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/models/app_settings.dart | 8 + lib/screens/chat_screen.dart | 1 + lib/screens/contacts_screen.dart | 1 + lib/screens/map_screen.dart | 284 +++++++++++++++++++++++- lib/screens/path_trace_map.dart | 146 +++++++++++- lib/services/app_settings_service.dart | 4 + lib/services/path_history_service.dart | 5 + lib/widgets/path_management_dialog.dart | 1 + 39 files changed, 587 insertions(+), 34 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 832e090..9733738 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.", "discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти", "discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?", - "common_deleteAll": "Изтрий всичко" + "common_deleteAll": "Изтрий всичко", + "map_guessedLocation": "Предполагано местоположение", + "map_showGuessedLocations": "Покажете местоположенията на предположените възли." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index c1d344f..ecfd8e4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1854,5 +1854,7 @@ "contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.", "common_deleteAll": "Alles löschen", "discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?", - "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen" + "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen", + "map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen", + "map_guessedLocation": "Geschätzter Ort" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 99221dc..12c9844 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -775,6 +775,8 @@ "map_publicKeyPrefix": "Public key prefix", "map_markers": "Markers", "map_showSharedMarkers": "Show shared markers", + "map_showGuessedLocations": "Show guessed node locations", + "map_guessedLocation": "Guessed location", "map_lastSeenTime": "Last Seen Time", "map_sharedPin": "Shared pin", "map_joinRoom": "Join Room", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1d3c4d5..82ffa55 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1854,5 +1854,7 @@ "contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.", "common_deleteAll": "Eliminar todo", "discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos", - "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!" + "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!", + "map_guessedLocation": "Ubicación estimada", + "map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1eae5b9..42d6ecb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.", "common_deleteAll": "Supprimer tout", "discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts", - "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?" + "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?", + "map_showGuessedLocations": "Afficher les emplacements des nœuds estimés", + "map_guessedLocation": "Lieu deviné" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 9a1f1a4..f3f3f53 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.", "common_deleteAll": "Elimina tutto", "discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?", - "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti" + "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti", + "map_guessedLocation": "Località indovinata", + "map_showGuessedLocations": "Mostra le posizioni stimate dei nodi" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0eee461..7a77e19 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2638,6 +2638,18 @@ abstract class AppLocalizations { /// **'Show shared markers'** String get map_showSharedMarkers; + /// No description provided for @map_showGuessedLocations. + /// + /// In en, this message translates to: + /// **'Show guessed node locations'** + String get map_showGuessedLocations; + + /// No description provided for @map_guessedLocation. + /// + /// In en, this message translates to: + /// **'Guessed location'** + String get map_guessedLocation; + /// No description provided for @map_lastSeenTime. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index aa597c5..85dcf42 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1443,6 +1443,13 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_showSharedMarkers => 'Покажи споделени маркери'; + @override + String get map_showGuessedLocations => + 'Покажете местоположенията на предположените възли.'; + + @override + String get map_guessedLocation => 'Предполагано местоположение'; + @override String get map_lastSeenTime => 'Последна видяна дата'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 479a050..67dcad8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1442,6 +1442,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker'; + @override + String get map_showGuessedLocations => + 'Zeige die vermuteten Knotenpositionen'; + + @override + String get map_guessedLocation => 'Geschätzter Ort'; + @override String get map_lastSeenTime => 'Letzte Sichtung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2fbc97a..87742a6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1421,6 +1421,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_showSharedMarkers => 'Show shared markers'; + @override + String get map_showGuessedLocations => 'Show guessed node locations'; + + @override + String get map_guessedLocation => 'Guessed location'; + @override String get map_lastSeenTime => 'Last Seen Time'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6227e5f..18166ae 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1440,6 +1440,13 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostrar marcadores compartidos'; + @override + String get map_showGuessedLocations => + 'Mostrar las ubicaciones estimadas de los nodos.'; + + @override + String get map_guessedLocation => 'Ubicación estimada'; + @override String get map_lastSeenTime => 'Última vez que se vio'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7ce62b9..7f36c78 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1447,6 +1447,13 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; + @override + String get map_showGuessedLocations => + 'Afficher les emplacements des nœuds estimés'; + + @override + String get map_guessedLocation => 'Lieu deviné'; + @override String get map_lastSeenTime => 'Dernière fois vu'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d173456..f84f461 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1439,6 +1439,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostra i segnaposto condivisi'; + @override + String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi'; + + @override + String get map_guessedLocation => 'Località indovinata'; + @override String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2a873a5..5ed036d 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1434,6 +1434,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_showSharedMarkers => 'Toon gedeelde markeringen'; + @override + String get map_showGuessedLocations => + 'Toon de voorspelde locaties van de knopen'; + + @override + String get map_guessedLocation => 'Geroerde locatie'; + @override String get map_lastSeenTime => 'Laatste Bekeken Tijd'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index e8ee410..70c8061 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1440,6 +1440,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; + @override + String get map_showGuessedLocations => + 'Wyświetl lokalizacje zgadanych węzłów'; + + @override + String get map_guessedLocation => 'Wydana lokalizacja'; + @override String get map_lastSeenTime => 'Ostatni raz widiany'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 93e1ffb..aa37f8a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1441,6 +1441,13 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_showSharedMarkers => 'Mostrar marcadores compartilhados'; + @override + String get map_showGuessedLocations => + 'Mostrar as localizações dos nós estimados'; + + @override + String get map_guessedLocation => 'Localização estimada'; + @override String get map_lastSeenTime => 'Último Tempo de Visualização'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1e59160..affb256 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1442,6 +1442,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_showSharedMarkers => 'Показывать общие метки'; + @override + String get map_showGuessedLocations => + 'Отобразить предполагаемые места расположения узлов'; + + @override + String get map_guessedLocation => 'Угаданное место'; + @override String get map_lastSeenTime => 'Время последнего появления'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 6e68a97..0861ca8 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1435,6 +1435,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_showSharedMarkers => 'Zobraziť zdieľané značky'; + @override + String get map_showGuessedLocations => + 'Zobraziť umiestnenia odhadnutých uzlov'; + + @override + String get map_guessedLocation => 'Odhadnutá lokalita'; + @override String get map_lastSeenTime => 'Posledný čas sledovania'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 9b08106..0b4bd0f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1431,6 +1431,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_showSharedMarkers => 'Pokaži skupno označenja'; + @override + String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.'; + + @override + String get map_guessedLocation => 'Predpostavljena lokacija'; + @override String get map_lastSeenTime => 'Datum zadnjega vpogleda'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 2a41947..7281f10 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1427,6 +1427,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_showSharedMarkers => 'Visa delade markörer'; + @override + String get map_showGuessedLocations => + 'Visa upp de antagna nodernas placeringar'; + + @override + String get map_guessedLocation => 'Gissad plats'; + @override String get map_lastSeenTime => 'Senaste Visats Tid'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 87d2318..5bf6da2 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1441,6 +1441,13 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_showSharedMarkers => 'Показувати спільні маркери'; + @override + String get map_showGuessedLocations => + 'Показати місцезнаходження передбачених вузлів'; + + @override + String get map_guessedLocation => 'Визначено місцезнаходження'; + @override String get map_lastSeenTime => 'Час останньої активності'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index eb79671..c3ee3e6 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1363,6 +1363,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_showSharedMarkers => '显示共享标记'; + @override + String get map_showGuessedLocations => '显示猜测的节点位置'; + + @override + String get map_guessedLocation => '猜测的位置'; + @override String get map_lastSeenTime => '最后在线时间'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index ce533e7..cde7457 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.", "common_deleteAll": "Alles verwijderen", "discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten", - "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?" + "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?", + "map_guessedLocation": "Geroerde locatie", + "map_showGuessedLocations": "Toon de voorspelde locaties van de knopen" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 509be40..41152a5 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.", "common_deleteAll": "Usuń wszystko", "discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?", - "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty" + "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty", + "map_guessedLocation": "Wydana lokalizacja", + "map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8989d29..f843119 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.", "common_deleteAll": "Excluir Tudo", "discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos", - "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?" + "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?", + "map_guessedLocation": "Localização estimada", + "map_showGuessedLocations": "Mostrar as localizações dos nós estimados" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 42b440f..0897cba 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1066,5 +1066,7 @@ "contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.", "common_deleteAll": "Удалить все", "discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?", - "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты" + "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты", + "map_guessedLocation": "Угаданное место", + "map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 7fea7e3..16aae9b 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.", "discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty", "common_deleteAll": "Zmazať všetko", - "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?" + "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?", + "map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov", + "map_guessedLocation": "Odhadnutá lokalita" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 8dadec8..97d913f 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.", "common_deleteAll": "Izbriši vse", "discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?", - "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte" + "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte", + "map_guessedLocation": "Predpostavljena lokacija", + "map_showGuessedLocations": "Pokaži lokacije domnevnih not." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 2467015..b451082 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.", "common_deleteAll": "Ta bort alla", "discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?", - "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter" + "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter", + "map_guessedLocation": "Gissad plats", + "map_showGuessedLocations": "Visa upp de antagna nodernas placeringar" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index dc48f09..fb60b81 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1826,5 +1826,7 @@ "contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.", "common_deleteAll": "Видалити все", "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", - "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?" + "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?", + "map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів", + "map_guessedLocation": "Визначено місцезнаходження" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index ad58f4c..51bd60c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1831,5 +1831,7 @@ "contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。", "common_deleteAll": "删除全部", "discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?", - "discoveredContacts_deleteContactAll": "删除所有发现的联系人" + "discoveredContacts_deleteContactAll": "删除所有发现的联系人", + "map_showGuessedLocations": "显示猜测的节点位置", + "map_guessedLocation": "猜测的位置" } diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 62ba9ca..abcc729 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -22,6 +22,7 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final bool mapShowGuessedLocations; final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; @@ -48,6 +49,7 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.mapShowGuessedLocations = true, this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, @@ -78,6 +80,7 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'map_show_guessed_locations': mapShowGuessedLocations, 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, @@ -115,6 +118,8 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + mapShowGuessedLocations: + json['map_show_guessed_locations'] as bool? ?? true, enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), @@ -159,6 +164,7 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + bool? mapShowGuessedLocations, bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, @@ -185,6 +191,8 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + mapShowGuessedLocations: + mapShowGuessedLocations ?? this.mapShowGuessedLocations, enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 7c8fcfb..0075040 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -818,6 +818,7 @@ class _ChatScreenState extends State { title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), flipPathRound: true, + targetContact: widget.contact, ), ), ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 47bac9c..e5cc785 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1131,6 +1131,7 @@ class _ContactsScreenState extends State contact.name, ), path: contact.traceRouteBytes ?? Uint8List(0), + targetContact: contact, ), ), ); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 2ec71a0..c4808f1 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -15,6 +15,7 @@ import '../models/app_settings.dart'; import '../models/channel.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; +import '../services/path_history_service.dart'; import '../services/map_marker_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; @@ -64,6 +65,8 @@ class _MapScreenState extends State { final List _polylines = []; bool _legendExpanded = false; bool _showNodeLabels = true; + List<_GuessedLocation> _cachedGuessedLocations = []; + String _guessedLocationsCacheKey = ''; @override void initState() { @@ -119,8 +122,8 @@ class _MapScreenState extends State { @override Widget build(BuildContext context) { - return Consumer2( - builder: (context, connector, settingsService, child) { + return Consumer3( + builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); final settings = settingsService.settings; final contacts = connector.contacts; @@ -160,6 +163,31 @@ class _MapScreenState extends State { .where((c) => c.hasLocation) .toList(); + // All contacts with a known location — used as anchors regardless of + // time/key-prefix filters so that repeaters are always available. + final allContactsWithLocation = contacts + .where((c) => c.hasLocation) + .toList(); + + // Compute guessed locations with caching + final maxRangeKm = _estimateLoRaRangeKm(connector); + final cacheKey = + '${filteredByKeyPrefix.length}:${allContactsWithLocation.length}:${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; + if (cacheKey != _guessedLocationsCacheKey) { + _guessedLocationsCacheKey = cacheKey; + _cachedGuessedLocations = settings.mapShowGuessedLocations + ? _computeGuessedLocations( + filteredByKeyPrefix, + allContactsWithLocation, + pathHistory, + maxRangeKm, + ) + : []; + } + final guessedLocations = settings.mapShowGuessedLocations + ? _cachedGuessedLocations + : <_GuessedLocation>[]; + _polylines.clear(); _polylines.addAll( _points.length > 1 @@ -430,6 +458,8 @@ class _MapScreenState extends State { size: 34, ), ), + if (!_isBuildingPathTrace) + ...guessedLocations.map(_buildGuessedMarker), ..._buildMarkers( contactsWithLocation, settings, @@ -489,6 +519,7 @@ class _MapScreenState extends State { contactsWithLocation, settings, sharedMarkers.length, + guessedLocations.length, ), if (_isBuildingPathTrace) _buildPathTraceOverlay(), ], @@ -512,6 +543,201 @@ class _MapScreenState extends State { ); } + List<_GuessedLocation> _computeGuessedLocations( + List allContacts, + List withLocation, + PathHistoryService pathHistory, + double? maxRangeKm, + ) { + // Index known-location repeaters by their 1-byte hash. + // null value = two repeaters share the same hash byte (ambiguous collision). + final repeaterByHash = {}; + for (final c in withLocation) { + if (c.type == advTypeRepeater) { + if (repeaterByHash.containsKey(c.publicKey[0])) { + repeaterByHash[c.publicKey[0]] = null; // collision: can't disambiguate + } else { + repeaterByHash[c.publicKey[0]] = c; + } + } + } + + final result = <_GuessedLocation>[]; + + for (final contact in allContacts) { + if (contact.hasLocation) continue; + + final anchorSet = {}; + + // Collect the contact-side (last-hop) repeater from every known path. + // path = [device-side hop, ..., contact-side hop] + // Only path.last is actually within radio range of the contact — using + // earlier bytes would anchor against our own side of the network. + final pathSets = >[ + contact.path.toList(), + ...pathHistory + .getRecentPaths(contact.publicKeyHex) + .map((r) => r.pathBytes), + ]; + final lastHopBytes = {}; + for (final pathBytes in pathSets) { + if (pathBytes.isEmpty) continue; + final lastHop = pathBytes.last; + lastHopBytes.add(lastHop); + final r = repeaterByHash[lastHop]; + if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!)); + } + + // Fallback: for any last-hop byte with no GPS repeater, average the + // positions of contacts with known GPS that share the same last hop. + // Those contacts are all adjacent to the same unknown repeater, so their + // centroid is a reasonable proxy for its location. + for (final byte in lastHopBytes) { + if (repeaterByHash.containsKey(byte)) continue; + for (final c in withLocation) { + if (c.path.isNotEmpty && c.path.last == byte) { + anchorSet.add(LatLng(c.latitude!, c.longitude!)); + } + } + } + + // Filter anchors that are geometrically inconsistent with radio range. + // Two anchors more than 2 * maxRange apart cannot both be in direct radio + // range of the same node, so isolated outliers are removed. + final anchors = maxRangeKm != null && anchorSet.length > 1 + ? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm) + : anchorSet.toList(); + + if (anchors.isEmpty) continue; + + final LatLng position; + if (anchors.length == 1) { + // Offset single-anchor guesses so they don't overlap the repeater marker. + // Use the contact's public key byte as a deterministic angle seed. + const offsetDeg = 0.003; // ~330 m at the equator + final angle = (contact.publicKey[1] / 255.0) * 2 * pi; + position = LatLng( + anchors[0].latitude + offsetDeg * cos(angle), + anchors[0].longitude + offsetDeg * sin(angle), + ); + } else { + double lat = 0, lon = 0; + for (final a in anchors) { + lat += a.latitude; + lon += a.longitude; + } + position = LatLng(lat / anchors.length, lon / anchors.length); + } + result.add( + _GuessedLocation( + contact: contact, + position: position, + highConfidence: anchors.length >= 2, + ), + ); + } + + return result; + } + + /// Estimates the free-space maximum LoRa range in km from the connected + /// device's current radio parameters. Returns null if parameters are unknown. + double? _estimateLoRaRangeKm(MeshCoreConnector connector) { + final freqHz = connector.currentFreqHz; + final bwHz = connector.currentBwHz; + final sf = connector.currentSf; + final txPower = connector.currentTxPower; + if (freqHz == null || bwHz == null || sf == null || txPower == null) { + return null; + } + // LoRa receiver sensitivity = thermal noise + NF + required demod SNR + const noiseFigureDb = 6.0; + final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10; + final sensitivityDbm = + thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf); + // FSPL at max range equals link budget: + // FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55 + final linkBudgetDb = txPower.toDouble() - sensitivityDbm; + final exponent = + (linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20; + return pow(10, exponent) / 1000; + } + + double _sfToRequiredSnrDb(int sf) { + switch (sf) { + case 5: + return -2.5; + case 6: + return -5.0; + case 7: + return -7.5; + case 8: + return -10.0; + case 9: + return -12.5; + case 10: + return -15.0; + case 11: + return -17.5; + case 12: + return -20.0; + default: + return -10.0; + } + } + + /// Removes anchors that have no neighbour within 2 * maxRangeKm. + /// A node cannot be simultaneously in radio range of two points farther apart + /// than twice the expected maximum range. + List _filterConsistentAnchors( + List anchors, + double maxRangeKm, + ) { + const distance = Distance(); + final maxDistM = maxRangeKm * 2000; + return anchors + .where( + (a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM), + ) + .toList(); + } + + Marker _buildGuessedMarker(_GuessedLocation guess) { + final color = _getNodeColor(guess.contact.type); + return Marker( + point: guess.position, + width: 35, + height: 35, + child: GestureDetector( + onTap: () => _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.not_listed_location, + color: Colors.white, + size: 20, + ), + ), + ), + ); + } + List _buildMarkers( List contacts, settings, { @@ -657,6 +883,7 @@ class _MapScreenState extends State { List contactsWithLocation, settings, int markerCount, + int guessedCount, ) { int nodeCount = 0; for (final contact in contactsWithLocation) { @@ -696,7 +923,12 @@ class _MapScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - context.l10n.map_nodesCount(nodeCount), + context.l10n.map_nodesCount( + nodeCount + + (settings.mapShowGuessedLocations + ? guessedCount + : 0), + ), style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, @@ -764,6 +996,12 @@ class _MapScreenState extends State { context.l10n.map_pinPublic, Colors.orange, ), + if (settings.mapShowGuessedLocations && guessedCount > 0) + _buildLegendItem( + Icons.not_listed_location, + context.l10n.map_guessedLocation, + Colors.grey, + ), ], ), ), @@ -952,7 +1190,11 @@ class _MapScreenState extends State { ); } - void _showNodeInfo(BuildContext context, Contact contact) { + void _showNodeInfo( + BuildContext context, + Contact contact, { + LatLng? guessedPosition, + }) { showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -972,10 +1214,16 @@ class _MapScreenState extends State { children: [ _buildInfoRow('Type', contact.typeLabel), _buildInfoRow('Path', contact.pathLabel), - _buildInfoRow( - 'Location', - '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', - ), + if (contact.hasLocation) + _buildInfoRow( + 'Location', + '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', + ) + else if (guessedPosition != null) + _buildInfoRow( + 'Est. Location', + '~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}', + ), _buildInfoRow( context.l10n.map_lastSeen, _formatLastSeen(contact.lastSeen), @@ -1481,6 +1729,14 @@ class _MapScreenState extends State { }, contentPadding: EdgeInsets.zero, ), + CheckboxListTile( + title: Text(context.l10n.map_showGuessedLocations), + value: settings.mapShowGuessedLocations, + onChanged: (value) { + service.setMapShowGuessedLocations(value ?? true); + }, + contentPadding: EdgeInsets.zero, + ), const SizedBox(height: 16), Text( context.l10n.map_keyPrefix, @@ -1744,6 +2000,18 @@ class _MapScreenState extends State { } } +class _GuessedLocation { + final Contact contact; + final LatLng position; + final bool highConfidence; + + _GuessedLocation({ + required this.contact, + required this.position, + required this.highConfidence, + }); +} + class _MarkerPayload { final LatLng position; final String label; diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5f86cc1..465d7db 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -54,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget { final int? repeaterId; final bool flipPathRound; final bool reversePathRound; + final Contact? targetContact; const PathTraceMapScreen({ super.key, @@ -62,6 +63,7 @@ class PathTraceMapScreen extends StatefulWidget { this.repeaterId, this.flipPathRound = false, this.reversePathRound = false, + this.targetContact, }); @override @@ -78,6 +80,11 @@ class _PathTraceMapScreenState extends State { bool _failed2Loaded = false; bool _hasData = false; PathTraceData? _traceData; + // Inferred positions for hops that have no GPS location, keyed by hop byte. + Map _inferredHopPositions = {}; + // Endpoint position for the target contact (GPS or guessed). + LatLng? _targetContactPosition; + bool _targetContactIsGuessed = false; List _points = []; List _polylines = []; LatLng? _initialCenter = LatLng(0, 0); @@ -242,25 +249,90 @@ class _PathTraceMapScreenState extends State { } }); + // For hops with no GPS contact, infer position from other contacts + // with known GPS that share the same last-hop byte. + final Map inferredPositions = {}; + for (final hop in pathData) { + final contact = pathContacts[hop]; + if (contact != null && contact.hasLocation) continue; + final peers = connector.contacts + .where( + (c) => + c.hasLocation && + c.path.isNotEmpty && + c.path.last == hop, + ) + .toList(); + if (peers.isNotEmpty) { + final lat = + peers.map((c) => c.latitude!).reduce((a, b) => a + b) / + peers.length; + final lon = + peers.map((c) => c.longitude!).reduce((a, b) => a + b) / + peers.length; + inferredPositions[hop] = LatLng(lat, lon); + } + } + setState(() { _isLoading = false; _hasData = true; + _inferredHopPositions = inferredPositions; _traceData = PathTraceData( pathData: pathData, snrData: snrData, pathContacts: pathContacts, ); + // Compute endpoint position for the target contact. + LatLng? targetPos; + bool targetGuessed = false; + final target = widget.targetContact; + if (target != null) { + if (target.hasLocation) { + targetPos = LatLng(target.latitude!, target.longitude!); + } else if (pathData.isNotEmpty) { + // Infer from the last hop: average GPS contacts sharing that hop. + final lastHop = pathData.last; + final peers = connector.contacts + .where( + (c) => + c.hasLocation && + c.path.isNotEmpty && + c.path.last == lastHop, + ) + .toList(); + if (peers.isNotEmpty) { + final lat = + peers.map((c) => c.latitude!).reduce((a, b) => a + b) / + peers.length; + final lon = + peers.map((c) => c.longitude!).reduce((a, b) => a + b) / + peers.length; + const offsetDeg = 0.003; + final angle = (target.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + lat + offsetDeg * cos(angle), + lon + offsetDeg * sin(angle), + ); + targetGuessed = true; + } + } + } + _targetContactPosition = targetPos; + _targetContactIsGuessed = targetGuessed; + _points = []; _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); for (final hop in _traceData!.pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact != null && - contact.hasLocation && - contact.latitude != null && - contact.longitude != null) { + if (contact != null && contact.hasLocation) { _points.add(LatLng(contact.latitude!, contact.longitude!)); + } else { + final inferred = inferredPositions[hop]; + if (inferred != null) _points.add(inferred); } } + if (targetPos != null) _points.add(targetPos); _polylines = _points.length > 1 ? [ Polyline( @@ -382,8 +454,13 @@ class _PathTraceMapScreenState extends State { final markers = []; for (final hop in pathData) { final contact = _traceData!.pathContacts[hop]; - if (contact == null || !contact.hasLocation) continue; - final point = LatLng(contact.latitude!, contact.longitude!); + final inferred = _inferredHopPositions[hop]; + final hasGps = contact != null && contact.hasLocation; + if (!hasGps && inferred == null) continue; + final point = hasGps + ? LatLng(contact.latitude!, contact.longitude!) + : inferred!; + final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); markers.add( Marker( point: point, @@ -392,7 +469,9 @@ class _PathTraceMapScreenState extends State { child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Colors.green, + color: hasGps + ? Colors.green + : Colors.orange.withValues(alpha: 0.75), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ @@ -405,10 +484,7 @@ class _PathTraceMapScreenState extends State { ), alignment: Alignment.center, child: Text( - contact.publicKey - .sublist(0, 1) - .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) - .join(), + hasGps ? label : '~$label', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -419,7 +495,12 @@ class _PathTraceMapScreenState extends State { ), ); if (showLabels) { - markers.add(_buildNodeLabelMarker(point: point, label: contact.name)); + markers.add( + _buildNodeLabelMarker( + point: point, + label: contact?.name ?? '~$label', + ), + ); } } @@ -468,6 +549,47 @@ class _PathTraceMapScreenState extends State { } } + // Add target contact endpoint marker. + final targetPos = _targetContactPosition; + if (targetPos != null) { + final isGuessed = _targetContactIsGuessed; + final targetName = widget.targetContact?.name ?? '?'; + markers.add( + Marker( + point: targetPos, + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: isGuessed + ? Colors.purple.withValues(alpha: 0.55) + : Colors.red, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: const Icon(Icons.person, color: Colors.white, size: 18), + ), + ), + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: targetPos, + label: isGuessed ? '~$targetName' : targetName, + ), + ); + } + } + return markers; } diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index eacf26f..c74fa40 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setMapShowGuessedLocations(bool value) async { + await updateSettings(_settings.copyWith(mapShowGuessedLocations: value)); + } + Future setEnableMessageTracing(bool value) async { await updateSettings(_settings.copyWith(enableMessageTracing: value)); } diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 1314f48..3da87cc 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -15,6 +15,9 @@ class PathHistoryService extends ChangeNotifier { final List _cacheAccessOrder = []; static const int _maxHistoryEntries = 100; + + int _version = 0; + int get version => _version; static const int _autoRotationTopCount = 3; PathHistoryService(this._storage); @@ -185,6 +188,7 @@ class PathHistoryService extends ChangeNotifier { ) { var history = _cache[contactPubKeyHex]; if (history == null) return; + _version++; final existing = _findPathRecord(contactPubKeyHex, pathBytes); if (existing != null) { @@ -241,6 +245,7 @@ class PathHistoryService extends ChangeNotifier { _cache[contactPubKeyHex] = loaded; _trackAccess(contactPubKeyHex); _evictIfNeeded(); + _version++; notifyListeners(); } }); diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index c2b6d12..384f92b 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -79,6 +79,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), flipPathRound: true, + targetContact: widget.contact, ), ), ), From 7c479f912184447e18893dcce59f25dfdd04b6ab Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:03:12 -0700 Subject: [PATCH 270/421] Formatted --- lib/screens/map_screen.dart | 7 +++---- lib/screens/path_trace_map.dart | 5 +---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index c4808f1..79ce54a 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -555,7 +555,8 @@ class _MapScreenState extends State { for (final c in withLocation) { if (c.type == advTypeRepeater) { if (repeaterByHash.containsKey(c.publicKey[0])) { - repeaterByHash[c.publicKey[0]] = null; // collision: can't disambiguate + repeaterByHash[c.publicKey[0]] = + null; // collision: can't disambiguate } else { repeaterByHash[c.publicKey[0]] = c; } @@ -696,9 +697,7 @@ class _MapScreenState extends State { const distance = Distance(); final maxDistM = maxRangeKm * 2000; return anchors - .where( - (a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM), - ) + .where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM)) .toList(); } diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 465d7db..df5b19a 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -257,10 +257,7 @@ class _PathTraceMapScreenState extends State { if (contact != null && contact.hasLocation) continue; final peers = connector.contacts .where( - (c) => - c.hasLocation && - c.path.isNotEmpty && - c.path.last == hop, + (c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop, ) .toList(); if (peers.isNotEmpty) { From b2770ef028ec25190bfaf23efaacd7cd0544f312 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:11:21 -0700 Subject: [PATCH 271/421] fix ai suggestions --- lib/screens/map_screen.dart | 8 +++++++- lib/services/path_history_service.dart | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 79ce54a..0956b96 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -171,8 +171,14 @@ class _MapScreenState extends State { // Compute guessed locations with caching final maxRangeKm = _estimateLoRaRangeKm(connector); + final filteredKeys = filteredByKeyPrefix + .map((c) => c.publicKeyHex) + .join(','); + final anchorKeys = allContactsWithLocation + .map((c) => c.publicKeyHex) + .join(','); final cacheKey = - '${filteredByKeyPrefix.length}:${allContactsWithLocation.length}:${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; + '$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; if (cacheKey != _guessedLocationsCacheKey) { _guessedLocationsCacheKey = cacheKey; _cachedGuessedLocations = settings.mapShowGuessedLocations diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 3da87cc..569fada 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -281,6 +281,7 @@ class PathHistoryService extends ChangeNotifier { _autoRotationIndex.remove(contactPubKeyHex); _floodStats.remove(contactPubKeyHex); await _storage.clearPathHistory(contactPubKeyHex); + _version++; notifyListeners(); } @@ -300,6 +301,7 @@ class PathHistoryService extends ChangeNotifier { ); await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!); + _version++; notifyListeners(); } From 81548fdc21a6cb6ccb8a53d5044d64ae2d2c3013 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:18:48 -0700 Subject: [PATCH 272/421] ai fixes --- lib/screens/map_screen.dart | 7 +++++-- lib/screens/path_trace_map.dart | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 0956b96..3d94701 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -172,10 +172,13 @@ class _MapScreenState extends State { // Compute guessed locations with caching final maxRangeKm = _estimateLoRaRangeKm(connector); final filteredKeys = filteredByKeyPrefix - .map((c) => c.publicKeyHex) + .map((c) => '${c.publicKeyHex}:${c.path.join("-")}') .join(','); final anchorKeys = allContactsWithLocation - .map((c) => c.publicKeyHex) + .map( + (c) => + '${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}', + ) .join(','); final cacheKey = '$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index df5b19a..c6d800e 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -289,7 +289,11 @@ class _PathTraceMapScreenState extends State { targetPos = LatLng(target.latitude!, target.longitude!); } else if (pathData.isNotEmpty) { // Infer from the last hop: average GPS contacts sharing that hop. - final lastHop = pathData.last; + // For a round-trip path (flipPathRound), the target-side hop sits + // in the middle of the symmetric sequence; .last is the local side. + final lastHop = (widget.flipPathRound && pathData.length > 1) + ? pathData[(pathData.length - 1) ~/ 2] + : pathData.last; final peers = connector.contacts .where( (c) => From 7a0b8aad3d6a59e7d01ba31a19c6601956b39557 Mon Sep 17 00:00:00 2001 From: zach Date: Fri, 6 Mar 2026 15:39:54 -0700 Subject: [PATCH 273/421] Added more crypto payment options --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 10fb0a5..daedcd6 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,11 @@ If you find MeshCore Open useful and would like to support development, you can **Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ` + +**Monero Address:** '453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m' + +**Bitcoin Address:** 'bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5' + Your support helps maintain and improve this open-source project! ## Acknowledgments From ea2354712d3cb2a237381442c517b720537fbb7e Mon Sep 17 00:00:00 2001 From: zjs81 <30362347+zjs81@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:41:02 -0700 Subject: [PATCH 274/421] Fix formatting of cryptocurrency addresses in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index daedcd6..3b230dd 100644 --- a/README.md +++ b/README.md @@ -232,9 +232,9 @@ If you find MeshCore Open useful and would like to support development, you can **Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ` -**Monero Address:** '453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m' +**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m` -**Bitcoin Address:** 'bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5' +**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5` Your support helps maintain and improve this open-source project! From 0565cee461f857c4e124c8b70acbff74dc7fec94 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Fri, 6 Mar 2026 20:38:03 -0500 Subject: [PATCH 275/421] Enhance message merging logic and improve USB port listing --- lib/connector/meshcore_connector.dart | 20 +++++++++++++++----- lib/services/usb_serial_service_web.dart | 5 +---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 71418a4..4299c43 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -385,13 +385,23 @@ class MeshCoreConnector extends ChangeNotifier { final currentMessages = _conversations[contactKeyHex] ?? const []; final mergedMessages = [...windowedMessages]; - final existingKeys = { - for (final message in windowedMessages) _messageMergeKey(message), - }; + final persistedKeyCounts = {}; + for (final message in windowedMessages) { + final key = _messageMergeKey(message); + persistedKeyCounts[key] = (persistedKeyCounts[key] ?? 0) + 1; + } + final currentKeyCounts = {}; for (final message in currentMessages) { final key = _messageMergeKey(message); - if (existingKeys.add(key)) { + final currentCount = (currentKeyCounts[key] ?? 0) + 1; + currentKeyCounts[key] = currentCount; + final persistedCount = persistedKeyCounts[key] ?? 0; + + // Preserve distinct duplicates without IDs (for example same text + // received multiple times in the same second) by only skipping the + // overlapping occurrences that already exist in persisted storage. + if (currentCount > persistedCount) { mergedMessages.add(message); } } @@ -413,7 +423,7 @@ class MeshCoreConnector extends ChangeNotifier { if (messageId != null && messageId.isNotEmpty) { return 'id:$messageId'; } - return 'fallback:${message.isOutgoing}:${message.timestamp.millisecondsSinceEpoch}:${message.text}'; + return 'fallback:${message.senderKeyHex}:${message.isOutgoing}:${message.isCli}:${message.timestamp.millisecondsSinceEpoch}:${message.text}'; } /// Load older messages for a contact (pagination) diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index df5893b..4953cf5 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -57,10 +57,7 @@ class UsbSerialService { _resetPortCache(); final ports = await _getAuthorizedPorts(); - if (ports.isEmpty) { - return [_requestPortListEntry]; - } - return ports.map(_listEntryForPort).toList(growable: false); + return [_requestPortListEntry, ...ports.map(_listEntryForPort)]; } Future connect({ From 8238b6197fd16e947ffb893a894ced4c3c08ce7f Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sat, 7 Mar 2026 01:16:04 -0500 Subject: [PATCH 276/421] Regenerated localization files --- lib/l10n/app_bg.arb | 21 ++++++++++++++++++- lib/l10n/app_de.arb | 21 ++++++++++++++++++- lib/l10n/app_es.arb | 21 ++++++++++++++++++- lib/l10n/app_fr.arb | 21 ++++++++++++++++++- lib/l10n/app_it.arb | 21 ++++++++++++++++++- lib/l10n/app_localizations_bg.dart | 6 +++--- lib/l10n/app_localizations_de.dart | 8 +++---- lib/l10n/app_localizations_es.dart | 8 +++---- lib/l10n/app_localizations_fr.dart | 4 ++-- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 4 ++-- lib/l10n/app_localizations_pl.dart | 4 ++-- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 5 ++--- lib/l10n/app_localizations_sk.dart | 6 +++--- lib/l10n/app_localizations_sl.dart | 9 ++++---- lib/l10n/app_localizations_sv.dart | 5 +++-- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/l10n/app_nl.arb | 21 ++++++++++++++++++- lib/l10n/app_pl.arb | 21 ++++++++++++++++++- lib/l10n/app_pt.arb | 21 ++++++++++++++++++- lib/l10n/app_ru.arb | 21 ++++++++++++++++++- lib/l10n/app_sk.arb | 21 ++++++++++++++++++- lib/l10n/app_sl.arb | 21 ++++++++++++++++++- lib/l10n/app_sv.arb | 21 ++++++++++++++++++- lib/l10n/app_uk.arb | 21 ++++++++++++++++++- lib/l10n/app_zh.arb | 21 ++++++++++++++++++- macos/Flutter/GeneratedPluginRegistrant.swift | 2 -- 29 files changed, 314 insertions(+), 49 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 9733738..c2879dd 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?", "common_deleteAll": "Изтрий всичко", "map_guessedLocation": "Предполагано местоположение", - "map_showGuessedLocations": "Покажете местоположенията на предположените възли." + "map_showGuessedLocations": "Покажете местоположенията на предположените възли.", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "Свържете се чрез USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenSubtitle": "Изберете открития сериен уред и свържете директно към вашия MeshCore възел.", + "usbScreenStatus": "Изберете USB устройство", + "usbScreenNote": "USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.", + "usbScreenEmptyState": "Няма открити USB устройства. Включете едно и опитайте отново.", + "usbErrorPermissionDenied": "Не беше разрешено достъпът през USB.", + "usbErrorDeviceMissing": "Избраното USB устройство вече не е налично.", + "usbErrorInvalidPort": "Изберете валитно USB устройство.", + "usbErrorBusy": "Друг мол за свързване през USB вече е в процес на изпълнение.", + "usbErrorNotConnected": "Няма свързано USB устройство.", + "usbErrorOpenFailed": "Не успях да отворя избраното USB устройство.", + "usbErrorConnectFailed": "Не успях да се свържа с избраното USB устройство.", + "usbErrorUnsupported": "USB серийната комуникация не се поддържа на тази платформа.", + "usbErrorAlreadyActive": "USB връзката вече е активирана.", + "usbErrorNoDeviceSelected": "Няма избран USB устройство.", + "usbErrorPortClosed": "USB връзката не е активна.", + "usbErrorConnectTimedOut": "Изчаква се, но устройството не отговаря в рамките на зададения време." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index dc4cbfc..ad6b0bf 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1856,5 +1856,24 @@ "discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?", "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen", "map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen", - "map_guessedLocation": "Geschätzter Ort" + "map_guessedLocation": "Geschätzter Ort", + "usbScreenSubtitle": "Wählen Sie ein erkannten serielles Gerät aus und verbinden Sie es direkt mit Ihrem MeshCore-Knoten.", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "Verbinden über USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenStatus": "Wählen Sie ein USB-Gerät aus", + "usbScreenNote": "Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.", + "usbScreenEmptyState": "Keine USB-Geräte gefunden. Schließen Sie eines an und aktualisieren Sie.", + "usbErrorPermissionDenied": "Die USB-Berechtigung wurde abgelehnt.", + "usbErrorDeviceMissing": "Das ausgewählte USB-Gerät ist nicht mehr verfügbar.", + "usbErrorInvalidPort": "Wählen Sie ein gültiges USB-Gerät aus.", + "usbErrorBusy": "Eine weitere Anfrage für eine USB-Verbindung ist bereits in Bearbeitung.", + "usbErrorNotConnected": "Es ist kein USB-Gerät angeschlossen.", + "usbErrorOpenFailed": "Fehlgeschlagen beim Öffnen des ausgewählten USB-Geräts.", + "usbErrorConnectFailed": "Keine Verbindung zum ausgewählten USB-Gerät hergestellt.", + "usbErrorUnsupported": "Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.", + "usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.", + "usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.", + "usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.", + "usbErrorConnectTimedOut": "Wartezeit abgelaufen, da keine Antwort vom Gerät empfangen wurde." } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 5020bef..09545c7 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1856,5 +1856,24 @@ "discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos", "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!", "map_guessedLocation": "Ubicación estimada", - "map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos." + "map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos.", + "usbScreenTitle": "Conecte mediante USB", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenSubtitle": "Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.", + "usbScreenStatus": "Seleccione un dispositivo USB", + "usbScreenNote": "La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.", + "usbScreenEmptyState": "No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.", + "usbErrorPermissionDenied": "Se denegó el permiso de acceso a través de USB.", + "usbErrorDeviceMissing": "El dispositivo USB seleccionado ya no está disponible.", + "usbErrorInvalidPort": "Seleccione un dispositivo USB válido.", + "usbErrorBusy": "Ya se ha iniciado una solicitud de conexión USB adicional.", + "usbErrorNotConnected": "No hay ningún dispositivo USB conectado.", + "usbErrorOpenFailed": "No se pudo abrir el dispositivo USB seleccionado.", + "usbErrorConnectFailed": "No se pudo conectar con el dispositivo USB seleccionado.", + "usbErrorUnsupported": "La comunicación serial a través de USB no está soportada en esta plataforma.", + "usbErrorAlreadyActive": "La conexión USB ya está activa.", + "usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.", + "usbErrorPortClosed": "La conexión USB no está activa.", + "usbErrorConnectTimedOut": "Se ha agotado el tiempo de espera mientras se esperaba la respuesta del dispositivo." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7207fe8..f6750f8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts", "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?", "map_showGuessedLocations": "Afficher les emplacements des nœuds estimés", - "map_guessedLocation": "Lieu deviné" + "map_guessedLocation": "Lieu deviné", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "Connectez via USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.", + "usbScreenStatus": "Sélectionnez un périphérique USB", + "usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.", + "usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.", + "usbErrorPermissionDenied": "L'accès via USB a été refusé.", + "usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.", + "usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.", + "usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.", + "usbErrorNotConnected": "Aucun appareil USB n'est connecté.", + "usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.", + "usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.", + "usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.", + "usbErrorAlreadyActive": "Une connexion USB est déjà établie.", + "usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.", + "usbErrorPortClosed": "La connexion USB n'est pas établie.", + "usbErrorConnectTimedOut": "Attente avec délai, en attendant une réponse de l'appareil." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index f3f3f53..b4e5c14 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?", "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti", "map_guessedLocation": "Località indovinata", - "map_showGuessedLocations": "Mostra le posizioni stimate dei nodi" + "map_showGuessedLocations": "Mostra le posizioni stimate dei nodi", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenSubtitle": "Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "Connessione tramite USB", + "usbScreenStatus": "Seleziona un dispositivo USB", + "usbScreenNote": "La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.", + "usbScreenEmptyState": "Nessun dispositivo USB rilevato. Collegare uno e aggiornare.", + "usbErrorPermissionDenied": "È stato negato l'accesso tramite USB.", + "usbErrorDeviceMissing": "Il dispositivo USB selezionato non è più disponibile.", + "usbErrorInvalidPort": "Seleziona un dispositivo USB valido.", + "usbErrorBusy": "Un'altra richiesta di connessione tramite USB è già in corso.", + "usbErrorNotConnected": "Non è collegato alcun dispositivo USB.", + "usbErrorOpenFailed": "Impossibile aprire il dispositivo USB selezionato.", + "usbErrorConnectFailed": "Impossibile connettersi al dispositivo USB selezionato.", + "usbErrorUnsupported": "La comunicazione seriale tramite USB non è supportata su questa piattaforma.", + "usbErrorAlreadyActive": "La connessione USB è già attiva.", + "usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.", + "usbErrorPortClosed": "La connessione USB non è attiva.", + "usbErrorConnectTimedOut": "Attesa superata, in attesa di una risposta dal dispositivo." } diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 5720b77..cb4739a 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -118,11 +118,11 @@ class AppLocalizationsBg extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Свързване чрез USB'; + String get usbScreenTitle => 'Свържете се чрез USB'; @override String get usbScreenSubtitle => - 'Изберете открит сериен уред и се свържете директно към вашия MeshCore възел.'; + 'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.'; @override String get usbScreenStatus => 'Изберете USB устройство'; @@ -143,7 +143,7 @@ class AppLocalizationsBg extends AppLocalizations { 'Избраното USB устройство вече не е налично.'; @override - String get usbErrorInvalidPort => 'Изберете валидно USB устройство.'; + String get usbErrorInvalidPort => 'Изберете валитно USB устройство.'; @override String get usbErrorBusy => diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index afe91e0..ad7dd84 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -118,7 +118,7 @@ class AppLocalizationsDe extends AppLocalizations { String get connectionChoiceBluetoothLabel => 'Bluetooth'; @override - String get usbScreenTitle => 'Über USB verbinden'; + String get usbScreenTitle => 'Verbinden über USB'; @override String get usbScreenSubtitle => @@ -129,7 +129,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get usbScreenNote => - 'USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; + 'Die USB-Serielle Schnittstelle ist auf unterstützten Android-Geräten und Desktop-Plattformen aktiv.'; @override String get usbScreenEmptyState => @@ -163,7 +163,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get usbErrorUnsupported => - 'Die Unterstützung für USB-Seriellschnittstellen ist auf dieser Plattform nicht vorhanden.'; + 'Die USB-Serielle Schnittstelle wird auf dieser Plattform nicht unterstützt.'; @override String get usbErrorAlreadyActive => @@ -177,7 +177,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Die Wartezeit ist abgelaufen, da keine Antwort vom Gerät empfangen wurde.'; + 'Wartezeit abgelaufen, da keine Antwort vom Gerät empfangen wurde.'; @override String get scanner_scanning => 'Scannen nach Geräten...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8d591e3..60b0936 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -122,7 +122,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbScreenSubtitle => - 'Seleccione un dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.'; + 'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.'; @override String get usbScreenStatus => 'Seleccione un dispositivo USB'; @@ -133,7 +133,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbScreenEmptyState => - 'No se encontraron dispositivos USB. Conecte uno y vuelva a cargar.'; + 'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.'; @override String get usbErrorPermissionDenied => @@ -163,7 +163,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbErrorUnsupported => - 'La comunicación serial mediante USB no está soportada en esta plataforma.'; + 'La comunicación serial a través de USB no está soportada en esta plataforma.'; @override String get usbErrorAlreadyActive => 'La conexión USB ya está activa.'; @@ -177,7 +177,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Se ha producido un error debido a la espera prolongada para recibir una respuesta del dispositivo.'; + 'Se ha agotado el tiempo de espera mientras se esperaba la respuesta del dispositivo.'; @override String get scanner_scanning => 'Escaneando dispositivos...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6f9a4b8..fdeee92 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -129,11 +129,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get usbScreenNote => - 'La communication série USB est active sur les appareils Android et les plateformes de bureau pris en charge.'; + 'La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.'; @override String get usbScreenEmptyState => - 'Aucun périphérique USB n\'a été trouvé. Veuillez connecter un périphérique et rafraîchir la page.'; + 'Aucun périphérique USB n\'a été trouvé. Veuillez en brancher un et rafraîchir la page.'; @override String get usbErrorPermissionDenied => 'L\'accès via USB a été refusé.'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 1cea4f6..a5ae362 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -133,7 +133,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String get usbScreenEmptyState => - 'Nessun dispositivo USB rilevato. Collegare uno e riavviare.'; + 'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.'; @override String get usbErrorPermissionDenied => diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index dbd3290..a437106 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -122,7 +122,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get usbScreenSubtitle => - 'Kies een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; + 'Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.'; @override String get usbScreenStatus => 'Selecteer een USB-apparaat'; @@ -175,7 +175,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Wachtperiode is verlopen, er is geen reactie ontvangen van het apparaat.'; + 'Wachtperiode is verlopen, aangezien het apparaat niet reageerde.'; @override String get scanner_scanning => 'Scannen naar apparaten...'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 930a6a1..2f034b9 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -129,7 +129,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get usbScreenNote => - 'Port szeregowy USB jest aktywny na urządzeniach z Androidem i platformach stacjonarnych, które obsługują tę funkcję.'; + 'Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.'; @override String get usbScreenEmptyState => @@ -159,7 +159,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String get usbErrorConnectFailed => - 'Nie udało się nawiązać połączenia z wybranym urządzeniem USB.'; + 'Nie udało się połączyć z wybranym urządzeniem USB.'; @override String get usbErrorUnsupported => diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 50c0a35..7795e53 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -122,7 +122,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get usbScreenSubtitle => - 'Selecione o dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.'; + 'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.'; @override String get usbScreenStatus => 'Selecione um dispositivo USB'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index cec7a97..b58ae8d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -151,7 +151,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Еще одно запрошенное соединение через USB уже находится в процессе.'; @override - String get usbErrorNotConnected => 'Ни одно устройство USB не подключено.'; + String get usbErrorNotConnected => 'Ни одно USB-устройство не подключено.'; @override String get usbErrorOpenFailed => @@ -166,8 +166,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Поддержка последовательного USB отсутствует на данной платформе.'; @override - String get usbErrorAlreadyActive => - 'USB-соединение уже установлено и работает.'; + String get usbErrorAlreadyActive => 'USB-соединение уже установлено.'; @override String get usbErrorNoDeviceSelected => diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index a8db3ad..403a612 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -148,7 +148,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get usbErrorBusy => - 'Ďalšia požiadavka na pripojenie cez USB je aktuálne v prebiehajúcom procese.'; + 'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.'; @override String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.'; @@ -159,7 +159,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get usbErrorConnectFailed => - 'Nezvládlo sa pripojenie k vybranému USB zariadeniu.'; + 'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.'; @override String get usbErrorUnsupported => @@ -177,7 +177,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Čakal som, kým sa zariadenie neozvými, ale časový limit sa dosiahol.'; + 'Čakal som, kým sa zariadenie neozvými, ale časový limit sa dobehol.'; @override String get scanner_scanning => 'Skrívania zariadení...'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 8c501d3..db9368f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -125,7 +125,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.'; @override - String get usbScreenStatus => 'Izberite USB naprave.'; + String get usbScreenStatus => 'Izberite USB naprave'; @override String get usbScreenNote => @@ -140,7 +140,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.'; @override - String get usbErrorDeviceMissing => 'Izbrani USB napravega več ni na voljo.'; + String get usbErrorDeviceMissing => 'Izbrani USB napravej je več ne.'; @override String get usbErrorInvalidPort => 'Izberite veljavno USB naprave.'; @@ -149,10 +149,11 @@ class AppLocalizationsSl extends AppLocalizations { String get usbErrorBusy => 'Že je v teku zahteva za povezavo preko USB.'; @override - String get usbErrorNotConnected => 'Ni priklopljenih USB naprav.'; + String get usbErrorNotConnected => 'Ni priklopljenih USB naprave.'; @override - String get usbErrorOpenFailed => 'Niso uspeli odkriti izbrane USB naprave.'; + String get usbErrorOpenFailed => + 'Uspešno ni bilo mogo, da se odpre izbran naprave USB.'; @override String get usbErrorConnectFailed => diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6cff1ec..d223cd6 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -129,7 +129,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbScreenNote => - 'USB-seriell kommunikation är aktiv på stödda Android-enheter och skrivbordsplattformar.'; + 'USB-seriell kommunikation är aktiv på stöderliga Android-enheter och på skrivbordsplattformar.'; @override String get usbScreenEmptyState => @@ -153,7 +153,8 @@ class AppLocalizationsSv extends AppLocalizations { String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.'; @override - String get usbErrorOpenFailed => 'Kunde inte öppna det valda USB-enheten.'; + String get usbErrorOpenFailed => + 'Misslyckades med att öppna det valda USB-enheten.'; @override String get usbErrorConnectFailed => diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 7b5901f..676e941 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -129,7 +129,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get usbScreenNote => - 'USB-серіальний порт активний на підтримуваних пристроях на базі Android та на десктопних платформах.'; + 'USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.'; @override String get usbScreenEmptyState => diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9e26c50..ff42929 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -127,7 +127,7 @@ class AppLocalizationsZh extends AppLocalizations { String get usbScreenStatus => '选择一个 USB 设备'; @override - String get usbScreenNote => '在支持的 Android 设备和桌面平台上,USB 串行通信功能已启用。'; + String get usbScreenNote => 'USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。'; @override String get usbScreenEmptyState => '未找到任何 USB 设备。请插入一个,然后刷新。'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index cde7457..7d56216 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten", "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?", "map_guessedLocation": "Geroerde locatie", - "map_showGuessedLocations": "Toon de voorspelde locaties van de knopen" + "map_showGuessedLocations": "Toon de voorspelde locaties van de knopen", + "connectionChoiceUsbLabel": "USB", + "usbScreenSubtitle": "Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenTitle": "Verbind via USB", + "usbScreenStatus": "Selecteer een USB-apparaat", + "usbScreenNote": "USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.", + "usbScreenEmptyState": "Geen USB-apparaten gevonden. Sluit er een aan en herlaad.", + "usbErrorPermissionDenied": "Toegang via USB is geweigerd.", + "usbErrorDeviceMissing": "Het geselecteerde USB-apparaat is niet meer beschikbaar.", + "usbErrorInvalidPort": "Selecteer een geldig USB-apparaat.", + "usbErrorBusy": "Een andere verzoek om een USB-verbinding is al in behandeling.", + "usbErrorNotConnected": "Er is geen USB-apparaat aangesloten.", + "usbErrorOpenFailed": "Kon het geselecteerde USB-apparaat niet openen.", + "usbErrorConnectFailed": "Kon niet verbinding maken met het geselecteerde USB-apparaat.", + "usbErrorUnsupported": "USB-serieel is niet ondersteund op deze platform.", + "usbErrorAlreadyActive": "Een USB-verbinding is al actief.", + "usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.", + "usbErrorPortClosed": "De USB-verbinding is niet actief.", + "usbErrorConnectTimedOut": "Wachtperiode is verlopen, aangezien het apparaat niet reageerde." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 41152a5..f6cd0be 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?", "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty", "map_guessedLocation": "Wydana lokalizacja", - "map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów" + "map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów", + "usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.", + "usbScreenTitle": "Połącz przez USB", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenStatus": "Wybierz urządzenie USB", + "usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.", + "usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.", + "usbErrorPermissionDenied": "Zostało odrzucone żądanie dostępu przez USB.", + "usbErrorDeviceMissing": "Wybór urządzenia USB już nie jest dostępny.", + "usbErrorInvalidPort": "Wybierz prawidłowe urządzenie USB.", + "usbErrorBusy": "Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.", + "usbErrorNotConnected": "Brak podłączonego urządzenia USB.", + "usbErrorOpenFailed": "Nie udało się otworzyć wybranego urządzenia USB.", + "usbErrorConnectFailed": "Nie udało się połączyć z wybranym urządzeniem USB.", + "usbErrorUnsupported": "Port szeregowy USB nie jest obsługiwany na tym urządzeniu.", + "usbErrorAlreadyActive": "Połączenie USB jest już aktywne.", + "usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.", + "usbErrorPortClosed": "Połączenie USB nie jest aktywne.", + "usbErrorConnectTimedOut": "Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index f843119..77bc5c7 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos", "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?", "map_guessedLocation": "Localização estimada", - "map_showGuessedLocations": "Mostrar as localizações dos nós estimados" + "map_showGuessedLocations": "Mostrar as localizações dos nós estimados", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenTitle": "Conecte via USB", + "usbScreenSubtitle": "Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.", + "usbScreenStatus": "Selecione um dispositivo USB", + "usbScreenNote": "A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.", + "usbScreenEmptyState": "Nenhum dispositivo USB encontrado. Conecte um e atualize.", + "usbErrorPermissionDenied": "A permissão para acesso via USB foi negada.", + "usbErrorDeviceMissing": "O dispositivo USB selecionado não está mais disponível.", + "usbErrorInvalidPort": "Selecione um dispositivo USB válido.", + "usbErrorBusy": "Já existe uma solicitação de conexão USB em andamento.", + "usbErrorNotConnected": "Não há nenhum dispositivo USB conectado.", + "usbErrorOpenFailed": "Não foi possível abrir o dispositivo USB selecionado.", + "usbErrorConnectFailed": "Não foi possível conectar ao dispositivo USB selecionado.", + "usbErrorUnsupported": "A comunicação serial via USB não é suportada nesta plataforma.", + "usbErrorAlreadyActive": "A conexão USB já está ativa.", + "usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.", + "usbErrorPortClosed": "A conexão USB não está ativa.", + "usbErrorConnectTimedOut": "Tempo limite aguardando a resposta do dispositivo." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0897cba..319496a 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1068,5 +1068,24 @@ "discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?", "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты", "map_guessedLocation": "Угаданное место", - "map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов" + "map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов", + "connectionChoiceUsbLabel": "USB", + "usbScreenSubtitle": "Выберите обнаруженное устройство с последовательным интерфейсом и подключите его напрямую к вашему узлу MeshCore.", + "usbScreenTitle": "Подключение через USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenStatus": "Выберите USB-устройство", + "usbScreenNote": "USB-серийный порт активен на поддерживаемых устройствах Android и на настольных платформах.", + "usbScreenEmptyState": "Не обнаружено устройств USB. Подключите одно из них и обновите список.", + "usbErrorPermissionDenied": "Запрос на доступ через USB был отклонен.", + "usbErrorDeviceMissing": "Выбранное USB-устройство больше недоступно.", + "usbErrorInvalidPort": "Выберите действительное USB-устройство.", + "usbErrorBusy": "Еще одно запрошенное соединение через USB уже находится в процессе.", + "usbErrorNotConnected": "Ни одно USB-устройство не подключено.", + "usbErrorOpenFailed": "Не удалось открыть выбранное USB-устройство.", + "usbErrorConnectFailed": "Не удалось установить соединение с выбранным USB-устройством.", + "usbErrorUnsupported": "Поддержка последовательного USB отсутствует на данной платформе.", + "usbErrorAlreadyActive": "USB-соединение уже установлено.", + "usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.", + "usbErrorPortClosed": "USB-соединение не установлено.", + "usbErrorConnectTimedOut": "Ожидание ответа от устройства превысило установленное время." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 16aae9b..571d2f2 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1828,5 +1828,24 @@ "common_deleteAll": "Zmazať všetko", "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?", "map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov", - "map_guessedLocation": "Odhadnutá lokalita" + "map_guessedLocation": "Odhadnutá lokalita", + "usbScreenTitle": "Pripojte cez USB", + "usbScreenSubtitle": "Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.", + "connectionChoiceUsbLabel": "USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "usbScreenStatus": "Vyberte USB zariadenie", + "usbScreenNote": "USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.", + "usbScreenEmptyState": "Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.", + "usbErrorPermissionDenied": "Žiadosť o prístup cez USB bola zamietnutá.", + "usbErrorDeviceMissing": "Vybrané USB zariadenie už nie je dostupné.", + "usbErrorInvalidPort": "Vyberte platné USB zariadenie.", + "usbErrorBusy": "Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.", + "usbErrorNotConnected": "Nie je pripojené žiadne USB zariadenie.", + "usbErrorOpenFailed": "Nepodarilo sa otvoriť vybrané USB zariadenie.", + "usbErrorConnectFailed": "Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.", + "usbErrorUnsupported": "Podpora USB sériového rozhrania nie je na tejto platforme dostupná.", + "usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.", + "usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.", + "usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.", + "usbErrorConnectTimedOut": "Čakal som, kým sa zariadenie neozvými, ale časový limit sa dobehol." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 97d913f..631e75e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?", "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte", "map_guessedLocation": "Predpostavljena lokacija", - "map_showGuessedLocations": "Pokaži lokacije domnevnih not." + "map_showGuessedLocations": "Pokaži lokacije domnevnih not.", + "usbScreenTitle": "Povežite preko USB", + "usbScreenSubtitle": "Izberite zaznano serijsko napravo in se neposredno povežite z vašo MeshCore napravo.", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "usbScreenStatus": "Izberite USB naprave", + "usbScreenNote": "USB serijska povezava je aktivna na podprtih napravah Android in na desktop platformah.", + "usbScreenEmptyState": "Niti en USB naprave niso najdeni. Povežite eno in posodobite.", + "usbErrorPermissionDenied": "Dovoljenje za dostop preko USB-ja je bilo zavrnjeno.", + "usbErrorDeviceMissing": "Izbrani USB napravej je več ne.", + "usbErrorInvalidPort": "Izberite veljavno USB naprave.", + "usbErrorBusy": "Že je v teku zahteva za povezavo preko USB.", + "usbErrorNotConnected": "Ni priklopljenih USB naprave.", + "usbErrorOpenFailed": "Uspešno ni bilo mogo, da se odpre izbran naprave USB.", + "usbErrorConnectFailed": "Niso bilo mogoče uskladiti povezave z izbranim USB napom.", + "usbErrorUnsupported": "USB serijska komunikacija ni podprta na tej platformi.", + "usbErrorAlreadyActive": "USB povezava je že aktivirana.", + "usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.", + "usbErrorPortClosed": "USB povezava ni aktivirana.", + "usbErrorConnectTimedOut": "Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b451082..be1ada8 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?", "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter", "map_guessedLocation": "Gissad plats", - "map_showGuessedLocations": "Visa upp de antagna nodernas placeringar" + "map_showGuessedLocations": "Visa upp de antagna nodernas placeringar", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", + "usbScreenTitle": "Anslut via USB", + "usbScreenStatus": "Välj en USB-enhet", + "usbScreenNote": "USB-seriell kommunikation är aktiv på stöderliga Android-enheter och på skrivbordsplattformar.", + "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.", + "usbErrorPermissionDenied": "Tillgången via USB nekas.", + "usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.", + "usbErrorInvalidPort": "Välj en giltig USB-enhet.", + "usbErrorBusy": "En annan förfrågan om USB-anslutning är redan pågående.", + "usbErrorNotConnected": "Ingen USB-enhet är ansluten.", + "usbErrorOpenFailed": "Misslyckades med att öppna det valda USB-enheten.", + "usbErrorConnectFailed": "Kunde inte ansluta till det valda USB-enheten.", + "usbErrorUnsupported": "USB-seriell kommunikation stöds inte på denna plattform.", + "usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.", + "usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.", + "usbErrorPortClosed": "USB-anslutningen är inte aktiv.", + "usbErrorConnectTimedOut": "Tiden har löpt ut medan vi väntade på att enheten skulle svara." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index fb60b81..76ac380 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1828,5 +1828,24 @@ "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?", "map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів", - "map_guessedLocation": "Визначено місцезнаходження" + "map_guessedLocation": "Визначено місцезнаходження", + "usbScreenSubtitle": "Виберіть виявлене серійне пристрій і підключіть його безпосередньо до вашого вузла MeshCore.", + "usbScreenTitle": "Підключити через USB", + "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceUsbLabel": "USB", + "usbScreenStatus": "Виберіть пристрій USB", + "usbScreenNote": "USB-серіальний інтерфейс активний на підтримуваних пристроях на базі Android та на десктопних платформах.", + "usbScreenEmptyState": "Не знайдено жодних пристроїв USB. Підключіть один і перезавантажте.", + "usbErrorPermissionDenied": "Було відмовлено у наданні дозволу на використання USB.", + "usbErrorDeviceMissing": "Вибране USB-пристрій більше недоступне.", + "usbErrorInvalidPort": "Виберіть дійсний USB-пристрій.", + "usbErrorBusy": "Ще один запит на підключення через USB вже обробляється.", + "usbErrorNotConnected": "Немає підключених пристроїв USB.", + "usbErrorOpenFailed": "Не вдалося відкрити вибране USB-пристрій.", + "usbErrorConnectFailed": "Не вдалося підключитися до вибраного USB-пристрою.", + "usbErrorUnsupported": "Підтримка USB-серіального інтерфейсу не реалізована на цій платформі.", + "usbErrorAlreadyActive": "USB-з'єднання вже встановлено.", + "usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.", + "usbErrorPortClosed": "З'єднання USB не встановлено.", + "usbErrorConnectTimedOut": "Час очікування закінчився, оскільки пристрій не відповів." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 51bd60c..76893c9 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1833,5 +1833,24 @@ "discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?", "discoveredContacts_deleteContactAll": "删除所有发现的联系人", "map_showGuessedLocations": "显示猜测的节点位置", - "map_guessedLocation": "猜测的位置" + "map_guessedLocation": "猜测的位置", + "connectionChoiceUsbLabel": "USB", + "usbScreenTitle": "通过USB连接", + "usbScreenSubtitle": "选择已检测到的串行设备,并直接连接到您的 MeshCore 节点。", + "connectionChoiceBluetoothLabel": "蓝牙", + "usbScreenStatus": "选择一个 USB 设备", + "usbScreenNote": "USB 串行接口在支持的 Android 设备和桌面平台上处于活动状态。", + "usbScreenEmptyState": "未找到任何 USB 设备。请插入一个,然后刷新。", + "usbErrorPermissionDenied": "拒绝了USB权限。", + "usbErrorDeviceMissing": "所选的USB设备已不再可用。", + "usbErrorInvalidPort": "选择一个有效的USB设备。", + "usbErrorBusy": "还有一个 USB 连接请求正在进行中。", + "usbErrorNotConnected": "没有连接任何USB设备。", + "usbErrorOpenFailed": "未能打开所选的USB设备。", + "usbErrorConnectFailed": "未能连接到所选的USB设备。", + "usbErrorUnsupported": "此平台不支持USB串行通信。", + "usbErrorAlreadyActive": "USB 连接已建立。", + "usbErrorNoDeviceSelected": "未选择任何 USB 设备。", + "usbErrorPortClosed": "USB 连接未建立。", + "usbErrorConnectTimedOut": "等待设备响应超时。" } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4084d9b..d2ea57e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus -import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -21,7 +20,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From c2671ac2aea8edcf5e44f12cfd070a851e9096eb Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 7 Mar 2026 01:23:46 -0800 Subject: [PATCH 277/421] Refactor data handling of contacts (#267) * Refactor data handling in MeshCoreConnector and BufferReader for improved readability and efficiency * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix pointer tracking in BufferReader's readCString method --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 53 +++++++++++++++------------ lib/connector/meshcore_protocol.dart | 28 +++++++++++++- lib/models/contact.dart | 39 ++++++++++---------- 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index c00b4ca..75b5287 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -3820,40 +3820,40 @@ class MeshCoreConnector extends ChangeNotifier { void _handleRxData(Uint8List frame) { final packet = BufferReader(frame); - double snr = 0.0; - int routeType = 0; - int payloadType = 0; - Uint8List pathBytes = Uint8List(0); - Uint8List payload = Uint8List(0); try { packet.skipBytes(1); // Skip frame type byte - snr = packet.readInt8() / 4.0; + final snr = packet.readInt8() / 4.0; packet.skipBytes(1); // Skip RSSI byte //final rssi = packet.readByte(); final header = packet.readByte(); - routeType = header & 0x03; - payloadType = (header >> 2) & 0x0F; + final routeType = header & 0x03; + final payloadType = (header >> 2) & 0x0F; + if (routeType == _routeTransportFlood || + routeType == _routeTransportDirect) { + packet.skipBytes(4); // Skip transport-specific bytes + } //final payloadVer = (header >> 6) & 0x03; final pathLen = packet.readByte(); - pathBytes = packet.readBytes(pathLen); - payload = packet.readBytes(packet.remaining); + final pathBytes = packet.readBytes(pathLen); + final payload = packet.readBytes(packet.remaining); + + final rawPacket = frame.sublist(3); + switch (payloadType) { + case payloadTypeADVERT: + _handlePayloadAdvertReceived( + rawPacket, + payload, + pathBytes, + routeType, + snr, + ); + break; + default: + } } catch (e) { appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); return; } - final rawPacket = frame.sublist(3); - switch (payloadType) { - case payloadTypeADVERT: - _handlePayloadAdvertReceived( - rawPacket, - payload, - pathBytes, - routeType, - snr, - ); - break; - default: - } } void importContact(Uint8List frame) { @@ -3865,7 +3865,12 @@ class MeshCoreConnector extends ChangeNotifier { packet.skipBytes(1); // Skip SNR byte packet.skipBytes(1); // Skip RSSI byte final header = packet.readByte(); + final routeType = header & 0x03; payloadType = (header >> 2) & 0x0F; + if (routeType == _routeTransportFlood || + routeType == _routeTransportDirect) { + packet.skipBytes(4); // Skip transport-specific bytes + } //final payloadVer = (header >> 6) & 0x03; final pathLen = packet.readByte(); pathBytes = packet.readBytes(pathLen); @@ -3915,7 +3920,7 @@ class MeshCoreConnector extends ChangeNotifier { publicKey: publicKey, name: name, type: type, - pathLength: pathBytes.length, + pathLength: pathBytes.isEmpty ? -1 : pathBytes.length, path: Uint8List.fromList( pathBytes.reversed.toList(), ), // Store path in reverse for easier use in outgoing messages diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 58abf6f..3484d47 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; // Buffer Reader - sequential binary data reader with pointer tracking class BufferReader { int _pointer = 0; + int _lastPointer = 0; final Uint8List _buffer; BufferReader(Uint8List data) : _buffer = Uint8List.fromList(data); @@ -13,6 +14,7 @@ class BufferReader { int readByte() => readBytes(1)[0]; Uint8List readBytes(int count) { + _lastPointer = _pointer; if (_pointer + count > _buffer.length) { throw RangeError( 'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}', @@ -24,6 +26,7 @@ class BufferReader { } void skipBytes(int count) { + _lastPointer = _pointer; if (_pointer + count > _buffer.length) { throw RangeError( 'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}', @@ -35,6 +38,7 @@ class BufferReader { Uint8List readRemainingBytes() => readBytes(remaining); String readString() { + _lastPointer = _pointer; final value = readRemainingBytes(); try { return utf8.decode(Uint8List.fromList(value), allowMalformed: true); @@ -43,7 +47,8 @@ class BufferReader { } } - String readCString(int maxLength) { + String readCStringGreedy(int maxLength) { + _lastPointer = _pointer; final value = []; final bytes = readBytes(maxLength); for (final byte in bytes) { @@ -57,6 +62,24 @@ class BufferReader { } } + String readCString(int maxLength) { + final backupPointer = _pointer; + final value = []; + int counter = 0; + while (counter < maxLength) { + final byte = readByte(); + if (byte == 0) break; + value.add(byte); + counter++; + } + _lastPointer = backupPointer; + try { + return utf8.decode(Uint8List.fromList(value), allowMalformed: true); + } catch (e) { + return String.fromCharCodes(value); // Latin-1 fallback + } + } + int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0); int readInt8() => readBytes(1).buffer.asByteData().getInt8(0); int readUInt16LE() => @@ -78,6 +101,9 @@ class BufferReader { if ((value & 0x800000) != 0) value -= 0x1000000; return value; } + + void resetPointer() => _pointer = 0; + void rewind() => _pointer = _lastPointer; } // Buffer Writer - accumulating binary data builder diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 7d8e011..b4acff7 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -1,4 +1,6 @@ import 'dart:typed_data'; +import 'package:meshcore_open/utils/app_logger.dart'; + import '../connector/meshcore_protocol.dart'; class Contact { @@ -166,28 +168,27 @@ class Contact { static Contact? fromFrame(Uint8List data) { if (data.isEmpty) return null; - if (data[0] != respCodeContact) return null; + final reader = BufferReader(data); try { - final pubKey = Uint8List.fromList( - data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), - ); - final type = data[contactTypeOffset]; - final flags = data[contactFlagsOffset]; - final pathLen = data[contactPathLenOffset].toSigned(8); + final respCode = reader.readByte(); + if (respCode != respCodeContact && respCode != pushCodeNewAdvert) { + return null; + } + final pubKey = reader.readBytes(pubKeySize); + final type = reader.readByte(); + final flags = reader.readByte(); + final pathLen = reader.readByte(); final safePathLen = pathLen > 0 ? (pathLen > maxPathSize ? maxPathSize : pathLen) : 0; - final pathBytes = safePathLen > 0 - ? Uint8List.fromList( - data.sublist(contactPathOffset, contactPathOffset + safePathLen), - ) - : Uint8List(0); - final name = readCString(data, contactNameOffset, maxNameSize); - final lastmod = readUint32LE(data, contactLastModOffset); + final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen); + final name = reader.readCStringGreedy(maxNameSize); + + final lastMod = reader.readUInt32LE(); double? lat, lon; - final latRaw = readInt32LE(data, contactLatOffset); - final lonRaw = readInt32LE(data, contactLonOffset); + final latRaw = reader.readInt32LE(); + final lonRaw = reader.readInt32LE(); if (latRaw != 0 || lonRaw != 0) { lat = latRaw / 1e6; lon = lonRaw / 1e6; @@ -198,14 +199,14 @@ class Contact { name: name.isEmpty ? 'Unknown' : name, type: type, flags: flags, - pathLength: pathLen, + pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1, path: pathBytes, latitude: lat, longitude: lon, - lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000), + lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000), ); } catch (e) { - // If parsing fails, return null + appLogger.error('Failed to parse contact frame: $e'); return null; } } From b748b96237679f450049229ac5a1e985fdfa8aec Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 7 Mar 2026 01:45:53 -0800 Subject: [PATCH 278/421] Enhance contact handling logic in MeshCoreConnector to support conditional addition based on auto-add settings (#268) --- lib/connector/meshcore_connector.dart | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 75b5287..7aa0e5d 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1926,7 +1926,7 @@ class MeshCoreConnector extends ChangeNotifier { case pushCodeNewAdvert: debugPrint('Got New CONTACT'); // It's the same format as respCodeContact, so we can reuse the handler - _handleContact(frame); + _handleContact(frame, isContact: false); break; case respCodeContact: debugPrint('Got CONTACT'); @@ -2217,7 +2217,7 @@ class MeshCoreConnector extends ChangeNotifier { } } - void _handleContact(Uint8List frame) { + void _handleContact(Uint8List frame, {bool isContact = true}) { final contact = Contact.fromFrame(frame); if (contact != null) { if (contact.type == advTypeRepeater) { @@ -2256,11 +2256,23 @@ class MeshCoreConnector extends ChangeNotifier { tag: 'Connector', ); } else { - _contacts.add(contact); - appLogger.info( - 'Added new contact ${contact.name}: pathLen=${contact.pathLength}', - tag: 'Connector', - ); + if ((_autoAddUsers && contact.type == advTypeChat) || + (_autoAddRepeaters && contact.type == advTypeRepeater) || + (_autoAddRoomServers && contact.type == advTypeRoom) || + (_autoAddSensors && contact.type == advTypeSensor) || + isContact) { + _contacts.add(contact); + appLogger.info( + 'Added new contact ${contact.name}: pathLen=${contact.pathLength}', + tag: 'Connector', + ); + } else { + appLogger.info( + "Discovered contact ${contact.name} (type ${contact.typeLabel}) not added due to auto-add settings", + tag: 'Connector', + ); + return; + } } _knownContactKeys.add(contact.publicKeyHex); _loadMessagesForContact(contact.publicKeyHex); From 84ec139ce698ddb9e5ce6f3fbac9b31b50ed4800 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 7 Mar 2026 11:02:47 -0800 Subject: [PATCH 279/421] Add latitude and longitude fields to contact handling in MeshCoreConnector --- lib/connector/meshcore_connector.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7aa0e5d..af18b2c 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1572,6 +1572,8 @@ class MeshCoreConnector extends ChangeNotifier { type: contact.type, pathLength: contact.pathLength, path: contact.path, + latitude: contact.latitude, + longitude: contact.longitude, lastSeen: DateTime.now(), ), ); @@ -3890,8 +3892,8 @@ class MeshCoreConnector extends ChangeNotifier { appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); return; } - double latitude = 0.0; - double longitude = 0.0; + double? latitude; + double? longitude; String name = ''; Uint8List publicKey = Uint8List(0); int type = 0; @@ -3951,8 +3953,8 @@ class MeshCoreConnector extends ChangeNotifier { double snr, ) { final advert = BufferReader(payload); - double latitude = 0.0; - double longitude = 0.0; + double? latitude; + double? longitude; String name = ''; String contactKeyHex = ''; Uint8List publicKey = Uint8List(0); From fef73b7b62caeb31d4e214a41094474aac9822eb Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 7 Mar 2026 12:38:28 -0700 Subject: [PATCH 280/421] Refactor USB screen, add debug logging, fix UI issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite UsbScreen to mirror ScannerScreen patterns (status bar, tap-to-connect port list, bottom FABs, SnackBar errors) - Extract MeshCoreUsbManager from MeshCoreConnector for cleaner USB transport ownership - Add debug logging throughout USB connection flow (connector, manager, web/native services) - Print debug logs to console in debug mode even when app debug log setting is disabled - Localize remaining hardcoded strings (Web Serial Device fallback label, USB status bar keys, companion firmware timeout hint) - Fix Swedish misspelling in translations (stöderliga → stödda) - Guard Linux notification init against missing D-Bus session bus - Fix SNRIndicator hit-test error by adding minimum size constraints - Update USB flow tests for new UI patterns --- .../meshcore_open/MeshcoreUsbFunctions.kt | 41 +- lib/connector/meshcore_connector.dart | 83 ++- lib/connector/meshcore_connector_usb.dart | 77 ++- lib/l10n/app_bg.arb | 14 +- lib/l10n/app_de.arb | 14 +- lib/l10n/app_en.arb | 14 +- lib/l10n/app_es.arb | 14 +- lib/l10n/app_fr.arb | 14 +- lib/l10n/app_it.arb | 14 +- lib/l10n/app_localizations.dart | 32 +- lib/l10n/app_localizations_bg.dart | 20 +- lib/l10n/app_localizations_de.dart | 19 +- lib/l10n/app_localizations_en.dart | 19 +- lib/l10n/app_localizations_es.dart | 19 +- lib/l10n/app_localizations_fr.dart | 20 +- lib/l10n/app_localizations_it.dart | 20 +- lib/l10n/app_localizations_nl.dart | 19 +- lib/l10n/app_localizations_pl.dart | 20 +- lib/l10n/app_localizations_pt.dart | 19 +- lib/l10n/app_localizations_ru.dart | 20 +- lib/l10n/app_localizations_sk.dart | 19 +- lib/l10n/app_localizations_sl.dart | 20 +- lib/l10n/app_localizations_sv.dart | 21 +- lib/l10n/app_localizations_uk.dart | 20 +- lib/l10n/app_localizations_zh.dart | 19 +- lib/l10n/app_nl.arb | 14 +- lib/l10n/app_pl.arb | 14 +- lib/l10n/app_pt.arb | 14 +- lib/l10n/app_ru.arb | 14 +- lib/l10n/app_sk.arb | 14 +- lib/l10n/app_sl.arb | 14 +- lib/l10n/app_sv.arb | 16 +- lib/l10n/app_uk.arb | 14 +- lib/l10n/app_zh.arb | 14 +- lib/screens/usb_screen.dart | 625 +++++++----------- lib/services/app_debug_log_service.dart | 7 +- lib/services/notification_service.dart | 21 + lib/services/usb_serial_service_native.dart | 4 + lib/services/usb_serial_service_web.dart | 26 + lib/utils/usb_port_labels.dart | 3 +- lib/widgets/snr_indicator.dart | 62 +- test/screens/usb_flow_test.dart | 47 +- 42 files changed, 981 insertions(+), 553 deletions(-) diff --git a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt index 52e7650..279ba8a 100644 --- a/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt +++ b/android/app/src/main/kotlin/com/meshcore/meshcore_open/MeshcoreUsbFunctions.kt @@ -40,15 +40,15 @@ class MeshcoreUsbFunctions( private val mainHandler = Handler(Looper.getMainLooper()) private val usbIoExecutor: ExecutorService = Executors.newSingleThreadExecutor() - private var eventSink: EventChannel.EventSink? = null - private var usbConnection: UsbDeviceConnection? = null - private var usbInEndpoint: UsbEndpoint? = null - private var usbOutEndpoint: UsbEndpoint? = null - private var controlInterface: UsbInterface? = null - private var dataInterface: UsbInterface? = null + @Volatile private var eventSink: EventChannel.EventSink? = null + @Volatile private var usbConnection: UsbDeviceConnection? = null + @Volatile private var usbInEndpoint: UsbEndpoint? = null + @Volatile private var usbOutEndpoint: UsbEndpoint? = null + @Volatile private var controlInterface: UsbInterface? = null + @Volatile private var dataInterface: UsbInterface? = null private var readThread: Thread? = null @Volatile private var isReading = false - private var connectedDeviceName: String? = null + @Volatile private var connectedDeviceName: String? = null private var pendingConnectResult: MethodChannel.Result? = null private var pendingConnectPortName: String? = null @@ -86,7 +86,7 @@ class MeshcoreUsbFunctions( if (device == null) { result.error( "usb_device_missing", - "USB device no longer available for $portName", + null, null, ) return @@ -95,7 +95,7 @@ class MeshcoreUsbFunctions( val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) if (!granted || !usbManager.hasPermission(device)) { - result.error("usb_permission_denied", "USB permission denied", null) + result.error("usb_permission_denied", null, null) return } @@ -176,13 +176,13 @@ class MeshcoreUsbFunctions( val portName = call.argument("portName") val baudRate = call.argument("baudRate") ?: 115200 if (portName.isNullOrBlank()) { - result.error("usb_invalid_port", "Port name is required", null) + result.error("usb_invalid_port", null, null) return } val device = findUsbDevice(portName) if (device == null) { - result.error("usb_device_missing", "USB device not found for $portName", null) + result.error("usb_device_missing", null, null) return } @@ -192,7 +192,7 @@ class MeshcoreUsbFunctions( } if (pendingConnectResult != null) { - result.error("usb_busy", "Another USB permission request is already pending", null) + result.error("usb_busy", null, null) return } @@ -214,11 +214,11 @@ class MeshcoreUsbFunctions( val connection = usbConnection val endpoint = usbOutEndpoint if (data == null) { - result.error("usb_invalid_data", "Data is required", null) + result.error("usb_invalid_data", null, null) return } if (connection == null || endpoint == null) { - result.error("usb_not_connected", "USB serial port is not connected", null) + result.error("usb_not_connected", null, null) return } @@ -259,7 +259,7 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_driver_missing", - "No compatible USB serial interface for ${device.deviceName}", + null, null, ) } @@ -271,7 +271,7 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_open_failed", - "UsbManager could not open ${device.deviceName}", + null, null, ) } @@ -283,7 +283,7 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_open_failed", - "Could not claim USB data interface for ${device.deviceName}", + null, null, ) } @@ -299,20 +299,21 @@ class MeshcoreUsbFunctions( mainHandler.post { result.error( "usb_open_failed", - "Could not claim USB control interface for ${device.deviceName}", + null, null, ) } return@execute } - configureDevice(connection, config, baudRate) - usbConnection = connection usbInEndpoint = config.inEndpoint usbOutEndpoint = config.outEndpoint controlInterface = config.controlInterface dataInterface = config.dataInterface + + configureDevice(connection, config, baudRate) + connectedDeviceName = device.deviceName startReadLoop() diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 4299c43..4fa8412 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -21,7 +21,7 @@ import '../services/path_history_service.dart'; import '../services/app_settings_service.dart'; import '../services/background_service.dart'; import '../services/notification_service.dart'; -import '../services/usb_serial_service.dart'; +import 'meshcore_connector_usb.dart'; import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; @@ -114,11 +114,9 @@ class MeshCoreConnector extends ChangeNotifier { String? _lastDeviceId; String? _lastDeviceDisplayName; bool _manualDisconnect = false; - final UsbSerialService _usbSerialService = UsbSerialService(); + final MeshCoreUsbManager _usbManager = MeshCoreUsbManager(); StreamSubscription? _usbFrameSubscription; MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; - String? _activeUsbPortKey; - String? _activeUsbPortLabel; final List _scanResults = []; final List _contacts = []; @@ -252,9 +250,8 @@ class MeshCoreConnector extends ChangeNotifier { String get deviceIdLabel => _deviceId ?? 'Unknown'; MeshCoreTransportType get activeTransport => _activeTransport; - String? get activeUsbPort => _activeUsbPortKey; - String? get activeUsbPortDisplayLabel => - _activeUsbPortLabel ?? _activeUsbPortKey; + String? get activeUsbPort => _usbManager.activePortKey; + String? get activeUsbPortDisplayLabel => _usbManager.activePortDisplayLabel; bool get isUsbTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.usb; @@ -661,7 +658,7 @@ class MeshCoreConnector extends ChangeNotifier { _bleDebugLogService = bleDebugLogService; _appDebugLogService = appDebugLogService; _backgroundService = backgroundService; - _usbSerialService.setDebugLogService(_appDebugLogService); + _usbManager.setDebugLogService(_appDebugLogService); // Initialize notification service _notificationService.initialize(); @@ -871,10 +868,14 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future> listUsbPorts() => _usbSerialService.listPorts(); + Future> listUsbPorts() => _usbManager.listPorts(); void setUsbRequestPortLabel(String label) { - _usbSerialService.setRequestPortLabel(label); + _usbManager.setRequestPortLabel(label); + } + + void setUsbFallbackDeviceName(String label) { + _usbManager.setFallbackDeviceName(label); } Future connectUsb({ @@ -883,53 +884,70 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { + _appDebugLogService?.warn( + 'connectUsb ignored: already $_state', + tag: 'USB', + ); return; } - _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPortKey = null; - _activeUsbPortLabel = null; + _appDebugLogService?.info( + 'connectUsb: port=$portName baud=$baudRate', + tag: 'USB', + ); await stopScan(); _cancelReconnectTimer(); _manualDisconnect = false; _resetConnectionHandshakeState(); _activeTransport = MeshCoreTransportType.usb; - _activeUsbPortKey = portName; - _activeUsbPortLabel = portName; _setState(MeshCoreConnectionState.connecting); try { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; - await _usbSerialService.connect(portName: portName, baudRate: baudRate); - _activeUsbPortKey = _usbSerialService.activePortKey ?? _activeUsbPortKey; - _activeUsbPortLabel = - _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; + _appDebugLogService?.info( + 'connectUsb: opening serial port…', + tag: 'USB', + ); + await _usbManager.connect(portName: portName, baudRate: baudRate); + _appDebugLogService?.info( + 'connectUsb: serial port opened, label=${_usbManager.activePortDisplayLabel}', + tag: 'USB', + ); notifyListeners(); if (PlatformInfo.isWeb) { await stopScan(); } await Future.delayed(const Duration(milliseconds: 200)); - _usbFrameSubscription = _usbSerialService.frameStream.listen( + _usbFrameSubscription = _usbManager.frameStream.listen( _handleFrame, onError: (error, stackTrace) { _appDebugLogService?.error('USB transport error: $error', tag: 'USB'); unawaited(disconnect(manual: false)); }, onDone: () { + _appDebugLogService?.warn('USB frame stream ended', tag: 'USB'); unawaited(disconnect(manual: false)); }, ); _setState(MeshCoreConnectionState.connected); _pendingInitialChannelSync = true; + _appDebugLogService?.info( + 'connectUsb: requesting device info…', + tag: 'USB', + ); await _requestDeviceInfo(); _startBatteryPolling(); var gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), ); if (!gotSelfInfo) { + _appDebugLogService?.warn( + 'connectUsb: SELF_INFO timeout, retrying…', + tag: 'USB', + ); await refreshDeviceInfo(); gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), @@ -939,7 +957,9 @@ class MeshCoreConnector extends ChangeNotifier { throw StateError('Timed out waiting for SELF_INFO during connect'); } + _appDebugLogService?.info('connectUsb: syncing time…', tag: 'USB'); await syncTime(); + _appDebugLogService?.info('connectUsb: complete', tag: 'USB'); } catch (error) { _appDebugLogService?.error('USB connection error: $error', tag: 'USB'); await disconnect(manual: false); @@ -954,8 +974,6 @@ class MeshCoreConnector extends ChangeNotifier { } _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPortKey = null; - _activeUsbPortLabel = null; await stopScan(); _setState(MeshCoreConnectionState.connecting); @@ -1282,7 +1300,7 @@ class MeshCoreConnector extends ChangeNotifier { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; - await _usbSerialService.disconnect(); + await _usbManager.disconnect(); await _notifySubscription?.cancel(); _notifySubscription = null; @@ -1341,8 +1359,6 @@ class MeshCoreConnector extends ChangeNotifier { _reactionSendQueueSequence = 0; _activeTransport = MeshCoreTransportType.bluetooth; - _activeUsbPortKey = null; - _activeUsbPortLabel = null; _setState(MeshCoreConnectionState.disconnected); _appDebugLogService?.info( @@ -1365,7 +1381,7 @@ class MeshCoreConnector extends ChangeNotifier { _bleDebugLogService?.logFrame(data, outgoing: true); if (_activeTransport == MeshCoreTransportType.usb) { - await _usbSerialService.write(data); + await _usbManager.write(data); } else { if (_rxCharacteristic == null) { throw Exception("MeshCore RX characteristic not available"); @@ -2464,9 +2480,7 @@ class MeshCoreConnector extends ChangeNotifier { if (_activeTransport == MeshCoreTransportType.usb && selfName != null && selfName.isNotEmpty) { - _usbSerialService.updateConnectedLabel(selfName); - _activeUsbPortLabel = - _usbSerialService.activePortDisplayLabel ?? _activeUsbPortLabel; + _usbManager.updateConnectedLabel(selfName); } _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); @@ -4246,7 +4260,7 @@ class MeshCoreConnector extends ChangeNotifier { _reconnectTimer?.cancel(); _batteryPollTimer?.cancel(); _receivedFramesController.close(); - _usbSerialService.dispose(); + _usbManager.dispose(); // Flush pending unread writes before disposal _unreadStore.flush(); @@ -4269,6 +4283,10 @@ class MeshCoreConnector extends ChangeNotifier { final header = packet.readByte(); routeType = header & 0x03; payloadType = (header >> 2) & 0x0F; + if (routeType == _routeTransportFlood || + routeType == _routeTransportDirect) { + packet.skipBytes(4); // Skip transport-specific bytes + } //final payloadVer = (header >> 6) & 0x03; final pathLen = packet.readByte(); pathBytes = packet.readBytes(pathLen); @@ -4301,7 +4319,12 @@ class MeshCoreConnector extends ChangeNotifier { packet.skipBytes(1); // Skip SNR byte packet.skipBytes(1); // Skip RSSI byte final header = packet.readByte(); + final routeType = header & 0x03; payloadType = (header >> 2) & 0x0F; + if (routeType == _routeTransportFlood || + routeType == _routeTransportDirect) { + packet.skipBytes(4); // Skip transport-specific bytes + } //final payloadVer = (header >> 6) & 0x03; final pathLen = packet.readByte(); pathBytes = packet.readBytes(pathLen); diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart index b1fbe15..a0f70d5 100644 --- a/lib/connector/meshcore_connector_usb.dart +++ b/lib/connector/meshcore_connector_usb.dart @@ -1,32 +1,71 @@ -import 'package:flutter/foundation.dart'; +import 'dart:typed_data'; -import 'meshcore_connector.dart'; +import '../services/app_debug_log_service.dart'; +import '../services/usb_serial_service.dart'; -class MeshCoreConnectorUsb { - const MeshCoreConnectorUsb(this.connector); +/// Manages USB serial transport for MeshCore devices. +/// +/// Owns the [UsbSerialService] and USB-specific connection state. +/// The main [MeshCoreConnector] delegates all USB operations here. +class MeshCoreUsbManager { + MeshCoreUsbManager(); - final MeshCoreConnector connector; + final UsbSerialService _service = UsbSerialService(); + AppDebugLogService? _debugLog; + String? _activePortKey; + String? _activePortLabel; - MeshCoreConnectionState get state => connector.state; - MeshCoreTransportType get activeTransport => connector.activeTransport; - String? get activeUsbPortDisplayLabel => connector.activeUsbPortDisplayLabel; - bool get isUsbTransportConnected => connector.isUsbTransportConnected; + // --- Getters --- + String? get activePortKey => _activePortKey; + String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey; + bool get isConnected => _service.isConnected; + Stream get frameStream => _service.frameStream; - void addListener(VoidCallback listener) => connector.addListener(listener); - void removeListener(VoidCallback listener) => - connector.removeListener(listener); + // --- Configuration --- + Future> listPorts() => _service.listPorts(); - Future> listPorts() => connector.listUsbPorts(); + void setRequestPortLabel(String label) => + _service.setRequestPortLabel(label); - void setRequestPortLabel(String label) { - connector.setUsbRequestPortLabel(label); + void setFallbackDeviceName(String label) => + _service.setFallbackDeviceName(label); + + void setDebugLogService(AppDebugLogService? service) { + _debugLog = service; + _service.setDebugLogService(service); } - Future connect({required String portName, int baudRate = 115200}) { - return connector.connectUsb(portName: portName, baudRate: baudRate); + // --- Connection lifecycle --- + Future connect({required String portName, int baudRate = 115200}) async { + _debugLog?.info( + 'UsbManager.connect: portName=$portName baud=$baudRate', + tag: 'USB', + ); + await _service.connect(portName: portName, baudRate: baudRate); + _activePortKey = _service.activePortKey ?? portName; + _activePortLabel = _service.activePortDisplayLabel ?? portName; + _debugLog?.info( + 'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel', + tag: 'USB', + ); } - Future disconnect({bool manual = true}) { - return connector.disconnect(manual: manual); + Future disconnect() async { + _debugLog?.info('UsbManager.disconnect', tag: 'USB'); + await _service.disconnect(); + _activePortKey = null; + _activePortLabel = null; + } + + Future write(Uint8List data) => _service.write(data); + + // --- Label management --- + void updateConnectedLabel(String selfName) { + _service.updateConnectedLabel(selfName); + _activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel; + } + + void dispose() { + _service.dispose(); } } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c2879dd..94d8997 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "USB връзката вече е активирана.", "usbErrorNoDeviceSelected": "Няма избран USB устройство.", "usbErrorPortClosed": "USB връзката не е активна.", - "usbErrorConnectTimedOut": "Изчаква се, но устройството не отговаря в рамките на зададения време." + "usbFallbackDeviceName": "Устройство за четене на уеб серийни данни", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_connecting": "Свързване към USB устройство...", + "usbConnectionFailed": "Неуспешно свързване през USB: {error}", + "usbStatus_notConnected": "Изберете USB устройство", + "usbStatus_searching": "Търсене на USB устройства...", + "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ad6b0bf..9ba0f51 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1875,5 +1875,17 @@ "usbErrorAlreadyActive": "Eine USB-Verbindung ist bereits hergestellt.", "usbErrorNoDeviceSelected": "Kein USB-Gerät wurde ausgewählt.", "usbErrorPortClosed": "Die USB-Verbindung ist nicht aktiv.", - "usbErrorConnectTimedOut": "Wartezeit abgelaufen, da keine Antwort vom Gerät empfangen wurde." + "usbFallbackDeviceName": "Web-Serielle Geräte", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Suche nach USB-Geräten...", + "usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus", + "usbStatus_connecting": "Verbindung zum USB-Gerät...", + "usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}", + "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4ffa573..2605628 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -65,7 +65,19 @@ "usbErrorAlreadyActive": "A USB connection is already active.", "usbErrorNoDeviceSelected": "No USB device was selected.", "usbErrorPortClosed": "The USB connection is not open.", - "usbErrorConnectTimedOut": "Timed out waiting for the device to respond.", + "usbErrorConnectTimedOut": "Connection timed out. Make sure the device has USB Companion firmware.", + "usbFallbackDeviceName": "Web Serial Device", + "usbStatus_notConnected": "Select a USB device", + "usbStatus_connecting": "Connecting to USB device...", + "usbStatus_searching": "Searching for USB devices...", + "usbConnectionFailed": "USB connection failed: {error}", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "scanner_scanning": "Scanning for devices...", "scanner_connecting": "Connecting...", "scanner_disconnecting": "Disconnecting...", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 09545c7..9b791d3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1875,5 +1875,17 @@ "usbErrorAlreadyActive": "La conexión USB ya está activa.", "usbErrorNoDeviceSelected": "No se ha seleccionado ningún dispositivo USB.", "usbErrorPortClosed": "La conexión USB no está activa.", - "usbErrorConnectTimedOut": "Se ha agotado el tiempo de espera mientras se esperaba la respuesta del dispositivo." + "usbFallbackDeviceName": "Dispositivo de serie web", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_connecting": "Conectándose al dispositivo USB...", + "usbStatus_searching": "Buscando dispositivos USB...", + "usbStatus_notConnected": "Seleccione un dispositivo USB", + "usbConnectionFailed": "Error al conectar mediante USB: {error}", + "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f6750f8..a7bedc9 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Une connexion USB est déjà établie.", "usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.", "usbErrorPortClosed": "La connexion USB n'est pas établie.", - "usbErrorConnectTimedOut": "Attente avec délai, en attendant une réponse de l'appareil." + "usbFallbackDeviceName": "Dispositif de communication série sur le Web", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_notConnected": "Sélectionnez un périphérique USB", + "usbConnectionFailed": "Échec de la connexion USB : {error}", + "usbStatus_connecting": "Connexion au périphérique USB...", + "usbStatus_searching": "Recherche de périphériques USB...", + "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b4e5c14..423ff40 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "La connessione USB è già attiva.", "usbErrorNoDeviceSelected": "Non è stato selezionato alcun dispositivo USB.", "usbErrorPortClosed": "La connessione USB non è attiva.", - "usbErrorConnectTimedOut": "Attesa superata, in attesa di una risposta dal dispositivo." + "usbFallbackDeviceName": "Dispositivo per comunicazione seriale su rete", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Ricerca di dispositivi USB...", + "usbConnectionFailed": "Errore nella connessione USB: {error}", + "usbStatus_notConnected": "Seleziona un dispositivo USB", + "usbStatus_connecting": "Connessione al dispositivo USB...", + "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d3e5db9..8d3f86b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -433,9 +433,39 @@ abstract class AppLocalizations { /// No description provided for @usbErrorConnectTimedOut. /// /// In en, this message translates to: - /// **'Timed out waiting for the device to respond.'** + /// **'Connection timed out. Make sure the device has USB Companion firmware.'** String get usbErrorConnectTimedOut; + /// No description provided for @usbFallbackDeviceName. + /// + /// In en, this message translates to: + /// **'Web Serial Device'** + String get usbFallbackDeviceName; + + /// No description provided for @usbStatus_notConnected. + /// + /// In en, this message translates to: + /// **'Select a USB device'** + String get usbStatus_notConnected; + + /// No description provided for @usbStatus_connecting. + /// + /// In en, this message translates to: + /// **'Connecting to USB device...'** + String get usbStatus_connecting; + + /// No description provided for @usbStatus_searching. + /// + /// In en, this message translates to: + /// **'Searching for USB devices...'** + String get usbStatus_searching; + + /// No description provided for @usbConnectionFailed. + /// + /// In en, this message translates to: + /// **'USB connection failed: {error}'** + String usbConnectionFailed(String error); + /// No description provided for @scanner_scanning. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cb4739a..356106e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -175,7 +175,25 @@ class AppLocalizationsBg extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Изчаква се, но устройството не отговаря в рамките на зададения време.'; + 'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.'; + + @override + String get usbFallbackDeviceName => + 'Устройство за четене на уеб серийни данни'; + + @override + String get usbStatus_notConnected => 'Изберете USB устройство'; + + @override + String get usbStatus_connecting => 'Свързване към USB устройство...'; + + @override + String get usbStatus_searching => 'Търсене на USB устройства...'; + + @override + String usbConnectionFailed(String error) { + return 'Неуспешно свързване през USB: $error'; + } @override String get scanner_scanning => 'Сканиране за устройства...'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ad7dd84..6353f35 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -177,7 +177,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Wartezeit abgelaufen, da keine Antwort vom Gerät empfangen wurde.'; + 'Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.'; + + @override + String get usbFallbackDeviceName => 'Web-Serielle Geräte'; + + @override + String get usbStatus_notConnected => 'Wählen Sie ein USB-Gerät aus'; + + @override + String get usbStatus_connecting => 'Verbindung zum USB-Gerät...'; + + @override + String get usbStatus_searching => 'Suche nach USB-Geräten...'; + + @override + String usbConnectionFailed(String error) { + return 'Fehler beim USB-Verbindungsaufbau: $error'; + } @override String get scanner_scanning => 'Scannen nach Geräten...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5a9fc53..9c20df7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -174,7 +174,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Timed out waiting for the device to respond.'; + 'Connection timed out. Make sure the device has USB Companion firmware.'; + + @override + String get usbFallbackDeviceName => 'Web Serial Device'; + + @override + String get usbStatus_notConnected => 'Select a USB device'; + + @override + String get usbStatus_connecting => 'Connecting to USB device...'; + + @override + String get usbStatus_searching => 'Searching for USB devices...'; + + @override + String usbConnectionFailed(String error) { + return 'USB connection failed: $error'; + } @override String get scanner_scanning => 'Scanning for devices...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 60b0936..eecbd48 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -177,7 +177,24 @@ class AppLocalizationsEs extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Se ha agotado el tiempo de espera mientras se esperaba la respuesta del dispositivo.'; + 'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => 'Dispositivo de serie web'; + + @override + String get usbStatus_notConnected => 'Seleccione un dispositivo USB'; + + @override + String get usbStatus_connecting => 'Conectándose al dispositivo USB...'; + + @override + String get usbStatus_searching => 'Buscando dispositivos USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Error al conectar mediante USB: $error'; + } @override String get scanner_scanning => 'Escaneando dispositivos...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index fdeee92..5cabc86 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -176,7 +176,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Attente avec délai, en attendant une réponse de l\'appareil.'; + 'La connexion a expiré. Assurez-vous que l\'appareil dispose du firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Dispositif de communication série sur le Web'; + + @override + String get usbStatus_notConnected => 'Sélectionnez un périphérique USB'; + + @override + String get usbStatus_connecting => 'Connexion au périphérique USB...'; + + @override + String get usbStatus_searching => 'Recherche de périphériques USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Échec de la connexion USB : $error'; + } @override String get scanner_scanning => 'Recherche de périphériques...'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a5ae362..d170540 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -177,7 +177,25 @@ class AppLocalizationsIt extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Attesa superata, in attesa di una risposta dal dispositivo.'; + 'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Dispositivo per comunicazione seriale su rete'; + + @override + String get usbStatus_notConnected => 'Seleziona un dispositivo USB'; + + @override + String get usbStatus_connecting => 'Connessione al dispositivo USB...'; + + @override + String get usbStatus_searching => 'Ricerca di dispositivi USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Errore nella connessione USB: $error'; + } @override String get scanner_scanning => 'Scansione in corso per i dispositivi...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a437106..323ba34 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -175,7 +175,24 @@ class AppLocalizationsNl extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Wachtperiode is verlopen, aangezien het apparaat niet reageerde.'; + 'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.'; + + @override + String get usbFallbackDeviceName => 'Web-serieapparaat'; + + @override + String get usbStatus_notConnected => 'Selecteer een USB-apparaat'; + + @override + String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...'; + + @override + String get usbStatus_searching => 'Zoeken naar USB-apparaten...'; + + @override + String usbConnectionFailed(String error) { + return 'Fout bij de USB-verbinding: $error'; + } @override String get scanner_scanning => 'Scannen naar apparaten...'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2f034b9..9175c3e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -177,7 +177,25 @@ class AppLocalizationsPl extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji.'; + 'Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".'; + + @override + String get usbFallbackDeviceName => + 'Urządzenie do komunikacji przez sieć (seria)'; + + @override + String get usbStatus_notConnected => 'Wybierz urządzenie USB'; + + @override + String get usbStatus_connecting => 'Połączenie z urządzeniem USB...'; + + @override + String get usbStatus_searching => 'Wyszukiwanie urządzeń USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Błąd połączenia USB: $error'; + } @override String get scanner_scanning => 'Skanowanie urządzeń...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 7795e53..ff09213 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -177,7 +177,24 @@ class AppLocalizationsPt extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Tempo limite aguardando a resposta do dispositivo.'; + 'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web'; + + @override + String get usbStatus_notConnected => 'Selecione um dispositivo USB'; + + @override + String get usbStatus_connecting => 'Conectando ao dispositivo USB...'; + + @override + String get usbStatus_searching => 'Procurando por dispositivos USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Falha na conexão USB: $error'; + } @override String get scanner_scanning => 'Procurando por dispositivos...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index b58ae8d..69a5891 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -177,7 +177,25 @@ class AppLocalizationsRu extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Ожидание ответа от устройства превысило установленное время.'; + 'Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Устройство для последовательного подключения к сети'; + + @override + String get usbStatus_notConnected => 'Выберите USB-устройство'; + + @override + String get usbStatus_connecting => 'Подключение к USB-устройству...'; + + @override + String get usbStatus_searching => 'Поиск USB-устройств...'; + + @override + String usbConnectionFailed(String error) { + return 'Не удалось установить соединение через USB: $error'; + } @override String get scanner_scanning => 'Поиск устройств...'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 403a612..d0e75b0 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -177,7 +177,24 @@ class AppLocalizationsSk extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Čakal som, kým sa zariadenie neozvými, ale časový limit sa dobehol.'; + 'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => 'Webový sériový zariadenie'; + + @override + String get usbStatus_notConnected => 'Vyberte USB zariadenie'; + + @override + String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...'; + + @override + String get usbStatus_searching => 'Hľadanie USB zariadení...'; + + @override + String usbConnectionFailed(String error) { + return 'Neúspešné pripojenie cez USB: $error'; + } @override String get scanner_scanning => 'Skrívania zariadení...'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index db9368f..21e3d9d 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -174,7 +174,25 @@ class AppLocalizationsSl extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval.'; + 'Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Naprave za serijsko komunikacijo preko spleta'; + + @override + String get usbStatus_notConnected => 'Izberite USB naprave.'; + + @override + String get usbStatus_connecting => 'Povezava z USB napravo...'; + + @override + String get usbStatus_searching => 'Iskanje USB naprav...'; + + @override + String usbConnectionFailed(String error) { + return 'Napaka pri povezavi preko USB: $error'; + } @override String get scanner_scanning => 'Skeniram za naprave...'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index d223cd6..5951fae 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -129,7 +129,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbScreenNote => - 'USB-seriell kommunikation är aktiv på stöderliga Android-enheter och på skrivbordsplattformar.'; + 'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.'; @override String get usbScreenEmptyState => @@ -175,7 +175,24 @@ class AppLocalizationsSv extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Tiden har löpt ut medan vi väntade på att enheten skulle svara.'; + 'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.'; + + @override + String get usbFallbackDeviceName => 'Web-serieenhet'; + + @override + String get usbStatus_notConnected => 'Välj en USB-enhet'; + + @override + String get usbStatus_connecting => 'Anslutning till USB-enhet...'; + + @override + String get usbStatus_searching => 'Söker efter USB-enheter...'; + + @override + String usbConnectionFailed(String error) { + return 'Fel vid USB-anslutning: $error'; + } @override String get scanner_scanning => 'Söker efter enheter...'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 676e941..b8fd60a 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -175,7 +175,25 @@ class AppLocalizationsUk extends AppLocalizations { @override String get usbErrorConnectTimedOut => - 'Час очікування закінчився, оскільки пристрій не відповів.'; + 'З\'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.'; + + @override + String get usbFallbackDeviceName => + 'Пристрій для передачі даних по веб-серіалах'; + + @override + String get usbStatus_notConnected => 'Виберіть пристрій USB'; + + @override + String get usbStatus_connecting => 'Підключення до USB-пристрою...'; + + @override + String get usbStatus_searching => 'Пошук пристроїв USB...'; + + @override + String usbConnectionFailed(String error) { + return 'Не вдалося встановити з\'єднання через USB: $error'; + } @override String get scanner_scanning => 'Пошук пристроїв...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ff42929..27e6c21 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -166,7 +166,24 @@ class AppLocalizationsZh extends AppLocalizations { String get usbErrorPortClosed => 'USB 连接未建立。'; @override - String get usbErrorConnectTimedOut => '等待设备响应超时。'; + String get usbErrorConnectTimedOut => '连接超时。请确保设备已安装 USB 伴侣固件。'; + + @override + String get usbFallbackDeviceName => 'Web 串流设备'; + + @override + String get usbStatus_notConnected => '选择一个 USB 设备'; + + @override + String get usbStatus_connecting => '连接USB设备...'; + + @override + String get usbStatus_searching => '正在搜索 USB 设备...'; + + @override + String usbConnectionFailed(String error) { + return 'USB 连接失败:$error'; + } @override String get scanner_scanning => '正在搜索设备...'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7d56216..94df130 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Een USB-verbinding is al actief.", "usbErrorNoDeviceSelected": "Geen USB-apparaat is geselecteerd.", "usbErrorPortClosed": "De USB-verbinding is niet actief.", - "usbErrorConnectTimedOut": "Wachtperiode is verlopen, aangezien het apparaat niet reageerde." + "usbFallbackDeviceName": "Web-serieapparaat", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbConnectionFailed": "Fout bij de USB-verbinding: {error}", + "usbStatus_notConnected": "Selecteer een USB-apparaat", + "usbStatus_connecting": "Verbinding maken met USB-apparaat...", + "usbStatus_searching": "Zoeken naar USB-apparaten...", + "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index f6cd0be..d020e0e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Połączenie USB jest już aktywne.", "usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.", "usbErrorPortClosed": "Połączenie USB nie jest aktywne.", - "usbErrorConnectTimedOut": "Czekanie na odpowiedź urządzenia zakończyło się z powodu braku reakcji." + "usbFallbackDeviceName": "Urządzenie do komunikacji przez sieć (seria)", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Wyszukiwanie urządzeń USB...", + "usbStatus_connecting": "Połączenie z urządzeniem USB...", + "usbStatus_notConnected": "Wybierz urządzenie USB", + "usbConnectionFailed": "Błąd połączenia USB: {error}", + "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 77bc5c7..d52cb41 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "A conexão USB já está ativa.", "usbErrorNoDeviceSelected": "Nenhum dispositivo USB foi selecionado.", "usbErrorPortClosed": "A conexão USB não está ativa.", - "usbErrorConnectTimedOut": "Tempo limite aguardando a resposta do dispositivo." + "usbFallbackDeviceName": "Dispositivo de Serial para a Web", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Procurando por dispositivos USB...", + "usbStatus_notConnected": "Selecione um dispositivo USB", + "usbConnectionFailed": "Falha na conexão USB: {error}", + "usbStatus_connecting": "Conectando ao dispositivo USB...", + "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 319496a..92fd55e 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1087,5 +1087,17 @@ "usbErrorAlreadyActive": "USB-соединение уже установлено.", "usbErrorNoDeviceSelected": "Не было выбрано ни одно устройство USB.", "usbErrorPortClosed": "USB-соединение не установлено.", - "usbErrorConnectTimedOut": "Ожидание ответа от устройства превысило установленное время." + "usbFallbackDeviceName": "Устройство для последовательного подключения к сети", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Поиск USB-устройств...", + "usbStatus_connecting": "Подключение к USB-устройству...", + "usbConnectionFailed": "Не удалось установить соединение через USB: {error}", + "usbStatus_notConnected": "Выберите USB-устройство", + "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 571d2f2..141147c 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "Pripojenie cez USB je už aktivované.", "usbErrorNoDeviceSelected": "Nebolo vybrané žiadne USB zariadenie.", "usbErrorPortClosed": "Pripojenie cez USB nie je aktivované.", - "usbErrorConnectTimedOut": "Čakal som, kým sa zariadenie neozvými, ale časový limit sa dobehol." + "usbFallbackDeviceName": "Webový sériový zariadenie", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Hľadanie USB zariadení...", + "usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}", + "usbStatus_notConnected": "Vyberte USB zariadenie", + "usbStatus_connecting": "Pripojenie k USB zariadeniu...", + "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 631e75e..12529d6 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "USB povezava je že aktivirana.", "usbErrorNoDeviceSelected": "Ni bilo izbranega USB naprave.", "usbErrorPortClosed": "USB povezava ni aktivirana.", - "usbErrorConnectTimedOut": "Čakanje je preseglo določeno časovno obdobo, ker se naprave ni odzval." + "usbFallbackDeviceName": "Naprave za serijsko komunikacijo preko spleta", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_notConnected": "Izberite USB naprave.", + "usbStatus_connecting": "Povezava z USB napravo...", + "usbStatus_searching": "Iskanje USB naprav...", + "usbConnectionFailed": "Napaka pri povezavi preko USB: {error}", + "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index be1ada8..f7615df 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1834,7 +1834,7 @@ "usbScreenSubtitle": "Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.", "usbScreenTitle": "Anslut via USB", "usbScreenStatus": "Välj en USB-enhet", - "usbScreenNote": "USB-seriell kommunikation är aktiv på stöderliga Android-enheter och på skrivbordsplattformar.", + "usbScreenNote": "USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.", "usbScreenEmptyState": "Inga USB-enheter hittades. Anslut en och uppdatera.", "usbErrorPermissionDenied": "Tillgången via USB nekas.", "usbErrorDeviceMissing": "Den valda USB-enheten är inte längre tillgänglig.", @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "En USB-anslutning är redan aktiv.", "usbErrorNoDeviceSelected": "Ingen USB-enhet valdes.", "usbErrorPortClosed": "USB-anslutningen är inte aktiv.", - "usbErrorConnectTimedOut": "Tiden har löpt ut medan vi väntade på att enheten skulle svara." + "usbFallbackDeviceName": "Web-serieenhet", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_connecting": "Anslutning till USB-enhet...", + "usbStatus_notConnected": "Välj en USB-enhet", + "usbConnectionFailed": "Fel vid USB-anslutning: {error}", + "usbStatus_searching": "Söker efter USB-enheter...", + "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 76ac380..7794098 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1847,5 +1847,17 @@ "usbErrorAlreadyActive": "USB-з'єднання вже встановлено.", "usbErrorNoDeviceSelected": "Не було вибрано жодного пристрою USB.", "usbErrorPortClosed": "З'єднання USB не встановлено.", - "usbErrorConnectTimedOut": "Час очікування закінчився, оскільки пристрій не відповів." + "usbFallbackDeviceName": "Пристрій для передачі даних по веб-серіалах", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "Пошук пристроїв USB...", + "usbStatus_notConnected": "Виберіть пристрій USB", + "usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", + "usbStatus_connecting": "Підключення до USB-пристрою...", + "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 76893c9..dfc8e64 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1852,5 +1852,17 @@ "usbErrorAlreadyActive": "USB 连接已建立。", "usbErrorNoDeviceSelected": "未选择任何 USB 设备。", "usbErrorPortClosed": "USB 连接未建立。", - "usbErrorConnectTimedOut": "等待设备响应超时。" + "usbFallbackDeviceName": "Web 串流设备", + "@usbConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "usbStatus_searching": "正在搜索 USB 设备...", + "usbStatus_connecting": "连接USB设备...", + "usbStatus_notConnected": "选择一个 USB 设备", + "usbConnectionFailed": "USB 连接失败:{error}", + "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。" } diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 5c4dfb1..c78e0c1 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -1,16 +1,15 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; -import '../connector/meshcore_connector_usb.dart'; import '../l10n/l10n.dart'; import '../utils/app_logger.dart'; import '../utils/platform_info.dart'; import '../utils/usb_port_labels.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import 'contacts_screen.dart'; import 'scanner_screen.dart'; @@ -24,20 +23,12 @@ class UsbScreen extends StatefulWidget { class _UsbScreenState extends State { final List _ports = []; bool _isLoadingPorts = true; - bool _isConnecting = false; bool _navigatedToContacts = false; bool _didScheduleInitialLoad = false; - String? _selectedPort; - String? _connectedPortDisplayLabel; - String? _errorText; Timer? _hotPlugTimer; late final MeshCoreConnector _connector; - late final MeshCoreConnectorUsb _usbConnector; late final VoidCallback _connectionListener; - /// Whether the current platform supports dynamic hot-plug polling. - /// On desktop (macOS, Windows, Linux) we poll continuously so the user - /// never needs to hit Refresh manually. bool get _supportsHotPlug => PlatformInfo.isWindows || PlatformInfo.isLinux || PlatformInfo.isMacOS; @@ -45,25 +36,13 @@ class _UsbScreenState extends State { void initState() { super.initState(); _connector = context.read(); - _usbConnector = MeshCoreConnectorUsb(_connector); _connectionListener = () { if (!mounted) return; - final activeUsbPortDisplayLabel = _usbConnector.activeUsbPortDisplayLabel; - final shouldUpdateDisplayLabel = - activeUsbPortDisplayLabel != _connectedPortDisplayLabel; - if (_usbConnector.state == MeshCoreConnectionState.disconnected) { + if (_connector.state == MeshCoreConnectionState.disconnected) { _navigatedToContacts = false; - setState(() { - _isConnecting = false; - _connectedPortDisplayLabel = activeUsbPortDisplayLabel; - }); - } else if (shouldUpdateDisplayLabel) { - setState(() { - _connectedPortDisplayLabel = activeUsbPortDisplayLabel; - }); } - if (_usbConnector.state == MeshCoreConnectionState.connected && - _usbConnector.isUsbTransportConnected && + if (_connector.state == MeshCoreConnectionState.connected && + _connector.isUsbTransportConnected && !_navigatedToContacts) { _navigatedToContacts = true; Navigator.of(context).pushReplacement( @@ -71,14 +50,15 @@ class _UsbScreenState extends State { ); } }; - _usbConnector.addListener(_connectionListener); + _connector.addListener(_connectionListener); _startHotPlugTimer(); } @override void didChangeDependencies() { super.didChangeDependencies(); - _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + _connector.setUsbFallbackDeviceName(context.l10n.usbFallbackDeviceName); if (!_didScheduleInitialLoad) { _didScheduleInitialLoad = true; unawaited(_loadPorts()); @@ -89,12 +69,12 @@ class _UsbScreenState extends State { void dispose() { _hotPlugTimer?.cancel(); _hotPlugTimer = null; - _usbConnector.removeListener(_connectionListener); + _connector.removeListener(_connectionListener); if (!_navigatedToContacts && - _usbConnector.activeTransport == MeshCoreTransportType.usb && - _usbConnector.state != MeshCoreConnectionState.disconnected) { + _connector.activeTransport == MeshCoreTransportType.usb && + _connector.state != MeshCoreConnectionState.disconnected) { WidgetsBinding.instance.addPostFrameCallback((_) { - unawaited(_usbConnector.disconnect(manual: true)); + unawaited(_connector.disconnect(manual: true)); }); } super.dispose(); @@ -102,234 +82,192 @@ class _UsbScreenState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - final l10n = context.l10n; - return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () { - appLogger.info('Back button pressed', tag: 'UsbScreen'); - Navigator.of(context).maybePop(); - }, - ), - title: Text( - l10n.connectionChoiceUsbLabel, - style: theme.textTheme.titleLarge, + onPressed: () => Navigator.of(context).maybePop(), ), + title: AdaptiveAppBarTitle(context.l10n.usbScreenTitle), centerTitle: true, - actions: [ - if (PlatformInfo.isWeb || - PlatformInfo.isAndroid || - PlatformInfo.isIOS) - TextButton.icon( - onPressed: () { - appLogger.info( - 'Bluetooth selected, opening ScannerScreen', - tag: 'UsbScreen', - ); - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const ScannerScreen()), - ); - }, - icon: const Icon(Icons.bluetooth), - label: Text(l10n.connectionChoiceBluetoothLabel), - ), - ], ), body: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - final availableHeight = constraints.maxHeight.isFinite - ? constraints.maxHeight - : 600.0; - final availableWidth = constraints.maxWidth.isFinite - ? constraints.maxWidth - : 800.0; - final gap = math.max(8.0, math.min(16.0, availableHeight * 0.025)); - final iconSize = math.max( - 28.0, - math.min(72.0, availableHeight * 0.12), - ); - final isNarrow = availableWidth < 460.0; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // ── Compact header ────────────────────────────────────── - Row( - children: [ - Icon( - Icons.usb, - size: iconSize.clamp(24.0, 40.0), - color: theme.colorScheme.primary, - ), - SizedBox(width: gap), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - l10n.usbScreenTitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - Text( - l10n.usbScreenSubtitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - SizedBox(height: gap), - // ── Port list takes all remaining space ───────────────── - Expanded(child: _buildPortList(context)), - if (_errorText != null) ...[ - SizedBox(height: gap * 0.5), - Text( - _errorText!, - textAlign: TextAlign.center, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.error, - ), - ), - ], - SizedBox(height: gap), - // ── Action buttons ────────────────────────────────────── - if (isNarrow) - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!_supportsHotPlug) ...[ - OutlinedButton.icon( - onPressed: _isLoadingPorts || _isConnecting - ? null - : () { - appLogger.info( - 'Refresh ports pressed', - tag: 'UsbScreen', - ); - _loadPorts(); - }, - icon: const Icon(Icons.refresh), - label: Text(l10n.repeater_refresh), - ), - SizedBox(height: gap), - ], - FilledButton.icon( - onPressed: _canConnect - ? () { - final rawPortName = normalizeUsbPortName( - _selectedPort!, - ); - appLogger.info( - 'Connect pressed for $_selectedPort (raw: $rawPortName)', - tag: 'UsbScreen', - ); - _connectSelectedPort(); - } - : null, - icon: _isConnecting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon(Icons.usb), - label: Text(l10n.common_connect), - ), - ], - ) - else - Row( - children: [ - if (!_supportsHotPlug) ...[ - Expanded( - child: OutlinedButton.icon( - onPressed: _isLoadingPorts || _isConnecting - ? null - : () { - appLogger.info( - 'Refresh ports pressed', - tag: 'UsbScreen', - ); - _loadPorts(); - }, - icon: const Icon(Icons.refresh), - label: Text(l10n.repeater_refresh), - ), - ), - SizedBox(width: gap), - ], - Expanded( - child: FilledButton.icon( - onPressed: _canConnect - ? () { - final rawPortName = normalizeUsbPortName( - _selectedPort!, - ); - appLogger.info( - 'Connect pressed for $_selectedPort (raw: $rawPortName)', - tag: 'UsbScreen', - ); - _connectSelectedPort(); - } - : null, - icon: _isConnecting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon(Icons.usb), - label: Text(l10n.common_connect), - ), - ), - ], - ), - SizedBox(height: math.max(4.0, gap * 0.5)), - Text( - l10n.usbScreenNote, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), + top: false, + child: Consumer( + builder: (context, connector, child) { + return Column( + children: [ + _buildStatusBar(context, connector), + Expanded(child: _buildPortList(context, connector)), + ], ); }, ), ), + bottomNavigationBar: Consumer( + builder: (context, connector, child) { + final isLoading = _isLoadingPorts; + final showBle = PlatformInfo.isWeb || + PlatformInfo.isAndroid || + PlatformInfo.isIOS; + + return SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (showBle) + FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => const ScannerScreen(), + ), + ); + }, + heroTag: 'usb_ble_action', + icon: const Icon(Icons.bluetooth), + label: Text(context.l10n.connectionChoiceBluetoothLabel), + ), + if (showBle) const SizedBox(width: 12), + if (!_supportsHotPlug) + FloatingActionButton.extended( + onPressed: isLoading ? null : _loadPorts, + heroTag: 'usb_refresh_action', + icon: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + label: Text(context.l10n.repeater_refresh), + ), + ], + ), + ); + }, + ), ); } - bool get _canConnect => - !_isLoadingPorts && - !_isConnecting && - _selectedPort != null && - _selectedPort!.isNotEmpty; + Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + String statusText; + Color statusColor; + + if (_isLoadingPorts) { + statusText = l10n.usbStatus_searching; + statusColor = Colors.blue; + } else if (connector.isUsbTransportConnected) { + switch (connector.state) { + case MeshCoreConnectionState.connected: + statusText = l10n.scanner_connectedTo( + connector.activeUsbPortDisplayLabel ?? 'USB', + ); + statusColor = Colors.green; + case MeshCoreConnectionState.disconnecting: + statusText = l10n.scanner_disconnecting; + statusColor = Colors.orange; + default: + statusText = l10n.usbStatus_notConnected; + statusColor = Colors.grey; + } + } else if (connector.state == MeshCoreConnectionState.connecting && + connector.activeTransport == MeshCoreTransportType.usb) { + statusText = l10n.usbStatus_connecting; + statusColor = Colors.orange; + } else { + statusText = l10n.usbStatus_notConnected; + statusColor = Colors.grey; + } + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + color: statusColor.withValues(alpha: 0.1), + child: Row( + children: [ + Icon(Icons.circle, size: 12, color: statusColor), + const SizedBox(width: 8), + Text( + statusText, + style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), + ), + ], + ), + ); + } + + Widget _buildPortList(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + + if (_isLoadingPorts) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.usb, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + l10n.usbStatus_searching, + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ], + ), + ); + } + + if (_ports.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.usb, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + l10n.usbScreenEmptyState, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + ], + ), + ); + } + + final isConnecting = + connector.state == MeshCoreConnectionState.connecting && + connector.activeTransport == MeshCoreTransportType.usb; + + return ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: _ports.length, + separatorBuilder: (context, index) => const Divider(), + itemBuilder: (context, index) { + final port = _ports[index]; + final displayName = friendlyUsbPortName(port); + final rawName = normalizeUsbPortName(port); + final showRawName = + rawName != displayName && !rawName.startsWith('web:'); + + return ListTile( + leading: const Icon(Icons.usb), + title: Text( + displayName, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: showRawName ? Text(rawName) : null, + trailing: ElevatedButton( + onPressed: + isConnecting ? null : () => _connectPort(port), + child: Text(l10n.common_connect), + ), + onTap: isConnecting ? null : () => _connectPort(port), + ); + }, + ); + } void _startHotPlugTimer() { if (!_supportsHotPlug) return; @@ -340,9 +278,10 @@ class _UsbScreenState extends State { } Future _pollHotPlug() async { - // Don't interfere with an active connection attempt or initial load. - if (_isConnecting || _isLoadingPorts) return; + if (_isLoadingPorts) return; if (!mounted) return; + // Don't poll while connecting or connected. + if (_connector.state != MeshCoreConnectionState.disconnected) return; try { final ports = await _connector.listUsbPorts(); if (!mounted) return; @@ -353,186 +292,72 @@ class _UsbScreenState extends State { _ports ..clear() ..addAll(ports); - if (_ports.isEmpty) { - _selectedPort = null; - } else if (added.isNotEmpty) { - // Auto-select the newly-connected device. - _selectedPort = added.first; - } else if (_selectedPort != null && !_ports.contains(_selectedPort)) { - // Previously-selected device was unplugged. - _selectedPort = _ports.isNotEmpty ? _ports.first : null; - } }); } catch (_) { // Silent — hot-plug failures are non-critical. } } - Widget _buildPortList(BuildContext context) { - final theme = Theme.of(context); - final l10n = context.l10n; - - if (_isLoadingPorts) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 12), - Text(l10n.common_loading), - ], - ), - ); - } - - if (_ports.isEmpty) { - return Center( - child: Text( - l10n.usbScreenEmptyState, - textAlign: TextAlign.center, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ); - } - - return ListView.separated( - itemCount: _ports.length, - itemBuilder: (context, index) { - final port = _ports[index]; - final isSelected = port == _selectedPort; - final displayName = _friendlyPortName(port); - final rawName = normalizeUsbPortName(port); - final showRawName = - rawName != displayName && !rawName.startsWith('web:'); - return Material( - color: isSelected - ? theme.colorScheme.primaryContainer - : theme.colorScheme.surfaceContainerLow, - borderRadius: BorderRadius.circular(16), - child: ListTile( - onTap: _isConnecting - ? null - : () { - setState(() { - _selectedPort = port; - _errorText = null; - }); - appLogger.info('Selected port $port', tag: 'UsbScreen'); - }, - leading: Icon( - Icons.usb, - color: isSelected - ? theme.colorScheme.onPrimaryContainer - : theme.colorScheme.onSurfaceVariant, - ), - title: Text( - displayName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium?.copyWith( - color: isSelected ? theme.colorScheme.onPrimaryContainer : null, - ), - ), - subtitle: showRawName - ? Text( - rawName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: isSelected - ? theme.colorScheme.onPrimaryContainer - : theme.colorScheme.onSurfaceVariant, - ), - ) - : null, - trailing: isSelected - ? Icon( - Icons.check_circle, - color: theme.colorScheme.onPrimaryContainer, - ) - : null, - ), - ); - }, - separatorBuilder: (context, index) => const SizedBox(height: 10), - ); - } - Future _loadPorts() async { if (!mounted) return; - _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); + _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); setState(() { _isLoadingPorts = true; - _errorText = null; }); try { - final ports = await _usbConnector.listPorts(); + final ports = await _connector.listUsbPorts(); if (!mounted) return; setState(() { _ports ..clear() ..addAll(ports); - if (_ports.isEmpty) { - _selectedPort = null; - } else if (!_ports.contains(_selectedPort)) { - _selectedPort = _ports.first; - } _isLoadingPorts = false; }); } catch (error) { if (!mounted) return; setState(() { _ports.clear(); - _selectedPort = null; - _errorText = _friendlyErrorMessage(error); _isLoadingPorts = false; }); + _showError(error); } } - Future _connectSelectedPort() async { - final selectedPort = _selectedPort; - if (selectedPort == null || selectedPort.isEmpty) { - return; - } - _usbConnector.setRequestPortLabel(context.l10n.usbScreenStatus); - if (_usbConnector.state != MeshCoreConnectionState.disconnected) { - setState(() { - _isConnecting = false; - _errorText = null; - }); - return; - } - final rawPortName = normalizeUsbPortName(selectedPort); + Future _connectPort(String port) async { + if (_connector.state != MeshCoreConnectionState.disconnected) return; - setState(() { - _isConnecting = true; - _errorText = null; - }); + final rawPortName = normalizeUsbPortName(port); + appLogger.info('Connect tapped for $port (raw: $rawPortName)', + tag: 'UsbScreen'); try { - await _usbConnector.connect(portName: rawPortName); + await _connector.connectUsb(portName: rawPortName); } catch (error, stackTrace) { appLogger.error( 'Connect failed for $rawPortName: $error\n$stackTrace', tag: 'UsbScreen', ); if (!mounted) return; - setState(() { - _isConnecting = false; - _errorText = _friendlyErrorMessage(error); - }); - // Re-scan so stale or renamed port entries are cleared from the list. + _showError(error); unawaited(_loadPorts()); } } + void _showError(Object error) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(_friendlyErrorMessage(error)), + backgroundColor: Colors.red, + ), + ); + } + String _friendlyErrorMessage(Object error) { final l10n = context.l10n; + if (error is PlatformException) { switch (error.code) { case 'usb_permission_denied': @@ -546,43 +371,35 @@ class _UsbScreenState extends State { return l10n.usbErrorBusy; case 'usb_not_connected': return l10n.usbErrorNotConnected; - case 'usb_driver_missing': case 'usb_open_failed': + case 'usb_driver_missing': return l10n.usbErrorOpenFailed; case 'usb_connect_failed': - case 'usb_write_failed': - case 'usb_io_error': return l10n.usbErrorConnectFailed; } } - var msg = error.toString(); - if (msg.startsWith('Bad state: ')) { - msg = msg.substring('Bad state: '.length); - } else if (msg.startsWith('Exception: ')) { - msg = msg.substring('Exception: '.length); + if (error is UnsupportedError) { + return l10n.usbErrorUnsupported; } - switch (msg) { - case 'USB serial transport is already active': - return l10n.usbErrorAlreadyActive; - case 'No USB serial device selected': + if (error is StateError) { + final msg = error.message; + if (msg.contains('already active')) return l10n.usbErrorAlreadyActive; + if (msg.contains('No USB serial device selected')) { return l10n.usbErrorNoDeviceSelected; - case 'USB serial port is not open': + } + if (msg.contains('not open') || msg.contains('closed')) { return l10n.usbErrorPortClosed; - case 'USB serial is not supported on this platform.': - case 'Web Serial is not supported by this browser.': - return l10n.usbErrorUnsupported; - case 'Timed out waiting for SELF_INFO during connect': - return l10n.usbErrorConnectTimedOut; + } + if (msg.contains('Timed out')) return l10n.usbErrorConnectTimedOut; + if (msg.contains('Failed to open')) return l10n.usbErrorOpenFailed; } - if (msg.startsWith('Failed to open USB port ')) { - return l10n.usbErrorOpenFailed; + if (error is TimeoutException) { + return l10n.usbErrorConnectTimedOut; } - return msg; + return error.toString(); } - - String _friendlyPortName(String portLabel) => friendlyUsbPortName(portLabel); } diff --git a/lib/services/app_debug_log_service.dart b/lib/services/app_debug_log_service.dart index 6a35b17..c63e625 100644 --- a/lib/services/app_debug_log_service.dart +++ b/lib/services/app_debug_log_service.dart @@ -52,7 +52,12 @@ class AppDebugLogService extends ChangeNotifier { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, }) { - if (!_enabled) return; + if (!_enabled && !kDebugMode) return; + if (!_enabled) { + // In debug mode, still print to console but don't store entries. + debugPrint('[$tag] $message'); + return; + } _entries.add( AppDebugLogEntry( diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index b3df59f..7295376 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -1,9 +1,11 @@ +import 'dart:io' show Platform, File; import 'dart:ui'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; import '../l10n/app_localizations.dart'; +import '../utils/platform_info.dart'; class NotificationService { static final NotificationService _instance = NotificationService._internal(); @@ -75,6 +77,15 @@ class NotificationService { linux: linuxSettings, ); + // On Linux, the notifications plugin opens a D-Bus session bus + // connection whose async subscription can throw an unhandled + // SocketException when the bus socket is missing (e.g. running as + // root or inside a container without a session bus). + if (PlatformInfo.isLinux && !_isDbusSessionAvailable()) { + debugPrint('Skipping notification init: D-Bus session bus unavailable'); + return; + } + try { await _notifications.initialize( settings: initSettings, @@ -86,6 +97,16 @@ class NotificationService { } } + static bool _isDbusSessionAvailable() { + final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS']; + if (addr != null && addr.isNotEmpty) return true; + // Fallback: check the default socket for the current user. + final uid = Platform.environment['UID'] ?? + Platform.environment['EUID']; + final path = '/run/user/${uid ?? '1000'}/bus'; + return File(path).existsSync(); + } + Future _ensureInitialized() async { if (!_isInitialized) { await initialize(); diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index 69ed5b8..fca3d19 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -325,6 +325,10 @@ class UsbSerialService { // Native implementations do not use a synthetic chooser row. } + void setFallbackDeviceName(String label) { + // Native implementations use OS-provided device names. + } + void updateConnectedLabel(String label) { final trimmed = label.trim(); if (trimmed.isEmpty) { diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 4953cf5..88aa81d 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -32,6 +32,7 @@ class UsbSerialService { String? _connectedPortName; String? _connectedPortKey; String _requestPortLabel = 'Choose USB Device'; + String _fallbackDeviceName = 'Web Serial Device'; AppDebugLogService? _debugLogService; UsbSerialStatus get status => _status; @@ -77,11 +78,19 @@ class UsbSerialService { try { final requestedPortName = normalizeUsbPortName(portName); + _debugLogService?.info( + 'Web connect: requested=$requestedPortName baud=$baudRate', + tag: 'USB Serial', + ); final selectedPortKey = requestedPortName.startsWith('web:port:') ? requestedPortName : null; _port = _authorizedPortsByKey[requestedPortName]; final authorizedPorts = await _getAuthorizedPorts(); + _debugLogService?.info( + 'Web connect: ${authorizedPorts.length} authorized port(s), cached=${_port != null}', + tag: 'USB Serial', + ); _port ??= _selectPort(authorizedPorts, requestedPortName); _port ??= await _requestPort(); @@ -89,6 +98,10 @@ class UsbSerialService { throw StateError('No USB serial device selected'); } + _debugLogService?.info( + 'Web connect: opening port at $baudRate baud…', + tag: 'USB Serial', + ); await _openPort(_port!, baudRate); _connectedPortKey = _cachePort(_port!, preferredKey: selectedPortKey); _connectedPortName = _displayLabelForPort( @@ -105,6 +118,10 @@ class UsbSerialService { tag: 'USB Serial', ); } catch (error) { + _debugLogService?.error( + 'Web connect failed: $error', + tag: 'USB Serial', + ); await _cleanupFailedConnect(); _status = UsbSerialStatus.disconnected; _connectedPortName = null; @@ -194,6 +211,14 @@ class UsbSerialService { _requestPortLabel = trimmed; } + void setFallbackDeviceName(String label) { + final trimmed = label.trim(); + if (trimmed.isEmpty) { + return; + } + _fallbackDeviceName = trimmed; + } + void setDebugLogService(AppDebugLogService? service) { _debugLogService = service; } @@ -403,6 +428,7 @@ class UsbSerialService { vendorId: hasVendor ? vendorId : null, productId: hasProduct ? productId : null, requestPortLabel: _requestPortLabel, + fallbackDeviceName: _fallbackDeviceName, knownUsbNames: _knownUsbNames, ); } diff --git a/lib/utils/usb_port_labels.dart b/lib/utils/usb_port_labels.dart index 1eb8796..05dfc85 100644 --- a/lib/utils/usb_port_labels.dart +++ b/lib/utils/usb_port_labels.dart @@ -31,6 +31,7 @@ String describeWebUsbPort({ required int? vendorId, required int? productId, String requestPortLabel = 'Choose USB Device', + String fallbackDeviceName = 'Web Serial Device', Map knownUsbNames = const {}, }) { if (vendorId == null && productId == null) { @@ -43,7 +44,7 @@ String describeWebUsbPort({ ? knownUsbNames['${vendorHex.toLowerCase()}:${productHex.toLowerCase()}'] : null; - final parts = [knownName ?? 'Web Serial Device']; + final parts = [knownName ?? fallbackDeviceName]; if (vendorHex != null) { parts.add('VID:$vendorHex'); } diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index f3becea..1f592eb 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -78,40 +78,36 @@ class _SNRIndicatorState extends State { widget.connector.currentSf, ); - return InkWell( - onTap: () { - if (directRepeater != null) { - _showFullPathDialog(context, directBestRepeaters); - } - }, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(snrUi.icon, size: 18, color: snrUi.color), - Text( - snrUi.text, - style: TextStyle(fontSize: 12, color: snrUi.color), - ), - ], - ), - if (directRepeater != null) + return ConstrainedBox( + constraints: const BoxConstraints(minWidth: 40, minHeight: 40), + child: InkWell( + onTap: directRepeater != null + ? () => _showFullPathDialog(context, directBestRepeaters) + : null, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(snrUi.icon, size: 18, color: snrUi.color), Text( - '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Colors.grey, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + snrUi.text, + style: TextStyle(fontSize: 12, color: snrUi.color), ), - ], + if (directRepeater != null) + Text( + '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ), ); @@ -148,8 +144,10 @@ class _SNRIndicatorState extends State { builder: (context) => AlertDialog( title: Text(l10n.snrIndicator_nearByRepeaters), content: SizedBox( + width: double.maxFinite, child: Scrollbar( child: ListView.separated( + shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 4), itemCount: directBestRepeaters.length, separatorBuilder: (_, _) => const Divider(height: 1), diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 12ecdbe..1d8b504 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -19,6 +19,7 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { final List _ports; String? requestPortLabel; + String? fallbackDeviceName; int connectUsbCalls = 0; String? lastConnectPortName; String? fakeActiveUsbPort; @@ -30,6 +31,9 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { @override MeshCoreConnectionState get state => initialState; + @override + MeshCoreTransportType get activeTransport => MeshCoreTransportType.usb; + @override String? get activeUsbPort => fakeActiveUsbPort; @@ -64,6 +68,11 @@ class _FakeMeshCoreConnector extends MeshCoreConnector { void setUsbRequestPortLabel(String label) { requestPortLabel = label; } + + @override + void setUsbFallbackDeviceName(String label) { + fallbackDeviceName = label; + } } Widget _buildTestApp({ @@ -107,16 +116,23 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + )); await tester.pump(); expect(connector.connectUsbCalls, 0); - expect(find.byType(CircularProgressIndicator), findsNothing); + + // UsbScreen.dispose() schedules disconnect work that debounces notify. + // Drain that debounce timer before test teardown. + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(milliseconds: 60)); }, ); testWidgets( - 'UsbScreen keeps raw selection when connector USB display label changes', + 'UsbScreen sends raw port name when tapping Connect', (tester) async { final connector = _FakeMeshCoreConnector( ports: ['COM6 - USB Serial Device (COM6)'], @@ -127,12 +143,10 @@ void main() { ); await tester.pumpAndSettle(); - connector.fakeActiveUsbPortDisplayLabel = - 'COM6 - KD3CGK mesh-utility.org'; - connector.notifyListeners(); - await tester.pump(const Duration(milliseconds: 60)); - - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + )); await tester.pump(); expect(connector.connectUsbCalls, 1); @@ -163,7 +177,8 @@ void main() { }); group('Error Handling', () { - testWidgets('shows error message when listing ports fails', (tester) async { + testWidgets('shows error SnackBar when listing ports fails', + (tester) async { final connector = _FakeMeshCoreConnector(); connector.listUsbPortsImpl = () async { throw PlatformException( @@ -180,7 +195,7 @@ void main() { expect(find.text('USB permission was denied.'), findsOneWidget); }); - testWidgets('connection failure completes without leaving loading state', ( + testWidgets('connection failure shows SnackBar error', ( tester, ) async { final connector = _FakeMeshCoreConnector(ports: ['COM1']); @@ -195,11 +210,17 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + )); await tester.pumpAndSettle(); expect(connectAttempted, isTrue); - expect(find.byType(CircularProgressIndicator), findsNothing); + expect( + find.text('Another USB connection request is already in progress.'), + findsOneWidget, + ); }); }); } From 421bc71bb7fd568db86c68b1c131a4a41ac66a4f Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 7 Mar 2026 12:55:15 -0700 Subject: [PATCH 281/421] Enhance USB port opening and reading logic with improved error handling and debug logging --- lib/services/usb_serial_service_web.dart | 37 +++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 88aa81d..86f0b1e 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -268,9 +268,23 @@ class UsbSerialService { return null; } - Future _openPort(JSObject port, int baudRate) { - final options = JSObject()..['baudRate'] = baudRate.toJS; - return port.callMethod>('open'.toJS, options).toDart; + Future _openPort(JSObject port, int baudRate) async { + final options = JSObject() + ..['baudRate'] = baudRate.toJS + ..['flowControl'] = 'none'.toJS; + await port.callMethod>('open'.toJS, options).toDart; + + // Prevent ESP32 USB-CDC reset: hold DTR=true, RTS=false after open. + try { + final signals = JSObject() + ..['dataTerminalReady'] = true.toJS + ..['requestToSend'] = false.toJS; + await port + .callMethod>('setSignals'.toJS, signals) + .toDart; + } catch (_) { + // setSignals may not be supported on all browsers/devices. + } } Future _cleanupFailedConnect() async { @@ -324,8 +338,12 @@ class UsbSerialService { Future _pumpReads() async { final reader = _reader; - if (reader == null) return; + if (reader == null) { + _debugLogService?.warn('_pumpReads: reader is null', tag: 'USB Serial'); + return; + } + _debugLogService?.info('_pumpReads: started', tag: 'USB Serial'); try { while (_status == UsbSerialStatus.connected && identical(reader, _reader)) { @@ -333,6 +351,7 @@ class UsbSerialService { .callMethod>('read'.toJS) .toDart; if (result == null) { + _debugLogService?.warn('_pumpReads: null result', tag: 'USB Serial'); break; } final resultObject = result as JSObject; @@ -340,20 +359,30 @@ class UsbSerialService { final doneValue = resultObject.getProperty('done'.toJS); final done = doneValue != null && doneValue.dartify() == true; if (done) { + _debugLogService?.info('_pumpReads: done=true', tag: 'USB Serial'); break; } final value = resultObject.getProperty('value'.toJS); final bytes = _coerceBytes(value); if (bytes != null && bytes.isNotEmpty) { + _debugLogService?.info( + 'USB RX raw: ${bytes.length} byte(s)', + tag: 'USB Serial', + ); _ingestRawBytes(bytes); } } } catch (error, stackTrace) { + _debugLogService?.error( + '_pumpReads error: $error', + tag: 'USB Serial', + ); if (_status == UsbSerialStatus.connected) { _addFrameError(error, stackTrace); } } finally { + _debugLogService?.info('_pumpReads: ended', tag: 'USB Serial'); _releaseLock(reader); if (_status == UsbSerialStatus.connected && identical(reader, _reader)) { _addFrameError(StateError('USB serial connection closed')); From e1327a93c74d82a8ce4acb0014e9cbef97e26d0d Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 7 Mar 2026 13:00:23 -0700 Subject: [PATCH 282/421] Fix contact sync fallback when channel 0 never arrives On web BLE, contact sync is deferred until channel 0 arrives via _handleChannelInfo. If channel 0 times out or channel sync completes without it, _pendingInitialContactsSync stays true and contacts never load. Add fallback in _cleanupChannelSync to trigger getContacts() if the flag is still set when channel sync ends. --- lib/connector/meshcore_connector.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 4fa8412..0c58436 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2250,6 +2250,14 @@ class MeshCoreConnector extends ChangeNotifier { _hasLoadedChannels = true; _previousChannelsCache.clear(); } + + // Fallback: if contact sync was deferred waiting for channel 0 but + // channel sync finished without triggering it, start contacts now. + if (_pendingInitialContactsSync && isConnected) { + _pendingInitialContactsSync = false; + unawaited(getContacts()); + } + // Keep cache on failure/disconnection for future attempts } From b2da695102023bd2262a9abbcdebe4ec3f55227a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 7 Mar 2026 13:01:27 -0700 Subject: [PATCH 283/421] Run dart format --- lib/connector/meshcore_connector.dart | 5 +- lib/connector/meshcore_connector_usb.dart | 8 ++- lib/screens/usb_screen.dart | 14 +++-- lib/services/notification_service.dart | 3 +- lib/services/usb_serial_service_web.dart | 10 +--- test/screens/usb_flow_test.dart | 68 ++++++++++++----------- 6 files changed, 53 insertions(+), 55 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 0c58436..d4a183a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -906,10 +906,7 @@ class MeshCoreConnector extends ChangeNotifier { try { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; - _appDebugLogService?.info( - 'connectUsb: opening serial port…', - tag: 'USB', - ); + _appDebugLogService?.info('connectUsb: opening serial port…', tag: 'USB'); await _usbManager.connect(portName: portName, baudRate: baudRate); _appDebugLogService?.info( 'connectUsb: serial port opened, label=${_usbManager.activePortDisplayLabel}', diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart index a0f70d5..376ae1a 100644 --- a/lib/connector/meshcore_connector_usb.dart +++ b/lib/connector/meshcore_connector_usb.dart @@ -24,8 +24,7 @@ class MeshCoreUsbManager { // --- Configuration --- Future> listPorts() => _service.listPorts(); - void setRequestPortLabel(String label) => - _service.setRequestPortLabel(label); + void setRequestPortLabel(String label) => _service.setRequestPortLabel(label); void setFallbackDeviceName(String label) => _service.setFallbackDeviceName(label); @@ -36,7 +35,10 @@ class MeshCoreUsbManager { } // --- Connection lifecycle --- - Future connect({required String portName, int baudRate = 115200}) async { + Future connect({ + required String portName, + int baudRate = 115200, + }) async { _debugLog?.info( 'UsbManager.connect: portName=$portName baud=$baudRate', tag: 'USB', diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index c78e0c1..e230a54 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -107,7 +107,8 @@ class _UsbScreenState extends State { bottomNavigationBar: Consumer( builder: (context, connector, child) { final isLoading = _isLoadingPorts; - final showBle = PlatformInfo.isWeb || + final showBle = + PlatformInfo.isWeb || PlatformInfo.isAndroid || PlatformInfo.isIOS; @@ -238,7 +239,7 @@ class _UsbScreenState extends State { final isConnecting = connector.state == MeshCoreConnectionState.connecting && - connector.activeTransport == MeshCoreTransportType.usb; + connector.activeTransport == MeshCoreTransportType.usb; return ListView.separated( padding: const EdgeInsets.all(8), @@ -259,8 +260,7 @@ class _UsbScreenState extends State { ), subtitle: showRawName ? Text(rawName) : null, trailing: ElevatedButton( - onPressed: - isConnecting ? null : () => _connectPort(port), + onPressed: isConnecting ? null : () => _connectPort(port), child: Text(l10n.common_connect), ), onTap: isConnecting ? null : () => _connectPort(port), @@ -329,8 +329,10 @@ class _UsbScreenState extends State { if (_connector.state != MeshCoreConnectionState.disconnected) return; final rawPortName = normalizeUsbPortName(port); - appLogger.info('Connect tapped for $port (raw: $rawPortName)', - tag: 'UsbScreen'); + appLogger.info( + 'Connect tapped for $port (raw: $rawPortName)', + tag: 'UsbScreen', + ); try { await _connector.connectUsb(portName: rawPortName); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 7295376..95326d2 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -101,8 +101,7 @@ class NotificationService { final addr = Platform.environment['DBUS_SESSION_BUS_ADDRESS']; if (addr != null && addr.isNotEmpty) return true; // Fallback: check the default socket for the current user. - final uid = Platform.environment['UID'] ?? - Platform.environment['EUID']; + final uid = Platform.environment['UID'] ?? Platform.environment['EUID']; final path = '/run/user/${uid ?? '1000'}/bus'; return File(path).existsSync(); } diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 86f0b1e..4c83d7d 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -118,10 +118,7 @@ class UsbSerialService { tag: 'USB Serial', ); } catch (error) { - _debugLogService?.error( - 'Web connect failed: $error', - tag: 'USB Serial', - ); + _debugLogService?.error('Web connect failed: $error', tag: 'USB Serial'); await _cleanupFailedConnect(); _status = UsbSerialStatus.disconnected; _connectedPortName = null; @@ -374,10 +371,7 @@ class UsbSerialService { } } } catch (error, stackTrace) { - _debugLogService?.error( - '_pumpReads error: $error', - tag: 'USB Serial', - ); + _debugLogService?.error('_pumpReads error: $error', tag: 'USB Serial'); if (_status == UsbSerialStatus.connected) { _addFrameError(error, stackTrace); } diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 1d8b504..436d230 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -116,10 +116,12 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.ancestor( - of: find.text('Connect'), - matching: find.bySubtype(), - )); + await tester.tap( + find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + ), + ); await tester.pump(); expect(connector.connectUsbCalls, 0); @@ -131,28 +133,29 @@ void main() { }, ); - testWidgets( - 'UsbScreen sends raw port name when tapping Connect', - (tester) async { - final connector = _FakeMeshCoreConnector( - ports: ['COM6 - USB Serial Device (COM6)'], - ); + testWidgets('UsbScreen sends raw port name when tapping Connect', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector( + ports: ['COM6 - USB Serial Device (COM6)'], + ); - await tester.pumpWidget( - _buildTestApp(connector: connector, child: const UsbScreen()), - ); - await tester.pumpAndSettle(); + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); - await tester.tap(find.ancestor( - of: find.text('Connect'), - matching: find.bySubtype(), - )); - await tester.pump(); + await tester.tap( + find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + ), + ); + await tester.pump(); - expect(connector.connectUsbCalls, 1); - expect(connector.lastConnectPortName, 'COM6'); - }, - ); + expect(connector.connectUsbCalls, 1); + expect(connector.lastConnectPortName, 'COM6'); + }); testWidgets('ScannerScreen USB action reflects platform support', ( tester, @@ -177,8 +180,9 @@ void main() { }); group('Error Handling', () { - testWidgets('shows error SnackBar when listing ports fails', - (tester) async { + testWidgets('shows error SnackBar when listing ports fails', ( + tester, + ) async { final connector = _FakeMeshCoreConnector(); connector.listUsbPortsImpl = () async { throw PlatformException( @@ -195,9 +199,7 @@ void main() { expect(find.text('USB permission was denied.'), findsOneWidget); }); - testWidgets('connection failure shows SnackBar error', ( - tester, - ) async { + testWidgets('connection failure shows SnackBar error', (tester) async { final connector = _FakeMeshCoreConnector(ports: ['COM1']); var connectAttempted = false; connector.connectUsbImpl = ({required String portName}) async { @@ -210,10 +212,12 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.ancestor( - of: find.text('Connect'), - matching: find.bySubtype(), - )); + await tester.tap( + find.ancestor( + of: find.text('Connect'), + matching: find.bySubtype(), + ), + ); await tester.pumpAndSettle(); expect(connectAttempted, isTrue); From 06fa176367b30f977b4a4afedb09e47950902d0b Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 7 Mar 2026 13:10:42 -0700 Subject: [PATCH 284/421] Narrow macOS sandbox entitlement to /dev/cu. and /dev/tty. only The /dev/ prefix granted read/write to all device nodes. The app only needs access to serial port devices (/dev/cu.* and /dev/tty.*) for USB LoRa communication. --- macos/Runner/DebugProfile.entitlements | 4 +++- macos/Runner/Release.entitlements | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 17455ef..2fcad1b 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -14,9 +14,11 @@ com.apple.security.device.usb + com.apple.security.temporary-exception.files.absolute-path.read-write - /dev/ + /dev/cu. + /dev/tty. com.apple.security.device.camera diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 11bd5b8..2b1c694 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -10,9 +10,11 @@ com.apple.security.device.usb + com.apple.security.temporary-exception.files.absolute-path.read-write - /dev/ + /dev/cu. + /dev/tty. com.apple.security.device.camera From 90c8cf5f3e4bc3f59b2b15518e28fb0c528256d9 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 7 Mar 2026 13:12:45 -0700 Subject: [PATCH 285/421] Add TODO to switch flserial to official repo --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 692326a..82e4d9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 flutter_blue_plus: ^2.1.0 + # TODO: Switch to official flserial repo once changes are upstreamed flserial: git: url: https://github.com/MeshEnvy/flserial.git From 7a2bb20bf732da4c7dbb8ef7f161c5b90d991c0e Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sat, 7 Mar 2026 20:07:19 -0500 Subject: [PATCH 286/421] feat: Add TCP connection support and UI integration - Implemented TCP transport service for native platforms. - Added TCP connection screen with input fields for host and port. - Integrated TCP connection options into the scanner and USB screens. - Updated localization files for new TCP-related strings. - Added tests for TCP connection flow and error handling. - Enhanced USB screen to include TCP connection option. - Improved layout to ensure no overflow in narrow widths for scanner and USB screens. --- lib/connector/meshcore_connector.dart | 99 ++++++- lib/connector/meshcore_connector_tcp.dart | 37 +++ lib/connector/meshcore_connector_usb.dart | 3 + lib/l10n/app_bg.arb | 30 +- lib/l10n/app_de.arb | 30 +- lib/l10n/app_en.arb | 30 +- lib/l10n/app_es.arb | 30 +- lib/l10n/app_fr.arb | 30 +- lib/l10n/app_it.arb | 30 +- lib/l10n/app_localizations.dart | 84 ++++++ lib/l10n/app_localizations_bg.dart | 47 +++ lib/l10n/app_localizations_de.dart | 50 ++++ lib/l10n/app_localizations_en.dart | 47 +++ lib/l10n/app_localizations_es.dart | 47 +++ lib/l10n/app_localizations_fr.dart | 49 ++++ lib/l10n/app_localizations_it.dart | 48 ++++ lib/l10n/app_localizations_nl.dart | 48 ++++ lib/l10n/app_localizations_pl.dart | 49 ++++ lib/l10n/app_localizations_pt.dart | 49 ++++ lib/l10n/app_localizations_ru.dart | 48 ++++ lib/l10n/app_localizations_sk.dart | 47 +++ lib/l10n/app_localizations_sl.dart | 47 +++ lib/l10n/app_localizations_sv.dart | 47 +++ lib/l10n/app_localizations_uk.dart | 48 ++++ lib/l10n/app_localizations_zh.dart | 46 +++ lib/l10n/app_nl.arb | 30 +- lib/l10n/app_pl.arb | 30 +- lib/l10n/app_pt.arb | 30 +- lib/l10n/app_ru.arb | 30 +- lib/l10n/app_sk.arb | 30 +- lib/l10n/app_sl.arb | 30 +- lib/l10n/app_sv.arb | 30 +- lib/l10n/app_uk.arb | 30 +- lib/l10n/app_zh.arb | 30 +- lib/screens/scanner_screen.dart | 116 ++++---- lib/screens/tcp_screen.dart | 272 ++++++++++++++++++ lib/screens/usb_screen.dart | 105 ++++--- lib/services/tcp_transport_service.dart | 2 + .../tcp_transport_service_native.dart | 205 +++++++++++++ lib/services/tcp_transport_service_web.dart | 35 +++ test/screens/tcp_flow_test.dart | 170 +++++++++++ test/screens/usb_flow_test.dart | 83 ++++-- .../tcp_transport_service_native_test.dart | 136 +++++++++ 43 files changed, 2391 insertions(+), 123 deletions(-) create mode 100644 lib/connector/meshcore_connector_tcp.dart create mode 100644 lib/screens/tcp_screen.dart create mode 100644 lib/services/tcp_transport_service.dart create mode 100644 lib/services/tcp_transport_service_native.dart create mode 100644 lib/services/tcp_transport_service_web.dart create mode 100644 test/screens/tcp_flow_test.dart create mode 100644 test/services/tcp_transport_service_native_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 89aeca0..135299e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -22,6 +22,7 @@ import '../services/app_settings_service.dart'; import '../services/background_service.dart'; import '../services/notification_service.dart'; import 'meshcore_connector_usb.dart'; +import 'meshcore_connector_tcp.dart'; import '../storage/channel_message_store.dart'; import '../storage/channel_order_store.dart'; import '../storage/channel_settings_store.dart'; @@ -86,7 +87,7 @@ enum MeshCoreConnectionState { disconnecting, } -enum MeshCoreTransportType { bluetooth, usb } +enum MeshCoreTransportType { bluetooth, usb, tcp } class RepeaterBatterySnapshot { final int millivolts; @@ -116,6 +117,8 @@ class MeshCoreConnector extends ChangeNotifier { bool _manualDisconnect = false; final MeshCoreUsbManager _usbManager = MeshCoreUsbManager(); StreamSubscription? _usbFrameSubscription; + final MeshCoreTcpManager _tcpManager = MeshCoreTcpManager(); + StreamSubscription? _tcpFrameSubscription; MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; final List _scanResults = []; @@ -255,6 +258,10 @@ class MeshCoreConnector extends ChangeNotifier { bool get isUsbTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.usb; + String? get activeTcpEndpoint => _tcpManager.activeEndpoint; + bool get isTcpTransportConnected => + _state == MeshCoreConnectionState.connected && + _activeTransport == MeshCoreTransportType.tcp; String get deviceDisplayName { if (_selfName != null && _selfName!.isNotEmpty) { @@ -659,6 +666,7 @@ class MeshCoreConnector extends ChangeNotifier { _appDebugLogService = appDebugLogService; _backgroundService = backgroundService; _usbManager.setDebugLogService(_appDebugLogService); + _tcpManager.setDebugLogService(_appDebugLogService); // Initialize notification service _notificationService.initialize(); @@ -964,6 +972,70 @@ class MeshCoreConnector extends ChangeNotifier { } } + Future connectTcp({required String host, required int port}) async { + if (_state == MeshCoreConnectionState.connecting || + _state == MeshCoreConnectionState.connected) { + _appDebugLogService?.warn( + 'connectTcp ignored: already $_state', + tag: 'TCP', + ); + return; + } + + _appDebugLogService?.info('connectTcp: endpoint=$host:$port', tag: 'TCP'); + + await stopScan(); + _cancelReconnectTimer(); + _manualDisconnect = false; + _resetConnectionHandshakeState(); + _activeTransport = MeshCoreTransportType.tcp; + _setState(MeshCoreConnectionState.connecting); + + try { + await _tcpFrameSubscription?.cancel(); + _tcpFrameSubscription = null; + await _tcpManager.connect(host: host, port: port); + notifyListeners(); + + await Future.delayed(const Duration(milliseconds: 200)); + _tcpFrameSubscription = _tcpManager.frameStream.listen( + _handleFrame, + onError: (error, stackTrace) { + _appDebugLogService?.error('TCP transport error: $error', tag: 'TCP'); + unawaited(disconnect(manual: false)); + }, + onDone: () { + _appDebugLogService?.warn('TCP frame stream ended', tag: 'TCP'); + unawaited(disconnect(manual: false)); + }, + ); + + _setState(MeshCoreConnectionState.connected); + _pendingInitialChannelSync = true; + await _requestDeviceInfo(); + _startBatteryPolling(); + + var gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + } + if (!gotSelfInfo) { + throw StateError('Timed out waiting for SELF_INFO during TCP connect'); + } + + await syncTime(); + } catch (error) { + _appDebugLogService?.error('TCP connection error: $error', tag: 'TCP'); + await disconnect(manual: false); + rethrow; + } + } + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { @@ -1230,6 +1302,7 @@ class MeshCoreConnector extends ChangeNotifier { bool get _shouldGateInitialChannelSync => _activeTransport == MeshCoreTransportType.usb || + _activeTransport == MeshCoreTransportType.tcp || (_activeTransport == MeshCoreTransportType.bluetooth && PlatformInfo.isWeb); @@ -1276,9 +1349,11 @@ class MeshCoreConnector extends ChangeNotifier { Future disconnect({bool manual = true}) async { if (_state == MeshCoreConnectionState.disconnecting) return; final transportAtDisconnect = _activeTransport; - final transportLabel = transportAtDisconnect == MeshCoreTransportType.usb - ? 'USB' - : 'BLE'; + final transportLabel = switch (transportAtDisconnect) { + MeshCoreTransportType.bluetooth => 'BLE', + MeshCoreTransportType.usb => 'USB', + MeshCoreTransportType.tcp => 'TCP', + }; _appDebugLogService?.info( 'Starting disconnect transport=$transportLabel manual=$manual', @@ -1298,6 +1373,9 @@ class MeshCoreConnector extends ChangeNotifier { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; await _usbManager.disconnect(); + await _tcpFrameSubscription?.cancel(); + _tcpFrameSubscription = null; + await _tcpManager.disconnect(); await _notifySubscription?.cancel(); _notifySubscription = null; @@ -1379,6 +1457,8 @@ class MeshCoreConnector extends ChangeNotifier { if (_activeTransport == MeshCoreTransportType.usb) { await _usbManager.write(data); + } else if (_activeTransport == MeshCoreTransportType.tcp) { + await _tcpManager.write(data); } else { if (_rxCharacteristic == null) { throw Exception("MeshCore RX characteristic not available"); @@ -2338,7 +2418,8 @@ class MeshCoreConnector extends ChangeNotifier { } if (_pendingDeferredChannelSyncAfterContacts && (_activeTransport == MeshCoreTransportType.bluetooth || - _activeTransport == MeshCoreTransportType.usb)) { + _activeTransport == MeshCoreTransportType.usb || + _activeTransport == MeshCoreTransportType.tcp)) { _pendingDeferredChannelSyncAfterContacts = false; _pendingInitialChannelSync = false; unawaited(getChannels()); @@ -2505,14 +2586,16 @@ class MeshCoreConnector extends ChangeNotifier { if (PlatformInfo.isWeb && _activeTransport == MeshCoreTransportType.bluetooth) { _pendingInitialContactsSync = true; - } else if (_activeTransport == MeshCoreTransportType.usb) { + } else if (_activeTransport == MeshCoreTransportType.usb || + _activeTransport == MeshCoreTransportType.tcp) { _pendingDeferredChannelSyncAfterContacts = true; getContacts(); } else { getContacts(); } if (_shouldGateInitialChannelSync && - _activeTransport != MeshCoreTransportType.usb) { + _activeTransport != MeshCoreTransportType.usb && + _activeTransport != MeshCoreTransportType.tcp) { _maybeStartInitialChannelSync(); } } @@ -4274,12 +4357,14 @@ class MeshCoreConnector extends ChangeNotifier { _scanSubscription?.cancel(); _connectionSubscription?.cancel(); _usbFrameSubscription?.cancel(); + _tcpFrameSubscription?.cancel(); _notifySubscription?.cancel(); _notifyListenersTimer?.cancel(); _reconnectTimer?.cancel(); _batteryPollTimer?.cancel(); _receivedFramesController.close(); _usbManager.dispose(); + _tcpManager.dispose(); // Flush pending unread writes before disposal _unreadStore.flush(); diff --git a/lib/connector/meshcore_connector_tcp.dart b/lib/connector/meshcore_connector_tcp.dart new file mode 100644 index 0000000..01c2b92 --- /dev/null +++ b/lib/connector/meshcore_connector_tcp.dart @@ -0,0 +1,37 @@ +import 'dart:typed_data'; + +import '../services/app_debug_log_service.dart'; +import '../services/tcp_transport_service.dart'; + +class MeshCoreTcpManager { + final TcpTransportService _service = TcpTransportService(); + AppDebugLogService? _debugLog; + + String? get activeEndpoint => _service.activeEndpoint; + bool get isConnected => _service.isConnected; + Stream get frameStream => _service.frameStream; + + void setDebugLogService(AppDebugLogService? service) { + _debugLog = service; + _service.setDebugLogService(service); + } + + Future connect({required String host, required int port}) async { + _debugLog?.info('TcpManager.connect endpoint=$host:$port', tag: 'TCP'); + await _service.connect(host: host, port: port); + } + + Future disconnect() async { + if (!_service.isConnected && _service.activeEndpoint == null) { + return; + } + _debugLog?.info('TcpManager.disconnect', tag: 'TCP'); + await _service.disconnect(); + } + + Future write(Uint8List data) => _service.write(data); + + void dispose() { + _service.dispose(); + } +} diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart index 376ae1a..74e7355 100644 --- a/lib/connector/meshcore_connector_usb.dart +++ b/lib/connector/meshcore_connector_usb.dart @@ -53,6 +53,9 @@ class MeshCoreUsbManager { } Future disconnect() async { + if (!_service.isConnected && _activePortKey == null) { + return; + } _debugLog?.info('UsbManager.disconnect', tag: 'USB'); await _service.disconnect(); _activePortKey = null; diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 94d8997..972d376 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1859,5 +1859,33 @@ "usbConnectionFailed": "Неуспешно свързване през USB: {error}", "usbStatus_notConnected": "Изберете USB устройство", "usbStatus_searching": "Търсене на USB устройства...", - "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка." + "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostHint": "192.168.40.10", + "tcpScreenTitle": "Свържете се чрез TCP", + "connectionChoiceTcpLabel": "TCP", + "tcpHostLabel": "IP адрес", + "tcpPortLabel": "Пристанище", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Въведете крайната точка и свържете се.", + "tcpStatus_connecting": "Свързване към TCP крайния пункт...", + "tcpStatus_connectingTo": "Свързване към {endpoint}...", + "tcpErrorHostRequired": "Необходим е IP адрес.", + "tcpErrorPortInvalid": "Портът трябва да бъде между 1 и 65535.", + "tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.", + "tcpErrorTimedOut": "Връзката TCP изтекла.", + "tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9ba0f51..d6ff0b0 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1887,5 +1887,33 @@ "usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus", "usbStatus_connecting": "Verbindung zum USB-Gerät...", "usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}", - "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält." + "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostLabel": "IP-Adresse", + "connectionChoiceTcpLabel": "TCP", + "tcpHostHint": "192.168.40.10", + "tcpScreenTitle": "Verbinden über TCP", + "tcpPortLabel": "Hafen", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.", + "tcpStatus_connecting": "Verbindung zum TCP-Endpunkt hergestellt...", + "tcpStatus_connectingTo": "Verbindung zu {endpoint}...", + "tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.", + "tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.", + "tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.", + "tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.", + "tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2605628..544d574 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -49,6 +49,34 @@ "scanner_title": "MeshCore Open", "connectionChoiceUsbLabel": "USB", "connectionChoiceBluetoothLabel": "Bluetooth", + "connectionChoiceTcpLabel": "TCP", + "tcpScreenTitle": "Connect over TCP", + "tcpHostLabel": "IP Address", + "tcpHostHint": "192.168.40.10", + "tcpPortLabel": "Port", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Enter endpoint and connect", + "tcpStatus_connecting": "Connecting to TCP endpoint...", + "tcpStatus_connectingTo": "Connecting to {endpoint}...", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "tcpErrorHostRequired": "IP address is required.", + "tcpErrorPortInvalid": "Port must be between 1 and 65535.", + "tcpErrorUnsupported": "TCP transport is not supported on this platform.", + "tcpErrorTimedOut": "TCP connection timed out.", + "tcpConnectionFailed": "TCP connection failed: {error}", + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, "usbScreenTitle": "Connect over USB", "usbScreenSubtitle": "Choose a detected serial device and connect directly to your MeshCore node.", "usbScreenStatus": "Select a USB device", @@ -1898,4 +1926,4 @@ "discoveredContacts_deleteContact": "Delete Discovered Contact", "discoveredContacts_deleteContactAll": "Delete All Discovered Contacts", "discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?" -} \ No newline at end of file +} diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9b791d3..dcad901 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1887,5 +1887,33 @@ "usbStatus_searching": "Buscando dispositivos USB...", "usbStatus_notConnected": "Seleccione un dispositivo USB", "usbConnectionFailed": "Error al conectar mediante USB: {error}", - "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion." + "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpScreenTitle": "Establecer conexión a través de TCP", + "connectionChoiceTcpLabel": "TCP", + "tcpHostHint": "192.168.40.10", + "tcpHostLabel": "Dirección IP", + "tcpPortLabel": "Puerto", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Ingrese la dirección final y conecte.", + "tcpStatus_connecting": "Conectándose al punto final TCP...", + "tcpStatus_connectingTo": "Conectándose a {endpoint}...", + "tcpErrorHostRequired": "Se requiere la dirección IP.", + "tcpErrorPortInvalid": "El puerto debe estar entre 1 y 65535.", + "tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.", + "tcpErrorTimedOut": "La conexión TCP ha caducado.", + "tcpConnectionFailed": "Error en la conexión TCP: {error}" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a7bedc9..e934b7e 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1859,5 +1859,33 @@ "usbConnectionFailed": "Échec de la connexion USB : {error}", "usbStatus_connecting": "Connexion au périphérique USB...", "usbStatus_searching": "Recherche de périphériques USB...", - "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion." + "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostLabel": "Adresse IP", + "connectionChoiceTcpLabel": "TCP", + "tcpScreenTitle": "Établir une connexion via TCP", + "tcpHostHint": "192.168.40.10", + "tcpPortLabel": "Port", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Entrez l'adresse de destination et connectez-vous.", + "tcpStatus_connecting": "Connexion au point de terminaison TCP...", + "tcpStatus_connectingTo": "Connexion à {endpoint}...", + "tcpErrorHostRequired": "Une adresse IP est obligatoire.", + "tcpErrorPortInvalid": "La taille du port doit être comprise entre 1 et 65535.", + "tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.", + "tcpErrorTimedOut": "La connexion TCP a expiré.", + "tcpConnectionFailed": "Échec de la connexion TCP : {error}" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 423ff40..30b19b0 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1859,5 +1859,33 @@ "usbConnectionFailed": "Errore nella connessione USB: {error}", "usbStatus_notConnected": "Seleziona un dispositivo USB", "usbStatus_connecting": "Connessione al dispositivo USB...", - "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion." + "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostLabel": "Indirizzo IP", + "tcpHostHint": "192.168.40.10", + "connectionChoiceTcpLabel": "TCP", + "tcpScreenTitle": "Stabilire una connessione tramite TCP", + "tcpPortLabel": "Porto", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Inserisci l'endpoint e connettiti.", + "tcpStatus_connecting": "Connessione al punto finale TCP...", + "tcpStatus_connectingTo": "Connessione a {endpoint}...", + "tcpErrorHostRequired": "È necessario fornire un indirizzo IP.", + "tcpErrorPortInvalid": "La dimensione della porta deve essere compresa tra 1 e 65535.", + "tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.", + "tcpErrorTimedOut": "La connessione TCP è scaduta.", + "tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8d3f86b..536d7fb 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -334,6 +334,90 @@ abstract class AppLocalizations { /// **'Bluetooth'** String get connectionChoiceBluetoothLabel; + /// No description provided for @connectionChoiceTcpLabel. + /// + /// In en, this message translates to: + /// **'TCP'** + String get connectionChoiceTcpLabel; + + /// No description provided for @tcpScreenTitle. + /// + /// In en, this message translates to: + /// **'Connect over TCP'** + String get tcpScreenTitle; + + /// No description provided for @tcpHostLabel. + /// + /// In en, this message translates to: + /// **'IP Address'** + String get tcpHostLabel; + + /// No description provided for @tcpHostHint. + /// + /// In en, this message translates to: + /// **'192.168.40.10'** + String get tcpHostHint; + + /// No description provided for @tcpPortLabel. + /// + /// In en, this message translates to: + /// **'Port'** + String get tcpPortLabel; + + /// No description provided for @tcpPortHint. + /// + /// In en, this message translates to: + /// **'5000'** + String get tcpPortHint; + + /// No description provided for @tcpStatus_notConnected. + /// + /// In en, this message translates to: + /// **'Enter endpoint and connect'** + String get tcpStatus_notConnected; + + /// No description provided for @tcpStatus_connecting. + /// + /// In en, this message translates to: + /// **'Connecting to TCP endpoint...'** + String get tcpStatus_connecting; + + /// No description provided for @tcpStatus_connectingTo. + /// + /// In en, this message translates to: + /// **'Connecting to {endpoint}...'** + String tcpStatus_connectingTo(String endpoint); + + /// No description provided for @tcpErrorHostRequired. + /// + /// In en, this message translates to: + /// **'IP address is required.'** + String get tcpErrorHostRequired; + + /// No description provided for @tcpErrorPortInvalid. + /// + /// In en, this message translates to: + /// **'Port must be between 1 and 65535.'** + String get tcpErrorPortInvalid; + + /// No description provided for @tcpErrorUnsupported. + /// + /// In en, this message translates to: + /// **'TCP transport is not supported on this platform.'** + String get tcpErrorUnsupported; + + /// No description provided for @tcpErrorTimedOut. + /// + /// In en, this message translates to: + /// **'TCP connection timed out.'** + String get tcpErrorTimedOut; + + /// No description provided for @tcpConnectionFailed. + /// + /// In en, this message translates to: + /// **'TCP connection failed: {error}'** + String tcpConnectionFailed(String error); + /// No description provided for @usbScreenTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 356106e..2ea0e0f 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -117,6 +117,53 @@ class AppLocalizationsBg extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Свържете се чрез TCP'; + + @override + String get tcpHostLabel => 'IP адрес'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Пристанище'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Въведете крайната точка и свържете се.'; + + @override + String get tcpStatus_connecting => 'Свързване към TCP крайния пункт...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Свързване към $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Необходим е IP адрес.'; + + @override + String get tcpErrorPortInvalid => 'Портът трябва да бъде между 1 и 65535.'; + + @override + String get tcpErrorUnsupported => + 'Транспортът чрез TCP не се поддържа на тази платформа.'; + + @override + String get tcpErrorTimedOut => 'Връзката TCP изтекла.'; + + @override + String tcpConnectionFailed(String error) { + return 'Неуспешно е установено TCP връзката: $error'; + } + @override String get usbScreenTitle => 'Свържете се чрез USB'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 6353f35..495b37b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -117,6 +117,56 @@ class AppLocalizationsDe extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Verbinden über TCP'; + + @override + String get tcpHostLabel => 'IP-Adresse'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Hafen'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => + 'Geben Sie den Endpunkt ein und verbinden Sie sich.'; + + @override + String get tcpStatus_connecting => + 'Verbindung zum TCP-Endpunkt hergestellt...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Verbindung zu $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Eine IP-Adresse ist erforderlich.'; + + @override + String get tcpErrorPortInvalid => + 'Die Portnummer muss zwischen 1 und 65535 liegen.'; + + @override + String get tcpErrorUnsupported => + 'Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.'; + + @override + String get tcpErrorTimedOut => 'Die TCP-Verbindung ist abgelaufen.'; + + @override + String tcpConnectionFailed(String error) { + return 'Fehler beim TCP-Verbindungsaufbau: $error'; + } + @override String get usbScreenTitle => 'Verbinden über USB'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9c20df7..c20a6df 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -117,6 +117,53 @@ class AppLocalizationsEn extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Connect over TCP'; + + @override + String get tcpHostLabel => 'IP Address'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Port'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Enter endpoint and connect'; + + @override + String get tcpStatus_connecting => 'Connecting to TCP endpoint...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Connecting to $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'IP address is required.'; + + @override + String get tcpErrorPortInvalid => 'Port must be between 1 and 65535.'; + + @override + String get tcpErrorUnsupported => + 'TCP transport is not supported on this platform.'; + + @override + String get tcpErrorTimedOut => 'TCP connection timed out.'; + + @override + String tcpConnectionFailed(String error) { + return 'TCP connection failed: $error'; + } + @override String get usbScreenTitle => 'Connect over USB'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index eecbd48..56bba0e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -117,6 +117,53 @@ class AppLocalizationsEs extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Establecer conexión a través de TCP'; + + @override + String get tcpHostLabel => 'Dirección IP'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Puerto'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Ingrese la dirección final y conecte.'; + + @override + String get tcpStatus_connecting => 'Conectándose al punto final TCP...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Conectándose a $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Se requiere la dirección IP.'; + + @override + String get tcpErrorPortInvalid => 'El puerto debe estar entre 1 y 65535.'; + + @override + String get tcpErrorUnsupported => + 'El protocolo de transporte TCP no está soportado en esta plataforma.'; + + @override + String get tcpErrorTimedOut => 'La conexión TCP ha caducado.'; + + @override + String tcpConnectionFailed(String error) { + return 'Error en la conexión TCP: $error'; + } + @override String get usbScreenTitle => 'Conecte mediante USB'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 5cabc86..0dd88f7 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -117,6 +117,55 @@ class AppLocalizationsFr extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Établir une connexion via TCP'; + + @override + String get tcpHostLabel => 'Adresse IP'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Port'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => + 'Entrez l\'adresse de destination et connectez-vous.'; + + @override + String get tcpStatus_connecting => 'Connexion au point de terminaison TCP...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Connexion à $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Une adresse IP est obligatoire.'; + + @override + String get tcpErrorPortInvalid => + 'La taille du port doit être comprise entre 1 et 65535.'; + + @override + String get tcpErrorUnsupported => + 'Le protocole TCP n\'est pas pris en charge sur cette plateforme.'; + + @override + String get tcpErrorTimedOut => 'La connexion TCP a expiré.'; + + @override + String tcpConnectionFailed(String error) { + return 'Échec de la connexion TCP : $error'; + } + @override String get usbScreenTitle => 'Connectez via USB'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d170540..ce7b658 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -117,6 +117,54 @@ class AppLocalizationsIt extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Stabilire una connessione tramite TCP'; + + @override + String get tcpHostLabel => 'Indirizzo IP'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Porto'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Inserisci l\'endpoint e connettiti.'; + + @override + String get tcpStatus_connecting => 'Connessione al punto finale TCP...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Connessione a $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'È necessario fornire un indirizzo IP.'; + + @override + String get tcpErrorPortInvalid => + 'La dimensione della porta deve essere compresa tra 1 e 65535.'; + + @override + String get tcpErrorUnsupported => + 'Il protocollo TCP non è supportato su questa piattaforma.'; + + @override + String get tcpErrorTimedOut => 'La connessione TCP è scaduta.'; + + @override + String tcpConnectionFailed(String error) { + return 'Impossibile stabilire la connessione TCP: $error'; + } + @override String get usbScreenTitle => 'Connessione tramite USB'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 323ba34..70dbfe7 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -117,6 +117,54 @@ class AppLocalizationsNl extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Verbind via TCP'; + + @override + String get tcpHostLabel => 'IP-adres'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Haven'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Voer het eindpunt in en verbind'; + + @override + String get tcpStatus_connecting => 'Verbinding maken met TCP-eindpunt...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Verbinding maken met $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Een IP-adres is vereist.'; + + @override + String get tcpErrorPortInvalid => + 'De poortwaarde moet tussen 1 en 65535 liggen.'; + + @override + String get tcpErrorUnsupported => + 'TCP-transport wordt niet ondersteund op deze platform.'; + + @override + String get tcpErrorTimedOut => 'De TCP-verbinding is verlopen.'; + + @override + String tcpConnectionFailed(String error) { + return 'Verbinding met TCP mislukt: $error'; + } + @override String get usbScreenTitle => 'Verbind via USB'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 9175c3e..121c5fe 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -117,6 +117,55 @@ class AppLocalizationsPl extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Połącz się za pomocą protokołu TCP'; + + @override + String get tcpHostLabel => 'Adres IP'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Port'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Wprowadź adres URL i połącz'; + + @override + String get tcpStatus_connecting => 'Połączenie z punktem TCP...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Połączenie z $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Wymagana jest adresa IP.'; + + @override + String get tcpErrorPortInvalid => + 'Numer portu musi mieścić się w zakresie od 1 do 65535.'; + + @override + String get tcpErrorUnsupported => + 'Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.'; + + @override + String get tcpErrorTimedOut => + 'Połączenie TCP zakończyło się bez powodzenia.'; + + @override + String tcpConnectionFailed(String error) { + return 'Błąd połączenia TCP: $error'; + } + @override String get usbScreenTitle => 'Połącz przez USB'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ff09213..9545143 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -117,6 +117,55 @@ class AppLocalizationsPt extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Estabelecer conexão via TCP'; + + @override + String get tcpHostLabel => 'Endereço IP'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Porto'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Insira o endereço final e conecte-se.'; + + @override + String get tcpStatus_connecting => + 'Conectando ao ponto de extremidade TCP...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Conectando a $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'É necessário fornecer um endereço IP.'; + + @override + String get tcpErrorPortInvalid => + 'O valor do porto deve estar entre 1 e 65535.'; + + @override + String get tcpErrorUnsupported => + 'O protocolo TCP não é suportado nesta plataforma.'; + + @override + String get tcpErrorTimedOut => 'A conexão TCP expirou.'; + + @override + String tcpConnectionFailed(String error) { + return 'Falha na conexão TCP: $error'; + } + @override String get usbScreenTitle => 'Conecte via USB'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 69a5891..a0af1b3 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -117,6 +117,54 @@ class AppLocalizationsRu extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Установить соединение по протоколу TCP'; + + @override + String get tcpHostLabel => 'IP-адрес'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Порт'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Введите адрес и подключитесь.'; + + @override + String get tcpStatus_connecting => 'Установление соединения с TCP-портом...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Подключение к $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Необходимо указать IP-адрес.'; + + @override + String get tcpErrorPortInvalid => + 'Порт должен находиться в диапазоне от 1 до 65535.'; + + @override + String get tcpErrorUnsupported => + 'Протокол TCP не поддерживается на этой платформе.'; + + @override + String get tcpErrorTimedOut => 'Соединение TCP не удалось установить.'; + + @override + String tcpConnectionFailed(String error) { + return 'Не удалось установить соединение TCP: $error'; + } + @override String get usbScreenTitle => 'Подключение через USB'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index d0e75b0..f29c1e6 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -117,6 +117,53 @@ class AppLocalizationsSk extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Spojte sa pomocou protokolu TCP'; + + @override + String get tcpHostLabel => 'IP adresa'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Pri항'; + + @override + String get tcpPortHint => '5 000'; + + @override + String get tcpStatus_notConnected => 'Zadajte cieľovú adresu a pripojte sa.'; + + @override + String get tcpStatus_connecting => 'Pripojenie k TCP endpointu...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Pripojenie k $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Je potrebné zadať IP adresu.'; + + @override + String get tcpErrorPortInvalid => 'Číslo portu musí byť medzi 1 a 65535.'; + + @override + String get tcpErrorUnsupported => + 'Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.'; + + @override + String get tcpErrorTimedOut => 'Pripojenie TCP vypršalo.'; + + @override + String tcpConnectionFailed(String error) { + return 'Neúspešné vytvorenie TCP spojenia: $error'; + } + @override String get usbScreenTitle => 'Pripojte cez USB'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 21e3d9d..f4c6df0 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -117,6 +117,53 @@ class AppLocalizationsSl extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Komunicirajte preko protokola TCP'; + + @override + String get tcpHostLabel => 'IP naslov'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Pril'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Vnesite končni naslov in se povežite'; + + @override + String get tcpStatus_connecting => 'Povezava z TCP koncem...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Povezava z $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Potrebna je IP-naslov.'; + + @override + String get tcpErrorPortInvalid => 'Port mora biti med 1 in 65535.'; + + @override + String get tcpErrorUnsupported => + 'Transport preko protokola TCP ni podprt na tej platformi.'; + + @override + String get tcpErrorTimedOut => 'Povezava TCP je presegla časovno obdobje.'; + + @override + String tcpConnectionFailed(String error) { + return 'Napaka pri povezavi TCP: $error'; + } + @override String get usbScreenTitle => 'Povežite preko USB'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 5951fae..140e3eb 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -117,6 +117,53 @@ class AppLocalizationsSv extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'Anslut via TCP'; + + @override + String get tcpHostLabel => 'IP-adress'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Hamn'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Ange slutpunkt och anslut'; + + @override + String get tcpStatus_connecting => 'Anslutning till TCP-slutpunkt...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Anslutning till $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'IP-adress krävs.'; + + @override + String get tcpErrorPortInvalid => 'Porten måste vara mellan 1 och 65535.'; + + @override + String get tcpErrorUnsupported => + 'TCP-transport fungerar inte på denna plattform.'; + + @override + String get tcpErrorTimedOut => 'TCP-anslutningen har tidsut gått.'; + + @override + String tcpConnectionFailed(String error) { + return 'Fel vid TCP-anslutning: $error'; + } + @override String get usbScreenTitle => 'Anslut via USB'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index b8fd60a..5b28e57 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -117,6 +117,54 @@ class AppLocalizationsUk extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => 'Bluetooth'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => 'З\'єднатися через протокол TCP'; + + @override + String get tcpHostLabel => 'IP-адреса'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => 'Порт'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => 'Введіть кінцеву точку та підключіться'; + + @override + String get tcpStatus_connecting => 'Підключення до TCP-кінцевої точки...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return 'Підключення до $endpoint...'; + } + + @override + String get tcpErrorHostRequired => 'Необхідно вказати IP-адресу.'; + + @override + String get tcpErrorPortInvalid => 'Порт повинен бути в межах від 1 до 65535.'; + + @override + String get tcpErrorUnsupported => + 'Транспорт TCP не підтримується на цій платформі.'; + + @override + String get tcpErrorTimedOut => + 'З\'єднання TCP завершилося через закінчення часу очікування.'; + + @override + String tcpConnectionFailed(String error) { + return 'Не вдалося встановити з\'єднання TCP: $error'; + } + @override String get usbScreenTitle => 'Підключити через USB'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 27e6c21..2c0034c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -117,6 +117,52 @@ class AppLocalizationsZh extends AppLocalizations { @override String get connectionChoiceBluetoothLabel => '蓝牙'; + @override + String get connectionChoiceTcpLabel => 'TCP'; + + @override + String get tcpScreenTitle => '通过 TCP 连接'; + + @override + String get tcpHostLabel => 'IP地址'; + + @override + String get tcpHostHint => '192.168.40.10'; + + @override + String get tcpPortLabel => '港'; + + @override + String get tcpPortHint => '5000'; + + @override + String get tcpStatus_notConnected => '输入目标地址,然后连接'; + + @override + String get tcpStatus_connecting => '连接到 TCP 终点...'; + + @override + String tcpStatus_connectingTo(String endpoint) { + return '连接到 $endpoint...'; + } + + @override + String get tcpErrorHostRequired => '需要提供IP地址。'; + + @override + String get tcpErrorPortInvalid => '端口号必须在 1 到 65535 之间。'; + + @override + String get tcpErrorUnsupported => '此平台不支持 TCP 传输。'; + + @override + String get tcpErrorTimedOut => 'TCP 连接超时。'; + + @override + String tcpConnectionFailed(String error) { + return 'TCP 连接失败:$error'; + } + @override String get usbScreenTitle => '通过USB连接'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 94df130..29b9eca 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1859,5 +1859,33 @@ "usbStatus_notConnected": "Selecteer een USB-apparaat", "usbStatus_connecting": "Verbinding maken met USB-apparaat...", "usbStatus_searching": "Zoeken naar USB-apparaten...", - "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft." + "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpScreenTitle": "Verbind via TCP", + "tcpHostLabel": "IP-adres", + "tcpHostHint": "192.168.40.10", + "connectionChoiceTcpLabel": "TCP", + "tcpPortLabel": "Haven", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Voer het eindpunt in en verbind", + "tcpStatus_connecting": "Verbinding maken met TCP-eindpunt...", + "tcpStatus_connectingTo": "Verbinding maken met {endpoint}...", + "tcpErrorHostRequired": "Een IP-adres is vereist.", + "tcpErrorPortInvalid": "De poortwaarde moet tussen 1 en 65535 liggen.", + "tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.", + "tcpErrorTimedOut": "De TCP-verbinding is verlopen.", + "tcpConnectionFailed": "Verbinding met TCP mislukt: {error}" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d020e0e..7576cf7 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1859,5 +1859,33 @@ "usbStatus_connecting": "Połączenie z urządzeniem USB...", "usbStatus_notConnected": "Wybierz urządzenie USB", "usbConnectionFailed": "Błąd połączenia USB: {error}", - "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"." + "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "connectionChoiceTcpLabel": "TCP", + "tcpHostHint": "192.168.40.10", + "tcpScreenTitle": "Połącz się za pomocą protokołu TCP", + "tcpHostLabel": "Adres IP", + "tcpPortLabel": "Port", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Wprowadź adres URL i połącz", + "tcpStatus_connecting": "Połączenie z punktem TCP...", + "tcpStatus_connectingTo": "Połączenie z {endpoint}...", + "tcpErrorHostRequired": "Wymagana jest adresa IP.", + "tcpErrorPortInvalid": "Numer portu musi mieścić się w zakresie od 1 do 65535.", + "tcpErrorUnsupported": "Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.", + "tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.", + "tcpConnectionFailed": "Błąd połączenia TCP: {error}" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index d52cb41..428772c 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1859,5 +1859,33 @@ "usbStatus_notConnected": "Selecione um dispositivo USB", "usbConnectionFailed": "Falha na conexão USB: {error}", "usbStatus_connecting": "Conectando ao dispositivo USB...", - "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion." + "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostLabel": "Endereço IP", + "connectionChoiceTcpLabel": "TCP", + "tcpScreenTitle": "Estabelecer conexão via TCP", + "tcpHostHint": "192.168.40.10", + "tcpPortLabel": "Porto", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Insira o endereço final e conecte-se.", + "tcpStatus_connecting": "Conectando ao ponto de extremidade TCP...", + "tcpStatus_connectingTo": "Conectando a {endpoint}...", + "tcpErrorHostRequired": "É necessário fornecer um endereço IP.", + "tcpErrorPortInvalid": "O valor do porto deve estar entre 1 e 65535.", + "tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.", + "tcpErrorTimedOut": "A conexão TCP expirou.", + "tcpConnectionFailed": "Falha na conexão TCP: {error}" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 92fd55e..f42e959 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1099,5 +1099,33 @@ "usbStatus_connecting": "Подключение к USB-устройству...", "usbConnectionFailed": "Не удалось установить соединение через USB: {error}", "usbStatus_notConnected": "Выберите USB-устройство", - "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion." + "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostHint": "192.168.40.10", + "connectionChoiceTcpLabel": "TCP", + "tcpHostLabel": "IP-адрес", + "tcpScreenTitle": "Установить соединение по протоколу TCP", + "tcpPortLabel": "Порт", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Введите адрес и подключитесь.", + "tcpStatus_connecting": "Установление соединения с TCP-портом...", + "tcpStatus_connectingTo": "Подключение к {endpoint}...", + "tcpErrorHostRequired": "Необходимо указать IP-адрес.", + "tcpErrorPortInvalid": "Порт должен находиться в диапазоне от 1 до 65535.", + "tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.", + "tcpErrorTimedOut": "Соединение TCP не удалось установить.", + "tcpConnectionFailed": "Не удалось установить соединение TCP: {error}" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 141147c..57f95ab 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1859,5 +1859,33 @@ "usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}", "usbStatus_notConnected": "Vyberte USB zariadenie", "usbStatus_connecting": "Pripojenie k USB zariadeniu...", - "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion." + "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostHint": "192.168.40.10", + "tcpHostLabel": "IP adresa", + "tcpScreenTitle": "Spojte sa pomocou protokolu TCP", + "connectionChoiceTcpLabel": "TCP", + "tcpPortLabel": "Pri항", + "tcpPortHint": "5 000", + "tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.", + "tcpStatus_connecting": "Pripojenie k TCP endpointu...", + "tcpStatus_connectingTo": "Pripojenie k {endpoint}...", + "tcpErrorHostRequired": "Je potrebné zadať IP adresu.", + "tcpErrorPortInvalid": "Číslo portu musí byť medzi 1 a 65535.", + "tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.", + "tcpErrorTimedOut": "Pripojenie TCP vypršalo.", + "tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 12529d6..252c7d7 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1859,5 +1859,33 @@ "usbStatus_connecting": "Povezava z USB napravo...", "usbStatus_searching": "Iskanje USB naprav...", "usbConnectionFailed": "Napaka pri povezavi preko USB: {error}", - "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion." + "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "connectionChoiceTcpLabel": "TCP", + "tcpHostLabel": "IP naslov", + "tcpHostHint": "192.168.40.10", + "tcpScreenTitle": "Komunicirajte preko protokola TCP", + "tcpPortLabel": "Pril", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Vnesite končni naslov in se povežite", + "tcpStatus_connecting": "Povezava z TCP koncem...", + "tcpStatus_connectingTo": "Povezava z {endpoint}...", + "tcpErrorHostRequired": "Potrebna je IP-naslov.", + "tcpErrorPortInvalid": "Port mora biti med 1 in 65535.", + "tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.", + "tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.", + "tcpConnectionFailed": "Napaka pri povezavi TCP: {error}" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index f7615df..349a4d3 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1859,5 +1859,33 @@ "usbStatus_notConnected": "Välj en USB-enhet", "usbConnectionFailed": "Fel vid USB-anslutning: {error}", "usbStatus_searching": "Söker efter USB-enheter...", - "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware." + "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostHint": "192.168.40.10", + "tcpHostLabel": "IP-adress", + "tcpScreenTitle": "Anslut via TCP", + "connectionChoiceTcpLabel": "TCP", + "tcpPortLabel": "Hamn", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Ange slutpunkt och anslut", + "tcpStatus_connecting": "Anslutning till TCP-slutpunkt...", + "tcpStatus_connectingTo": "Anslutning till {endpoint}...", + "tcpErrorHostRequired": "IP-adress krävs.", + "tcpErrorPortInvalid": "Porten måste vara mellan 1 och 65535.", + "tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.", + "tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.", + "tcpConnectionFailed": "Fel vid TCP-anslutning: {error}" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 7794098..8d1ea80 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1859,5 +1859,33 @@ "usbStatus_notConnected": "Виберіть пристрій USB", "usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", "usbStatus_connecting": "Підключення до USB-пристрою...", - "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion." + "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "connectionChoiceTcpLabel": "TCP", + "tcpHostHint": "192.168.40.10", + "tcpHostLabel": "IP-адреса", + "tcpScreenTitle": "З'єднатися через протокол TCP", + "tcpPortLabel": "Порт", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "Введіть кінцеву точку та підключіться", + "tcpStatus_connecting": "Підключення до TCP-кінцевої точки...", + "tcpStatus_connectingTo": "Підключення до {endpoint}...", + "tcpErrorHostRequired": "Необхідно вказати IP-адресу.", + "tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.", + "tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.", + "tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.", + "tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index dfc8e64..1df60a7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1864,5 +1864,33 @@ "usbStatus_connecting": "连接USB设备...", "usbStatus_notConnected": "选择一个 USB 设备", "usbConnectionFailed": "USB 连接失败:{error}", - "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。" + "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。", + "@tcpStatus_connectingTo": { + "placeholders": { + "endpoint": { + "type": "String" + } + } + }, + "@tcpConnectionFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "tcpHostLabel": "IP地址", + "tcpHostHint": "192.168.40.10", + "tcpScreenTitle": "通过 TCP 连接", + "connectionChoiceTcpLabel": "TCP", + "tcpPortLabel": "港", + "tcpPortHint": "5000", + "tcpStatus_notConnected": "输入目标地址,然后连接", + "tcpStatus_connecting": "连接到 TCP 终点...", + "tcpStatus_connectingTo": "连接到 {endpoint}...", + "tcpErrorHostRequired": "需要提供IP地址。", + "tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。", + "tcpErrorUnsupported": "此平台不支持 TCP 传输。", + "tcpErrorTimedOut": "TCP 连接超时。", + "tcpConnectionFailed": "TCP 连接失败:{error}" } diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 45fd3fb..986a598 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -10,6 +10,7 @@ import '../utils/app_logger.dart'; import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; import 'contacts_screen.dart'; +import 'tcp_screen.dart'; import 'usb_screen.dart'; /// Screen for scanning and connecting to MeshCore devices @@ -125,61 +126,78 @@ class _ScannerScreenState extends State { connector.state == MeshCoreConnectionState.scanning; final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; final usbSupported = PlatformInfo.supportsUsbSerial; + final tcpSupported = !PlatformInfo.isWeb; return SafeArea( top: false, minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (usbSupported) + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (usbSupported) + FloatingActionButton.extended( + onPressed: () { + appLogger.info( + 'USB selected, opening UsbScreen', + tag: 'ScannerScreen', + ); + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const UsbScreen()), + ); + }, + heroTag: 'scanner_usb_action', + icon: const Icon(Icons.usb), + label: Text(context.l10n.connectionChoiceUsbLabel), + ), + if (usbSupported) const SizedBox(width: 12), + if (tcpSupported) + FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const TcpScreen()), + ); + }, + heroTag: 'scanner_tcp_action', + icon: const Icon(Icons.lan), + label: Text(context.l10n.connectionChoiceTcpLabel), + ), + if (tcpSupported) const SizedBox(width: 12), FloatingActionButton.extended( - onPressed: () { - appLogger.info( - 'USB selected, opening UsbScreen', - tag: 'ScannerScreen', - ); - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const UsbScreen()), - ); - }, - heroTag: 'scanner_usb_action', - icon: const Icon(Icons.usb), - label: Text(context.l10n.connectionChoiceUsbLabel), + heroTag: 'scanner_ble_action', + onPressed: isBluetoothOff + ? null + : () { + if (isScanning) { + connector.stopScan(); + } else { + unawaited( + connector.startScan().catchError((e) { + appLogger.warn( + 'startScan error: $e', + tag: 'ScannerScreen', + ); + }), + ); + } + }, + icon: isScanning + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.bluetooth_searching), + label: Text( + isScanning + ? context.l10n.scanner_stop + : context.l10n.scanner_scan, + ), ), - if (usbSupported) const SizedBox(width: 12), - FloatingActionButton.extended( - heroTag: 'scanner_ble_action', - onPressed: isBluetoothOff - ? null - : () { - if (isScanning) { - connector.stopScan(); - } else { - unawaited( - connector.startScan().catchError((e) { - appLogger.warn( - 'startScan error: $e', - tag: 'ScannerScreen', - ); - }), - ); - } - }, - icon: isScanning - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.bluetooth_searching), - label: Text( - isScanning - ? context.l10n.scanner_stop - : context.l10n.scanner_scan, - ), - ), - ], + ], + ), ), ); }, diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart new file mode 100644 index 0000000..f3702c3 --- /dev/null +++ b/lib/screens/tcp_screen.dart @@ -0,0 +1,272 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; +import '../utils/platform_info.dart'; +import '../widgets/adaptive_app_bar_title.dart'; +import 'contacts_screen.dart'; +import 'usb_screen.dart'; + +class TcpScreen extends StatefulWidget { + const TcpScreen({super.key}); + + @override + State createState() => _TcpScreenState(); +} + +class _TcpScreenState extends State { + late final TextEditingController _hostController; + late final TextEditingController _portController; + late final MeshCoreConnector _connector; + late final VoidCallback _connectionListener; + bool _navigatedToContacts = false; + + @override + void initState() { + super.initState(); + _hostController = TextEditingController(text: '192.168.40.10'); + _portController = TextEditingController(text: '5000'); + _connector = context.read(); + + _connectionListener = () { + if (!mounted) return; + if (_connector.state == MeshCoreConnectionState.disconnected) { + _navigatedToContacts = false; + } + if (_connector.state == MeshCoreConnectionState.connected && + _connector.isTcpTransportConnected && + !_navigatedToContacts) { + _navigatedToContacts = true; + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const ContactsScreen()), + ); + } + }; + _connector.addListener(_connectionListener); + } + + @override + void dispose() { + _hostController.dispose(); + _portController.dispose(); + _connector.removeListener(_connectionListener); + if (!_navigatedToContacts && + _connector.activeTransport == MeshCoreTransportType.tcp && + _connector.state != MeshCoreConnectionState.disconnected) { + WidgetsBinding.instance.addPostFrameCallback((_) { + unawaited(_connector.disconnect(manual: true)); + }); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).maybePop(), + ), + title: AdaptiveAppBarTitle(context.l10n.tcpScreenTitle), + centerTitle: true, + ), + body: SafeArea( + top: false, + child: Consumer( + builder: (context, connector, child) { + final isConnecting = + connector.state == MeshCoreConnectionState.connecting && + connector.activeTransport == MeshCoreTransportType.tcp; + return Column( + children: [ + _buildStatusBar(context, connector), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: _hostController, + decoration: InputDecoration( + labelText: context.l10n.tcpHostLabel, + hintText: context.l10n.tcpHostHint, + border: const OutlineInputBorder(), + ), + enabled: !isConnecting, + keyboardType: TextInputType.url, + ), + const SizedBox(height: 12), + TextField( + controller: _portController, + decoration: InputDecoration( + labelText: context.l10n.tcpPortLabel, + hintText: context.l10n.tcpPortHint, + border: const OutlineInputBorder(), + ), + enabled: !isConnecting, + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: isConnecting ? null : _connectTcp, + icon: isConnecting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.lan), + label: Text( + isConnecting + ? context.l10n.scanner_connecting + : context.l10n.common_connect, + ), + ), + ], + ), + ), + ], + ); + }, + ), + ), + bottomNavigationBar: SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (PlatformInfo.supportsUsbSerial) + FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const UsbScreen()), + ); + }, + heroTag: 'tcp_usb_action', + icon: const Icon(Icons.usb), + label: Text(context.l10n.connectionChoiceUsbLabel), + ), + if (PlatformInfo.supportsUsbSerial) const SizedBox(width: 12), + FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).maybePop(); + }, + heroTag: 'tcp_ble_action', + icon: const Icon(Icons.bluetooth), + label: Text(context.l10n.connectionChoiceBluetoothLabel), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStatusBar(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + String statusText; + Color statusColor; + + if (connector.isTcpTransportConnected) { + statusText = l10n.scanner_connectedTo( + connector.activeTcpEndpoint ?? 'TCP', + ); + statusColor = Colors.green; + } else if (connector.state == MeshCoreConnectionState.connecting && + connector.activeTransport == MeshCoreTransportType.tcp) { + statusText = l10n.tcpStatus_connectingTo( + '${_hostController.text}:${_portController.text}', + ); + statusColor = Colors.orange; + } else if (connector.state == MeshCoreConnectionState.disconnecting && + connector.activeTransport == MeshCoreTransportType.tcp) { + statusText = l10n.scanner_disconnecting; + statusColor = Colors.orange; + } else { + statusText = l10n.tcpStatus_notConnected; + statusColor = Colors.grey; + } + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + color: statusColor.withValues(alpha: 0.1), + child: Row( + children: [ + Icon(Icons.circle, size: 12, color: statusColor), + const SizedBox(width: 8), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + statusText, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } + + Future _connectTcp() async { + if (_connector.state != MeshCoreConnectionState.disconnected) return; + + final host = _hostController.text.trim(); + final parsedPort = int.tryParse(_portController.text.trim()); + if (host.isEmpty) { + _showError(context.l10n.tcpErrorHostRequired); + return; + } + if (parsedPort == null || parsedPort < 1 || parsedPort > 65535) { + _showError(context.l10n.tcpErrorPortInvalid); + return; + } + + try { + await _connector.connectTcp(host: host, port: parsedPort); + } catch (error) { + if (!mounted) return; + _showError(_friendlyErrorMessage(error)); + } + } + + void _showError(String message) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message), backgroundColor: Colors.red), + ); + } + + String _friendlyErrorMessage(Object error) { + if (error is UnsupportedError) { + return context.l10n.tcpErrorUnsupported; + } + if (error is TimeoutException) { + return context.l10n.tcpErrorTimedOut; + } + if (error is StateError) { + return context.l10n.tcpConnectionFailed(error.message); + } + if (error is ArgumentError) { + return context.l10n.tcpConnectionFailed( + error.message?.toString() ?? error.toString(), + ); + } + return context.l10n.tcpConnectionFailed(error.toString()); + } +} diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index e230a54..0cc9007 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -12,6 +12,7 @@ import '../utils/usb_port_labels.dart'; import '../widgets/adaptive_app_bar_title.dart'; import 'contacts_screen.dart'; import 'scanner_screen.dart'; +import 'tcp_screen.dart'; class UsbScreen extends StatefulWidget { const UsbScreen({super.key}); @@ -111,41 +112,68 @@ class _UsbScreenState extends State { PlatformInfo.isWeb || PlatformInfo.isAndroid || PlatformInfo.isIOS; + final showTcp = !PlatformInfo.isWeb; return SafeArea( top: false, minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (showBle) - FloatingActionButton.extended( - onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (_) => const ScannerScreen(), - ), - ); - }, - heroTag: 'usb_ble_action', - icon: const Icon(Icons.bluetooth), - label: Text(context.l10n.connectionChoiceBluetoothLabel), - ), - if (showBle) const SizedBox(width: 12), - if (!_supportsHotPlug) - FloatingActionButton.extended( - onPressed: isLoading ? null : _loadPorts, - heroTag: 'usb_refresh_action', - icon: isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.refresh), - label: Text(context.l10n.repeater_refresh), - ), - ], + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (showTcp) + FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const TcpScreen()), + ); + }, + heroTag: 'usb_tcp_action', + extendedPadding: const EdgeInsets.symmetric( + horizontal: 12, + ), + icon: const Icon(Icons.lan), + label: Text(context.l10n.connectionChoiceTcpLabel), + ), + if (showTcp && showBle) const SizedBox(width: 12), + if (showBle) + FloatingActionButton.extended( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (_) => const ScannerScreen(), + ), + ); + }, + heroTag: 'usb_ble_action', + extendedPadding: const EdgeInsets.symmetric( + horizontal: 12, + ), + icon: const Icon(Icons.bluetooth), + label: Text(context.l10n.connectionChoiceBluetoothLabel), + ), + if ((showTcp || showBle) && !_supportsHotPlug) + const SizedBox(width: 12), + if (!_supportsHotPlug) + FloatingActionButton.extended( + onPressed: isLoading ? null : _loadPorts, + heroTag: 'usb_refresh_action', + extendedPadding: const EdgeInsets.symmetric( + horizontal: 12, + ), + icon: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.usb), + label: Text(context.l10n.scanner_scan), + ), + ], + ), ), ); }, @@ -192,9 +220,18 @@ class _UsbScreenState extends State { children: [ Icon(Icons.circle, size: 12, color: statusColor), const SizedBox(width: 8), - Text( - statusText, - style: TextStyle(color: statusColor, fontWeight: FontWeight.w500), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + statusText, + style: TextStyle( + color: statusColor, + fontWeight: FontWeight.w500, + ), + ), + ), ), ], ), diff --git a/lib/services/tcp_transport_service.dart b/lib/services/tcp_transport_service.dart new file mode 100644 index 0000000..0e6f7a1 --- /dev/null +++ b/lib/services/tcp_transport_service.dart @@ -0,0 +1,2 @@ +export 'tcp_transport_service_native.dart' + if (dart.library.js_interop) 'tcp_transport_service_web.dart'; diff --git a/lib/services/tcp_transport_service_native.dart b/lib/services/tcp_transport_service_native.dart new file mode 100644 index 0000000..d8a3ab3 --- /dev/null +++ b/lib/services/tcp_transport_service_native.dart @@ -0,0 +1,205 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'app_debug_log_service.dart'; +import 'usb_serial_frame_codec.dart'; + +class TcpTransportService { + final StreamController _frameController = + StreamController.broadcast(); + final UsbSerialFrameDecoder _frameDecoder = UsbSerialFrameDecoder(); + + StreamSubscription? _socketSubscription; + Socket? _socket; + AppDebugLogService? _debugLogService; + TcpTransportStatus _status = TcpTransportStatus.disconnected; + String? _activeHost; + int? _activePort; + Future _pendingWrite = Future.value(); + int _connectGeneration = 0; + + TcpTransportStatus get status => _status; + Stream get frameStream => _frameController.stream; + bool get isConnected => _status == TcpTransportStatus.connected; + String? get activeEndpoint => _activeHost == null || _activePort == null + ? null + : '$_activeHost:$_activePort'; + + void setDebugLogService(AppDebugLogService? service) { + _debugLogService = service; + } + + Future connect({ + required String host, + required int port, + Duration timeout = const Duration(seconds: 10), + }) async { + if (_status == TcpTransportStatus.connected || + _status == TcpTransportStatus.connecting) { + throw StateError('TCP transport is already active'); + } + final trimmedHost = host.trim(); + if (trimmedHost.isEmpty) { + throw ArgumentError.value(host, 'host', 'Host cannot be empty'); + } + if (port < 1 || port > 65535) { + throw ArgumentError.value(port, 'port', 'Port must be in 1..65535'); + } + + _status = TcpTransportStatus.connecting; + final generation = ++_connectGeneration; + _frameDecoder.reset(); + + try { + final socket = await Socket.connect(trimmedHost, port, timeout: timeout); + if (generation != _connectGeneration || + _status != TcpTransportStatus.connecting) { + try { + await socket.close(); + } catch (_) {} + try { + socket.destroy(); + } catch (_) {} + return; + } + socket.setOption(SocketOption.tcpNoDelay, true); + _socket = socket; + _activeHost = trimmedHost; + _activePort = port; + _socketSubscription = socket.listen( + _handleSocketData, + onError: _handleSocketError, + onDone: _handleSocketDone, + ); + _status = TcpTransportStatus.connected; + _debugLogService?.info( + 'TCP transport opened endpoint=$activeEndpoint', + tag: 'TCP', + ); + } catch (error) { + await _cleanupFailedConnect(); + _status = TcpTransportStatus.disconnected; + rethrow; + } + } + + Future write(Uint8List data) async { + if (!isConnected || _socket == null) { + throw StateError('TCP transport is not connected'); + } + + final packet = wrapUsbSerialTxFrame(data); + _logFrameSummary('TCP TX frame', data); + + final writeTask = _pendingWrite.then((_) async { + final socket = _socket; + if (!isConnected || socket == null) { + throw StateError('TCP transport is not connected'); + } + socket.add(packet); + await socket.flush(); + }); + + _pendingWrite = writeTask.catchError((_) {}); + await writeTask; + } + + Future disconnect() async { + _connectGeneration += 1; + if (_status == TcpTransportStatus.disconnected) return; + + final endpoint = activeEndpoint; + _status = TcpTransportStatus.disconnecting; + _frameDecoder.reset(); + _activeHost = null; + _activePort = null; + + final subscription = _socketSubscription; + _socketSubscription = null; + await subscription?.cancel(); + + final socket = _socket; + _socket = null; + try { + await socket?.close(); + } catch (_) {} + try { + socket?.destroy(); + } catch (_) {} + + _status = TcpTransportStatus.disconnected; + _debugLogService?.info( + 'TCP transport closed endpoint=${endpoint ?? 'unknown'}', + tag: 'TCP', + ); + } + + void dispose() { + unawaited(disconnect().whenComplete(_closeFrameController)); + } + + Future _cleanupFailedConnect() async { + final subscription = _socketSubscription; + _socketSubscription = null; + await subscription?.cancel(); + final socket = _socket; + _socket = null; + try { + await socket?.close(); + } catch (_) {} + try { + socket?.destroy(); + } catch (_) {} + _activeHost = null; + _activePort = null; + _frameDecoder.reset(); + } + + void _handleSocketData(Uint8List bytes) { + for (final packet in _frameDecoder.ingest(bytes)) { + if (!packet.isRxFrame) { + _debugLogService?.info( + 'TCP ignored packet start=0x${packet.frameStart.toRadixString(16).padLeft(2, '0')} len=${packet.payload.length}', + tag: 'TCP', + ); + continue; + } + _addFrame(packet.payload); + } + } + + void _handleSocketError(Object error, [StackTrace? stackTrace]) { + _addFrameError(error, stackTrace); + unawaited(disconnect()); + } + + void _handleSocketDone() { + unawaited(disconnect()); + } + + void _addFrame(Uint8List payload) { + if (_frameController.isClosed) return; + _frameController.add(payload); + } + + void _addFrameError(Object error, [StackTrace? stackTrace]) { + if (_frameController.isClosed) return; + _frameController.addError(error, stackTrace); + } + + void _logFrameSummary(String prefix, Uint8List payload) { + final code = payload.isNotEmpty ? payload.first : -1; + _debugLogService?.info( + '$prefix code=$code len=${payload.length}', + tag: 'TCP', + ); + } + + Future _closeFrameController() async { + if (_frameController.isClosed) return; + await _frameController.close(); + } +} + +enum TcpTransportStatus { disconnected, connecting, connected, disconnecting } diff --git a/lib/services/tcp_transport_service_web.dart b/lib/services/tcp_transport_service_web.dart new file mode 100644 index 0000000..c23179b --- /dev/null +++ b/lib/services/tcp_transport_service_web.dart @@ -0,0 +1,35 @@ +import 'dart:typed_data'; + +import 'app_debug_log_service.dart'; + +class TcpTransportService { + AppDebugLogService? _debugLogService; + + Stream get frameStream => const Stream.empty(); + bool get isConnected => false; + String? get activeEndpoint => null; + + void setDebugLogService(AppDebugLogService? service) { + _debugLogService = service; + } + + Future connect({ + required String host, + required int port, + Duration timeout = const Duration(seconds: 10), + }) async { + _debugLogService?.warn( + 'TCP transport requested on web for $host:$port', + tag: 'TCP', + ); + throw UnsupportedError('TCP transport is not supported on web.'); + } + + Future write(Uint8List data) async { + throw UnsupportedError('TCP transport is not supported on web.'); + } + + Future disconnect() async {} + + void dispose() {} +} diff --git a/test/screens/tcp_flow_test.dart b/test/screens/tcp_flow_test.dart new file mode 100644 index 0000000..501046d --- /dev/null +++ b/test/screens/tcp_flow_test.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/l10n/app_localizations.dart'; +import 'package:meshcore_open/screens/scanner_screen.dart'; +import 'package:meshcore_open/screens/tcp_screen.dart'; + +class _FakeMeshCoreConnector extends MeshCoreConnector { + _FakeMeshCoreConnector(); + + MeshCoreConnectionState initialState = MeshCoreConnectionState.disconnected; + MeshCoreTransportType initialTransport = MeshCoreTransportType.bluetooth; + String? initialEndpoint; + int connectTcpCalls = 0; + String? lastHost; + int? lastPort; + + @override + MeshCoreConnectionState get state => initialState; + + @override + MeshCoreTransportType get activeTransport => initialTransport; + + @override + bool get isTcpTransportConnected => + initialState == MeshCoreConnectionState.connected && + initialTransport == MeshCoreTransportType.tcp; + + @override + String? get activeTcpEndpoint => initialEndpoint; + + @override + Future connectTcp({required String host, required int port}) async { + connectTcpCalls += 1; + lastHost = host; + lastPort = port; + } +} + +Widget _buildTestApp({ + required MeshCoreConnector connector, + required Widget child, + Locale? locale, +}) { + return ChangeNotifierProvider.value( + value: connector, + child: MaterialApp( + locale: locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: child, + ), + ); +} + +void main() { + testWidgets('TcpScreen uses localized TCP copy', (tester) async { + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp( + connector: connector, + child: const TcpScreen(), + locale: const Locale('en'), + ), + ); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(TcpScreen)); + final l10n = AppLocalizations.of(context); + + expect(find.text(l10n.tcpScreenTitle), findsOneWidget); + expect(find.text(l10n.tcpHostLabel), findsOneWidget); + expect(find.text(l10n.tcpPortLabel), findsOneWidget); + expect(find.text(l10n.tcpStatus_notConnected), findsOneWidget); + }); + + testWidgets('TcpScreen validation errors are localized', (tester) async { + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp( + connector: connector, + child: const TcpScreen(), + locale: const Locale('en'), + ), + ); + await tester.pumpAndSettle(); + + final context = tester.element(find.byType(TcpScreen)); + final l10n = AppLocalizations.of(context); + + await tester.enterText(find.byType(TextField).first, ''); + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pumpAndSettle(); + + expect(find.text(l10n.tcpErrorHostRequired), findsOneWidget); + expect(connector.connectTcpCalls, 0); + + await tester.enterText(find.byType(TextField).first, '192.168.1.50'); + await tester.enterText(find.byType(TextField).at(1), '99999'); + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pumpAndSettle(); + + expect(connector.connectTcpCalls, 0); + }); + + testWidgets('TCP Bluetooth action returns to existing scanner route', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const ScannerScreen()), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(FloatingActionButton, 'TCP')); + await tester.pumpAndSettle(); + expect(find.byType(TcpScreen), findsOneWidget); + + await tester.tap(find.widgetWithText(FloatingActionButton, 'Bluetooth')); + await tester.pumpAndSettle(); + + expect(find.byType(TcpScreen), findsNothing); + expect(find.byType(ScannerScreen), findsOneWidget); + final navigatorState = tester.state(find.byType(Navigator)); + expect(navigatorState.canPop(), isFalse); + + // ScannerScreen.dispose() schedules disconnect work that debounces notify. + // Drain that debounce timer before test teardown. + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(milliseconds: 60)); + }); + + testWidgets('TcpScreen narrow width long status text does not overflow', ( + tester, + ) async { + await tester.binding.setSurfaceSize(const Size(320, 700)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + final connector = _FakeMeshCoreConnector() + ..initialState = MeshCoreConnectionState.connected + ..initialTransport = MeshCoreTransportType.tcp + ..initialEndpoint = 'meshcore-room-server-very-long-hostname.local:5000'; + + await tester.pumpWidget( + _buildTestApp( + connector: connector, + child: const TcpScreen(), + locale: const Locale('en'), + ), + ); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + + final context = tester.element(find.byType(TcpScreen)); + final l10n = AppLocalizations.of(context); + expect( + find.text(l10n.scanner_connectedTo(connector.initialEndpoint!)), + findsOneWidget, + ); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(milliseconds: 60)); + }); +} diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart index 436d230..16e5a95 100644 --- a/test/screens/usb_flow_test.dart +++ b/test/screens/usb_flow_test.dart @@ -116,12 +116,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap( - find.ancestor( - of: find.text('Connect'), - matching: find.bySubtype(), - ), - ); + await tester.tap(find.byType(ListTile).first); await tester.pump(); expect(connector.connectUsbCalls, 0); @@ -145,12 +140,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap( - find.ancestor( - of: find.text('Connect'), - matching: find.bySubtype(), - ), - ); + await tester.tap(find.byType(ListTile).first); await tester.pump(); expect(connector.connectUsbCalls, 1); @@ -179,6 +169,68 @@ void main() { await tester.pump(const Duration(milliseconds: 60)); }); + testWidgets('ScannerScreen narrow width keeps actions without overflow', ( + tester, + ) async { + await tester.binding.setSurfaceSize(const Size(320, 700)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const ScannerScreen()), + ); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + + final context = tester.element(find.byType(ScannerScreen)); + final l10n = AppLocalizations.of(context); + expect(find.text(l10n.scanner_scan), findsOneWidget); + + if (PlatformInfo.supportsUsbSerial) { + expect(find.text(l10n.connectionChoiceUsbLabel), findsOneWidget); + } + if (!PlatformInfo.isWeb) { + expect(find.text(l10n.connectionChoiceTcpLabel), findsOneWidget); + } + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(milliseconds: 60)); + }); + + testWidgets('UsbScreen narrow width long status text does not overflow', ( + tester, + ) async { + await tester.binding.setSurfaceSize(const Size(320, 700)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + final connector = + _FakeMeshCoreConnector(initialState: MeshCoreConnectionState.connected) + ..fakeUsbTransportConnected = true + ..fakeActiveUsbPortDisplayLabel = + '/dev/bus/usb/001/002 - KD3CGK mesh-utility.org very long label'; + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + + final context = tester.element(find.byType(UsbScreen)); + final l10n = AppLocalizations.of(context); + expect( + find.text( + l10n.scanner_connectedTo(connector.fakeActiveUsbPortDisplayLabel!), + ), + findsOneWidget, + ); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(const Duration(milliseconds: 60)); + }); + group('Error Handling', () { testWidgets('shows error SnackBar when listing ports fails', ( tester, @@ -212,12 +264,7 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap( - find.ancestor( - of: find.text('Connect'), - matching: find.bySubtype(), - ), - ); + await tester.tap(find.byType(ListTile).first); await tester.pumpAndSettle(); expect(connectAttempted, isTrue); diff --git a/test/services/tcp_transport_service_native_test.dart b/test/services/tcp_transport_service_native_test.dart new file mode 100644 index 0000000..24c62ad --- /dev/null +++ b/test/services/tcp_transport_service_native_test.dart @@ -0,0 +1,136 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/services/tcp_transport_service_native.dart'; +import 'package:meshcore_open/services/usb_serial_frame_codec.dart'; + +final class _DelayedConnectOverrides extends IOOverrides { + _DelayedConnectOverrides(this.delay); + + final Duration delay; + + @override + Future socketConnect( + host, + int port, { + sourceAddress, + int sourcePort = 0, + Duration? timeout, + }) async { + await Future.delayed(delay); + return super.socketConnect( + host, + port, + sourceAddress: sourceAddress, + sourcePort: sourcePort, + timeout: timeout, + ); + } +} + +void main() { + test('connect/disconnect updates TCP transport state', () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final service = TcpTransportService(); + + try { + await service.connect( + host: InternetAddress.loopbackIPv4.address, + port: server.port, + ); + + expect(service.isConnected, isTrue); + expect( + service.activeEndpoint, + '${InternetAddress.loopbackIPv4.address}:${server.port}', + ); + + await service.disconnect(); + + expect(service.isConnected, isFalse); + expect(service.activeEndpoint, isNull); + } finally { + await service.disconnect(); + await server.close(); + } + }); + + test('disconnect is safe when already disconnected', () async { + final service = TcpTransportService(); + + await service.disconnect(); + await service.disconnect(); + + expect(service.isConnected, isFalse); + expect(service.activeEndpoint, isNull); + }); + + test('emits only RX frames from socket stream', () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final acceptedSocket = Completer(); + final service = TcpTransportService(); + final receivedFrames = []; + + final serverSub = server.listen((socket) { + if (!acceptedSocket.isCompleted) { + acceptedSocket.complete(socket); + } else { + socket.destroy(); + } + }); + final frameSub = service.frameStream.listen(receivedFrames.add); + + try { + await service.connect( + host: InternetAddress.loopbackIPv4.address, + port: server.port, + ); + + final socket = await acceptedSocket.future.timeout( + const Duration(seconds: 2), + ); + + socket.add([usbSerialTxFrameStart, 0x01, 0x00, 0x11]); + socket.add([usbSerialRxFrameStart, 0x02, 0x00, 0x33, 0x44]); + await socket.flush(); + + await Future.delayed(const Duration(milliseconds: 20)); + + expect(receivedFrames, hasLength(1)); + expect(receivedFrames.single, orderedEquals([0x33, 0x44])); + } finally { + await service.disconnect(); + await frameSub.cancel(); + await serverSub.cancel(); + await server.close(); + } + }); + + test( + 'disconnect during in-flight connect keeps transport disconnected', + () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + final service = TcpTransportService(); + final host = InternetAddress.loopbackIPv4.address; + + try { + await IOOverrides.runWithIOOverrides(() async { + final connectFuture = service.connect(host: host, port: server.port); + + await Future.delayed(const Duration(milliseconds: 10)); + await service.disconnect(); + await connectFuture; + + expect(service.isConnected, isFalse); + expect(service.status, TcpTransportStatus.disconnected); + expect(service.activeEndpoint, isNull); + }, _DelayedConnectOverrides(const Duration(milliseconds: 120))); + } finally { + await service.disconnect(); + await server.close(); + } + }, + ); +} From 929c1c3d286664d1d65301b1f9cb985a3edaafd1 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Mon, 9 Mar 2026 20:39:17 -0400 Subject: [PATCH 287/421] `fix(tcp): cancel pending connects on disconnect and propagate remote close` --- lib/connector/meshcore_connector_tcp.dart | 3 --- lib/services/tcp_transport_service_native.dart | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/connector/meshcore_connector_tcp.dart b/lib/connector/meshcore_connector_tcp.dart index 01c2b92..92b98d7 100644 --- a/lib/connector/meshcore_connector_tcp.dart +++ b/lib/connector/meshcore_connector_tcp.dart @@ -22,9 +22,6 @@ class MeshCoreTcpManager { } Future disconnect() async { - if (!_service.isConnected && _service.activeEndpoint == null) { - return; - } _debugLog?.info('TcpManager.disconnect', tag: 'TCP'); await _service.disconnect(); } diff --git a/lib/services/tcp_transport_service_native.dart b/lib/services/tcp_transport_service_native.dart index d8a3ab3..a6db99b 100644 --- a/lib/services/tcp_transport_service_native.dart +++ b/lib/services/tcp_transport_service_native.dart @@ -175,6 +175,11 @@ class TcpTransportService { } void _handleSocketDone() { + if (_status == TcpTransportStatus.disconnecting || + _status == TcpTransportStatus.disconnected) { + return; + } + _addFrameError(StateError('TCP socket closed by remote endpoint')); unawaited(disconnect()); } From 1913a5aa114ad57e2f0fcaf2af3b36f040f20d60 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Tue, 10 Mar 2026 19:27:39 -0400 Subject: [PATCH 288/421] fix(tcp): guard connect cancellation race and align USB screen actions - add connectTcp cancellation guards after socket connect and connect delay so handshake does not proceed when transport/state changed - ignore late TCP connect errors after manual cancel or transport switch to avoid spurious second disconnect paths - keep TCP action hidden only on web and show Bluetooth action on USB screen across platforms for navigation consistency --- lib/connector/meshcore_connector.dart | 38 +++++++++++++++++++++++++++ lib/screens/usb_screen.dart | 5 +--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 135299e..aad2c71 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -995,9 +995,37 @@ class MeshCoreConnector extends ChangeNotifier { await _tcpFrameSubscription?.cancel(); _tcpFrameSubscription = null; await _tcpManager.connect(host: host, port: port); + final isTcpConnectCancelled = + _activeTransport != MeshCoreTransportType.tcp || + _state != MeshCoreConnectionState.connecting || + !_tcpManager.isConnected; + if (isTcpConnectCancelled) { + _appDebugLogService?.warn( + 'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', + tag: 'TCP', + ); + if (_tcpManager.isConnected) { + await _tcpManager.disconnect(); + } + return; + } notifyListeners(); await Future.delayed(const Duration(milliseconds: 200)); + final isTcpConnectCancelledAfterDelay = + _activeTransport != MeshCoreTransportType.tcp || + _state != MeshCoreConnectionState.connecting || + !_tcpManager.isConnected; + if (isTcpConnectCancelledAfterDelay) { + _appDebugLogService?.warn( + 'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', + tag: 'TCP', + ); + if (_tcpManager.isConnected) { + await _tcpManager.disconnect(); + } + return; + } _tcpFrameSubscription = _tcpManager.frameStream.listen( _handleFrame, onError: (error, stackTrace) { @@ -1031,6 +1059,16 @@ class MeshCoreConnector extends ChangeNotifier { await syncTime(); } catch (error) { _appDebugLogService?.error('TCP connection error: $error', tag: 'TCP'); + final tcpConnectNoLongerActive = + _activeTransport != MeshCoreTransportType.tcp || + _state != MeshCoreConnectionState.connecting; + if (tcpConnectNoLongerActive) { + _appDebugLogService?.info( + 'Ignoring late TCP connect error after cancellation/switch: state=$_state transport=$_activeTransport', + tag: 'TCP', + ); + return; + } await disconnect(manual: false); rethrow; } diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 0cc9007..2f2713a 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -108,10 +108,7 @@ class _UsbScreenState extends State { bottomNavigationBar: Consumer( builder: (context, connector, child) { final isLoading = _isLoadingPorts; - final showBle = - PlatformInfo.isWeb || - PlatformInfo.isAndroid || - PlatformInfo.isIOS; + final showBle = true; final showTcp = !PlatformInfo.isWeb; return SafeArea( From 9db79e9d4034e6b07aa0fe5f826e2637a64fcf2b Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Tue, 10 Mar 2026 20:06:05 -0400 Subject: [PATCH 289/421] test(tcp): harden cancel-race handling and add coverage - tighten late TCP connect error suppression to manual-cancel disconnecting/disconnected windows - keep TCP handshake failures surfaced outside explicit cancel flow - allow TcpScreen connect action when connector is scanning - add connector-level tests for late-error suppression classifier - add TcpScreen test covering connect from scanning state --- lib/connector/meshcore_connector.dart | 25 ++++++-- lib/screens/tcp_screen.dart | 6 +- ...hcore_connector_tcp_error_filter_test.dart | 64 +++++++++++++++++++ test/screens/tcp_flow_test.dart | 23 +++++++ 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 test/connector/meshcore_connector_tcp_error_filter_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index aad2c71..0de1a90 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1059,10 +1059,14 @@ class MeshCoreConnector extends ChangeNotifier { await syncTime(); } catch (error) { _appDebugLogService?.error('TCP connection error: $error', tag: 'TCP'); - final tcpConnectNoLongerActive = - _activeTransport != MeshCoreTransportType.tcp || - _state != MeshCoreConnectionState.connecting; - if (tcpConnectNoLongerActive) { + final tcpConnectCancelledBeforeHandshake = + shouldIgnoreLateTcpConnectError( + manualDisconnect: _manualDisconnect, + state: _state, + activeTransport: _activeTransport, + tcpManagerConnected: _tcpManager.isConnected, + ); + if (tcpConnectCancelledBeforeHandshake) { _appDebugLogService?.info( 'Ignoring late TCP connect error after cancellation/switch: state=$_state transport=$_activeTransport', tag: 'TCP', @@ -1074,6 +1078,19 @@ class MeshCoreConnector extends ChangeNotifier { } } + @visibleForTesting + static bool shouldIgnoreLateTcpConnectError({ + required bool manualDisconnect, + required MeshCoreConnectionState state, + required MeshCoreTransportType activeTransport, + required bool tcpManagerConnected, + }) { + return manualDisconnect && + (state == MeshCoreConnectionState.disconnected || + state == MeshCoreConnectionState.disconnecting) && + (activeTransport != MeshCoreTransportType.tcp || !tcpManagerConnected); + } + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index f3702c3..55bec20 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -224,7 +224,11 @@ class _TcpScreenState extends State { } Future _connectTcp() async { - if (_connector.state != MeshCoreConnectionState.disconnected) return; + if (_connector.state == MeshCoreConnectionState.connecting || + _connector.state == MeshCoreConnectionState.connected || + _connector.state == MeshCoreConnectionState.disconnecting) { + return; + } final host = _hostController.text.trim(); final parsedPort = int.tryParse(_portController.text.trim()); diff --git a/test/connector/meshcore_connector_tcp_error_filter_test.dart b/test/connector/meshcore_connector_tcp_error_filter_test.dart new file mode 100644 index 0000000..ee6a382 --- /dev/null +++ b/test/connector/meshcore_connector_tcp_error_filter_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; + +void main() { + group('shouldIgnoreLateTcpConnectError', () { + test('returns true for manual cancel during disconnecting state', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.disconnecting, + activeTransport: MeshCoreTransportType.bluetooth, + tcpManagerConnected: false, + ); + + expect(result, isTrue); + }); + + test( + 'returns true for manual cancel after reaching disconnected state', + () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.disconnected, + activeTransport: MeshCoreTransportType.bluetooth, + tcpManagerConnected: false, + ); + + expect(result, isTrue); + }, + ); + + test('returns false when not a manual disconnect', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: false, + state: MeshCoreConnectionState.disconnecting, + activeTransport: MeshCoreTransportType.bluetooth, + tcpManagerConnected: false, + ); + + expect(result, isFalse); + }); + + test('returns false for connected state handshake failures', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.connected, + activeTransport: MeshCoreTransportType.tcp, + tcpManagerConnected: true, + ); + + expect(result, isFalse); + }); + + test('returns false when TCP is still active while disconnecting', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.disconnecting, + activeTransport: MeshCoreTransportType.tcp, + tcpManagerConnected: true, + ); + + expect(result, isFalse); + }); + }); +} diff --git a/test/screens/tcp_flow_test.dart b/test/screens/tcp_flow_test.dart index 501046d..5c240f4 100644 --- a/test/screens/tcp_flow_test.dart +++ b/test/screens/tcp_flow_test.dart @@ -135,6 +135,29 @@ void main() { await tester.pump(const Duration(milliseconds: 60)); }); + testWidgets('TcpScreen allows connect while connector is scanning', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector() + ..initialState = MeshCoreConnectionState.scanning; + + await tester.pumpWidget( + _buildTestApp( + connector: connector, + child: const TcpScreen(), + locale: const Locale('en'), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pumpAndSettle(); + + expect(connector.connectTcpCalls, 1); + expect(connector.lastHost, '192.168.40.10'); + expect(connector.lastPort, 5000); + }); + testWidgets('TcpScreen narrow width long status text does not overflow', ( tester, ) async { From 2f770bbd532cb80044a8128682e5940fe639c632 Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Tue, 10 Mar 2026 21:38:35 -0400 Subject: [PATCH 290/421] fix(tcp): reset state on aborted pre-handshake connect --- lib/connector/meshcore_connector.dart | 42 +++++++++++++------ ...hcore_connector_tcp_error_filter_test.dart | 29 +++++++++++++ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 0de1a90..c89e37f 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -992,6 +992,21 @@ class MeshCoreConnector extends ChangeNotifier { _setState(MeshCoreConnectionState.connecting); try { + Future handleTcpConnectAbort({required String message}) async { + _appDebugLogService?.warn(message, tag: 'TCP'); + final shouldResetState = shouldResetStateAfterTcpConnectAbort( + state: _state, + activeTransport: _activeTransport, + ); + if (shouldResetState) { + await disconnect(manual: false); + return; + } + if (_tcpManager.isConnected) { + await _tcpManager.disconnect(); + } + } + await _tcpFrameSubscription?.cancel(); _tcpFrameSubscription = null; await _tcpManager.connect(host: host, port: port); @@ -1000,13 +1015,10 @@ class MeshCoreConnector extends ChangeNotifier { _state != MeshCoreConnectionState.connecting || !_tcpManager.isConnected; if (isTcpConnectCancelled) { - _appDebugLogService?.warn( - 'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', - tag: 'TCP', + await handleTcpConnectAbort( + message: + 'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', ); - if (_tcpManager.isConnected) { - await _tcpManager.disconnect(); - } return; } notifyListeners(); @@ -1017,13 +1029,10 @@ class MeshCoreConnector extends ChangeNotifier { _state != MeshCoreConnectionState.connecting || !_tcpManager.isConnected; if (isTcpConnectCancelledAfterDelay) { - _appDebugLogService?.warn( - 'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', - tag: 'TCP', + await handleTcpConnectAbort( + message: + 'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', ); - if (_tcpManager.isConnected) { - await _tcpManager.disconnect(); - } return; } _tcpFrameSubscription = _tcpManager.frameStream.listen( @@ -1091,6 +1100,15 @@ class MeshCoreConnector extends ChangeNotifier { (activeTransport != MeshCoreTransportType.tcp || !tcpManagerConnected); } + @visibleForTesting + static bool shouldResetStateAfterTcpConnectAbort({ + required MeshCoreConnectionState state, + required MeshCoreTransportType activeTransport, + }) { + return state == MeshCoreConnectionState.connecting && + activeTransport == MeshCoreTransportType.tcp; + } + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { diff --git a/test/connector/meshcore_connector_tcp_error_filter_test.dart b/test/connector/meshcore_connector_tcp_error_filter_test.dart index ee6a382..c363b97 100644 --- a/test/connector/meshcore_connector_tcp_error_filter_test.dart +++ b/test/connector/meshcore_connector_tcp_error_filter_test.dart @@ -61,4 +61,33 @@ void main() { expect(result, isFalse); }); }); + + group('shouldResetStateAfterTcpConnectAbort', () { + test('returns true when TCP connect is still in connecting state', () { + final result = MeshCoreConnector.shouldResetStateAfterTcpConnectAbort( + state: MeshCoreConnectionState.connecting, + activeTransport: MeshCoreTransportType.tcp, + ); + + expect(result, isTrue); + }); + + test('returns false when state is already disconnected', () { + final result = MeshCoreConnector.shouldResetStateAfterTcpConnectAbort( + state: MeshCoreConnectionState.disconnected, + activeTransport: MeshCoreTransportType.tcp, + ); + + expect(result, isFalse); + }); + + test('returns false when transport switched away from TCP', () { + final result = MeshCoreConnector.shouldResetStateAfterTcpConnectAbort( + state: MeshCoreConnectionState.connecting, + activeTransport: MeshCoreTransportType.bluetooth, + ); + + expect(result, isFalse); + }); + }); } From 1fba5312a234d19f7c617f04ec49937d3da97c82 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 12 Mar 2026 00:14:48 -0700 Subject: [PATCH 291/421] Refactor storage classes to include companion's public key (#277) * Refactor storage classes to include public key handling and improve data loading/saving logic * Remove redundant publicKeyHex handling from ContactDiscoveryStore and fix key reference in saveContacts method * Remove unused app_logger import from ContactDiscoveryStore * Add warning log for empty publicKeyHex in saveChannelMessages method * Add warning log for empty publicKeyHex in clearMessages method * Migrate legacy storage keys to scoped keys across multiple stores * Remove legacy unscoped keys during migration in storage classes * Update lib/storage/contact_store.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 26 ++++++++++++-- lib/screens/channels_screen.dart | 2 ++ lib/storage/channel_message_store.dart | 46 ++++++++++++++++++++---- lib/storage/channel_order_store.dart | 41 +++++++++++++++++---- lib/storage/channel_settings_store.dart | 38 ++++++++++++++++++-- lib/storage/channel_store.dart | 38 +++++++++++++++++--- lib/storage/community_store.dart | 33 +++++++++++++++-- lib/storage/contact_discovery_store.dart | 6 ++-- lib/storage/contact_group_store.dart | 39 +++++++++++++++++--- lib/storage/contact_settings_store.dart | 38 ++++++++++++++++++-- lib/storage/contact_store.dart | 39 +++++++++++++++++--- lib/storage/message_store.dart | 44 ++++++++++++++++++++--- lib/storage/unread_store.dart | 39 +++++++++++++++++--- 13 files changed, 378 insertions(+), 51 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 89aeca0..2ea09ca 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -291,6 +291,7 @@ class MeshCoreConnector extends ChangeNotifier { bool get isLoadingChannels => _isLoadingChannels; Stream get receivedFrames => _receivedFramesController.stream; Uint8List? get selfPublicKey => _selfPublicKey; + String get selfPublicKeyHex => pubKeyToHex(_selfPublicKey ?? Uint8List(0)); String? get selfName => _selfName; double? get selfLatitude => _selfLatitude; double? get selfLongitude => _selfLongitude; @@ -663,6 +664,7 @@ class MeshCoreConnector extends ChangeNotifier { // Initialize notification service _notificationService.initialize(); _loadChannelOrder(); + _loadDiscoveredContactCache(); // Initialize retry service callbacks _retryService?.initialize( @@ -691,7 +693,7 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future loadDiscoveredContactCache() async { + Future _loadDiscoveredContactCache() async { final cached = await _discoveryContactStore.loadContacts(); _discoveredContacts ..clear() @@ -1193,7 +1195,6 @@ class MeshCoreConnector extends ChangeNotifier { await _requestDeviceInfo(); _startBatteryPolling(); - unawaited(loadDiscoveredContactCache()); final gotSelfInfo = await _waitForSelfInfo( timeout: const Duration(seconds: 3), @@ -2489,6 +2490,27 @@ class MeshCoreConnector extends ChangeNotifier { selfName.isNotEmpty) { _usbManager.updateConnectedLabel(selfName); } + + //set all the stores' public key so they can load the correct data + _channelMessageStore.setPublicKeyHex = selfPublicKeyHex; + _messageStore.setPublicKeyHex = selfPublicKeyHex; + _channelOrderStore.setPublicKeyHex = selfPublicKeyHex; + _channelSettingsStore.setPublicKeyHex = selfPublicKeyHex; + _contactSettingsStore.setPublicKeyHex = selfPublicKeyHex; + _contactStore.setPublicKeyHex = selfPublicKeyHex; + _channelStore.setPublicKeyHex = selfPublicKeyHex; + _unreadStore.setPublicKeyHex = selfPublicKeyHex; + + // Now that we have self info, we can load all the persisted data for this node + _loadChannelOrder(); + loadContactCache(); + loadChannelSettings(); + loadCachedChannels(); + + // Load persisted channel messages + loadAllChannelMessages(); + loadUnreadState(); + _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); _selfInfoRetryTimer = null; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 582fee7..00820ed 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -106,7 +106,9 @@ class _ChannelsScreenState extends State @override Widget build(BuildContext context) { final connector = context.watch(); + final channelMessageStore = ChannelMessageStore(); + channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex; // Auto-navigate back to scanner if disconnected if (!checkConnectionAndNavigate(connector)) { diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 1151514..9c9f7e8 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -1,5 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:meshcore_open/utils/app_logger.dart'; + import '../models/channel_message.dart'; import '../helpers/smaz.dart'; import 'prefs_manager.dart'; @@ -7,13 +9,25 @@ import 'prefs_manager.dart'; class ChannelMessageStore { static const String _keyPrefix = 'channel_messages_'; + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; + /// Save messages for a specific channel Future saveChannelMessages( int channelIndex, List messages, ) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save channel messages.', + ); + return; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; // Convert messages to JSON final jsonList = messages.map((msg) => _messageToJson(msg)).toList(); @@ -24,11 +38,31 @@ class ChannelMessageStore { /// Load messages for a specific channel Future> loadChannelMessages(int channelIndex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load channel messages.', + ); + return []; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; - final jsonString = prefs.getString(key); - if (jsonString == null) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $key', + ); + await prefs.setString(key, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { final jsonList = jsonDecode(jsonString) as List; @@ -42,14 +76,14 @@ class ChannelMessageStore { /// Clear messages for a specific channel Future clearChannelMessages(int channelIndex) async { final prefs = PrefsManager.instance; - final key = '$_keyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; await prefs.remove(key); } /// Clear all channel messages Future clearAllChannelMessages() async { final prefs = PrefsManager.instance; - final keys = prefs.getKeys().where((k) => k.startsWith(_keyPrefix)); + final keys = prefs.getKeys().where((k) => k.startsWith(keyFor)); for (var key in keys) { await prefs.remove(key); } diff --git a/lib/storage/channel_order_store.dart b/lib/storage/channel_order_store.dart index b9657c4..48a80f2 100644 --- a/lib/storage/channel_order_store.dart +++ b/lib/storage/channel_order_store.dart @@ -1,20 +1,49 @@ import 'dart:convert'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ChannelOrderStore { - static const String _key = 'channel_order'; + static const String _keyPrefix = 'channel_order_'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future saveChannelOrder(List order) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save channel order.'); + return; + } final prefs = PrefsManager.instance; - await prefs.setString(_key, jsonEncode(order)); + await prefs.setString(keyFor, jsonEncode(order)); } Future> loadChannelOrder() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load channel order.'); + return []; + } final prefs = PrefsManager.instance; - final raw = prefs.getString(_key); - if (raw == null || raw.isEmpty) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel order from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final decoded = jsonDecode(raw); + final decoded = jsonDecode(jsonString); if (decoded is List) { return decoded .map((value) => value is int ? value : int.tryParse('$value')) @@ -24,7 +53,7 @@ class ChannelOrderStore { } catch (_) { // fall through to legacy parse } - return raw + return jsonString .split(',') .map((value) => int.tryParse(value)) .whereType() diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index eee97aa..3b639cd 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -1,17 +1,49 @@ +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ChannelSettingsStore { - static const String _smazKeyPrefix = 'channel_smaz_'; + static const String _keyPrefix = 'channel_smaz_'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future loadSmazEnabled(int channelIndex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load channel settings.', + ); + return false; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; + final oldKey = '$_keyPrefix$channelIndex'; + bool? enabled = prefs.getBool(key); + if (enabled == null) { + // Attempt migration from legacy unscoped key on first load + enabled = prefs.getBool(oldKey); + prefs.remove(oldKey); + if (enabled != null) { + appLogger.info( + 'Migrating channel settings from legacy key $oldKey to scoped key $key', + ); + await prefs.setBool(key, enabled); + } + } return prefs.getBool(key) ?? false; } Future saveSmazEnabled(int channelIndex, bool enabled) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save channel settings.', + ); + return; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$channelIndex'; + final key = '$keyFor$channelIndex'; await prefs.setBool(key, enabled); } } diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index eaa7a61..1bad7e3 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -2,18 +2,42 @@ import 'dart:convert'; import 'dart:typed_data'; import '../models/channel.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ChannelStore { - static const String _key = 'channels'; + static const String _keyPrefix = 'channels'; + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length >= 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future> loadChannels() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load channels.'); + return []; + } final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_key); - if (jsonStr == null) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final jsonList = jsonDecode(jsonStr) as List; + final jsonList = jsonDecode(jsonString) as List; return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); @@ -23,9 +47,13 @@ class ChannelStore { } Future saveChannels(List channels) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save channels.'); + return; + } final prefs = PrefsManager.instance; final jsonList = channels.map(_toJson).toList(); - await prefs.setString(_key, jsonEncode(jsonList)); + await prefs.setString(keyFor, jsonEncode(jsonList)); } Map _toJson(Channel channel) { diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index a81cccd..c7198e7 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import '../models/community.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; /// Persists communities to local storage using SharedPreferences. @@ -9,12 +10,34 @@ import 'prefs_manager.dart'; /// Each community contains its secret K, so this data should /// be considered sensitive (though device encryption handles security). class CommunityStore { - static const String _communitiesKey = 'communities_v1'; + static const String _keyPrefix = 'communities_v1'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; /// Load all communities from storage Future> loadCommunities() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load communities.'); + return []; + } final prefs = PrefsManager.instance; - final jsonString = prefs.getString(_communitiesKey); + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating communities from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } if (jsonString == null || jsonString.isEmpty) { return []; } @@ -32,9 +55,13 @@ class CommunityStore { /// Save all communities to storage Future saveCommunities(List communities) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save communities.'); + return; + } final prefs = PrefsManager.instance; final jsonList = communities.map((c) => c.toJson()).toList(); - await prefs.setString(_communitiesKey, jsonEncode(jsonList)); + await prefs.setString(keyFor, jsonEncode(jsonList)); } /// Add a new community diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index 37bfbb4..ac47615 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -5,11 +5,11 @@ import '../models/discovery_contact.dart'; import 'prefs_manager.dart'; class ContactDiscoveryStore { - static const String _key = 'discovered_contacts'; + static const String _keyPrefix = 'discovered_contacts'; Future> loadContacts() async { final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_key); + final jsonStr = prefs.getString(_keyPrefix); if (jsonStr == null) return []; try { @@ -25,7 +25,7 @@ class ContactDiscoveryStore { Future saveContacts(List contacts) async { final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); - await prefs.setString(_key, jsonEncode(jsonList)); + await prefs.setString(_keyPrefix, jsonEncode(jsonList)); } Map _toJson(DiscoveryContact contact) { diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index 907cc5c..c1a7702 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -1,17 +1,42 @@ import 'dart:convert'; import '../models/contact_group.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ContactGroupStore { - static const String _key = 'contact_groups'; + static const String _keyPrefix = 'contact_groups'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future> loadGroups() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load contact groups.'); + return []; + } final prefs = PrefsManager.instance; - final raw = prefs.getString(_key); - if (raw == null || raw.isEmpty) return []; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final decoded = jsonDecode(raw); + final decoded = jsonDecode(jsonString); if (decoded is List) { return decoded .whereType>() @@ -25,8 +50,12 @@ class ContactGroupStore { } Future saveGroups(List groups) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save contact groups.'); + return; + } final prefs = PrefsManager.instance; final encoded = jsonEncode(groups.map((group) => group.toJson()).toList()); - await prefs.setString(_key, encoded); + await prefs.setString(keyFor, encoded); } } diff --git a/lib/storage/contact_settings_store.dart b/lib/storage/contact_settings_store.dart index 5a7949d..94c6430 100644 --- a/lib/storage/contact_settings_store.dart +++ b/lib/storage/contact_settings_store.dart @@ -1,17 +1,49 @@ +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ContactSettingsStore { - static const String _smazKeyPrefix = 'contact_smaz_'; + static const String _keyPrefix = 'contact_smaz_'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future loadSmazEnabled(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load contact settings.', + ); + return false; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; + final oldKey = '$_keyPrefix$contactKeyHex'; + bool? enabled = prefs.getBool(key); + if (enabled == null) { + // Attempt migration from legacy unscoped key on first load + enabled = prefs.getBool(oldKey); + prefs.remove(oldKey); + if (enabled != null) { + appLogger.info( + 'Migrating contact settings from legacy key $oldKey to scoped key $key', + ); + await prefs.setBool(key, enabled); + } + } return prefs.getBool(key) ?? false; } Future saveSmazEnabled(String contactKeyHex, bool enabled) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save contact settings.', + ); + return; + } final prefs = PrefsManager.instance; - final key = '$_smazKeyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; await prefs.setBool(key, enabled); } } diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 504ff16..8f9e84d 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -2,18 +2,43 @@ import 'dart:convert'; import 'dart:typed_data'; import '../models/contact.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class ContactStore { - static const String _key = 'contacts'; + static const String _keyPrefix = 'contacts'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; Future> loadContacts() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load contacts.'); + return []; + } final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_key); - if (jsonStr == null) return []; + String? jsonString = prefs.getString(keyFor); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating contacts from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { - final jsonList = jsonDecode(jsonStr) as List; + final jsonList = jsonDecode(jsonString) as List; return jsonList .map((entry) => _fromJson(entry as Map)) .toList(); @@ -23,9 +48,13 @@ class ContactStore { } Future saveContacts(List contacts) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save contacts.'); + return; + } final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); - await prefs.setString(_key, jsonEncode(jsonList)); + await prefs.setString(keyFor, jsonEncode(jsonList)); } Map _toJson(Contact contact) { diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index 9526ef3..82caa78 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -2,26 +2,56 @@ import 'dart:convert'; import 'dart:typed_data'; import '../models/message.dart'; import '../helpers/smaz.dart'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; class MessageStore { static const String _keyPrefix = 'messages_'; + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; + Future saveMessages( String contactKeyHex, List messages, ) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save messages.'); + return; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; final jsonList = messages.map(_messageToJson).toList(); await prefs.setString(key, jsonEncode(jsonList)); } Future> loadMessages(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load messages.'); + return []; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$contactKeyHex'; - final jsonString = prefs.getString(key); - if (jsonString == null) return []; + final key = '$keyFor$contactKeyHex'; + final oldKey = '$_keyPrefix$contactKeyHex'; + String? jsonString = prefs.getString(key); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(oldKey); + prefs.remove(oldKey); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating messages from legacy key $oldKey to scoped key $key', + ); + await prefs.setString(key, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return []; + } try { final jsonList = jsonDecode(jsonString) as List; @@ -32,8 +62,12 @@ class MessageStore { } Future clearMessages(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot clear messages.'); + return; + } final prefs = PrefsManager.instance; - final key = '$_keyPrefix$contactKeyHex'; + final key = '$keyFor$contactKeyHex'; await prefs.remove(key); } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index 201d25e..c0cecee 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -1,11 +1,18 @@ import 'dart:async'; import 'dart:convert'; +import '../utils/app_logger.dart'; import 'prefs_manager.dart'; /// Storage for unread message tracking with debounced writes to reduce I/O. class UnreadStore { - static const String _contactUnreadCountKey = 'contact_unread_count'; + static const String _keyPrefix = 'contact_unread_count'; + + String publicKeyHex = ''; + set setPublicKeyHex(String value) => + publicKeyHex = value.length >= 10 ? value.substring(0, 10) : ''; + + String get keyFor => '$_keyPrefix$publicKeyHex'; // Debounce timers to batch rapid writes Timer? _contactUnreadSaveTimer; @@ -20,12 +27,30 @@ class UnreadStore { } Future> loadContactUnreadCount() async { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot load unread counts.'); + return {}; + } final prefs = PrefsManager.instance; - final jsonStr = prefs.getString(_contactUnreadCountKey); - if (jsonStr == null) return {}; + String? jsonString = prefs.getString(_keyPrefix); + if (jsonString == null || jsonString.isEmpty) { + // Attempt migration from legacy unscoped key on first load + final legacyJsonString = prefs.getString(_keyPrefix); + prefs.remove(_keyPrefix); + if (legacyJsonString != null && legacyJsonString.isNotEmpty) { + appLogger.info( + 'Migrating channel messages from legacy key $_keyPrefix to scoped key $keyFor', + ); + await prefs.setString(keyFor, legacyJsonString); + jsonString = legacyJsonString; + } + } + if (jsonString == null || jsonString.isEmpty) { + return {}; + } try { - final json = jsonDecode(jsonStr) as Map; + final json = jsonDecode(jsonString) as Map; return json.map((key, value) => MapEntry(key, value as int)); } catch (_) { return {}; @@ -33,6 +58,10 @@ class UnreadStore { } void saveContactUnreadCount(Map counts) { + if (publicKeyHex.isEmpty) { + appLogger.warn('Public key hex is not set. Cannot save unread counts.'); + return; + } _pendingContactUnreadCount = counts; _contactUnreadSaveTimer?.cancel(); @@ -49,7 +78,7 @@ class UnreadStore { final prefs = PrefsManager.instance; final jsonStr = jsonEncode(_pendingContactUnreadCount); - await prefs.setString(_contactUnreadCountKey, jsonStr); + await prefs.setString(keyFor, jsonStr); _pendingContactUnreadCount = null; } From c81791cf1eea320af55fc4ffa3e443161b8d542d Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 12 Mar 2026 08:39:17 -0700 Subject: [PATCH 292/421] Migrate legacy storage keys to scoped keys in various store classes (#289) --- lib/storage/channel_message_store.dart | 13 ++++++++----- lib/storage/channel_settings_store.dart | 2 +- lib/storage/channel_store.dart | 4 ++++ lib/storage/community_store.dart | 3 +++ lib/storage/contact_group_store.dart | 3 +++ lib/storage/contact_store.dart | 3 +++ lib/storage/message_store.dart | 3 +++ lib/storage/unread_store.dart | 3 +++ 8 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 9c9f7e8..50d13f7 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -46,24 +46,27 @@ class ChannelMessageStore { } final prefs = PrefsManager.instance; final key = '$keyFor$channelIndex'; + final oldKey = '$_keyPrefix$channelIndex'; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(oldKey); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load - final legacyJsonString = prefs.getString(_keyPrefix); - prefs.remove(_keyPrefix); + final legacyJsonString = prefs.getString(oldKey); + prefs.remove(oldKey); if (legacyJsonString != null && legacyJsonString.isNotEmpty) { appLogger.info( - 'Migrating channel messages from legacy key $_keyPrefix to scoped key $key', + 'Migrating channel messages from legacy key $oldKey to scoped key $key', ); await prefs.setString(key, legacyJsonString); jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } - try { final jsonList = jsonDecode(jsonString) as List; return jsonList.map((json) => _messageFromJson(json)).toList(); diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index 3b639cd..3fb00eb 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -20,7 +20,7 @@ class ChannelSettingsStore { final prefs = PrefsManager.instance; final key = '$keyFor$channelIndex'; final oldKey = '$_keyPrefix$channelIndex'; - bool? enabled = prefs.getBool(key); + bool? enabled = prefs.getBool(oldKey); if (enabled == null) { // Attempt migration from legacy unscoped key on first load enabled = prefs.getBool(oldKey); diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index 1bad7e3..775398e 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -32,6 +32,10 @@ class ChannelStore { jsonString = legacyJsonString; } } + + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index c7198e7..6df859a 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -38,6 +38,9 @@ class CommunityStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index c1a7702..986bfdd 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -31,6 +31,9 @@ class ContactGroupStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 8f9e84d..a4e2f0d 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -33,6 +33,9 @@ class ContactStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index 82caa78..9a39e3f 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -49,6 +49,9 @@ class MessageStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return []; } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index c0cecee..d46fb41 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -45,6 +45,9 @@ class UnreadStore { jsonString = legacyJsonString; } } + if (jsonString == null || jsonString.isEmpty) { + jsonString = prefs.getString(keyFor); + } if (jsonString == null || jsonString.isEmpty) { return {}; } From 81758adc61d5efb84cefe5979185254038adf0e8 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 12 Mar 2026 23:08:46 -0700 Subject: [PATCH 293/421] Dev discovery (#291) * Refactor contact handling: replace DiscoveryContact with Contact, update related methods and settings * Enhance contact handling: include latitude, longitude, and last modified timestamp in contact updates; refactor path handling to accommodate discovered contacts across multiple screens * Enhance SNRIndicator: include discovered contacts in name resolution for repeaters * Refactor path handling: replace addReturnPath with buildPath to improve path construction logic and handle target contact types * Update lib/screens/map_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add localization for "Show Discovery Contacts" in multiple languages and refactor location plausibility check in map screen * Enhance contact management: update discovered contacts' active status and improve contact handling with flags and raw packet data * Refactor ChannelsScreen: pass ChannelMessageStore to buildExpandedContent and ensure messages are cleared after channel creation * Update MapScreen: adjust label zoom threshold and refactor guessed marker building to include labels * Refactor ChannelsScreen: change channelMessageStore to a private getter and update its usage in buildExpandedContent calls * Enhance location plausibility check: add latitude and longitude bounds to ensure valid coordinates * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor MeshCoreConnector and related stores: update discovered contacts handling, migrate legacy keys, and set public key in community store * Refactor MeshCoreConnector and ChannelsScreen: update discovered contacts handling and set public key in community store; enhance location plausibility check in MapScreen * Update CMD_ADD_UPDATE_CONTACT frame format to include optional latitude and longitude fields --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 58 +++++-- lib/connector/meshcore_protocol.dart | 53 +++++-- lib/l10n/app_bg.arb | 3 +- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_it.arb | 3 +- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_bg.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_sk.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_localizations_sv.dart | 3 + lib/l10n/app_localizations_uk.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pl.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_sk.arb | 3 +- lib/l10n/app_sl.arb | 3 +- lib/l10n/app_sv.arb | 3 +- lib/l10n/app_uk.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/models/app_settings.dart | 8 + lib/models/contact.dart | 10 ++ lib/models/discovery_contact.dart | 105 ------------ lib/screens/ble_debug_log_screen.dart | 13 ++ lib/screens/channel_message_path_screen.dart | 17 +- lib/screens/channels_screen.dart | 42 +++-- lib/screens/community_qr_scanner_screen.dart | 5 + lib/screens/discovery_screen.dart | 15 +- lib/screens/map_screen.dart | 159 ++++++++++++++----- lib/screens/neighbors_screen.dart | 8 +- lib/screens/path_trace_map.dart | 55 +++++-- lib/services/app_settings_service.dart | 4 + lib/storage/channel_message_store.dart | 2 +- lib/storage/channel_order_store.dart | 2 +- lib/storage/channel_settings_store.dart | 2 +- lib/storage/channel_store.dart | 2 +- lib/storage/community_store.dart | 2 +- lib/storage/contact_discovery_store.dart | 38 ++++- lib/storage/contact_group_store.dart | 2 +- lib/storage/contact_store.dart | 8 + lib/storage/unread_store.dart | 2 +- lib/utils/contact_search.dart | 4 +- lib/widgets/snr_indicator.dart | 7 +- 56 files changed, 476 insertions(+), 241 deletions(-) delete mode 100644 lib/models/discovery_contact.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2ea09ca..9ee6e92 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:crypto/crypto.dart' as crypto; -import 'package:meshcore_open/models/discovery_contact.dart'; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -120,7 +119,7 @@ class MeshCoreConnector extends ChangeNotifier { final List _scanResults = []; final List _contacts = []; - final List _discoveredContacts = []; + final List _discoveredContacts = []; final List _channels = []; final Map> _conversations = {}; final Map> _channelMessages = {}; @@ -281,7 +280,7 @@ class MeshCoreConnector extends ChangeNotifier { ); } - List get discoveredContacts { + List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -664,7 +663,6 @@ class MeshCoreConnector extends ChangeNotifier { // Initialize notification service _notificationService.initialize(); _loadChannelOrder(); - _loadDiscoveredContactCache(); // Initialize retry service callbacks _retryService?.initialize( @@ -1904,7 +1902,11 @@ class MeshCoreConnector extends ChangeNotifier { Future removeContact(Contact contact) async { if (!isConnected) return; - _handleDiscovery(contact, Uint8List(0), noNotify: true); + _handleDiscovery( + contact, + contact.rawPacket ?? Uint8List(0), + noNotify: true, + ); await sendFrame(buildRemoveContactFrame(contact.publicKey)); _contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex); @@ -1920,7 +1922,20 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } - Future removeDiscoveredContact(DiscoveryContact contact) async { + Future updateKnownDiscovered() async { + if (!isConnected) return; + for (int i = 0; i < _discoveredContacts.length; i++) { + _discoveredContacts[i] = _discoveredContacts[i].copyWith( + isActive: _knownContactKeys.contains( + _discoveredContacts[i].publicKeyHex, + ), + ); + } + unawaited(_persistDiscoveredContacts()); + notifyListeners(); + } + + Future removeDiscoveredContact(Contact contact) async { if (!isConnected) return; _discoveredContacts.removeWhere( (c) => c.publicKeyHex == contact.publicKeyHex, @@ -1929,7 +1944,7 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } - Future importDiscoveredContact(DiscoveryContact contact) async { + Future importDiscoveredContact(Contact contact) async { if (!isConnected) return; await sendFrame( @@ -1938,11 +1953,23 @@ class MeshCoreConnector extends ChangeNotifier { contact.path, contact.pathLength, type: contact.type, - flags: 0, + flags: contact.flags, name: contact.name, + lat: contact.latitude, + lon: contact.longitude, + lastModified: contact.lastSeen, ), ); + // Update the discovered contact to mark it as active (imported) + final discoveredIndex = _discoveredContacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + if (discoveredIndex >= 0) { + _discoveredContacts[discoveredIndex] = + _discoveredContacts[discoveredIndex].copyWith(isActive: true); + } + _handleContactAdvert( Contact( publicKey: contact.publicKey, @@ -1953,6 +1980,7 @@ class MeshCoreConnector extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: DateTime.now(), + flags: contact.flags, ), ); notifyListeners(); @@ -1969,6 +1997,8 @@ class MeshCoreConnector extends ChangeNotifier { final existing = _contacts[existingIndex]; // Use copyWith to preserve pathOverride and pathOverrideBytes _contacts[existingIndex] = existing.copyWith( + pathOverride: null, + pathOverrideBytes: null, pathLength: -1, path: Uint8List(0), ); @@ -2324,6 +2354,7 @@ class MeshCoreConnector extends ChangeNotifier { debugPrint('Got END_OF_CONTACTS'); _isLoadingContacts = false; _preserveContactsOnRefresh = false; + unawaited(updateKnownDiscovered()); notifyListeners(); unawaited(_persistContacts()); if (PlatformInfo.isWeb && @@ -2510,6 +2541,7 @@ class MeshCoreConnector extends ChangeNotifier { // Load persisted channel messages loadAllChannelMessages(); loadUnreadState(); + _loadDiscoveredContactCache(); _awaitingSelfInfo = false; _selfInfoRetryTimer?.cancel(); @@ -4406,7 +4438,7 @@ class MeshCoreConnector extends ChangeNotifier { } importDiscoveredContact( - DiscoveryContact( + Contact( rawPacket: frame, publicKey: publicKey, name: name, @@ -4477,6 +4509,7 @@ class MeshCoreConnector extends ChangeNotifier { if (isNewContact) { final newContact = Contact( + rawPacket: rawPacket, publicKey: publicKey, name: name, type: type, @@ -4622,13 +4655,15 @@ class MeshCoreConnector extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: contact.lastSeen, + flags: 0, + isActive: false, ); notifyListeners(); unawaited(_persistDiscoveredContacts()); return; } - final disContact = DiscoveryContact( + final disContact = Contact( rawPacket: rawPacket, publicKey: contact.publicKey, name: contact.name, @@ -4638,6 +4673,9 @@ class MeshCoreConnector extends ChangeNotifier { latitude: contact.latitude, longitude: contact.longitude, lastSeen: contact.lastSeen, + lastMessageAt: contact.lastMessageAt, + isActive: false, + flags: 0, ); _discoveredContacts.add(disContact); diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 3484d47..dc9a9f5 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -148,6 +148,19 @@ class BufferWriter { void writeHex(String hex) { writeBytes(hex2Uint8List(hex)); } + + void writeBytesPadded(Uint8List bytes, int totalLength) { + // Path data (64 bytes, zero-padded) + final bytesPadded = Uint8List(totalLength); + final len = bytes.length < totalLength ? bytes.length : totalLength; + if (bytes.isNotEmpty && len > 0) { + final copyLen = bytes.length < totalLength ? bytes.length : totalLength; + for (int i = 0; i < copyLen; i++) { + bytesPadded[i] = bytes[i]; + } + } + writeBytes(bytesPadded); + } } Uint8List hex2Uint8List(String hex) { @@ -676,14 +689,17 @@ Uint8List buildResetPathFrame(Uint8List pubKey) { } // Build CMD_ADD_UPDATE_CONTACT frame to set custom path -// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][timestamp x4] +// Format: [cmd][pub_key x32][type][flags][path_len][path x64][name x32][Lat? x4, Lon? x4][timestamp? x4] Uint8List buildUpdateContactPathFrame( Uint8List pubKey, - Uint8List customPath, + Uint8List path, int pathLen, { int type = 1, // ADV_TYPE_CHAT int flags = 0, String name = '', + double? lat, + double? lon, + DateTime? lastModified, }) { final writer = BufferWriter(); writer.writeByte(cmdAddUpdateContact); @@ -692,17 +708,7 @@ Uint8List buildUpdateContactPathFrame( writer.writeByte(flags); writer.writeByte(pathLen); - // Path data (64 bytes, zero-padded) - final pathPadded = Uint8List(maxPathSize); - if (customPath.isNotEmpty && pathLen > 0) { - final copyLen = customPath.length < maxPathSize - ? customPath.length - : maxPathSize; - for (int i = 0; i < copyLen; i++) { - pathPadded[i] = customPath[i]; - } - } - writer.writeBytes(pathPadded); + writer.writeBytesPadded(path, maxPathSize); // Name (32 bytes, null-padded) writer.writeCString(name, maxNameSize); @@ -711,6 +717,27 @@ Uint8List buildUpdateContactPathFrame( final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; writer.writeUInt32LE(timestamp); + if ((lat == null || lon == null) && lastModified != null) { + // If lat/lon not provided, write zeros + writer.writeInt32LE(0); + writer.writeInt32LE(0); + } else { + // Latitude and Longitude are expected in degrees, convert to int by multiplying by 1e6 + // Latitude + final latitude = lat ?? 0.0; + writer.writeInt32LE((latitude * 1e6).round()); + + // Longitude + final longitude = lon ?? 0.0; + writer.writeInt32LE((longitude * 1e6).round()); + } + + if (lastModified != null) { + // Last modified + final lastModifiedTimestamp = lastModified.millisecondsSinceEpoch ~/ 1000; + writer.writeUInt32LE(lastModifiedTimestamp); + } + return writer.toBytes(); } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 94d8997..a2723d1 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Неуспешно свързване през USB: {error}", "usbStatus_notConnected": "Изберете USB устройство", "usbStatus_searching": "Търсене на USB устройства...", - "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка." + "usbErrorConnectTimedOut": "Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.", + "map_showDiscoveryContacts": "Покажи контакти за откриване" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9ba0f51..2f51360 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1887,5 +1887,6 @@ "usbStatus_notConnected": "Wählen Sie ein USB-Gerät aus", "usbStatus_connecting": "Verbindung zum USB-Gerät...", "usbConnectionFailed": "Fehler beim USB-Verbindungsaufbau: {error}", - "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält." + "usbErrorConnectTimedOut": "Verbindung konnte nicht hergestellt werden. Stellen Sie sicher, dass das Gerät die entsprechende USB-Firmware enthält.", + "map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2605628..ab33cea 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -807,6 +807,7 @@ "map_markers": "Markers", "map_showSharedMarkers": "Show shared markers", "map_showGuessedLocations": "Show guessed node locations", + "map_showDiscoveryContacts": "Show Discovery Contacts", "map_guessedLocation": "Guessed location", "map_lastSeenTime": "Last Seen Time", "map_sharedPin": "Shared pin", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9b791d3..98d7c04 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1887,5 +1887,6 @@ "usbStatus_searching": "Buscando dispositivos USB...", "usbStatus_notConnected": "Seleccione un dispositivo USB", "usbConnectionFailed": "Error al conectar mediante USB: {error}", - "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion." + "usbErrorConnectTimedOut": "La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.", + "map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a7bedc9..fe437e6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Échec de la connexion USB : {error}", "usbStatus_connecting": "Connexion au périphérique USB...", "usbStatus_searching": "Recherche de périphériques USB...", - "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion." + "usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion.", + "map_showDiscoveryContacts": "Afficher les contacts de découverte" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 423ff40..6368ab3 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Errore nella connessione USB: {error}", "usbStatus_notConnected": "Seleziona un dispositivo USB", "usbStatus_connecting": "Connessione al dispositivo USB...", - "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion." + "usbErrorConnectTimedOut": "La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.", + "map_showDiscoveryContacts": "Mostra Contatti di Discovery" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8d3f86b..a2a25ec 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2788,6 +2788,12 @@ abstract class AppLocalizations { /// **'Show guessed node locations'** String get map_showGuessedLocations; + /// No description provided for @map_showDiscoveryContacts. + /// + /// In en, this message translates to: + /// **'Show Discovery Contacts'** + String get map_showDiscoveryContacts; + /// No description provided for @map_guessedLocation. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 356106e..ce9c061 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1531,6 +1531,9 @@ class AppLocalizationsBg extends AppLocalizations { String get map_showGuessedLocations => 'Покажете местоположенията на предположените възли.'; + @override + String get map_showDiscoveryContacts => 'Покажи контакти за откриване'; + @override String get map_guessedLocation => 'Предполагано местоположение'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 6353f35..14cb58c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1531,6 +1531,9 @@ class AppLocalizationsDe extends AppLocalizations { String get map_showGuessedLocations => 'Zeige die vermuteten Knotenpositionen'; + @override + String get map_showDiscoveryContacts => 'Entdeckungs-Kontakte anzeigen'; + @override String get map_guessedLocation => 'Geschätzter Ort'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9c20df7..9d9dab4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1506,6 +1506,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_showGuessedLocations => 'Show guessed node locations'; + @override + String get map_showDiscoveryContacts => 'Show Discovery Contacts'; + @override String get map_guessedLocation => 'Guessed location'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index eecbd48..98c0f37 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1529,6 +1529,9 @@ class AppLocalizationsEs extends AppLocalizations { String get map_showGuessedLocations => 'Mostrar las ubicaciones estimadas de los nodos.'; + @override + String get map_showDiscoveryContacts => 'Mostrar Contactos de Descubrimiento'; + @override String get map_guessedLocation => 'Ubicación estimada'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 5cabc86..697db20 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1536,6 +1536,9 @@ class AppLocalizationsFr extends AppLocalizations { String get map_showGuessedLocations => 'Afficher les emplacements des nœuds estimés'; + @override + String get map_showDiscoveryContacts => 'Afficher les contacts de découverte'; + @override String get map_guessedLocation => 'Lieu deviné'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d170540..3655cdd 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1528,6 +1528,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi'; + @override + String get map_showDiscoveryContacts => 'Mostra Contatti di Discovery'; + @override String get map_guessedLocation => 'Località indovinata'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 323ba34..1acb14d 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1521,6 +1521,9 @@ class AppLocalizationsNl extends AppLocalizations { String get map_showGuessedLocations => 'Toon de voorspelde locaties van de knopen'; + @override + String get map_showDiscoveryContacts => 'Ontdek contacten weergeven'; + @override String get map_guessedLocation => 'Geroerde locatie'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 9175c3e..5d8e335 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1530,6 +1530,9 @@ class AppLocalizationsPl extends AppLocalizations { String get map_showGuessedLocations => 'Wyświetl lokalizacje zgadanych węzłów'; + @override + String get map_showDiscoveryContacts => 'Pokaż kontakty odkrywania'; + @override String get map_guessedLocation => 'Wydana lokalizacja'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ff09213..a533fa5 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1530,6 +1530,9 @@ class AppLocalizationsPt extends AppLocalizations { String get map_showGuessedLocations => 'Mostrar as localizações dos nós estimados'; + @override + String get map_showDiscoveryContacts => 'Mostrar Contatos de Descoberta'; + @override String get map_guessedLocation => 'Localização estimada'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 69a5891..7bf4418 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1532,6 +1532,9 @@ class AppLocalizationsRu extends AppLocalizations { String get map_showGuessedLocations => 'Отобразить предполагаемые места расположения узлов'; + @override + String get map_showDiscoveryContacts => 'Показать контакты Discovery'; + @override String get map_guessedLocation => 'Угаданное место'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index d0e75b0..516fb6c 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1524,6 +1524,9 @@ class AppLocalizationsSk extends AppLocalizations { String get map_showGuessedLocations => 'Zobraziť umiestnenia odhadnutých uzlov'; + @override + String get map_showDiscoveryContacts => 'Zobraziť kontakty objavov'; + @override String get map_guessedLocation => 'Odhadnutá lokalita'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 21e3d9d..f6f0df8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1517,6 +1517,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.'; + @override + String get map_showDiscoveryContacts => 'Prikaži odkritja kontaktov'; + @override String get map_guessedLocation => 'Predpostavljena lokacija'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 5951fae..9595dc0 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1514,6 +1514,9 @@ class AppLocalizationsSv extends AppLocalizations { String get map_showGuessedLocations => 'Visa upp de antagna nodernas placeringar'; + @override + String get map_showDiscoveryContacts => 'Visa Discovery-kontakter'; + @override String get map_guessedLocation => 'Gissad plats'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index b8fd60a..2e2537b 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1529,6 +1529,9 @@ class AppLocalizationsUk extends AppLocalizations { String get map_showGuessedLocations => 'Показати місцезнаходження передбачених вузлів'; + @override + String get map_showDiscoveryContacts => 'Показати контакти Відкриття'; + @override String get map_guessedLocation => 'Визначено місцезнаходження'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 27e6c21..058dce1 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1440,6 +1440,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_showGuessedLocations => '显示猜测的节点位置'; + @override + String get map_showDiscoveryContacts => '显示发现联系人'; + @override String get map_guessedLocation => '猜测的位置'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 94df130..0a29595 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Selecteer een USB-apparaat", "usbStatus_connecting": "Verbinding maken met USB-apparaat...", "usbStatus_searching": "Zoeken naar USB-apparaten...", - "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft." + "usbErrorConnectTimedOut": "Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.", + "map_showDiscoveryContacts": "Ontdek contacten weergeven" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d020e0e..43ab9dd 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1859,5 +1859,6 @@ "usbStatus_connecting": "Połączenie z urządzeniem USB...", "usbStatus_notConnected": "Wybierz urządzenie USB", "usbConnectionFailed": "Błąd połączenia USB: {error}", - "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"." + "usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".", + "map_showDiscoveryContacts": "Pokaż kontakty odkrywania" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index d52cb41..11aa84d 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Selecione um dispositivo USB", "usbConnectionFailed": "Falha na conexão USB: {error}", "usbStatus_connecting": "Conectando ao dispositivo USB...", - "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion." + "usbErrorConnectTimedOut": "A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.", + "map_showDiscoveryContacts": "Mostrar Contatos de Descoberta" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 92fd55e..af9c220 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1099,5 +1099,6 @@ "usbStatus_connecting": "Подключение к USB-устройству...", "usbConnectionFailed": "Не удалось установить соединение через USB: {error}", "usbStatus_notConnected": "Выберите USB-устройство", - "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion." + "usbErrorConnectTimedOut": "Соединение не установлено. Убедитесь, что устройство имеет установленное программное обеспечение USB Companion.", + "map_showDiscoveryContacts": "Показать контакты Discovery" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 141147c..e844f60 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1859,5 +1859,6 @@ "usbConnectionFailed": "Neúspešné pripojenie cez USB: {error}", "usbStatus_notConnected": "Vyberte USB zariadenie", "usbStatus_connecting": "Pripojenie k USB zariadeniu...", - "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion." + "usbErrorConnectTimedOut": "Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.", + "map_showDiscoveryContacts": "Zobraziť kontakty objavov" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 12529d6..939aad6 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1859,5 +1859,6 @@ "usbStatus_connecting": "Povezava z USB napravo...", "usbStatus_searching": "Iskanje USB naprav...", "usbConnectionFailed": "Napaka pri povezavi preko USB: {error}", - "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion." + "usbErrorConnectTimedOut": "Vzpostavitve ni bilo mogo. Prosimo, da se prepričate, da ima naprave trenutno nameštan firmware USB Companion.", + "map_showDiscoveryContacts": "Prikaži odkritja kontaktov" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index f7615df..9611f18 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Välj en USB-enhet", "usbConnectionFailed": "Fel vid USB-anslutning: {error}", "usbStatus_searching": "Söker efter USB-enheter...", - "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware." + "usbErrorConnectTimedOut": "Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.", + "map_showDiscoveryContacts": "Visa Discovery-kontakter" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 7794098..389184c 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1859,5 +1859,6 @@ "usbStatus_notConnected": "Виберіть пристрій USB", "usbConnectionFailed": "Не вдалося встановити з'єднання через USB: {error}", "usbStatus_connecting": "Підключення до USB-пристрою...", - "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion." + "usbErrorConnectTimedOut": "З'єднання не вдалося встановити. Переконайтеся, що пристрій має встановлене програмне забезпечення USB Companion.", + "map_showDiscoveryContacts": "Показати контакти Відкриття" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index dfc8e64..8a52983 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1864,5 +1864,6 @@ "usbStatus_connecting": "连接USB设备...", "usbStatus_notConnected": "选择一个 USB 设备", "usbConnectionFailed": "USB 连接失败:{error}", - "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。" + "usbErrorConnectTimedOut": "连接超时。请确保设备已安装 USB 伴侣固件。", + "map_showDiscoveryContacts": "显示发现联系人" } diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index abcc729..c89ac27 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -39,6 +39,7 @@ class AppSettings { final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; final Set mutedChannels; + final bool mapShowDiscoveryContacts; AppSettings({ this.clearPathOnMaxRetry = false, @@ -66,6 +67,7 @@ class AppSettings { Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, Set? mutedChannels, + this.mapShowDiscoveryContacts = true, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, mutedChannels = mutedChannels ?? {}; @@ -97,6 +99,7 @@ class AppSettings { 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, 'muted_channels': mutedChannels.toList(), + 'map_show_discovery_contacts': mapShowDiscoveryContacts, }; } @@ -152,6 +155,8 @@ class AppSettings { ?.map((e) => e.toString()) .toSet()) ?? {}, + mapShowDiscoveryContacts: + json['map_show_discovery_contacts'] as bool? ?? true, ); } @@ -181,6 +186,7 @@ class AppSettings { Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, Set? mutedChannels, + bool? mapShowDiscoveryContacts, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -217,6 +223,8 @@ class AppSettings { batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, unitSystem: unitSystem ?? this.unitSystem, mutedChannels: mutedChannels ?? this.mutedChannels, + mapShowDiscoveryContacts: + mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts, ); } } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index b4acff7..cab58cb 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -17,6 +17,8 @@ class Contact { final double? longitude; final DateTime lastSeen; final DateTime lastMessageAt; + final bool isActive; + final Uint8List? rawPacket; Contact({ required this.publicKey, @@ -31,6 +33,8 @@ class Contact { this.longitude, required this.lastSeen, DateTime? lastMessageAt, + this.isActive = true, + this.rawPacket, }) : lastMessageAt = lastMessageAt ?? lastSeen; String get publicKeyHex => pubKeyToHex(publicKey); @@ -78,6 +82,8 @@ class Contact { double? longitude, DateTime? lastSeen, DateTime? lastMessageAt, + bool? isActive, + Uint8List? rawPacket, }) { return Contact( publicKey: publicKey ?? this.publicKey, @@ -96,6 +102,8 @@ class Contact { longitude: longitude ?? this.longitude, lastSeen: lastSeen ?? this.lastSeen, lastMessageAt: lastMessageAt ?? this.lastMessageAt, + isActive: isActive ?? this.isActive, + rawPacket: rawPacket ?? this.rawPacket, ); } @@ -204,6 +212,8 @@ class Contact { latitude: lat, longitude: lon, lastSeen: DateTime.fromMillisecondsSinceEpoch(lastMod * 1000), + isActive: true, + rawPacket: null, ); } catch (e) { appLogger.error('Failed to parse contact frame: $e'); diff --git a/lib/models/discovery_contact.dart b/lib/models/discovery_contact.dart deleted file mode 100644 index f6c6a52..0000000 --- a/lib/models/discovery_contact.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'dart:typed_data'; -import '../connector/meshcore_protocol.dart'; - -class DiscoveryContact { - final Uint8List rawPacket; - final Uint8List publicKey; - final String name; - final int type; - final int pathLength; // -1 = flood, 0+ = direct hops (from device) - final Uint8List path; // Path bytes from device - final double? latitude; - final double? longitude; - final DateTime lastSeen; - - DiscoveryContact({ - required this.rawPacket, - required this.publicKey, - required this.name, - required this.type, - required this.pathLength, - required this.path, - this.latitude, - this.longitude, - required this.lastSeen, - }); - - String get publicKeyHex => pubKeyToHex(publicKey); - - String get typeLabel { - switch (type) { - case advTypeChat: - return 'Chat'; - case advTypeRepeater: - return 'Repeater'; - case advTypeRoom: - return 'Room'; - case advTypeSensor: - return 'Sensor'; - default: - return 'Unknown'; - } - } - - String get pathLabel { - if (pathLength < 0) return 'Flood'; - if (pathLength == 0) return 'Direct'; - return '$pathLength hops'; - } - - bool get hasLocation => latitude != null && longitude != null; - - DiscoveryContact copyWith({ - Uint8List? rawPacket, - Uint8List? publicKey, - String? name, - int? type, - int? pathLength, - Uint8List? path, - double? latitude, - double? longitude, - DateTime? lastSeen, - }) { - return DiscoveryContact( - rawPacket: rawPacket ?? this.rawPacket, - publicKey: publicKey ?? this.publicKey, - name: name ?? this.name, - type: type ?? this.type, - pathLength: pathLength ?? this.pathLength, - path: path ?? this.path, - latitude: latitude ?? this.latitude, - longitude: longitude ?? this.longitude, - lastSeen: lastSeen ?? this.lastSeen, - ); - } - - String get pathIdList { - final pathBytes = path; - if (pathBytes.isEmpty) return ''; - final parts = []; - final groupSize = pathHashSize; - for (int i = 0; i < pathBytes.length; i += groupSize) { - final end = (i + groupSize) <= pathBytes.length - ? (i + groupSize) - : pathBytes.length; - final chunk = pathBytes.sublist(i, end); - parts.add( - chunk - .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) - .join(), - ); - } - return parts.join(','); - } - - String get shortPubKeyHex { - return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; - } - - @override - bool operator ==(Object other) => - other is DiscoveryContact && publicKeyHex == other.publicKeyHex; - - @override - int get hashCode => publicKeyHex.hashCode; -} diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 88f734b..a90f9f0 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -118,6 +118,19 @@ class _BleDebugLogScreenState extends State { : Icons.download, size: 18, ), + onLongPress: () async { + await Clipboard.setData( + ClipboardData( + text: entry.payload + .map( + (b) => b + .toRadixString(16) + .padLeft(2, '0'), + ) + .join(''), + ), + ); + }, ); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 44dfe79..c2c57f0 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -40,8 +40,11 @@ class ChannelMessagePathScreen extends StatelessWidget { final primaryPath = !channelMessage && !message.isOutgoing ? Uint8List.fromList(primaryPathTmp.reversed.toList()) : primaryPathTmp; - - final hops = _buildPathHops(primaryPath, connector.contacts, l10n); + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; + final hops = _buildPathHops(primaryPath, contacts, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( primaryPath.length, @@ -364,11 +367,11 @@ class _ChannelMessagePathMapScreenState : selectedPathTmp; final selectedIndex = _indexForPath(selectedPath, observedPaths); - final hops = _buildPathHops( - selectedPath, - connector.contacts, - context.l10n, - ); + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; + final hops = _buildPathHops(selectedPath, contacts, context.l10n); final points = []; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 00820ed..b56b563 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -51,6 +51,8 @@ class _ChannelsScreenState extends State // Cache of PSK hex -> Community for quick lookup final Map _pskToCommunity = {}; + ChannelMessageStore get _channelMessageStore => ChannelMessageStore(); + @override void initState() { super.initState(); @@ -61,6 +63,8 @@ class _ChannelsScreenState extends State } Future _loadCommunities() async { + final connector = context.read(); + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; final communities = await _communityStore.loadCommunities(); if (mounted) { setState(() { @@ -714,6 +718,8 @@ class _ChannelsScreenState extends State bool isRegularHashtag = true; Community? selectedCommunity; + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; + showDialog( context: context, builder: (dialogContext) => StatefulBuilder( @@ -765,7 +771,9 @@ class _ChannelsScreenState extends State ); } - Widget? buildExpandedContent() { + Widget? buildExpandedContent( + ChannelMessageStore channelMessageStore, + ) { switch (selectedOption) { case 0: // Create Private Channel return Column( @@ -790,7 +798,7 @@ class _ChannelsScreenState extends State children: [ Expanded( child: FilledButton( - onPressed: () { + onPressed: () async { final name = nameController.text.trim(); if (name.isEmpty) { ScaffoldMessenger.of( @@ -812,7 +820,14 @@ class _ChannelsScreenState extends State psk[i] = random.nextInt(256); } Navigator.pop(dialogContext); - connector.setChannel(nextIndex, name, psk); + await connector.setChannel( + nextIndex, + name, + psk, + ); + await channelMessageStore.clearChannelMessages( + nextIndex, + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -1331,7 +1346,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_createPrivateChannelDesc, ), - if (selectedOption == 0) buildExpandedContent()!, + if (selectedOption == 0) + buildExpandedContent(_channelMessageStore)!, const Divider(height: 1), buildOptionTile( optionIndex: 1, @@ -1340,7 +1356,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_joinPrivateChannelDesc, ), - if (selectedOption == 1) buildExpandedContent()!, + if (selectedOption == 1) + buildExpandedContent(_channelMessageStore)!, if (!hasPublicChannel) ...[ const Divider(height: 1), buildOptionTile( @@ -1350,7 +1367,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_joinPublicChannelDesc, ), - if (selectedOption == 2) buildExpandedContent()!, + if (selectedOption == 2) + buildExpandedContent(_channelMessageStore)!, ], const Divider(height: 1), buildOptionTile( @@ -1360,7 +1378,8 @@ class _ChannelsScreenState extends State subtitle: dialogContext.l10n.channels_joinHashtagChannelDesc, ), - if (selectedOption == 3) buildExpandedContent()!, + if (selectedOption == 3) + buildExpandedContent(_channelMessageStore)!, const Divider(height: 1), buildOptionTile( optionIndex: 4, @@ -1368,7 +1387,8 @@ class _ChannelsScreenState extends State title: dialogContext.l10n.community_scanQr, subtitle: dialogContext.l10n.community_join, ), - if (selectedOption == 4) buildExpandedContent()!, + if (selectedOption == 4) + buildExpandedContent(_channelMessageStore)!, const Divider(height: 1), buildOptionTile( optionIndex: 5, @@ -1376,7 +1396,8 @@ class _ChannelsScreenState extends State title: dialogContext.l10n.community_create, subtitle: dialogContext.l10n.community_createDesc, ), - if (selectedOption == 5) buildExpandedContent()!, + if (selectedOption == 5) + buildExpandedContent(_channelMessageStore)!, ], ), ), @@ -1526,7 +1547,7 @@ class _ChannelsScreenState extends State try { await connector.deleteChannel(channel.index); - channelMessageStore.clearChannelMessages(channel.index); + await channelMessageStore.clearChannelMessages(channel.index); if (!context.mounted) return; @@ -1751,6 +1772,7 @@ class _ChannelsScreenState extends State } final channelCount = communityChannels.length; + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; showDialog( context: context, diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index 9f8602d..6852dfa 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -51,6 +51,9 @@ class _CommunityQrScannerScreenState extends State { _isProcessing = true; }); + final connector = context.read(); + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; + try { // Parse the community data final community = Community.fromQrData(const Uuid().v4(), data); @@ -209,6 +212,8 @@ class _CommunityQrScannerScreenState extends State { bool addPublicChannel, ) async { // Save community to local storage + final connector = context.read(); + _communityStore.setPublicKeyHex = connector.selfPublicKeyHex; await _communityStore.addCommunity(community); // Optionally add the community public channel to the device diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index f122654..7f065aa 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -7,7 +7,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; -import '../models/discovery_contact.dart'; +import '../models/contact.dart'; import '../utils/contact_search.dart'; import '../widgets/app_bar.dart'; import '../widgets/list_filter_widget.dart'; @@ -129,7 +129,7 @@ class _DiscoveryScreenState extends State { } Future _showContactContextMenu( - DiscoveryContact contact, + Contact contact, MeshCoreConnector connector, ) async { final action = await showModalBottomSheet( @@ -169,7 +169,8 @@ class _DiscoveryScreenState extends State { connector.importDiscoveredContact(contact); break; case 'copy_contact': - final hexString = pubKeyToHex(contact.rawPacket); + if (contact.rawPacket == null) return; + final hexString = pubKeyToHex(contact.rawPacket!); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( @@ -207,7 +208,7 @@ class _DiscoveryScreenState extends State { } Widget _buildFilters( - List filteredAndSorted, + List filteredAndSorted, MeshCoreConnector connector, ) { String hintText = ""; @@ -309,8 +310,8 @@ class _DiscoveryScreenState extends State { ); } - List _filterAndSortContacts( - List contacts, + List _filterAndSortContacts( + List contacts, MeshCoreConnector connector, ) { var filtered = contacts.where((contact) { @@ -350,7 +351,7 @@ class _DiscoveryScreenState extends State { return filtered; } - bool _matchesTypeFilter(DiscoveryContact contact) { + bool _matchesTypeFilter(Contact contact) { switch (typeFilter) { case ContactTypeFilter.all: return true; diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 3d94701..7ffec56 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -50,7 +51,8 @@ class MapScreen extends StatefulWidget { } class _MapScreenState extends State { - static const double _labelZoomThreshold = 8.5; + // Zoom level at which node labels start to appear + static const double _labelZoomThreshold = 12.0; final MapController _mapController = MapController(); final MapMarkerService _markerService = MapMarkerService(); @@ -91,6 +93,15 @@ class _MapScreenState extends State { }); } + bool _checkLocationPlausibility(double lat, double lon) { + const double epsilon = 1e-6; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + double _standardDeviation(List values) { if (values.length <= 1) { return 0.0; @@ -126,7 +137,15 @@ class _MapScreenState extends State { builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); final settings = settingsService.settings; - final contacts = connector.contacts; + final allContacts = [ + ...connector.contacts, + ...connector.discoveredContacts.where((c) => !c.isActive), + ]; + + final contacts = settings.mapShowDiscoveryContacts + ? allContacts + : allContacts.where((c) => c.isActive).toList(); + final highlightPosition = widget.highlightPosition; final sharedMarkers = settings.mapShowMarkers ? _collectSharedMarkers(connector) @@ -159,14 +178,21 @@ class _MapScreenState extends State { : filteredByTime; // Filter by location - final contactsWithLocation = filteredByKeyPrefix - .where((c) => c.hasLocation) - .toList(); + final contactsWithLocation = filteredByKeyPrefix.where((c) { + if (!c.hasLocation) { + return false; + } + return _checkLocationPlausibility(c.latitude!, c.longitude!); + }).toList(); // All contacts with a known location — used as anchors regardless of // time/key-prefix filters so that repeaters are always available. - final allContactsWithLocation = contacts - .where((c) => c.hasLocation) + final allContactsWithLocation = allContacts + .where( + (c) => + c.hasLocation && + _checkLocationPlausibility(c.latitude!, c.longitude!), + ) .toList(); // Compute guessed locations with caching @@ -468,7 +494,10 @@ class _MapScreenState extends State { ), ), if (!_isBuildingPathTrace) - ...guessedLocations.map(_buildGuessedMarker), + ..._buildGuessedMarker( + guessedLocations, + showLabels: _showNodeLabels, + ), ..._buildMarkers( contactsWithLocation, settings, @@ -630,6 +659,13 @@ class _MapScreenState extends State { anchors[0].latitude + offsetDeg * cos(angle), anchors[0].longitude + offsetDeg * sin(angle), ); + + if (!_checkLocationPlausibility( + position.latitude, + position.longitude, + )) { + continue; // discard implausible guesses near (0, 0) + } } else { double lat = 0, lon = 0; for (final a in anchors) { @@ -637,6 +673,12 @@ class _MapScreenState extends State { lon += a.longitude; } position = LatLng(lat / anchors.length, lon / anchors.length); + if (!_checkLocationPlausibility( + position.latitude, + position.longitude, + )) { + continue; // discard implausible guesses near (0, 0 + } } result.add( _GuessedLocation( @@ -710,40 +752,61 @@ class _MapScreenState extends State { .toList(); } - Marker _buildGuessedMarker(_GuessedLocation guess) { - final color = _getNodeColor(guess.contact.type); - return Marker( - point: guess.position, - width: 35, - height: 35, - child: GestureDetector( - onTap: () => _showNodeInfo( - context, - guess.contact, - guessedPosition: guess.position, - ), - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), + List _buildGuessedMarker( + List<_GuessedLocation> guessed, { + required bool showLabels, + }) { + final markers = []; + + for (final guess in guessed) { + final color = _getNodeColor(guess.contact.type); + final marker = Marker( + point: guess.position, + width: 35, + height: 35, + child: GestureDetector( + onTap: () => _showNodeInfo( + context, + guess.contact, + guessedPosition: guess.position, + ), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color.withValues( + alpha: guess.highConfidence ? 0.55 : 0.30, ), - ], - ), - child: const Icon( - Icons.not_listed_location, - color: Colors.white, - size: 20, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.not_listed_location, + color: Colors.white, + size: 20, + ), ), ), - ), - ); + ); + + markers.add(marker); + + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: guess.position, + label: guess.contact.name, + ), + ); + } + } + return markers; } List _buildMarkers( @@ -1203,6 +1266,7 @@ class _MapScreenState extends State { Contact contact, { LatLng? guessedPosition, }) { + final connector = context.read(); showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -1248,6 +1312,9 @@ class _MapScreenState extends State { advTypeChat) // Only show chat button for chat nodes TextButton( onPressed: () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } Navigator.pop(dialogContext); Navigator.push( context, @@ -1261,6 +1328,9 @@ class _MapScreenState extends State { if (contact.type == advTypeRepeater) TextButton( onPressed: () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } Navigator.pop(dialogContext); _showRepeaterLogin(context, contact); }, @@ -1269,6 +1339,9 @@ class _MapScreenState extends State { if (contact.type == advTypeRoom) TextButton( onPressed: () { + if (!contact.isActive) { + connector.importDiscoveredContact(contact); + } Navigator.pop(dialogContext); _showRoomLogin(context, contact); }, @@ -1745,6 +1818,14 @@ class _MapScreenState extends State { }, contentPadding: EdgeInsets.zero, ), + CheckboxListTile( + title: Text(context.l10n.map_showDiscoveryContacts), + value: settings.mapShowDiscoveryContacts, + onChanged: (value) { + service.setMapShowDiscoveryContacts(value ?? true); + }, + contentPadding: EdgeInsets.zero, + ), const SizedBox(height: 16), Text( context.l10n.map_keyPrefix, diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 3dee339..5cb8e45 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -124,12 +124,14 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); - connector.contacts.where((c) => c.type == advTypeRepeater).forEach(( - repeater, - ) { + contacts.where((c) => c.type == advTypeRepeater).forEach((repeater) { for (var neighborData in parsedNeighbors) { final publicKey = neighborData['publicKey']; if (listEquals( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index c6d800e..ceb60a6 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -114,14 +114,37 @@ class _PathTraceMapScreenState extends State { super.dispose(); } - Uint8List addReturnPath(Uint8List pathBytes) { - Uint8List? traceBytes; - final len = (pathBytes.length + pathBytes.length - 1); - traceBytes = Uint8List(len); - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length - 1) { - traceBytes[len - 1 - i] = pathBytes[i]; + Uint8List buildPath(Uint8List pathBytes) { + Uint8List traceBytes; + + if (pathBytes.isEmpty) { + traceBytes = Uint8List(1); + traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0; + return traceBytes; + } + + if (widget.targetContact?.type == advTypeRepeater || + widget.targetContact?.type == advTypeRoom) { + final len = (pathBytes.length + pathBytes.length + 1); + traceBytes = Uint8List(len); + traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0; + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length) { + traceBytes[len - 1 - i] = pathBytes[i]; + } + } + } else { + if (pathBytes.length < 2) { + return pathBytes[0] == 0 ? Uint8List(0) : pathBytes; + } + final len = (pathBytes.length + pathBytes.length - 1); + traceBytes = Uint8List(len); + for (int i = 0; i < pathBytes.length; i++) { + traceBytes[i] = pathBytes[i]; + if (i < pathBytes.length - 1) { + traceBytes[len - 1 - i] = pathBytes[i]; + } } } return traceBytes; @@ -142,11 +165,16 @@ class _PathTraceMapScreenState extends State { : widget.path; if (widget.flipPathRound) { - path = addReturnPath(pathTmp); + path = buildPath(pathTmp); } else { path = pathTmp; } + appLogger.info( + 'Initiating path trace with path: ${_formatPathPrefixes(path)}', + tag: 'PathTraceMapScreen', + ); + final connector = Provider.of(context, listen: false); final frame = buildTraceReq( DateTime.now().millisecondsSinceEpoch ~/ 1000, @@ -235,10 +263,11 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - - connector.contacts.where((c) => c.type != advTypeChat).forEach(( - repeater, - ) { + final contacts = [ + ...connector.contacts, + ...connector.discoveredContacts, + ]; + contacts.where((c) => c.type != advTypeChat).forEach((repeater) { for (var repeaterData in pathData) { if (listEquals( repeater.publicKey.sublist(0, 1), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index c74fa40..a52e364 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -134,6 +134,10 @@ class AppSettingsService extends ChangeNotifier { appLogger.setEnabled(value); } + Future setMapShowDiscoveryContacts(bool value) async { + await updateSettings(_settings.copyWith(mapShowDiscoveryContacts: value)); + } + Future setBatteryChemistryForDevice( String deviceId, String chemistry, diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 50d13f7..7bf44bd 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -48,7 +48,7 @@ class ChannelMessageStore { final key = '$keyFor$channelIndex'; final oldKey = '$_keyPrefix$channelIndex'; - String? jsonString = prefs.getString(oldKey); + String? jsonString = prefs.getString(key); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(oldKey); diff --git a/lib/storage/channel_order_store.dart b/lib/storage/channel_order_store.dart index 48a80f2..88d3f7a 100644 --- a/lib/storage/channel_order_store.dart +++ b/lib/storage/channel_order_store.dart @@ -26,7 +26,7 @@ class ChannelOrderStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index 3fb00eb..276826d 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -32,7 +32,7 @@ class ChannelSettingsStore { await prefs.setBool(key, enabled); } } - return prefs.getBool(key) ?? false; + return enabled ?? false; } Future saveSmazEnabled(int channelIndex, bool enabled) async { diff --git a/lib/storage/channel_store.dart b/lib/storage/channel_store.dart index 775398e..4f40482 100644 --- a/lib/storage/channel_store.dart +++ b/lib/storage/channel_store.dart @@ -19,7 +19,7 @@ class ChannelStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/community_store.dart b/lib/storage/community_store.dart index 6df859a..c69d0b8 100644 --- a/lib/storage/community_store.dart +++ b/lib/storage/community_store.dart @@ -25,7 +25,7 @@ class CommunityStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/contact_discovery_store.dart b/lib/storage/contact_discovery_store.dart index ac47615..89ca027 100644 --- a/lib/storage/contact_discovery_store.dart +++ b/lib/storage/contact_discovery_store.dart @@ -1,13 +1,13 @@ import 'dart:convert'; import 'dart:typed_data'; -import '../models/discovery_contact.dart'; +import '../models/contact.dart'; import 'prefs_manager.dart'; class ContactDiscoveryStore { static const String _keyPrefix = 'discovered_contacts'; - Future> loadContacts() async { + Future> loadContacts() async { final prefs = PrefsManager.instance; final jsonStr = prefs.getString(_keyPrefix); if (jsonStr == null) return []; @@ -22,40 +22,62 @@ class ContactDiscoveryStore { } } - Future saveContacts(List contacts) async { + Future saveContacts(List contacts) async { final prefs = PrefsManager.instance; final jsonList = contacts.map(_toJson).toList(); await prefs.setString(_keyPrefix, jsonEncode(jsonList)); } - Map _toJson(DiscoveryContact contact) { + Map _toJson(Contact contact) { return { - 'rawPacket': base64Encode(contact.rawPacket), 'publicKey': base64Encode(contact.publicKey), 'name': contact.name, 'type': contact.type, + 'flags': contact.flags, 'pathLength': contact.pathLength, 'path': base64Encode(contact.path), + 'pathOverride': contact.pathOverride, + 'pathOverrideBytes': contact.pathOverrideBytes != null + ? base64Encode(contact.pathOverrideBytes!) + : null, 'latitude': contact.latitude, 'longitude': contact.longitude, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, + 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, + 'rawPacket': contact.rawPacket != null + ? base64Encode(contact.rawPacket!) + : null, }; } - DiscoveryContact _fromJson(Map json) { + Contact _fromJson(Map json) { final lastSeenMs = json['lastSeen'] as int? ?? 0; - return DiscoveryContact( - rawPacket: Uint8List.fromList(base64Decode(json['rawPacket'] as String)), + final lastMessageMs = json['lastMessageAt'] as int?; + return Contact( publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', type: json['type'] as int? ?? 0, + flags: json['flags'] as int? ?? 0, pathLength: json['pathLength'] as int? ?? -1, path: json['path'] != null ? Uint8List.fromList(base64Decode(json['path'] as String)) : Uint8List(0), + pathOverride: json['pathOverride'] as int?, + pathOverrideBytes: json['pathOverrideBytes'] != null + ? Uint8List.fromList( + base64Decode(json['pathOverrideBytes'] as String), + ) + : null, latitude: (json['latitude'] as num?)?.toDouble(), longitude: (json['longitude'] as num?)?.toDouble(), lastSeen: DateTime.fromMillisecondsSinceEpoch(lastSeenMs), + lastMessageAt: DateTime.fromMillisecondsSinceEpoch( + lastMessageMs ?? lastSeenMs, + ), + isActive: false, + rawPacket: json['rawPacket'] != null + ? Uint8List.fromList(base64Decode(json['rawPacket'] as String)) + : null, ); } } diff --git a/lib/storage/contact_group_store.dart b/lib/storage/contact_group_store.dart index 986bfdd..ce6a0c6 100644 --- a/lib/storage/contact_group_store.dart +++ b/lib/storage/contact_group_store.dart @@ -18,7 +18,7 @@ class ContactGroupStore { return []; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index a4e2f0d..0e2e3ad 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -76,6 +76,10 @@ class ContactStore { 'longitude': contact.longitude, 'lastSeen': contact.lastSeen.millisecondsSinceEpoch, 'lastMessageAt': contact.lastMessageAt.millisecondsSinceEpoch, + 'isActive': contact.isActive, + 'rawPacket': contact.rawPacket != null + ? base64Encode(contact.rawPacket!) + : null, }; } @@ -103,6 +107,10 @@ class ContactStore { lastMessageAt: DateTime.fromMillisecondsSinceEpoch( lastMessageMs ?? lastSeenMs, ), + isActive: json['isActive'] as bool? ?? true, + rawPacket: json['rawPacket'] != null + ? Uint8List.fromList(base64Decode(json['rawPacket'] as String)) + : null, ); } } diff --git a/lib/storage/unread_store.dart b/lib/storage/unread_store.dart index d46fb41..3b615b1 100644 --- a/lib/storage/unread_store.dart +++ b/lib/storage/unread_store.dart @@ -32,7 +32,7 @@ class UnreadStore { return {}; } final prefs = PrefsManager.instance; - String? jsonString = prefs.getString(_keyPrefix); + String? jsonString = prefs.getString(keyFor); if (jsonString == null || jsonString.isEmpty) { // Attempt migration from legacy unscoped key on first load final legacyJsonString = prefs.getString(_keyPrefix); diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index beec880..1f05fdc 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,5 +1,3 @@ -import 'package:meshcore_open/models/discovery_contact.dart'; - import '../models/contact.dart'; bool matchesContactQuery(Contact contact, String query) { @@ -16,7 +14,7 @@ bool matchesContactQuery(Contact contact, String query) { return contact.publicKeyHex.toLowerCase().startsWith(hexPrefix); } -bool matchesDiscoveryContactQuery(DiscoveryContact contact, String query) { +bool matchesDiscoveryContactQuery(Contact contact, String query) { final normalizedQuery = query.trim().toLowerCase(); if (normalizedQuery.isEmpty) return true; diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 1f592eb..f122836 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -157,8 +157,11 @@ class _SNRIndicatorState extends State { repeater.snr, widget.connector.currentSf, ); - - final name = widget.connector.contacts + final allContacts = [ + ...widget.connector.contacts, + ...widget.connector.discoveredContacts, + ]; + final name = allContacts .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) .map((c) => c.name) .firstOrNull; From db935a745433367dffb46228c621a81ba8982376 Mon Sep 17 00:00:00 2001 From: Zach Date: Fri, 13 Mar 2026 10:58:52 -0700 Subject: [PATCH 294/421] refactor(tcp): promote MeshCoreTcpConnector, fix translations, harden UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace thin MeshCoreTcpManager facade with a proper MeshCoreTcpConnector that owns TcpTransportService and the frame subscription, mirroring MeshCoreUsbManager. The connector no longer holds a raw TcpTransportService or a _tcpFrameSubscription field. - Remove hardcoded default host IP from TcpScreen (keep port 5000 hint). - Disable connect button during scanning state, not just connecting state. - Fix tcpPortLabel mistranslated as nautical "port/harbor" in de, it, pt, nl, sv, sk, sl, zh; fix corrupted Slovak tcpPortHint ("5 000" → "5000"). - Remove unused tcpStatus_connecting string from all 15 locale arb files and all generated app_localizations_*.dart files. - Add extendedPadding to TCP screen FABs to match USB screen. - Add Key to connect button; update tests to use byKey and assert onPressed == null when button is disabled during scanning. --- lib/connector/meshcore_connector.dart | 39 +++++++++----------- lib/connector/meshcore_connector_tcp.dart | 44 ++++++++++++++++++++--- lib/l10n/app_bg.arb | 1 - lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 1 - lib/l10n/app_es.arb | 1 - lib/l10n/app_fr.arb | 1 - lib/l10n/app_it.arb | 3 +- lib/l10n/app_localizations.dart | 6 ---- lib/l10n/app_localizations_bg.dart | 3 -- lib/l10n/app_localizations_de.dart | 6 +--- lib/l10n/app_localizations_en.dart | 3 -- lib/l10n/app_localizations_es.dart | 3 -- lib/l10n/app_localizations_fr.dart | 3 -- lib/l10n/app_localizations_it.dart | 5 +-- lib/l10n/app_localizations_nl.dart | 5 +-- lib/l10n/app_localizations_pl.dart | 3 -- lib/l10n/app_localizations_pt.dart | 6 +--- lib/l10n/app_localizations_ru.dart | 3 -- lib/l10n/app_localizations_sk.dart | 7 ++-- lib/l10n/app_localizations_sl.dart | 5 +-- lib/l10n/app_localizations_sv.dart | 5 +-- lib/l10n/app_localizations_uk.dart | 3 -- lib/l10n/app_localizations_zh.dart | 5 +-- lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pl.arb | 1 - lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 1 - lib/l10n/app_sk.arb | 5 ++- lib/l10n/app_sl.arb | 3 +- lib/l10n/app_sv.arb | 3 +- lib/l10n/app_uk.arb | 1 - lib/l10n/app_zh.arb | 3 +- lib/screens/tcp_screen.dart | 10 ++++-- test/screens/tcp_flow_test.dart | 17 +++++---- 35 files changed, 91 insertions(+), 123 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b800cfa..d974b7b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -116,8 +116,7 @@ class MeshCoreConnector extends ChangeNotifier { bool _manualDisconnect = false; final MeshCoreUsbManager _usbManager = MeshCoreUsbManager(); StreamSubscription? _usbFrameSubscription; - final MeshCoreTcpManager _tcpManager = MeshCoreTcpManager(); - StreamSubscription? _tcpFrameSubscription; + final MeshCoreTcpConnector _tcpConnector = MeshCoreTcpConnector(); MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth; final List _scanResults = []; @@ -257,7 +256,7 @@ class MeshCoreConnector extends ChangeNotifier { bool get isUsbTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.usb; - String? get activeTcpEndpoint => _tcpManager.activeEndpoint; + String? get activeTcpEndpoint => _tcpConnector.activeEndpoint; bool get isTcpTransportConnected => _state == MeshCoreConnectionState.connected && _activeTransport == MeshCoreTransportType.tcp; @@ -666,7 +665,7 @@ class MeshCoreConnector extends ChangeNotifier { _appDebugLogService = appDebugLogService; _backgroundService = backgroundService; _usbManager.setDebugLogService(_appDebugLogService); - _tcpManager.setDebugLogService(_appDebugLogService); + _tcpConnector.setDebugLogService(_appDebugLogService); // Initialize notification service _notificationService.initialize(); @@ -1002,22 +1001,21 @@ class MeshCoreConnector extends ChangeNotifier { await disconnect(manual: false); return; } - if (_tcpManager.isConnected) { - await _tcpManager.disconnect(); + if (_tcpConnector.isConnected) { + await _tcpConnector.disconnect(); } } - await _tcpFrameSubscription?.cancel(); - _tcpFrameSubscription = null; - await _tcpManager.connect(host: host, port: port); + await _tcpConnector.cancelFrameSubscription(); + await _tcpConnector.connect(host: host, port: port); final isTcpConnectCancelled = _activeTransport != MeshCoreTransportType.tcp || _state != MeshCoreConnectionState.connecting || - !_tcpManager.isConnected; + !_tcpConnector.isConnected; if (isTcpConnectCancelled) { await handleTcpConnectAbort( message: - 'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', + 'connectTcp aborted before handshake: state=$_state transport=$_activeTransport connected=${_tcpConnector.isConnected}', ); return; } @@ -1027,16 +1025,16 @@ class MeshCoreConnector extends ChangeNotifier { final isTcpConnectCancelledAfterDelay = _activeTransport != MeshCoreTransportType.tcp || _state != MeshCoreConnectionState.connecting || - !_tcpManager.isConnected; + !_tcpConnector.isConnected; if (isTcpConnectCancelledAfterDelay) { await handleTcpConnectAbort( message: - 'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpManager.isConnected}', + 'connectTcp aborted after connect delay: state=$_state transport=$_activeTransport connected=${_tcpConnector.isConnected}', ); return; } - _tcpFrameSubscription = _tcpManager.frameStream.listen( - _handleFrame, + _tcpConnector.listenFrames( + onFrame: _handleFrame, onError: (error, stackTrace) { _appDebugLogService?.error('TCP transport error: $error', tag: 'TCP'); unawaited(disconnect(manual: false)); @@ -1073,7 +1071,7 @@ class MeshCoreConnector extends ChangeNotifier { manualDisconnect: _manualDisconnect, state: _state, activeTransport: _activeTransport, - tcpManagerConnected: _tcpManager.isConnected, + tcpManagerConnected: _tcpConnector.isConnected, ); if (tcpConnectCancelledBeforeHandshake) { _appDebugLogService?.info( @@ -1445,9 +1443,7 @@ class MeshCoreConnector extends ChangeNotifier { await _usbFrameSubscription?.cancel(); _usbFrameSubscription = null; await _usbManager.disconnect(); - await _tcpFrameSubscription?.cancel(); - _tcpFrameSubscription = null; - await _tcpManager.disconnect(); + await _tcpConnector.disconnect(); await _notifySubscription?.cancel(); _notifySubscription = null; @@ -1530,7 +1526,7 @@ class MeshCoreConnector extends ChangeNotifier { if (_activeTransport == MeshCoreTransportType.usb) { await _usbManager.write(data); } else if (_activeTransport == MeshCoreTransportType.tcp) { - await _tcpManager.write(data); + await _tcpConnector.write(data); } else { if (_rxCharacteristic == null) { throw Exception("MeshCore RX characteristic not available"); @@ -4484,14 +4480,13 @@ class MeshCoreConnector extends ChangeNotifier { _scanSubscription?.cancel(); _connectionSubscription?.cancel(); _usbFrameSubscription?.cancel(); - _tcpFrameSubscription?.cancel(); _notifySubscription?.cancel(); _notifyListenersTimer?.cancel(); _reconnectTimer?.cancel(); _batteryPollTimer?.cancel(); _receivedFramesController.close(); _usbManager.dispose(); - _tcpManager.dispose(); + _tcpConnector.dispose(); // Flush pending unread writes before disposal _unreadStore.flush(); diff --git a/lib/connector/meshcore_connector_tcp.dart b/lib/connector/meshcore_connector_tcp.dart index 92b98d7..7c93d9f 100644 --- a/lib/connector/meshcore_connector_tcp.dart +++ b/lib/connector/meshcore_connector_tcp.dart @@ -1,34 +1,70 @@ +import 'dart:async'; import 'dart:typed_data'; import '../services/app_debug_log_service.dart'; import '../services/tcp_transport_service.dart'; -class MeshCoreTcpManager { +/// Manages TCP transport for MeshCore devices. +/// +/// Owns the [TcpTransportService] and TCP-specific connection state. +/// The main [MeshCoreConnector] delegates all TCP operations here. +class MeshCoreTcpConnector { final TcpTransportService _service = TcpTransportService(); AppDebugLogService? _debugLog; + StreamSubscription? _frameSubscription; + // --- Getters --- String? get activeEndpoint => _service.activeEndpoint; bool get isConnected => _service.isConnected; - Stream get frameStream => _service.frameStream; + // --- Configuration --- void setDebugLogService(AppDebugLogService? service) { _debugLog = service; _service.setDebugLogService(service); } + // --- Connection lifecycle --- Future connect({required String host, required int port}) async { - _debugLog?.info('TcpManager.connect endpoint=$host:$port', tag: 'TCP'); + _debugLog?.info('TcpConnector.connect endpoint=$host:$port', tag: 'TCP'); + await _frameSubscription?.cancel(); + _frameSubscription = null; await _service.connect(host: host, port: port); + _debugLog?.info( + 'TcpConnector.connect done, endpoint=${_service.activeEndpoint}', + tag: 'TCP', + ); + } + + StreamSubscription listenFrames({ + required void Function(Uint8List) onFrame, + required void Function(Object, StackTrace?) onError, + required void Function() onDone, + }) { + _frameSubscription = _service.frameStream.listen( + onFrame, + onError: onError, + onDone: onDone, + ); + return _frameSubscription!; + } + + Future cancelFrameSubscription() async { + await _frameSubscription?.cancel(); + _frameSubscription = null; } Future disconnect() async { - _debugLog?.info('TcpManager.disconnect', tag: 'TCP'); + if (!_service.isConnected && _frameSubscription == null) return; + _debugLog?.info('TcpConnector.disconnect', tag: 'TCP'); + await _frameSubscription?.cancel(); + _frameSubscription = null; await _service.disconnect(); } Future write(Uint8List data) => _service.write(data); void dispose() { + _frameSubscription?.cancel(); _service.dispose(); } } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index fa84e50..c5acba9 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1881,7 +1881,6 @@ "tcpPortLabel": "Пристанище", "tcpPortHint": "5000", "tcpStatus_notConnected": "Въведете крайната точка и свържете се.", - "tcpStatus_connecting": "Свързване към TCP крайния пункт...", "tcpStatus_connectingTo": "Свързване към {endpoint}...", "tcpErrorHostRequired": "Необходим е IP адрес.", "tcpErrorPortInvalid": "Портът трябва да бъде между 1 и 65535.", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index eae33e5..2eed922 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1906,10 +1906,9 @@ "connectionChoiceTcpLabel": "TCP", "tcpHostHint": "192.168.40.10", "tcpScreenTitle": "Verbinden über TCP", - "tcpPortLabel": "Hafen", + "tcpPortLabel": "Port", "tcpPortHint": "5000", "tcpStatus_notConnected": "Geben Sie den Endpunkt ein und verbinden Sie sich.", - "tcpStatus_connecting": "Verbindung zum TCP-Endpunkt hergestellt...", "tcpStatus_connectingTo": "Verbindung zu {endpoint}...", "tcpErrorHostRequired": "Eine IP-Adresse ist erforderlich.", "tcpErrorPortInvalid": "Die Portnummer muss zwischen 1 und 65535 liegen.", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0acf9a5..58569e4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -56,7 +56,6 @@ "tcpPortLabel": "Port", "tcpPortHint": "5000", "tcpStatus_notConnected": "Enter endpoint and connect", - "tcpStatus_connecting": "Connecting to TCP endpoint...", "tcpStatus_connectingTo": "Connecting to {endpoint}...", "@tcpStatus_connectingTo": { "placeholders": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 974e2c3..25e0345 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1909,7 +1909,6 @@ "tcpPortLabel": "Puerto", "tcpPortHint": "5000", "tcpStatus_notConnected": "Ingrese la dirección final y conecte.", - "tcpStatus_connecting": "Conectándose al punto final TCP...", "tcpStatus_connectingTo": "Conectándose a {endpoint}...", "tcpErrorHostRequired": "Se requiere la dirección IP.", "tcpErrorPortInvalid": "El puerto debe estar entre 1 y 65535.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 3c36a96..5d586f4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1881,7 +1881,6 @@ "tcpPortLabel": "Port", "tcpPortHint": "5000", "tcpStatus_notConnected": "Entrez l'adresse de destination et connectez-vous.", - "tcpStatus_connecting": "Connexion au point de terminaison TCP...", "tcpStatus_connectingTo": "Connexion à {endpoint}...", "tcpErrorHostRequired": "Une adresse IP est obligatoire.", "tcpErrorPortInvalid": "La taille du port doit être comprise entre 1 et 65535.", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 8a1c29d..4de9e9d 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1878,10 +1878,9 @@ "tcpHostHint": "192.168.40.10", "connectionChoiceTcpLabel": "TCP", "tcpScreenTitle": "Stabilire una connessione tramite TCP", - "tcpPortLabel": "Porto", + "tcpPortLabel": "Porta", "tcpPortHint": "5000", "tcpStatus_notConnected": "Inserisci l'endpoint e connettiti.", - "tcpStatus_connecting": "Connessione al punto finale TCP...", "tcpStatus_connectingTo": "Connessione a {endpoint}...", "tcpErrorHostRequired": "È necessario fornire un indirizzo IP.", "tcpErrorPortInvalid": "La dimensione della porta deve essere compresa tra 1 e 65535.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 36be955..3690bf2 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -376,12 +376,6 @@ abstract class AppLocalizations { /// **'Enter endpoint and connect'** String get tcpStatus_notConnected; - /// No description provided for @tcpStatus_connecting. - /// - /// In en, this message translates to: - /// **'Connecting to TCP endpoint...'** - String get tcpStatus_connecting; - /// No description provided for @tcpStatus_connectingTo. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index fd61be0..785f373 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -138,9 +138,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get tcpStatus_notConnected => 'Въведете крайната точка и свържете се.'; - @override - String get tcpStatus_connecting => 'Свързване към TCP крайния пункт...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Свързване към $endpoint...'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 0400237..708e0ab 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -130,7 +130,7 @@ class AppLocalizationsDe extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => 'Hafen'; + String get tcpPortLabel => 'Port'; @override String get tcpPortHint => '5000'; @@ -139,10 +139,6 @@ class AppLocalizationsDe extends AppLocalizations { String get tcpStatus_notConnected => 'Geben Sie den Endpunkt ein und verbinden Sie sich.'; - @override - String get tcpStatus_connecting => - 'Verbindung zum TCP-Endpunkt hergestellt...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Verbindung zu $endpoint...'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d01dc36..4938dd4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -138,9 +138,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tcpStatus_notConnected => 'Enter endpoint and connect'; - @override - String get tcpStatus_connecting => 'Connecting to TCP endpoint...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Connecting to $endpoint...'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 7e5f806..2d4e2fb 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -138,9 +138,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get tcpStatus_notConnected => 'Ingrese la dirección final y conecte.'; - @override - String get tcpStatus_connecting => 'Conectándose al punto final TCP...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Conectándose a $endpoint...'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c7a225b..28bbab3 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -139,9 +139,6 @@ class AppLocalizationsFr extends AppLocalizations { String get tcpStatus_notConnected => 'Entrez l\'adresse de destination et connectez-vous.'; - @override - String get tcpStatus_connecting => 'Connexion au point de terminaison TCP...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Connexion à $endpoint...'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 121c2f8..b510bc1 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -130,7 +130,7 @@ class AppLocalizationsIt extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => 'Porto'; + String get tcpPortLabel => 'Porta'; @override String get tcpPortHint => '5000'; @@ -138,9 +138,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get tcpStatus_notConnected => 'Inserisci l\'endpoint e connettiti.'; - @override - String get tcpStatus_connecting => 'Connessione al punto finale TCP...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Connessione a $endpoint...'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 48fe379..7c054dd 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -130,7 +130,7 @@ class AppLocalizationsNl extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => 'Haven'; + String get tcpPortLabel => 'Poort'; @override String get tcpPortHint => '5000'; @@ -138,9 +138,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get tcpStatus_notConnected => 'Voer het eindpunt in en verbind'; - @override - String get tcpStatus_connecting => 'Verbinding maken met TCP-eindpunt...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Verbinding maken met $endpoint...'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 97681be..dec6583 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -138,9 +138,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get tcpStatus_notConnected => 'Wprowadź adres URL i połącz'; - @override - String get tcpStatus_connecting => 'Połączenie z punktem TCP...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Połączenie z $endpoint...'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eb2fc7f..4d8d20e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -130,7 +130,7 @@ class AppLocalizationsPt extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => 'Porto'; + String get tcpPortLabel => 'Porta'; @override String get tcpPortHint => '5000'; @@ -138,10 +138,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get tcpStatus_notConnected => 'Insira o endereço final e conecte-se.'; - @override - String get tcpStatus_connecting => - 'Conectando ao ponto de extremidade TCP...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Conectando a $endpoint...'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 1a28e8d..60aa486 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -138,9 +138,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get tcpStatus_notConnected => 'Введите адрес и подключитесь.'; - @override - String get tcpStatus_connecting => 'Установление соединения с TCP-портом...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Подключение к $endpoint...'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index eb53c40..4e11719 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -130,17 +130,14 @@ class AppLocalizationsSk extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => 'Pri항'; + String get tcpPortLabel => 'Port'; @override - String get tcpPortHint => '5 000'; + String get tcpPortHint => '5000'; @override String get tcpStatus_notConnected => 'Zadajte cieľovú adresu a pripojte sa.'; - @override - String get tcpStatus_connecting => 'Pripojenie k TCP endpointu...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Pripojenie k $endpoint...'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index be87248..f967db4 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -130,7 +130,7 @@ class AppLocalizationsSl extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => 'Pril'; + String get tcpPortLabel => 'Vrata'; @override String get tcpPortHint => '5000'; @@ -138,9 +138,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get tcpStatus_notConnected => 'Vnesite končni naslov in se povežite'; - @override - String get tcpStatus_connecting => 'Povezava z TCP koncem...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Povezava z $endpoint...'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 55ac5c6..200bdbe 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -130,7 +130,7 @@ class AppLocalizationsSv extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => 'Hamn'; + String get tcpPortLabel => 'Port'; @override String get tcpPortHint => '5000'; @@ -138,9 +138,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get tcpStatus_notConnected => 'Ange slutpunkt och anslut'; - @override - String get tcpStatus_connecting => 'Anslutning till TCP-slutpunkt...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Anslutning till $endpoint...'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 8466adf..8dfe123 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -138,9 +138,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get tcpStatus_notConnected => 'Введіть кінцеву точку та підключіться'; - @override - String get tcpStatus_connecting => 'Підключення до TCP-кінцевої точки...'; - @override String tcpStatus_connectingTo(String endpoint) { return 'Підключення до $endpoint...'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 85026c2..ecd6813 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -130,7 +130,7 @@ class AppLocalizationsZh extends AppLocalizations { String get tcpHostHint => '192.168.40.10'; @override - String get tcpPortLabel => '港'; + String get tcpPortLabel => '端口'; @override String get tcpPortHint => '5000'; @@ -138,9 +138,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get tcpStatus_notConnected => '输入目标地址,然后连接'; - @override - String get tcpStatus_connecting => '连接到 TCP 终点...'; - @override String tcpStatus_connectingTo(String endpoint) { return '连接到 $endpoint...'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 51c045c..d38fb4c 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1878,10 +1878,9 @@ "tcpHostLabel": "IP-adres", "tcpHostHint": "192.168.40.10", "connectionChoiceTcpLabel": "TCP", - "tcpPortLabel": "Haven", + "tcpPortLabel": "Poort", "tcpPortHint": "5000", "tcpStatus_notConnected": "Voer het eindpunt in en verbind", - "tcpStatus_connecting": "Verbinding maken met TCP-eindpunt...", "tcpStatus_connectingTo": "Verbinding maken met {endpoint}...", "tcpErrorHostRequired": "Een IP-adres is vereist.", "tcpErrorPortInvalid": "De poortwaarde moet tussen 1 en 65535 liggen.", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index e711a99..9dc3b33 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1881,7 +1881,6 @@ "tcpPortLabel": "Port", "tcpPortHint": "5000", "tcpStatus_notConnected": "Wprowadź adres URL i połącz", - "tcpStatus_connecting": "Połączenie z punktem TCP...", "tcpStatus_connectingTo": "Połączenie z {endpoint}...", "tcpErrorHostRequired": "Wymagana jest adresa IP.", "tcpErrorPortInvalid": "Numer portu musi mieścić się w zakresie od 1 do 65535.", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6c1ad9e..cded31f 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1878,10 +1878,9 @@ "connectionChoiceTcpLabel": "TCP", "tcpScreenTitle": "Estabelecer conexão via TCP", "tcpHostHint": "192.168.40.10", - "tcpPortLabel": "Porto", + "tcpPortLabel": "Porta", "tcpPortHint": "5000", "tcpStatus_notConnected": "Insira o endereço final e conecte-se.", - "tcpStatus_connecting": "Conectando ao ponto de extremidade TCP...", "tcpStatus_connectingTo": "Conectando a {endpoint}...", "tcpErrorHostRequired": "É necessário fornecer um endereço IP.", "tcpErrorPortInvalid": "O valor do porto deve estar entre 1 e 65535.", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index d90c387..43e1b9a 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1121,7 +1121,6 @@ "tcpPortLabel": "Порт", "tcpPortHint": "5000", "tcpStatus_notConnected": "Введите адрес и подключитесь.", - "tcpStatus_connecting": "Установление соединения с TCP-портом...", "tcpStatus_connectingTo": "Подключение к {endpoint}...", "tcpErrorHostRequired": "Необходимо указать IP-адрес.", "tcpErrorPortInvalid": "Порт должен находиться в диапазоне от 1 до 65535.", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index ae55843..f03d276 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1878,10 +1878,9 @@ "tcpHostLabel": "IP adresa", "tcpScreenTitle": "Spojte sa pomocou protokolu TCP", "connectionChoiceTcpLabel": "TCP", - "tcpPortLabel": "Pri항", - "tcpPortHint": "5 000", + "tcpPortLabel": "Port", + "tcpPortHint": "5000", "tcpStatus_notConnected": "Zadajte cieľovú adresu a pripojte sa.", - "tcpStatus_connecting": "Pripojenie k TCP endpointu...", "tcpStatus_connectingTo": "Pripojenie k {endpoint}...", "tcpErrorHostRequired": "Je potrebné zadať IP adresu.", "tcpErrorPortInvalid": "Číslo portu musí byť medzi 1 a 65535.", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 6ad2449..4a4b5cb 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1878,10 +1878,9 @@ "tcpHostLabel": "IP naslov", "tcpHostHint": "192.168.40.10", "tcpScreenTitle": "Komunicirajte preko protokola TCP", - "tcpPortLabel": "Pril", + "tcpPortLabel": "Vrata", "tcpPortHint": "5000", "tcpStatus_notConnected": "Vnesite končni naslov in se povežite", - "tcpStatus_connecting": "Povezava z TCP koncem...", "tcpStatus_connectingTo": "Povezava z {endpoint}...", "tcpErrorHostRequired": "Potrebna je IP-naslov.", "tcpErrorPortInvalid": "Port mora biti med 1 in 65535.", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b70049f..6a33e11 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1878,10 +1878,9 @@ "tcpHostLabel": "IP-adress", "tcpScreenTitle": "Anslut via TCP", "connectionChoiceTcpLabel": "TCP", - "tcpPortLabel": "Hamn", + "tcpPortLabel": "Port", "tcpPortHint": "5000", "tcpStatus_notConnected": "Ange slutpunkt och anslut", - "tcpStatus_connecting": "Anslutning till TCP-slutpunkt...", "tcpStatus_connectingTo": "Anslutning till {endpoint}...", "tcpErrorHostRequired": "IP-adress krävs.", "tcpErrorPortInvalid": "Porten måste vara mellan 1 och 65535.", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 9dcf88f..c179ca3 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1881,7 +1881,6 @@ "tcpPortLabel": "Порт", "tcpPortHint": "5000", "tcpStatus_notConnected": "Введіть кінцеву точку та підключіться", - "tcpStatus_connecting": "Підключення до TCP-кінцевої точки...", "tcpStatus_connectingTo": "Підключення до {endpoint}...", "tcpErrorHostRequired": "Необхідно вказати IP-адресу.", "tcpErrorPortInvalid": "Порт повинен бути в межах від 1 до 65535.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 199f85c..cac4b79 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1883,10 +1883,9 @@ "tcpHostHint": "192.168.40.10", "tcpScreenTitle": "通过 TCP 连接", "connectionChoiceTcpLabel": "TCP", - "tcpPortLabel": "港", + "tcpPortLabel": "端口", "tcpPortHint": "5000", "tcpStatus_notConnected": "输入目标地址,然后连接", - "tcpStatus_connecting": "连接到 TCP 终点...", "tcpStatus_connectingTo": "连接到 {endpoint}...", "tcpErrorHostRequired": "需要提供IP地址。", "tcpErrorPortInvalid": "端口号必须在 1 到 65535 之间。", diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index 55bec20..cf87382 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -27,7 +27,7 @@ class _TcpScreenState extends State { @override void initState() { super.initState(); - _hostController = TextEditingController(text: '192.168.40.10'); + _hostController = TextEditingController(); _portController = TextEditingController(text: '5000'); _connector = context.read(); @@ -81,6 +81,9 @@ class _TcpScreenState extends State { final isConnecting = connector.state == MeshCoreConnectionState.connecting && connector.activeTransport == MeshCoreTransportType.tcp; + final isButtonDisabled = + isConnecting || + connector.state == MeshCoreConnectionState.scanning; return Column( children: [ _buildStatusBar(context, connector), @@ -112,7 +115,8 @@ class _TcpScreenState extends State { ), const SizedBox(height: 16), FilledButton.icon( - onPressed: isConnecting ? null : _connectTcp, + key: const Key('tcp_connect_button'), + onPressed: isButtonDisabled ? null : _connectTcp, icon: isConnecting ? const SizedBox( width: 18, @@ -153,6 +157,7 @@ class _TcpScreenState extends State { ); }, heroTag: 'tcp_usb_action', + extendedPadding: const EdgeInsets.symmetric(horizontal: 12), icon: const Icon(Icons.usb), label: Text(context.l10n.connectionChoiceUsbLabel), ), @@ -162,6 +167,7 @@ class _TcpScreenState extends State { Navigator.of(context).maybePop(); }, heroTag: 'tcp_ble_action', + extendedPadding: const EdgeInsets.symmetric(horizontal: 12), icon: const Icon(Icons.bluetooth), label: Text(context.l10n.connectionChoiceBluetoothLabel), ), diff --git a/test/screens/tcp_flow_test.dart b/test/screens/tcp_flow_test.dart index 5c240f4..725388a 100644 --- a/test/screens/tcp_flow_test.dart +++ b/test/screens/tcp_flow_test.dart @@ -93,7 +93,7 @@ void main() { final l10n = AppLocalizations.of(context); await tester.enterText(find.byType(TextField).first, ''); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.byKey(const Key('tcp_connect_button'))); await tester.pumpAndSettle(); expect(find.text(l10n.tcpErrorHostRequired), findsOneWidget); @@ -101,7 +101,7 @@ void main() { await tester.enterText(find.byType(TextField).first, '192.168.1.50'); await tester.enterText(find.byType(TextField).at(1), '99999'); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.tap(find.byKey(const Key('tcp_connect_button'))); await tester.pumpAndSettle(); expect(connector.connectTcpCalls, 0); @@ -135,7 +135,7 @@ void main() { await tester.pump(const Duration(milliseconds: 60)); }); - testWidgets('TcpScreen allows connect while connector is scanning', ( + testWidgets('TcpScreen disables connect button while connector is scanning', ( tester, ) async { final connector = _FakeMeshCoreConnector() @@ -150,12 +150,11 @@ void main() { ); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(FilledButton, 'Connect')); - await tester.pumpAndSettle(); - - expect(connector.connectTcpCalls, 1); - expect(connector.lastHost, '192.168.40.10'); - expect(connector.lastPort, 5000); + final button = tester.widget( + find.byKey(const Key('tcp_connect_button')), + ); + expect(button.onPressed, isNull); + expect(connector.connectTcpCalls, 0); }); testWidgets('TcpScreen narrow width long status text does not overflow', ( From 71f59d23df9fe83ebbc84f9a3a5929850484c41e Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 09:33:37 -0700 Subject: [PATCH 295/421] feat: add set-as-my-location from map long-press, connector and UI improvements Add "Set as my location" option to the map long-press bottom sheet, allowing users to set their device position directly from the map. Includes connector, chat, contacts, and message retry service improvements. --- lib/connector/meshcore_connector.dart | 92 +++++++++---- lib/l10n/app_bg.arb | 3 +- lib/l10n/app_de.arb | 3 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 3 +- lib/l10n/app_fr.arb | 3 +- lib/l10n/app_it.arb | 3 +- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_bg.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_sk.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_localizations_sv.dart | 3 + lib/l10n/app_localizations_uk.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_nl.arb | 3 +- lib/l10n/app_pl.arb | 3 +- lib/l10n/app_pt.arb | 3 +- lib/l10n/app_ru.arb | 3 +- lib/l10n/app_sk.arb | 3 +- lib/l10n/app_sl.arb | 3 +- lib/l10n/app_sv.arb | 3 +- lib/l10n/app_uk.arb | 3 +- lib/l10n/app_zh.arb | 3 +- lib/screens/chat_screen.dart | 104 ++++++++++---- lib/screens/contacts_screen.dart | 8 ++ lib/screens/map_screen.dart | 16 +++ lib/services/message_retry_service.dart | 173 +++++++++++++++++++----- lib/services/notification_service.dart | 55 ++++++++ 37 files changed, 434 insertions(+), 108 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d974b7b..7cf32ef 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -199,6 +199,9 @@ class MeshCoreConnector extends ChangeNotifier { int _queueSyncRetries = 0; static const int _maxQueueSyncRetries = 3; static const int _queueSyncTimeoutMs = 5000; // 5 second timeout + // Serializes path operations (setContactPath/clearContactPath) to prevent + // interleaved async calls from leaving in-memory state inconsistent with device. + Future _pathOpLock = Future.value(); Map? _currentCustomVars; // Channel syncing state (sequential pattern) @@ -558,6 +561,10 @@ class MeshCoreConnector extends ChangeNotifier { _unreadStore.saveContactUnreadCount( Map.from(_contactUnreadCount), ); + _notificationService.clearContactNotification( + contactKeyHex, + getTotalUnreadCount(), + ); notifyListeners(); } } @@ -576,6 +583,10 @@ class MeshCoreConnector extends ChangeNotifier { _channels.isNotEmpty ? _channels : _cachedChannels, ), ); + _notificationService.clearChannelNotification( + channelIndex, + getTotalUnreadCount(), + ); notifyListeners(); } } @@ -1740,18 +1751,33 @@ class MeshCoreConnector extends ChangeNotifier { Uint8List customPath, int pathLen, ) async { - if (!isConnected) return; + // Serialize path operations to prevent interleaved async calls from + // leaving in-memory state inconsistent with the device. + final prev = _pathOpLock; + final completer = Completer(); + _pathOpLock = completer.future; + await prev; + try { + if (!isConnected) return; - await sendFrame( - buildUpdateContactPathFrame( - contact.publicKey, - customPath, - pathLen, - type: contact.type, - flags: contact.flags, - name: contact.name, - ), - ); + await sendFrame( + buildUpdateContactPathFrame( + contact.publicKey, + customPath, + pathLen, + type: contact.type, + flags: contact.flags, + name: contact.name, + ), + ); + // USB writes return instantly (no BLE flow control), so give the firmware + // time to persist the path change before subsequent commands. + if (_activeTransport == MeshCoreTransportType.usb) { + await Future.delayed(const Duration(milliseconds: 100)); + } + } finally { + completer.complete(); + } } Future setContactFavorite(Contact contact, bool isFavorite) async { @@ -2136,25 +2162,34 @@ class MeshCoreConnector extends ChangeNotifier { } Future clearContactPath(Contact contact) async { - if (!isConnected) return; + // Serialize path operations to prevent interleaved async calls. + final prev = _pathOpLock; + final completer = Completer(); + _pathOpLock = completer.future; + await prev; + try { + if (!isConnected) return; - await sendFrame(buildResetPathFrame(contact.publicKey)); - final existingIndex = _contacts.indexWhere( - (c) => c.publicKeyHex == contact.publicKeyHex, - ); - if (existingIndex >= 0) { - final existing = _contacts[existingIndex]; - // Use copyWith to preserve pathOverride and pathOverrideBytes - _contacts[existingIndex] = existing.copyWith( - pathOverride: null, - pathOverrideBytes: null, - pathLength: -1, - path: Uint8List(0), + await sendFrame(buildResetPathFrame(contact.publicKey)); + if (_activeTransport == MeshCoreTransportType.usb) { + await Future.delayed(const Duration(milliseconds: 100)); + } + final existingIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, ); - notifyListeners(); - unawaited(_persistContacts()); + if (existingIndex >= 0) { + final existing = _contacts[existingIndex]; + // Preserve pathOverride and pathOverrideBytes — only reset device path + _contacts[existingIndex] = existing.copyWith( + pathLength: -1, + path: Uint8List(0), + ); + notifyListeners(); + unawaited(_persistContacts()); + } + } finally { + completer.complete(); } - // The device will send updated contact info with path_len = -1 } void updateContactInMemory( @@ -2490,6 +2525,9 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; + case pushCodeAdvert: + // Known contact was seen again - just a pub key, no action needed + break; case pushCodeNewAdvert: debugPrint('Got New CONTACT'); // It's the same format as respCodeContact, so we can reuse the handler diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c5acba9..07c6ae9 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "Транспортът чрез TCP не се поддържа на тази платформа.", "tcpErrorTimedOut": "Връзката TCP изтекла.", "tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}", - "map_showDiscoveryContacts": "Покажи контакти за откриване" + "map_showDiscoveryContacts": "Покажи контакти за откриване", + "map_setAsMyLocation": "Задайте като моя местоположение" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2eed922..0e3b5f4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1915,5 +1915,6 @@ "tcpErrorUnsupported": "Die TCP-Übertragung wird auf dieser Plattform nicht unterstützt.", "tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.", "tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}", - "map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen" + "map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen", + "map_setAsMyLocation": "Als meine aktuelle Position festlegen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 58569e4..712a164 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -807,6 +807,7 @@ "map_source": "Source", "map_flags": "Flags", "map_shareMarkerHere": "Share marker here", + "map_setAsMyLocation": "Set as my location", "map_pinLabel": "Pin label", "map_label": "Label", "map_pointOfInterest": "Point of interest", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 25e0345..53d7b70 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1915,5 +1915,6 @@ "tcpErrorUnsupported": "El protocolo de transporte TCP no está soportado en esta plataforma.", "tcpErrorTimedOut": "La conexión TCP ha caducado.", "tcpConnectionFailed": "Error en la conexión TCP: {error}", - "map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento" + "map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento", + "map_setAsMyLocation": "Establecer mi ubicación" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5d586f4..ef19d15 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "Le protocole TCP n'est pas pris en charge sur cette plateforme.", "tcpErrorTimedOut": "La connexion TCP a expiré.", "tcpConnectionFailed": "Échec de la connexion TCP : {error}", - "map_showDiscoveryContacts": "Afficher les contacts de découverte" + "map_showDiscoveryContacts": "Afficher les contacts de découverte", + "map_setAsMyLocation": "Définir comme ma localisation" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4de9e9d..a9f659b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "Il protocollo TCP non è supportato su questa piattaforma.", "tcpErrorTimedOut": "La connessione TCP è scaduta.", "tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}", - "map_showDiscoveryContacts": "Mostra Contatti di Discovery" + "map_showDiscoveryContacts": "Mostra Contatti di Discovery", + "map_setAsMyLocation": "Imposta come la mia posizione" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 3690bf2..fdc89d4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2746,6 +2746,12 @@ abstract class AppLocalizations { /// **'Share marker here'** String get map_shareMarkerHere; + /// No description provided for @map_setAsMyLocation. + /// + /// In en, this message translates to: + /// **'Set as my location'** + String get map_setAsMyLocation; + /// No description provided for @map_pinLabel. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 785f373..e17056c 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1511,6 +1511,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_shareMarkerHere => 'Споделете маркер тук'; + @override + String get map_setAsMyLocation => 'Задайте като моя местоположение'; + @override String get map_pinLabel => 'Етикетиране на пин'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 708e0ab..68317c8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1513,6 +1513,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_shareMarkerHere => 'Teilen Sie den Marker hier.'; + @override + String get map_setAsMyLocation => 'Als meine aktuelle Position festlegen'; + @override String get map_pinLabel => 'Pin Name'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4938dd4..95e8130 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1487,6 +1487,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_shareMarkerHere => 'Share marker here'; + @override + String get map_setAsMyLocation => 'Set as my location'; + @override String get map_pinLabel => 'Pin label'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 2d4e2fb..fad2737 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1509,6 +1509,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_shareMarkerHere => 'Compartir marcador aquí'; + @override + String get map_setAsMyLocation => 'Establecer mi ubicación'; + @override String get map_pinLabel => 'Etiqueta de marcador'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 28bbab3..b11b373 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1518,6 +1518,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_shareMarkerHere => 'Partager le marqueur ici'; + @override + String get map_setAsMyLocation => 'Définir comme ma localisation'; + @override String get map_pinLabel => 'Étiquete de repin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b510bc1..d1b2d95 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1510,6 +1510,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_shareMarkerHere => 'Condividi marcatore qui'; + @override + String get map_setAsMyLocation => 'Imposta come la mia posizione'; + @override String get map_pinLabel => 'Etichetta PIN'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 7c054dd..47e2341 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1502,6 +1502,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_shareMarkerHere => 'Deel marker hier'; + @override + String get map_setAsMyLocation => 'Stel dit in als mijn locatie'; + @override String get map_pinLabel => 'Label vastzetten'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index dec6583..a8abd13 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1512,6 +1512,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_shareMarkerHere => 'Udostępnij znacznik tutaj'; + @override + String get map_setAsMyLocation => 'Ustaw jako moje lokalizację'; + @override String get map_pinLabel => 'Oznacz etykietę'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 4d8d20e..54facda 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1511,6 +1511,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_shareMarkerHere => 'Compartilhar marcador aqui'; + @override + String get map_setAsMyLocation => 'Defina minha localização'; + @override String get map_pinLabel => 'Rótulo de marcador'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 60aa486..beb37de 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1513,6 +1513,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_shareMarkerHere => 'Поделиться меткой здесь'; + @override + String get map_setAsMyLocation => 'Установить мое местоположение'; + @override String get map_pinLabel => 'Метка'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 4e11719..c59014c 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1504,6 +1504,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_shareMarkerHere => 'Zdieľte značku tu'; + @override + String get map_setAsMyLocation => 'Nastavte ako moju polohu'; + @override String get map_pinLabel => 'Označka upozornenia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f967db4..4746550 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1498,6 +1498,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_shareMarkerHere => 'Delite točke tukaj.'; + @override + String get map_setAsMyLocation => 'Nastavite to kot mojo lokacijo'; + @override String get map_pinLabel => 'Oznaka za pritrditev'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 200bdbe..d86f994 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1494,6 +1494,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_shareMarkerHere => 'Dela markeringen här'; + @override + String get map_setAsMyLocation => 'Ange som min plats'; + @override String get map_pinLabel => 'Fästetikett'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 8dfe123..e40dd88 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1510,6 +1510,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_shareMarkerHere => 'Поділитися маркером тут'; + @override + String get map_setAsMyLocation => 'Встановити моє місцезнаходження'; + @override String get map_pinLabel => 'Мітка піна'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ecd6813..d5f4f1d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1421,6 +1421,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_shareMarkerHere => '在此分享标记'; + @override + String get map_setAsMyLocation => '设置为我的位置'; + @override String get map_pinLabel => '标签'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index d38fb4c..9512f18 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "TCP-transport wordt niet ondersteund op deze platform.", "tcpErrorTimedOut": "De TCP-verbinding is verlopen.", "tcpConnectionFailed": "Verbinding met TCP mislukt: {error}", - "map_showDiscoveryContacts": "Ontdek contacten weergeven" + "map_showDiscoveryContacts": "Ontdek contacten weergeven", + "map_setAsMyLocation": "Stel dit in als mijn locatie" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9dc3b33..88856aa 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.", "tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.", "tcpConnectionFailed": "Błąd połączenia TCP: {error}", - "map_showDiscoveryContacts": "Pokaż kontakty odkrywania" + "map_showDiscoveryContacts": "Pokaż kontakty odkrywania", + "map_setAsMyLocation": "Ustaw jako moje lokalizację" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index cded31f..3a5fe3b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "O protocolo TCP não é suportado nesta plataforma.", "tcpErrorTimedOut": "A conexão TCP expirou.", "tcpConnectionFailed": "Falha na conexão TCP: {error}", - "map_showDiscoveryContacts": "Mostrar Contatos de Descoberta" + "map_showDiscoveryContacts": "Mostrar Contatos de Descoberta", + "map_setAsMyLocation": "Defina minha localização" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 43e1b9a..8fec7fa 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1127,5 +1127,6 @@ "tcpErrorUnsupported": "Протокол TCP не поддерживается на этой платформе.", "tcpErrorTimedOut": "Соединение TCP не удалось установить.", "tcpConnectionFailed": "Не удалось установить соединение TCP: {error}", - "map_showDiscoveryContacts": "Показать контакты Discovery" + "map_showDiscoveryContacts": "Показать контакты Discovery", + "map_setAsMyLocation": "Установить мое местоположение" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index f03d276..374e9e4 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "Prevoz prostredníctvom protokolu TCP nie je na tejto platforme podporovaný.", "tcpErrorTimedOut": "Pripojenie TCP vypršalo.", "tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}", - "map_showDiscoveryContacts": "Zobraziť kontakty objavov" + "map_showDiscoveryContacts": "Zobraziť kontakty objavov", + "map_setAsMyLocation": "Nastavte ako moju polohu" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4a4b5cb..2436c78 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "Transport preko protokola TCP ni podprt na tej platformi.", "tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.", "tcpConnectionFailed": "Napaka pri povezavi TCP: {error}", - "map_showDiscoveryContacts": "Prikaži odkritja kontaktov" + "map_showDiscoveryContacts": "Prikaži odkritja kontaktov", + "map_setAsMyLocation": "Nastavite to kot mojo lokacijo" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 6a33e11..75231d0 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "TCP-transport fungerar inte på denna plattform.", "tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.", "tcpConnectionFailed": "Fel vid TCP-anslutning: {error}", - "map_showDiscoveryContacts": "Visa Discovery-kontakter" + "map_showDiscoveryContacts": "Visa Discovery-kontakter", + "map_setAsMyLocation": "Ange som min plats" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index c179ca3..35d7bd2 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1887,5 +1887,6 @@ "tcpErrorUnsupported": "Транспорт TCP не підтримується на цій платформі.", "tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.", "tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}", - "map_showDiscoveryContacts": "Показати контакти Відкриття" + "map_showDiscoveryContacts": "Показати контакти Відкриття", + "map_setAsMyLocation": "Встановити моє місцезнаходження" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index cac4b79..9e7c155 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1892,5 +1892,6 @@ "tcpErrorUnsupported": "此平台不支持 TCP 传输。", "tcpErrorTimedOut": "TCP 连接超时。", "tcpConnectionFailed": "TCP 连接失败:{error}", - "map_showDiscoveryContacts": "显示发现联系人" + "map_showDiscoveryContacts": "显示发现联系人", + "map_setAsMyLocation": "设置为我的位置" } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 0075040..96203ea 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -106,10 +106,9 @@ class _ChatScreenState extends State { final unreadLabel = context.l10n.chat_unread(unreadCount); final pathLabel = _currentPathLabel(contact); - // Show path details if we have path data (from device or override) - final hasPathData = - contact.path.isNotEmpty || contact.pathOverrideBytes != null; + // Show path details if we have non-empty path data (from device or override) final effectivePath = contact.pathOverrideBytes ?? contact.path; + final hasPathData = effectivePath.isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -143,12 +142,25 @@ class _ChatScreenState extends State { final contact = _resolveContact(connector); final isFloodMode = contact.pathOverride == -1; + final isDirectMode = contact.pathOverride == 0; + final activeMode = isFloodMode + ? 'flood' + : isDirectMode + ? 'direct' + : 'auto'; + return PopupMenuButton( icon: Icon(isFloodMode ? Icons.waves : Icons.route), tooltip: context.l10n.chat_routingMode, onSelected: (mode) async { if (mode == 'flood') { await connector.setPathOverride(contact, pathLen: -1); + } else if (mode == 'direct') { + await connector.setPathOverride( + contact, + pathLen: 0, + pathBytes: Uint8List(0), + ); } else { await connector.setPathOverride(contact, pathLen: null); } @@ -161,7 +173,7 @@ class _ChatScreenState extends State { Icon( Icons.auto_mode, size: 20, - color: !isFloodMode + color: activeMode == 'auto' ? Theme.of(context).primaryColor : null, ), @@ -169,7 +181,30 @@ class _ChatScreenState extends State { Text( context.l10n.chat_autoUseSavedPath, style: TextStyle( - fontWeight: !isFloodMode + fontWeight: activeMode == 'auto' + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'direct', + child: Row( + children: [ + Icon( + Icons.near_me, + size: 20, + color: activeMode == 'direct' + ? Theme.of(context).primaryColor + : null, + ), + const SizedBox(width: 8), + Text( + context.l10n.chat_direct, + style: TextStyle( + fontWeight: activeMode == 'direct' ? FontWeight.bold : FontWeight.normal, ), @@ -184,7 +219,7 @@ class _ChatScreenState extends State { Icon( Icons.waves, size: 20, - color: isFloodMode + color: activeMode == 'flood' ? Theme.of(context).primaryColor : null, ), @@ -192,7 +227,7 @@ class _ChatScreenState extends State { Text( context.l10n.chat_forceFloodMode, style: TextStyle( - fontWeight: isFloodMode + fontWeight: activeMode == 'flood' ? FontWeight.bold : FontWeight.normal, ), @@ -251,7 +286,9 @@ class _ChatScreenState extends State { ), const SizedBox(height: 8), Text( - context.l10n.chat_sendMessageTo(widget.contact.name), + context.l10n.chat_sendMessageTo( + _resolveContact(context.read()).name, + ), style: TextStyle(fontSize: 14, color: Colors.grey[500]), ), ], @@ -269,6 +306,7 @@ class _ChatScreenState extends State { // Auto-scroll to bottom if user is already at bottom WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; _scrollController.scrollToBottomIfAtBottom(); }); @@ -293,10 +331,10 @@ class _ChatScreenState extends State { ); } final messageIndex = index; - Contact contact = widget.contact; + Contact contact = _resolveContact(connector); final message = reversedMessages[messageIndex]; String fourByteHex = ''; - if (widget.contact.type == advTypeRoom) { + if (contact.type == advTypeRoom) { contact = _resolveContactFrom4Bytes( connector, message.fourByteRoomContactKey.isEmpty @@ -314,12 +352,13 @@ class _ChatScreenState extends State { final textScale = context.select( (service) => service.scale, ); + final resolvedContact = _resolveContact(connector); return _MessageBubble( message: message, - senderName: widget.contact.type == advTypeRoom + senderName: resolvedContact.type == advTypeRoom ? "${contact.name} [$fourByteHex]" : contact.name, - isRoomServer: widget.contact.type == advTypeRoom, + isRoomServer: resolvedContact.type == advTypeRoom, textScale: textScale, onTap: () => _openMessagePath(message, contact), onLongPress: () => _showMessageActions(message, contact), @@ -457,7 +496,7 @@ class _ChatScreenState extends State { return; } - connector.sendMessage(widget.contact, text); + connector.sendMessage(_resolveContact(connector), text); _textController.clear(); _textFieldFocusNode.requestFocus(); } @@ -654,7 +693,7 @@ class _ChatScreenState extends State { // Set the path override to persist user's choice await connector.setPathOverride( - widget.contact, + _resolveContact(connector), pathLen: pathLength, pathBytes: pathBytes, ); @@ -663,7 +702,7 @@ class _ChatScreenState extends State { Navigator.pop(context); await _notifyPathSet( connector, - widget.contact, + _resolveContact(connector), pathBytes, path.hopCount, ); @@ -722,7 +761,9 @@ class _ChatScreenState extends State { style: const TextStyle(fontSize: 11), ), onTap: () async { - await connector.clearContactPath(widget.contact); + await connector.clearContactPath( + _resolveContact(connector), + ); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -750,7 +791,7 @@ class _ChatScreenState extends State { ), onTap: () async { await connector.setPathOverride( - widget.contact, + _resolveContact(connector), pathLen: -1, ); if (!context.mounted) return; @@ -1005,11 +1046,7 @@ class _ChatScreenState extends State { ); if (result == null) { - appLogger.info( - 'PathSelectionDialog was cancelled or returned null', - tag: 'ChatScreen', - ); - return; + return; // Cancelled — keep existing path } if (!mounted) { @@ -1025,14 +1062,19 @@ class _ChatScreenState extends State { tag: 'ChatScreen', ); await connector.setPathOverride( - widget.contact, + _resolveContact(connector), pathLen: result.length, pathBytes: result, ); appLogger.info('setPathOverride completed', tag: 'ChatScreen'); if (!mounted) return; - await _notifyPathSet(connector, widget.contact, result, result.length); + await _notifyPathSet( + connector, + _resolveContact(connector), + result, + result.length, + ); } void _openMessagePath(Message message, Contact contact) { @@ -1044,10 +1086,10 @@ class _ChatScreenState extends State { final String senderName; if (message.isOutgoing) { senderName = connector.selfName ?? context.l10n.chat_me; - } else if (widget.contact.type == advTypeRoom) { + } else if (_resolveContact(connector).type == advTypeRoom) { senderName = "${contact.name} [$fourByteHex]"; } else { - senderName = widget.contact.name; + senderName = _resolveContact(connector).name; } final pathMessage = ChannelMessage( senderKey: null, @@ -1110,7 +1152,8 @@ class _ChatScreenState extends State { _retryMessage(message); }, ), - if (widget.contact.type == advTypeRoom) + if (_resolveContact(context.read()).type == + advTypeRoom) ListTile( leading: const Icon(Icons.chat), title: Text(context.l10n.contacts_openChat), @@ -1148,7 +1191,7 @@ class _ChatScreenState extends State { void _retryMessage(Message message) { final connector = Provider.of(context, listen: false); // Retry using the contact's current path override setting - connector.sendMessage(widget.contact, message.text); + connector.sendMessage(_resolveContact(connector), message.text); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(context.l10n.chat_retryingMessage))); @@ -1174,7 +1217,8 @@ class _ChatScreenState extends State { // For room servers, include sender name (like channels) since multiple users // For 1:1 chats, sender is implicit (null) - final senderName = widget.contact.type == advTypeRoom + final liveContact = _resolveContact(connector); + final senderName = liveContact.type == advTypeRoom ? senderContact.name : null; final hash = ReactionHelper.computeReactionHash( @@ -1183,7 +1227,7 @@ class _ChatScreenState extends State { message.text, ); final reactionText = 'r:$hash:$emojiIndex'; - connector.sendMessage(widget.contact, reactionText); + connector.sendMessage(_resolveContact(connector), reactionText); } } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 3fef9ec..243c8c4 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; +import 'package:meshcore_open/services/notification_service.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; @@ -64,6 +65,13 @@ class _ContactsScreenState extends State super.initState(); _loadGroups(); _setupFrameListener(); + _clearAdvertNotifications(); + } + + void _clearAdvertNotifications() { + final connector = context.read(); + final contactIds = connector.contacts.map((c) => c.publicKeyHex).toList(); + NotificationService().clearAdvertNotifications(contactIds); } @override diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 7ffec56..402a4ee 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1509,6 +1509,22 @@ class _MapScreenState extends State { ); }, ), + ListTile( + leading: const Icon(Icons.my_location), + title: Text(context.l10n.map_setAsMyLocation), + onTap: () async { + final messenger = ScaffoldMessenger.of(context); + final message = context.l10n.settings_locationUpdated; + Navigator.pop(sheetContext); + await connector.setNodeLocation( + lat: position.latitude, + lon: position.longitude, + ); + await connector.refreshDeviceInfo(); + if (!mounted) return; + messenger.showSnackBar(SnackBar(content: Text(message))); + }, + ), ListTile( leading: const Icon(Icons.close), title: Text(context.l10n.common_cancel), diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 694a616..df81d16 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -44,6 +44,12 @@ class MessageRetryService extends ChangeNotifier { []; // Rolling buffer of recent ACK hashes final Map> _pendingMessageQueuePerContact = {}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed) + final Map> _sendQueue = + {}; // contactPubKeyHex → ordered list of messageIds awaiting send + final Set _activeMessages = + {}; // messageIds currently in-flight (sent/retrying) + final Set _resolvedMessages = + {}; // messageIds already resolved (prevents double _onMessageResolved) final Map _expectedHashToMessageId = {}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash) @@ -156,7 +162,40 @@ class MessageRetryService extends ChangeNotifier { _addMessageCallback!(contact.publicKeyHex, message); } - await _attemptSend(messageId); + // Queue per contact — only one message in-flight at a time to avoid + // overflowing the firmware's 8-entry expected_ack_table. + final contactKey = contact.publicKeyHex; + _sendQueue[contactKey] ??= []; + _sendQueue[contactKey]!.add(messageId); + + if (!_activeMessages.any( + (id) => _pendingContacts[id]?.publicKeyHex == contactKey, + )) { + _sendNextForContact(contactKey); + } + } + + void _sendNextForContact(String contactKey) { + final queue = _sendQueue[contactKey]; + if (queue == null) return; + + // Drain stale entries iteratively instead of recursing. + while (queue.isNotEmpty) { + final messageId = queue.removeAt(0); + if (_pendingMessages.containsKey(messageId)) { + _activeMessages.add(messageId); + _attemptSend(messageId); + return; + } + // Message was cancelled/cleaned up while queued — try next + } + } + + void _onMessageResolved(String messageId, String contactKey) { + if (_resolvedMessages.contains(messageId)) return; + _resolvedMessages.add(messageId); + _activeMessages.remove(messageId); + _sendNextForContact(contactKey); } Future _attemptSend(String messageId) async { @@ -169,13 +208,11 @@ class MessageRetryService extends ChangeNotifier { // Use the path that was captured when the message was first sent if (_setContactPathCallback != null && _clearContactPathCallback != null) { if (message.pathLength != null && message.pathLength! < 0) { - // Flood mode - clear the path debugPrint( 'Setting flood mode for retry attempt ${message.retryCount}', ); - _clearContactPathCallback!(contact); + await _clearContactPathCallback!(contact); } else if (message.pathLength != null && message.pathLength! >= 0) { - // Specific path (including direct neighbor with pathLength=0) final pathStr = message.pathBytes.isEmpty ? 'direct' : message.pathBytes @@ -192,6 +229,24 @@ class MessageRetryService extends ChangeNotifier { } } + // Re-validate after async gap — a timer or ACK could have resolved/retried + // this message while we were awaiting the path callback. + final currentMessage = _pendingMessages[messageId]; + if (currentMessage == null || _resolvedMessages.contains(messageId)) { + debugPrint( + '_attemptSend: message $messageId resolved during path sync, aborting', + ); + return; + } + // If the message was retried by a timer during our await, the retryCount + // will have advanced. Only proceed if it still matches the attempt we started. + if (currentMessage.retryCount != message.retryCount) { + debugPrint( + '_attemptSend: message $messageId retryCount changed during path sync, aborting', + ); + return; + } + final attempt = message.retryCount.clamp(0, 3); final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000; @@ -231,6 +286,15 @@ class MessageRetryService extends ChangeNotifier { if (_sendMessageCallback != null) { _sendMessageCallback!(contact, message.text, attempt, timestampSeconds); + } else { + // No send callback — message would be stuck forever. Fail it immediately. + debugPrint( + '_attemptSend: no sendMessageCallback, failing message $messageId', + ); + final failedMessage = message.copyWith(status: MessageStatus.failed); + _pendingMessages[messageId] = failedMessage; + _updateMessageCallback?.call(failedMessage); + _onMessageResolved(messageId, contact.publicKeyHex); } } @@ -281,6 +345,7 @@ class MessageRetryService extends ChangeNotifier { } // FALLBACK: Old queue-based matching (for messages sent before hash computation was added) + // Only match within a single contact's queue to avoid cross-contact mismatches. if (messageId == null && allowQueueFallback) { _debugLogService?.warn( 'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue', @@ -290,13 +355,28 @@ class MessageRetryService extends ChangeNotifier { 'Hash-based match failed for $ackHashHex, falling back to queue-based matching', ); - for (var entry in _pendingMessageQueuePerContact.entries) { + // Try to identify the correct contact from _activeMessages first. + String? targetContactKey; + for (final activeId in _activeMessages) { + final activeContact = _pendingContacts[activeId]; + if (activeContact != null) { + targetContactKey = activeContact.publicKeyHex; + break; + } + } + + final queuesToSearch = targetContactKey != null + ? {targetContactKey: _pendingMessageQueuePerContact[targetContactKey]} + : _pendingMessageQueuePerContact; + + for (var entry in queuesToSearch.entries) { final contactKey = entry.key; final queue = entry.value; + if (queue == null) continue; - if (queue.isNotEmpty) { + // Drain stale entries until we find a valid one or exhaust the queue. + while (queue.isNotEmpty) { final candidateMessageId = queue.removeAt(0); - if (_pendingMessages.containsKey(candidateMessageId)) { messageId = candidateMessageId; contact = _pendingContacts[candidateMessageId]; @@ -304,21 +384,10 @@ class MessageRetryService extends ChangeNotifier { 'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey', ); break; - } else { - debugPrint('Dequeued stale message $candidateMessageId - skipping'); - if (queue.isNotEmpty) { - final nextMessageId = queue.removeAt(0); - if (_pendingMessages.containsKey(nextMessageId)) { - messageId = nextMessageId; - contact = _pendingContacts[nextMessageId]; - debugPrint( - 'Queue-based match (fallback): $ackHashHex → message $messageId', - ); - break; - } - } } + debugPrint('Dequeued stale message $candidateMessageId - skipping'); } + if (messageId != null) break; } } @@ -463,22 +532,7 @@ class MessageRetryService extends ChangeNotifier { } else { // Max retries reached - mark as failed final failedMessage = message.copyWith(status: MessageStatus.failed); - - // Move ACK hashes to history before removing - _moveAckHashesToHistory(messageId); - - _pendingMessages.remove(messageId); - _pendingContacts.remove(messageId); - _pendingPathSelections.remove(messageId); - _timeoutTimers[messageId]?.cancel(); - _timeoutTimers.remove(messageId); - - // Clean up the queue entry for this contact - _pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId); - if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? - false) { - _pendingMessageQueuePerContact.remove(contact.publicKeyHex); - } + _pendingMessages[messageId] = failedMessage; // Check if we should clear the path on max retry if (_appSettingsService?.settings.clearPathOnMaxRetry == true && @@ -499,6 +553,30 @@ class MessageRetryService extends ChangeNotifier { } notifyListeners(); + + // Message is done retrying — send next queued message for this contact + _onMessageResolved(messageId, contact.publicKeyHex); + + // Keep message in pending maps for 30s grace period so late ACKs + // can still match and update the message to delivered. + _timeoutTimers[messageId] = Timer(const Duration(seconds: 30), () { + _moveAckHashesToHistory(messageId); + // Clean up ALL hash mappings for this message + _ackHashToMessageId.removeWhere( + (_, mapping) => mapping.messageId == messageId, + ); + _expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId); + _pendingMessages.remove(messageId); + _pendingContacts.remove(messageId); + _pendingPathSelections.remove(messageId); + _timeoutTimers.remove(messageId); + _resolvedMessages.remove(messageId); + final contactKey = contact.publicKeyHex; + _pendingMessageQueuePerContact[contactKey]?.remove(messageId); + if (_pendingMessageQueuePerContact[contactKey]?.isEmpty ?? false) { + _pendingMessageQueuePerContact.remove(contactKey); + } + }); } } @@ -594,7 +672,15 @@ class MessageRetryService extends ChangeNotifier { } if (matchedMessageId != null) { - final message = _pendingMessages[matchedMessageId]!; + final message = _pendingMessages[matchedMessageId]; + if (message == null) { + // Message was already cleaned up (e.g. grace period expired) + _ackHashToMessageId.remove(ackHashHex); + debugPrint( + 'ACK matched $matchedMessageId but message already cleaned up', + ); + return; + } final contact = _pendingContacts[matchedMessageId]; final selection = _pendingPathSelections[matchedMessageId]; @@ -616,12 +702,21 @@ class MessageRetryService extends ChangeNotifier { tripTimeMs: tripTimeMs, ); + // Clean up ALL hash mappings for this message (from all retry attempts) + _ackHashToMessageId.removeWhere( + (_, mapping) => mapping.messageId == matchedMessageId, + ); + _expectedHashToMessageId.removeWhere( + (_, msgId) => msgId == matchedMessageId, + ); + // Move ACK hashes to history before removing _moveAckHashesToHistory(matchedMessageId); _pendingMessages.remove(matchedMessageId); _pendingContacts.remove(matchedMessageId); _pendingPathSelections.remove(matchedMessageId); + _resolvedMessages.remove(matchedMessageId); // Clean up the queue entry for this contact (remove any remaining references to this message) if (contact != null) { @@ -646,6 +741,7 @@ class MessageRetryService extends ChangeNotifier { true, tripTimeMs, ); + _onMessageResolved(matchedMessageId, contact.publicKeyHex); } notifyListeners(); @@ -783,6 +879,9 @@ class MessageRetryService extends ChangeNotifier { _ackHistory.clear(); _ackHashToMessageId.clear(); _pendingMessageQueuePerContact.clear(); + _sendQueue.clear(); + _activeMessages.clear(); + _resolvedMessages.clear(); super.dispose(); } } diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 95326d2..4ed59d0 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -331,6 +331,61 @@ class NotificationService { await _notifications.cancel(id: id); } + /// Cancel the notification for a specific contact and update the app badge. + Future clearContactNotification( + String contactId, + int totalUnreadCount, + ) async { + if (!await _ensureInitialized()) return; + await _notifications.cancel(id: contactId.hashCode); + await _updateBadge(totalUnreadCount); + } + + /// Cancel the notification for a specific channel and update the app badge. + Future clearChannelNotification( + int channelIndex, + int totalUnreadCount, + ) async { + if (!await _ensureInitialized()) return; + await _notifications.cancel(id: channelIndex.hashCode); + await _updateBadge(totalUnreadCount); + } + + /// Cancel advert notifications for the given contact public key hexes. + Future clearAdvertNotifications(List contactIds) async { + if (!await _ensureInitialized()) return; + for (final id in contactIds) { + await _notifications.cancel(id: id.hashCode); + } + } + + Future _updateBadge(int count) async { + if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { + // On Apple platforms, set the badge number directly via a silent update. + final darwinDetails = DarwinNotificationDetails( + presentAlert: false, + presentSound: false, + presentBadge: true, + badgeNumber: count, + ); + final details = NotificationDetails( + iOS: darwinDetails, + macOS: darwinDetails, + ); + // Use a fixed ID so each update replaces the previous one. + await _notifications.show( + id: 'badge_update'.hashCode, + title: null, + body: null, + notificationDetails: details, + ); + // Immediately cancel the silent notification so it doesn't appear in tray. + await _notifications.cancel(id: 'badge_update'.hashCode); + } + // On Android, badge count is derived from active notifications, + // so cancelling the specific notification above is sufficient. + } + // ───────────────────────────────────────────────────────────────── // Public notification methods (rate limiting is enforced automatically) // ───────────────────────────────────────────────────────────────── From 91608ff09e248fd96398cd040e90893ecb80c91c Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 09:44:37 -0700 Subject: [PATCH 296/421] feat: improve message matching logic and update notification IDs for advertisements --- .swift-version | 1 + lib/services/message_retry_service.dart | 16 ++-------------- lib/services/notification_service.dart | 6 ++++-- 3 files changed, 7 insertions(+), 16 deletions(-) create mode 100644 .swift-version diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..31b44b0 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +6.2.4 \ No newline at end of file diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index df81d16..56d7b82 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -355,24 +355,12 @@ class MessageRetryService extends ChangeNotifier { 'Hash-based match failed for $ackHashHex, falling back to queue-based matching', ); - // Try to identify the correct contact from _activeMessages first. - String? targetContactKey; - for (final activeId in _activeMessages) { - final activeContact = _pendingContacts[activeId]; - if (activeContact != null) { - targetContactKey = activeContact.publicKeyHex; - break; - } - } - - final queuesToSearch = targetContactKey != null - ? {targetContactKey: _pendingMessageQueuePerContact[targetContactKey]} - : _pendingMessageQueuePerContact; + // Search all contact queues so concurrent chats don't miss matches. + final queuesToSearch = _pendingMessageQueuePerContact; for (var entry in queuesToSearch.entries) { final contactKey = entry.key; final queue = entry.value; - if (queue == null) continue; // Drain stale entries until we find a valid one or exhaust the queue. while (queue.isNotEmpty) { diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 4ed59d0..62d3796 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -232,7 +232,9 @@ class NotificationService { try { await _notifications.show( - id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + id: contactId != null + ? 'advert:$contactId'.hashCode + : DateTime.now().millisecondsSinceEpoch, title: _l10n.notification_newTypeDiscovered(contactType), body: contactName, notificationDetails: notificationDetails, @@ -355,7 +357,7 @@ class NotificationService { Future clearAdvertNotifications(List contactIds) async { if (!await _ensureInitialized()) return; for (final id in contactIds) { - await _notifications.cancel(id: id.hashCode); + await _notifications.cancel(id: 'advert:$id'.hashCode); } } From fa4da979af2c7ad4cead7b06d7b8d32528e838b6 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 09:54:50 -0700 Subject: [PATCH 297/421] feat: enhance location update feedback and improve message retry error handling --- lib/screens/map_screen.dart | 5 +++-- lib/services/message_retry_service.dart | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 402a4ee..1dd3a5f 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1514,15 +1514,16 @@ class _MapScreenState extends State { title: Text(context.l10n.map_setAsMyLocation), onTap: () async { final messenger = ScaffoldMessenger.of(context); - final message = context.l10n.settings_locationUpdated; + final successMsg = context.l10n.settings_locationUpdated; Navigator.pop(sheetContext); + if (!connector.isConnected) return; await connector.setNodeLocation( lat: position.latitude, lon: position.longitude, ); await connector.refreshDeviceInfo(); if (!mounted) return; - messenger.showSnackBar(SnackBar(content: Text(message))); + messenger.showSnackBar(SnackBar(content: Text(successMsg))); }, ), ListTile( diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 56d7b82..db4475f 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -184,7 +184,16 @@ class MessageRetryService extends ChangeNotifier { final messageId = queue.removeAt(0); if (_pendingMessages.containsKey(messageId)) { _activeMessages.add(messageId); - _attemptSend(messageId); + _attemptSend(messageId).catchError((e) { + debugPrint('_attemptSend threw for $messageId: $e'); + final msg = _pendingMessages[messageId]; + if (msg != null) { + final failed = msg.copyWith(status: MessageStatus.failed); + _pendingMessages[messageId] = failed; + _updateMessageCallback?.call(failed); + } + _onMessageResolved(messageId, contactKey); + }); return; } // Message was cancelled/cleaned up while queued — try next From 79a45c527b7293d393d37160049c17ff78b56d03 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Mar 2026 11:45:47 -0700 Subject: [PATCH 298/421] Unify contact retrieval by introducing allContacts getter --- lib/connector/meshcore_connector.dart | 17 +++++++++++++++-- lib/screens/channel_message_path_screen.dart | 10 ++-------- lib/screens/chat_screen.dart | 2 +- lib/screens/map_screen.dart | 5 +---- lib/screens/neighbors_screen.dart | 5 +---- lib/screens/path_trace_map.dart | 5 +---- lib/widgets/path_management_dialog.dart | 2 +- lib/widgets/path_selection_dialog.dart | 3 ++- lib/widgets/snr_indicator.dart | 5 +---- 9 files changed, 25 insertions(+), 29 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7cf32ef..dad5ed1 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -289,6 +289,10 @@ class MeshCoreConnector extends ChangeNotifier { ); } + List get allContacts => List.unmodifiable([ + ..._contacts, + ..._discoveredContacts.where((c) => !c.isActive), + ]); List get discoveredContacts { return List.unmodifiable(_discoveredContacts); } @@ -2909,6 +2913,8 @@ class MeshCoreConnector extends ChangeNotifier { void _handleContact(Uint8List frame, {bool isContact = true}) { final contact = Contact.fromFrame(frame); if (contact != null) { + _handleDiscovery(contact, frame, noNotify: true, addActive: true); + if (contact.type == advTypeRepeater) { _contactUnreadCount.remove(contact.publicKeyHex); _unreadStore.saveContactUnreadCount( @@ -4717,6 +4723,12 @@ class MeshCoreConnector extends ChangeNotifier { (_autoAddRoomServers && type == advTypeRoom) || (_autoAddSensors && type == advTypeSensor)) { _handleContactAdvert(newContact); + _handleDiscovery( + newContact, + rawPacket, + noNotify: true, + addActive: true, + ); } else { _handleDiscovery(newContact, rawPacket); } @@ -4827,6 +4839,7 @@ class MeshCoreConnector extends ChangeNotifier { Contact contact, Uint8List rawPacket, { bool noNotify = false, + bool addActive = false, }) { appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector'); @@ -4847,7 +4860,7 @@ class MeshCoreConnector extends ChangeNotifier { longitude: contact.longitude, lastSeen: contact.lastSeen, flags: 0, - isActive: false, + isActive: addActive, ); notifyListeners(); unawaited(_persistDiscoveredContacts()); @@ -4865,7 +4878,7 @@ class MeshCoreConnector extends ChangeNotifier { longitude: contact.longitude, lastSeen: contact.lastSeen, lastMessageAt: contact.lastMessageAt, - isActive: false, + isActive: addActive, flags: 0, ); _discoveredContacts.add(disContact); diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index c2c57f0..32eadef 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -40,10 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget { final primaryPath = !channelMessage && !message.isOutgoing ? Uint8List.fromList(primaryPathTmp.reversed.toList()) : primaryPathTmp; - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; final hops = _buildPathHops(primaryPath, contacts, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( @@ -367,10 +364,7 @@ class _ChannelMessagePathMapScreenState : selectedPathTmp; final selectedIndex = _indexForPath(selectedPath, observedPaths); - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; final hops = _buildPathHops(selectedPath, contacts, context.l10n); final points = []; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 96203ea..6558ecd 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1027,7 +1027,7 @@ class _ChatScreenState extends State { final currentPathLabel = _currentPathLabel(currentContact); // Filter out the current contact from available contacts - final availableContacts = connector.contacts + final availableContacts = connector.allContacts .where((c) => c != widget.contact) .toList(); diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 1dd3a5f..497c05f 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -137,10 +137,7 @@ class _MapScreenState extends State { builder: (context, connector, settingsService, pathHistory, child) { final tileCache = context.read(); final settings = settingsService.settings; - final allContacts = [ - ...connector.contacts, - ...connector.discoveredContacts.where((c) => !c.isActive), - ]; + final allContacts = connector.allContacts; final contacts = settings.mapShowDiscoveryContacts ? allContacts diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart index 5cb8e45..5afeda4 100644 --- a/lib/screens/neighbors_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -124,10 +124,7 @@ class _NeighborsScreenState extends State { void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; try { final neighborCount = buffer.readUInt16LE(); final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index ceb60a6..6277886 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -263,10 +263,7 @@ class _PathTraceMapScreenState extends State { .toList(); Map pathContacts = {}; - final contacts = [ - ...connector.contacts, - ...connector.discoveredContacts, - ]; + final contacts = connector.allContacts; contacts.where((c) => c.type != advTypeChat).forEach((repeater) { for (var repeaterData in pathData) { if (listEquals( diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 384f92b..0233b43 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -107,7 +107,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { } final pathForInput = currentContact.pathIdList; - final availableContacts = connector.contacts + final availableContacts = connector.allContacts .where((c) => c.publicKeyHex != currentContact.publicKeyHex) .toList(); diff --git a/lib/widgets/path_selection_dialog.dart b/lib/widgets/path_selection_dialog.dart index 4e6cfe5..b1733fc 100644 --- a/lib/widgets/path_selection_dialog.dart +++ b/lib/widgets/path_selection_dialog.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; @@ -65,7 +66,7 @@ class _PathSelectionDialogState extends State { void _filterValidContacts() { _validContacts = widget.availableContacts - .where((c) => c.type == 2 || c.type == 3) + .where((c) => c.type == advTypeRepeater || c.type == advTypeRoom) .toList(); } diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index f122836..30956e2 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -157,10 +157,7 @@ class _SNRIndicatorState extends State { repeater.snr, widget.connector.currentSf, ); - final allContacts = [ - ...widget.connector.contacts, - ...widget.connector.discoveredContacts, - ]; + final allContacts = widget.connector.allContacts; final name = allContacts .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) .map((c) => c.name) From 24fa78741b2fb8094be33dbabea974e801f2eee9 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Mar 2026 11:46:05 -0700 Subject: [PATCH 299/421] add TCP server address and port settings to AppSettings and update TcpScreen --- lib/models/app_settings.dart | 12 ++++++++++++ lib/screens/tcp_screen.dart | 18 ++++++++++++++++-- lib/services/app_settings_service.dart | 8 ++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index c89ac27..fc84851 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -40,6 +40,8 @@ class AppSettings { final UnitSystem unitSystem; final Set mutedChannels; final bool mapShowDiscoveryContacts; + final String tcpServerAddress; + final int tcpServerPort; AppSettings({ this.clearPathOnMaxRetry = false, @@ -68,6 +70,8 @@ class AppSettings { this.unitSystem = UnitSystem.metric, Set? mutedChannels, this.mapShowDiscoveryContacts = true, + this.tcpServerAddress = '', + this.tcpServerPort = 0, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, mutedChannels = mutedChannels ?? {}; @@ -100,6 +104,8 @@ class AppSettings { 'unit_system': unitSystem.value, 'muted_channels': mutedChannels.toList(), 'map_show_discovery_contacts': mapShowDiscoveryContacts, + 'tcp_server_address': tcpServerAddress, + 'tcp_server_port': tcpServerPort, }; } @@ -157,6 +163,8 @@ class AppSettings { {}, mapShowDiscoveryContacts: json['map_show_discovery_contacts'] as bool? ?? true, + tcpServerAddress: json['tcp_server_address'] as String? ?? '', + tcpServerPort: json['tcp_server_port'] as int? ?? 0, ); } @@ -187,6 +195,8 @@ class AppSettings { UnitSystem? unitSystem, Set? mutedChannels, bool? mapShowDiscoveryContacts, + String? tcpServerAddress, + int? tcpServerPort, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -225,6 +235,8 @@ class AppSettings { mutedChannels: mutedChannels ?? this.mutedChannels, mapShowDiscoveryContacts: mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts, + tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress, + tcpServerPort: tcpServerPort ?? this.tcpServerPort, ); } } diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index cf87382..02b9b5a 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/models/app_settings.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../services/app_settings_service.dart'; import '../utils/platform_info.dart'; import '../widgets/adaptive_app_bar_title.dart'; import 'contacts_screen.dart'; @@ -27,8 +29,14 @@ class _TcpScreenState extends State { @override void initState() { super.initState(); - _hostController = TextEditingController(); - _portController = TextEditingController(text: '5000'); + _hostController = TextEditingController( + text: context.read().settings.tcpServerAddress, + ); + _portController = TextEditingController( + text: context.read().settings.tcpServerPort > 0 + ? context.read().settings.tcpServerPort.toString() + : '', + ); _connector = context.read(); _connectionListener = () { @@ -39,6 +47,12 @@ class _TcpScreenState extends State { if (_connector.state == MeshCoreConnectionState.connected && _connector.isTcpTransportConnected && !_navigatedToContacts) { + context.read().setTcpServerAddress( + _hostController.text, + ); + context.read().setTcpServerPort( + int.tryParse(_portController.text) ?? 0, + ); _navigatedToContacts = true; Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const ContactsScreen()), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index a52e364..88c1f81 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -182,4 +182,12 @@ class AppSettingsService extends ChangeNotifier { ..remove(channelName); await updateSettings(_settings.copyWith(mutedChannels: updated)); } + + Future setTcpServerAddress(String value) async { + await updateSettings(_settings.copyWith(tcpServerAddress: value)); + } + + Future setTcpServerPort(int value) async { + await updateSettings(_settings.copyWith(tcpServerPort: value)); + } } From 86e9b7fe0135e2132c138d39b1d4ecb56d71b013 Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 15 Mar 2026 00:34:09 +0100 Subject: [PATCH 300/421] squashed commit of ez_group_dropdown --- lib/l10n/app_bg.arb | 1 + lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 1 + lib/l10n/app_fr.arb | 1 + lib/l10n/app_it.arb | 1 + lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_bg.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 4 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_sk.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_localizations_sv.dart | 3 + lib/l10n/app_localizations_uk.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/l10n/app_nl.arb | 1 + lib/l10n/app_pl.arb | 1 + lib/l10n/app_pt.arb | 1 + lib/l10n/app_ru.arb | 1 + lib/l10n/app_sk.arb | 1 + lib/l10n/app_sl.arb | 1 + lib/l10n/app_sv.arb | 1 + lib/l10n/app_uk.arb | 1 + lib/l10n/app_zh.arb | 1 + lib/main.dart | 7 + lib/screens/channels_screen.dart | 99 ++- lib/screens/contacts_screen.dart | 730 +++++++++++------- lib/services/chat_text_scale_service.dart | 2 +- lib/services/ui_view_state_service.dart | 154 ++++ lib/utils/contact_search.dart | 4 + lib/widgets/list_filter_widget.dart | 39 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + 39 files changed, 743 insertions(+), 361 deletions(-) create mode 100644 lib/services/ui_view_state_service.dart diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c5acba9..ba97771 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Нова група", "contacts_groupName": "Група", "contacts_groupNameRequired": "Името на групата е задължително.", + "contacts_groupNameReserved": "Това име на група е запазено", "contacts_groupAlreadyExists": "Групата \"{name}\" вече съществува.", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2eed922..3b623ba 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Neue Gruppe", "contacts_groupName": "Gruppenname", "contacts_groupNameRequired": "Der Gruppennamen ist erforderlich.", + "contacts_groupNameReserved": "Dieser Gruppenname ist reserviert", "contacts_groupAlreadyExists": "Die Gruppe \"{name}\" existiert bereits.", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 58569e4..96060a5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -416,6 +416,7 @@ "contacts_newGroup": "New Group", "contacts_groupName": "Group name", "contacts_groupNameRequired": "Group name is required", + "contacts_groupNameReserved": "This group name is reserved", "contacts_groupAlreadyExists": "Group \"{name}\" already exists", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 25e0345..7cf7898 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Nuevo Grupo", "contacts_groupName": "Nombre del grupo", "contacts_groupNameRequired": "El nombre del grupo es obligatorio", + "contacts_groupNameReserved": "Este nombre de grupo está reservado", "contacts_groupAlreadyExists": "El grupo \"{name}\" ya existe", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5d586f4..bbd488c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Nouveau Groupe", "contacts_groupName": "Nom du groupe", "contacts_groupNameRequired": "Le nom du groupe est obligatoire.", + "contacts_groupNameReserved": "Ce nom de groupe est réservé", "contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 4de9e9d..06dbd12 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Nuovo Gruppo", "contacts_groupName": "Nome gruppo", "contacts_groupNameRequired": "Il nome del gruppo è obbligatorio.", + "contacts_groupNameReserved": "Questo nome del gruppo è riservato", "contacts_groupAlreadyExists": "Il gruppo \"{name}\" esiste già.", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 3690bf2..b278e36 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1714,6 +1714,12 @@ abstract class AppLocalizations { /// **'Group name is required'** String get contacts_groupNameRequired; + /// No description provided for @contacts_groupNameReserved. + /// + /// In en, this message translates to: + /// **'This group name is reserved'** + String get contacts_groupNameReserved; + /// No description provided for @contacts_groupAlreadyExists. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 785f373..8b6c121 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -902,6 +902,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get contacts_groupNameRequired => 'Името на групата е задължително.'; + @override + String get contacts_groupNameReserved => 'Това име на група е запазено'; + @override String contacts_groupAlreadyExists(String name) { return 'Групата \"$name\" вече съществува.'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 708e0ab..1aa2109 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -902,6 +902,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_groupNameRequired => 'Der Gruppennamen ist erforderlich.'; + @override + String get contacts_groupNameReserved => 'Dieser Gruppenname ist reserviert'; + @override String contacts_groupAlreadyExists(String name) { return 'Die Gruppe \"$name\" existiert bereits.'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4938dd4..255f12b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -889,6 +889,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contacts_groupNameRequired => 'Group name is required'; + @override + String get contacts_groupNameReserved => 'This group name is reserved'; + @override String contacts_groupAlreadyExists(String name) { return 'Group \"$name\" already exists'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 2d4e2fb..36efdb3 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -901,6 +901,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get contacts_groupNameRequired => 'El nombre del grupo es obligatorio'; + @override + String get contacts_groupNameReserved => + 'Este nombre de grupo está reservado'; + @override String contacts_groupAlreadyExists(String name) { return 'El grupo \"$name\" ya existe'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 28bbab3..ee76be3 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -905,6 +905,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get contacts_groupNameRequired => 'Le nom du groupe est obligatoire.'; + @override + String get contacts_groupNameReserved => 'Ce nom de groupe est réservé'; + @override String contacts_groupAlreadyExists(String name) { return 'Le groupe \"$name\" existe déjà.'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b510bc1..6566d6a 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -901,6 +901,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get contacts_groupNameRequired => 'Il nome del gruppo è obbligatorio.'; + @override + String get contacts_groupNameReserved => 'Questo nome del gruppo è riservato'; + @override String contacts_groupAlreadyExists(String name) { return 'Il gruppo \"$name\" esiste già.'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 7c054dd..99ca553 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -895,6 +895,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get contacts_groupNameRequired => 'De groepnaam is verplicht.'; + @override + String get contacts_groupNameReserved => 'Deze groepsnaam is gereserveerd'; + @override String contacts_groupAlreadyExists(String name) { return 'De groep \"$name\" bestaat al.'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index dec6583..353f448 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -904,6 +904,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get contacts_groupNameRequired => 'Nazwa grupy jest wymagana'; + @override + String get contacts_groupNameReserved => 'Ta nazwa grupy jest zastrzeżona'; + @override String contacts_groupAlreadyExists(String name) { return 'Grupa \"$name\" już istnieje'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 4d8d20e..8427a49 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -903,6 +903,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get contacts_groupNameRequired => 'O nome do grupo é obrigatório.'; + @override + String get contacts_groupNameReserved => 'Este nome de grupo está reservado'; + @override String contacts_groupAlreadyExists(String name) { return 'O grupo \"$name\" já existe'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 60aa486..74036a2 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -902,6 +902,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get contacts_groupNameRequired => 'Имя группы обязательно'; + @override + String get contacts_groupNameReserved => 'Это имя группы зарезервировано'; + @override String contacts_groupAlreadyExists(String name) { return 'Группа \"$name\" уже существует'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 4e11719..de01520 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -894,6 +894,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get contacts_groupNameRequired => 'Skupina musí mať názov.'; + @override + String get contacts_groupNameReserved => 'Tento názov skupiny je rezervovaný'; + @override String contacts_groupAlreadyExists(String name) { return 'Skupina \"$name\" už existuje'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f967db4..cfe7427 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -892,6 +892,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get contacts_groupNameRequired => 'Ime skupine je obvezno.'; + @override + String get contacts_groupNameReserved => 'To ime skupine je rezervirano'; + @override String contacts_groupAlreadyExists(String name) { return 'Skupina \"$name\" že obstaja'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 200bdbe..93b8917 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -888,6 +888,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get contacts_groupNameRequired => 'Gruppnamnet är obligatoriskt'; + @override + String get contacts_groupNameReserved => 'Detta gruppnamn är reserverat'; + @override String contacts_groupAlreadyExists(String name) { return 'Gruppen \"$name\" finns redan.'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 8dfe123..0db473c 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -898,6 +898,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get contacts_groupNameRequired => 'Назва групи обов\'язкова.'; + @override + String get contacts_groupNameReserved => 'Ця назва групи зарезервована'; + @override String contacts_groupAlreadyExists(String name) { return 'Група «$name» вже існує.'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index ecd6813..55a4063 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -845,6 +845,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get contacts_groupNameRequired => '请输入群聊名称'; + @override + String get contacts_groupNameReserved => '该群组名称已被保留'; + @override String contacts_groupAlreadyExists(String name) { return '名为 \"$name\" 的群聊已存在'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index d38fb4c..427b998 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Nieuwe Groep", "contacts_groupName": "Groepnaam", "contacts_groupNameRequired": "De groepnaam is verplicht.", + "contacts_groupNameReserved": "Deze groepsnaam is gereserveerd", "contacts_groupAlreadyExists": "De groep \"{name}\" bestaat al.", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9dc3b33..ab980ff 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Nowa Grupa", "contacts_groupName": "Nazwa grupy", "contacts_groupNameRequired": "Nazwa grupy jest wymagana", + "contacts_groupNameReserved": "Ta nazwa grupy jest zastrzeżona", "contacts_groupAlreadyExists": "Grupa \"{name}\" już istnieje", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index cded31f..e53649a 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Novo Grupo", "contacts_groupName": "Nome do grupo", "contacts_groupNameRequired": "O nome do grupo é obrigatório.", + "contacts_groupNameReserved": "Este nome de grupo está reservado", "contacts_groupAlreadyExists": "O grupo \"{name}\" já existe", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 43e1b9a..00b71d0 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -212,6 +212,7 @@ "contacts_newGroup": "Новая группа", "contacts_groupName": "Имя группы", "contacts_groupNameRequired": "Имя группы обязательно", + "contacts_groupNameReserved": "Это имя группы зарезервировано", "contacts_groupAlreadyExists": "Группа \"{name}\" уже существует", "contacts_filterContacts": "Фильтр контактов...", "contacts_noContactsMatchFilter": "Нет контактов, соответствующих фильтру", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index f03d276..c05a171 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Nová skupina", "contacts_groupName": "Názov skupiny", "contacts_groupNameRequired": "Skupina musí mať názov.", + "contacts_groupNameReserved": "Tento názov skupiny je rezervovaný", "contacts_groupAlreadyExists": "Skupina \"{name}\" už existuje", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4a4b5cb..e521ed2 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Nova skupina", "contacts_groupName": "Ime skupine", "contacts_groupNameRequired": "Ime skupine je obvezno.", + "contacts_groupNameReserved": "To ime skupine je rezervirano", "contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 6a33e11..c2a538d 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -285,6 +285,7 @@ "contacts_newGroup": "Ny grupp", "contacts_groupName": "Gruppnamn", "contacts_groupNameRequired": "Gruppnamnet är obligatoriskt", + "contacts_groupNameReserved": "Detta gruppnamn är reserverat", "contacts_groupAlreadyExists": "Gruppen \"{name}\" finns redan.", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index c179ca3..3d265dc 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -286,6 +286,7 @@ "contacts_newGroup": "Нова група", "contacts_groupName": "Назва групи", "contacts_groupNameRequired": "Назва групи обов'язкова.", + "contacts_groupNameReserved": "Ця назва групи зарезервована", "contacts_groupAlreadyExists": "Група «{name}» вже існує.", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index cac4b79..26fc6e6 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -300,6 +300,7 @@ "contacts_newGroup": "新建群聊", "contacts_groupName": "群聊名称", "contacts_groupNameRequired": "请输入群聊名称", + "contacts_groupNameReserved": "该群组名称已被保留", "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", "@contacts_groupAlreadyExists": { "placeholders": { diff --git a/lib/main.dart b/lib/main.dart index 9e53e21..1ad1989 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart'; import 'services/background_service.dart'; import 'services/map_tile_cache_service.dart'; import 'services/chat_text_scale_service.dart'; +import 'services/ui_view_state_service.dart'; import 'storage/prefs_manager.dart'; import 'utils/app_logger.dart'; @@ -39,6 +40,7 @@ void main() async { final backgroundService = BackgroundService(); final mapTileCacheService = MapTileCacheService(); final chatTextScaleService = ChatTextScaleService(); + final uiViewStateService = UiViewStateService(); // Load settings await appSettingsService.loadSettings(); @@ -56,6 +58,7 @@ void main() async { _registerThirdPartyLicenses(); await chatTextScaleService.initialize(); + await uiViewStateService.initialize(); // Wire up connector with services connector.initialize( @@ -86,6 +89,7 @@ void main() async { appDebugLogService: appDebugLogService, mapTileCacheService: mapTileCacheService, chatTextScaleService: chatTextScaleService, + uiViewStateService: uiViewStateService, ), ); } @@ -121,6 +125,7 @@ class MeshCoreApp extends StatelessWidget { final AppDebugLogService appDebugLogService; final MapTileCacheService mapTileCacheService; final ChatTextScaleService chatTextScaleService; + final UiViewStateService uiViewStateService; const MeshCoreApp({ super.key, @@ -133,6 +138,7 @@ class MeshCoreApp extends StatelessWidget { required this.appDebugLogService, required this.mapTileCacheService, required this.chatTextScaleService, + required this.uiViewStateService, }); @override @@ -146,6 +152,7 @@ class MeshCoreApp extends StatelessWidget { ChangeNotifierProvider.value(value: bleDebugLogService), ChangeNotifierProvider.value(value: appDebugLogService), ChangeNotifierProvider.value(value: chatTextScaleService), + ChangeNotifierProvider.value(value: uiViewStateService), Provider.value(value: storage), Provider.value(value: mapTileCacheService), ], diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index b56b563..98694be 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -11,6 +11,7 @@ import 'package:uuid/uuid.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; +import '../services/ui_view_state_service.dart'; import '../models/channel.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; @@ -28,8 +29,6 @@ import 'contacts_screen.dart'; import 'map_screen.dart'; import 'settings_screen.dart'; -enum ChannelSortOption { manual, name, latestMessages, unread } - class ChannelsScreen extends StatefulWidget { final bool hideBackButton; @@ -43,9 +42,7 @@ class _ChannelsScreenState extends State with DisconnectNavigationMixin { final TextEditingController _searchController = TextEditingController(); final CommunityStore _communityStore = CommunityStore(); - String _searchQuery = ''; Timer? _searchDebounce; - ChannelSortOption _sortOption = ChannelSortOption.manual; List _communities = []; // Cache of PSK hex -> Community for quick lookup @@ -56,6 +53,9 @@ class _ChannelsScreenState extends State @override void initState() { super.initState(); + _searchController.text = context + .read() + .channelsSearchText; WidgetsBinding.instance.addPostFrameCallback((_) { context.read().getChannels(); _loadCommunities(); @@ -110,6 +110,7 @@ class _ChannelsScreenState extends State @override Widget build(BuildContext context) { final connector = context.watch(); + final viewState = context.watch(); final channelMessageStore = ChannelMessageStore(); channelMessageStore.setPublicKeyHex = connector.selfPublicKeyHex; @@ -205,6 +206,7 @@ class _ChannelsScreenState extends State final filteredChannels = _filterAndSortChannels( channels, connector, + viewState, ); return Column( @@ -219,17 +221,19 @@ class _ChannelsScreenState extends State suffixIcon: Row( mainAxisSize: MainAxisSize.min, children: [ - if (_searchQuery.isNotEmpty) + if (viewState.channelsSearchText.isNotEmpty) IconButton( icon: const Icon(Icons.clear), onPressed: () { + _searchDebounce?.cancel(); + _searchDebounce = null; _searchController.clear(); - setState(() { - _searchQuery = ''; - }); + context + .read() + .setChannelsSearchText(''); }, ), - _buildFilterButton(), + _buildFilterButton(viewState), ], ), border: OutlineInputBorder( @@ -246,9 +250,9 @@ class _ChannelsScreenState extends State const Duration(milliseconds: 300), () { if (!mounted) return; - setState(() { - _searchQuery = value.toLowerCase(); - }); + context + .read() + .setChannelsSearchText(value); }, ); }, @@ -283,8 +287,9 @@ class _ChannelsScreenState extends State ), ], ) - : (_sortOption == ChannelSortOption.manual && - _searchQuery.isEmpty) + : (viewState.channelsSortOption == + ChannelSortOption.manual && + viewState.channelsSearchText.isEmpty) ? ReorderableListView.builder( padding: const EdgeInsets.only( left: 16, @@ -584,59 +589,40 @@ class _ChannelsScreenState extends State await showDisconnectDialog(context, connector); } - Widget _buildFilterButton() { - const actionSortManual = 0; - const actionSortName = 1; - const actionSortLatest = 2; - const actionSortUnread = 3; - - return SortFilterMenu( + Widget _buildFilterButton(UiViewStateService viewState) { + return SortFilterMenu( tooltip: context.l10n.listFilter_tooltip, sections: [ - SortFilterMenuSection( + SortFilterMenuSection( title: context.l10n.channels_sortBy, options: [ - SortFilterMenuOption( - value: actionSortManual, + SortFilterMenuOption( + value: ChannelSortOption.manual, label: context.l10n.channels_sortManual, - checked: _sortOption == ChannelSortOption.manual, + checked: viewState.channelsSortOption == ChannelSortOption.manual, ), - SortFilterMenuOption( - value: actionSortName, + SortFilterMenuOption( + value: ChannelSortOption.name, label: context.l10n.channels_sortAZ, - checked: _sortOption == ChannelSortOption.name, + checked: viewState.channelsSortOption == ChannelSortOption.name, ), - SortFilterMenuOption( - value: actionSortLatest, + SortFilterMenuOption( + value: ChannelSortOption.latestMessages, label: context.l10n.channels_sortLatestMessages, - checked: _sortOption == ChannelSortOption.latestMessages, + checked: + viewState.channelsSortOption == + ChannelSortOption.latestMessages, ), - SortFilterMenuOption( - value: actionSortUnread, + SortFilterMenuOption( + value: ChannelSortOption.unread, label: context.l10n.channels_sortUnread, - checked: _sortOption == ChannelSortOption.unread, + checked: viewState.channelsSortOption == ChannelSortOption.unread, ), ], ), ], - onSelected: (action) { - setState(() { - switch (action) { - case actionSortManual: - _sortOption = ChannelSortOption.manual; - break; - case actionSortLatest: - _sortOption = ChannelSortOption.latestMessages; - break; - case actionSortUnread: - _sortOption = ChannelSortOption.unread; - break; - case actionSortName: - default: - _sortOption = ChannelSortOption.name; - break; - } - }); + onSelected: (sortOption) { + viewState.setChannelsSortOption(sortOption); }, ); } @@ -644,11 +630,14 @@ class _ChannelsScreenState extends State List _filterAndSortChannels( List channels, MeshCoreConnector connector, + UiViewStateService viewState, ) { var filtered = channels.where((channel) { - if (_searchQuery.isEmpty) return true; + if (viewState.channelsSearchText.isEmpty) return true; final label = _normalizeChannelName(channel); - return label.toLowerCase().contains(_searchQuery); + return label.toLowerCase().contains( + viewState.channelsSearchText.toLowerCase(), + ); }).toList(); int compareByName(Channel a, Channel b) { @@ -657,7 +646,7 @@ class _ChannelsScreenState extends State return nameA.toLowerCase().compareTo(nameB.toLowerCase()); } - switch (_sortOption) { + switch (viewState.channelsSortOption) { case ChannelSortOption.manual: break; case ChannelSortOption.latestMessages: diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 3fef9ec..abb29fa 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -12,8 +12,9 @@ import '../l10n/l10n.dart'; import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; import '../models/contact_group.dart'; -import '../storage/contact_group_store.dart'; +import '../services/ui_view_state_service.dart'; import '../utils/contact_search.dart'; +import '../storage/contact_group_store.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; import '../utils/emoji_utils.dart'; @@ -47,12 +48,10 @@ class ContactsScreen extends StatefulWidget { class _ContactsScreenState extends State with DisconnectNavigationMixin { final TextEditingController _searchController = TextEditingController(); - String _searchQuery = ''; - ContactSortOption _sortOption = ContactSortOption.lastSeen; - bool _showUnreadOnly = false; - ContactTypeFilter _typeFilter = ContactTypeFilter.all; final ContactGroupStore _groupStore = ContactGroupStore(); + MeshCoreConnector? _scopeSyncConnector; List _groups = []; + String _loadedGroupScopeKeyHex = ''; Timer? _searchDebounce; final Set _pendingOperations = {}; @@ -62,30 +61,91 @@ class _ContactsScreenState extends State @override void initState() { super.initState(); + _searchController.text = context + .read() + .contactsSearchText; _loadGroups(); _setupFrameListener(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final connector = context.read(); + if (!identical(_scopeSyncConnector, connector)) { + _scopeSyncConnector?.removeListener(_handleConnectorScopeChange); + _scopeSyncConnector = connector; + _scopeSyncConnector?.addListener(_handleConnectorScopeChange); + } + _handleConnectorScopeChange(); + } + @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); _frameSubscription?.cancel(); + _scopeSyncConnector?.removeListener(_handleConnectorScopeChange); super.dispose(); } + void _handleConnectorScopeChange() { + final connector = _scopeSyncConnector; + if (connector == null) return; + _syncGroupScopeIfNeeded(connector); + } + Future _loadGroups() async { + final selfPublicKeyHex = context.read().selfPublicKeyHex; + if (selfPublicKeyHex.isEmpty) { + return; + } + _groupStore.setPublicKeyHex = selfPublicKeyHex; final groups = await _groupStore.loadGroups(); if (!mounted) return; setState(() { + _loadedGroupScopeKeyHex = selfPublicKeyHex; _groups = groups; + _ensureValidSelectedGroup(); }); } Future _saveGroups() async { + final selfPublicKeyHex = context.read().selfPublicKeyHex; + if (selfPublicKeyHex.isEmpty) { + return; + } + _groupStore.setPublicKeyHex = selfPublicKeyHex; await _groupStore.saveGroups(_groups); } + bool _hasGroupStoreScope(MeshCoreConnector connector) { + return connector.selfPublicKeyHex.isNotEmpty; + } + + void _syncGroupScopeIfNeeded(MeshCoreConnector connector) { + final selfPublicKeyHex = connector.selfPublicKeyHex; + if (selfPublicKeyHex.isEmpty || + selfPublicKeyHex == _loadedGroupScopeKeyHex) { + return; + } + _loadGroups(); + } + + void _collapseContactsSearch(UiViewStateService viewState) { + _searchDebounce?.cancel(); + _searchDebounce = null; + _searchController.clear(); + viewState.setContactsSearchText(''); + viewState.setContactsSearchExpanded(false); + } + + void _showGroupsUnavailableMessage(BuildContext context) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.common_loading))); + } + void _setupFrameListener() { final connector = Provider.of(context, listen: false); // Listen for incoming text messages from the repeater @@ -375,31 +435,163 @@ class _ContactsScreenState extends State await showDisconnectDialog(context, connector); } - Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) { + ContactGroup? _selectedGroupForName(String selectedGroupName) { + if (selectedGroupName == contactsAllGroupsValue) return null; + for (final group in _groups) { + if (group.name == selectedGroupName) return group; + } + return null; + } + + void _ensureValidSelectedGroup() { + final viewState = context.read(); + if (viewState.contactsSelectedGroupName == contactsAllGroupsValue) return; + final exists = _groups.any( + (group) => group.name == viewState.contactsSelectedGroupName, + ); + if (!exists) { + viewState.setContactsSelectedGroupName(contactsAllGroupsValue); + } + } + + void _closeDropdownAndRun(BuildContext context, VoidCallback action) { + Navigator.of(context).pop(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + action(); + }); + } + + Widget _buildFilterButton( + BuildContext context, + UiViewStateService viewState, + ) { return ContactsFilterMenu( - sortOption: _sortOption, - typeFilter: _typeFilter, - showUnreadOnly: _showUnreadOnly, + sortOption: viewState.contactsSortOption, + typeFilter: viewState.contactsTypeFilter, + showUnreadOnly: viewState.contactsShowUnreadOnly, onSortChanged: (value) { - setState(() { - _sortOption = value; - }); + viewState.setContactsSortOption(value); }, onTypeFilterChanged: (value) { - setState(() { - _typeFilter = value; - }); + viewState.setContactsTypeFilter(value); }, onUnreadOnlyChanged: (value) { - setState(() { - _showUnreadOnly = value; - }); + viewState.setContactsShowUnreadOnly(value); }, - onNewGroup: () => _showGroupEditor(context, connector.contacts), + ); + } + + Widget _buildGroupButton( + BuildContext context, + MeshCoreConnector connector, + UiViewStateService viewState, + List contacts, + List sortedGroups, + ) { + final canManageGroups = _hasGroupStoreScope(connector); + final selectedGroupName = + _selectedGroupForName(viewState.contactsSelectedGroupName)?.name ?? + context.l10n.listFilter_all; + final double menuWidth = (MediaQuery.sizeOf(context).width - 16).clamp( + 0.0, + double.infinity, + ); + + return PopupMenuButton( + position: PopupMenuPosition.under, + constraints: BoxConstraints.tightFor(width: menuWidth), + onSelected: (String value) { + viewState.setContactsSelectedGroupName(value); + }, + itemBuilder: (menuContext) => [ + PopupMenuItem( + value: contactsAllGroupsValue, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(menuContext.l10n.listFilter_all), + IconButton( + tooltip: menuContext.l10n.contacts_newGroup, + icon: const Icon(Icons.group_add, size: 20), + onPressed: canManageGroups + ? () => _closeDropdownAndRun( + menuContext, + () => _showGroupEditor(this.context, contacts), + ) + : () => _closeDropdownAndRun( + menuContext, + () => _showGroupsUnavailableMessage(this.context), + ), + ), + ], + ), + ), + ...sortedGroups.map((group) { + return PopupMenuItem( + value: group.name, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text(group.name, overflow: TextOverflow.ellipsis), + ), + IconButton( + tooltip: menuContext.l10n.contacts_editGroup, + icon: const Icon(Icons.edit, size: 20), + onPressed: canManageGroups + ? () => _closeDropdownAndRun( + menuContext, + () => _showGroupEditor( + this.context, + contacts, + group: group, + ), + ) + : () => _closeDropdownAndRun( + menuContext, + () => _showGroupsUnavailableMessage(this.context), + ), + ), + const SizedBox(width: 8), + IconButton( + tooltip: menuContext.l10n.contacts_deleteGroup, + icon: const Icon(Icons.delete, size: 20, color: Colors.red), + onPressed: canManageGroups + ? () => _closeDropdownAndRun( + menuContext, + () => _confirmDeleteGroup(this.context, group), + ) + : () => _closeDropdownAndRun( + menuContext, + () => _showGroupsUnavailableMessage(this.context), + ), + ), + ], + ), + ); + }), + ], + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Row( + children: [ + Expanded( + child: Text(selectedGroupName, overflow: TextOverflow.ellipsis), + ), + const SizedBox(width: 8), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), ); } Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) { + final viewState = context.watch(); final contacts = connector.contacts; final shouldShowStartupSpinner = contacts.isEmpty && @@ -421,92 +613,171 @@ class _ContactsScreenState extends State ); } - final filteredAndSorted = _filterAndSortContacts(contacts, connector); - final filteredGroups = _showUnreadOnly - ? const [] - : _filterAndSortGroups(_groups, contacts); + final filteredAndSorted = _filterAndSortContacts( + contacts, + connector, + viewState, + ); String hintText = ""; - switch (_typeFilter) { + switch (viewState.contactsTypeFilter) { case ContactTypeFilter.all: hintText = context.l10n.contacts_searchContacts( filteredAndSorted.length, - _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + viewState.contactsShowUnreadOnly + ? " ${context.l10n.contacts_unread}" + : "", ); break; case ContactTypeFilter.users: hintText = context.l10n.contacts_searchUsers( filteredAndSorted.length, - _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + viewState.contactsShowUnreadOnly + ? " ${context.l10n.contacts_unread}" + : "", ); break; case ContactTypeFilter.repeaters: hintText = context.l10n.contacts_searchRepeaters( filteredAndSorted.length, - _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + viewState.contactsShowUnreadOnly + ? " ${context.l10n.contacts_unread}" + : "", ); break; case ContactTypeFilter.rooms: hintText = context.l10n.contacts_searchRoomServers( filteredAndSorted.length, - _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + viewState.contactsShowUnreadOnly + ? " ${context.l10n.contacts_unread}" + : "", ); break; case ContactTypeFilter.favorites: hintText = context.l10n.contacts_searchFavorites( filteredAndSorted.length, - _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + viewState.contactsShowUnreadOnly + ? " ${context.l10n.contacts_unread}" + : "", ); break; } + final groupsByName = {}; + for (final group in _groups) { + groupsByName.putIfAbsent(group.name, () => group); + } + final sortedGroups = groupsByName.values.toList() + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + + final screenWidth = MediaQuery.sizeOf(context).width; + final searchExpandedWidth = (screenWidth * 0.52).clamp( + 97.0, + double.infinity, + ); // allow expansion up to 52% of screen width, but not less than the collapsed width + final searchCollapsedWidth = (screenWidth * 0.22).clamp( + 97.0, + 120.0, + ); //two 48px icon buttons + 1px divider + return Column( children: [ Padding( padding: const EdgeInsets.all(8.0), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: hintText, - prefixIcon: const Icon(Icons.search), - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_searchQuery.isNotEmpty) - IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchController.clear(); - setState(() { - _searchQuery = ''; - }); - }, + child: Row( + children: [ + Expanded( + child: _buildGroupButton( + context, + connector, + viewState, + contacts, + sortedGroups, + ), + ), + const SizedBox(width: 8), + AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + width: viewState.contactsSearchExpanded + ? searchExpandedWidth + : searchCollapsedWidth, + height: 48, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, ), - _buildFilterButton(context, connector), - ], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + child: viewState.contactsSearchExpanded + ? TextField( + controller: _searchController, + autofocus: true, + decoration: InputDecoration( + hintText: hintText, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + ), + onChanged: (value) { + _searchDebounce?.cancel(); + _searchDebounce = Timer( + const Duration(milliseconds: 300), + () { + if (!mounted) return; + context + .read() + .setContactsSearchText(value); + }, + ); + }, + ) + : const SizedBox.shrink(), + ), + SizedBox( + width: 48, + height: 48, + child: IconButton( + onPressed: () { + if (viewState.contactsSearchExpanded) { + _collapseContactsSearch(viewState); + return; + } + viewState.setContactsSearchExpanded(true); + }, + icon: Icon( + viewState.contactsSearchExpanded + ? Icons.close + : Icons.search, + ), + ), + ), + Container( + width: 1, + height: 24, + color: Theme.of(context).colorScheme.outlineVariant, + ), + SizedBox( + width: 48, + height: 48, + child: _buildFilterButton(context, viewState), + ), + ], + ), + ), ), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - ), - onChanged: (value) { - _searchDebounce?.cancel(); - _searchDebounce = Timer(const Duration(milliseconds: 300), () { - if (!mounted) return; - setState(() { - _searchQuery = value.toLowerCase(); - }); - }); - }, + ], ), ), Expanded( - child: filteredAndSorted.isEmpty && filteredGroups.isEmpty + child: filteredAndSorted.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -514,7 +785,7 @@ class _ContactsScreenState extends State Icon(Icons.search_off, size: 64, color: Colors.grey[400]), const SizedBox(height: 16), Text( - _showUnreadOnly + viewState.contactsShowUnreadOnly ? context.l10n.contacts_noUnreadContacts : context.l10n.contacts_noContactsFound, style: TextStyle(fontSize: 16, color: Colors.grey[600]), @@ -525,14 +796,9 @@ class _ContactsScreenState extends State : RefreshIndicator( onRefresh: () => connector.getContacts(), child: ListView.builder( - itemCount: filteredGroups.length + filteredAndSorted.length, + itemCount: filteredAndSorted.length, itemBuilder: (context, index) { - if (index < filteredGroups.length) { - final group = filteredGroups[index]; - return _buildGroupTile(context, group, contacts); - } - final contact = - filteredAndSorted[index - filteredGroups.length]; + final contact = filteredAndSorted[index]; final unreadCount = connector.getUnreadCountForContact( contact, ); @@ -553,55 +819,26 @@ class _ContactsScreenState extends State ); } - List _filterAndSortGroups( - List groups, - List contacts, - ) { - final query = _searchQuery.trim().toLowerCase(); - final contactsByKey = {}; - for (final contact in contacts) { - contactsByKey[contact.publicKeyHex] = contact; - } - - final filtered = groups - .where((group) { - if (query.isEmpty) return true; - if (group.name.toLowerCase().contains(query)) return true; - for (final key in group.memberKeys) { - final contact = contactsByKey[key]; - if (contact != null && matchesContactQuery(contact, query)) { - return true; - } - } - return false; - }) - .where((group) { - if (_typeFilter == ContactTypeFilter.all) return true; - // Groups don't have a favorite flag, so hide them under favorites filter - if (_typeFilter == ContactTypeFilter.favorites) return false; - for (final key in group.memberKeys) { - final contact = contactsByKey[key]; - if (contact != null && _matchesTypeFilter(contact)) return true; - } - return false; - }) - .toList(); - - filtered.sort( - (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), - ); - return filtered; - } - List _filterAndSortContacts( List contacts, MeshCoreConnector connector, + UiViewStateService viewState, ) { var filtered = contacts.where((contact) { - if (_searchQuery.isEmpty) return true; - return matchesContactQuery(contact, _searchQuery); + if (viewState.contactsSearchText.isEmpty) return true; + return matchesContactQuery(contact, viewState.contactsSearchText); }).toList(); + final selectedGroup = _selectedGroupForName( + viewState.contactsSelectedGroupName, + ); + if (selectedGroup != null) { + final memberKeys = selectedGroup.memberKeys.toSet(); + filtered = filtered + .where((contact) => memberKeys.contains(contact.publicKeyHex)) + .toList(); + } + // Filter out own node from the list if (connector.selfPublicKey != null) { final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!); @@ -610,17 +847,22 @@ class _ContactsScreenState extends State }).toList(); } - if (_typeFilter != ContactTypeFilter.all) { - filtered = filtered.where(_matchesTypeFilter).toList(); + if (viewState.contactsTypeFilter != ContactTypeFilter.all) { + filtered = filtered + .where( + (contact) => + _matchesTypeFilter(contact, viewState.contactsTypeFilter), + ) + .toList(); } - if (_showUnreadOnly) { + if (viewState.contactsShowUnreadOnly) { filtered = filtered.where((contact) { return connector.getUnreadCountForContact(contact) > 0; }).toList(); } - switch (_sortOption) { + switch (viewState.contactsSortOption) { case ContactSortOption.lastSeen: filtered.sort( (a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)), @@ -649,8 +891,8 @@ class _ContactsScreenState extends State return filtered; } - bool _matchesTypeFilter(Contact contact) { - switch (_typeFilter) { + bool _matchesTypeFilter(Contact contact, ContactTypeFilter typeFilter) { + switch (typeFilter) { case ContactTypeFilter.all: return true; case ContactTypeFilter.favorites: @@ -671,57 +913,6 @@ class _ContactsScreenState extends State : contact.lastSeen; } - Widget _buildGroupTile( - BuildContext context, - ContactGroup group, - List contacts, - ) { - final memberContacts = _resolveGroupContacts(group, contacts); - final subtitle = _formatGroupMembers(context, memberContacts); - return ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.teal, - child: Icon(Icons.group, color: Colors.white, size: 20), - ), - title: Text(group.name), - subtitle: Text(subtitle), - trailing: Text( - memberContacts.length.toString(), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - onTap: () => _showGroupOptions(context, group, contacts), - onLongPress: () => _showGroupOptions(context, group, contacts), - ); - } - - List _resolveGroupContacts( - ContactGroup group, - List contacts, - ) { - final byKey = {}; - for (final contact in contacts) { - byKey[contact.publicKeyHex] = contact; - } - final resolved = []; - for (final key in group.memberKeys) { - final contact = byKey[key]; - if (contact != null) { - resolved.add(contact); - } - } - resolved.sort( - (a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()), - ); - return resolved; - } - - String _formatGroupMembers(BuildContext context, List members) { - if (members.isEmpty) return context.l10n.contacts_noMembers; - final names = members.map((c) => c.name).toList(); - if (names.length <= 2) return names.join(', '); - return '${names.take(2).join(', ')} +${names.length - 2}'; - } - void _openChat(BuildContext context, Contact contact) { // Check if this is a repeater if (contact.type == advTypeRepeater) { @@ -799,58 +990,11 @@ class _ContactsScreenState extends State ); } - void _showGroupOptions( - BuildContext context, - ContactGroup group, - List contacts, - ) { - final members = _resolveGroupContacts(group, contacts); - showModalBottomSheet( - context: context, - builder: (sheetContext) => SafeArea( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.edit), - title: Text(context.l10n.contacts_editGroup), - onTap: () { - Navigator.pop(sheetContext); - _showGroupEditor(context, contacts, group: group); - }, - ), - ListTile( - leading: const Icon(Icons.delete, color: Colors.red), - title: Text( - context.l10n.contacts_deleteGroup, - style: const TextStyle(color: Colors.red), - ), - onTap: () { - Navigator.pop(sheetContext); - _confirmDeleteGroup(context, group); - }, - ), - if (members.isNotEmpty) const Divider(), - ...members.map((member) { - return ListTile( - leading: const Icon(Icons.person), - title: Text(member.name), - subtitle: Text(member.typeLabel), - onTap: () { - Navigator.pop(sheetContext); - _openChat(context, member); - }, - ); - }), - ], - ), - ), - ), - ); - } - void _confirmDeleteGroup(BuildContext context, ContactGroup group) { + if (!_hasGroupStoreScope(context.read())) { + _showGroupsUnavailableMessage(context); + return; + } showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -866,6 +1010,7 @@ class _ContactsScreenState extends State Navigator.pop(dialogContext); setState(() { _groups.removeWhere((g) => g.name == group.name); + _ensureValidSelectedGroup(); }); await _saveGroups(); }, @@ -884,6 +1029,10 @@ class _ContactsScreenState extends State List contacts, { ContactGroup? group, }) { + if (!_hasGroupStoreScope(context.read())) { + _showGroupsUnavailableMessage(context); + return; + } final isEditing = group != null; final nameController = TextEditingController(text: group?.name ?? ''); final selectedKeys = {...group?.memberKeys ?? []}; @@ -910,64 +1059,70 @@ class _ContactsScreenState extends State ), content: SizedBox( width: double.maxFinite, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: nameController, - decoration: InputDecoration( - labelText: context.l10n.contacts_groupName, - border: const OutlineInputBorder(), + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: InputDecoration( + labelText: context.l10n.contacts_groupName, + border: const OutlineInputBorder(), + ), ), - ), - const SizedBox(height: 12), - TextField( - decoration: InputDecoration( - hintText: context.l10n.contacts_filterContacts, - prefixIcon: const Icon(Icons.search), - border: const OutlineInputBorder(), - isDense: true, + const SizedBox(height: 12), + TextField( + decoration: InputDecoration( + hintText: context.l10n.contacts_filterContacts, + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + isDense: true, + ), + onChanged: (value) { + setDialogState(() { + filterQuery = value.toLowerCase(); + }); + }, ), - onChanged: (value) { - setDialogState(() { - filterQuery = value.toLowerCase(); - }); - }, - ), - const SizedBox(height: 12), - SizedBox( - height: 240, - child: filteredContacts.isEmpty - ? Center( - child: Text( - context.l10n.contacts_noContactsMatchFilter, + const SizedBox(height: 12), + Expanded( + child: filteredContacts.isEmpty + ? Center( + child: Text( + context.l10n.contacts_noContactsMatchFilter, + ), + ) + : ListView.builder( + itemCount: filteredContacts.length, + itemBuilder: (context, index) { + final contact = filteredContacts[index]; + final isSelected = selectedKeys.contains( + contact.publicKeyHex, + ); + return CheckboxListTile( + value: isSelected, + title: Text(contact.name), + subtitle: Text(contact.typeLabel), + onChanged: (value) { + setDialogState(() { + if (value == true) { + selectedKeys.add(contact.publicKeyHex); + } else { + selectedKeys.remove( + contact.publicKeyHex, + ); + } + }); + }, + ); + }, ), - ) - : ListView.builder( - itemCount: filteredContacts.length, - itemBuilder: (context, index) { - final contact = filteredContacts[index]; - final isSelected = selectedKeys.contains( - contact.publicKeyHex, - ); - return CheckboxListTile( - value: isSelected, - title: Text(contact.name), - subtitle: Text(contact.typeLabel), - onChanged: (value) { - setDialogState(() { - if (value == true) { - selectedKeys.add(contact.publicKeyHex); - } else { - selectedKeys.remove(contact.publicKeyHex); - } - }); - }, - ); - }, - ), - ), - ], + ), + ], + ), ), ), actions: [ @@ -986,6 +1141,15 @@ class _ContactsScreenState extends State ); return; } + if (name.toLowerCase() == + contactsAllGroupsValue.toLowerCase()) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_groupNameReserved), + ), + ); + return; + } final exists = _groups.any((g) { if (isEditing && g.name == group.name) return false; return g.name.toLowerCase() == name.toLowerCase(); @@ -1001,15 +1165,21 @@ class _ContactsScreenState extends State return; } setState(() { + final viewState = context.read(); if (isEditing) { final index = _groups.indexWhere( (g) => g.name == group.name, ); if (index != -1) { + final wasSelected = + viewState.contactsSelectedGroupName == group.name; _groups[index] = ContactGroup( name: name, memberKeys: selectedKeys.toList(), ); + if (wasSelected) { + viewState.setContactsSelectedGroupName(name); + } } } else { _groups.add( @@ -1018,7 +1188,9 @@ class _ContactsScreenState extends State memberKeys: selectedKeys.toList(), ), ); + viewState.setContactsSelectedGroupName(name); } + _ensureValidSelectedGroup(); }); await _saveGroups(); if (dialogContext.mounted) { diff --git a/lib/services/chat_text_scale_service.dart b/lib/services/chat_text_scale_service.dart index 21d6a5f..205c258 100644 --- a/lib/services/chat_text_scale_service.dart +++ b/lib/services/chat_text_scale_service.dart @@ -65,7 +65,7 @@ class ChatTextScaleService extends ChangeNotifier { void _commitScale() { _saveTimer?.cancel(); - PrefsManager.instance.setDouble(_prefKey, _scale); + unawaited(PrefsManager.instance.setDouble(_prefKey, _scale)); } double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble(); diff --git a/lib/services/ui_view_state_service.dart b/lib/services/ui_view_state_service.dart new file mode 100644 index 0000000..7f2a03a --- /dev/null +++ b/lib/services/ui_view_state_service.dart @@ -0,0 +1,154 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../storage/prefs_manager.dart'; +import '../utils/contact_search.dart'; + +const String contactsAllGroupsValue = '__all__'; + +enum ChannelSortOption { manual, name, latestMessages, unread } + +class UiViewStateService extends ChangeNotifier { + static const _keyContactsSelectedGroupName = 'ui_contacts_selected_group'; + static const _keyContactsSortOption = 'ui_contacts_sort_option'; + static const _keyContactsShowUnreadOnly = 'ui_contacts_show_unread_only'; + static const _keyContactsTypeFilter = 'ui_contacts_type_filter'; + static const _keyChannelsSortOption = 'ui_channels_sort_option'; + static const _keyChannelsSortIndexLegacy = 'ui_channels_sort_index'; + + String _contactsSelectedGroupName = contactsAllGroupsValue; + String _contactsSearchText = ''; + bool _contactsSearchExpanded = false; + ContactSortOption _contactsSortOption = ContactSortOption.lastSeen; + bool _contactsShowUnreadOnly = false; + ContactTypeFilter _contactsTypeFilter = ContactTypeFilter.all; + + String _channelsSearchText = ''; + ChannelSortOption _channelsSortOption = ChannelSortOption.manual; + + String get contactsSelectedGroupName => _contactsSelectedGroupName; + String get contactsSearchText => _contactsSearchText; + bool get contactsSearchExpanded => _contactsSearchExpanded; + ContactSortOption get contactsSortOption => _contactsSortOption; + bool get contactsShowUnreadOnly => _contactsShowUnreadOnly; + ContactTypeFilter get contactsTypeFilter => _contactsTypeFilter; + String get channelsSearchText => _channelsSearchText; + ChannelSortOption get channelsSortOption => _channelsSortOption; + + Future initialize() async { + final prefs = PrefsManager.instance; + + final selectedGroupName = prefs.getString(_keyContactsSelectedGroupName); + if (selectedGroupName != null && selectedGroupName.isNotEmpty) { + _contactsSelectedGroupName = selectedGroupName; + } + + final sortStr = prefs.getString(_keyContactsSortOption); + if (sortStr != null) { + _contactsSortOption = ContactSortOption.values.firstWhere( + (e) => e.name == sortStr, + orElse: () => ContactSortOption.lastSeen, + ); + } + + _contactsShowUnreadOnly = + prefs.getBool(_keyContactsShowUnreadOnly) ?? false; + + final typeStr = prefs.getString(_keyContactsTypeFilter); + if (typeStr != null) { + _contactsTypeFilter = ContactTypeFilter.values.firstWhere( + (e) => e.name == typeStr, + orElse: () => ContactTypeFilter.all, + ); + } + + final channelSortStr = prefs.getString(_keyChannelsSortOption); + if (channelSortStr != null) { + _channelsSortOption = ChannelSortOption.values.firstWhere( + (e) => e.name == channelSortStr, + orElse: () => ChannelSortOption.manual, + ); + return; + } + + // Backward compatibility for old persisted index format. + switch (prefs.getInt(_keyChannelsSortIndexLegacy) ?? 0) { + case 0: + _channelsSortOption = ChannelSortOption.manual; + break; + case 1: + _channelsSortOption = ChannelSortOption.name; + break; + case 2: + _channelsSortOption = ChannelSortOption.latestMessages; + break; + case 3: + _channelsSortOption = ChannelSortOption.unread; + break; + default: + _channelsSortOption = ChannelSortOption.manual; + } + } + + void setContactsSelectedGroupName(String value) { + if (_contactsSelectedGroupName == value) return; + _contactsSelectedGroupName = value; + notifyListeners(); + unawaited( + PrefsManager.instance.setString(_keyContactsSelectedGroupName, value), + ); + } + + void setContactsSearchText(String value) { + if (_contactsSearchText == value) return; + _contactsSearchText = value; + notifyListeners(); + } + + void setContactsSearchExpanded(bool value) { + if (_contactsSearchExpanded == value) return; + _contactsSearchExpanded = value; + notifyListeners(); + } + + void setContactsSortOption(ContactSortOption value) { + if (_contactsSortOption == value) return; + _contactsSortOption = value; + notifyListeners(); + unawaited( + PrefsManager.instance.setString(_keyContactsSortOption, value.name), + ); + } + + void setContactsShowUnreadOnly(bool value) { + if (_contactsShowUnreadOnly == value) return; + _contactsShowUnreadOnly = value; + notifyListeners(); + unawaited(PrefsManager.instance.setBool(_keyContactsShowUnreadOnly, value)); + } + + void setContactsTypeFilter(ContactTypeFilter value) { + if (_contactsTypeFilter == value) return; + _contactsTypeFilter = value; + notifyListeners(); + unawaited( + PrefsManager.instance.setString(_keyContactsTypeFilter, value.name), + ); + } + + void setChannelsSearchText(String value) { + if (_channelsSearchText == value) return; + _channelsSearchText = value; + notifyListeners(); + } + + void setChannelsSortOption(ChannelSortOption value) { + if (_channelsSortOption == value) return; + _channelsSortOption = value; + notifyListeners(); + unawaited( + PrefsManager.instance.setString(_keyChannelsSortOption, value.name), + ); + } +} diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 1f05fdc..849172a 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,5 +1,9 @@ import '../models/contact.dart'; +enum ContactSortOption { lastSeen, recentMessages, name } + +enum ContactTypeFilter { all, favorites, users, repeaters, rooms } + bool matchesContactQuery(Contact contact, String query) { final normalizedQuery = query.trim().toLowerCase(); if (normalizedQuery.isEmpty) return true; diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index ee6fcd4..8b2874b 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; import '../l10n/l10n.dart'; +import '../utils/contact_search.dart'; -enum ContactSortOption { lastSeen, recentMessages, name } - -enum ContactTypeFilter { all, favorites, users, repeaters, rooms } - -class SortFilterMenuOption { - final int value; +class SortFilterMenuOption { + final T value; final String label; final bool? checked; @@ -17,16 +14,16 @@ class SortFilterMenuOption { }); } -class SortFilterMenuSection { +class SortFilterMenuSection { final String title; - final List options; + final List> options; const SortFilterMenuSection({required this.title, required this.options}); } -class SortFilterMenu extends StatelessWidget { - final List sections; - final ValueChanged onSelected; +class SortFilterMenu extends StatelessWidget { + final List> sections; + final ValueChanged onSelected; final String tooltip; final Widget icon; @@ -40,7 +37,7 @@ class SortFilterMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return PopupMenuButton( + return PopupMenuButton( icon: icon, tooltip: tooltip, onSelected: onSelected, @@ -53,11 +50,11 @@ class SortFilterMenu extends StatelessWidget { final visibleSections = sections .where((section) => section.options.isNotEmpty) .toList(); - final entries = >[]; + final entries = >[]; for (int i = 0; i < visibleSections.length; i++) { final section = visibleSections[i]; entries.add( - PopupMenuItem( + PopupMenuItem( enabled: false, child: Text(section.title, style: labelStyle), ), @@ -65,14 +62,14 @@ class SortFilterMenu extends StatelessWidget { for (final option in section.options) { if (option.checked == null) { entries.add( - PopupMenuItem( + PopupMenuItem( value: option.value, child: Text(option.label), ), ); } else { entries.add( - CheckedPopupMenuItem( + CheckedPopupMenuItem( value: option.value, checked: option.checked ?? false, child: Text(option.label), @@ -99,7 +96,6 @@ const int _actionFilterUsers = 6; const int _actionFilterRepeaters = 7; const int _actionFilterRooms = 8; const int _actionToggleUnreadOnly = 9; -const int _actionNewGroup = 10; class ContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; @@ -108,7 +104,6 @@ class ContactsFilterMenu extends StatelessWidget { final ValueChanged onSortChanged; final ValueChanged onTypeFilterChanged; final ValueChanged onUnreadOnlyChanged; - final VoidCallback onNewGroup; const ContactsFilterMenu({ super.key, @@ -118,7 +113,6 @@ class ContactsFilterMenu extends StatelessWidget { required this.onSortChanged, required this.onTypeFilterChanged, required this.onUnreadOnlyChanged, - required this.onNewGroup, }); @override @@ -180,10 +174,6 @@ class ContactsFilterMenu extends StatelessWidget { label: l10n.listFilter_unreadOnly, checked: showUnreadOnly, ), - SortFilterMenuOption( - value: _actionNewGroup, - label: l10n.listFilter_newGroup, - ), ], ), ], @@ -216,9 +206,6 @@ class ContactsFilterMenu extends StatelessWidget { case _actionToggleUnreadOnly: onUnreadOnlyChanged(!showUnreadOnly); break; - case _actionNewGroup: - onNewGroup(); - break; } }, ); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e..4084d9b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From 2ee2358eccebb18f5e34b062d95b26f9e73eb8fe Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 16:56:11 -0700 Subject: [PATCH 301/421] feat: add ML-based adaptive timeout prediction using LinearRegressor Train a linear regression model on actual message delivery times to predict tighter timeouts, replacing worst-case physics estimates. Features: path length, message bytes, seconds since last RX, flood mode. Global model with per-contact blending after 10+ observations per contact. Falls back to existing physics formula when model has insufficient data. --- lib/connector/meshcore_connector.dart | 36 ++- lib/main.dart | 8 + lib/models/delivery_observation.dart | 43 ++++ lib/services/message_retry_service.dart | 54 +++-- lib/services/storage_service.dart | 50 ++++ lib/services/timeout_prediction_service.dart | 224 ++++++++++++++++++ pubspec.yaml | 2 + test/services/ml_algo_sanity_test.dart | 122 ++++++++++ .../timeout_prediction_service_test.dart | 164 +++++++++++++ 9 files changed, 683 insertions(+), 20 deletions(-) create mode 100644 lib/models/delivery_observation.dart create mode 100644 lib/services/timeout_prediction_service.dart create mode 100644 test/services/ml_algo_sanity_test.dart create mode 100644 test/services/timeout_prediction_service_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7cf32ef..d05a8f9 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -19,6 +19,7 @@ import '../services/message_retry_service.dart'; import '../services/path_history_service.dart'; import '../services/app_settings_service.dart'; import '../services/background_service.dart'; +import '../services/timeout_prediction_service.dart'; import '../services/notification_service.dart'; import 'meshcore_connector_usb.dart'; import 'meshcore_connector_tcp.dart'; @@ -166,6 +167,8 @@ class MeshCoreConnector extends ChangeNotifier { bool _isLoadingContacts = false; bool _isLoadingChannels = false; bool _hasLoadedChannels = false; + TimeoutPredictionService? _timeoutPredictionService; + DateTime _lastRxTime = DateTime.now(); bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; @@ -668,6 +671,7 @@ class MeshCoreConnector extends ChangeNotifier { BleDebugLogService? bleDebugLogService, AppDebugLogService? appDebugLogService, BackgroundService? backgroundService, + TimeoutPredictionService? timeoutPredictionService, }) { _retryService = retryService; _pathHistoryService = pathHistoryService; @@ -675,6 +679,7 @@ class MeshCoreConnector extends ChangeNotifier { _bleDebugLogService = bleDebugLogService; _appDebugLogService = appDebugLogService; _backgroundService = backgroundService; + _timeoutPredictionService = timeoutPredictionService; _usbManager.setDebugLogService(_appDebugLogService); _tcpConnector.setDebugLogService(_appDebugLogService); @@ -689,13 +694,23 @@ class MeshCoreConnector extends ChangeNotifier { updateMessageCallback: _updateMessage, clearContactPathCallback: clearContactPath, setContactPathCallback: setContactPath, - calculateTimeoutCallback: (pathLength, messageBytes) => - calculateTimeout(pathLength: pathLength, messageBytes: messageBytes), + calculateTimeoutCallback: (pathLength, messageBytes, {String? contactKey}) => + calculateTimeout(pathLength: pathLength, messageBytes: messageBytes, contactKey: contactKey), getSelfPublicKeyCallback: () => _selfPublicKey, prepareContactOutboundTextCallback: prepareContactOutboundText, appSettingsService: appSettingsService, debugLogService: _appDebugLogService, recordPathResultCallback: _recordPathResult, + onDeliveryObservedCallback: (contactKey, pathLength, messageBytes, tripTimeMs) { + final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; + _timeoutPredictionService?.recordObservation( + contactKey: contactKey, + pathLength: pathLength, + messageBytes: messageBytes, + tripTimeMs: tripTimeMs, + secondsSinceLastRx: secSinceRx, + ); + }, ); } @@ -2498,6 +2513,7 @@ class MeshCoreConnector extends ChangeNotifier { void _handleFrame(List data) { if (data.isEmpty) return; + _lastRxTime = DateTime.now(); final frame = Uint8List.fromList(data); _receivedFramesController.add(frame); @@ -2876,7 +2892,21 @@ class MeshCoreConnector extends ChangeNotifier { /// Calculate timeout for a message based on radio settings and path length /// Returns timeout in milliseconds, considering number of hops - int calculateTimeout({required int pathLength, int messageBytes = 100}) { + int calculateTimeout({ + required int pathLength, + int messageBytes = 100, + String? contactKey, + }) { + // Try ML-based prediction first + final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; + final mlTimeout = _timeoutPredictionService?.predictTimeout( + contactKey: contactKey, + pathLength: pathLength, + messageBytes: messageBytes, + secondsSinceLastRx: secSinceRx, + ); + if (mlTimeout != null) return mlTimeout; + // If we have radio settings, use them for accurate calculation if (_currentFreqHz != null && _currentBwHz != null && diff --git a/lib/main.dart b/lib/main.dart index 9e53e21..72909e2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart'; import 'services/background_service.dart'; import 'services/map_tile_cache_service.dart'; import 'services/chat_text_scale_service.dart'; +import 'services/timeout_prediction_service.dart'; import 'storage/prefs_manager.dart'; import 'utils/app_logger.dart'; @@ -39,6 +40,7 @@ void main() async { final backgroundService = BackgroundService(); final mapTileCacheService = MapTileCacheService(); final chatTextScaleService = ChatTextScaleService(); + final timeoutPredictionService = TimeoutPredictionService(storage); // Load settings await appSettingsService.loadSettings(); @@ -56,6 +58,7 @@ void main() async { _registerThirdPartyLicenses(); await chatTextScaleService.initialize(); + await timeoutPredictionService.initialize(); // Wire up connector with services connector.initialize( @@ -65,6 +68,7 @@ void main() async { bleDebugLogService: bleDebugLogService, appDebugLogService: appDebugLogService, backgroundService: backgroundService, + timeoutPredictionService: timeoutPredictionService, ); await connector.loadContactCache(); @@ -86,6 +90,7 @@ void main() async { appDebugLogService: appDebugLogService, mapTileCacheService: mapTileCacheService, chatTextScaleService: chatTextScaleService, + timeoutPredictionService: timeoutPredictionService, ), ); } @@ -121,6 +126,7 @@ class MeshCoreApp extends StatelessWidget { final AppDebugLogService appDebugLogService; final MapTileCacheService mapTileCacheService; final ChatTextScaleService chatTextScaleService; + final TimeoutPredictionService timeoutPredictionService; const MeshCoreApp({ super.key, @@ -133,6 +139,7 @@ class MeshCoreApp extends StatelessWidget { required this.appDebugLogService, required this.mapTileCacheService, required this.chatTextScaleService, + required this.timeoutPredictionService, }); @override @@ -148,6 +155,7 @@ class MeshCoreApp extends StatelessWidget { ChangeNotifierProvider.value(value: chatTextScaleService), Provider.value(value: storage), Provider.value(value: mapTileCacheService), + ChangeNotifierProvider.value(value: timeoutPredictionService), ], child: Consumer( builder: (context, settingsService, child) { diff --git a/lib/models/delivery_observation.dart b/lib/models/delivery_observation.dart new file mode 100644 index 0000000..a598d2a --- /dev/null +++ b/lib/models/delivery_observation.dart @@ -0,0 +1,43 @@ +class DeliveryObservation { + final String contactKey; + final int pathLength; + final int messageBytes; + final int secondsSinceLastRx; + final bool isFlood; + final int deliveryMs; + final DateTime timestamp; + + DeliveryObservation({ + required this.contactKey, + required this.pathLength, + required this.messageBytes, + required this.secondsSinceLastRx, + required this.isFlood, + required this.deliveryMs, + required this.timestamp, + }); + + Map toJson() { + return { + 'contact_key': contactKey, + 'path_length': pathLength, + 'message_bytes': messageBytes, + 'seconds_since_last_rx': secondsSinceLastRx, + 'is_flood': isFlood, + 'delivery_ms': deliveryMs, + 'timestamp': timestamp.toIso8601String(), + }; + } + + factory DeliveryObservation.fromJson(Map json) { + return DeliveryObservation( + contactKey: json['contact_key'] as String, + pathLength: json['path_length'] as int, + messageBytes: json['message_bytes'] as int, + secondsSinceLastRx: json['seconds_since_last_rx'] as int? ?? 0, + isFlood: json['is_flood'] as bool, + deliveryMs: json['delivery_ms'] as int, + timestamp: DateTime.parse(json['timestamp'] as String), + ); + } +} diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index db4475f..d94b763 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -58,12 +58,13 @@ class MessageRetryService extends ChangeNotifier { Function(Message)? _updateMessageCallback; Function(Contact)? _clearContactPathCallback; Function(Contact, Uint8List, int)? _setContactPathCallback; - Function(int, int)? _calculateTimeoutCallback; + Function(int, int, {String? contactKey})? _calculateTimeoutCallback; Uint8List? Function()? _getSelfPublicKeyCallback; String Function(Contact, String)? _prepareContactOutboundTextCallback; AppSettingsService? _appSettingsService; AppDebugLogService? _debugLogService; Function(String, PathSelection, bool, int?)? _recordPathResultCallback; + Function(String, int, int, int)? _onDeliveryObservedCallback; MessageRetryService(); @@ -73,12 +74,14 @@ class MessageRetryService extends ChangeNotifier { required Function(Message) updateMessageCallback, Function(Contact)? clearContactPathCallback, Function(Contact, Uint8List, int)? setContactPathCallback, - Function(int pathLength, int messageBytes)? calculateTimeoutCallback, + Function(int pathLength, int messageBytes, {String? contactKey})? calculateTimeoutCallback, Uint8List? Function()? getSelfPublicKeyCallback, String Function(Contact, String)? prepareContactOutboundTextCallback, AppSettingsService? appSettingsService, AppDebugLogService? debugLogService, Function(String, PathSelection, bool, int?)? recordPathResultCallback, + Function(String contactKey, int pathLength, int messageBytes, int tripTimeMs)? + onDeliveryObservedCallback, }) { _sendMessageCallback = sendMessageCallback; _addMessageCallback = addMessageCallback; @@ -91,6 +94,7 @@ class MessageRetryService extends ChangeNotifier { _appSettingsService = appSettingsService; _debugLogService = debugLogService; _recordPathResultCallback = recordPathResultCallback; + _onDeliveryObservedCallback = onDeliveryObservedCallback; } /// Compute expected ACK hash using same algorithm as firmware: @@ -423,25 +427,33 @@ class MessageRetryService extends ChangeNotifier { ); } - // Use device-provided timeout, or calculate from radio settings if timeout is 0 or invalid + // Calculate timeout: prefer ML prediction, then device-provided, then physics fallback + int pathLengthValue; + if (selection != null) { + pathLengthValue = selection.useFlood ? -1 : selection.hopCount; + if (pathLengthValue < 0) pathLengthValue = contact.pathLength; + } else if (message.pathLength != null) { + pathLengthValue = message.pathLength!; + } else { + pathLengthValue = contact.pathLength; + } + int actualTimeout = timeoutMs; - if (timeoutMs <= 0 && _calculateTimeoutCallback != null) { - int pathLengthValue; - if (selection != null) { - pathLengthValue = selection.useFlood ? -1 : selection.hopCount; - if (pathLengthValue < 0) pathLengthValue = contact.pathLength; - } else if (message.pathLength != null) { - pathLengthValue = message.pathLength!; - } else { - pathLengthValue = contact.pathLength; - } - actualTimeout = _calculateTimeoutCallback!( + if (_calculateTimeoutCallback != null) { + final calculated = _calculateTimeoutCallback!( pathLengthValue, message.text.length, + contactKey: contact.publicKeyHex, ); - debugPrint( - 'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue', - ); + // calculateTimeout tries ML first, falls back to physics. + // Use calculated value if device didn't provide one, or if ML + // produced a tighter prediction than the device's estimate. + if (timeoutMs <= 0 || calculated < timeoutMs) { + actualTimeout = calculated; + debugPrint( + 'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue', + ); + } } final updatedMessage = message.copyWith( @@ -738,6 +750,14 @@ class MessageRetryService extends ChangeNotifier { true, tripTimeMs, ); + if (_onDeliveryObservedCallback != null && tripTimeMs > 0) { + _onDeliveryObservedCallback!( + contact.publicKeyHex, + message.pathLength ?? 0, + message.text.length, + tripTimeMs, + ); + } _onMessageResolved(matchedMessageId, contact.publicKeyHex); } diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index ce0c4f1..c591f64 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import '../models/delivery_observation.dart'; import '../models/path_history.dart'; import '../storage/prefs_manager.dart'; @@ -6,6 +7,8 @@ class StorageService { static const String _pathHistoryPrefix = 'path_history_'; static const String _pendingMessagesKey = 'pending_messages'; static const String _repeaterPasswordsKey = 'repeater_passwords'; + static const String _deliveryObservationsKey = 'delivery_observations'; + static const String _timeoutModelKey = 'timeout_ml_model'; Future savePathHistory( String contactPubKeyHex, @@ -122,4 +125,51 @@ class StorageService { final prefs = PrefsManager.instance; await prefs.remove(_repeaterPasswordsKey); } + + Future saveDeliveryObservations( + List observations, + ) async { + final prefs = PrefsManager.instance; + final jsonStr = jsonEncode(observations.map((o) => o.toJson()).toList()); + await prefs.setString(_deliveryObservationsKey, jsonStr); + } + + Future> loadDeliveryObservations() async { + final prefs = PrefsManager.instance; + final jsonStr = prefs.getString(_deliveryObservationsKey); + + if (jsonStr == null) return []; + + try { + final list = jsonDecode(jsonStr) as List; + return list + .map( + (e) => + DeliveryObservation.fromJson(e as Map), + ) + .toList(); + } catch (e) { + return []; + } + } + + Future clearDeliveryObservations() async { + final prefs = PrefsManager.instance; + await prefs.remove(_deliveryObservationsKey); + } + + Future saveTimeoutModel(String modelJson) async { + final prefs = PrefsManager.instance; + await prefs.setString(_timeoutModelKey, modelJson); + } + + Future loadTimeoutModel() async { + final prefs = PrefsManager.instance; + return prefs.getString(_timeoutModelKey); + } + + Future clearTimeoutModel() async { + final prefs = PrefsManager.instance; + await prefs.remove(_timeoutModelKey); + } } diff --git a/lib/services/timeout_prediction_service.dart b/lib/services/timeout_prediction_service.dart new file mode 100644 index 0000000..21e229e --- /dev/null +++ b/lib/services/timeout_prediction_service.dart @@ -0,0 +1,224 @@ +import 'dart:convert'; +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:ml_algo/ml_algo.dart'; +import 'package:ml_dataframe/ml_dataframe.dart'; +import '../models/delivery_observation.dart'; +import 'storage_service.dart'; + +class _ContactStats { + int count = 0; + double _sum = 0; + double _sumSq = 0; + + void add(double ms) { + count++; + _sum += ms; + _sumSq += ms * ms; + } + + double get mean => _sum / count; + double get stdDev => sqrt((_sumSq / count) - (mean * mean)); +} + +class TimeoutPredictionService extends ChangeNotifier { + final StorageService? _storage; + + static const int minObservations = 10; + static const int maxObservations = 100; + static const int _retrainInterval = 5; + static const double _safetyMargin = 1.5; + static const int _minTimeoutMs = 2000; + static const int _maxTimeoutMs = 120000; + static const int _minContactObservations = 10; + + List _observations = []; + LinearRegressor? _model; + List _activeFeatures = []; + int _observationsSinceLastTrain = 0; + final Map _contactStats = {}; + + TimeoutPredictionService(StorageService storage) : _storage = storage; + TimeoutPredictionService.noStorage() : _storage = null; + + int get observationCount => _observations.length; + bool get hasModel => _model != null; + + Future initialize() async { + _observations = await _storage?.loadDeliveryObservations() ?? []; + _rebuildContactStats(); + + if (_observations.length >= minObservations) { + _trainModel(); + } + + debugPrint( + 'TimeoutPrediction: initialized with ${_observations.length} observations, ' + 'model=${_model != null ? "ready" : "waiting for data"}', + ); + } + + void recordObservation({ + required String contactKey, + required int pathLength, + required int messageBytes, + required int tripTimeMs, + int secondsSinceLastRx = 0, + }) { + final observation = DeliveryObservation( + contactKey: contactKey, + pathLength: pathLength, + messageBytes: messageBytes, + secondsSinceLastRx: secondsSinceLastRx, + isFlood: pathLength < 0, + deliveryMs: tripTimeMs, + timestamp: DateTime.now(), + ); + + _observations.add(observation); + if (_observations.length > maxObservations) { + _observations.removeAt(0); + } + + _contactStats.putIfAbsent(contactKey, () => _ContactStats()); + _contactStats[contactKey]!.add(tripTimeMs.toDouble()); + + _observationsSinceLastTrain++; + if (_observationsSinceLastTrain >= _retrainInterval && + _observations.length >= minObservations) { + _trainModel(); + } + + _storage?.saveDeliveryObservations(_observations); + debugPrint( + 'TimeoutPrediction: recorded ${tripTimeMs}ms for $pathLength hops ' + '(${_observations.length} total)', + ); + } + + int? predictTimeout({ + String? contactKey, + required int pathLength, + required int messageBytes, + int secondsSinceLastRx = 0, + }) { + if (_model == null) return null; + + try { + if (_activeFeatures.isEmpty) return null; + + final allFeatures = { + 'pathLength': pathLength.toDouble(), + 'messageBytes': messageBytes.toDouble(), + 'secSinceRx': secondsSinceLastRx.toDouble(), + 'isFlood': pathLength < 0 ? 1.0 : 0.0, + }; + final row = _activeFeatures.map((f) => allFeatures[f]!).toList(); + + final features = DataFrame( + [row], + headerExists: false, + header: _activeFeatures, + ); + + final prediction = _model!.predict(features); + final rawValue = prediction.rows.first.first; + var predictedMs = (rawValue is double) ? rawValue : (rawValue as num).toDouble(); + + debugPrint( + 'TimeoutPrediction: raw prediction=$predictedMs for ' + 'pathLength=$pathLength, messageBytes=$messageBytes, ' + 'features=$_activeFeatures', + ); + + // Sanity check: if prediction is negative or zero, fall back + if (predictedMs <= 0) return null; + + // Blend with per-contact mean if enough data + if (contactKey != null) { + final stats = _contactStats[contactKey]; + if (stats != null && stats.count >= _minContactObservations) { + predictedMs = 0.5 * predictedMs + 0.5 * stats.mean; + } + } + + final timeout = + (predictedMs * _safetyMargin).ceil().clamp(_minTimeoutMs, _maxTimeoutMs); + debugPrint( + 'TimeoutPrediction: ML timeout ${timeout}ms ' + '(raw: ${predictedMs.round()}ms, contact: $contactKey)', + ); + return timeout; + } catch (e) { + debugPrint('TimeoutPrediction: prediction failed: $e'); + return null; + } + } + + void _trainModel() { + try { + // Build feature columns, then exclude any with zero variance + // (ml_algo's OLS produces all-zero coefficients for singular matrices) + final allNames = ['pathLength', 'messageBytes', 'secSinceRx', 'isFlood']; + final allExtractors = [ + (o) => o.pathLength.toDouble(), + (o) => o.messageBytes.toDouble(), + (o) => o.secondsSinceLastRx.toDouble(), + (o) => o.isFlood ? 1.0 : 0.0, + ]; + + _activeFeatures = []; + for (var i = 0; i < allNames.length; i++) { + final values = _observations.map(allExtractors[i]).toSet(); + if (values.length > 1) _activeFeatures.add(allNames[i]); + } + + if (_activeFeatures.isEmpty) { + debugPrint('TimeoutPrediction: no features with variance, skipping training'); + return; + } + + final header = [..._activeFeatures, 'deliveryMs']; + final rows = _observations.map((o) { + final row = []; + for (var i = 0; i < allNames.length; i++) { + if (_activeFeatures.contains(allNames[i])) { + row.add(allExtractors[i](o)); + } + } + row.add(o.deliveryMs.toDouble()); + return row; + }); + + final data = DataFrame( + [header, ...rows], + headerExists: true, + ); + + _model = LinearRegressor(data, 'deliveryMs'); + _observationsSinceLastTrain = 0; + + // Log training summary with sample predictions + final avgMs = _observations.map((o) => o.deliveryMs).reduce((a, b) => a + b) / + _observations.length; + debugPrint( + 'TimeoutPrediction: trained on ${_observations.length} observations ' + '(avg: ${avgMs.round()}ms, features: $_activeFeatures)', + ); + + final modelJson = jsonEncode(_model!.toJson()); + _storage?.saveTimeoutModel(modelJson); + notifyListeners(); + } catch (e) { + debugPrint('TimeoutPrediction: training failed: $e'); + } + } + + void _rebuildContactStats() { + _contactStats.clear(); + for (final obs in _observations) { + _contactStats.putIfAbsent(obs.contactKey, () => _ContactStats()); + _contactStats[obs.contactKey]!.add(obs.deliveryMs.toDouble()); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 82e4d9c..4831e67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,8 @@ dependencies: material_symbols_icons: ^4.2906.0 web: ^1.1.1 flutter_svg: ^2.0.10+1 + ml_algo: ^16.0.0 + ml_dataframe: ^1.0.0 dev_dependencies: flutter_test: diff --git a/test/services/ml_algo_sanity_test.dart b/test/services/ml_algo_sanity_test.dart new file mode 100644 index 0000000..e4f980e --- /dev/null +++ b/test/services/ml_algo_sanity_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ml_algo/ml_algo.dart'; +import 'package:ml_dataframe/ml_dataframe.dart'; + +void main() { + test('LinearRegressor basic sanity check', () { + // Simple: y = 2x + 100 + final data = DataFrame([ + [1.0, 102.0], + [2.0, 104.0], + [3.0, 106.0], + [4.0, 108.0], + [5.0, 110.0], + [10.0, 120.0], + [20.0, 140.0], + [50.0, 200.0], + [0.0, 100.0], + [100.0, 300.0], + ], headerExists: false, header: ['x', 'y']); + + debugPrint('Training data columns: ${data.header}'); + debugPrint('Training data rows: ${data.rows.length}'); + + final model = LinearRegressor(data, 'y'); + + final testDf = DataFrame( + [[25.0]], + headerExists: false, + header: ['x'], + ); + + final prediction = model.predict(testDf); + final value = prediction.rows.first.first; + debugPrint('Predict x=25 → y=$value (expected ~150)'); + expect((value as num).toDouble(), closeTo(150, 5)); + }); + + test('LinearRegressor multi-feature with constant column produces zeros', () { + // isFlood=0 for all rows → zero-variance column → singular matrix + final data = DataFrame([ + [0.0, 50.0, 14.0, 0.0, 1900.0], + [0.0, 80.0, 14.0, 0.0, 2200.0], + [2.0, 50.0, 14.0, 0.0, 5000.0], + [4.0, 50.0, 14.0, 0.0, 9500.0], + ], headerExists: false, header: [ + 'pathLength', 'messageBytes', 'hourOfDay', 'isFlood', 'deliveryMs', + ]); + + final model = LinearRegressor(data, 'deliveryMs'); + final testDf = DataFrame( + [[2.0, 50.0, 14.0, 0.0]], + headerExists: false, + header: ['pathLength', 'messageBytes', 'hourOfDay', 'isFlood'], + ); + final pred = model.predict(testDf).rows.first.first; + debugPrint('With constant isFlood column: hops=2 → ${(pred as num).round()}ms (likely 0)'); + }); + + test('LinearRegressor 2-feature works correctly', () { + // Just pathLength + messageBytes → deliveryMs + final data = DataFrame([ + [0.0, 50.0, 1900.0], + [0.0, 80.0, 2200.0], + [2.0, 50.0, 5000.0], + [2.0, 80.0, 5500.0], + [4.0, 50.0, 9500.0], + [4.0, 80.0, 10000.0], + [0.0, 30.0, 1800.0], + [2.0, 30.0, 4800.0], + [4.0, 30.0, 9000.0], + [0.0, 60.0, 2000.0], + ], headerExists: false, header: ['pathLength', 'messageBytes', 'deliveryMs']); + + final model = LinearRegressor(data, 'deliveryMs'); + + for (final hops in [0.0, 2.0, 4.0]) { + final testDf = DataFrame( + [[hops, 50.0]], + headerExists: false, + header: ['pathLength', 'messageBytes'], + ); + final pred = model.predict(testDf).rows.first.first; + debugPrint('2-feature: hops=$hops → ${(pred as num).round()}ms'); + } + }); + + test('LinearRegressor multi-feature with variance in all columns', () { + // Mix flood and direct so isFlood has variance + final data = DataFrame([ + [0.0, 50.0, 14.0, 0.0, 1900.0], + [0.0, 80.0, 10.0, 0.0, 2200.0], + [2.0, 50.0, 16.0, 0.0, 5000.0], + [2.0, 80.0, 20.0, 0.0, 5500.0], + [4.0, 50.0, 8.0, 0.0, 9500.0], + [4.0, 80.0, 12.0, 0.0, 10000.0], + [-1.0, 40.0, 14.0, 1.0, 5000.0], + [-1.0, 60.0, 18.0, 1.0, 6500.0], + [-1.0, 30.0, 10.0, 1.0, 4000.0], + [-1.0, 80.0, 22.0, 1.0, 7000.0], + ], headerExists: false, header: [ + 'pathLength', 'messageBytes', 'hourOfDay', 'isFlood', 'deliveryMs', + ]); + + final model = LinearRegressor(data, 'deliveryMs'); + + for (final tc in [ + [0.0, 50.0, 14.0, 0.0], + [2.0, 50.0, 14.0, 0.0], + [4.0, 50.0, 14.0, 0.0], + [-1.0, 50.0, 14.0, 1.0], + ]) { + final testDf = DataFrame( + [tc], + headerExists: false, + header: ['pathLength', 'messageBytes', 'hourOfDay', 'isFlood'], + ); + final pred = model.predict(testDf).rows.first.first; + debugPrint('4-feature: hops=${tc[0]} flood=${tc[3]} → ${(pred as num).round()}ms'); + } + }); +} diff --git a/test/services/timeout_prediction_service_test.dart b/test/services/timeout_prediction_service_test.dart new file mode 100644 index 0000000..46dc5df --- /dev/null +++ b/test/services/timeout_prediction_service_test.dart @@ -0,0 +1,164 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/models/delivery_observation.dart'; +import 'package:meshcore_open/services/timeout_prediction_service.dart'; + +void main() { + late TimeoutPredictionService service; + + setUp(() { + service = TimeoutPredictionService.noStorage(); + }); + + test('trains on sample data and predicts sensible timeouts', () { + // Simulate realistic delivery data: + // Direct 0-hop messages: ~1500-2500ms + // 2-hop messages: ~4000-6000ms + // 4-hop messages: ~8000-12000ms + // Flood messages: ~3000-8000ms + final sampleData = [ + // 0-hop direct + _obs(pathLength: 0, messageBytes: 20, deliveryMs: 1800), + _obs(pathLength: 0, messageBytes: 50, deliveryMs: 2100), + _obs(pathLength: 0, messageBytes: 80, deliveryMs: 2400), + _obs(pathLength: 0, messageBytes: 30, deliveryMs: 1925), + // 2-hop direct + _obs(pathLength: 2, messageBytes: 40, deliveryMs: 4500), + _obs(pathLength: 2, messageBytes: 60, deliveryMs: 5200), + _obs(pathLength: 2, messageBytes: 25, deliveryMs: 4100), + // 4-hop direct + _obs(pathLength: 4, messageBytes: 50, deliveryMs: 9800), + _obs(pathLength: 4, messageBytes: 30, deliveryMs: 8500), + _obs(pathLength: 4, messageBytes: 70, deliveryMs: 10570), + // Flood + _obs(pathLength: -1, messageBytes: 40, deliveryMs: 5000), + _obs(pathLength: -1, messageBytes: 60, deliveryMs: 6500), + ]; + + // Feed all observations + for (final obs in sampleData) { + service.recordObservation( + contactKey: obs.contactKey, + pathLength: obs.pathLength, + messageBytes: obs.messageBytes, + tripTimeMs: obs.deliveryMs, + ); + } + + expect(service.hasModel, isTrue); + expect(service.observationCount, equals(12)); + + // Predict for different scenarios + final direct0 = service.predictTimeout(pathLength: 0, messageBytes: 50); + final direct2 = service.predictTimeout(pathLength: 2, messageBytes: 50); + final direct4 = service.predictTimeout(pathLength: 4, messageBytes: 50); + final flood = service.predictTimeout(pathLength: -1, messageBytes: 50); + + // All should return non-null (model is trained) + expect(direct0, isNotNull); + expect(direct2, isNotNull); + expect(direct4, isNotNull); + expect(flood, isNotNull); + + // More hops should predict longer timeouts + expect(direct4!, greaterThan(direct2!)); + expect(direct2, greaterThan(direct0!)); + + // All should be within the clamp range + expect(direct0, greaterThanOrEqualTo(2000)); + expect(direct4, lessThanOrEqualTo(120000)); + + // Print predictions for visibility + debugPrint('Predictions (with 1.5x safety margin):'); + debugPrint(' 0-hop direct: ${direct0}ms'); + debugPrint(' 2-hop direct: ${direct2}ms'); + debugPrint(' 4-hop direct: ${direct4}ms'); + debugPrint(' flood: ${flood}ms'); + }); + + test('returns null before minimum observations', () { + for (var i = 0; i < TimeoutPredictionService.minObservations - 1; i++) { + service.recordObservation( + contactKey: 'abc', + pathLength: 0, + messageBytes: 50, + tripTimeMs: 2000, + ); + } + + expect(service.hasModel, isFalse); + expect(service.predictTimeout(pathLength: 0, messageBytes: 50), isNull); + }); + + test('caps observations at maxObservations', () { + for (var i = 0; i < TimeoutPredictionService.maxObservations + 20; i++) { + service.recordObservation( + contactKey: 'abc', + pathLength: 0, + messageBytes: 50, + tripTimeMs: 2000 + i, + ); + } + + expect( + service.observationCount, + equals(TimeoutPredictionService.maxObservations), + ); + }); + + test('blends per-contact stats after enough observations', () { + // Train with mixed contacts and varied features: + // contactA is fast (0-hop), contactB is slow (2-hop) + for (var i = 0; i < 12; i++) { + service.recordObservation( + contactKey: 'contactA', + pathLength: 0, + messageBytes: 30 + i, + tripTimeMs: 1500, + ); + service.recordObservation( + contactKey: 'contactB', + pathLength: 2, + messageBytes: 30 + i, + tripTimeMs: 8000, + ); + } + + final predA = service.predictTimeout( + contactKey: 'contactA', + pathLength: 0, + messageBytes: 50, + ); + final predB = service.predictTimeout( + contactKey: 'contactB', + pathLength: 0, + messageBytes: 50, + ); + + expect(predA, isNotNull); + expect(predB, isNotNull); + // Contact B (slow) should have a higher predicted timeout than A (fast) + expect(predB!, greaterThan(predA!)); + + debugPrint('Per-contact blending:'); + debugPrint(' contactA (fast): ${predA}ms'); + debugPrint(' contactB (slow): ${predB}ms'); + }); +} + +DeliveryObservation _obs({ + required int pathLength, + required int messageBytes, + required int deliveryMs, + String contactKey = 'test_contact', +}) { + return DeliveryObservation( + contactKey: contactKey, + pathLength: pathLength, + messageBytes: messageBytes, + secondsSinceLastRx: 5, + isFlood: pathLength < 0, + deliveryMs: deliveryMs, + timestamp: DateTime.now(), + ); +} From b336aedbc58e2646149c071509ac94264744c8b6 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 17:32:08 -0700 Subject: [PATCH 302/421] fix: address PR #296 code review feedback - Clamp ML predictions between physics floor (raw airtime) and ceiling (worst-case formula) so model can never produce unsafe timeouts - Replace hourOfDay feature with secondsSinceLastRx for network activity - Remove unused _ContactStats.stdDev and dead model persistence code - Debounce observation writes (2s) instead of writing on every delivery - Skip recording observations when pathLength is null to avoid corrupting training data - Add comment explaining global (not per-contact) RX time tracking - Remove notifyListeners from retrain to avoid unnecessary widget rebuilds - Run dart format --- lib/connector/meshcore_connector.dart | 107 ++++++++------ lib/services/message_retry_service.dart | 18 ++- lib/services/storage_service.dart | 21 +-- lib/services/timeout_prediction_service.dart | 41 +++--- test/services/ml_algo_sanity_test.dart | 136 +++++++++++------- .../timeout_prediction_service_test.dart | 6 +- 6 files changed, 187 insertions(+), 142 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d05a8f9..33e5c48 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -168,6 +168,8 @@ class MeshCoreConnector extends ChangeNotifier { bool _isLoadingChannels = false; bool _hasLoadedChannels = false; TimeoutPredictionService? _timeoutPredictionService; + // Intentionally global (not per-contact): tracks overall network activity. + // Frequent RX from any source indicates a busy network with more collisions. DateTime _lastRxTime = DateTime.now(); bool _batteryRequested = false; bool _awaitingSelfInfo = false; @@ -694,23 +696,28 @@ class MeshCoreConnector extends ChangeNotifier { updateMessageCallback: _updateMessage, clearContactPathCallback: clearContactPath, setContactPathCallback: setContactPath, - calculateTimeoutCallback: (pathLength, messageBytes, {String? contactKey}) => - calculateTimeout(pathLength: pathLength, messageBytes: messageBytes, contactKey: contactKey), + calculateTimeoutCallback: + (pathLength, messageBytes, {String? contactKey}) => calculateTimeout( + pathLength: pathLength, + messageBytes: messageBytes, + contactKey: contactKey, + ), getSelfPublicKeyCallback: () => _selfPublicKey, prepareContactOutboundTextCallback: prepareContactOutboundText, appSettingsService: appSettingsService, debugLogService: _appDebugLogService, recordPathResultCallback: _recordPathResult, - onDeliveryObservedCallback: (contactKey, pathLength, messageBytes, tripTimeMs) { - final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; - _timeoutPredictionService?.recordObservation( - contactKey: contactKey, - pathLength: pathLength, - messageBytes: messageBytes, - tripTimeMs: tripTimeMs, - secondsSinceLastRx: secSinceRx, - ); - }, + onDeliveryObservedCallback: + (contactKey, pathLength, messageBytes, tripTimeMs) { + final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; + _timeoutPredictionService?.recordObservation( + contactKey: contactKey, + pathLength: pathLength, + messageBytes: messageBytes, + tripTimeMs: tripTimeMs, + secondsSinceLastRx: secSinceRx, + ); + }, ); } @@ -2890,14 +2897,54 @@ class MeshCoreConnector extends ChangeNotifier { } } - /// Calculate timeout for a message based on radio settings and path length - /// Returns timeout in milliseconds, considering number of hops + /// Estimate single-packet airtime in ms from radio settings, or a fallback. + int _estimateAirtimeMs(int messageBytes) { + if (_currentFreqHz != null && + _currentBwHz != null && + _currentSf != null && + _currentCr != null) { + final cr = _currentCr! <= 4 ? _currentCr! : _currentCr! - 4; + return calculateLoRaAirtime( + payloadBytes: messageBytes, + spreadingFactor: _currentSf!, + bandwidthHz: _currentBwHz!, + codingRate: cr, + lowDataRateOptimize: _currentSf! >= 11, + ); + } + return 50; // fallback: ~SF7/BW125 for 100 bytes + } + + /// Physics-based worst-case timeout (ceiling). + int _physicsMaxTimeout(int pathLength, int airtime) { + if (pathLength < 0) { + return 500 + (16 * airtime); + } else { + return 500 + ((airtime * 6 + 250) * (pathLength + 1)); + } + } + + /// Physics-based minimum timeout (floor): raw traversal time. + int _physicsMinTimeout(int pathLength, int airtime) { + if (pathLength < 0) { + return airtime; + } else { + return airtime * (pathLength + 1); + } + } + + /// Calculate timeout for a message based on radio settings and path length. + /// Returns timeout in milliseconds, considering number of hops. int calculateTimeout({ required int pathLength, int messageBytes = 100, String? contactKey, }) { - // Try ML-based prediction first + final airtime = _estimateAirtimeMs(messageBytes); + final physicsMin = _physicsMinTimeout(pathLength, airtime); + final physicsMax = _physicsMaxTimeout(pathLength, airtime); + + // Try ML-based prediction, clamped between physics bounds final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; final mlTimeout = _timeoutPredictionService?.predictTimeout( contactKey: contactKey, @@ -2905,35 +2952,11 @@ class MeshCoreConnector extends ChangeNotifier { messageBytes: messageBytes, secondsSinceLastRx: secSinceRx, ); - if (mlTimeout != null) return mlTimeout; - - // If we have radio settings, use them for accurate calculation - if (_currentFreqHz != null && - _currentBwHz != null && - _currentSf != null && - _currentCr != null) { - final cr = _currentCr! <= 4 ? _currentCr! : _currentCr! - 4; - return calculateMessageTimeout( - freqHz: _currentFreqHz!, - bwHz: _currentBwHz!, - sf: _currentSf!, - cr: cr, - pathLength: pathLength, - messageBytes: messageBytes, - ); + if (mlTimeout != null) { + return mlTimeout.clamp(physicsMin, physicsMax); } - // Fallback: Conservative estimates based on typical settings - // Assume SF7, BW125, which gives ~50ms airtime for 100 bytes - const estimatedAirtime = 50; - - if (pathLength < 0) { - // Flood mode: Base delay + 16× airtime - return 500 + (16 * estimatedAirtime); - } else { - // Direct path: Base delay + ((airtime×6 + 250ms)×(hops+1)) - return 500 + ((estimatedAirtime * 6 + 250) * (pathLength + 1)); - } + return physicsMax; } void _handleContact(Uint8List frame, {bool isContact = true}) { diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index d94b763..b66ba51 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -74,14 +74,20 @@ class MessageRetryService extends ChangeNotifier { required Function(Message) updateMessageCallback, Function(Contact)? clearContactPathCallback, Function(Contact, Uint8List, int)? setContactPathCallback, - Function(int pathLength, int messageBytes, {String? contactKey})? calculateTimeoutCallback, + Function(int pathLength, int messageBytes, {String? contactKey})? + calculateTimeoutCallback, Uint8List? Function()? getSelfPublicKeyCallback, String Function(Contact, String)? prepareContactOutboundTextCallback, AppSettingsService? appSettingsService, AppDebugLogService? debugLogService, Function(String, PathSelection, bool, int?)? recordPathResultCallback, - Function(String contactKey, int pathLength, int messageBytes, int tripTimeMs)? - onDeliveryObservedCallback, + Function( + String contactKey, + int pathLength, + int messageBytes, + int tripTimeMs, + )? + onDeliveryObservedCallback, }) { _sendMessageCallback = sendMessageCallback; _addMessageCallback = addMessageCallback; @@ -750,10 +756,12 @@ class MessageRetryService extends ChangeNotifier { true, tripTimeMs, ); - if (_onDeliveryObservedCallback != null && tripTimeMs > 0) { + if (_onDeliveryObservedCallback != null && + tripTimeMs > 0 && + message.pathLength != null) { _onDeliveryObservedCallback!( contact.publicKeyHex, - message.pathLength ?? 0, + message.pathLength!, message.text.length, tripTimeMs, ); diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index c591f64..a86c1f6 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -8,7 +8,6 @@ class StorageService { static const String _pendingMessagesKey = 'pending_messages'; static const String _repeaterPasswordsKey = 'repeater_passwords'; static const String _deliveryObservationsKey = 'delivery_observations'; - static const String _timeoutModelKey = 'timeout_ml_model'; Future savePathHistory( String contactPubKeyHex, @@ -143,10 +142,7 @@ class StorageService { try { final list = jsonDecode(jsonStr) as List; return list - .map( - (e) => - DeliveryObservation.fromJson(e as Map), - ) + .map((e) => DeliveryObservation.fromJson(e as Map)) .toList(); } catch (e) { return []; @@ -157,19 +153,4 @@ class StorageService { final prefs = PrefsManager.instance; await prefs.remove(_deliveryObservationsKey); } - - Future saveTimeoutModel(String modelJson) async { - final prefs = PrefsManager.instance; - await prefs.setString(_timeoutModelKey, modelJson); - } - - Future loadTimeoutModel() async { - final prefs = PrefsManager.instance; - return prefs.getString(_timeoutModelKey); - } - - Future clearTimeoutModel() async { - final prefs = PrefsManager.instance; - await prefs.remove(_timeoutModelKey); - } } diff --git a/lib/services/timeout_prediction_service.dart b/lib/services/timeout_prediction_service.dart index 21e229e..1f3d6dd 100644 --- a/lib/services/timeout_prediction_service.dart +++ b/lib/services/timeout_prediction_service.dart @@ -1,5 +1,4 @@ -import 'dart:convert'; -import 'dart:math'; +import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:ml_algo/ml_algo.dart'; import 'package:ml_dataframe/ml_dataframe.dart'; @@ -9,16 +8,13 @@ import 'storage_service.dart'; class _ContactStats { int count = 0; double _sum = 0; - double _sumSq = 0; void add(double ms) { count++; _sum += ms; - _sumSq += ms * ms; } double get mean => _sum / count; - double get stdDev => sqrt((_sumSq / count) - (mean * mean)); } class TimeoutPredictionService extends ChangeNotifier { @@ -27,9 +23,10 @@ class TimeoutPredictionService extends ChangeNotifier { static const int minObservations = 10; static const int maxObservations = 100; static const int _retrainInterval = 5; + // 1.5x multiplier on raw prediction to account for variance in delivery + // times — tight enough to improve on worst-case physics, loose enough + // to avoid premature timeouts from model noise. static const double _safetyMargin = 1.5; - static const int _minTimeoutMs = 2000; - static const int _maxTimeoutMs = 120000; static const int _minContactObservations = 10; List _observations = []; @@ -37,6 +34,7 @@ class TimeoutPredictionService extends ChangeNotifier { List _activeFeatures = []; int _observationsSinceLastTrain = 0; final Map _contactStats = {}; + Timer? _persistTimer; TimeoutPredictionService(StorageService storage) : _storage = storage; TimeoutPredictionService.noStorage() : _storage = null; @@ -89,7 +87,10 @@ class TimeoutPredictionService extends ChangeNotifier { _trainModel(); } - _storage?.saveDeliveryObservations(_observations); + _persistTimer?.cancel(); + _persistTimer = Timer(const Duration(seconds: 2), () { + _storage?.saveDeliveryObservations(_observations); + }); debugPrint( 'TimeoutPrediction: recorded ${tripTimeMs}ms for $pathLength hops ' '(${_observations.length} total)', @@ -123,7 +124,9 @@ class TimeoutPredictionService extends ChangeNotifier { final prediction = _model!.predict(features); final rawValue = prediction.rows.first.first; - var predictedMs = (rawValue is double) ? rawValue : (rawValue as num).toDouble(); + var predictedMs = (rawValue is double) + ? rawValue + : (rawValue as num).toDouble(); debugPrint( 'TimeoutPrediction: raw prediction=$predictedMs for ' @@ -142,8 +145,8 @@ class TimeoutPredictionService extends ChangeNotifier { } } - final timeout = - (predictedMs * _safetyMargin).ceil().clamp(_minTimeoutMs, _maxTimeoutMs); + // Connector clamps this between physics min/max bounds + final timeout = (predictedMs * _safetyMargin).ceil(); debugPrint( 'TimeoutPrediction: ML timeout ${timeout}ms ' '(raw: ${predictedMs.round()}ms, contact: $contactKey)', @@ -174,7 +177,9 @@ class TimeoutPredictionService extends ChangeNotifier { } if (_activeFeatures.isEmpty) { - debugPrint('TimeoutPrediction: no features with variance, skipping training'); + debugPrint( + 'TimeoutPrediction: no features with variance, skipping training', + ); return; } @@ -190,25 +195,19 @@ class TimeoutPredictionService extends ChangeNotifier { return row; }); - final data = DataFrame( - [header, ...rows], - headerExists: true, - ); + final data = DataFrame([header, ...rows], headerExists: true); _model = LinearRegressor(data, 'deliveryMs'); _observationsSinceLastTrain = 0; // Log training summary with sample predictions - final avgMs = _observations.map((o) => o.deliveryMs).reduce((a, b) => a + b) / + final avgMs = + _observations.map((o) => o.deliveryMs).reduce((a, b) => a + b) / _observations.length; debugPrint( 'TimeoutPrediction: trained on ${_observations.length} observations ' '(avg: ${avgMs.round()}ms, features: $_activeFeatures)', ); - - final modelJson = jsonEncode(_model!.toJson()); - _storage?.saveTimeoutModel(modelJson); - notifyListeners(); } catch (e) { debugPrint('TimeoutPrediction: training failed: $e'); } diff --git a/test/services/ml_algo_sanity_test.dart b/test/services/ml_algo_sanity_test.dart index e4f980e..427a8a6 100644 --- a/test/services/ml_algo_sanity_test.dart +++ b/test/services/ml_algo_sanity_test.dart @@ -6,18 +6,22 @@ import 'package:ml_dataframe/ml_dataframe.dart'; void main() { test('LinearRegressor basic sanity check', () { // Simple: y = 2x + 100 - final data = DataFrame([ - [1.0, 102.0], - [2.0, 104.0], - [3.0, 106.0], - [4.0, 108.0], - [5.0, 110.0], - [10.0, 120.0], - [20.0, 140.0], - [50.0, 200.0], - [0.0, 100.0], - [100.0, 300.0], - ], headerExists: false, header: ['x', 'y']); + final data = DataFrame( + [ + [1.0, 102.0], + [2.0, 104.0], + [3.0, 106.0], + [4.0, 108.0], + [5.0, 110.0], + [10.0, 120.0], + [20.0, 140.0], + [50.0, 200.0], + [0.0, 100.0], + [100.0, 300.0], + ], + headerExists: false, + header: ['x', 'y'], + ); debugPrint('Training data columns: ${data.header}'); debugPrint('Training data rows: ${data.rows.length}'); @@ -25,7 +29,9 @@ void main() { final model = LinearRegressor(data, 'y'); final testDf = DataFrame( - [[25.0]], + [ + [25.0], + ], headerExists: false, header: ['x'], ); @@ -38,45 +44,63 @@ void main() { test('LinearRegressor multi-feature with constant column produces zeros', () { // isFlood=0 for all rows → zero-variance column → singular matrix - final data = DataFrame([ - [0.0, 50.0, 14.0, 0.0, 1900.0], - [0.0, 80.0, 14.0, 0.0, 2200.0], - [2.0, 50.0, 14.0, 0.0, 5000.0], - [4.0, 50.0, 14.0, 0.0, 9500.0], - ], headerExists: false, header: [ - 'pathLength', 'messageBytes', 'hourOfDay', 'isFlood', 'deliveryMs', - ]); + final data = DataFrame( + [ + [0.0, 50.0, 14.0, 0.0, 1900.0], + [0.0, 80.0, 14.0, 0.0, 2200.0], + [2.0, 50.0, 14.0, 0.0, 5000.0], + [4.0, 50.0, 14.0, 0.0, 9500.0], + ], + headerExists: false, + header: [ + 'pathLength', + 'messageBytes', + 'hourOfDay', + 'isFlood', + 'deliveryMs', + ], + ); final model = LinearRegressor(data, 'deliveryMs'); final testDf = DataFrame( - [[2.0, 50.0, 14.0, 0.0]], + [ + [2.0, 50.0, 14.0, 0.0], + ], headerExists: false, header: ['pathLength', 'messageBytes', 'hourOfDay', 'isFlood'], ); final pred = model.predict(testDf).rows.first.first; - debugPrint('With constant isFlood column: hops=2 → ${(pred as num).round()}ms (likely 0)'); + debugPrint( + 'With constant isFlood column: hops=2 → ${(pred as num).round()}ms (likely 0)', + ); }); test('LinearRegressor 2-feature works correctly', () { // Just pathLength + messageBytes → deliveryMs - final data = DataFrame([ - [0.0, 50.0, 1900.0], - [0.0, 80.0, 2200.0], - [2.0, 50.0, 5000.0], - [2.0, 80.0, 5500.0], - [4.0, 50.0, 9500.0], - [4.0, 80.0, 10000.0], - [0.0, 30.0, 1800.0], - [2.0, 30.0, 4800.0], - [4.0, 30.0, 9000.0], - [0.0, 60.0, 2000.0], - ], headerExists: false, header: ['pathLength', 'messageBytes', 'deliveryMs']); + final data = DataFrame( + [ + [0.0, 50.0, 1900.0], + [0.0, 80.0, 2200.0], + [2.0, 50.0, 5000.0], + [2.0, 80.0, 5500.0], + [4.0, 50.0, 9500.0], + [4.0, 80.0, 10000.0], + [0.0, 30.0, 1800.0], + [2.0, 30.0, 4800.0], + [4.0, 30.0, 9000.0], + [0.0, 60.0, 2000.0], + ], + headerExists: false, + header: ['pathLength', 'messageBytes', 'deliveryMs'], + ); final model = LinearRegressor(data, 'deliveryMs'); for (final hops in [0.0, 2.0, 4.0]) { final testDf = DataFrame( - [[hops, 50.0]], + [ + [hops, 50.0], + ], headerExists: false, header: ['pathLength', 'messageBytes'], ); @@ -87,20 +111,28 @@ void main() { test('LinearRegressor multi-feature with variance in all columns', () { // Mix flood and direct so isFlood has variance - final data = DataFrame([ - [0.0, 50.0, 14.0, 0.0, 1900.0], - [0.0, 80.0, 10.0, 0.0, 2200.0], - [2.0, 50.0, 16.0, 0.0, 5000.0], - [2.0, 80.0, 20.0, 0.0, 5500.0], - [4.0, 50.0, 8.0, 0.0, 9500.0], - [4.0, 80.0, 12.0, 0.0, 10000.0], - [-1.0, 40.0, 14.0, 1.0, 5000.0], - [-1.0, 60.0, 18.0, 1.0, 6500.0], - [-1.0, 30.0, 10.0, 1.0, 4000.0], - [-1.0, 80.0, 22.0, 1.0, 7000.0], - ], headerExists: false, header: [ - 'pathLength', 'messageBytes', 'hourOfDay', 'isFlood', 'deliveryMs', - ]); + final data = DataFrame( + [ + [0.0, 50.0, 14.0, 0.0, 1900.0], + [0.0, 80.0, 10.0, 0.0, 2200.0], + [2.0, 50.0, 16.0, 0.0, 5000.0], + [2.0, 80.0, 20.0, 0.0, 5500.0], + [4.0, 50.0, 8.0, 0.0, 9500.0], + [4.0, 80.0, 12.0, 0.0, 10000.0], + [-1.0, 40.0, 14.0, 1.0, 5000.0], + [-1.0, 60.0, 18.0, 1.0, 6500.0], + [-1.0, 30.0, 10.0, 1.0, 4000.0], + [-1.0, 80.0, 22.0, 1.0, 7000.0], + ], + headerExists: false, + header: [ + 'pathLength', + 'messageBytes', + 'hourOfDay', + 'isFlood', + 'deliveryMs', + ], + ); final model = LinearRegressor(data, 'deliveryMs'); @@ -116,7 +148,9 @@ void main() { header: ['pathLength', 'messageBytes', 'hourOfDay', 'isFlood'], ); final pred = model.predict(testDf).rows.first.first; - debugPrint('4-feature: hops=${tc[0]} flood=${tc[3]} → ${(pred as num).round()}ms'); + debugPrint( + '4-feature: hops=${tc[0]} flood=${tc[3]} → ${(pred as num).round()}ms', + ); } }); } diff --git a/test/services/timeout_prediction_service_test.dart b/test/services/timeout_prediction_service_test.dart index 46dc5df..dbd852d 100644 --- a/test/services/timeout_prediction_service_test.dart +++ b/test/services/timeout_prediction_service_test.dart @@ -64,9 +64,9 @@ void main() { expect(direct4!, greaterThan(direct2!)); expect(direct2, greaterThan(direct0!)); - // All should be within the clamp range - expect(direct0, greaterThanOrEqualTo(2000)); - expect(direct4, lessThanOrEqualTo(120000)); + // All should be positive + expect(direct0, greaterThan(0)); + expect(direct4, greaterThan(0)); // Print predictions for visibility debugPrint('Predictions (with 1.5x safety margin):'); From fffcff3b74896e9fe0dd64d02756fd94d7565062 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 17:39:01 -0700 Subject: [PATCH 303/421] fix: cancel persist timer on dispose to prevent post-dispose writes --- lib/services/timeout_prediction_service.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/services/timeout_prediction_service.dart b/lib/services/timeout_prediction_service.dart index 1f3d6dd..d92ca64 100644 --- a/lib/services/timeout_prediction_service.dart +++ b/lib/services/timeout_prediction_service.dart @@ -213,6 +213,12 @@ class TimeoutPredictionService extends ChangeNotifier { } } + @override + void dispose() { + _persistTimer?.cancel(); + super.dispose(); + } + void _rebuildContactStats() { _contactStats.clear(); for (final obs in _observations) { From 06a906f4f71ec8e33aaebb26ee7242a7b9ff1039 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Mar 2026 17:51:24 -0700 Subject: [PATCH 304/421] Enhance location handling and improve path trace functionality across screens --- lib/connector/meshcore_connector.dart | 16 +++- lib/models/contact.dart | 54 +++-------- lib/screens/channel_message_path_screen.dart | 5 +- lib/screens/chat_screen.dart | 2 +- lib/screens/contacts_screen.dart | 17 ++-- lib/screens/map_screen.dart | 11 +-- lib/screens/path_trace_map.dart | 99 +++++++++++++++----- lib/services/app_debug_log_service.dart | 17 ++-- lib/utils/app_logger.dart | 15 +-- lib/widgets/path_management_dialog.dart | 2 +- 10 files changed, 138 insertions(+), 100 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index dad5ed1..86484d8 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4753,8 +4753,20 @@ class MeshCoreConnector extends ChangeNotifier { // CRITICAL: Preserve user's path override when contact is refreshed from device _contacts[existingIndex] = existing.copyWith( - latitude: hasLocation ? latitude : existing.latitude, - longitude: hasLocation ? longitude : existing.longitude, + latitude: + hasLocation && + latitude != null && + latitude.abs() <= 90 && + longitude != 0 + ? latitude + : existing.latitude, + longitude: + hasLocation && + longitude != null && + longitude.abs() <= 180 && + longitude != 0 + ? longitude + : existing.longitude, name: hasName ? name : existing.name, path: Uint8List.fromList(path.reversed.toList()), pathLength: path.length, diff --git a/lib/models/contact.dart b/lib/models/contact.dart index cab58cb..858d712 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -1,4 +1,5 @@ import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import '../connector/meshcore_protocol.dart'; @@ -65,7 +66,17 @@ class Contact { return '$pathLength hops'; } - bool get hasLocation => latitude != null && longitude != null; + bool get hasLocation { + const double epsilon = 1e-6; + final lat = latitude ?? 0.0; + final lon = longitude ?? 0.0; + return (lat.abs() > epsilon || lon.abs() > epsilon) && + lat >= -90.0 && + lat <= 90.0 && + lon >= -180.0 && + lon <= 180.0; + } + bool get isFavorite => (flags & contactFlagFavorite) != 0; Contact copyWith({ @@ -108,7 +119,7 @@ class Contact { } String get pathIdList { - final pathBytes = _pathBytesForDisplay; + final pathBytes = pathBytesForDisplay; if (pathBytes.isEmpty) return ''; final parts = []; final groupSize = pathHashSize; @@ -130,43 +141,7 @@ class Contact { return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; } - Uint8List? get traceRouteBytes { - final pathBytes = _pathBytesForDisplay; - Uint8List? traceBytes; - - if (pathBytes.isEmpty) { - traceBytes = Uint8List(1); - traceBytes[0] = publicKey[0]; - return traceBytes; - } - - if (type == advTypeRepeater || type == advTypeRoom) { - final len = (pathBytes.length + pathBytes.length + 1); - traceBytes = Uint8List(len); - traceBytes[pathBytes.length] = publicKey[0]; - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length) { - traceBytes[len - 1 - i] = pathBytes[i]; - } - } - } else { - if (pathBytes.length < 2) { - return pathBytes[0] == 0 ? null : pathBytes; - } - final len = (pathBytes.length + pathBytes.length - 1); - traceBytes = Uint8List(len); - for (int i = 0; i < pathBytes.length; i++) { - traceBytes[i] = pathBytes[i]; - if (i < pathBytes.length - 1) { - traceBytes[len - 1 - i] = pathBytes[i]; - } - } - } - return traceBytes; - } - - Uint8List get _pathBytesForDisplay { + Uint8List get pathBytesForDisplay { if (pathOverride != null) { if (pathOverride! < 0) return Uint8List(0); return pathOverrideBytes ?? Uint8List(0); @@ -197,6 +172,7 @@ class Contact { double? lat, lon; final latRaw = reader.readInt32LE(); final lonRaw = reader.readInt32LE(); + if (latRaw != 0 || lonRaw != 0) { lat = latRaw / 1e6; lon = lonRaw / 1e6; diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 32eadef..747c2bf 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -62,8 +62,9 @@ class ChannelMessagePathScreen extends StatelessWidget { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: primaryPath, - flipPathRound: true, - reversePathRound: !message.isOutgoing && !channelMessage, + flipPathAround: true, + reversePathAround: + !(!channelMessage && !message.isOutgoing), ), ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 6558ecd..5209b41 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -858,7 +858,7 @@ class _ChatScreenState extends State { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), - flipPathRound: true, + flipPathAround: true, targetContact: widget.contact, ), ), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 243c8c4..ed2e171 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1064,7 +1064,7 @@ class _ContactsScreenState extends State if (isRepeater) ...[ ListTile( leading: const Icon(Icons.radar, color: Colors.green), - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? Text(context.l10n.contacts_pathTrace) : Text(context.l10n.contacts_ping), onTap: () { @@ -1072,10 +1072,12 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_repeaterPathTrace : context.l10n.contacts_repeaterPing, - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: true, + targetContact: contact, ), ), ); @@ -1100,10 +1102,12 @@ class _ContactsScreenState extends State context, MaterialPageRoute( builder: (context) => PathTraceMapScreen( - title: contact.pathLength > 0 + title: contact.pathBytesForDisplay.isNotEmpty ? context.l10n.contacts_roomPathTrace : context.l10n.contacts_roomPing, - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: contact.pathBytesForDisplay.isNotEmpty, + targetContact: contact, ), ), ); @@ -1145,7 +1149,8 @@ class _ContactsScreenState extends State title: context.l10n.contacts_pathTraceTo( contact.name, ), - path: contact.traceRouteBytes ?? Uint8List(0), + path: contact.pathBytesForDisplay, + flipPathAround: true, targetContact: contact, ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 497c05f..df16a59 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -176,20 +176,13 @@ class _MapScreenState extends State { // Filter by location final contactsWithLocation = filteredByKeyPrefix.where((c) { - if (!c.hasLocation) { - return false; - } - return _checkLocationPlausibility(c.latitude!, c.longitude!); + return c.hasLocation; }).toList(); // All contacts with a known location — used as anchors regardless of // time/key-prefix filters so that repeaters are always available. final allContactsWithLocation = allContacts - .where( - (c) => - c.hasLocation && - _checkLocationPlausibility(c.latitude!, c.longitude!), - ) + .where((c) => c.hasLocation) .toList(); // Compute guessed locations with caching diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 6277886..d50a185 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -52,8 +52,8 @@ class PathTraceMapScreen extends StatefulWidget { final String title; final Uint8List path; final int? repeaterId; - final bool flipPathRound; - final bool reversePathRound; + final bool flipPathAround; + final bool reversePathAround; final Contact? targetContact; const PathTraceMapScreen({ @@ -61,8 +61,8 @@ class PathTraceMapScreen extends StatefulWidget { required this.title, required this.path, this.repeaterId, - this.flipPathRound = false, - this.reversePathRound = false, + this.flipPathAround = false, + this.reversePathAround = false, this.targetContact, }); @@ -93,6 +93,7 @@ class _PathTraceMapScreenState extends State { ValueKey _mapKey = const ValueKey('initial'); double _pathDistanceMeters = 0.0; bool _showNodeLabels = true; + Contact? target; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -158,21 +159,16 @@ class _PathTraceMapScreenState extends State { }); } - final Uint8List path; - - Uint8List pathTmp = widget.reversePathRound + final pathTmp = widget.reversePathAround ? Uint8List.fromList(widget.path.reversed.toList()) : widget.path; - if (widget.flipPathRound) { - path = buildPath(pathTmp); - } else { - path = pathTmp; - } + final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; appLogger.info( 'Initiating path trace with path: ${_formatPathPrefixes(path)}', tag: 'PathTraceMapScreen', + noNotify: !mounted, ); final connector = Provider.of(context, listen: false); @@ -309,18 +305,20 @@ class _PathTraceMapScreenState extends State { // Compute endpoint position for the target contact. LatLng? targetPos; bool targetGuessed = false; - final target = widget.targetContact; + target = widget.targetContact; + if (target != null) { - if (target.hasLocation) { - targetPos = LatLng(target.latitude!, target.longitude!); - } else if (pathData.isNotEmpty) { + if (target?.hasLocation ?? false) { + targetPos = LatLng(target!.latitude!, target!.longitude!); + } else if (widget.path.length > 1) { // Infer from the last hop: average GPS contacts sharing that hop. // For a round-trip path (flipPathRound), the target-side hop sits // in the middle of the symmetric sequence; .last is the local side. - final lastHop = (widget.flipPathRound && pathData.length > 1) - ? pathData[(pathData.length - 1) ~/ 2] - : pathData.last; - final peers = connector.contacts + final lastHop = widget.reversePathAround + ? widget.path.first + : widget.path.last; + + final peers = connector.allContacts .where( (c) => c.hasLocation && @@ -336,12 +334,35 @@ class _PathTraceMapScreenState extends State { peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length; const offsetDeg = 0.003; - final angle = (target.publicKey[1] / 255.0) * 2 * pi; + final angle = (target!.publicKey[1] / 255.0) * 2 * pi; targetPos = LatLng( lat + offsetDeg * cos(angle), lon + offsetDeg * sin(angle), ); targetGuessed = true; + } else if (inferredPositions.containsKey(lastHop)) { + final lat = inferredPositions[lastHop]!.latitude; + final lon = inferredPositions[lastHop]!.longitude; + const offsetDeg = 0.003; + final angle = (target!.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + lat + offsetDeg * cos(angle), + lon + offsetDeg * sin(angle), + ); + targetGuessed = true; + } else { + // As a last resort, just place it at the same position as the last hop. + final contact = pathContacts[lastHop]; + if (contact != null && contact.hasLocation) { + const offsetDeg = 0.003; + final angle = (target!.publicKey[1] / 255.0) * 2 * pi; + targetPos = LatLng( + contact.latitude! + offsetDeg * cos(angle), + contact.longitude! + offsetDeg * sin(angle), + ); + targetGuessed = true; + targetGuessed = true; + } } } } @@ -350,7 +371,12 @@ class _PathTraceMapScreenState extends State { _points = []; _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + int hopLast = 0; + int hopLastLast = 0; for (final hop in _traceData!.pathData) { + if (hop == hopLastLast && widget.flipPathAround) { + break; //skip duplicate hops in round-trip paths + } final contact = _traceData!.pathContacts[hop]; if (contact != null && contact.hasLocation) { _points.add(LatLng(contact.latitude!, contact.longitude!)); @@ -358,8 +384,14 @@ class _PathTraceMapScreenState extends State { final inferred = inferredPositions[hop]; if (inferred != null) _points.add(inferred); } + hopLastLast = hopLast; + hopLast = hop; + } + if (targetPos != null) { + if (target != null && target!.type == advTypeChat) { + _points.add(targetPos); + } } - if (targetPos != null) _points.add(targetPos); _polylines = _points.length > 1 ? [ Polyline( @@ -448,7 +480,7 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData) _buildMapPathTrace(context, tileCache), + if (_hasData) _buildMapPathTrace(context, tileCache, target), if (_points.isEmpty && !_hasData && !_isLoading && @@ -477,17 +509,28 @@ class _PathTraceMapScreenState extends State { List _buildHopMarkers( List pathData, { required bool showLabels, + required Contact? target, }) { final markers = []; + int hopLast = 0; + int hopLastLast = 0; for (final hop in pathData) { final contact = _traceData!.pathContacts[hop]; final inferred = _inferredHopPositions[hop]; final hasGps = contact != null && contact.hasLocation; - if (!hasGps && inferred == null) continue; + if (hop == hopLastLast && widget.flipPathAround) { + continue; //skip duplicate hops in round-trip paths + } + if (!hasGps && inferred == null) { + hopLastLast = hopLast; + hopLast = hop; + continue; //skip hops with no GPS and no inferred position + } final point = hasGps ? LatLng(contact.latitude!, contact.longitude!) : inferred!; final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); + markers.add( Marker( point: point, @@ -529,6 +572,8 @@ class _PathTraceMapScreenState extends State { ), ); } + hopLastLast = hopLast; + hopLast = hop; } final selfLat = context.read().selfLatitude; @@ -578,9 +623,9 @@ class _PathTraceMapScreenState extends State { // Add target contact endpoint marker. final targetPos = _targetContactPosition; - if (targetPos != null) { + if (targetPos != null && target != null && target.type == advTypeChat) { final isGuessed = _targetContactIsGuessed; - final targetName = widget.targetContact?.name ?? '?'; + final targetName = target.name; markers.add( Marker( point: targetPos, @@ -716,6 +761,7 @@ class _PathTraceMapScreenState extends State { Widget _buildMapPathTrace( BuildContext context, MapTileCacheService tileCache, + Contact? target, ) { return FlutterMap( key: _mapKey, @@ -754,6 +800,7 @@ class _PathTraceMapScreenState extends State { markers: _buildHopMarkers( _traceData!.pathData, showLabels: _showNodeLabels, + target: target, ), ), ], diff --git a/lib/services/app_debug_log_service.dart b/lib/services/app_debug_log_service.dart index c63e625..d31c3e5 100644 --- a/lib/services/app_debug_log_service.dart +++ b/lib/services/app_debug_log_service.dart @@ -51,6 +51,7 @@ class AppDebugLogService extends ChangeNotifier { String message, { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, + bool noNotify = false, }) { if (!_enabled && !kDebugMode) return; if (!_enabled) { @@ -72,22 +73,24 @@ class AppDebugLogService extends ChangeNotifier { _entries.removeRange(0, _entries.length - maxEntries); } - notifyListeners(); + if (!noNotify) { + notifyListeners(); + } // Also print to console for development debugPrint('[$tag] $message'); } - void info(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.info); + void info(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.info, noNotify: noNotify); } - void warn(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.warning); + void warn(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.warning, noNotify: noNotify); } - void error(String message, {String tag = 'App'}) { - log(message, tag: tag, level: AppDebugLogLevel.error); + void error(String message, {String tag = 'App', bool noNotify = false}) { + log(message, tag: tag, level: AppDebugLogLevel.error, noNotify: noNotify); } void clear() { diff --git a/lib/utils/app_logger.dart b/lib/utils/app_logger.dart index e57261e..1f34a5e 100644 --- a/lib/utils/app_logger.dart +++ b/lib/utils/app_logger.dart @@ -23,23 +23,23 @@ class AppLogger { bool get isEnabled => _enabled; /// Log an info message - void info(String message, {String tag = 'App'}) { + void info(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.info(message, tag: tag); + _service!.info(message, tag: tag, noNotify: noNotify); } } /// Log a warning message - void warn(String message, {String tag = 'App'}) { + void warn(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.warn(message, tag: tag); + _service!.warn(message, tag: tag, noNotify: noNotify); } } /// Log an error message - void error(String message, {String tag = 'App'}) { + void error(String message, {String tag = 'App', bool noNotify = false}) { if (_enabled && _service != null) { - _service!.error(message, tag: tag); + _service!.error(message, tag: tag, noNotify: noNotify); } } @@ -48,9 +48,10 @@ class AppLogger { String message, { String tag = 'App', AppDebugLogLevel level = AppDebugLogLevel.info, + bool noNotify = false, }) { if (_enabled && _service != null) { - _service!.log(message, tag: tag, level: level); + _service!.log(message, tag: tag, level: level, noNotify: noNotify); } } } diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 0233b43..861241b 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -78,7 +78,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, path: Uint8List.fromList(pathBytes), - flipPathRound: true, + flipPathAround: true, targetContact: widget.contact, ), ), From 566e3aadf83d9124007014617cc5e36e8679ee50 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 17:59:48 -0700 Subject: [PATCH 305/421] fix: migrate filter menus to type-safe generics and harden popup dismissal - Move ContactSortOption/ContactTypeFilter enums to dedicated contact_filter_types.dart (re-exported from contact_search.dart) - Migrate ContactsFilterMenu and DiscoveryContactsFilterMenu to use sealed class action types with SortFilterMenu generics, replacing int action constants and switch statements - Guard _closeDropdownAndRun with ModalRoute.isCurrent check to prevent accidental dismissal of parent routes Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/screens/contacts_screen.dart | 7 +- lib/utils/contact_filter_types.dart | 3 + lib/utils/contact_search.dart | 4 +- lib/widgets/list_filter_widget.dart | 130 ++++++++++++---------------- 4 files changed, 66 insertions(+), 78 deletions(-) create mode 100644 lib/utils/contact_filter_types.dart diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index abb29fa..a6739e1 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -454,8 +454,11 @@ class _ContactsScreenState extends State } } - void _closeDropdownAndRun(BuildContext context, VoidCallback action) { - Navigator.of(context).pop(); + void _closeDropdownAndRun(BuildContext popupContext, VoidCallback action) { + final route = ModalRoute.of(popupContext); + if (route != null && route.isCurrent) { + Navigator.of(popupContext).pop(); + } WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; action(); diff --git a/lib/utils/contact_filter_types.dart b/lib/utils/contact_filter_types.dart new file mode 100644 index 0000000..08e07c2 --- /dev/null +++ b/lib/utils/contact_filter_types.dart @@ -0,0 +1,3 @@ +enum ContactSortOption { lastSeen, recentMessages, name } + +enum ContactTypeFilter { all, favorites, users, repeaters, rooms } diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 849172a..7a82c53 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,8 +1,6 @@ import '../models/contact.dart'; -enum ContactSortOption { lastSeen, recentMessages, name } - -enum ContactTypeFilter { all, favorites, users, repeaters, rooms } +export 'contact_filter_types.dart'; bool matchesContactQuery(Contact contact, String query) { final normalizedQuery = query.trim().toLowerCase(); diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index 8b2874b..c4fd5aa 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -87,15 +87,23 @@ class SortFilterMenu extends StatelessWidget { } } -const int _actionSortRecentMessages = 1; -const int _actionSortName = 2; -const int _actionSortLastSeen = 3; -const int _actionFilterAll = 4; -const int _actionFilterFavorites = 5; -const int _actionFilterUsers = 6; -const int _actionFilterRepeaters = 7; -const int _actionFilterRooms = 8; -const int _actionToggleUnreadOnly = 9; +sealed class _ContactsFilterAction { + const _ContactsFilterAction(); +} + +class _SortAction extends _ContactsFilterAction { + final ContactSortOption option; + const _SortAction(this.option); +} + +class _TypeFilterAction extends _ContactsFilterAction { + final ContactTypeFilter filter; + const _TypeFilterAction(this.filter); +} + +class _ToggleUnreadAction extends _ContactsFilterAction { + const _ToggleUnreadAction(); +} class ContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; @@ -118,24 +126,24 @@ class ContactsFilterMenu extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - return SortFilterMenu( + return SortFilterMenu<_ContactsFilterAction>( tooltip: l10n.listFilter_tooltip, sections: [ SortFilterMenuSection( title: l10n.listFilter_sortBy, options: [ SortFilterMenuOption( - value: _actionSortRecentMessages, + value: _SortAction(ContactSortOption.recentMessages), label: l10n.listFilter_latestMessages, checked: sortOption == ContactSortOption.recentMessages, ), SortFilterMenuOption( - value: _actionSortLastSeen, + value: _SortAction(ContactSortOption.lastSeen), label: l10n.listFilter_heardRecently, checked: sortOption == ContactSortOption.lastSeen, ), SortFilterMenuOption( - value: _actionSortName, + value: _SortAction(ContactSortOption.name), label: l10n.listFilter_az, checked: sortOption == ContactSortOption.name, ), @@ -145,32 +153,32 @@ class ContactsFilterMenu extends StatelessWidget { title: l10n.listFilter_filters, options: [ SortFilterMenuOption( - value: _actionFilterAll, + value: _TypeFilterAction(ContactTypeFilter.all), label: l10n.listFilter_all, checked: typeFilter == ContactTypeFilter.all, ), SortFilterMenuOption( - value: _actionFilterFavorites, + value: _TypeFilterAction(ContactTypeFilter.favorites), label: l10n.listFilter_favorites, checked: typeFilter == ContactTypeFilter.favorites, ), SortFilterMenuOption( - value: _actionFilterUsers, + value: _TypeFilterAction(ContactTypeFilter.users), label: l10n.listFilter_users, checked: typeFilter == ContactTypeFilter.users, ), SortFilterMenuOption( - value: _actionFilterRepeaters, + value: _TypeFilterAction(ContactTypeFilter.repeaters), label: l10n.listFilter_repeaters, checked: typeFilter == ContactTypeFilter.repeaters, ), SortFilterMenuOption( - value: _actionFilterRooms, + value: _TypeFilterAction(ContactTypeFilter.rooms), label: l10n.listFilter_roomServers, checked: typeFilter == ContactTypeFilter.rooms, ), SortFilterMenuOption( - value: _actionToggleUnreadOnly, + value: const _ToggleUnreadAction(), label: l10n.listFilter_unreadOnly, checked: showUnreadOnly, ), @@ -179,39 +187,32 @@ class ContactsFilterMenu extends StatelessWidget { ], onSelected: (action) { switch (action) { - case _actionSortRecentMessages: - onSortChanged(ContactSortOption.recentMessages); - break; - case _actionSortName: - onSortChanged(ContactSortOption.name); - break; - case _actionSortLastSeen: - onSortChanged(ContactSortOption.lastSeen); - break; - case _actionFilterAll: - onTypeFilterChanged(ContactTypeFilter.all); - break; - case _actionFilterUsers: - onTypeFilterChanged(ContactTypeFilter.users); - break; - case _actionFilterFavorites: - onTypeFilterChanged(ContactTypeFilter.favorites); - break; - case _actionFilterRepeaters: - onTypeFilterChanged(ContactTypeFilter.repeaters); - break; - case _actionFilterRooms: - onTypeFilterChanged(ContactTypeFilter.rooms); - break; - case _actionToggleUnreadOnly: + case _SortAction(:final option): + onSortChanged(option); + case _TypeFilterAction(:final filter): + onTypeFilterChanged(filter); + case _ToggleUnreadAction(): onUnreadOnlyChanged(!showUnreadOnly); - break; } }, ); } } +sealed class _DiscoveryFilterAction { + const _DiscoveryFilterAction(); +} + +class _DiscoverySortAction extends _DiscoveryFilterAction { + final ContactSortOption option; + const _DiscoverySortAction(this.option); +} + +class _DiscoveryTypeFilterAction extends _DiscoveryFilterAction { + final ContactTypeFilter filter; + const _DiscoveryTypeFilterAction(this.filter); +} + class DiscoveryContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; final ContactTypeFilter typeFilter; @@ -229,19 +230,19 @@ class DiscoveryContactsFilterMenu extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - return SortFilterMenu( + return SortFilterMenu<_DiscoveryFilterAction>( tooltip: l10n.listFilter_tooltip, sections: [ SortFilterMenuSection( title: l10n.listFilter_sortBy, options: [ SortFilterMenuOption( - value: _actionSortLastSeen, + value: _DiscoverySortAction(ContactSortOption.lastSeen), label: l10n.listFilter_heardRecently, checked: sortOption == ContactSortOption.lastSeen, ), SortFilterMenuOption( - value: _actionSortName, + value: _DiscoverySortAction(ContactSortOption.name), label: l10n.listFilter_az, checked: sortOption == ContactSortOption.name, ), @@ -251,22 +252,22 @@ class DiscoveryContactsFilterMenu extends StatelessWidget { title: l10n.listFilter_filters, options: [ SortFilterMenuOption( - value: _actionFilterAll, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.all), label: l10n.listFilter_all, checked: typeFilter == ContactTypeFilter.all, ), SortFilterMenuOption( - value: _actionFilterUsers, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.users), label: l10n.listFilter_users, checked: typeFilter == ContactTypeFilter.users, ), SortFilterMenuOption( - value: _actionFilterRepeaters, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.repeaters), label: l10n.listFilter_repeaters, checked: typeFilter == ContactTypeFilter.repeaters, ), SortFilterMenuOption( - value: _actionFilterRooms, + value: _DiscoveryTypeFilterAction(ContactTypeFilter.rooms), label: l10n.listFilter_roomServers, checked: typeFilter == ContactTypeFilter.rooms, ), @@ -275,27 +276,10 @@ class DiscoveryContactsFilterMenu extends StatelessWidget { ], onSelected: (action) { switch (action) { - case _actionSortName: - onSortChanged(ContactSortOption.name); - break; - case _actionSortLastSeen: - onSortChanged(ContactSortOption.lastSeen); - break; - case _actionFilterAll: - onTypeFilterChanged(ContactTypeFilter.all); - break; - case _actionFilterUsers: - onTypeFilterChanged(ContactTypeFilter.users); - break; - case _actionFilterFavorites: - onTypeFilterChanged(ContactTypeFilter.favorites); - break; - case _actionFilterRepeaters: - onTypeFilterChanged(ContactTypeFilter.repeaters); - break; - case _actionFilterRooms: - onTypeFilterChanged(ContactTypeFilter.rooms); - break; + case _DiscoverySortAction(:final option): + onSortChanged(option); + case _DiscoveryTypeFilterAction(:final filter): + onTypeFilterChanged(filter); } }, ); From 4b744184c2b27072e6a07dbb31332d600390211f Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Mar 2026 18:09:54 -0700 Subject: [PATCH 306/421] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/models/contact.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 858d712..c047622 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -1,5 +1,4 @@ import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import '../connector/meshcore_protocol.dart'; From 9265daaf16aeaba5f6e4c5e120bdfd7d413e3bc3 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Mar 2026 18:10:09 -0700 Subject: [PATCH 307/421] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/tcp_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index 02b9b5a..11ab80a 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:meshcore_open/models/app_settings.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; From dc85e7a41c41aabcb684458116292dc9ace0e542 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Mar 2026 18:10:17 -0700 Subject: [PATCH 308/421] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/path_trace_map.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index d50a185..86bba2a 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -361,7 +361,6 @@ class _PathTraceMapScreenState extends State { contact.longitude! + offsetDeg * sin(angle), ); targetGuessed = true; - targetGuessed = true; } } } From 3593cfa84397fb019c4d90f2a16cd9e8bac667d5 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Mar 2026 18:10:44 -0700 Subject: [PATCH 309/421] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/path_trace_map.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 86bba2a..e3c877b 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -312,8 +312,8 @@ class _PathTraceMapScreenState extends State { targetPos = LatLng(target!.latitude!, target!.longitude!); } else if (widget.path.length > 1) { // Infer from the last hop: average GPS contacts sharing that hop. - // For a round-trip path (flipPathRound), the target-side hop sits - // in the middle of the symmetric sequence; .last is the local side. + // For a round-trip path (flipPathAround/reversePathAround), the target-side hop + // sits in the middle of the symmetric sequence; .last is the local side. final lastHop = widget.reversePathAround ? widget.path.first : widget.path.last; From 28a423e0a8df22bbaa3ad5e00cce7b271288fe8e Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 18:14:39 -0700 Subject: [PATCH 310/421] fix: correct location validation and clean up target contact handling - Fix asymmetric lat/lon validation in _handleContactAdvert (was checking longitude != 0 for latitude; now uses (latitude != 0 || longitude != 0) for both) - Remove duplicate targetGuessed assignment in path_trace_map - Rename public target field to private _targetContact, use local variable to avoid unnecessary null-aware operators Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/connector/meshcore_connector.dart | 4 ++-- lib/screens/path_trace_map.dart | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 86484d8..8f93f33 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -4757,14 +4757,14 @@ class MeshCoreConnector extends ChangeNotifier { hasLocation && latitude != null && latitude.abs() <= 90 && - longitude != 0 + (latitude != 0 || longitude != 0) ? latitude : existing.latitude, longitude: hasLocation && longitude != null && longitude.abs() <= 180 && - longitude != 0 + (latitude != 0 || longitude != 0) ? longitude : existing.longitude, name: hasName ? name : existing.name, diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index e3c877b..e64a906 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -93,7 +93,7 @@ class _PathTraceMapScreenState extends State { ValueKey _mapKey = const ValueKey('initial'); double _pathDistanceMeters = 0.0; bool _showNodeLabels = true; - Contact? target; + Contact? _targetContact; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -305,11 +305,12 @@ class _PathTraceMapScreenState extends State { // Compute endpoint position for the target contact. LatLng? targetPos; bool targetGuessed = false; - target = widget.targetContact; + _targetContact = widget.targetContact; - if (target != null) { - if (target?.hasLocation ?? false) { - targetPos = LatLng(target!.latitude!, target!.longitude!); + if (_targetContact != null) { + final tc = _targetContact!; + if (tc.hasLocation) { + targetPos = LatLng(tc.latitude!, tc.longitude!); } else if (widget.path.length > 1) { // Infer from the last hop: average GPS contacts sharing that hop. // For a round-trip path (flipPathAround/reversePathAround), the target-side hop @@ -334,7 +335,7 @@ class _PathTraceMapScreenState extends State { peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length; const offsetDeg = 0.003; - final angle = (target!.publicKey[1] / 255.0) * 2 * pi; + final angle = (tc.publicKey[1] / 255.0) * 2 * pi; targetPos = LatLng( lat + offsetDeg * cos(angle), lon + offsetDeg * sin(angle), @@ -344,7 +345,7 @@ class _PathTraceMapScreenState extends State { final lat = inferredPositions[lastHop]!.latitude; final lon = inferredPositions[lastHop]!.longitude; const offsetDeg = 0.003; - final angle = (target!.publicKey[1] / 255.0) * 2 * pi; + final angle = (tc.publicKey[1] / 255.0) * 2 * pi; targetPos = LatLng( lat + offsetDeg * cos(angle), lon + offsetDeg * sin(angle), @@ -355,7 +356,7 @@ class _PathTraceMapScreenState extends State { final contact = pathContacts[lastHop]; if (contact != null && contact.hasLocation) { const offsetDeg = 0.003; - final angle = (target!.publicKey[1] / 255.0) * 2 * pi; + final angle = (tc.publicKey[1] / 255.0) * 2 * pi; targetPos = LatLng( contact.latitude! + offsetDeg * cos(angle), contact.longitude! + offsetDeg * sin(angle), @@ -387,7 +388,7 @@ class _PathTraceMapScreenState extends State { hopLast = hop; } if (targetPos != null) { - if (target != null && target!.type == advTypeChat) { + if (_targetContact != null && _targetContact!.type == advTypeChat) { _points.add(targetPos); } } @@ -479,7 +480,8 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData) _buildMapPathTrace(context, tileCache, target), + if (_hasData) + _buildMapPathTrace(context, tileCache, _targetContact), if (_points.isEmpty && !_hasData && !_isLoading && From 6dfb7a4b6941bc828069258724dc24988e5f7266 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 18:41:21 -0700 Subject: [PATCH 311/421] fix: auto-add flag parsing, contact cache restore, and USB reconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix operator precedence bug in _handleAutoAddConfig where `flags & flag != 0` was parsed as `flags & (flag != 0)`, always checking bit 0 instead of the correct flag bit - Populate _contacts from cache in loadContactCache() so contacts persist across app restarts - Toggle DTR low→high on USB connect to force device to see a fresh connection - Add 10ms inter-frame delay for USB sends to prevent missed responses - Deassert DTR before closing USB port on disconnect/dispose Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/connector/meshcore_connector.dart | 17 ++++++++++----- lib/connector/meshcore_connector_usb.dart | 2 ++ lib/services/usb_serial_service_native.dart | 23 +++++++++++++++++++++ lib/services/usb_serial_service_web.dart | 11 ++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 8f93f33..1af3c0b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -708,6 +708,9 @@ class MeshCoreConnector extends ChangeNotifier { _knownContactKeys ..clear() ..addAll(cached.map((c) => c.publicKeyHex)); + _contacts + ..clear() + ..addAll(cached); for (final contact in cached) { _ensureContactSmazSettingLoaded(contact.publicKeyHex); } @@ -1540,6 +1543,10 @@ class MeshCoreConnector extends ChangeNotifier { if (_activeTransport == MeshCoreTransportType.usb) { await _usbManager.write(data); + // Brief pause so the device firmware can process each frame before the + // next arrives. Without this, rapid-fire frames over USB can cause the + // device to miss responses (especially on reconnect). + await Future.delayed(const Duration(milliseconds: 10)); } else if (_activeTransport == MeshCoreTransportType.tcp) { await _tcpConnector.write(data); } else { @@ -4837,11 +4844,11 @@ class MeshCoreConnector extends ChangeNotifier { try { reader.skipBytes(1); // Skip the response code byte final flags = reader.readByte(); - _autoAddUsers = flags & autoAddChatFlag != 0; - _autoAddRepeaters = flags & autoAddRepeaterFlag != 0; - _autoAddRoomServers = flags & autoAddRoomServerFlag != 0; - _autoAddSensors = flags & autoAddSensorFlag != 0; - _overwriteOldest = flags & autoAddOverwriteOldestFlag != 0; + _autoAddUsers = (flags & autoAddChatFlag) != 0; + _autoAddRepeaters = (flags & autoAddRepeaterFlag) != 0; + _autoAddRoomServers = (flags & autoAddRoomServerFlag) != 0; + _autoAddSensors = (flags & autoAddSensorFlag) != 0; + _overwriteOldest = (flags & autoAddOverwriteOldestFlag) != 0; } catch (e) { appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector'); } diff --git a/lib/connector/meshcore_connector_usb.dart b/lib/connector/meshcore_connector_usb.dart index 74e7355..56718bc 100644 --- a/lib/connector/meshcore_connector_usb.dart +++ b/lib/connector/meshcore_connector_usb.dart @@ -64,6 +64,8 @@ class MeshCoreUsbManager { Future write(Uint8List data) => _service.write(data); + Future writeRaw(Uint8List data) => _service.writeRaw(data); + // --- Label management --- void updateConnectedLabel(String selfName) { _service.updateConnectedLabel(selfName); diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index fca3d19..c1d3946 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -189,6 +189,10 @@ class UsbSerialService { serial.setStopBits1(); serial.setFlowControlNone(); serial.setRTS(false); + // Toggle DTR low→high so the device sees a fresh connection even + // if the previous disconnect didn't cleanly signal DTR drop. + serial.setDTR(false); + await Future.delayed(const Duration(milliseconds: 50)); serial.setDTR(true); _serial = serial; // Update the normalized port name to whichever candidate succeeded. @@ -249,6 +253,23 @@ class UsbSerialService { _status = UsbSerialStatus.connected; } + Future writeRaw(Uint8List data) async { + if (!isConnected) { + throw StateError('USB serial port is not open'); + } + if (_useAndroidUsbHost) { + try { + await _androidMethodChannel.invokeMethod('write', { + 'data': data, + }); + } on PlatformException catch (error) { + throw StateError(error.message ?? error.code); + } + } else { + _serial!.write(data); + } + } + Future write(Uint8List data) async { if (!isConnected) { throw StateError('USB serial port is not open'); @@ -300,6 +321,7 @@ class UsbSerialService { _serial = null; try { if (serial?.isOpen() == FlOpenStatus.open) { + serial?.setDTR(false); serial?.closePort(); } } catch (_) { @@ -350,6 +372,7 @@ class UsbSerialService { final serial = _serial; try { if (serial?.isOpen() == FlOpenStatus.open) { + serial?.setDTR(false); serial?.closePort(); // synchronous C call — kills the SerialThread } } catch (_) {} diff --git a/lib/services/usb_serial_service_web.dart b/lib/services/usb_serial_service_web.dart index 4c83d7d..5261308 100644 --- a/lib/services/usb_serial_service_web.dart +++ b/lib/services/usb_serial_service_web.dart @@ -127,6 +127,17 @@ class UsbSerialService { } } + Future writeRaw(Uint8List data) async { + if (!isConnected || _writer == null) { + throw StateError('USB serial port is not open'); + } + final promise = _writer!.callMethod>( + 'write'.toJS, + data.toJS, + ); + await promise.toDart; + } + Future write(Uint8List data) async { if (!isConnected || _writer == null) { throw StateError('USB serial port is not open'); From 60e8ee013053a06d1f8c74d8f654d3ecf97f0288 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 18:41:57 -0700 Subject: [PATCH 312/421] fix: simplify method call for writing data in UsbSerialService --- lib/services/usb_serial_service_native.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/services/usb_serial_service_native.dart b/lib/services/usb_serial_service_native.dart index c1d3946..40861db 100644 --- a/lib/services/usb_serial_service_native.dart +++ b/lib/services/usb_serial_service_native.dart @@ -259,9 +259,7 @@ class UsbSerialService { } if (_useAndroidUsbHost) { try { - await _androidMethodChannel.invokeMethod('write', { - 'data': data, - }); + await _androidMethodChannel.invokeMethod('write', {'data': data}); } on PlatformException catch (error) { throw StateError(error.message ?? error.code); } From 64d75dde45e6e842012956b790ac1e8c5bf0763a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Mar 2026 18:46:29 -0700 Subject: [PATCH 313/421] chore: update version to 7.0.0+8 in pubspec.yaml --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4831e67..663622b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 6.0.0+7 +version: 7.0.0+8 environment: sdk: ^3.9.2 From 3664ae34cd32e9cf89b6474f277db33420c7132e Mon Sep 17 00:00:00 2001 From: ericz Date: Sun, 15 Mar 2026 11:42:46 +0100 Subject: [PATCH 314/421] reimplement location aware snr-indikator after alpha7 --- lib/utils/contact_search.dart | 55 +++++++++++++++++++ lib/widgets/snr_indicator.dart | 24 ++++++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 7a82c53..8aa75b0 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -1,3 +1,6 @@ +import 'package:latlong2/latlong.dart'; + +import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; export 'contact_filter_types.dart'; @@ -43,3 +46,55 @@ String? _extractHexPrefix(String query) { if (!RegExp(r'^[0-9a-f]+$').hasMatch(cleaned)) return null; return cleaned; } + +Contact? getRepeaterPrefixMatchNearLocation( + List contacts, + int pubkeyFirstByte, { + LatLng? searchPoint, + bool preferFavorites = false, +}) { + final candidates = contacts + .where( + (c) => + c.publicKey.isNotEmpty && + c.publicKey.first == pubkeyFirstByte && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + + if (candidates.isEmpty) return null; + + candidates.sort((a, b) { + if (preferFavorites) { + final favA = a.isFavorite ? 1 : 0; + final favB = b.isFavorite ? 1 : 0; + final favCompare = favB.compareTo(favA); + if (favCompare != 0) return favCompare; + } + + final seenCompare = b.lastSeen.compareTo(a.lastSeen); + if (seenCompare != 0) return seenCompare; + + return a.publicKeyHex.compareTo(b.publicKeyHex); + }); + + if (searchPoint == null) { + return candidates.first; + } + + final distance = Distance(); + Contact best = candidates.first; + var bestDistance = double.infinity; + + for (final c in candidates) { + if (c.hasLocation) { + final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); + if (d < bestDistance) { + bestDistance = d; + best = c; + } + } + } + + return best; +} diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index 30956e2..cf3c275 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; + import '../connector/meshcore_connector.dart'; +import '../utils/contact_search.dart'; import '../l10n/l10n.dart'; import 'signal_ui.dart'; @@ -158,10 +161,23 @@ class _SNRIndicatorState extends State { widget.connector.currentSf, ); final allContacts = widget.connector.allContacts; - final name = allContacts - .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) - .map((c) => c.name) - .firstOrNull; + + final selfLat = widget.connector.selfLatitude; + final selfLon = widget.connector.selfLongitude; + + LatLng? selfPoint; + if (selfLat != null && selfLon != null) { + selfPoint = LatLng(selfLat, selfLon); + } + + final contact = getRepeaterPrefixMatchNearLocation( + allContacts, + repeater.pubkeyFirstByte, + searchPoint: selfPoint, + preferFavorites: true, + ); + + final name = contact?.name; return Column( children: [ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e..4084d9b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From 0ef2194fb02a95cabf39516205329abb7c482285 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Sun, 15 Mar 2026 12:10:47 +0100 Subject: [PATCH 315/421] codex suggested fix: explicit check if contact location is not null Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/utils/contact_search.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/contact_search.dart b/lib/utils/contact_search.dart index 8aa75b0..6a708e8 100644 --- a/lib/utils/contact_search.dart +++ b/lib/utils/contact_search.dart @@ -87,7 +87,7 @@ Contact? getRepeaterPrefixMatchNearLocation( var bestDistance = double.infinity; for (final c in candidates) { - if (c.hasLocation) { + if (c.hasLocation && c.latitude != null && c.longitude != null) { final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!)); if (d < bestDistance) { bestDistance = d; From be690c81943073b5ce8e172c4e49d266d528036a Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Sun, 15 Mar 2026 16:48:40 -0400 Subject: [PATCH 316/421] fix: provide AppSettingsService in tcp_flow_test TcpScreen.initState reads AppSettingsService from context to pre-fill host/port fields, but the test helper only provided MeshCoreConnector. Switch to MultiProvider so AppSettingsService is also in the widget tree. --- test/screens/tcp_flow_test.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/screens/tcp_flow_test.dart b/test/screens/tcp_flow_test.dart index 725388a..1d8174c 100644 --- a/test/screens/tcp_flow_test.dart +++ b/test/screens/tcp_flow_test.dart @@ -6,6 +6,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/l10n/app_localizations.dart'; import 'package:meshcore_open/screens/scanner_screen.dart'; import 'package:meshcore_open/screens/tcp_screen.dart'; +import 'package:meshcore_open/services/app_settings_service.dart'; class _FakeMeshCoreConnector extends MeshCoreConnector { _FakeMeshCoreConnector(); @@ -44,8 +45,13 @@ Widget _buildTestApp({ required Widget child, Locale? locale, }) { - return ChangeNotifierProvider.value( - value: connector, + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: connector), + ChangeNotifierProvider( + create: (_) => AppSettingsService(), + ), + ], child: MaterialApp( locale: locale, localizationsDelegates: AppLocalizations.localizationsDelegates, From faba1208234e74f593d4ddf8badb8430ea9c9a7c Mon Sep 17 00:00:00 2001 From: Stephan Rodemeier Date: Sun, 15 Mar 2026 23:01:38 +0100 Subject: [PATCH 317/421] Add more explicit platform support table The platform support was a bit vague, this adds a table to better convey the differences. --- README.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3b230dd..2f87e91 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Device Management -- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth +- **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP - **Device Settings**: Configure radio parameters, power settings, and network options - **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves - **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon) @@ -75,10 +75,16 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Platform Support -- ✅ **Android**: Full support (API 21+) -- ✅ **iOS**: Full support (iOS 12+) -- 🚧 **Desktop**: Limited support (macOS/Linux/Windows) -- 🚧 **Web**: Under construction (Chrome) +| Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web | +|--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:| +| BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ | +| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌
(requires websocket bridge) | +| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ### Dependencies @@ -189,6 +195,7 @@ Messages are transmitted as binary frames using a custom protocol optimized for ### App Settings - **Theme**: System default, light, or dark mode +- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian) - **Notifications**: Configurable for messages, channels, and node advertisements - **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types - **Message Retry**: Automatic retry with configurable path clearing From 723bf7293c0765510096dbf0a29a324493d62f0d Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 17 Mar 2026 21:56:42 +0100 Subject: [PATCH 318/421] location aware channel_message_path --- .gitignore | 3 +- lib/screens/channel_message_path_screen.dart | 93 +++++++++++--------- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index a312737..779856c 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ secrets.dart .DS_Store .AppleDouble .LSOverride +macos/Flutter/GeneratedPluginRegistrant.swift # iOS **/ios/Pods/ @@ -85,4 +86,4 @@ keystore.properties .vscode/settings.json # Cloudflare Wrangler -.wrangler \ No newline at end of file +.wrangler diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 747c2bf..e2c5b49 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget { final primaryPath = !channelMessage && !message.isOutgoing ? Uint8List.fromList(primaryPathTmp.reversed.toList()) : primaryPathTmp; - final contacts = connector.allContacts; - final hops = _buildPathHops(primaryPath, contacts, l10n); + final hops = _buildPathHops(primaryPath, connector, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( primaryPath.length, @@ -365,8 +364,7 @@ class _ChannelMessagePathMapScreenState : selectedPathTmp; final selectedIndex = _indexForPath(selectedPath, observedPaths); - final contacts = connector.allContacts; - final hops = _buildPathHops(selectedPath, contacts, context.l10n); + final hops = _buildPathHops(selectedPath, connector, context.l10n); final points = []; @@ -787,17 +785,62 @@ class _ObservedPath { List<_PathHop> _buildPathHops( Uint8List pathBytes, - List contacts, + MeshCoreConnector connector, AppLocalizations l10n, ) { + if (pathBytes.isEmpty) return const []; + final candidatesByPrefix = >{}; + for (final contact in connector.allContacts) { + if (contact.publicKey.isEmpty) continue; + if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { + continue; + } + final prefix = contact.publicKey.first; + candidatesByPrefix.putIfAbsent(prefix, () => []).add(contact); + } + for (final candidates in candidatesByPrefix.values) { + candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); + } + final startPoint = + (connector.selfLatitude != null && connector.selfLongitude != null) + ? LatLng(connector.selfLatitude!, connector.selfLongitude!) + : null; + var previousPosition = startPoint; + final distance = Distance(); + final hops = <_PathHop>[]; for (var i = 0; i < pathBytes.length; i++) { - final prefix = pathBytes[i]; - final contact = _matchContactForPrefix(contacts, prefix); + final searchPoint = i == 0 ? startPoint : previousPosition; + final candidates = candidatesByPrefix[pathBytes[i]]; + Contact? contact; + if (candidates != null && candidates.isNotEmpty) { + var bestIndex = 0; + if (searchPoint != null) { + var bestDistance = double.infinity; + for (var j = 0; j < candidates.length; j++) { + final candidate = candidates[j]; + if (!candidate.hasLocation) continue; + final currentDistance = distance( + searchPoint, + LatLng(candidate.latitude!, candidate.longitude!), + ); + if (currentDistance < bestDistance) { + bestDistance = currentDistance; + bestIndex = j; + } + } + } + contact = candidates.removeAt(bestIndex); + if (candidates.isEmpty) { + candidatesByPrefix.remove(pathBytes[i]); + } + } + + previousPosition = _resolvePosition(contact); hops.add( _PathHop( index: i + 1, - prefix: prefix, + prefix: pathBytes[i], contact: contact, position: _resolvePosition(contact), l10n: l10n, @@ -807,44 +850,12 @@ List<_PathHop> _buildPathHops( return hops; } -Contact? _matchContactForPrefix(List contacts, int prefix) { - final matches = contacts - .where( - (contact) => - (contact.type == advTypeRepeater || contact.type == advTypeRoom) && - contact.publicKey.isNotEmpty && - contact.publicKey[0] == prefix, - ) - .toList(); - if (matches.isEmpty) return null; - - Contact? pickWhere(bool Function(Contact) predicate) { - for (final contact in matches) { - if (predicate(contact)) return contact; - } - return null; - } - - return pickWhere((c) => c.type == advTypeRepeater && _hasValidLocation(c)) ?? - pickWhere((c) => c.type == advTypeRepeater) ?? - pickWhere(_hasValidLocation) ?? - matches.first; -} - LatLng? _resolvePosition(Contact? contact) { if (contact == null) return null; - if (!_hasValidLocation(contact)) return null; + if (!contact.hasLocation) return null; return LatLng(contact.latitude!, contact.longitude!); } -bool _hasValidLocation(Contact contact) { - final lat = contact.latitude; - final lon = contact.longitude; - if (lat == null || lon == null) return false; - if (lat == 0 && lon == 0) return false; - return true; -} - String _formatPrefix(int prefix) { return prefix.toRadixString(16).padLeft(2, '0').toUpperCase(); } From d2df2b0beda0f3826bd6d712be77e474e87b8066 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Tue, 17 Mar 2026 22:23:23 +0100 Subject: [PATCH 319/421] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/channel_message_path_screen.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index e2c5b49..d2f8d81 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -836,13 +836,16 @@ List<_PathHop> _buildPathHops( } } - previousPosition = _resolvePosition(contact); + final resolvedPosition = _resolvePosition(contact); + if (resolvedPosition != null) { + previousPosition = resolvedPosition; + } hops.add( _PathHop( index: i + 1, prefix: pathBytes[i], contact: contact, - position: _resolvePosition(contact), + position: resolvedPosition, l10n: l10n, ), ); From 11cb14a9256b53b43a937b55fe5810dbc2d10163 Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 17 Mar 2026 23:22:23 +0100 Subject: [PATCH 320/421] focus on hop if you click on one in the legend. --- lib/screens/channel_message_path_screen.dart | 29 +++++++++++++++++++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ 2 files changed, 31 insertions(+) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index d2f8d81..0092f22 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -302,10 +302,12 @@ class _ChannelMessagePathMapScreenState extends State { static const double _labelZoomThreshold = 8.5; + final MapController _mapController = MapController(); Uint8List? _selectedPath; double _pathDistance = 0.0; bool _showNodeLabels = true; bool _didReceivePositionUpdate = false; + int? _focusedHopIndex; @override void initState() { @@ -336,6 +338,22 @@ class _ChannelMessagePathMapScreenState return totalDistance; } + void _focusHop(_PathHop hop) { + if (!hop.hasLocation) return; + final targetZoom = _didReceivePositionUpdate + ? max(_mapController.camera.zoom, 14.0) + : 14.0; + _mapController.move(hop.position!, targetZoom); + } + + void _onHopTapped(_PathHop hop) { + _focusHop(hop); + if (!mounted) return; + setState(() { + _focusedHopIndex = hop.index; + }); + } + @override Widget build(BuildContext context) { return Consumer( @@ -419,6 +437,7 @@ class _ChannelMessagePathMapScreenState children: [ FlutterMap( key: mapKey, + mapController: _mapController, options: MapOptions( initialCenter: initialCenter, initialZoom: initialZoom, @@ -470,6 +489,7 @@ class _ChannelMessagePathMapScreenState ) { setState(() { _selectedPath = observedPaths[index].pathBytes; + _focusedHopIndex = null; }); }), if (points.isEmpty) @@ -725,8 +745,17 @@ class _ChannelMessagePathMapScreenState separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final hop = hops[index]; + final isFocused = _focusedHopIndex == hop.index; return ListTile( dense: true, + enabled: hop.hasLocation, + selected: isFocused, + selectedTileColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.12), + onTap: hop.hasLocation + ? () => _onHopTapped(hop) + : null, leading: CircleAvatar( radius: 14, child: Text( diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e..4084d9b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) From 7b3c09973604c592bca8d3002edd537788333d2a Mon Sep 17 00:00:00 2001 From: ericz Date: Wed, 18 Mar 2026 06:52:08 +0100 Subject: [PATCH 321/421] reduce zoomlevel --- lib/screens/channel_message_path_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 0092f22..76273c4 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -341,8 +341,8 @@ class _ChannelMessagePathMapScreenState void _focusHop(_PathHop hop) { if (!hop.hasLocation) return; final targetZoom = _didReceivePositionUpdate - ? max(_mapController.camera.zoom, 14.0) - : 14.0; + ? max(_mapController.camera.zoom, 10.0) + : 12.0; _mapController.move(hop.position!, targetZoom); } From 87d11c2e6b3d0e1e21099ee9c39ba852c863e92d Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Wed, 18 Mar 2026 07:00:16 +0100 Subject: [PATCH 322/421] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/channel_message_path_screen.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 76273c4..f64cb53 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -848,7 +848,11 @@ List<_PathHop> _buildPathHops( var bestDistance = double.infinity; for (var j = 0; j < candidates.length; j++) { final candidate = candidates[j]; - if (!candidate.hasLocation) continue; + if (!candidate.hasLocation || + candidate.latitude == null || + candidate.longitude == null) { + continue; + } final currentDistance = distance( searchPoint, LatLng(candidate.latitude!, candidate.longitude!), From b88e5e647ad9a0cda4a994210aa84ae6717148b0 Mon Sep 17 00:00:00 2001 From: ericszimmermann Date: Wed, 18 Mar 2026 08:06:22 +0100 Subject: [PATCH 323/421] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/channel_message_path_screen.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index f64cb53..ea07eae 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -889,7 +889,10 @@ List<_PathHop> _buildPathHops( LatLng? _resolvePosition(Contact? contact) { if (contact == null) return null; if (!contact.hasLocation) return null; - return LatLng(contact.latitude!, contact.longitude!); + final latitude = contact.latitude; + final longitude = contact.longitude; + if (latitude == null || longitude == null) return null; + return LatLng(latitude, longitude); } String _formatPrefix(int prefix) { From 4962a48e64222217c3678fa5294b822ca287dca8 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Fri, 20 Mar 2026 01:54:31 -0700 Subject: [PATCH 324/421] Msg Retry fixes, channel message fixes. Notification fixes. Make more desktop friendly. Enhance retry algo. Fix predicted location clustering add retries to reactions and fix the reactions in private DMS centralize and cleanup code in var areas --- android/app/build.gradle.kts | 2 +- lib/connector/meshcore_connector.dart | 591 +++++++++----- lib/connector/meshcore_protocol.dart | 4 +- lib/helpers/link_handler.dart | 39 + lib/helpers/path_helper.dart | 31 + lib/helpers/reaction_helper.dart | 44 + lib/l10n/app_bg.arb | 23 +- lib/l10n/app_de.arb | 23 +- lib/l10n/app_en.arb | 17 + lib/l10n/app_es.arb | 23 +- lib/l10n/app_fr.arb | 23 +- lib/l10n/app_it.arb | 23 +- lib/l10n/app_localizations.dart | 66 ++ lib/l10n/app_localizations_bg.dart | 45 + lib/l10n/app_localizations_de.dart | 43 + lib/l10n/app_localizations_en.dart | 42 + lib/l10n/app_localizations_es.dart | 43 + lib/l10n/app_localizations_fr.dart | 44 + lib/l10n/app_localizations_it.dart | 44 + lib/l10n/app_localizations_nl.dart | 43 + lib/l10n/app_localizations_pl.dart | 43 + lib/l10n/app_localizations_pt.dart | 43 + lib/l10n/app_localizations_ru.dart | 44 + lib/l10n/app_localizations_sk.dart | 42 + lib/l10n/app_localizations_sl.dart | 43 + lib/l10n/app_localizations_sv.dart | 42 + lib/l10n/app_localizations_uk.dart | 43 + lib/l10n/app_localizations_zh.dart | 37 + lib/l10n/app_nl.arb | 23 +- lib/l10n/app_pl.arb | 23 +- lib/l10n/app_pt.arb | 23 +- lib/l10n/app_ru.arb | 23 +- lib/l10n/app_sk.arb | 23 +- lib/l10n/app_sl.arb | 23 +- lib/l10n/app_sv.arb | 23 +- lib/l10n/app_uk.arb | 23 +- lib/l10n/app_zh.arb | 23 +- lib/models/app_settings.dart | 35 + lib/models/channel_message.dart | 4 + lib/models/contact.dart | 14 +- lib/models/message.dart | 7 +- lib/models/path_history.dart | 12 +- lib/models/path_selection.dart | 41 + lib/screens/app_settings_screen.dart | 112 +++ lib/screens/channel_chat_screen.dart | 32 +- lib/screens/channels_screen.dart | 153 ++-- lib/screens/chat_screen.dart | 142 +++- lib/screens/contacts_screen.dart | 122 +-- lib/screens/discovery_screen.dart | 11 +- lib/screens/map_screen.dart | 57 +- lib/services/app_settings_service.dart | 24 + lib/services/message_retry_service.dart | 610 +++++--------- lib/services/notification_service.dart | 21 +- lib/services/path_history_service.dart | 340 ++++++-- lib/storage/channel_message_store.dart | 2 + lib/storage/message_store.dart | 8 + lib/widgets/path_management_dialog.dart | 69 +- test/helpers/path_helper_test.dart | 36 + test/models/model_changes_test.dart | 357 ++++++++ test/services/path_history_service_test.dart | 815 +++++++++++++++++++ test/services/retry_and_protocol_test.dart | 628 ++++++++++++++ 61 files changed, 4509 insertions(+), 900 deletions(-) create mode 100644 lib/helpers/path_helper.dart create mode 100644 test/helpers/path_helper_test.dart create mode 100644 test/models/model_changes_test.dart create mode 100644 test/services/path_history_service_test.dart create mode 100644 test/services/retry_and_protocol_test.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e0a8029..c8028e0 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) { android { namespace = "com.meshcore.meshcore_open" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "29.0.14206865" compileOptions { sourceCompatibility = JavaVersion.VERSION_17 diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7211992..d00d49a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -171,6 +171,11 @@ class MeshCoreConnector extends ChangeNotifier { // Intentionally global (not per-contact): tracks overall network activity. // Frequent RX from any source indicates a busy network with more collisions. DateTime _lastRxTime = DateTime.now(); + DateTime _lastRadioRxTime = DateTime.fromMillisecondsSinceEpoch(0); + DateTime _lastContactMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0); + static const int _radioQuietMs = 3000; + static const int _radioQuietMaxWaitMs = 3000; + static const int _contactMsgBackoffMs = 5000; bool _batteryRequested = false; bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; @@ -694,24 +699,32 @@ class MeshCoreConnector extends ChangeNotifier { _loadChannelOrder(); // Initialize retry service callbacks - _retryService?.initialize( - sendMessageCallback: _sendMessageDirect, - addMessageCallback: _addMessage, - updateMessageCallback: _updateMessage, - clearContactPathCallback: clearContactPath, - setContactPathCallback: setContactPath, - calculateTimeoutCallback: + _retryService?.initialize(RetryServiceConfig( + sendMessage: _sendMessageDirect, + addMessage: _addMessage, + updateMessage: _updateMessage, + clearContactPath: clearContactPath, + setContactPath: setContactPath, + calculateTimeout: (pathLength, messageBytes, {String? contactKey}) => calculateTimeout( pathLength: pathLength, messageBytes: messageBytes, contactKey: contactKey, ), - getSelfPublicKeyCallback: () => _selfPublicKey, - prepareContactOutboundTextCallback: prepareContactOutboundText, + getSelfPublicKey: () => _selfPublicKey, + prepareContactOutboundText: prepareContactOutboundText, appSettingsService: appSettingsService, debugLogService: _appDebugLogService, - recordPathResultCallback: _recordPathResult, - onDeliveryObservedCallback: + recordPathResult: _recordPathResult, + selectRetryPath: + (contactKey, attemptIndex, maxRetries, recentSelections) => + _selectAutoPathForAttempt( + contactKey, + attemptIndex: attemptIndex, + maxRetries: maxRetries, + recentSelections: recentSelections, + ), + onDeliveryObserved: (contactKey, pathLength, messageBytes, tripTimeMs) { final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; _timeoutPredictionService?.recordObservation( @@ -722,7 +735,9 @@ class MeshCoreConnector extends ChangeNotifier { secondsSinceLastRx: secSinceRx, ); }, - ); + )); + final maxRetries = _appSettingsService?.settings.maxMessageRetries ?? 5; + _retryService?.setMaxRetries(maxRetries); } Future loadContactCache() async { @@ -753,22 +768,61 @@ class MeshCoreConnector extends ChangeNotifier { } } - void _sendMessageDirect( + Future _waitForRadioQuiet() async { + // Wait for backoff after receiving a contact message (avoid collision + // with their transmission still propagating through repeaters) + final msSinceContactMsg = DateTime.now() + .difference(_lastContactMsgRxTime) + .inMilliseconds; + if (msSinceContactMsg < _contactMsgBackoffMs) { + final waitMs = _contactMsgBackoffMs - msSinceContactMsg; + debugPrint('Contact message backoff: waiting ${waitMs}ms'); + await Future.delayed(Duration(milliseconds: waitMs)); + } + + // Then wait for radio silence (no RF activity for 3s) + final msSinceRx = DateTime.now() + .difference(_lastRadioRxTime) + .inMilliseconds; + if (msSinceRx >= _radioQuietMs) return; + + final deadline = DateTime.now().add( + const Duration(milliseconds: _radioQuietMaxWaitMs), + ); + while (DateTime.now().isBefore(deadline)) { + final quiet = DateTime.now().difference(_lastRadioRxTime).inMilliseconds; + if (quiet >= _radioQuietMs) { + debugPrint('Radio quiet for ${quiet}ms, proceeding with send'); + return; + } + await Future.delayed(const Duration(milliseconds: 200)); + } + debugPrint( + 'Radio quiet wait exceeded ${_radioQuietMaxWaitMs}ms, sending anyway', + ); + } + + Future _sendMessageDirect( Contact contact, String text, int attempt, int timestampSeconds, ) async { if (!isConnected || text.isEmpty) return; - final outboundText = prepareContactOutboundText(contact, text); - await sendFrame( - buildSendTextMsgFrame( - contact.publicKey, - outboundText, - attempt: attempt, - timestampSeconds: timestampSeconds, - ), - ); + try { + await _waitForRadioQuiet(); + final outboundText = prepareContactOutboundText(contact, text); + await sendFrame( + buildSendTextMsgFrame( + contact.publicKey, + outboundText, + attempt: attempt, + timestampSeconds: timestampSeconds, + ), + ); + } catch (e) { + appLogger.error('Failed to send message: $e', tag: 'Connector'); + } } void _updateMessage(Message message) { @@ -784,6 +838,20 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } } + + // If this is a reaction message, update the target message's reaction status + final reactionInfo = ReactionHelper.parseReaction(message.text); + if (reactionInfo != null && + (message.status == MessageStatus.delivered || + message.status == MessageStatus.failed)) { + final contactKey2 = pubKeyToHex(message.senderKey); + _setReactionStatus(contactKey2, reactionInfo, message.status); + _messageStore.saveMessages( + contactKey2, + _conversations[contactKey2] ?? [], + ); + notifyListeners(); + } } void _recordPathResult( @@ -793,35 +861,68 @@ class MeshCoreConnector extends ChangeNotifier { int? tripTimeMs, ) { if (_pathHistoryService == null) return; + final settings = _appSettingsService?.settings; _pathHistoryService!.recordPathResult( contactPubKeyHex, selection, success: success, tripTimeMs: tripTimeMs, + successIncrement: settings?.routeWeightSuccessIncrement ?? 0.2, + failureDecrement: settings?.routeWeightFailureDecrement ?? 0.2, + maxWeight: settings?.maxRouteWeight ?? 5.0, ); + + // Flood path attribution: when a flood delivery succeeds, credit the + // contact's current device path so the route the ACK traveled back + // through gets a weight boost in the path history. + if (selection.useFlood && success) { + final contact = _contacts.cast().firstWhere( + (c) => c?.publicKeyHex == contactPubKeyHex, + orElse: () => null, + ); + if (contact != null && + contact.pathLength >= 0 && + contact.path.isNotEmpty) { + _pathHistoryService!.recordFloodPathAttribution( + contactPubKeyHex: contactPubKeyHex, + pathBytes: contact.path, + hopCount: contact.pathLength, + tripTimeMs: tripTimeMs, + successIncrement: settings?.routeWeightSuccessIncrement ?? 0.2, + maxWeight: settings?.maxRouteWeight ?? 5.0, + ); + } + + // Request a fresh contact from the device so the next flood + // attribution uses the most up-to-date path. + if (contact != null) { + unawaited(getContactByKey(contact.publicKey)); + } + } } - Contact _applyAutoSelection(Contact contact, PathSelection? selection) { - if (selection == null || - selection.useFlood || - selection.pathBytes.isEmpty) { - return contact; + PathSelection? _selectAutoPathForAttempt( + String contactPubKeyHex, { + required int attemptIndex, + required int maxRetries, + List recentSelections = const [], + }) { + final hasKnownPaths = + _pathHistoryService?.getRecentPaths(contactPubKeyHex).isNotEmpty ?? false; + if (!hasKnownPaths) { + return null; } - return Contact( - publicKey: contact.publicKey, - name: contact.name, - type: contact.type, - flags: contact.flags, - pathLength: selection.hopCount >= 0 - ? selection.hopCount - : contact.pathLength, - path: Uint8List.fromList(selection.pathBytes), - latitude: contact.latitude, - longitude: contact.longitude, - lastSeen: contact.lastSeen, - lastMessageAt: contact.lastMessageAt, + final selection = _pathHistoryService?.selectPathForAttempt( + contactPubKeyHex, + attemptIndex: attemptIndex, + maxRetries: maxRetries, + recentSelections: recentSelections, ); + if (selection != null) { + _pathHistoryService?.recordPathAttempt(contactPubKeyHex, selection); + } + return selection; } Future startScan({ @@ -1730,47 +1831,43 @@ class MeshCoreConnector extends ChangeNotifier { Future sendMessage(Contact contact, String text) async { if (!isConnected || text.isEmpty) return; - // Handle auto-rotation if enabled - PathSelection? autoSelection; - if (_appSettingsService?.settings.autoRouteRotationEnabled == true) { - autoSelection = _pathHistoryService?.getNextAutoPathSelection( + // Check if this is a reaction - apply locally with pending status and route through retry service + final reactionInfo = ReactionHelper.parseReaction(text); + if (reactionInfo != null) { + _conversations.putIfAbsent(contact.publicKeyHex, () => []); + final messages = _conversations[contact.publicKeyHex]!; + + // Apply reaction locally with pending status + _processOutgoingContactReaction(messages, reactionInfo, contact); + _setReactionStatus( contact.publicKeyHex, + reactionInfo, + MessageStatus.pending, ); - if (autoSelection != null) { - _pathHistoryService?.recordPathAttempt( - contact.publicKeyHex, - autoSelection, - ); - if (!autoSelection.useFlood && autoSelection.pathBytes.isNotEmpty) { - await setContactPath( - contact, - Uint8List.fromList(autoSelection.pathBytes), - autoSelection.pathBytes.length, - ); - } + _messageStore.saveMessages(contact.publicKeyHex, messages); + notifyListeners(); + + // Route through retry service (same as normal messages) + // Don't use auto-rotation for reactions — just send directly + if (_retryService != null) { + _retryService!.sendMessageWithRetry(contact: contact, text: text); + } else { + final outboundText = prepareContactOutboundText(contact, text); + await sendFrame(buildSendTextMsgFrame(contact.publicKey, outboundText)); } + return; } if (_retryService != null) { - final pathBytes = _resolveOutgoingPathBytes(contact, autoSelection); - final pathLength = _resolveOutgoingPathLength(contact, autoSelection); - final selectedContact = _applyAutoSelection(contact, autoSelection); - await _retryService!.sendMessageWithRetry( - contact: selectedContact, - text: text, - pathSelection: autoSelection, - pathBytes: pathBytes, - pathLength: pathLength, - ); + await _retryService!.sendMessageWithRetry(contact: contact, text: text); } else { // Fallback to old behavior if retry service not initialized - final pathBytes = _resolveOutgoingPathBytes(contact, autoSelection); - final pathLength = _resolveOutgoingPathLength(contact, autoSelection); + final resolved = resolvePathSelection(contact); final message = Message.outgoing( contact.publicKey, text, - pathLength: pathLength, - pathBytes: pathBytes, + pathLength: resolved.useFlood ? -1 : resolved.hopCount, + pathBytes: Uint8List.fromList(resolved.pathBytes), ); _addMessage(contact.publicKeyHex, message); notifyListeners(); @@ -1808,6 +1905,16 @@ class MeshCoreConnector extends ChangeNotifier { if (_activeTransport == MeshCoreTransportType.usb) { await Future.delayed(const Duration(milliseconds: 100)); } + final idx = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + if (idx != -1) { + _contacts[idx] = _contacts[idx].copyWith( + pathLength: customPath.length, + path: customPath, + ); + notifyListeners(); + } } finally { completer.complete(); } @@ -1924,6 +2031,9 @@ class MeshCoreConnector extends ChangeNotifier { await _contactStore.saveContacts(_contacts); appLogger.info('Saved contacts to storage', tag: 'Connector'); + // Update any in-flight retries so they use the new path override + _retryService?.updatePendingContact(_contacts[index]); + // If setting a specific path (not flood, not auto), also sync with device if (pathLen != null && pathLen >= 0 && pathBytes != null) { appLogger.info('Sending path to device...', tag: 'Connector'); @@ -1942,27 +2052,27 @@ class MeshCoreConnector extends ChangeNotifier { final autoRotationEnabled = _appSettingsService?.settings.autoRouteRotationEnabled == true; if (autoRotationEnabled && contact.pathOverride == null) { - autoSelection = _pathHistoryService?.getNextAutoPathSelection( + final maxRetries = _appSettingsService?.settings.maxMessageRetries ?? 5; + autoSelection = _selectAutoPathForAttempt( contact.publicKeyHex, + attemptIndex: 0, + maxRetries: maxRetries, ); - if (autoSelection != null) { - _pathHistoryService?.recordPathAttempt( - contact.publicKeyHex, - autoSelection, - ); - } } - final pathBytes = _resolveOutgoingPathBytes(contact, autoSelection); - final pathLength = _resolveOutgoingPathLength(contact, autoSelection) ?? -1; + final resolved = resolvePathSelection(contact, selection: autoSelection); - if (pathLength < 0) { + if (resolved.useFlood) { await clearContactPath(contact); } else { - await setContactPath(contact, pathBytes, pathLength); + await setContactPath( + contact, + Uint8List.fromList(resolved.pathBytes), + resolved.hopCount, + ); } - return _selectionFromPath(pathLength, pathBytes); + return resolved; } void trackRepeaterAck({ @@ -2626,6 +2736,7 @@ class MeshCoreConnector extends ChangeNotifier { case pushCodeStatusResponse: break; case pushCodeLogRxData: + _lastRadioRxTime = DateTime.now(); _handleRxData(frame); _handleLogRxData(frame); break; @@ -2929,16 +3040,17 @@ class MeshCoreConnector extends ChangeNotifier { /// Physics-based worst-case timeout (ceiling). int _physicsMaxTimeout(int pathLength, int airtime) { if (pathLength < 0) { + // Match firmware: SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * airtime) return 500 + (16 * airtime); } else { return 500 + ((airtime * 6 + 250) * (pathLength + 1)); } } - /// Physics-based minimum timeout (floor): raw traversal time. int _physicsMinTimeout(int pathLength, int airtime) { if (pathLength < 0) { - return airtime; + // Same as max for flood — firmware uses a single formula + return 500 + (16 * airtime); } else { return airtime * (pathLength + 1); } @@ -2955,7 +3067,7 @@ class MeshCoreConnector extends ChangeNotifier { final physicsMin = _physicsMinTimeout(pathLength, airtime); final physicsMax = _physicsMaxTimeout(pathLength, airtime); - // Try ML-based prediction, clamped between physics bounds + // Try ML-based prediction final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; final mlTimeout = _timeoutPredictionService?.predictTimeout( contactKey: contactKey, @@ -2964,9 +3076,14 @@ class MeshCoreConnector extends ChangeNotifier { secondsSinceLastRx: secSinceRx, ); if (mlTimeout != null) { + if (pathLength < 0) { + // Flood: trust ML, only enforce firmware formula as floor + return mlTimeout.clamp(physicsMin, mlTimeout); + } return mlTimeout.clamp(physicsMin, physicsMax); } + // No ML data — use firmware formula return physicsMax; } @@ -3255,6 +3372,9 @@ class MeshCoreConnector extends ChangeNotifier { } if (message != null) { + if (!message.isOutgoing) { + _lastContactMsgRxTime = DateTime.now(); + } // Ignore messages from self (device hearing its own broadcast) // BUT allow repeated messages (pathLength indicates it went through repeater) if (_selfPublicKey != null && @@ -3302,7 +3422,6 @@ class MeshCoreConnector extends ChangeNotifier { _appSettingsService != null) { final settings = _appSettingsService!.settings; if (settings.notificationsEnabled && settings.notifyOnNewMessage) { - // Find the contact name if (contact?.type == advTypeChat) { _notificationService.showMessageNotification( contactName: contact?.name ?? 'Unknown', @@ -3313,7 +3432,9 @@ class MeshCoreConnector extends ChangeNotifier { } else if (contact?.type == advTypeRoom) { _notificationService.showMessageNotification( contactName: contact?.name ?? 'Unknown Room', - message: message.text.substring(4), + message: message.text.length > 4 + ? message.text.substring(4) + : message.text, contactId: message.senderKeyHex, badgeCount: getTotalUnreadCount(), ); @@ -3488,6 +3609,7 @@ class MeshCoreConnector extends ChangeNotifier { _notificationService.showChannelMessageNotification( channelName: label, + senderName: message.senderName, message: message.text, channelIndex: channelIndex, badgeCount: getTotalUnreadCount(), @@ -3495,14 +3617,20 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleIncomingChannelMessage(Uint8List frame) { - final message = ChannelMessage.fromFrame(frame); - if (message != null && message.channelIndex != null) { + final parsed = ChannelMessage.fromFrame(frame); + if (parsed != null && parsed.channelIndex != null) { if (_shouldDropSelfChannelMessage( - message.senderName, - message.pathBytes, + parsed.senderName, + parsed.pathBytes, )) { return; } + final contentHash = _computeContentHash( + parsed.channelIndex!, + parsed.timestamp.millisecondsSinceEpoch ~/ 1000, + '${parsed.senderName}: ${parsed.text}', + ); + final message = parsed.copyWith(packetHash: contentHash); _updateContactLastMessageAtByName( message.senderName, message.timestamp, @@ -3554,6 +3682,8 @@ class MeshCoreConnector extends ChangeNotifier { return; } + final pktHash = _computePacketHash(packet.payloadType, packet.payload); + final message = ChannelMessage( senderKey: null, senderName: parsed.senderName, @@ -3561,9 +3691,10 @@ class MeshCoreConnector extends ChangeNotifier { timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000), isOutgoing: false, status: ChannelMessageStatus.sent, - pathLength: packet.isFlood ? packet.pathBytes.length : 0, + pathLength: packet.isFlood ? packet.hopCount : 0, pathBytes: packet.pathBytes, channelIndex: channel.index, + packetHash: pktHash, ); _updateContactLastMessageAtByName( @@ -3611,21 +3742,13 @@ class MeshCoreConnector extends ChangeNotifier { final retryService = _retryService; if (retryService != null && - retryService.updateMessageFromSent( - ackHash, - timeoutMs, - allowQueueFallback: false, - )) { + retryService.updateMessageFromSent(ackHash, timeoutMs)) { return; } if (_markNextPendingChannelMessageSent()) { return; } - - if (retryService != null) { - retryService.updateMessageFromSent(ackHash, timeoutMs); - } } else { // Fallback to old behavior for (var messages in _conversations.values) { @@ -4016,55 +4139,98 @@ class MeshCoreConnector extends ChangeNotifier { ReactionInfo reactionInfo, String contactPubKeyHex, ) { - // Find target message by computing hash and comparing - final targetHash = reactionInfo.targetHash; final contact = _contacts.cast().firstWhere( (c) => c?.publicKeyHex == contactPubKeyHex, orElse: () => null, ); final isRoomServer = contact?.type == advTypeRoom; + ReactionHelper.applyReaction( + messages: messages, + reactionInfo: reactionInfo, + // Incoming reactions in 1:1: match against outgoing messages only + shouldSkip: (msg) => isRoomServer != true && !msg.isOutgoing, + getTimestampSecs: (msg) => msg.timestamp.millisecondsSinceEpoch ~/ 1000, + getSenderName: (msg) => + _resolveContactSenderName(msg, contact, isRoomServer == true), + getMessageText: (msg) => msg.text, + getReactions: (msg) => msg.reactions, + updateMessage: (i, reactions) { + messages[i] = messages[i].copyWith(reactions: reactions); + }, + ); + } + + void _processOutgoingContactReaction( + List messages, + ReactionInfo reactionInfo, + Contact contact, + ) { + final isRoomServer = contact.type == advTypeRoom; + + ReactionHelper.applyReaction( + messages: messages, + reactionInfo: reactionInfo, + // Outgoing reactions in 1:1: match against incoming messages + shouldSkip: (msg) => !isRoomServer && msg.isOutgoing, + getTimestampSecs: (msg) => msg.timestamp.millisecondsSinceEpoch ~/ 1000, + getSenderName: (msg) => + _resolveContactSenderName(msg, contact, isRoomServer), + getMessageText: (msg) => msg.text, + getReactions: (msg) => msg.reactions, + updateMessage: (i, reactions) { + messages[i] = messages[i].copyWith(reactions: reactions); + }, + ); + } + + void _setReactionStatus( + String pubKeyHex, + ReactionInfo reactionInfo, + MessageStatus status, + ) { + final messages = _conversations[pubKeyHex]; + if (messages == null) return; + final contact = _contacts.cast().firstWhere( + (c) => c?.publicKeyHex == pubKeyHex, + orElse: () => null, + ); + final isRoomServer = contact?.type == advTypeRoom; for (int i = messages.length - 1; i >= 0; i--) { final msg = messages[i]; - - // For 1:1 chats: contact reacts to my outgoing messages only - // For room servers: any message can be reacted to (multi-user) - if (!isRoomServer && !msg.isOutgoing) continue; - final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000; - - // For room servers, include sender name (resolve from fourByteRoomContactKey) - // For 1:1 chats, sender is implicit (null) - String? senderName; - if (isRoomServer && !msg.isOutgoing) { - final senderContact = _contacts.cast().firstWhere( - (c) => - c != null && - _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), - orElse: () => null, - ); - senderName = senderContact?.name; - } else if (isRoomServer && msg.isOutgoing) { - senderName = selfName; - } - // For 1:1, senderName stays null - final msgHash = ReactionHelper.computeReactionHash( timestampSecs, - senderName, + _resolveContactSenderName(msg, contact, isRoomServer == true), msg.text, ); - if (msgHash == targetHash) { - final currentReactions = Map.from(msg.reactions); - currentReactions[reactionInfo.emoji] = - (currentReactions[reactionInfo.emoji] ?? 0) + 1; - - messages[i] = msg.copyWith(reactions: currentReactions); + if (msgHash == reactionInfo.targetHash) { + final statuses = Map.from(msg.reactionStatuses); + statuses[reactionInfo.emoji] = status; + messages[i] = msg.copyWith(reactionStatuses: statuses); break; } } } + String? _resolveContactSenderName( + Message msg, + Contact? contact, + bool isRoomServer, + ) { + if (!isRoomServer) return null; + if (!msg.isOutgoing) { + final senderContact = _contacts.cast().firstWhere( + (c) => + c != null && + _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey), + orElse: () => null, + ); + return senderContact?.name; + } + return selfName; + } + _RawPacket? _parseRawPacket(Uint8List raw) { if (raw.length < 3) return null; var index = 0; @@ -4077,10 +4243,11 @@ class MeshCoreConnector extends ChangeNotifier { index += 4; } if (raw.length <= index) return null; - final pathLen = raw[index++]; - if (raw.length < index + pathLen) return null; - final pathBytes = Uint8List.fromList(raw.sublist(index, index + pathLen)); - index += pathLen; + final pathLenRaw = raw[index++]; + final pathByteLen = _decodePathByteLen(pathLenRaw); + if (raw.length < index + pathByteLen) return null; + final pathBytes = Uint8List.fromList(raw.sublist(index, index + pathByteLen)); + index += pathByteLen; if (raw.length <= index) return null; final payload = Uint8List.fromList(raw.sublist(index)); @@ -4089,6 +4256,7 @@ class MeshCoreConnector extends ChangeNotifier { routeType: routeType, payloadType: (header >> _phTypeShift) & _phTypeMask, payloadVer: (header >> _phVerShift) & _phVerMask, + pathLenRaw: pathLenRaw, pathBytes: pathBytes, payload: payload, ); @@ -4099,6 +4267,30 @@ class MeshCoreConnector extends ChangeNotifier { return digest[0]; } + /// Firmware-compatible packet hash: SHA256(payloadType + payload) -> first 8 bytes as hex. + String _computePacketHash(int payloadType, Uint8List payload) { + final input = Uint8List(1 + payload.length); + input[0] = payloadType; + input.setRange(1, input.length, payload); + final digest = crypto.sha256.convert(input).bytes; + return digest.sublist(0, 8).map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + } + + /// Content-based dedup hash for sync queue messages (no raw payload available). + /// Prefixed with 'c:' to avoid collisions with packet hashes. + String _computeContentHash(int channelIdx, int timestampSecs, String fullText) { + final textBytes = utf8.encode(fullText); + final input = Uint8List(5 + textBytes.length); + input[0] = channelIdx; + input[1] = timestampSecs & 0xFF; + input[2] = (timestampSecs >> 8) & 0xFF; + input[3] = (timestampSecs >> 16) & 0xFF; + input[4] = (timestampSecs >> 24) & 0xFF; + input.setRange(5, 5 + textBytes.length, textBytes); + final digest = crypto.sha256.convert(input).bytes; + return 'c:${digest.sublist(0, 8).map((b) => b.toRadixString(16).padLeft(2, '0')).join()}'; + } + Uint8List? _decryptPayload(Uint8List psk, Uint8List encrypted) { if (encrypted.length <= _cipherMacSize) return null; final mac = encrypted.sublist(0, _cipherMacSize); @@ -4146,63 +4338,6 @@ class MeshCoreConnector extends ChangeNotifier { return _ParsedText(senderName: 'Unknown', text: text); } - Uint8List _resolveOutgoingPathBytes( - Contact contact, - PathSelection? selection, - ) { - // Priority 1: Check user's path override - if (contact.pathOverride != null) { - if (contact.pathOverride! < 0) { - return Uint8List(0); // Force flood - } - return contact.pathOverrideBytes ?? Uint8List(0); - } - - // Priority 2: Check device flood mode or PathSelection flood - if (contact.pathLength < 0 || selection?.useFlood == true) { - return Uint8List(0); - } - - // Priority 3: Check PathSelection (auto-rotation) - if (selection != null && selection.pathBytes.isNotEmpty) { - return Uint8List.fromList(selection.pathBytes); - } - - // Priority 4: Use device's discovered path - return contact.path; - } - - int? _resolveOutgoingPathLength(Contact contact, PathSelection? selection) { - // Priority 1: Check user's path override - if (contact.pathOverride != null) { - return contact.pathOverride; - } - - // Priority 2: Check device flood mode or PathSelection flood - if (contact.pathLength < 0 || selection?.useFlood == true) { - return -1; - } - - // Priority 3: Check PathSelection (auto-rotation) - if (selection != null && selection.pathBytes.isNotEmpty) { - return selection.hopCount; - } - - // Priority 4: Use device's discovered path - return contact.pathLength; - } - - PathSelection _selectionFromPath(int pathLength, Uint8List pathBytes) { - if (pathLength < 0) { - return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true); - } - return PathSelection( - pathBytes: pathBytes, - hopCount: pathLength, - useFlood: false, - ); - } - bool _addChannelMessage(int channelIndex, ChannelMessage message) { _channelMessages.putIfAbsent(channelIndex, () => []); final messages = _channelMessages[channelIndex]!; @@ -4292,6 +4427,7 @@ class MeshCoreConnector extends ChangeNotifier { pathLength: mergedPathLength, pathBytes: mergedPathBytes, pathVariants: mergedPathVariants, + packetHash: existing.packetHash ?? processedMessage.packetHash, // Mark as sent when first repeat is heard status: promotedFromPending ? ChannelMessageStatus.sent @@ -4326,35 +4462,38 @@ class MeshCoreConnector extends ChangeNotifier { List messages, ReactionInfo reactionInfo, ) { - // Find target message by computing hash and comparing - final targetHash = reactionInfo.targetHash; - for (int i = messages.length - 1; i >= 0; i--) { - final msg = messages[i]; - final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000; - final msgHash = ReactionHelper.computeReactionHash( - timestampSecs, - msg.senderName, - msg.text, - ); - if (msgHash == targetHash) { - final currentReactions = Map.from(msg.reactions); - currentReactions[reactionInfo.emoji] = - (currentReactions[reactionInfo.emoji] ?? 0) + 1; - - messages[i] = msg.copyWith(reactions: currentReactions); + ReactionHelper.applyReaction( + messages: messages, + reactionInfo: reactionInfo, + shouldSkip: (_) => false, + getTimestampSecs: (msg) => msg.timestamp.millisecondsSinceEpoch ~/ 1000, + getSenderName: (msg) => msg.senderName, + getMessageText: (msg) => msg.text, + getReactions: (msg) => msg.reactions, + updateMessage: (i, reactions) { + messages[i] = messages[i].copyWith(reactions: reactions); notifyListeners(); - break; - } - } + }, + ); } int _findChannelRepeatIndex( List messages, ChannelMessage incoming, ) { + // First pass: match by packet hash (exact dedup) + final incomingHash = incoming.packetHash; + if (incomingHash != null) { + for (int i = messages.length - 1; i >= 0; i--) { + final existingHash = messages[i].packetHash; + if (existingHash != null && existingHash == incomingHash) { + return i; + } + } + } + // Second pass: heuristic fallback (outgoing echo, old messages without hash) for (int i = messages.length - 1; i >= 0; i--) { - final existing = messages[i]; - if (_isChannelRepeat(existing, incoming)) { + if (_isChannelRepeat(messages[i], incoming)) { return i; } } @@ -4368,7 +4507,7 @@ class MeshCoreConnector extends ChangeNotifier { (existing.timestamp.millisecondsSinceEpoch - incoming.timestamp.millisecondsSinceEpoch) .abs(); - if (diffMs > 5000) return false; + if (diffMs > 30000) return false; if (existing.senderName == incoming.senderName) return true; @@ -4613,8 +4752,9 @@ class MeshCoreConnector extends ChangeNotifier { packet.skipBytes(4); // Skip transport-specific bytes } //final payloadVer = (header >> 6) & 0x03; - final pathLen = packet.readByte(); - final pathBytes = packet.readBytes(pathLen); + final pathLenRaw = packet.readByte(); + final pathByteLen = _decodePathByteLen(pathLenRaw); + final pathBytes = packet.readBytes(pathByteLen); final payload = packet.readBytes(packet.remaining); final rawPacket = frame.sublist(3); @@ -4652,8 +4792,9 @@ class MeshCoreConnector extends ChangeNotifier { packet.skipBytes(4); // Skip transport-specific bytes } //final payloadVer = (header >> 6) & 0x03; - final pathLen = packet.readByte(); - pathBytes = packet.readBytes(pathLen); + final pathLenRaw = packet.readByte(); + final pathByteLen = _decodePathByteLen(pathLenRaw); + pathBytes = packet.readBytes(pathByteLen); } catch (e) { appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); return; @@ -4990,11 +5131,20 @@ const int _routeTransportDirect = 0x03; const int _payloadTypeGroupText = 0x05; const int _cipherMacSize = 2; +/// Decodes the firmware's encoded path_len byte into actual byte length. +/// Bits 0-5: hash count (0-63), Bits 6-7: hash size code (0=1byte, 1=2bytes, 2=3bytes). +int _decodePathByteLen(int pathLenRaw) { + final hashCount = pathLenRaw & 63; + final hashSize = ((pathLenRaw >> 6) & 0x03) + 1; + return hashCount * hashSize; +} + class _RawPacket { final int header; final int routeType; final int payloadType; final int payloadVer; + final int pathLenRaw; final Uint8List pathBytes; final Uint8List payload; @@ -5003,12 +5153,15 @@ class _RawPacket { required this.routeType, required this.payloadType, required this.payloadVer, + required this.pathLenRaw, required this.pathBytes, required this.payload, }); bool get isFlood => routeType == _routeFlood || routeType == _routeTransportFlood; + + int get hopCount => pathLenRaw & 63; } class _ParsedText { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index dc9a9f5..1a0ada1 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -509,7 +509,7 @@ Uint8List buildSendTextMsgFrame( final writer = BufferWriter(); writer.writeByte(cmdSendTxtMsg); writer.writeByte(txtTypePlain); - writer.writeByte(attempt.clamp(0, 3)); + writer.writeByte(attempt.clamp(0, 255)); writer.writeUInt32LE(timestamp); writer.writeBytes(recipientPubKey.sublist(0, 6)); writer.writeString(text); @@ -838,7 +838,7 @@ Uint8List buildSendCliCommandFrame( final writer = BufferWriter(); writer.writeByte(cmdSendTxtMsg); writer.writeByte(txtTypeCliData); - writer.writeByte(attempt.clamp(0, 3)); + writer.writeByte(attempt.clamp(0, 255)); writer.writeUInt32LE(timestamp); writer.writeBytes(repeaterPubKey.sublist(0, 6)); writer.writeString(command); diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart index 7a032ef..57a5e59 100644 --- a/lib/helpers/link_handler.dart +++ b/lib/helpers/link_handler.dart @@ -1,8 +1,47 @@ import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:url_launcher/url_launcher.dart'; import '../l10n/l10n.dart'; +import '../utils/platform_info.dart'; class LinkHandler { + /// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile. + static Widget buildLinkifyText({ + required BuildContext context, + required String text, + required TextStyle style, + TextStyle? linkStyle, + }) { + final effectiveLinkStyle = + linkStyle ?? + style.copyWith( + color: Colors.green, + decoration: TextDecoration.underline, + ); + const options = LinkifyOptions(humanize: false, defaultToHttps: false); + const linkifiers = [UrlLinkifier()]; + void onOpen(LinkableElement link) => handleLinkTap(context, link.url); + + if (PlatformInfo.isDesktop) { + return SelectableLinkify( + text: text, + style: style, + linkStyle: effectiveLinkStyle, + options: options, + linkifiers: linkifiers, + onOpen: onOpen, + ); + } + return Linkify( + text: text, + style: style, + linkStyle: effectiveLinkStyle, + options: options, + linkifiers: linkifiers, + onOpen: onOpen, + ); + } + static Future handleLinkTap(BuildContext context, String url) async { // Show confirmation dialog final shouldOpen = await showDialog( diff --git a/lib/helpers/path_helper.dart b/lib/helpers/path_helper.dart new file mode 100644 index 0000000..fe51d63 --- /dev/null +++ b/lib/helpers/path_helper.dart @@ -0,0 +1,31 @@ +import '../models/contact.dart'; +import '../connector/meshcore_protocol.dart'; + +class PathHelper { + static String formatPathHex(List pathBytes) { + return pathBytes + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(','); + } + + static String resolvePathNames( + List pathBytes, + List allContacts, + ) { + return pathBytes + .map((b) { + final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase(); + final matches = allContacts + .where( + (c) => + c.publicKey.first == b && + (c.type == advTypeRepeater || c.type == advTypeRoom), + ) + .toList(); + if (matches.isEmpty) return hex; + if (matches.length == 1) return matches.first.name; + return matches.map((c) => c.name).join(' | '); + }) + .join(' \u2192 '); + } +} diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart index 88138d6..90733c3 100644 --- a/lib/helpers/reaction_helper.dart +++ b/lib/helpers/reaction_helper.dart @@ -8,6 +8,50 @@ class ReactionInfo { } class ReactionHelper { + /// Apply a reaction to a list of messages by matching the reaction hash. + /// + /// [messages] - the message list to search + /// [reactionInfo] - the parsed reaction + /// [getTimestampSecs] - extract timestamp seconds from a message + /// [getSenderName] - extract sender name for hash (null for 1:1 implicit) + /// [getMessageText] - extract message text + /// [getReactions] - extract current reactions map + /// [shouldSkip] - filter function to skip messages (e.g., skip outgoing for incoming reactions) + /// [updateMessage] - callback to update the message at index with new reactions + /// + /// Returns whether a match was found. + static bool applyReaction({ + required List messages, + required ReactionInfo reactionInfo, + required int Function(T) getTimestampSecs, + required String? Function(T) getSenderName, + required String Function(T) getMessageText, + required Map Function(T) getReactions, + required bool Function(T) shouldSkip, + required void Function(int index, Map newReactions) + updateMessage, + }) { + final targetHash = reactionInfo.targetHash; + for (int i = messages.length - 1; i >= 0; i--) { + final msg = messages[i]; + if (shouldSkip(msg)) continue; + + final msgHash = computeReactionHash( + getTimestampSecs(msg), + getSenderName(msg), + getMessageText(msg), + ); + if (msgHash == targetHash) { + final currentReactions = Map.from(getReactions(msg)); + currentReactions[reactionInfo.emoji] = + (currentReactions[reactionInfo.emoji] ?? 0) + 1; + updateMessage(i, currentReactions); + return true; + } + } + return false; + } + static List? _cachedEmojis; /// Combined list of all reaction emojis in fixed order. diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index ca27f8c..b8ea08f 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "Връзката TCP изтекла.", "tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}", "map_showDiscoveryContacts": "Покажи контакти за откриване", - "map_setAsMyLocation": "Задайте като моя местоположение" + "map_setAsMyLocation": "Задайте като моя местоположение", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeight": "Първоначална тежест на маршрута", + "appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута", + "appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути", + "appSettings_maxRouteWeightSubtitle": "Максималното тегло, което един маршрут може да събере от успешни доставки.", + "appSettings_routeWeightSuccessIncrement": "Увеличение на теглото за успех", + "appSettings_routeWeightSuccessIncrementSubtitle": "Тегло, добавено към път след успешно доставяне.", + "appSettings_routeWeightFailureDecrement": "Намаляване на теглото, свързано с неуспех", + "appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.", + "appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение", + "appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.", + "path_routeWeight": "{weight}/{max}" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index bd4aed5..681cff6 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1917,5 +1917,26 @@ "tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.", "tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}", "map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen", - "map_setAsMyLocation": "Als meine aktuelle Position festlegen" + "map_setAsMyLocation": "Als meine aktuelle Position festlegen", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade", + "appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.", + "appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge", + "appSettings_initialRouteWeight": "Anfangs-Streckengewicht", + "appSettings_routeWeightSuccessIncrement": "Erhöhung des Erfolgsgewichts", + "appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.", + "appSettings_routeWeightFailureDecrement": "Reduzierung des Gewichts bei Fehlern", + "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}" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5c95e60..3942afb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -269,6 +269,23 @@ "appSettings_autoRouteRotationSubtitle": "Cycle between best paths and flood mode", "appSettings_autoRouteRotationEnabled": "Auto route rotation enabled", "appSettings_autoRouteRotationDisabled": "Auto route rotation disabled", + "appSettings_maxRouteWeight": "Max Route Weight", + "appSettings_maxRouteWeightSubtitle": "Maximum weight a path can accumulate from successful deliveries", + "appSettings_initialRouteWeight": "Initial Route Weight", + "appSettings_initialRouteWeightSubtitle": "Starting weight for newly discovered paths", + "appSettings_routeWeightSuccessIncrement": "Success Weight Increment", + "appSettings_routeWeightSuccessIncrementSubtitle": "Weight added to a path after successful delivery", + "appSettings_routeWeightFailureDecrement": "Failure Weight Decrement", + "appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery", + "appSettings_maxMessageRetries": "Max Message Retries", + "appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed", + "path_routeWeight": "{weight}/{max}", + "@path_routeWeight": { + "placeholders": { + "weight": { "type": "String" }, + "max": { "type": "String" } + } + }, "appSettings_battery": "Battery", "appSettings_batteryChemistry": "Battery Chemistry", "appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 085b0c8..4a83680 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1917,5 +1917,26 @@ "tcpErrorTimedOut": "La conexión TCP ha caducado.", "tcpConnectionFailed": "Error en la conexión TCP: {error}", "map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento", - "map_setAsMyLocation": "Establecer mi ubicación" + "map_setAsMyLocation": "Establecer mi ubicación", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeight": "Peso inicial de la ruta", + "appSettings_maxRouteWeight": "Peso máximo permitido para la ruta", + "appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas", + "appSettings_maxRouteWeightSubtitle": "Peso máximo que una ruta puede acumular gracias a entregas exitosas.", + "appSettings_routeWeightSuccessIncrement": "Incremento de peso para el éxito", + "appSettings_routeWeightSuccessIncrementSubtitle": "Peso añadido a una ruta después de una entrega exitosa.", + "appSettings_routeWeightFailureDecrement": "Reducción del peso asociado al fallo", + "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}" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b7617bb..1d684bb 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "La connexion TCP a expiré.", "tcpConnectionFailed": "Échec de la connexion TCP : {error}", "map_showDiscoveryContacts": "Afficher les contacts de découverte", - "map_setAsMyLocation": "Définir comme ma localisation" + "map_setAsMyLocation": "Définir comme ma localisation", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.", + "appSettings_initialRouteWeight": "Poids initial de l'itinéraire", + "appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet", + "appSettings_initialRouteWeightSubtitle": "Poids de départ pour les nouveaux chemins découverts", + "appSettings_routeWeightSuccessIncrement": "Augmentation du poids de réussite", + "appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un itinéraire après une livraison réussie.", + "appSettings_routeWeightFailureDecrement": "Réduction du poids de pénalité", + "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}" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 728eaac..55a1054 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "La connessione TCP è scaduta.", "tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}", "map_showDiscoveryContacts": "Mostra Contatti di Discovery", - "map_setAsMyLocation": "Imposta come la mia posizione" + "map_setAsMyLocation": "Imposta come la mia posizione", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeight": "Peso iniziale del percorso", + "appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi", + "appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.", + "appSettings_maxRouteWeight": "Massimo peso consentito per il percorso", + "appSettings_routeWeightSuccessIncrement": "Aumento del peso del successo", + "appSettings_routeWeightSuccessIncrementSubtitle": "Peso aggiunto a un percorso dopo una consegna riuscita.", + "appSettings_routeWeightFailureDecrement": "Riduzione del peso associato al fallimento", + "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}" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b38c08f..84b5432 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1360,6 +1360,72 @@ abstract class AppLocalizations { /// **'Auto route rotation disabled'** String get appSettings_autoRouteRotationDisabled; + /// No description provided for @appSettings_maxRouteWeight. + /// + /// In en, this message translates to: + /// **'Max Route Weight'** + String get appSettings_maxRouteWeight; + + /// No description provided for @appSettings_maxRouteWeightSubtitle. + /// + /// In en, this message translates to: + /// **'Maximum weight a path can accumulate from successful deliveries'** + String get appSettings_maxRouteWeightSubtitle; + + /// No description provided for @appSettings_initialRouteWeight. + /// + /// In en, this message translates to: + /// **'Initial Route Weight'** + String get appSettings_initialRouteWeight; + + /// No description provided for @appSettings_initialRouteWeightSubtitle. + /// + /// In en, this message translates to: + /// **'Starting weight for newly discovered paths'** + String get appSettings_initialRouteWeightSubtitle; + + /// No description provided for @appSettings_routeWeightSuccessIncrement. + /// + /// In en, this message translates to: + /// **'Success Weight Increment'** + String get appSettings_routeWeightSuccessIncrement; + + /// No description provided for @appSettings_routeWeightSuccessIncrementSubtitle. + /// + /// In en, this message translates to: + /// **'Weight added to a path after successful delivery'** + String get appSettings_routeWeightSuccessIncrementSubtitle; + + /// No description provided for @appSettings_routeWeightFailureDecrement. + /// + /// In en, this message translates to: + /// **'Failure Weight Decrement'** + String get appSettings_routeWeightFailureDecrement; + + /// No description provided for @appSettings_routeWeightFailureDecrementSubtitle. + /// + /// In en, this message translates to: + /// **'Weight removed from a path after failed delivery'** + String get appSettings_routeWeightFailureDecrementSubtitle; + + /// No description provided for @appSettings_maxMessageRetries. + /// + /// In en, this message translates to: + /// **'Max Message Retries'** + String get appSettings_maxMessageRetries; + + /// No description provided for @appSettings_maxMessageRetriesSubtitle. + /// + /// In en, this message translates to: + /// **'Number of retry attempts before marking a message as failed'** + String get appSettings_maxMessageRetriesSubtitle; + + /// No description provided for @path_routeWeight. + /// + /// In en, this message translates to: + /// **'{weight}/{max}'** + String path_routeWeight(String weight, String max); + /// No description provided for @appSettings_battery. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 96b67d8..2821617 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -695,6 +695,51 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Автоматично маршрутизирането е деактивирано'; + @override + String get appSettings_maxRouteWeight => + 'Максимално допустимо тегло на маршрута'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Максималното тегло, което един маршрут може да събере от успешни доставки.'; + + @override + String get appSettings_initialRouteWeight => + 'Първоначална тежест на маршрута'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Начално тегло за новооткрити маршрути'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Увеличение на теглото за успех'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Тегло, добавено към път след успешно доставяне.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Намаляване на теглото, свързано с неуспех'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Тегло, което е било премахнато от пътя след неуспешен опит за доставка.'; + + @override + String get appSettings_maxMessageRetries => + 'Максимален брой опити за изпращане на съобщение'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Батерия'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index dcbcd3f..337915e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -695,6 +695,49 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Automatische Routenrotation deaktiviert'; + @override + String get appSettings_maxRouteWeight => 'Maximale Gesamtstreckenlänge'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.'; + + @override + String get appSettings_initialRouteWeight => 'Anfangs-Streckengewicht'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Ausgangsgewicht für neu entdeckte Pfade'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Erhöhung des Erfolgsgewichts'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Reduzierung des Gewichts bei Fehlern'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde'; + + @override + String get appSettings_maxMessageRetries => + 'Maximale Anzahl an Wiederholungsversuchen'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Akku'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 01127c6..1e4e5b0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -684,6 +684,48 @@ class AppLocalizationsEn extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Auto route rotation disabled'; + @override + String get appSettings_maxRouteWeight => 'Max Route Weight'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Maximum weight a path can accumulate from successful deliveries'; + + @override + String get appSettings_initialRouteWeight => 'Initial Route Weight'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Starting weight for newly discovered paths'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Success Weight Increment'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Weight added to a path after successful delivery'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Failure Weight Decrement'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Weight removed from a path after failed delivery'; + + @override + String get appSettings_maxMessageRetries => 'Max Message Retries'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Number of retry attempts before marking a message as failed'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Battery'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index fac431e..657d556 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -694,6 +694,49 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Rotación de ruta automática desactivada'; + @override + String get appSettings_maxRouteWeight => 'Peso máximo permitido para la ruta'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Peso máximo que una ruta puede acumular gracias a entregas exitosas.'; + + @override + String get appSettings_initialRouteWeight => 'Peso inicial de la ruta'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Peso inicial para rutas recién descubiertas'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Incremento de peso para el éxito'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Peso añadido a una ruta después de una entrega exitosa.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Reducción del peso asociado al fallo'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Peso retirado de un camino después de un intento de entrega fallido.'; + + @override + String get appSettings_maxMessageRetries => + 'Número máximo de reintentos de envío de mensajes'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Número de intentos de reintento antes de marcar un mensaje como fallido.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Batería'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6932437..7aa7ebe 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -698,6 +698,50 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Rotation de l\'itinéraire automatique désactivée'; + @override + String get appSettings_maxRouteWeight => + 'Poids maximal autorisé pour le trajet'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Poids maximal qu\'un itinéraire peut accumuler grâce à des livraisons réussies.'; + + @override + String get appSettings_initialRouteWeight => 'Poids initial de l\'itinéraire'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Poids de départ pour les nouveaux chemins découverts'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Augmentation du poids de réussite'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Poids ajouté à un itinéraire après une livraison réussie.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Réduction du poids de pénalité'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Poids retiré d\'un itinéraire après une tentative de livraison infructueuse.'; + + @override + String get appSettings_maxMessageRetries => + 'Nombre maximal de tentatives de récupération de messages'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Batterie'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 68c2af3..02c5937 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -695,6 +695,50 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Rotazione del percorso automatico disabilitata'; + @override + String get appSettings_maxRouteWeight => + 'Massimo peso consentito per il percorso'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Il peso massimo che un percorso può accumulare grazie a consegne di successo.'; + + @override + String get appSettings_initialRouteWeight => 'Peso iniziale del percorso'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Peso di partenza per nuovi percorsi'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Aumento del peso del successo'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Peso aggiunto a un percorso dopo una consegna riuscita.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Riduzione del peso associato al fallimento'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Peso rimosso da un percorso dopo un tentativo di consegna fallito.'; + + @override + String get appSettings_maxMessageRetries => + 'Numero massimo di tentativi di invio del messaggio'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Numero di tentativi di riprova prima di considerare un messaggio come fallito.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Batteria'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 4031ddf..9e51164 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -689,6 +689,49 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Automatische route rotatie is uitgeschakeld'; + @override + String get appSettings_maxRouteWeight => 'Maximale gewicht voor de route'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.'; + + @override + String get appSettings_initialRouteWeight => 'เริ่มต้น gewicht van de route'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Startgewicht voor nieuwe, ontdekte routes'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Toename in het gewicht van het succes'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Gewicht wordt toegevoegd aan een route na een succesvolle levering.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Vermindering van het gewicht van fouten'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Gewicht verwijderd van een pad na een mislukte levering'; + + @override + String get appSettings_maxMessageRetries => + 'Aantal pogingen om berichten te versturen'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Batterij'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 6378e74..176c17e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -698,6 +698,49 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Automatyczne obracanie tras wyłączone'; + @override + String get appSettings_maxRouteWeight => + 'Maksymalny dopuszczalny ciężar pojazdu'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.'; + + @override + String get appSettings_initialRouteWeight => 'Początkowa waga trasy'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Początkowa waga dla nowych, odkrytych ścieżek'; + + @override + String get appSettings_routeWeightSuccessIncrement => 'Wzrost wagi sukcesu'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Waga dodana do ścieżki po pomyślnym dostarczeniu'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Zmniejszenie wagi kary'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Waga usunięta z trasy po nieudanej dostawie'; + + @override + String get appSettings_maxMessageRetries => + 'Maksymalna liczba prób wysłania wiadomości'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Bateria'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 908ad96..a51e1b0 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -696,6 +696,49 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Rotação de roteamento automático desativada'; + @override + String get appSettings_maxRouteWeight => 'Peso Máximo da Rota'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.'; + + @override + String get appSettings_initialRouteWeight => 'Peso Inicial da Rota'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Peso inicial para novos caminhos descobertos'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Aumento do peso para indicar sucesso'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Peso adicionado a um caminho após a entrega bem-sucedida.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Redução do peso da falha'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Peso removido de um caminho após uma tentativa de entrega malsucedida.'; + + @override + String get appSettings_maxMessageRetries => + 'Número máximo de tentativas de envio de mensagens'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Número de tentativas de reenvio antes de classificar uma mensagem como falha.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Bateria'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 67011fb..7a6998f 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -696,6 +696,50 @@ class AppLocalizationsRu extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Автоматическое переключение маршрутов отключено'; + @override + String get appSettings_maxRouteWeight => + 'Максимальный допустимый вес маршрута'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.'; + + @override + String get appSettings_initialRouteWeight => 'Начальный вес маршрута'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Начальный вес для новых, только что открытых маршрутов'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Увеличение веса успеха'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Вес, добавленный к маршруту после успешной доставки.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Уменьшение веса неудачи'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Вес, который был удален с пути после неудачной доставки.'; + + @override + String get appSettings_maxMessageRetries => + 'Максимальное количество повторных попыток отправки сообщения'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Батарея'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 4f033f9..ae6c956 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -687,6 +687,48 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Automatické prekladanie trás pozastavené'; + @override + String get appSettings_maxRouteWeight => 'Maximálna hmotnosť trasy'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.'; + + @override + String get appSettings_initialRouteWeight => 'Počiatočná váha trasy'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Počiatočná váha pre nové, objavené cesty'; + + @override + String get appSettings_routeWeightSuccessIncrement => 'Zvyšenie váhy úspechu'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Hmotnosť pridaná k trase po úspešnej doručení'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Sníženie váhy, ktorá sa používa na odhad rizika.'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Hmotnosť odstránená z cesty po neúspešnej doručenie'; + + @override + String get appSettings_maxMessageRetries => + 'Maximalný počet pokusov o doručenie správ'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Počet pokusov o odošleť pred označením správy ako neúspešnej'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Batéria'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e7c48f6..96501cd 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -687,6 +687,49 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Samodejno krmilno rotiranje je onemogočeno'; + @override + String get appSettings_maxRouteWeight => 'Največja dovoljena teža poti'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.'; + + @override + String get appSettings_initialRouteWeight => 'Izvirna teža poti'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Izguba teže za nove, odkriti poti'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Učinkovitost: povečanje'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Težava, dodana poti po uspešni dostavi'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Zmanjšanje teže, ki je povezana s pomanjkanjem'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Težo, ki ni bila uspešno dostavljena, odstranili s poti.'; + + @override + String get appSettings_maxMessageRetries => + 'Najve število poskusov pošiljanja sporočil'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Baterija'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6ccea2f..a834230 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -682,6 +682,48 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Automatisk ruttrotation är avstängd'; + @override + String get appSettings_maxRouteWeight => 'Maximalt tillåtet vikt för rutten'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.'; + + @override + String get appSettings_initialRouteWeight => 'Initial vikt för rutt'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Initial vikt för nyligen upptäckta vägar'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Ökning av vikt för framgång'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Vikt läggs till en väg efter en lyckad leverans.'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Minskning av vikten för misslyckande'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Vikt som tagits bort från en väg efter ett misslyckat leveransförsök'; + + @override + String get appSettings_maxMessageRetries => 'Maximalt antal försök'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Batteri'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 788c9d1..7db1cc7 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -692,6 +692,49 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_autoRouteRotationDisabled => 'Авторотація маршрутизації вимкнена'; + @override + String get appSettings_maxRouteWeight => 'Максимальна вага маршруту'; + + @override + String get appSettings_maxRouteWeightSubtitle => + 'Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.'; + + @override + String get appSettings_initialRouteWeight => 'Початкова вартість маршруту'; + + @override + String get appSettings_initialRouteWeightSubtitle => + 'Початкова вага для нових відкритих шляхів'; + + @override + String get appSettings_routeWeightSuccessIncrement => + 'Збільшення ваги успіху'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + 'Вага, додана до маршруту після успішної доставки'; + + @override + String get appSettings_routeWeightFailureDecrement => + 'Зменшення ваги помилки'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + 'Вага, яка була знята з маршруту після невдалої доставки'; + + @override + String get appSettings_maxMessageRetries => + 'Максимальна кількість повторних спроб надсилання повідомлення'; + + @override + String get appSettings_maxMessageRetriesSubtitle => + 'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => 'Батарея'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index be7eeb0..dc1a17e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -648,6 +648,43 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用'; + @override + String get appSettings_maxRouteWeight => '最大路径重量'; + + @override + String get appSettings_maxRouteWeightSubtitle => '一条路径可以累积的最大重量,取决于成功交付的数量。'; + + @override + String get appSettings_initialRouteWeight => '初始路线权重'; + + @override + String get appSettings_initialRouteWeightSubtitle => '新发现路径的初始重量'; + + @override + String get appSettings_routeWeightSuccessIncrement => '成功权重增加'; + + @override + String get appSettings_routeWeightSuccessIncrementSubtitle => + '在成功交付后,将重量添加到路径中'; + + @override + String get appSettings_routeWeightFailureDecrement => '失败权重降低'; + + @override + String get appSettings_routeWeightFailureDecrementSubtitle => + '从一条路径上移除的货物,由于无法成功交付而移除。'; + + @override + String get appSettings_maxMessageRetries => '最大消息重试次数'; + + @override + String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数'; + + @override + String path_routeWeight(String weight, String max) { + return '$weight/$max'; + } + @override String get appSettings_battery => '电池'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 648d711..3caea31 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "De TCP-verbinding is verlopen.", "tcpConnectionFailed": "Verbinding met TCP mislukt: {error}", "map_showDiscoveryContacts": "Ontdek contacten weergeven", - "map_setAsMyLocation": "Stel dit in als mijn locatie" + "map_setAsMyLocation": "Stel dit in als mijn locatie", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.", + "appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route", + "appSettings_maxRouteWeight": "Maximale gewicht voor de route", + "appSettings_initialRouteWeightSubtitle": "Startgewicht voor nieuwe, ontdekte routes", + "appSettings_routeWeightSuccessIncrement": "Toename in het gewicht van het succes", + "appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht wordt toegevoegd aan een route na een succesvolle levering.", + "appSettings_routeWeightFailureDecrement": "Vermindering van het gewicht van fouten", + "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}" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index f4f3ac7..c6e3fc4 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.", "tcpConnectionFailed": "Błąd połączenia TCP: {error}", "map_showDiscoveryContacts": "Pokaż kontakty odkrywania", - "map_setAsMyLocation": "Ustaw jako moje lokalizację" + "map_setAsMyLocation": "Ustaw jako moje lokalizację", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeight": "Początkowa waga trasy", + "appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu", + "appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek", + "appSettings_maxRouteWeightSubtitle": "Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.", + "appSettings_routeWeightSuccessIncrement": "Wzrost wagi sukcesu", + "appSettings_routeWeightSuccessIncrementSubtitle": "Waga dodana do ścieżki po pomyślnym dostarczeniu", + "appSettings_routeWeightFailureDecrement": "Zmniejszenie wagi kary", + "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}" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index dd1698c..e7e2ec6 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "A conexão TCP expirou.", "tcpConnectionFailed": "Falha na conexão TCP: {error}", "map_showDiscoveryContacts": "Mostrar Contatos de Descoberta", - "map_setAsMyLocation": "Defina minha localização" + "map_setAsMyLocation": "Defina minha localização", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeight": "Peso Inicial da Rota", + "appSettings_maxRouteWeight": "Peso Máximo da Rota", + "appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.", + "appSettings_initialRouteWeightSubtitle": "Peso inicial para novos caminhos descobertos", + "appSettings_routeWeightSuccessIncrement": "Aumento do peso para indicar sucesso", + "appSettings_routeWeightSuccessIncrementSubtitle": "Peso adicionado a um caminho após a entrega bem-sucedida.", + "appSettings_routeWeightFailureDecrement": "Redução do peso da falha", + "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}" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index ea75aca..92a3800 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1129,5 +1129,26 @@ "tcpErrorTimedOut": "Соединение TCP не удалось установить.", "tcpConnectionFailed": "Не удалось установить соединение TCP: {error}", "map_showDiscoveryContacts": "Показать контакты Discovery", - "map_setAsMyLocation": "Установить мое местоположение" + "map_setAsMyLocation": "Установить мое местоположение", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута", + "appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.", + "appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов", + "appSettings_initialRouteWeight": "Начальный вес маршрута", + "appSettings_routeWeightSuccessIncrement": "Увеличение веса успеха", + "appSettings_routeWeightSuccessIncrementSubtitle": "Вес, добавленный к маршруту после успешной доставки.", + "appSettings_routeWeightFailureDecrement": "Уменьшение веса неудачи", + "appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.", + "appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения", + "appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.", + "path_routeWeight": "{weight}/{max}" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 636556e..75a7c7d 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "Pripojenie TCP vypršalo.", "tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}", "map_showDiscoveryContacts": "Zobraziť kontakty objavov", - "map_setAsMyLocation": "Nastavte ako moju polohu" + "map_setAsMyLocation": "Nastavte ako moju polohu", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.", + "appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty", + "appSettings_initialRouteWeight": "Počiatočná váha trasy", + "appSettings_maxRouteWeight": "Maximálna hmotnosť trasy", + "appSettings_routeWeightSuccessIncrement": "Zvyšenie váhy úspechu", + "appSettings_routeWeightSuccessIncrementSubtitle": "Hmotnosť pridaná k trase po úspešnej doručení", + "appSettings_routeWeightFailureDecrement": "Sníženie váhy, ktorá sa používa na odhad rizika.", + "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}" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index dfc5a69..5ab4736 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.", "tcpConnectionFailed": "Napaka pri povezavi TCP: {error}", "map_showDiscoveryContacts": "Prikaži odkritja kontaktov", - "map_setAsMyLocation": "Nastavite to kot mojo lokacijo" + "map_setAsMyLocation": "Nastavite to kot mojo lokacijo", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.", + "appSettings_initialRouteWeight": "Izvirna teža poti", + "appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti", + "appSettings_maxRouteWeight": "Največja dovoljena teža poti", + "appSettings_routeWeightSuccessIncrement": "Učinkovitost: povečanje", + "appSettings_routeWeightSuccessIncrementSubtitle": "Težava, dodana poti po uspešni dostavi", + "appSettings_routeWeightFailureDecrement": "Zmanjšanje teže, ki je povezana s pomanjkanjem", + "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}" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 6a8d801..644b43b 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.", "tcpConnectionFailed": "Fel vid TCP-anslutning: {error}", "map_showDiscoveryContacts": "Visa Discovery-kontakter", - "map_setAsMyLocation": "Ange som min plats" + "map_setAsMyLocation": "Ange som min plats", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar", + "appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten", + "appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.", + "appSettings_initialRouteWeight": "Initial vikt för rutt", + "appSettings_routeWeightSuccessIncrement": "Ökning av vikt för framgång", + "appSettings_routeWeightSuccessIncrementSubtitle": "Vikt läggs till en väg efter en lyckad leverans.", + "appSettings_routeWeightFailureDecrement": "Minskning av vikten för misslyckande", + "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}" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index a50bd78..249fd3b 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1889,5 +1889,26 @@ "tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.", "tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}", "map_showDiscoveryContacts": "Показати контакти Відкриття", - "map_setAsMyLocation": "Встановити моє місцезнаходження" + "map_setAsMyLocation": "Встановити моє місцезнаходження", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_initialRouteWeight": "Початкова вартість маршруту", + "appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів", + "appSettings_maxRouteWeight": "Максимальна вага маршруту", + "appSettings_maxRouteWeightSubtitle": "Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.", + "appSettings_routeWeightSuccessIncrement": "Збільшення ваги успіху", + "appSettings_routeWeightSuccessIncrementSubtitle": "Вага, додана до маршруту після успішної доставки", + "appSettings_routeWeightFailureDecrement": "Зменшення ваги помилки", + "appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки", + "appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення", + "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале", + "path_routeWeight": "{weight}/{max}" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 54d1e3c..1d4ed30 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1894,5 +1894,26 @@ "tcpErrorTimedOut": "TCP 连接超时。", "tcpConnectionFailed": "TCP 连接失败:{error}", "map_showDiscoveryContacts": "显示发现联系人", - "map_setAsMyLocation": "设置为我的位置" + "map_setAsMyLocation": "设置为我的位置", + "@path_routeWeight": { + "placeholders": { + "weight": { + "type": "String" + }, + "max": { + "type": "String" + } + } + }, + "appSettings_maxRouteWeight": "最大路径重量", + "appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量", + "appSettings_initialRouteWeight": "初始路线权重", + "appSettings_maxRouteWeightSubtitle": "一条路径可以累积的最大重量,取决于成功交付的数量。", + "appSettings_routeWeightSuccessIncrement": "成功权重增加", + "appSettings_routeWeightSuccessIncrementSubtitle": "在成功交付后,将重量添加到路径中", + "appSettings_routeWeightFailureDecrement": "失败权重降低", + "appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。", + "appSettings_maxMessageRetries": "最大消息重试次数", + "appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数", + "path_routeWeight": "{weight}/{max}" } diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index fc84851..8ee904d 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -32,6 +32,11 @@ class AppSettings { final bool notifyOnNewChannelMessage; final bool notifyOnNewAdvert; final bool autoRouteRotationEnabled; + final double maxRouteWeight; + final double initialRouteWeight; + final double routeWeightSuccessIncrement; + final double routeWeightFailureDecrement; + final int maxMessageRetries; final String themeMode; final String? languageOverride; // null = system default final bool appDebugLogEnabled; @@ -62,6 +67,11 @@ class AppSettings { this.notifyOnNewChannelMessage = true, this.notifyOnNewAdvert = true, this.autoRouteRotationEnabled = false, + this.maxRouteWeight = 5.0, + this.initialRouteWeight = 3.0, + this.routeWeightSuccessIncrement = 0.5, + this.routeWeightFailureDecrement = 0.2, + this.maxMessageRetries = 5, this.themeMode = 'system', this.languageOverride, this.appDebugLogEnabled = false, @@ -96,6 +106,11 @@ class AppSettings { 'notify_on_new_channel_message': notifyOnNewChannelMessage, 'notify_on_new_advert': notifyOnNewAdvert, 'auto_route_rotation_enabled': autoRouteRotationEnabled, + 'max_route_weight': maxRouteWeight, + 'initial_route_weight': initialRouteWeight, + 'route_weight_success_increment': routeWeightSuccessIncrement, + 'route_weight_failure_decrement': routeWeightFailureDecrement, + 'max_message_retries': maxMessageRetries, 'theme_mode': themeMode, 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, @@ -142,6 +157,14 @@ class AppSettings { notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true, autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false, + maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0, + initialRouteWeight: + (json['initial_route_weight'] as num?)?.toDouble() ?? 3.0, + routeWeightSuccessIncrement: + (json['route_weight_success_increment'] as num?)?.toDouble() ?? 0.5, + routeWeightFailureDecrement: + (json['route_weight_failure_decrement'] as num?)?.toDouble() ?? 0.2, + maxMessageRetries: json['max_message_retries'] as int? ?? 5, themeMode: json['theme_mode'] as String? ?? 'system', languageOverride: json['language_override'] as String?, appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false, @@ -187,6 +210,11 @@ class AppSettings { bool? notifyOnNewChannelMessage, bool? notifyOnNewAdvert, bool? autoRouteRotationEnabled, + double? maxRouteWeight, + double? initialRouteWeight, + double? routeWeightSuccessIncrement, + double? routeWeightFailureDecrement, + int? maxMessageRetries, String? themeMode, Object? languageOverride = _unset, bool? appDebugLogEnabled, @@ -222,6 +250,13 @@ class AppSettings { notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert, autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, + maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight, + initialRouteWeight: initialRouteWeight ?? this.initialRouteWeight, + routeWeightSuccessIncrement: + routeWeightSuccessIncrement ?? this.routeWeightSuccessIncrement, + routeWeightFailureDecrement: + routeWeightFailureDecrement ?? this.routeWeightFailureDecrement, + maxMessageRetries: maxMessageRetries ?? this.maxMessageRetries, themeMode: themeMode ?? this.themeMode, languageOverride: languageOverride == _unset ? this.languageOverride diff --git a/lib/models/channel_message.dart b/lib/models/channel_message.dart index 2418871..b0af3eb 100644 --- a/lib/models/channel_message.dart +++ b/lib/models/channel_message.dart @@ -36,6 +36,7 @@ class ChannelMessage { final List pathVariants; final int? channelIndex; final String messageId; + final String? packetHash; final String? replyToMessageId; final String? replyToSenderName; final String? replyToText; @@ -55,6 +56,7 @@ class ChannelMessage { List? pathVariants, this.channelIndex, String? messageId, + this.packetHash, this.replyToMessageId, this.replyToSenderName, this.replyToText, @@ -79,6 +81,7 @@ class ChannelMessage { int? pathLength, Uint8List? pathBytes, List? pathVariants, + String? packetHash, String? replyToMessageId, String? replyToSenderName, String? replyToText, @@ -98,6 +101,7 @@ class ChannelMessage { pathVariants: pathVariants ?? this.pathVariants, channelIndex: channelIndex, messageId: messageId, + packetHash: packetHash ?? this.packetHash, replyToMessageId: replyToMessageId ?? this.replyToMessageId, replyToSenderName: replyToSenderName ?? this.replyToSenderName, replyToText: replyToText ?? this.replyToText, diff --git a/lib/models/contact.dart b/lib/models/contact.dart index c047622..71467b1 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -157,6 +157,12 @@ class Contact { return null; } final pubKey = reader.readBytes(pubKeySize); + + // Guard: reject contacts with zeroed or mostly-zeroed public keys + // (indicates corrupt flash storage on the firmware side) + final zeroCount = pubKey.where((b) => b == 0).length; + if (zeroCount > pubKeySize ~/ 2) return null; + final type = reader.readByte(); final flags = reader.readByte(); final pathLen = reader.readByte(); @@ -166,6 +172,12 @@ class Contact { final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen); final name = reader.readCStringGreedy(maxNameSize); + // Guard: reject contacts with non-printable names (corrupt flash data) + if (name.isNotEmpty && + name.codeUnits.every((c) => c < 0x20 || c == 0xFFFD)) { + return null; + } + final lastMod = reader.readUInt32LE(); double? lat, lon; @@ -182,7 +194,7 @@ class Contact { name: name.isEmpty ? 'Unknown' : name, type: type, flags: flags, - pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1, + pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen, path: pathBytes, latitude: lat, longitude: lon, diff --git a/lib/models/message.dart b/lib/models/message.dart index 4f42d96..6f6ed88 100644 --- a/lib/models/message.dart +++ b/lib/models/message.dart @@ -23,6 +23,7 @@ class Message { final int? pathLength; final Uint8List pathBytes; final Map reactions; + final Map reactionStatuses; final Uint8List fourByteRoomContactKey; Message({ @@ -43,9 +44,11 @@ class Message { Uint8List? pathBytes, Uint8List? fourByteRoomContactKey, Map? reactions, + Map? reactionStatuses, }) : pathBytes = pathBytes ?? Uint8List(0), fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0), - reactions = reactions ?? {}; + reactions = reactions ?? {}, + reactionStatuses = reactionStatuses ?? {}; String get senderKeyHex => pubKeyToHex(senderKey); @@ -61,6 +64,7 @@ class Message { Uint8List? pathBytes, bool? isCli, Map? reactions, + Map? reactionStatuses, Uint8List? fourByteRoomContactKey, }) { return Message( @@ -80,6 +84,7 @@ class Message { pathLength: pathLength ?? this.pathLength, pathBytes: pathBytes ?? this.pathBytes, reactions: reactions ?? this.reactions, + reactionStatuses: reactionStatuses ?? this.reactionStatuses, fourByteRoomContactKey: fourByteRoomContactKey ?? this.fourByteRoomContactKey, ); diff --git a/lib/models/path_history.dart b/lib/models/path_history.dart index 5e3ea1f..ff2d226 100644 --- a/lib/models/path_history.dart +++ b/lib/models/path_history.dart @@ -1,11 +1,12 @@ class PathRecord { final int hopCount; final int tripTimeMs; - final DateTime timestamp; + final DateTime? timestamp; final bool wasFloodDiscovery; final List pathBytes; final int successCount; final int failureCount; + final double routeWeight; PathRecord({ required this.hopCount, @@ -15,6 +16,7 @@ class PathRecord { required this.pathBytes, required this.successCount, required this.failureCount, + this.routeWeight = 1.0, }); String get displayText => @@ -24,11 +26,12 @@ class PathRecord { return { 'hop_count': hopCount, 'trip_time_ms': tripTimeMs, - 'timestamp': timestamp.toIso8601String(), + 'timestamp': timestamp?.toIso8601String(), 'was_flood': wasFloodDiscovery, 'path_bytes': pathBytes, 'success_count': successCount, 'failure_count': failureCount, + 'route_weight': routeWeight, }; } @@ -36,12 +39,15 @@ class PathRecord { return PathRecord( hopCount: json['hop_count'] as int, tripTimeMs: json['trip_time_ms'] as int, - timestamp: DateTime.parse(json['timestamp'] as String), + timestamp: json['timestamp'] != null + ? DateTime.parse(json['timestamp'] as String) + : null, wasFloodDiscovery: json['was_flood'] as bool, pathBytes: (json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [], successCount: json['success_count'] as int? ?? 0, failureCount: json['failure_count'] as int? ?? 0, + routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0, ); } } diff --git a/lib/models/path_selection.dart b/lib/models/path_selection.dart index 65f2f27..cdb3d72 100644 --- a/lib/models/path_selection.dart +++ b/lib/models/path_selection.dart @@ -1,3 +1,9 @@ +import 'dart:typed_data'; + +import 'contact.dart'; + +const int recentAttemptDiversityWindow = 2; + class PathSelection { final List pathBytes; final int hopCount; @@ -9,3 +15,38 @@ class PathSelection { required this.useFlood, }); } + +PathSelection resolvePathSelection( + Contact contact, { + PathSelection? selection, + bool forceFlood = false, +}) { + if (contact.pathOverride != null) { + if (contact.pathOverride! < 0) { + return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true); + } + return PathSelection( + pathBytes: contact.pathOverrideBytes ?? Uint8List(0), + hopCount: contact.pathOverride!, + useFlood: false, + ); + } + + if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) { + return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true); + } + + if (selection != null && selection.pathBytes.isNotEmpty) { + return PathSelection( + pathBytes: selection.pathBytes, + hopCount: selection.hopCount, + useFlood: false, + ); + } + + return PathSelection( + pathBytes: contact.path, + hopCount: contact.pathLength, + useFlood: false, + ); +} diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index a2c920e..7e0980e 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -310,6 +310,118 @@ class AppSettingsScreen extends StatelessWidget { ); }, ), + if (settingsService.settings.autoRouteRotationEnabled) ...[ + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxRouteWeight), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.appSettings_maxRouteWeightSubtitle), + Slider( + value: settingsService.settings.maxRouteWeight, + min: 1, + max: 10, + divisions: 9, + label: settingsService.settings.maxRouteWeight + .round() + .toString(), + onChanged: (value) => + settingsService.setMaxRouteWeight(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_initialRouteWeight), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.appSettings_initialRouteWeightSubtitle), + Slider( + value: settingsService.settings.initialRouteWeight, + min: 0.5, + max: 5.0, + divisions: 9, + label: settingsService.settings.initialRouteWeight + .toStringAsFixed(1), + onChanged: (value) => + settingsService.setInitialRouteWeight(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_routeWeightSuccessIncrement), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightSuccessIncrementSubtitle, + ), + Slider( + value: settingsService.settings.routeWeightSuccessIncrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService.settings.routeWeightSuccessIncrement + .toStringAsFixed(1), + onChanged: (value) => + settingsService.setRouteWeightSuccessIncrement(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_routeWeightFailureDecrement), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context + .l10n + .appSettings_routeWeightFailureDecrementSubtitle, + ), + Slider( + value: settingsService.settings.routeWeightFailureDecrement, + min: 0.1, + max: 2.0, + divisions: 19, + label: settingsService.settings.routeWeightFailureDecrement + .toStringAsFixed(1), + onChanged: (value) => + settingsService.setRouteWeightFailureDecrement(value), + ), + ], + ), + ), + const Divider(height: 1), + ListTile( + title: Text(context.l10n.appSettings_maxMessageRetries), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.l10n.appSettings_maxMessageRetriesSubtitle), + Slider( + value: settingsService.settings.maxMessageRetries + .toDouble(), + min: 2, + max: 10, + divisions: 8, + label: settingsService.settings.maxMessageRetries + .toString(), + onChanged: (value) => + settingsService.setMaxMessageRetries(value.toInt()), + ), + ], + ), + ), + ], ], ), ); diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 4e3743d..20110e1 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,11 +4,11 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; +import '../utils/platform_info.dart'; import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/link_handler.dart'; @@ -311,8 +311,13 @@ class _ChannelChatScreenState extends State { ], Flexible( child: GestureDetector( - onTap: () => _showMessagePathInfo(message), + onTap: PlatformInfo.isDesktop + ? null + : () => _showMessagePathInfo(message), onLongPress: () => _showMessageActions(message), + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => _showMessageActions(message) + : null, child: Container( padding: gifId != null ? const EdgeInsets.all(4) @@ -430,7 +435,8 @@ class _ChannelChatScreenState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ Flexible( - child: Linkify( + child: LinkHandler.buildLinkifyText( + context: context, text: message.text, style: TextStyle( fontSize: bodyFontSize * textScale, @@ -440,15 +446,6 @@ class _ChannelChatScreenState extends State { color: Colors.green, decoration: TextDecoration.underline, ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => LinkHandler.handleLinkTap( - context, - link.url, - ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -557,7 +554,7 @@ class _ChannelChatScreenState extends State { ], ); - if (!isOutgoing) { + if (!isOutgoing && !PlatformInfo.isDesktop) { return _SwipeReplyBubble( maxSwipeOffset: maxSwipeOffset, replySwipeThreshold: replySwipeThreshold, @@ -1112,6 +1109,15 @@ class _ChannelChatScreenState extends State { _setReplyingTo(message); }, ), + if (PlatformInfo.isDesktop) + ListTile( + leading: const Icon(Icons.route), + title: Text(context.l10n.chat_path), + onTap: () { + Navigator.pop(sheetContext); + _showMessagePathInfo(message); + }, + ), // Can't react to your own messages if (!message.isOutgoing) ListTile( diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 98694be..51d2453 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:meshcore_open/storage/channel_message_store.dart'; +import 'package:meshcore_open/utils/platform_info.dart'; import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; @@ -417,78 +418,96 @@ class _ChannelsScreenState extends State return Card( key: ValueKey('channel_${channel.index}'), margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - dense: true, - minVerticalPadding: 0, - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - visualDensity: const VisualDensity(vertical: -2), - leading: Stack( - children: [ - CircleAvatar( - backgroundColor: bgColor, - child: Icon(icon, color: iconColor), - ), - if (isCommunityChannel) - Positioned( - right: 0, - bottom: 0, - child: Container( - width: 14, - height: 14, - decoration: BoxDecoration( - color: Colors.purple, - shape: BoxShape.circle, - border: Border.all( - color: Theme.of(context).cardColor, - width: 2, + child: GestureDetector( + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => _showChannelActions( + context, + connector, + channelMessageStore, + channel, + ) + : null, + child: ListTile( + dense: true, + minVerticalPadding: 0, + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + visualDensity: const VisualDensity(vertical: -2), + leading: Stack( + children: [ + CircleAvatar( + backgroundColor: bgColor, + child: Icon(icon, color: iconColor), + ), + if (isCommunityChannel) + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.purple, + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).cardColor, + width: 2, + ), + ), + child: const Icon( + Icons.people, + size: 8, + color: Colors.white, ), ), - child: const Icon(Icons.people, size: 8, color: Colors.white), ), - ), - ], - ), - title: Text( - channel.name.isEmpty - ? context.l10n.channels_channelIndex(channel.index) - : channel.name, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(width: 4), ], - if (showDragHandle && dragIndex != null) - ReorderableDelayedDragStartListener( - index: dragIndex, - child: Icon( - Icons.drag_handle, - color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + title: Text( + channel.name.isEmpty + ? context.l10n.channels_channelIndex(channel.index) + : channel.name, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (unreadCount > 0) ...[ + UnreadBadge(count: unreadCount), + const SizedBox(width: 4), + ], + if (showDragHandle && dragIndex != null) + ReorderableDelayedDragStartListener( + index: dragIndex, + child: Icon( + Icons.drag_handle, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), - ), - ], - ), - onTap: () async { - connector.markChannelRead(channel.index); - await Future.delayed(const Duration(milliseconds: 50)); - if (context.mounted) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChannelChatScreen(channel: channel), - ), - ); - } - }, - onLongPress: () => _showChannelActions( - context, - connector, - channelMessageStore, - channel, + ], + ), + onTap: () async { + connector.markChannelRead(channel.index); + await Future.delayed(const Duration(milliseconds: 50)); + if (context.mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChannelChatScreen(channel: channel), + ), + ); + } + }, + onLongPress: () => _showChannelActions( + context, + connector, + channelMessageStore, + channel, + ), ), ), ); diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 5209b41..ace82b5 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; + +import '../utils/platform_info.dart'; import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; @@ -16,6 +17,7 @@ import '../helpers/reaction_helper.dart'; import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; +import '../helpers/path_helper.dart'; import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; @@ -362,6 +364,8 @@ class _ChatScreenState extends State { textScale: textScale, onTap: () => _openMessagePath(message, contact), onLongPress: () => _showMessageActions(message, contact), + onRetryReaction: (msg, emoji) => + _sendReaction(msg, contact, emoji), ); }, ); @@ -820,7 +824,8 @@ class _ChatScreenState extends State { ); } - String _formatRelativeTime(DateTime time) { + String _formatRelativeTime(DateTime? time) { + if (time == null) return '—'; final diff = DateTime.now().difference(time); if (diff.inSeconds < 60) return context.l10n.time_justNow; if (diff.inMinutes < 60) { @@ -841,15 +846,31 @@ class _ChatScreenState extends State { return; } - final formattedPath = pathBytes - .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) - .join(','); + final connector = context.read(); + final allContacts = connector.allContacts; + + final formattedPath = PathHelper.formatPathHex(pathBytes); + final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts); showDialog( context: context, builder: (context) => AlertDialog( title: Text(context.l10n.chat_fullPath), - content: SelectableText(formattedPath), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(formattedPath), + const SizedBox(height: 8), + SelectableText( + resolvedNames, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), actions: [ TextButton( onPressed: () => Navigator.push( @@ -1127,6 +1148,15 @@ class _ChatScreenState extends State { _showEmojiPicker(message, contact); }, ), + if (PlatformInfo.isDesktop) + ListTile( + leading: const Icon(Icons.route), + title: Text(context.l10n.chat_path), + onTap: () { + Navigator.pop(sheetContext); + _openMessagePath(message, contact); + }, + ), ListTile( leading: const Icon(Icons.copy), title: Text(context.l10n.common_copy), @@ -1237,6 +1267,7 @@ class _MessageBubble extends StatelessWidget { final bool isRoomServer; final VoidCallback? onTap; final VoidCallback? onLongPress; + final void Function(Message message, String emoji)? onRetryReaction; final double textScale; const _MessageBubble({ @@ -1246,6 +1277,7 @@ class _MessageBubble extends StatelessWidget { required this.textScale, this.onTap, this.onLongPress, + this.onRetryReaction, }); @override @@ -1279,8 +1311,11 @@ class _MessageBubble extends StatelessWidget { : CrossAxisAlignment.start, children: [ GestureDetector( - onTap: onTap, + onTap: PlatformInfo.isDesktop ? null : onTap, onLongPress: onLongPress, + onSecondaryTapUp: PlatformInfo.isDesktop + ? (_) => onLongPress?.call() + : null, child: Row( mainAxisAlignment: isOutgoing ? MainAxisAlignment.end @@ -1397,7 +1432,8 @@ class _MessageBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ Flexible( - child: Linkify( + child: LinkHandler.buildLinkifyText( + context: context, text: messageText, style: TextStyle( color: textColor, @@ -1408,15 +1444,6 @@ class _MessageBubble extends StatelessWidget { decoration: TextDecoration.underline, fontSize: bodyFontSize * textScale, ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => LinkHandler.handleLinkTap( - context, - link.url, - ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -1606,33 +1633,64 @@ class _MessageBubble extends StatelessWidget { children: message.reactions.entries.map((entry) { final emoji = entry.key; final count = entry.value; + final status = message.reactionStatuses[emoji]; + final isPending = + status == MessageStatus.pending || status == MessageStatus.sent; + final isFailed = status == MessageStatus.failed; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: colorScheme.outline.withValues(alpha: 0.3), - width: 1, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(emoji, style: const TextStyle(fontSize: 16)), - if (count > 1) ...[ - const SizedBox(width: 4), - Text( - '$count', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: colorScheme.onSecondaryContainer, - ), + return GestureDetector( + onTap: isFailed && onRetryReaction != null + ? () => onRetryReaction!(message, emoji) + : null, + child: Opacity( + opacity: isPending ? 0.5 : 1.0, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isFailed + ? colorScheme.errorContainer + : colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isFailed + ? colorScheme.error + : colorScheme.outline.withValues(alpha: 0.3), + width: 1, ), - ], - ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(emoji, style: const TextStyle(fontSize: 16)), + if (count > 1) ...[ + const SizedBox(width: 4), + Text( + '$count', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colorScheme.onSecondaryContainer, + ), + ), + ], + if (isPending) ...[ + const SizedBox(width: 2), + SizedBox( + width: 8, + height: 8, + child: CircularProgressIndicator( + strokeWidth: 1.5, + color: colorScheme.onSecondaryContainer, + ), + ), + ], + if (isFailed) ...[ + const SizedBox(width: 2), + Icon(Icons.replay, size: 10, color: colorScheme.error), + ], + ], + ), + ), ), ); }).toList(), diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 23844fb..011e6d0 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:meshcore_open/services/notification_service.dart'; import 'package:meshcore_open/utils/app_logger.dart'; +import 'package:meshcore_open/utils/platform_info.dart'; import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; @@ -1439,66 +1440,77 @@ class _ContactTile extends StatelessWidget { @override Widget build(BuildContext context) { - return ListTile( - leading: CircleAvatar( - backgroundColor: _getTypeColor(contact.type), - child: _buildContactAvatar(contact), - ), - title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis), - Text( - contact.shortPubKeyHex, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 12), - ), - ], - ), - // Clamp text scaling in trailing section to prevent overflow while - // maintaining accessibility. Primary content (title/subtitle) scales normally. - trailing: MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.linear( - MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), - ), + return GestureDetector( + onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null, + child: ListTile( + leading: CircleAvatar( + backgroundColor: _getTypeColor(contact.type), + child: _buildContactAvatar(contact), ), - child: SizedBox( - width: 120, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(height: 4), - ], - Text( - _formatLastSeen(context, lastSeen), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.right, - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isFavorite) - Icon(Icons.star, size: 14, color: Colors.amber[700]), - if (isFavorite && contact.hasLocation) - const SizedBox(width: 2), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + contact.pathLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + contact.shortPubKeyHex, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), + ], + ), + // Clamp text scaling in trailing section to prevent overflow while + // maintaining accessibility. Primary content (title/subtitle) scales normally. + trailing: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear( + MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), + ), + ), + child: SizedBox( + width: 120, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (unreadCount > 0) ...[ + UnreadBadge(count: unreadCount), + const SizedBox(height: 4), ], - ), - ], + Text( + _formatLastSeen(context, lastSeen), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isFavorite) + Icon(Icons.star, size: 14, color: Colors.amber[700]), + if (isFavorite && contact.hasLocation) + const SizedBox(width: 2), + if (contact.hasLocation) + Icon( + Icons.location_on, + size: 14, + color: Colors.grey[400], + ), + ], + ), + ], + ), ), ), + onTap: onTap, + onLongPress: onLongPress, ), - onTap: onTap, - onLongPress: onLongPress, ); } diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart index 7f065aa..4e7c6e8 100644 --- a/lib/screens/discovery_screen.dart +++ b/lib/screens/discovery_screen.dart @@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../utils/contact_search.dart'; +import '../utils/platform_info.dart'; import '../widgets/app_bar.dart'; import '../widgets/list_filter_widget.dart'; @@ -88,7 +89,7 @@ class _DiscoveryScreenState extends State { itemCount: filteredAndSorted.length, itemBuilder: (context, index) { final contact = filteredAndSorted[index]; - return ListTile( + final tile = ListTile( leading: CircleAvatar( backgroundColor: _getTypeColor(contact.type), child: Icon( @@ -120,6 +121,14 @@ class _DiscoveryScreenState extends State { onLongPress: () => _showContactContextMenu(contact, connector), ); + if (PlatformInfo.isDesktop) { + return GestureDetector( + onSecondaryTapUp: (_) => + _showContactContextMenu(contact, connector), + child: tile, + ); + } + return tile; }, ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index df16a59..6aaebf0 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -617,19 +617,6 @@ class _MapScreenState extends State { if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!)); } - // Fallback: for any last-hop byte with no GPS repeater, average the - // positions of contacts with known GPS that share the same last hop. - // Those contacts are all adjacent to the same unknown repeater, so their - // centroid is a reasonable proxy for its location. - for (final byte in lastHopBytes) { - if (repeaterByHash.containsKey(byte)) continue; - for (final c in withLocation) { - if (c.path.isNotEmpty && c.path.last == byte) { - anchorSet.add(LatLng(c.latitude!, c.longitude!)); - } - } - } - // Filter anchors that are geometrically inconsistent with radio range. // Two anchors more than 2 * maxRange apart cannot both be in direct radio // range of the same node, so isolated outliers are removed. @@ -641,15 +628,12 @@ class _MapScreenState extends State { final LatLng position; if (anchors.length == 1) { - // Offset single-anchor guesses so they don't overlap the repeater marker. - // Use the contact's public key byte as a deterministic angle seed. - const offsetDeg = 0.003; // ~330 m at the equator - final angle = (contact.publicKey[1] / 255.0) * 2 * pi; - position = LatLng( - anchors[0].latitude + offsetDeg * cos(angle), - anchors[0].longitude + offsetDeg * sin(angle), + // Spread single-anchor guesses around the anchor so they remain visible. + position = _offsetGuessedPosition( + anchors[0], + contact, + radiusMeters: 330, ); - if (!_checkLocationPlausibility( position.latitude, position.longitude, @@ -662,7 +646,11 @@ class _MapScreenState extends State { lat += a.latitude; lon += a.longitude; } - position = LatLng(lat / anchors.length, lon / anchors.length); + position = _offsetGuessedPosition( + LatLng(lat / anchors.length, lon / anchors.length), + contact, + radiusMeters: anchors.length >= 3 ? 80 : 120, + ); if (!_checkLocationPlausibility( position.latitude, position.longitude, @@ -682,6 +670,31 @@ class _MapScreenState extends State { return result; } + LatLng _offsetGuessedPosition( + LatLng anchor, + Contact contact, { + required double radiusMeters, + }) { + final seed = _guessSeed(contact.publicKey); + final angle = ((seed & 0xFFFF) / 0x10000) * 2 * pi; + final latOffsetDeg = (radiusMeters / 111320.0) * cos(angle); + final lonScale = max(cos(anchor.latitude * pi / 180.0).abs(), 0.2); + final lonOffsetDeg = (radiusMeters / (111320.0 * lonScale)) * sin(angle); + return LatLng( + anchor.latitude + latOffsetDeg, + anchor.longitude + lonOffsetDeg, + ); + } + + int _guessSeed(Uint8List publicKey) { + var seed = 0x811C9DC5; + for (final byte in publicKey) { + seed ^= byte; + seed = (seed * 0x01000193) & 0x7FFFFFFF; + } + return seed; + } + /// Estimates the free-space maximum LoRa range in km from the connected /// device's current radio parameters. Returns null if parameters are unknown. double? _estimateLoRaRangeKm(MeshCoreConnector connector) { diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 88c1f81..e6697f4 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -120,6 +120,30 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(autoRouteRotationEnabled: value)); } + Future setMaxRouteWeight(double value) async { + await updateSettings(_settings.copyWith(maxRouteWeight: value)); + } + + Future setInitialRouteWeight(double value) async { + await updateSettings(_settings.copyWith(initialRouteWeight: value)); + } + + Future setRouteWeightSuccessIncrement(double value) async { + await updateSettings( + _settings.copyWith(routeWeightSuccessIncrement: value), + ); + } + + Future setRouteWeightFailureDecrement(double value) async { + await updateSettings( + _settings.copyWith(routeWeightFailureDecrement: value), + ); + } + + Future setMaxMessageRetries(int value) async { + await updateSettings(_settings.copyWith(maxMessageRetries: value)); + } + Future setThemeMode(String value) async { await updateSettings(_settings.copyWith(themeMode: value)); } diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index b66ba51..2f10511 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -21,86 +21,74 @@ class _AckHistoryEntry { }); } -class _AckHashMapping { - final String messageId; - final DateTime timestamp; +/// (messageId, timestamp, attemptIndex) — stored per ACK hash for O(1) lookup. +typedef AckHashMapping = ({String messageId, DateTime timestamp, int attemptIndex}); - _AckHashMapping({required this.messageId, required this.timestamp}); +class RetryServiceConfig { + final void Function(Contact, String, int, int) sendMessage; + final void Function(String, Message) addMessage; + final void Function(Message) updateMessage; + final Function(Contact)? clearContactPath; + final Function(Contact, Uint8List, int)? setContactPath; + final int Function(int pathLength, int messageBytes, {String? contactKey})? + calculateTimeout; + final Uint8List? Function()? getSelfPublicKey; + final String Function(Contact, String)? prepareContactOutboundText; + final AppSettingsService? appSettingsService; + final AppDebugLogService? debugLogService; + final void Function(String, PathSelection, bool, int?)? recordPathResult; + final void Function(String, int, int, int)? onDeliveryObserved; + final PathSelection? Function( + String contactKey, + int attemptIndex, + int maxRetries, + List recentSelections, + )? selectRetryPath; + + const RetryServiceConfig({ + required this.sendMessage, + required this.addMessage, + required this.updateMessage, + this.clearContactPath, + this.setContactPath, + this.calculateTimeout, + this.getSelfPublicKey, + this.prepareContactOutboundText, + this.appSettingsService, + this.debugLogService, + this.recordPathResult, + this.onDeliveryObserved, + this.selectRetryPath, + }); } class MessageRetryService extends ChangeNotifier { - static const int maxRetries = 5; static const int maxAckHistorySize = 100; + int _maxRetries = 5; + int get maxRetries => _maxRetries; final Map _timeoutTimers = {}; final Map _pendingMessages = {}; final Map _pendingContacts = {}; - final Map _pendingPathSelections = {}; - final Map _ackHashToMessageId = - {}; // ackHashHex → messageId + timestamp for O(1) lookup - final Map> _expectedAckHashes = - {}; // Track all expected ACKs for retries (for history) - final List<_AckHistoryEntry> _ackHistory = - []; // Rolling buffer of recent ACK hashes - final Map> _pendingMessageQueuePerContact = - {}; // contactPubKeyHex → FIFO queue of messageIds (DEPRECATED - will be removed) - final Map> _sendQueue = - {}; // contactPubKeyHex → ordered list of messageIds awaiting send - final Set _activeMessages = - {}; // messageIds currently in-flight (sent/retrying) - final Set _resolvedMessages = - {}; // messageIds already resolved (prevents double _onMessageResolved) - final Map _expectedHashToMessageId = - {}; // expectedAckHashHex → messageId (for matching RESP_CODE_SENT by hash) + final Map> _attemptPathHistory = {}; + final Map _ackHashToMessageId = {}; + final Map> _expectedAckHashes = {}; + final List<_AckHistoryEntry> _ackHistory = []; + final Map> _sendQueue = {}; + final Set _activeMessages = {}; + final Set _resolvedMessages = {}; + final Map _expectedHashToMessageId = {}; - Function(Contact, String, int, int)? _sendMessageCallback; - Function(String, Message)? _addMessageCallback; - Function(Message)? _updateMessageCallback; - Function(Contact)? _clearContactPathCallback; - Function(Contact, Uint8List, int)? _setContactPathCallback; - Function(int, int, {String? contactKey})? _calculateTimeoutCallback; - Uint8List? Function()? _getSelfPublicKeyCallback; - String Function(Contact, String)? _prepareContactOutboundTextCallback; - AppSettingsService? _appSettingsService; - AppDebugLogService? _debugLogService; - Function(String, PathSelection, bool, int?)? _recordPathResultCallback; - Function(String, int, int, int)? _onDeliveryObservedCallback; + RetryServiceConfig? _config; MessageRetryService(); - void initialize({ - required Function(Contact, String, int, int) sendMessageCallback, - required Function(String, Message) addMessageCallback, - required Function(Message) updateMessageCallback, - Function(Contact)? clearContactPathCallback, - Function(Contact, Uint8List, int)? setContactPathCallback, - Function(int pathLength, int messageBytes, {String? contactKey})? - calculateTimeoutCallback, - Uint8List? Function()? getSelfPublicKeyCallback, - String Function(Contact, String)? prepareContactOutboundTextCallback, - AppSettingsService? appSettingsService, - AppDebugLogService? debugLogService, - Function(String, PathSelection, bool, int?)? recordPathResultCallback, - Function( - String contactKey, - int pathLength, - int messageBytes, - int tripTimeMs, - )? - onDeliveryObservedCallback, - }) { - _sendMessageCallback = sendMessageCallback; - _addMessageCallback = addMessageCallback; - _updateMessageCallback = updateMessageCallback; - _clearContactPathCallback = clearContactPathCallback; - _setContactPathCallback = setContactPathCallback; - _calculateTimeoutCallback = calculateTimeoutCallback; - _getSelfPublicKeyCallback = getSelfPublicKeyCallback; - _prepareContactOutboundTextCallback = prepareContactOutboundTextCallback; - _appSettingsService = appSettingsService; - _debugLogService = debugLogService; - _recordPathResultCallback = recordPathResultCallback; - _onDeliveryObservedCallback = onDeliveryObservedCallback; + void initialize(RetryServiceConfig config) { + _config = config; + } + + void setMaxRetries(int value) { + _maxRetries = value.clamp(2, 10); } /// Compute expected ACK hash using same algorithm as firmware: @@ -139,17 +127,14 @@ class MessageRetryService extends ChangeNotifier { Future sendMessageWithRetry({ required Contact contact, required String text, - PathSelection? pathSelection, Uint8List? pathBytes, int? pathLength, }) async { final messageId = const Uuid().v4(); - final useFlood = pathSelection?.useFlood ?? false; - final messagePathBytes = - pathBytes ?? _resolveMessagePathBytes(contact, useFlood, pathSelection); + final resolved = resolvePathSelection(contact); + final messagePathBytes = pathBytes ?? Uint8List.fromList(resolved.pathBytes); final messagePathLength = - pathLength ?? - _resolveMessagePathLength(contact, useFlood, pathSelection); + pathLength ?? (resolved.useFlood ? -1 : resolved.hopCount); final message = Message( senderKey: contact.publicKey, text: text, @@ -164,13 +149,8 @@ class MessageRetryService extends ChangeNotifier { _pendingMessages[messageId] = message; _pendingContacts[messageId] = contact; - if (pathSelection != null) { - _pendingPathSelections[messageId] = pathSelection; - } - if (_addMessageCallback != null) { - _addMessageCallback!(contact.publicKeyHex, message); - } + _config?.addMessage(contact.publicKeyHex, message); // Queue per contact — only one message in-flight at a time to avoid // overflowing the firmware's 8-entry expected_ack_table. @@ -200,13 +180,12 @@ class MessageRetryService extends ChangeNotifier { if (msg != null) { final failed = msg.copyWith(status: MessageStatus.failed); _pendingMessages[messageId] = failed; - _updateMessageCallback?.call(failed); + _config?.updateMessage(failed); } _onMessageResolved(messageId, contactKey); }); return; } - // Message was cancelled/cleaned up while queued — try next } } @@ -217,33 +196,87 @@ class MessageRetryService extends ChangeNotifier { _sendNextForContact(contactKey); } + PathSelection? _selectPathForAttempt(Message message, Contact contact) { + final config = _config; + if (config == null) return null; + final autoRotationEnabled = + config.appSettingsService?.settings.autoRouteRotationEnabled == true; + if (!autoRotationEnabled || + contact.pathOverride != null || + config.selectRetryPath == null) { + return null; + } + + final recentSelections = List.from( + _attemptPathHistory[message.messageId] ?? const [], + ); + return config.selectRetryPath!( + contact.publicKeyHex, + message.retryCount, + maxRetries, + recentSelections, + ); + } + + void _recordAttemptPathHistory(String messageId, PathSelection selection) { + if (selection.useFlood) return; + final history = _attemptPathHistory.putIfAbsent(messageId, () => []); + history.add(selection); + if (history.length > recentAttemptDiversityWindow) { + history.removeAt(0); + } + } + Future _attemptSend(String messageId) async { final message = _pendingMessages[messageId]; final contact = _pendingContacts[messageId]; + final config = _config; - if (message == null || contact == null) return; + if (message == null || contact == null || config == null) return; + + final currentSelection = _selectPathForAttempt(message, contact); + + if (currentSelection != null) { + final updatedMessage = message.copyWith( + pathLength: currentSelection.useFlood ? -1 : currentSelection.hopCount, + pathBytes: currentSelection.useFlood + ? Uint8List(0) + : Uint8List.fromList(currentSelection.pathBytes), + ); + _pendingMessages[messageId] = updatedMessage; + } else if (message.retryCount > 0) { + // No schedule entry for this retry — re-resolve path from current contact + // state so user's path override changes are picked up between retries. + final resolved = resolvePathSelection(contact); + final updatedMessage = message.copyWith( + pathLength: resolved.useFlood ? -1 : resolved.hopCount, + pathBytes: Uint8List.fromList(resolved.pathBytes), + ); + _pendingMessages[messageId] = updatedMessage; + } + + // Re-read after potential schedule update + final effectiveMessage = _pendingMessages[messageId] ?? message; // Sync path settings with device before sending - // Use the path that was captured when the message was first sent - if (_setContactPathCallback != null && _clearContactPathCallback != null) { - if (message.pathLength != null && message.pathLength! < 0) { - debugPrint( - 'Setting flood mode for retry attempt ${message.retryCount}', - ); - await _clearContactPathCallback!(contact); - } else if (message.pathLength != null && message.pathLength! >= 0) { - final pathStr = message.pathBytes.isEmpty - ? 'direct' - : message.pathBytes - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(','); - debugPrint( - 'Setting path [$pathStr] (${message.pathLength} hops) for retry attempt ${message.retryCount}', - ); - await _setContactPathCallback!( + if (config.setContactPath != null && config.clearContactPath != null) { + final bool useFlood = currentSelection != null + ? currentSelection.useFlood + : (effectiveMessage.pathLength != null && effectiveMessage.pathLength! < 0); + final List pathBytes = currentSelection != null + ? currentSelection.pathBytes + : effectiveMessage.pathBytes; + final int hopCount = currentSelection != null + ? currentSelection.hopCount + : (effectiveMessage.pathLength ?? 0); + + if (useFlood) { + await config.clearContactPath!(contact); + } else if (effectiveMessage.pathLength != null) { + await config.setContactPath!( contact, - message.pathBytes, - message.pathLength!, + Uint8List.fromList(pathBytes), + hopCount, ); } } @@ -257,8 +290,6 @@ class MessageRetryService extends ChangeNotifier { ); return; } - // If the message was retried by a timer during our await, the retryCount - // will have advanced. Only proceed if it still matches the attempt we started. if (currentMessage.retryCount != message.retryCount) { debugPrint( '_attemptSend: message $messageId retryCount changed during path sync, aborting', @@ -266,15 +297,19 @@ class MessageRetryService extends ChangeNotifier { return; } - final attempt = message.retryCount.clamp(0, 3); + if (currentSelection != null) { + _recordAttemptPathHistory(messageId, currentSelection); + } + + final attempt = message.retryCount; final timestampSeconds = message.timestamp.millisecondsSinceEpoch ~/ 1000; // Compute expected ACK hash that device will return in RESP_CODE_SENT // IMPORTANT: Use the transformed text (with SMAZ encoding if enabled) to match device's hash - final selfPubKey = _getSelfPublicKeyCallback?.call(); + final selfPubKey = config.getSelfPublicKey?.call(); if (selfPubKey != null) { final outboundText = - _prepareContactOutboundTextCallback?.call(contact, message.text) ?? + config.prepareContactOutboundText?.call(contact, message.text) ?? message.text; final expectedHash = MessageRetryService.computeExpectedAckHash( timestampSeconds, @@ -290,43 +325,24 @@ class MessageRetryService extends ChangeNotifier { final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; - _debugLogService?.info( + config.debugLogService?.info( 'Sent "$shortText" to ${contact.name} → expect ACK hash $expectedHashHex (attempt $attempt)', tag: 'AckHash', ); - debugPrint( - 'Computed expected ACK hash $expectedHashHex for message $messageId', - ); } - // DEPRECATED: Old queue-based matching (kept for fallback) - _pendingMessageQueuePerContact[contact.publicKeyHex] ??= []; - _pendingMessageQueuePerContact[contact.publicKeyHex]!.add(messageId); - - if (_sendMessageCallback != null) { - _sendMessageCallback!(contact, message.text, attempt, timestampSeconds); - } else { - // No send callback — message would be stuck forever. Fail it immediately. - debugPrint( - '_attemptSend: no sendMessageCallback, failing message $messageId', - ); - final failedMessage = message.copyWith(status: MessageStatus.failed); - _pendingMessages[messageId] = failedMessage; - _updateMessageCallback?.call(failedMessage); - _onMessageResolved(messageId, contact.publicKeyHex); - } + config.sendMessage(contact, message.text, attempt, timestampSeconds); } - bool updateMessageFromSent( - Uint8List ackHash, - int timeoutMs, { - bool allowQueueFallback = true, - }) { + bool updateMessageFromSent(Uint8List ackHash, int timeoutMs) { + final config = _config; + if (config == null) return false; + final ackHashHex = ackHash .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(); - // NEW: Try hash-based matching first (fixes LoRa message drops causing mismatches) + // Try hash-based matching (fixes LoRa message drops causing mismatches) String? messageId = _expectedHashToMessageId.remove(ackHashHex); Contact? contact; @@ -338,89 +354,31 @@ class MessageRetryService extends ChangeNotifier { final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; - _debugLogService?.info( + config.debugLogService?.info( 'RESP_CODE_SENT received: ACK hash $ackHashHex ✓ matched "$shortText" to ${contact.name}', tag: 'AckHash', ); - debugPrint( - 'Hash-based match: ACK hash $ackHashHex → message $messageId ✓', - ); - - // Remove from old queue since we matched - _pendingMessageQueuePerContact[contact.publicKeyHex]?.remove(messageId); - if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? - false) { - _pendingMessageQueuePerContact.remove(contact.publicKeyHex); - } } else { - _debugLogService?.warn( + config.debugLogService?.warn( 'RESP_CODE_SENT: ACK hash $ackHashHex matched but message no longer pending', tag: 'AckHash', ); - debugPrint('Hash matched $messageId but message no longer pending'); messageId = null; contact = null; } } - // FALLBACK: Old queue-based matching (for messages sent before hash computation was added) - // Only match within a single contact's queue to avoid cross-contact mismatches. - if (messageId == null && allowQueueFallback) { - _debugLogService?.warn( - 'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue', - tag: 'AckHash', - ); - debugPrint( - 'Hash-based match failed for $ackHashHex, falling back to queue-based matching', - ); - - // Search all contact queues so concurrent chats don't miss matches. - final queuesToSearch = _pendingMessageQueuePerContact; - - for (var entry in queuesToSearch.entries) { - final contactKey = entry.key; - final queue = entry.value; - - // Drain stale entries until we find a valid one or exhaust the queue. - while (queue.isNotEmpty) { - final candidateMessageId = queue.removeAt(0); - if (_pendingMessages.containsKey(candidateMessageId)) { - messageId = candidateMessageId; - contact = _pendingContacts[candidateMessageId]; - debugPrint( - 'Queue-based match (fallback): $ackHashHex → message $messageId for $contactKey', - ); - break; - } - debugPrint('Dequeued stale message $candidateMessageId - skipping'); - } - if (messageId != null) break; - } - } - if (messageId == null || contact == null) { debugPrint('No pending message found for ACK hash: $ackHashHex'); return false; } - // Store the mapping for future lookups (e.g., when ACK arrives) - // Keep timestamp so we can clean up old mappings later - _ackHashToMessageId[ackHashHex] = _AckHashMapping( + final message = _pendingMessages[messageId]!; + _ackHashToMessageId[ackHashHex] = ( messageId: messageId, timestamp: DateTime.now(), + attemptIndex: message.retryCount, ); - debugPrint('Mapped ACK hash $ackHashHex to message $messageId'); - - final message = _pendingMessages[messageId]; - final selection = _pendingPathSelections[messageId]; - - if (message == null) { - debugPrint( - 'Message $messageId no longer pending for ACK hash: $ackHashHex', - ); - _ackHashToMessageId.remove(ackHashHex); - return false; - } // Add this ACK hash to the list of expected ACKs for this message (for history) _expectedAckHashes[messageId] ??= []; @@ -428,37 +386,20 @@ class MessageRetryService extends ChangeNotifier { (hash) => listEquals(hash, ackHash), )) { _expectedAckHashes[messageId]!.add(Uint8List.fromList(ackHash)); - debugPrint( - 'Added ACK hash $ackHashHex to message $messageId (total: ${_expectedAckHashes[messageId]!.length})', - ); } // Calculate timeout: prefer ML prediction, then device-provided, then physics fallback - int pathLengthValue; - if (selection != null) { - pathLengthValue = selection.useFlood ? -1 : selection.hopCount; - if (pathLengthValue < 0) pathLengthValue = contact.pathLength; - } else if (message.pathLength != null) { - pathLengthValue = message.pathLength!; - } else { - pathLengthValue = contact.pathLength; - } + final pathLengthValue = message.pathLength ?? contact.pathLength; int actualTimeout = timeoutMs; - if (_calculateTimeoutCallback != null) { - final calculated = _calculateTimeoutCallback!( + if (config.calculateTimeout != null) { + final calculated = config.calculateTimeout!( pathLengthValue, message.text.length, contactKey: contact.publicKeyHex, ); - // calculateTimeout tries ML first, falls back to physics. - // Use calculated value if device didn't provide one, or if ML - // produced a tighter prediction than the device's estimate. if (timeoutMs <= 0 || calculated < timeoutMs) { actualTimeout = calculated; - debugPrint( - 'Using calculated timeout: ${actualTimeout}ms for path length $pathLengthValue', - ); } } @@ -470,18 +411,26 @@ class MessageRetryService extends ChangeNotifier { ); _pendingMessages[messageId] = updatedMessage; - - if (_updateMessageCallback != null) { - _updateMessageCallback!(updatedMessage); - } + config.updateMessage(updatedMessage); _startTimeoutTimer(messageId, actualTimeout); - debugPrint('Updated message $messageId with ACK hash: $ackHashHex'); return true; } bool get hasPendingMessages => _pendingMessages.isNotEmpty; + /// Update the stored contact snapshot for all pending messages to this contact. + /// Call this when the contact's pathOverride changes so retries use the new path. + void updatePendingContact(Contact contact) { + final keys = _pendingContacts.entries + .where((e) => e.value.publicKeyHex == contact.publicKeyHex) + .map((e) => e.key) + .toList(); + for (final key in keys) { + _pendingContacts[key] = contact; + } + } + void _startTimeoutTimer(String messageId, int timeoutMs) { _timeoutTimers[messageId]?.cancel(); _timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () { @@ -489,10 +438,24 @@ class MessageRetryService extends ChangeNotifier { }); } + void _cleanupMessage(String messageId) { + _moveAckHashesToHistory(messageId); + _ackHashToMessageId.removeWhere( + (_, mapping) => mapping.messageId == messageId, + ); + _expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId); + _pendingMessages.remove(messageId); + _pendingContacts.remove(messageId); + _attemptPathHistory.remove(messageId); + _timeoutTimers.remove(messageId); + _resolvedMessages.remove(messageId); + } + void _handleTimeout(String messageId) { final message = _pendingMessages[messageId]; final contact = _pendingContacts[messageId]; - final selection = _pendingPathSelections[messageId]; + final config = _config; + final selection = message != null ? _selectionFromMessage(message) : null; if (message == null || contact == null) { debugPrint( @@ -504,44 +467,40 @@ class MessageRetryService extends ChangeNotifier { final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; - _debugLogService?.warn( + config?.debugLogService?.warn( 'Timeout: No ACK received for "$shortText" to ${contact.name} (attempt ${message.retryCount}) → retrying', tag: 'AckHash', ); - debugPrint( - 'Timeout for message $messageId (retry ${message.retryCount}/${maxRetries - 1})', - ); if (message.retryCount < maxRetries - 1) { final backoffMs = 1000 * (1 << message.retryCount); + if (selection != null) { + _recordPathResultFromMessage( + contact.publicKeyHex, + message, + selection, + false, + null, + ); + } + final updatedMessage = message.copyWith( retryCount: message.retryCount + 1, status: MessageStatus.pending, - // Keep expectedAckHash - it will be updated when the new attempt is sent ); _pendingMessages[messageId] = updatedMessage; + config?.updateMessage(updatedMessage); - if (_updateMessageCallback != null) { - _updateMessageCallback!(updatedMessage); - } - - _debugLogService?.info( + config?.debugLogService?.info( 'Scheduling retry for "$shortText" to ${contact.name} after ${backoffMs}ms backoff', tag: 'AckHash', ); - debugPrint('Scheduling retry after ${backoffMs}ms'); - // Store the backoff timer so it can be canceled if new RESP_CODE_SENT arrives _timeoutTimers[messageId] = Timer(Duration(milliseconds: backoffMs), () { - // Double-check message is still pending before retry if (_pendingMessages.containsKey(messageId)) { _attemptSend(messageId); - } else { - debugPrint( - 'Retry cancelled: message $messageId was delivered while waiting', - ); } }); } else { @@ -549,10 +508,9 @@ class MessageRetryService extends ChangeNotifier { final failedMessage = message.copyWith(status: MessageStatus.failed); _pendingMessages[messageId] = failedMessage; - // Check if we should clear the path on max retry - if (_appSettingsService?.settings.clearPathOnMaxRetry == true && - _clearContactPathCallback != null) { - _clearContactPathCallback!(contact); + if (config?.appSettingsService?.settings.clearPathOnMaxRetry == true && + config?.clearContactPath != null) { + config!.clearContactPath!(contact); } _recordPathResultFromMessage( @@ -563,34 +521,16 @@ class MessageRetryService extends ChangeNotifier { null, ); - if (_updateMessageCallback != null) { - _updateMessageCallback!(failedMessage); - } + config?.updateMessage(failedMessage); notifyListeners(); - // Message is done retrying — send next queued message for this contact _onMessageResolved(messageId, contact.publicKeyHex); // Keep message in pending maps for 30s grace period so late ACKs // can still match and update the message to delivered. _timeoutTimers[messageId] = Timer(const Duration(seconds: 30), () { - _moveAckHashesToHistory(messageId); - // Clean up ALL hash mappings for this message - _ackHashToMessageId.removeWhere( - (_, mapping) => mapping.messageId == messageId, - ); - _expectedHashToMessageId.removeWhere((_, msgId) => msgId == messageId); - _pendingMessages.remove(messageId); - _pendingContacts.remove(messageId); - _pendingPathSelections.remove(messageId); - _timeoutTimers.remove(messageId); - _resolvedMessages.remove(messageId); - final contactKey = contact.publicKeyHex; - _pendingMessageQueuePerContact[contactKey]?.remove(messageId); - if (_pendingMessageQueuePerContact[contactKey]?.isEmpty ?? false) { - _pendingMessageQueuePerContact.remove(contactKey); - } + _cleanupMessage(messageId); }); } } @@ -606,14 +546,9 @@ class MessageRetryService extends ChangeNotifier { ), ); - // Trim history to max size (rolling buffer) while (_ackHistory.length > maxAckHistorySize) { _ackHistory.removeAt(0); } - - debugPrint( - 'Moved ${ackHashes.length} ACK hashes to history for message $messageId (history size: ${_ackHistory.length})', - ); } } @@ -621,9 +556,6 @@ class MessageRetryService extends ChangeNotifier { for (final entry in _ackHistory) { for (final expectedHash in entry.ackHashes) { if (listEquals(expectedHash, ackHash)) { - debugPrint( - 'Found ACK match in history: messageId=${entry.messageId}, age=${DateTime.now().difference(entry.timestamp).inSeconds}s', - ); return true; } } @@ -632,14 +564,14 @@ class MessageRetryService extends ChangeNotifier { } void handleAckReceived(Uint8List ackHash, int tripTimeMs) { + final config = _config; String? matchedMessageId; + int? matchedAttemptIndex; final ackHashHex = ackHash .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(); - debugPrint('ACK received: $ackHashHex, trip time: ${tripTimeMs}ms'); - - // First, clean up old ACK hash mappings (older than 15 minutes) + // Clean up old ACK hash mappings (older than 15 minutes) final cutoffTime = DateTime.now().subtract(const Duration(minutes: 15)); final hashesToRemove = []; for (var entry in _ackHashToMessageId.entries) { @@ -650,24 +582,18 @@ class MessageRetryService extends ChangeNotifier { for (var hash in hashesToRemove) { _ackHashToMessageId.remove(hash); } - if (hashesToRemove.isNotEmpty) { - debugPrint('Cleaned up ${hashesToRemove.length} old ACK hash mappings'); - } // Use direct O(1) lookup via ACK hash mapping final mapping = _ackHashToMessageId[ackHashHex]; if (mapping != null) { matchedMessageId = mapping.messageId; - debugPrint('Matched ACK to message via direct lookup: $matchedMessageId'); + matchedAttemptIndex = mapping.attemptIndex; } else { - _debugLogService?.warn( + config?.debugLogService?.warn( 'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex not found in direct mapping, trying fallback', tag: 'AckHash', ); // Fallback: Check against ALL expected ACK hashes (from all retry attempts) - debugPrint( - 'ACK not in mapping, checking _expectedAckHashes (${_expectedAckHashes.length} messages)', - ); for (var entry in _expectedAckHashes.entries) { final messageId = entry.key; final expectedHashes = entry.value; @@ -675,9 +601,7 @@ class MessageRetryService extends ChangeNotifier { for (final expectedHash in expectedHashes) { if (listEquals(expectedHash, ackHash)) { matchedMessageId = messageId; - debugPrint( - 'Matched ACK to message via fallback: $matchedMessageId (attempt ${expectedHashes.indexOf(expectedHash)})', - ); + matchedAttemptIndex = expectedHashes.indexOf(expectedHash); break; } } @@ -689,27 +613,22 @@ class MessageRetryService extends ChangeNotifier { if (matchedMessageId != null) { final message = _pendingMessages[matchedMessageId]; if (message == null) { - // Message was already cleaned up (e.g. grace period expired) _ackHashToMessageId.remove(ackHashHex); - debugPrint( - 'ACK matched $matchedMessageId but message already cleaned up', - ); return; } final contact = _pendingContacts[matchedMessageId]; - final selection = _pendingPathSelections[matchedMessageId]; + final ackedAttempt = matchedAttemptIndex ?? message.retryCount; + final selection = _selectionFromMessage(message); final shortText = message.text.length > 20 ? '${message.text.substring(0, 20)}...' : message.text; - _debugLogService?.info( - 'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} in ${tripTimeMs}ms', + config?.debugLogService?.info( + 'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on retry ${ackedAttempt + 1} in ${tripTimeMs}ms', tag: 'AckHash', ); - // Cancel any pending timeout or retry _timeoutTimers[matchedMessageId]?.cancel(); - _timeoutTimers.remove(matchedMessageId); final deliveredMessage = message.copyWith( status: MessageStatus.delivered, @@ -717,36 +636,9 @@ class MessageRetryService extends ChangeNotifier { tripTimeMs: tripTimeMs, ); - // Clean up ALL hash mappings for this message (from all retry attempts) - _ackHashToMessageId.removeWhere( - (_, mapping) => mapping.messageId == matchedMessageId, - ); - _expectedHashToMessageId.removeWhere( - (_, msgId) => msgId == matchedMessageId, - ); + _cleanupMessage(matchedMessageId); - // Move ACK hashes to history before removing - _moveAckHashesToHistory(matchedMessageId); - - _pendingMessages.remove(matchedMessageId); - _pendingContacts.remove(matchedMessageId); - _pendingPathSelections.remove(matchedMessageId); - _resolvedMessages.remove(matchedMessageId); - - // Clean up the queue entry for this contact (remove any remaining references to this message) - if (contact != null) { - _pendingMessageQueuePerContact[contact.publicKeyHex]?.remove( - matchedMessageId, - ); - if (_pendingMessageQueuePerContact[contact.publicKeyHex]?.isEmpty ?? - false) { - _pendingMessageQueuePerContact.remove(contact.publicKeyHex); - } - } - - if (_updateMessageCallback != null) { - _updateMessageCallback!(deliveredMessage); - } + config?.updateMessage(deliveredMessage); if (contact != null) { _recordPathResultFromMessage( @@ -756,10 +648,10 @@ class MessageRetryService extends ChangeNotifier { true, tripTimeMs, ); - if (_onDeliveryObservedCallback != null && + if (config?.onDeliveryObserved != null && tripTimeMs > 0 && message.pathLength != null) { - _onDeliveryObservedCallback!( + config!.onDeliveryObserved!( contact.publicKeyHex, message.pathLength!, message.text.length, @@ -771,15 +663,13 @@ class MessageRetryService extends ChangeNotifier { notifyListeners(); } else { - // Check ACK history for recently completed messages if (_checkAckHistory(ackHash)) { - _debugLogService?.info( + config?.debugLogService?.info( 'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex matched a recently completed message (duplicate ACK)', tag: 'AckHash', ); - debugPrint('ACK matched a recently completed message from history'); } else { - _debugLogService?.error( + config?.debugLogService?.error( 'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex has no matching message!', tag: 'AckHash', ); @@ -788,57 +678,6 @@ class MessageRetryService extends ChangeNotifier { } } - Uint8List _resolveMessagePathBytes( - Contact contact, - bool forceFlood, - PathSelection? selection, - ) { - // Priority 1: Check user's path override - if (contact.pathOverride != null) { - if (contact.pathOverride! < 0) { - return Uint8List(0); // Force flood - } - return contact.pathOverrideBytes ?? Uint8List(0); - } - - // Priority 2: Check forceFlood or device flood mode - if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) { - return Uint8List(0); - } - - // Priority 3: Check PathSelection (auto-rotation) - if (selection != null && selection.pathBytes.isNotEmpty) { - return Uint8List.fromList(selection.pathBytes); - } - - // Priority 4: Use device's discovered path - return contact.path; - } - - int? _resolveMessagePathLength( - Contact contact, - bool forceFlood, - PathSelection? selection, - ) { - // Priority 1: Check user's path override - if (contact.pathOverride != null) { - return contact.pathOverride; - } - - // Priority 2: Check forceFlood or device flood mode - if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) { - return -1; - } - - // Priority 3: Check PathSelection (auto-rotation) - if (selection != null && selection.pathBytes.isNotEmpty) { - return selection.hopCount; - } - - // Priority 4: Use device's discovered path - return contact.pathLength; - } - String? getContactKeyForAckHash(Uint8List ackHash) { for (var entry in _pendingMessages.entries) { final message = entry.value; @@ -866,15 +705,11 @@ class MessageRetryService extends ChangeNotifier { bool success, int? tripTimeMs, ) { - if (_recordPathResultCallback == null) return; + final callback = _config?.recordPathResult; + if (callback == null) return; final recordSelection = selection ?? _selectionFromMessage(message); if (recordSelection == null) return; - _recordPathResultCallback!( - contactKey, - recordSelection, - success, - tripTimeMs, - ); + callback(contactKey, recordSelection, success, tripTimeMs); } PathSelection? _selectionFromMessage(Message message) { @@ -899,11 +734,10 @@ class MessageRetryService extends ChangeNotifier { _timeoutTimers.clear(); _pendingMessages.clear(); _pendingContacts.clear(); - _pendingPathSelections.clear(); + _attemptPathHistory.clear(); _expectedAckHashes.clear(); _ackHistory.clear(); _ackHashToMessageId.clear(); - _pendingMessageQueuePerContact.clear(); _sendQueue.clear(); _activeMessages.clear(); _resolvedMessages.clear(); diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 62d3796..b367e0e 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter/foundation.dart'; +import '../helpers/reaction_helper.dart'; import '../l10n/app_localizations.dart'; import '../utils/platform_info.dart'; @@ -145,6 +146,19 @@ class NotificationService { return true; } + /// Format special message types for human-readable notifications. + static String formatNotificationText(String text) { + final trimmed = text.trim(); + final reaction = ReactionHelper.parseReaction(trimmed); + if (reaction != null) { + return 'Reacted ${reaction.emoji}'; + } + if (RegExp(r'^g:[A-Za-z0-9_-]+$').hasMatch(trimmed)) { + return 'Sent a GIF'; + } + return text; + } + Future _showMessageNotificationImpl({ required String contactName, required String message, @@ -187,7 +201,7 @@ class NotificationService { await _notifications.show( id: contactId?.hashCode ?? 0, title: contactName, - body: message, + body: formatNotificationText(message), notificationDetails: notificationDetails, payload: 'message:$contactId', ); @@ -283,7 +297,7 @@ class NotificationService { macOS: macDetails, ); - final preview = message.trim(); + final preview = formatNotificationText(message.trim()); final body = preview.isEmpty ? _l10n.notification_receivedNewMessage : preview; @@ -430,6 +444,7 @@ class NotificationService { Future showChannelMessageNotification({ required String channelName, + required String senderName, required String message, int? channelIndex, int? badgeCount, @@ -440,7 +455,7 @@ class NotificationService { _PendingNotification( type: _NotificationType.channelMessage, title: channelName, - body: message, + body: '$senderName: $message', id: channelIndex?.toString(), badgeCount: badgeCount, ), diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 569fada..809f867 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -9,6 +9,8 @@ class PathHistoryService extends ChangeNotifier { final Map _cache = {}; final Map _autoRotationIndex = {}; final Map _floodStats = {}; + final Set _pendingLoads = {}; + final Map> _deferredRecords = {}; // LRU cache eviction tracking static const int _maxCachedContacts = 50; @@ -18,7 +20,6 @@ class PathHistoryService extends ChangeNotifier { int _version = 0; int get version => _version; - static const int _autoRotationTopCount = 3; PathHistoryService(this._storage); @@ -26,17 +27,21 @@ class PathHistoryService extends ChangeNotifier { // Load cached path histories on startup if needed } - void handlePathUpdated(Contact contact) { - if (contact.pathLength < 0) return; - + void handlePathUpdated(Contact contact, {double initialWeight = 1.0}) { + if (contact.pathLength < 0 && contact.path.isEmpty) return; + final hopCount = contact.pathLength < 0 + ? contact.path.length + : contact.pathLength; _addPathRecord( contactPubKeyHex: contact.publicKeyHex, - hopCount: contact.pathLength, + hopCount: hopCount, tripTimeMs: 0, wasFloodDiscovery: true, pathBytes: contact.path, successCount: 0, failureCount: 0, + routeWeight: initialWeight, + timestamp: null, ); } @@ -54,6 +59,44 @@ class PathHistoryService extends ChangeNotifier { pathBytes: selection.pathBytes, successCount: 0, failureCount: 0, + timestamp: null, + ); + } + + /// When a flood message is delivered, credit the contact's current device + /// path so that the route the ACK traveled back through gets a weight boost. + void recordFloodPathAttribution({ + required String contactPubKeyHex, + required List pathBytes, + required int hopCount, + int? tripTimeMs, + double successIncrement = 0.5, + double maxWeight = 5.0, + }) { + if (pathBytes.isEmpty || hopCount < 0) return; + + final existing = _findPathRecord(contactPubKeyHex, pathBytes); + final successCount = (existing?.successCount ?? 0) + 1; + final failureCount = existing?.failureCount ?? 0; + + final currentWeight = existing?.routeWeight ?? 1.0; + final newWeight = (currentWeight + successIncrement).clamp(0.0, maxWeight); + + debugPrint( + 'Flood path attribution: crediting path [${pathBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(',')}] ' + 'for $contactPubKeyHex (weight $currentWeight → $newWeight)', + ); + + _addPathRecord( + contactPubKeyHex: contactPubKeyHex, + hopCount: hopCount, + tripTimeMs: tripTimeMs ?? existing?.tripTimeMs ?? 0, + wasFloodDiscovery: true, + pathBytes: pathBytes, + successCount: successCount, + failureCount: failureCount, + routeWeight: newWeight, + timestamp: DateTime.now(), ); } @@ -62,6 +105,9 @@ class PathHistoryService extends ChangeNotifier { PathSelection selection, { required bool success, int? tripTimeMs, + double successIncrement = 0.5, + double failureDecrement = 0.5, + double maxWeight = 5.0, }) { if (selection.useFlood) { final stats = _floodStats.putIfAbsent( @@ -82,6 +128,18 @@ class PathHistoryService extends ChangeNotifier { final successCount = (existing?.successCount ?? 0) + (success ? 1 : 0); final failureCount = (existing?.failureCount ?? 0) + (success ? 0 : 1); + final currentWeight = existing?.routeWeight ?? 1.0; + double newWeight; + if (success) { + newWeight = (currentWeight + successIncrement).clamp(0.0, maxWeight); + } else { + newWeight = currentWeight - failureDecrement; + if (newWeight <= 0) { + removePathRecord(contactPubKeyHex, selection.pathBytes); + return; + } + } + _addPathRecord( contactPubKeyHex: contactPubKeyHex, hopCount: selection.hopCount, @@ -90,37 +148,68 @@ class PathHistoryService extends ChangeNotifier { pathBytes: selection.pathBytes, successCount: successCount, failureCount: failureCount, + routeWeight: newWeight, + timestamp: success ? DateTime.now() : existing?.timestamp, ); } - PathSelection getNextAutoPathSelection(String contactPubKeyHex) { - final ranked = _getRankedPaths( - contactPubKeyHex, - ).take(_autoRotationTopCount).toList(); + PathSelection selectPathForAttempt( + String contactPubKeyHex, { + required int attemptIndex, + required int maxRetries, + List recentSelections = const [], + }) { + if (maxRetries <= 0 || attemptIndex >= maxRetries - 1) { + return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true); + } + + final ranked = _getRankedPaths(contactPubKeyHex); if (ranked.isEmpty) { return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true); } _trackAccess(contactPubKeyHex); - final selections = - ranked - .map( - (path) => PathSelection( - pathBytes: path.pathBytes, - hopCount: path.hopCount, - useFlood: false, - ), - ) - .toList() - ..add( - const PathSelection(pathBytes: [], hopCount: -1, useFlood: true), - ); + final recentPaths = recentSelections + .where((selection) => !selection.useFlood) + .map((selection) => selection.pathBytes) + .toList(); + final candidates = recentPaths.isEmpty + ? ranked + : ranked + .where( + (path) => !recentPaths.any( + (recentPath) => _pathsEqual(path.pathBytes, recentPath), + ), + ) + .toList(); + final selected = candidates.isNotEmpty + ? (recentPaths.isEmpty + ? _selectRotatedCandidate(contactPubKeyHex, candidates) + : candidates.first) + : ranked.first; + + return PathSelection( + pathBytes: selected.pathBytes, + hopCount: selected.hopCount, + useFlood: false, + ); + } + + PathRecord _selectRotatedCandidate( + String contactPubKeyHex, + List candidates, + ) { + if (candidates.length <= 1) { + _autoRotationIndex[contactPubKeyHex] = 0; + return candidates.first; + } final currentIndex = _autoRotationIndex[contactPubKeyHex] ?? 0; - final selection = selections[currentIndex % selections.length]; - _autoRotationIndex[contactPubKeyHex] = currentIndex + 1; - return selection; + final selectedIndex = currentIndex % candidates.length; + _autoRotationIndex[contactPubKeyHex] = + (selectedIndex + 1) % candidates.length; + return candidates[selectedIndex]; } void _addPathRecord({ @@ -131,37 +220,68 @@ class PathHistoryService extends ChangeNotifier { required List pathBytes, required int successCount, required int failureCount, + double routeWeight = 1.0, + DateTime? timestamp, }) { var history = _cache[contactPubKeyHex]; if (history == null) { + // If a load is already in progress, defer this record + if (_pendingLoads.contains(contactPubKeyHex)) { + _deferredRecords.putIfAbsent(contactPubKeyHex, () => []); + _deferredRecords[contactPubKeyHex]!.add( + _DeferredPathRecord( + hopCount: hopCount, + tripTimeMs: tripTimeMs, + wasFloodDiscovery: wasFloodDiscovery, + pathBytes: pathBytes, + successCount: successCount, + failureCount: failureCount, + routeWeight: routeWeight, + timestamp: timestamp, + ), + ); + return; + } + + _pendingLoads.add(contactPubKeyHex); _loadHistoryFromStorage(contactPubKeyHex).then((loaded) { - if (loaded != null) { - _cache[contactPubKeyHex] = loaded; - _addPathRecordInternal( - contactPubKeyHex, - hopCount, - tripTimeMs, - wasFloodDiscovery, - pathBytes, - successCount, - failureCount, - ); - } else { - _cache[contactPubKeyHex] = ContactPathHistory( - contactPubKeyHex: contactPubKeyHex, - recentPaths: [], - ); - _addPathRecordInternal( - contactPubKeyHex, - hopCount, - tripTimeMs, - wasFloodDiscovery, - pathBytes, - successCount, - failureCount, - ); + _cache[contactPubKeyHex] = + loaded ?? + ContactPathHistory( + contactPubKeyHex: contactPubKeyHex, + recentPaths: [], + ); + _addPathRecordInternal( + contactPubKeyHex, + hopCount, + tripTimeMs, + wasFloodDiscovery, + pathBytes, + successCount, + failureCount, + routeWeight, + timestamp, + ); + + // Apply any deferred records + final deferred = _deferredRecords.remove(contactPubKeyHex); + if (deferred != null) { + for (final record in deferred) { + _addPathRecordInternal( + contactPubKeyHex, + record.hopCount, + record.tripTimeMs, + record.wasFloodDiscovery, + record.pathBytes, + record.successCount, + record.failureCount, + record.routeWeight, + record.timestamp, + ); + } } + _pendingLoads.remove(contactPubKeyHex); }); return; } @@ -174,6 +294,8 @@ class PathHistoryService extends ChangeNotifier { pathBytes, successCount, failureCount, + routeWeight, + timestamp, ); } @@ -185,6 +307,8 @@ class PathHistoryService extends ChangeNotifier { List pathBytes, int successCount, int failureCount, + double routeWeight, + DateTime? timestamp, ) { var history = _cache[contactPubKeyHex]; if (history == null) return; @@ -198,16 +322,18 @@ class PathHistoryService extends ChangeNotifier { tripTimeMs = existing.tripTimeMs; } wasFloodDiscovery = existing.wasFloodDiscovery || wasFloodDiscovery; + timestamp ??= existing.timestamp; } final newRecord = PathRecord( hopCount: hopCount, tripTimeMs: tripTimeMs, - timestamp: DateTime.now(), + timestamp: timestamp, wasFloodDiscovery: wasFloodDiscovery, pathBytes: pathBytes, successCount: successCount, failureCount: failureCount, + routeWeight: routeWeight, ); final updatedPaths = List.from(history.recentPaths); @@ -275,6 +401,23 @@ class PathHistoryService extends ChangeNotifier { return history?.mostRecent; } + ({ + int successCount, + int failureCount, + int lastTripTimeMs, + DateTime? lastUsed, + })? + getFloodStats(String contactPubKeyHex) { + final stats = _floodStats[contactPubKeyHex]; + if (stats == null) return null; + return ( + successCount: stats.successCount, + failureCount: stats.failureCount, + lastTripTimeMs: stats.lastTripTimeMs, + lastUsed: stats.lastUsed, + ); + } + Future clearPathHistory(String contactPubKeyHex) async { _cache.remove(contactPubKeyHex); _cacheAccessOrder.remove(contactPubKeyHex); @@ -322,26 +465,81 @@ class PathHistoryService extends ChangeNotifier { final ranked = List.from(history.recentPaths) ..removeWhere((p) => p.pathBytes.isEmpty); + final fastestTripMs = _getFastestKnownTripMs(ranked); + final highestRouteWeight = _getHighestKnownRouteWeight(ranked); ranked.sort((a, b) { - final aRate = - (a.successCount + 1) / (a.successCount + a.failureCount + 2); - final bRate = - (b.successCount + 1) / (b.successCount + b.failureCount + 2); - if (aRate != bRate) return bRate.compareTo(aRate); - if (a.successCount != b.successCount) { - return b.successCount.compareTo(a.successCount); + final scoreCompare = _scorePathRecord( + b, + fastestTripMs: fastestTripMs, + highestRouteWeight: highestRouteWeight, + ).compareTo( + _scorePathRecord( + a, + fastestTripMs: fastestTripMs, + highestRouteWeight: highestRouteWeight, + ), + ); + if (scoreCompare != 0) { + return scoreCompare; + } + if (a.routeWeight != b.routeWeight) { + return b.routeWeight.compareTo(a.routeWeight); } - final aTrip = a.tripTimeMs == 0 ? 999999 : a.tripTimeMs; final bTrip = b.tripTimeMs == 0 ? 999999 : b.tripTimeMs; if (aTrip != bTrip) return aTrip.compareTo(bTrip); - return b.timestamp.compareTo(a.timestamp); + final aTime = a.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0); + final bTime = b.timestamp ?? DateTime.fromMillisecondsSinceEpoch(0); + return bTime.compareTo(aTime); }); return ranked; } + int? _getFastestKnownTripMs(List paths) { + final knownTrips = paths + .where((path) => path.tripTimeMs > 0) + .map((path) => path.tripTimeMs) + .toList(); + if (knownTrips.isEmpty) return null; + return knownTrips.reduce((a, b) => a < b ? a : b); + } + + double _getHighestKnownRouteWeight(List paths) { + if (paths.isEmpty) return 1.0; + final highestWeight = paths + .map((path) => path.routeWeight) + .reduce((a, b) => a > b ? a : b); + return highestWeight <= 0 ? 1.0 : highestWeight; + } + + double _scorePathRecord( + PathRecord path, { + required int? fastestTripMs, + required double highestRouteWeight, + }) { + final totalAttempts = path.successCount + path.failureCount; + final reliability = (path.successCount + 1) / (totalAttempts + 2); + final latency = fastestTripMs == null || path.tripTimeMs <= 0 + ? 0.6 + : (fastestTripMs / path.tripTimeMs).clamp(0.0, 1.0); + final freshness = path.timestamp == null + ? 0.0 + : 1.0 / + (1.0 + + (DateTime.now().difference(path.timestamp!).inMinutes / + 60.0 / + 24.0)); + final routeWeight = + (path.routeWeight / highestRouteWeight).clamp(0.0, 1.0); + + return (reliability * 0.45) + + (latency * 0.25) + + (freshness * 0.1) + + (routeWeight * 0.2); + } + bool _pathsEqual(List a, List b) { return listEquals(a, b); } @@ -369,6 +567,28 @@ class PathHistoryService extends ChangeNotifier { } } +class _DeferredPathRecord { + final int hopCount; + final int tripTimeMs; + final bool wasFloodDiscovery; + final List pathBytes; + final int successCount; + final int failureCount; + final double routeWeight; + final DateTime? timestamp; + + _DeferredPathRecord({ + required this.hopCount, + required this.tripTimeMs, + required this.wasFloodDiscovery, + required this.pathBytes, + required this.successCount, + required this.failureCount, + this.routeWeight = 1.0, + this.timestamp, + }); +} + class _FloodStats { int successCount = 0; int failureCount = 0; diff --git a/lib/storage/channel_message_store.dart b/lib/storage/channel_message_store.dart index 7bf44bd..ddb42f6 100644 --- a/lib/storage/channel_message_store.dart +++ b/lib/storage/channel_message_store.dart @@ -108,6 +108,7 @@ class ChannelMessageStore { 'pathVariants': msg.pathVariants.map(base64Encode).toList(), 'repeats': msg.repeats.map(_repeatToJson).toList(), 'messageId': msg.messageId, + 'packetHash': msg.packetHash, 'replyToMessageId': msg.replyToMessageId, 'replyToSenderName': msg.replyToSenderName, 'replyToText': msg.replyToText, @@ -143,6 +144,7 @@ class ChannelMessageStore { const [], channelIndex: json['channelIndex'] as int?, messageId: json['messageId'] as String?, + packetHash: json['packetHash'] as String?, replyToMessageId: json['replyToMessageId'] as String?, replyToSenderName: json['replyToSenderName'] as String?, replyToText: json['replyToText'] as String?, diff --git a/lib/storage/message_store.dart b/lib/storage/message_store.dart index 9a39e3f..44d3621 100644 --- a/lib/storage/message_store.dart +++ b/lib/storage/message_store.dart @@ -96,6 +96,9 @@ class MessageStore { ? base64Encode(msg.pathBytes) : null, 'reactions': msg.reactions, + 'reactionStatuses': msg.reactionStatuses.map( + (key, value) => MapEntry(key, value.index), + ), 'fourByteRoomContactKey': base64Encode(msg.fourByteRoomContactKey), }; } @@ -135,6 +138,11 @@ class MessageStore { (key, value) => MapEntry(key, value as int), ) ?? {}, + reactionStatuses: + (json['reactionStatuses'] as Map?)?.map( + (key, value) => MapEntry(key, MessageStatus.values[value as int]), + ) ?? + {}, fourByteRoomContactKey: json['fourByteRoomContactKey'] != null ? Uint8List.fromList( base64Decode(json['fourByteRoomContactKey'] as String), diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 861241b..f667256 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -9,6 +9,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../helpers/path_helper.dart'; import '../services/path_history_service.dart'; import 'path_selection_dialog.dart'; @@ -40,7 +41,8 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { ); } - String _formatRelativeTime(BuildContext context, DateTime time) { + String _formatRelativeTime(BuildContext context, DateTime? time) { + if (time == null) return '—'; final l10n = context.l10n; final diff = DateTime.now().difference(time); if (diff.inSeconds < 60) return l10n.time_justNow; @@ -61,15 +63,31 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { return; } - final formattedPath = pathBytes - .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) - .join(','); + final connector = context.read(); + final allContacts = connector.allContacts; + + final formattedPath = PathHelper.formatPathHex(pathBytes); + final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts); showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.chat_fullPath), - content: SelectableText(formattedPath), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(formattedPath), + const SizedBox(height: 8), + SelectableText( + resolvedNames, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), actions: [ TextButton( onPressed: () => Navigator.push( @@ -262,16 +280,17 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { radius: 16, backgroundColor: color, child: Text( - '${path.hopCount}', - style: const TextStyle(fontSize: 12), + path.routeWeight.toStringAsFixed(1), + style: const TextStyle(fontSize: 10), ), ), title: Text( l10n.chat_hopsCount(path.hopCount), style: const TextStyle(fontSize: 14), ), + isThreeLine: true, subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)} • ${path.successCount} ${l10n.chat_successes}', + '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)}\n${path.successCount} ${l10n.chat_successes} • Score: ${path.routeWeight.toStringAsFixed(1)}', style: const TextStyle(fontSize: 11), ), trailing: Row( @@ -346,6 +365,40 @@ class _PathManagementDialogState extends State<_PathManagementDialog> { Text(l10n.chat_noPathHistoryYet), const Divider(), ], + // Flood delivery stats + Builder( + builder: (context) { + final floodStats = pathService.getFloodStats( + currentContact.publicKeyHex, + ); + if (floodStats == null || + (floodStats.successCount == 0 && + floodStats.failureCount == 0)) { + return const SizedBox.shrink(); + } + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + dense: true, + leading: const CircleAvatar( + radius: 16, + backgroundColor: Colors.blue, + child: Icon(Icons.waves, size: 16), + ), + title: const Text( + 'Flood Mode', + style: TextStyle(fontSize: 14), + ), + subtitle: Text( + '${floodStats.successCount} ${l10n.chat_successes} / ${floodStats.failureCount} failures' + '${floodStats.lastTripTimeMs > 0 ? ' • ${(floodStats.lastTripTimeMs / 1000).toStringAsFixed(2)}s' : ''}' + '${floodStats.lastUsed != null ? ' • ${_formatRelativeTime(context, floodStats.lastUsed!)}' : ''}', + style: const TextStyle(fontSize: 11), + ), + ), + ); + }, + ), const SizedBox(height: 8), Text( l10n.chat_pathActions, diff --git a/test/helpers/path_helper_test.dart b/test/helpers/path_helper_test.dart new file mode 100644 index 0000000..38abf2c --- /dev/null +++ b/test/helpers/path_helper_test.dart @@ -0,0 +1,36 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:meshcore_open/helpers/path_helper.dart'; +import 'package:meshcore_open/models/contact.dart'; + +Contact _contact({ + required int firstByte, + required String name, + required int type, +}) { + final key = Uint8List(32)..[0] = firstByte; + return Contact( + publicKey: key, + name: name, + type: type, + pathLength: 0, + path: Uint8List(0), + lastSeen: DateTime.now(), + ); +} + +void main() { + test('resolvePathNames ignores chat nodes and keeps repeater/room nodes', () { + final contacts = [ + _contact(firstByte: 0xF2, name: 'MunTui', type: advTypeChat), + _contact(firstByte: 0x7E, name: 'zrepeater', type: advTypeRepeater), + _contact(firstByte: 0xBA, name: 'USS Ronald Reagan', type: advTypeRoom), + ]; + + final resolved = PathHelper.resolvePathNames([0xF2, 0x7E, 0xBA], contacts); + + expect(resolved, equals('F2 → zrepeater → USS Ronald Reagan')); + }); +} diff --git a/test/models/model_changes_test.dart b/test/models/model_changes_test.dart new file mode 100644 index 0000000..165b91d --- /dev/null +++ b/test/models/model_changes_test.dart @@ -0,0 +1,357 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/models/path_history.dart'; +import 'package:meshcore_open/models/app_settings.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; + +// Builds a valid contact frame with the given pathLen and optional overrides. +// Frame layout: [respCode(1)][pubKey(32)][type(1)][flags(1)][pathLen(1)][path(64)][name(32)][timestamp(4)][lat(4)][lon(4)] +Uint8List _buildContactFrame({ + int pathLen = 0, + Uint8List? pubKey, + String name = 'TestNode', +}) { + final writer = BytesBuilder(); + writer.addByte(respCodeContact); // 3 + writer.add(pubKey ?? Uint8List.fromList(List.generate(32, (i) => i + 1))); // valid pubkey + writer.addByte(1); // type + writer.addByte(0); // flags + writer.addByte(pathLen); + writer.add(Uint8List(64)); // path bytes (zeros) + // name (32 bytes, null-padded) + final nameBytes = Uint8List(32); + final encoded = name.codeUnits; + for (var i = 0; i < encoded.length && i < 31; i++) { + nameBytes[i] = encoded[i]; + } + writer.add(nameBytes); + // timestamp (4 bytes LE) - some nonzero value + writer.add(Uint8List.fromList([0x01, 0x00, 0x00, 0x00])); + // lat, lon (4 bytes each) + writer.add(Uint8List(4)); // lat + writer.add(Uint8List(4)); // lon + return Uint8List.fromList(writer.toBytes()); +} + +void main() { + group('Contact.fromFrame — pathLen mapping', () { + test('pathLen == 0 → pathLength == 0 (direct, NOT flood)', () { + final frame = _buildContactFrame(pathLen: 0); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + expect(contact!.pathLength, equals(0)); + }); + + test('pathLen == 1 → pathLength == 1', () { + final frame = _buildContactFrame(pathLen: 1); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + expect(contact!.pathLength, equals(1)); + }); + + test('pathLen == 64 (maxPathSize) → pathLength == 64', () { + final frame = _buildContactFrame(pathLen: maxPathSize); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + expect(contact!.pathLength, equals(maxPathSize)); + }); + + test('pathLen == 0xFF → pathLength == -1 (flood)', () { + final frame = _buildContactFrame(pathLen: 0xFF); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + expect(contact!.pathLength, equals(-1)); + }); + + test('pathLen == 65 (over maxPathSize) → pathLength == -1 (flood)', () { + final frame = _buildContactFrame(pathLen: 65); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + expect(contact!.pathLength, equals(-1)); + }); + }); + + group('Contact.fromFrame — corrupt contact guards', () { + test('all-zero public key → returns null', () { + final zeroPubKey = Uint8List(32); // all zeros + final frame = _buildContactFrame(pubKey: zeroPubKey); + final contact = Contact.fromFrame(frame); + expect(contact, isNull); + }); + + test('mostly-zero public key (>16 zeros out of 32) → returns null', () { + // 17 zeros out of 32 bytes exceeds pubKeySize ~/ 2 == 16 + final pubKey = Uint8List(32); + pubKey[0] = 0xAB; + pubKey[1] = 0xCD; + pubKey[2] = 0xEF; + pubKey[3] = 0x12; + pubKey[4] = 0x34; + pubKey[5] = 0x56; + pubKey[6] = 0x78; + pubKey[7] = 0x9A; + pubKey[8] = 0xBC; + pubKey[9] = 0xDE; + pubKey[10] = 0xF0; + pubKey[11] = 0x11; + pubKey[12] = 0x22; + pubKey[13] = 0x33; + pubKey[14] = 0x44; + // bytes 15–31 are zero: that is 17 zeros (indices 15..31 inclusive) + final frame = _buildContactFrame(pubKey: pubKey); + final contact = Contact.fromFrame(frame); + expect(contact, isNull); + }); + + test('valid public key (few zeros) → returns Contact', () { + // Only 1 zero → well below the threshold + final pubKey = Uint8List.fromList(List.generate(32, (i) => i + 1)); + pubKey[5] = 0; // one zero byte + final frame = _buildContactFrame(pubKey: pubKey); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + }); + + test('name with all non-printable characters → returns null', () { + // Build frame with a name composed entirely of control characters (< 0x20) + final nameBytes = Uint8List(32); + nameBytes[0] = 0x01; + nameBytes[1] = 0x02; + nameBytes[2] = 0x03; + // remaining are 0x00 (null terminator ends the string after index 2, + // so readCStringGreedy returns a 3-char string of non-printables) + final writer = BytesBuilder(); + writer.addByte(respCodeContact); + writer.add(Uint8List.fromList(List.generate(32, (i) => i + 1))); + writer.addByte(1); // type + writer.addByte(0); // flags + writer.addByte(0); // pathLen + writer.add(Uint8List(64)); // path + writer.add(nameBytes); + writer.add(Uint8List.fromList([0x01, 0x00, 0x00, 0x00])); // timestamp + writer.add(Uint8List(4)); // lat + writer.add(Uint8List(4)); // lon + final frame = Uint8List.fromList(writer.toBytes()); + final contact = Contact.fromFrame(frame); + expect(contact, isNull); + }); + + test('name with valid printable characters → returns Contact', () { + final frame = _buildContactFrame(name: 'Alice'); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + expect(contact!.name, equals('Alice')); + }); + + test( + 'name with mix of printable and replacement chars → returns Contact (not all bad)', + () { + // Build a name with mostly printable chars and one replacement char (0xFFFD in codeUnits). + // utf8 allowMalformed: true maps invalid sequences to U+FFFD. + // We embed one invalid UTF-8 byte (0x80) among valid ASCII bytes. + // The decoded string will be "Hi\uFFFDThere" — not ALL bad, so should be accepted. + final nameBytes = Uint8List(32); + nameBytes[0] = 0x48; // 'H' + nameBytes[1] = 0x69; // 'i' + nameBytes[2] = 0x80; // invalid UTF-8 → decoded as U+FFFD + nameBytes[3] = 0x54; // 'T' + nameBytes[4] = 0x68; // 'h' + nameBytes[5] = 0x65; // 'e' + nameBytes[6] = 0x72; // 'r' + nameBytes[7] = 0x65; // 'e' + // rest are 0x00 (null terminator) + final writer = BytesBuilder(); + writer.addByte(respCodeContact); + writer.add(Uint8List.fromList(List.generate(32, (i) => i + 1))); + writer.addByte(1); // type + writer.addByte(0); // flags + writer.addByte(0); // pathLen + writer.add(Uint8List(64)); // path + writer.add(nameBytes); + writer.add(Uint8List.fromList([0x01, 0x00, 0x00, 0x00])); // timestamp + writer.add(Uint8List(4)); // lat + writer.add(Uint8List(4)); // lon + final frame = Uint8List.fromList(writer.toBytes()); + final contact = Contact.fromFrame(frame); + expect(contact, isNotNull); + }, + ); + }); + + group('PathRecord — routeWeight field', () { + test('default routeWeight is 1.0', () { + final record = PathRecord( + hopCount: 2, + tripTimeMs: 500, + timestamp: DateTime(2024), + wasFloodDiscovery: false, + pathBytes: [0x01, 0x02], + successCount: 1, + failureCount: 0, + ); + expect(record.routeWeight, equals(1.0)); + }); + + test('custom routeWeight is preserved', () { + final record = PathRecord( + hopCount: 3, + tripTimeMs: 800, + timestamp: DateTime(2024), + wasFloodDiscovery: false, + pathBytes: [0x01], + successCount: 5, + failureCount: 2, + routeWeight: 3.5, + ); + expect(record.routeWeight, equals(3.5)); + }); + + test('toJson includes route_weight', () { + final record = PathRecord( + hopCount: 1, + tripTimeMs: 200, + timestamp: DateTime(2024), + wasFloodDiscovery: true, + pathBytes: [], + successCount: 0, + failureCount: 0, + routeWeight: 2.25, + ); + final json = record.toJson(); + expect(json.containsKey('route_weight'), isTrue); + expect(json['route_weight'], equals(2.25)); + }); + + test('fromJson reads route_weight', () { + final json = { + 'hop_count': 2, + 'trip_time_ms': 400, + 'timestamp': DateTime(2024).toIso8601String(), + 'was_flood': false, + 'path_bytes': [1, 2, 3], + 'success_count': 3, + 'failure_count': 1, + 'route_weight': 4.0, + }; + final record = PathRecord.fromJson(json); + expect(record.routeWeight, equals(4.0)); + }); + + test('fromJson with missing route_weight defaults to 1.0 (backward compat)', + () { + final json = { + 'hop_count': 1, + 'trip_time_ms': 100, + 'timestamp': DateTime(2024).toIso8601String(), + 'was_flood': false, + 'path_bytes': [], + 'success_count': 0, + 'failure_count': 0, + // 'route_weight' intentionally omitted + }; + final record = PathRecord.fromJson(json); + expect(record.routeWeight, equals(1.0)); + }); + }); + + group('AppSettings — new fields', () { + test('default values are correct', () { + final settings = AppSettings(); + expect(settings.maxRouteWeight, equals(5.0)); + expect(settings.initialRouteWeight, equals(3.0)); + expect(settings.routeWeightSuccessIncrement, equals(0.5)); + expect(settings.routeWeightFailureDecrement, equals(0.2)); + expect(settings.maxMessageRetries, equals(5)); + }); + + test('toJson includes all new fields', () { + final settings = AppSettings(); + final json = settings.toJson(); + expect(json.containsKey('max_route_weight'), isTrue); + expect(json.containsKey('initial_route_weight'), isTrue); + expect(json.containsKey('route_weight_success_increment'), isTrue); + expect(json.containsKey('route_weight_failure_decrement'), isTrue); + expect(json.containsKey('max_message_retries'), isTrue); + expect(json['max_route_weight'], equals(5.0)); + expect(json['initial_route_weight'], equals(3.0)); + expect(json['route_weight_success_increment'], equals(0.5)); + expect(json['route_weight_failure_decrement'], equals(0.2)); + expect(json['max_message_retries'], equals(5)); + }); + + test('fromJson reads all new fields', () { + final json = { + 'max_route_weight': 10.0, + 'initial_route_weight': 2.0, + 'route_weight_success_increment': 1.0, + 'route_weight_failure_decrement': 1.5, + 'max_message_retries': 8, + }; + final settings = AppSettings.fromJson(json); + expect(settings.maxRouteWeight, equals(10.0)); + expect(settings.initialRouteWeight, equals(2.0)); + expect(settings.routeWeightSuccessIncrement, equals(1.0)); + expect(settings.routeWeightFailureDecrement, equals(1.5)); + expect(settings.maxMessageRetries, equals(8)); + }); + + test( + 'fromJson with missing new fields uses defaults (backward compat)', + () { + // Simulate an old settings JSON with none of the new fields + final json = {}; + final settings = AppSettings.fromJson(json); + expect(settings.maxRouteWeight, equals(5.0)); + expect(settings.initialRouteWeight, equals(3.0)); + expect(settings.routeWeightSuccessIncrement, equals(0.5)); + expect(settings.routeWeightFailureDecrement, equals(0.2)); + expect(settings.maxMessageRetries, equals(5)); + }, + ); + + test('copyWith works for maxRouteWeight', () { + final settings = AppSettings(); + final updated = settings.copyWith(maxRouteWeight: 8.0); + expect(updated.maxRouteWeight, equals(8.0)); + // Other fields should be unchanged + expect(updated.initialRouteWeight, equals(settings.initialRouteWeight)); + expect(updated.maxMessageRetries, equals(settings.maxMessageRetries)); + }); + + test('copyWith works for initialRouteWeight', () { + final settings = AppSettings(); + final updated = settings.copyWith(initialRouteWeight: 3.0); + expect(updated.initialRouteWeight, equals(3.0)); + expect(updated.maxRouteWeight, equals(settings.maxRouteWeight)); + }); + + test('copyWith works for routeWeightSuccessIncrement', () { + final settings = AppSettings(); + final updated = settings.copyWith(routeWeightSuccessIncrement: 0.25); + expect(updated.routeWeightSuccessIncrement, equals(0.25)); + expect( + updated.routeWeightFailureDecrement, + equals(settings.routeWeightFailureDecrement), + ); + }); + + test('copyWith works for routeWeightFailureDecrement', () { + final settings = AppSettings(); + final updated = settings.copyWith(routeWeightFailureDecrement: 0.75); + expect(updated.routeWeightFailureDecrement, equals(0.75)); + expect( + updated.routeWeightSuccessIncrement, + equals(settings.routeWeightSuccessIncrement), + ); + }); + + test('copyWith works for maxMessageRetries', () { + final settings = AppSettings(); + final updated = settings.copyWith(maxMessageRetries: 10); + expect(updated.maxMessageRetries, equals(10)); + expect(updated.maxRouteWeight, equals(settings.maxRouteWeight)); + }); + }); +} diff --git a/test/services/path_history_service_test.dart b/test/services/path_history_service_test.dart new file mode 100644 index 0000000..561bad3 --- /dev/null +++ b/test/services/path_history_service_test.dart @@ -0,0 +1,815 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/models/path_history.dart'; +import 'package:meshcore_open/models/path_selection.dart'; +import 'package:meshcore_open/services/path_history_service.dart'; +import 'package:meshcore_open/services/storage_service.dart'; + +// --------------------------------------------------------------------------- +// Fake storage — no SharedPreferences dependency, all in-memory. +// --------------------------------------------------------------------------- +class FakeStorageService extends StorageService { + final Map _store = {}; + + @override + Future savePathHistory( + String contactPubKeyHex, + ContactPathHistory history, + ) async { + _store[contactPubKeyHex] = history; + } + + @override + Future loadPathHistory(String contactPubKeyHex) async { + return _store[contactPubKeyHex]; + } + + @override + Future clearPathHistory(String contactPubKeyHex) async { + _store.remove(contactPubKeyHex); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Build a minimal Contact with the given pubKeyHex, pathLength, and path. +/// +/// [publicKeyHex] must be exactly 64 hex characters (32 bytes). +Contact _makeContact({ + required String publicKeyHex, + int pathLength = -1, + List path = const [], +}) { + assert(publicKeyHex.length == 64, 'publicKeyHex must be 64 chars'); + final bytes = Uint8List(32); + for (int i = 0; i < 32; i++) { + bytes[i] = int.parse(publicKeyHex.substring(i * 2, i * 2 + 2), radix: 16); + } + return Contact( + publicKey: bytes, + name: 'Test', + type: 1, + pathLength: pathLength, + path: Uint8List.fromList(path), + lastSeen: DateTime.now(), + ); +} + +/// A 64-char hex string derived from a short tag (padded with zeros). +String _hex(String tag) { + // Convert tag to hex-safe characters, then pad + final hexTag = tag.codeUnits + .map((c) => c.toRadixString(16).padLeft(2, '0')) + .join(); + return hexTag.padLeft(64, '0'); +} + +/// Flush the microtask / async queue so that deferred storage loads complete. +Future _flush() async { + await Future.delayed(Duration.zero); +} + +/// Seed the service's cache for [pubKeyHex] by adding one path record and +/// waiting for the async storage-load path to complete. +/// +/// Call this before making synchronous assertions on a contact that has never +/// been seen by the service. +Future _seed( + PathHistoryService svc, + String pubKeyHex, { + List pathBytes = const [1], + int hopCount = 1, + double weight = 1.0, +}) async { + final contact = _makeContact( + publicKeyHex: pubKeyHex, + pathLength: hopCount, + path: pathBytes, + ); + svc.handlePathUpdated(contact, initialWeight: weight); + await _flush(); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +void main() { + late FakeStorageService storage; + late PathHistoryService svc; + + setUp(() { + storage = FakeStorageService(); + svc = PathHistoryService(storage); + }); + + group('path selection', () { + test('empty path history returns flood', () { + const pubKey = + '0000000000000000000000000000000000000000000000000000000000000001'; + final selection = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + expect(selection.useFlood, isTrue); + }); + + test('returns flood when maxRetries == 0', () { + const pubKey = + '0000000000000000000000000000000000000000000000000000000000000001'; + final selection = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 0, + ); + expect(selection.useFlood, isTrue); + }); + + test('single known path is used for non-final attempts', () async { + final pubKey = _hex('aabb'); + await _seed(svc, pubKey, pathBytes: [0x01, 0x02], hopCount: 2); + + for (int i = 0; i < 4; i++) { + final selection = svc.selectPathForAttempt( + pubKey, + attemptIndex: i, + maxRetries: 5, + ); + expect(selection.useFlood, isFalse, reason: 'attempt $i should be path'); + expect(selection.pathBytes, equals([0x01, 0x02])); + } + }); + + test( + 'retries avoid immediately repeating the same path when possible', + () async { + final pubKey = _hex('rot1'); + await _seed(svc, pubKey, pathBytes: [0xAA], hopCount: 1, weight: 1.0); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0xBB], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.0, + ); + await _flush(); + + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + final second = svc.selectPathForAttempt( + pubKey, + attemptIndex: 1, + maxRetries: 5, + recentSelections: [first], + ); + + expect(first.useFlood, isFalse); + expect(second.useFlood, isFalse); + expect(second.pathBytes, isNot(equals(first.pathBytes))); + }, + ); + + test( + 'retries avoid the last two paths when a third option exists', + () async { + final pubKey = _hex('rot2'); + await _seed(svc, pubKey, pathBytes: [0xA1], hopCount: 1, weight: 3.0); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0xB2], hopCount: 1, useFlood: false), + success: true, + successIncrement: 1.0, + ); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0xC3], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.0, + ); + await _flush(); + + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + final second = svc.selectPathForAttempt( + pubKey, + attemptIndex: 1, + maxRetries: 5, + recentSelections: [first], + ); + final third = svc.selectPathForAttempt( + pubKey, + attemptIndex: 2, + maxRetries: 5, + recentSelections: [first, second], + ); + + final chosenPaths = [ + first.pathBytes, + second.pathBytes, + third.pathBytes, + ]; + expect( + chosenPaths + .map((path) => path.map((b) => b.toRadixString(16)).join(',')) + .toSet() + .length, + equals(3), + ); + expect( + chosenPaths, + everyElement(anyOf(equals([0xA1]), equals([0xB2]), equals([0xC3]))), + ); + }, + ); + + test('first-attempt selection rotates across ranked candidates', () async { + final pubKey = _hex('rot3'); + await _seed(svc, pubKey, pathBytes: [0xA1], hopCount: 1, weight: 4.0); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0xB2], hopCount: 1, useFlood: false), + success: true, + successIncrement: 1.0, + ); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0xC3], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.5, + ); + await _flush(); + + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + final second = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + final third = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + + expect(first.pathBytes, isNot(equals(second.pathBytes))); + expect(second.pathBytes, isNot(equals(third.pathBytes))); + expect( + [first.pathBytes, second.pathBytes, third.pathBytes] + .map((path) => path.map((b) => b.toRadixString(16)).join(',')) + .toSet() + .length, + equals(3), + ); + }); + + test('final attempt is always flood regardless of known paths', () async { + final pubKey = _hex('ef01'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1); + + for (final retries in [1, 2, 5, 10]) { + final lastAttempt = svc.selectPathForAttempt( + pubKey, + attemptIndex: retries - 1, + maxRetries: retries, + ); + expect( + lastAttempt.useFlood, + isTrue, + reason: 'maxRetries=$retries: last attempt must be flood', + ); + } + }); + }); + + group('path scoring', () { + test('higher reliability beats higher route weight', () async { + final pubKey = _hex('rank1'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 4.5); + + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: false, + failureDecrement: 0.1, + ); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: false, + failureDecrement: 0.1, + ); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x02], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.0, + ); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x02], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.0, + ); + await _flush(); + + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + expect(first.pathBytes, equals([0x02])); + }); + + test('lower latency wins when reliability is tied', () async { + final pubKey = _hex('rank2'); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x10], hopCount: 1, useFlood: false), + success: true, + tripTimeMs: 1200, + successIncrement: 0.0, + ); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x20], hopCount: 1, useFlood: false), + success: true, + tripTimeMs: 400, + successIncrement: 0.0, + ); + await _flush(); + + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + expect(first.pathBytes, equals([0x20])); + }); + + test('fresher path wins when reliability and latency are tied', () async { + final pubKey = _hex('rank3'); + final oldTimestamp = DateTime.now().subtract(const Duration(days: 10)); + final newTimestamp = DateTime.now().subtract(const Duration(hours: 1)); + storage._store[pubKey] = ContactPathHistory( + contactPubKeyHex: pubKey, + recentPaths: [ + PathRecord( + hopCount: 1, + tripTimeMs: 900, + timestamp: oldTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x01], + successCount: 1, + failureCount: 0, + routeWeight: 1.0, + ), + PathRecord( + hopCount: 1, + tripTimeMs: 900, + timestamp: newTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x02], + successCount: 1, + failureCount: 0, + routeWeight: 1.0, + ), + ], + ); + svc.getRecentPaths(pubKey); + await _flush(); + + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + expect(first.pathBytes, equals([0x02])); + }); + + test('higher route weight wins when other factors are effectively tied', () async { + final pubKey = _hex('rank4'); + final sharedTimestamp = + DateTime.now().subtract(const Duration(minutes: 30)); + storage._store[pubKey] = ContactPathHistory( + contactPubKeyHex: pubKey, + recentPaths: [ + PathRecord( + hopCount: 1, + tripTimeMs: 750, + timestamp: sharedTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x01], + successCount: 1, + failureCount: 0, + routeWeight: 4.0, + ), + PathRecord( + hopCount: 1, + tripTimeMs: 750, + timestamp: sharedTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x02], + successCount: 1, + failureCount: 0, + routeWeight: 1.0, + ), + ], + ); + svc.getRecentPaths(pubKey); + await _flush(); + + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + expect(first.pathBytes, equals([0x01])); + }); + }); + + // ------------------------------------------------------------------------- + // Group 3: recordPathResult — weight adjustment + // ------------------------------------------------------------------------- + group('recordPathResult weight adjustment', () { + test('success increments weight by successIncrement', () async { + final pubKey = _hex('w001'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 1.0); + + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.5, + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths, isNotEmpty); + expect(paths.first.routeWeight, closeTo(1.5, 0.001)); + expect(paths.first.timestamp, isNotNull); + }); + + test('attempts do not set timestamp before first success', () async { + final pubKey = _hex('w000'); + + svc.recordPathAttempt( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths, isNotEmpty); + expect(paths.first.successCount, equals(0)); + expect(paths.first.timestamp, isNull); + }); + + test('failure preserves the last success timestamp', () async { + final pubKey = _hex('w006'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 1.0); + + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.0, + ); + await _flush(); + final successTimestamp = svc.getRecentPaths(pubKey).first.timestamp; + + await Future.delayed(const Duration(milliseconds: 5)); + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: false, + failureDecrement: 0.1, + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths.first.timestamp, equals(successTimestamp)); + }); + + test('success clamps at maxWeight', () async { + final pubKey = _hex('w002'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 4.8); + + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: true, + successIncrement: 0.5, + maxWeight: 5.0, + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths.first.routeWeight, closeTo(5.0, 0.001)); + }); + + test('failure decrements weight', () async { + final pubKey = _hex('w003'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 2.0); + + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: false, + failureDecrement: 0.5, + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths.first.routeWeight, closeTo(1.5, 0.001)); + }); + + test('failure to 0 removes the path', () async { + final pubKey = _hex('w004'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 0.3); + + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [0x01], hopCount: 1, useFlood: false), + success: false, + failureDecrement: 0.5, // 0.3 - 0.5 = -0.2 → remove + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect( + paths.any((p) => p.pathBytes.length == 1 && p.pathBytes[0] == 0x01), + isFalse, + reason: 'path with weight <= 0 should have been removed', + ); + }); + + test( + 'flood result does not affect path records, updates floodStats', + () async { + final pubKey = _hex('w005'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 1.0); + + final pathsBefore = svc.getRecentPaths(pubKey); + final weightBefore = pathsBefore.first.routeWeight; + + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [], hopCount: -1, useFlood: true), + success: true, + tripTimeMs: 1234, + ); + await _flush(); + + // Path records should be unchanged. + final pathsAfter = svc.getRecentPaths(pubKey); + expect(pathsAfter.first.routeWeight, equals(weightBefore)); + + // Flood stats should be updated. + final stats = svc.getFloodStats(pubKey); + expect(stats, isNotNull); + expect(stats!.successCount, equals(1)); + expect(stats.lastTripTimeMs, equals(1234)); + }, + ); + }); + + // ------------------------------------------------------------------------- + // Group 4: handlePathUpdated + // ------------------------------------------------------------------------- + group('handlePathUpdated', () { + test( + 'pathLength >= 0 with path bytes → records path using pathLength', + () async { + final pubKey = _hex('h001'); + final contact = _makeContact( + publicKeyHex: pubKey, + pathLength: 3, + path: [0x01, 0x02, 0x03], + ); + + svc.handlePathUpdated(contact); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths, isNotEmpty); + expect(paths.first.hopCount, equals(3)); + expect(paths.first.pathBytes, equals([0x01, 0x02, 0x03])); + }, + ); + + test( + 'pathLength < 0 with path bytes → records path using path.length as hopCount', + () async { + final pubKey = _hex('h002'); + final contact = _makeContact( + publicKeyHex: pubKey, + pathLength: -1, // flood indicator from firmware + path: [0xAA, 0xBB], + ); + + svc.handlePathUpdated(contact); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths, isNotEmpty); + // hopCount should equal path.length (2), not pathLength (-1). + expect(paths.first.hopCount, equals(2)); + expect(paths.first.pathBytes, equals([0xAA, 0xBB])); + }, + ); + + test('pathLength < 0 with empty path → skipped (returns early)', () async { + final pubKey = _hex('h003'); + final contact = _makeContact( + publicKeyHex: pubKey, + pathLength: -1, + path: [], + ); + + svc.handlePathUpdated(contact); + await _flush(); + + // Nothing should have been recorded. + final paths = svc.getRecentPaths(pubKey); + expect(paths, isEmpty); + }); + + test('initialWeight is applied to the new record', () async { + final pubKey = _hex('h004'); + final contact = _makeContact( + publicKeyHex: pubKey, + pathLength: 1, + path: [0x55], + ); + + svc.handlePathUpdated(contact, initialWeight: 2.5); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths.first.routeWeight, closeTo(2.5, 0.001)); + }); + }); + + // ------------------------------------------------------------------------- + // Group 5: recordFloodPathAttribution + // ------------------------------------------------------------------------- + group('recordFloodPathAttribution', () { + test('credits existing path with success increment', () async { + final pubKey = _hex('fa01'); + await _seed( + svc, + pubKey, + pathBytes: [0x01, 0x02], + hopCount: 2, + weight: 1.0, + ); + + svc.recordFloodPathAttribution( + contactPubKeyHex: pubKey, + pathBytes: [0x01, 0x02], + hopCount: 2, + tripTimeMs: 3000, + successIncrement: 0.5, + maxWeight: 5.0, + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + final credited = paths.firstWhere( + (p) => p.pathBytes.length == 2 && p.pathBytes[0] == 0x01, + ); + expect(credited.routeWeight, closeTo(1.5, 0.001)); + expect(credited.successCount, equals(1)); + expect(credited.tripTimeMs, equals(3000)); + }); + + test('creates new path record when path is unknown', () async { + final pubKey = _hex('fa02'); + // Seed with a different path so the cache is warm. + await _seed(svc, pubKey, pathBytes: [0xAA], hopCount: 1, weight: 1.0); + + svc.recordFloodPathAttribution( + contactPubKeyHex: pubKey, + pathBytes: [0xBB, 0xCC], + hopCount: 2, + tripTimeMs: 2000, + successIncrement: 0.5, + maxWeight: 5.0, + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + final newPath = paths.firstWhere( + (p) => p.pathBytes.length == 2 && p.pathBytes[0] == 0xBB, + ); + // New path: weight = 1.0 (default) + 0.5 = 1.5 + expect(newPath.routeWeight, closeTo(1.5, 0.001)); + expect(newPath.successCount, equals(1)); + expect(newPath.wasFloodDiscovery, isTrue); + }); + + test('clamps weight at maxWeight', () async { + final pubKey = _hex('fa03'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 4.8); + + svc.recordFloodPathAttribution( + contactPubKeyHex: pubKey, + pathBytes: [0x01], + hopCount: 1, + successIncrement: 0.5, + maxWeight: 5.0, + ); + await _flush(); + + final paths = svc.getRecentPaths(pubKey); + expect(paths.first.routeWeight, closeTo(5.0, 0.001)); + }); + + test('ignores empty pathBytes', () async { + final pubKey = _hex('fa04'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 1.0); + + final pathsBefore = svc.getRecentPaths(pubKey); + final weightBefore = pathsBefore.first.routeWeight; + + svc.recordFloodPathAttribution( + contactPubKeyHex: pubKey, + pathBytes: [], + hopCount: 0, + successIncrement: 0.5, + maxWeight: 5.0, + ); + await _flush(); + + // Existing path should be untouched. + final pathsAfter = svc.getRecentPaths(pubKey); + expect(pathsAfter.first.routeWeight, equals(weightBefore)); + }); + + test('ignores negative hopCount (flood indicator)', () async { + final pubKey = _hex('fa05'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 1.0); + + final pathsBefore = svc.getRecentPaths(pubKey); + final weightBefore = pathsBefore.first.routeWeight; + + svc.recordFloodPathAttribution( + contactPubKeyHex: pubKey, + pathBytes: [0x01], + hopCount: -1, + successIncrement: 0.5, + maxWeight: 5.0, + ); + await _flush(); + + final pathsAfter = svc.getRecentPaths(pubKey); + expect(pathsAfter.first.routeWeight, equals(weightBefore)); + }); + + test('flood stats still recorded independently', () async { + final pubKey = _hex('fa06'); + await _seed(svc, pubKey, pathBytes: [0x01], hopCount: 1, weight: 1.0); + + // Record a flood success (this updates flood stats). + svc.recordPathResult( + pubKey, + const PathSelection(pathBytes: [], hopCount: -1, useFlood: true), + success: true, + tripTimeMs: 5000, + ); + + // Then attribute the flood success to a path. + svc.recordFloodPathAttribution( + contactPubKeyHex: pubKey, + pathBytes: [0x01], + hopCount: 1, + tripTimeMs: 5000, + successIncrement: 0.5, + maxWeight: 5.0, + ); + await _flush(); + + // Both flood stats and path attribution should exist. + final stats = svc.getFloodStats(pubKey); + expect(stats, isNotNull); + expect(stats!.successCount, equals(1)); + + final paths = svc.getRecentPaths(pubKey); + expect(paths.first.routeWeight, closeTo(1.5, 0.001)); + }); + }); +} diff --git a/test/services/retry_and_protocol_test.dart b/test/services/retry_and_protocol_test.dart new file mode 100644 index 0000000..b58da45 --- /dev/null +++ b/test/services/retry_and_protocol_test.dart @@ -0,0 +1,628 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/models/message.dart'; +import 'package:meshcore_open/services/message_retry_service.dart'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Replicates the SHA-256 computation from [MessageRetryService.computeExpectedAckHash] +/// so tests can cross-check without calling the real implementation twice. +Uint8List _manualAckHash( + int timestampSeconds, + int attemptMasked, // already masked to 0x03 + String text, + Uint8List senderPubKey, +) { + final textBytes = utf8.encode(text); + final buffer = Uint8List(4 + 1 + textBytes.length + senderPubKey.length); + int offset = 0; + + buffer[offset++] = timestampSeconds & 0xFF; + buffer[offset++] = (timestampSeconds >> 8) & 0xFF; + buffer[offset++] = (timestampSeconds >> 16) & 0xFF; + buffer[offset++] = (timestampSeconds >> 24) & 0xFF; + buffer[offset++] = attemptMasked & 0xFF; + + buffer.setRange(offset, offset + textBytes.length, textBytes); + offset += textBytes.length; + buffer.setRange(offset, offset + senderPubKey.length, senderPubKey); + + final hash = sha256.convert(buffer); + return Uint8List.fromList(hash.bytes.sublist(0, 4)); +} + +Uint8List _makeKey(int seed) { + final key = Uint8List(32); + for (int i = 0; i < 32; i++) { + key[i] = (seed + i) & 0xFF; + } + return key; +} + +Uint8List _makeRecipientKey() { + final key = Uint8List(32); + for (int i = 0; i < 32; i++) { + key[i] = (0xAA + i) & 0xFF; + } + return key; +} + +Contact _makeContact({ + required Uint8List publicKey, + int pathLength = -1, + List path = const [], +}) { + return Contact( + publicKey: publicKey, + name: 'Test', + type: 1, + pathLength: pathLength, + path: Uint8List.fromList(path), + lastSeen: DateTime.now(), + ); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +void main() { + // Fixed inputs reused across groups + const int fixedTs = 1700000000; + const String fixedText = 'Hello mesh'; + final Uint8List fixedKey = _makeKey(0x11); + final Uint8List recipientKey = _makeRecipientKey(); + + // ------------------------------------------------------------------------- + group('computeExpectedAckHash — attempt masking', () { + test('attempts 0–3 all produce different hashes', () { + final hashes = List.generate( + 4, + (i) => MessageRetryService.computeExpectedAckHash( + fixedTs, + i, + fixedText, + fixedKey, + ), + ); + + // All four must be pairwise distinct + for (int i = 0; i < hashes.length; i++) { + for (int j = i + 1; j < hashes.length; j++) { + expect( + hashes[i], + isNot(equals(hashes[j])), + reason: 'attempt $i and attempt $j should produce different hashes', + ); + } + } + }); + + test('attempt 4 produces same hash as attempt 0 (4 & 0x03 == 0)', () { + final hash0 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + fixedText, + fixedKey, + ); + final hash4 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 4, + fixedText, + fixedKey, + ); + expect(hash4, equals(hash0)); + }); + + test('attempt 5 produces same hash as attempt 1 (5 & 0x03 == 1)', () { + final hash1 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 1, + fixedText, + fixedKey, + ); + final hash5 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 5, + fixedText, + fixedKey, + ); + expect(hash5, equals(hash1)); + }); + + test('attempt 7 produces same hash as attempt 3 (7 & 0x03 == 3)', () { + final hash3 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 3, + fixedText, + fixedKey, + ); + final hash7 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 7, + fixedText, + fixedKey, + ); + expect(hash7, equals(hash3)); + }); + + test('same inputs always produce the same hash (deterministic)', () { + final first = MessageRetryService.computeExpectedAckHash( + fixedTs, + 2, + fixedText, + fixedKey, + ); + final second = MessageRetryService.computeExpectedAckHash( + fixedTs, + 2, + fixedText, + fixedKey, + ); + expect(first, equals(second)); + }); + + test('hash is exactly 4 bytes long', () { + final hash = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + fixedText, + fixedKey, + ); + expect(hash.length, equals(4)); + }); + + test('hash matches manual SHA-256 computation', () { + for (int attempt = 0; attempt < 4; attempt++) { + final actual = MessageRetryService.computeExpectedAckHash( + fixedTs, + attempt, + fixedText, + fixedKey, + ); + final expected = _manualAckHash(fixedTs, attempt, fixedText, fixedKey); + expect( + actual, + equals(expected), + reason: 'mismatch at attempt $attempt', + ); + } + }); + + test('different timestamps produce different hashes', () { + final hashA = MessageRetryService.computeExpectedAckHash( + 1700000000, + 0, + fixedText, + fixedKey, + ); + final hashB = MessageRetryService.computeExpectedAckHash( + 1700000001, + 0, + fixedText, + fixedKey, + ); + expect(hashA, isNot(equals(hashB))); + }); + + test('different texts produce different hashes', () { + final hashA = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + 'Hello mesh', + fixedKey, + ); + final hashB = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + 'Hello mesh!', + fixedKey, + ); + expect(hashA, isNot(equals(hashB))); + }); + + test('different sender keys produce different hashes', () { + final keyA = _makeKey(0x01); + final keyB = _makeKey(0x02); + final hashA = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + fixedText, + keyA, + ); + final hashB = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + fixedText, + keyB, + ); + expect(hashA, isNot(equals(hashB))); + }); + }); + + // ------------------------------------------------------------------------- + group('buildSendTextMsgFrame — attempt encoding', () { + // Frame layout: [cmd(1)][txtType(1)][attempt(1)][timestamp(4)][pubKeyPrefix(6)][text][null(1)] + // So byte index 2 carries the raw attempt & 0xFF. + + test('attempt 0 → byte[2] is 0', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 0, + timestampSeconds: fixedTs, + ); + expect(frame[2], equals(0)); + }); + + test('attempt 3 → byte[2] is 3', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 3, + timestampSeconds: fixedTs, + ); + expect(frame[2], equals(3)); + }); + + test('attempt 4 → byte[2] is 4 (raw value, not clamped to 3)', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 4, + timestampSeconds: fixedTs, + ); + expect(frame[2], equals(4)); + }); + + test('attempt 255 → byte[2] is 255', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 255, + timestampSeconds: fixedTs, + ); + expect(frame[2], equals(255)); + }); + + test('attempt 256 → byte[2] is 255 (clamped, not wrapped)', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 256, + timestampSeconds: fixedTs, + ); + expect(frame[2], equals(255)); + }); + + test('byte[0] is cmdSendTxtMsg (2)', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 0, + timestampSeconds: fixedTs, + ); + expect(frame[0], equals(cmdSendTxtMsg)); + }); + + test('byte[1] is txtTypePlain (0)', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 0, + timestampSeconds: fixedTs, + ); + expect(frame[1], equals(txtTypePlain)); + }); + + test('timestamp bytes[3..6] are little-endian encoded', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 0, + timestampSeconds: fixedTs, + ); + final decoded = + frame[3] | (frame[4] << 8) | (frame[5] << 16) | (frame[6] << 24); + expect(decoded, equals(fixedTs)); + }); + + test( + 'pub key prefix (bytes 7..12) matches first 6 bytes of recipient key', + () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 0, + timestampSeconds: fixedTs, + ); + expect(frame.sublist(7, 13), equals(recipientKey.sublist(0, 6))); + }, + ); + + test('frame is null-terminated after text', () { + final frame = buildSendTextMsgFrame( + recipientKey, + 'hi', + attempt: 0, + timestampSeconds: fixedTs, + ); + expect(frame.last, equals(0)); + }); + }); + + // ------------------------------------------------------------------------- + group( + 'ACK hash consistency between computeExpectedAckHash and firmware behavior', + () { + // The firmware reads the raw attempt byte from the frame, then masks it + // with & 3 when computing the ACK hash. Flutter does the same masking + // inside computeExpectedAckHash. So the two sides must agree. + + test('attempt 4: flutter hash (4 & 3 = 0) equals hash for attempt 0', () { + // Flutter sends raw byte 4 in the frame, but computes hash with 4&3=0. + // Firmware reads 4, masks to 0, computes same hash → they match. + final hashFor4 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 4, + fixedText, + fixedKey, + ); + final hashFor0 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + fixedText, + fixedKey, + ); + expect(hashFor4, equals(hashFor0)); + + // Also confirm the frame byte is raw 4, not 0 + final frame = buildSendTextMsgFrame( + recipientKey, + fixedText, + attempt: 4, + timestampSeconds: fixedTs, + ); + expect(frame[2], equals(4), reason: 'frame carries raw attempt byte'); + }); + + test( + 'attempt 3: flutter hash equals hash computed directly for attempt 3', + () { + // 3 & 3 == 3, so no wrapping — both sides agree. + final hashFor3 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 3, + fixedText, + fixedKey, + ); + final hashFor3Direct = _manualAckHash( + fixedTs, + 3, + fixedText, + fixedKey, + ); + expect(hashFor3, equals(hashFor3Direct)); + + final frame = buildSendTextMsgFrame( + recipientKey, + fixedText, + attempt: 3, + timestampSeconds: fixedTs, + ); + expect(frame[2], equals(3)); + }, + ); + + test( + 'attempt 3 and attempt 4 produce DIFFERENT hashes (3&3=3 vs 4&3=0)', + () { + final hash3 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 3, + fixedText, + fixedKey, + ); + final hash4 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 4, + fixedText, + fixedKey, + ); + expect(hash3, isNot(equals(hash4))); + }, + ); + + test('attempt 8 (8&3=0) produces the same hash as attempt 0', () { + final hash8 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 8, + fixedText, + fixedKey, + ); + final hash0 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + fixedText, + fixedKey, + ); + expect(hash8, equals(hash0)); + }); + + test( + 'hash cycle repeats every 4 attempts (modular arithmetic holds)', + () { + for (int base = 0; base < 4; base++) { + final hashBase = MessageRetryService.computeExpectedAckHash( + fixedTs, + base, + fixedText, + fixedKey, + ); + final hashPlus4 = MessageRetryService.computeExpectedAckHash( + fixedTs, + base + 4, + fixedText, + fixedKey, + ); + final hashPlus8 = MessageRetryService.computeExpectedAckHash( + fixedTs, + base + 8, + fixedText, + fixedKey, + ); + expect( + hashPlus4, + equals(hashBase), + reason: 'attempt ${base + 4} should match attempt $base', + ); + expect( + hashPlus8, + equals(hashBase), + reason: 'attempt ${base + 8} should match attempt $base', + ); + } + }, + ); + }, + ); + + // ------------------------------------------------------------------------- + group('_AckHashMapping.attemptIndex — indirect verification via public API', () { + // _AckHashMapping is private; we validate its purpose indirectly: that + // computeExpectedAckHash records the correct per-attempt hash so that the + // right hash is matched when an ACK arrives. + + test('each attempt index 0–3 produces a distinct 4-byte hash', () { + final hashes = {}; + for (int attempt = 0; attempt < 4; attempt++) { + final hash = MessageRetryService.computeExpectedAckHash( + fixedTs, + attempt, + fixedText, + fixedKey, + ); + final hex = hash.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + expect( + hashes.containsKey(hex), + isFalse, + reason: 'attempt $attempt collides with attempt ${hashes[hex]}', + ); + hashes[hex] = attempt; + } + expect(hashes.length, equals(4)); + }); + + test( + 'attempt index wraps: hash for attempt 4 matches stored hash for attempt 0', + () { + final storedHash = MessageRetryService.computeExpectedAckHash( + fixedTs, + 0, + fixedText, + fixedKey, + ); + // Simulates firmware reading raw attempt=4 and masking to 0 for hash. + final firmwareComputedHash = _manualAckHash( + fixedTs, + 4 & 0x03, // firmware masks here + fixedText, + fixedKey, + ); + expect(firmwareComputedHash, equals(storedHash)); + }, + ); + + test( + 'attempt index 1 and 5 map to the same slot — ACK from either retry is matched', + () { + final hashForAttempt1 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 1, + fixedText, + fixedKey, + ); + final hashForAttempt5 = MessageRetryService.computeExpectedAckHash( + fixedTs, + 5, + fixedText, + fixedKey, + ); + // Both should produce the identical bytes, confirming the service + // would record and match the correct attempt index. + expect(hashForAttempt5, equals(hashForAttempt1)); + }, + ); + }); + + group('sendMessageWithRetry — auto path fallback', () { + test( + 'preserves the contact path when auto-selection returns null', + () async { + final retryService = MessageRetryService(); + Message? addedMessage; + final contact = _makeContact( + publicKey: recipientKey, + pathLength: 2, + path: const [0x10, 0x20], + ); + + retryService.initialize( + RetryServiceConfig( + sendMessage: (_, _, _, _) {}, + addMessage: (_, message) => addedMessage = message, + updateMessage: (_) {}, + clearContactPath: (_) {}, + setContactPath: (_, _, _) {}, + selectRetryPath: (_, _, _, _) => null, + ), + ); + + await retryService.sendMessageWithRetry( + contact: contact, + text: 'hello', + ); + + expect(addedMessage, isNotNull); + expect(addedMessage!.pathLength, equals(2)); + expect( + addedMessage!.pathBytes, + equals(Uint8List.fromList([0x10, 0x20])), + ); + }, + ); + + test('uses flood when contact is in flood mode', () async { + final retryService = MessageRetryService(); + Message? addedMessage; + final contact = _makeContact( + publicKey: recipientKey, + pathLength: -1, + path: const [], + ); + + retryService.initialize( + RetryServiceConfig( + sendMessage: (_, _, _, _) {}, + addMessage: (_, message) => addedMessage = message, + updateMessage: (_) {}, + clearContactPath: (_) {}, + setContactPath: (_, _, _) {}, + ), + ); + + await retryService.sendMessageWithRetry(contact: contact, text: 'hello'); + + expect(addedMessage, isNotNull); + expect(addedMessage!.pathLength, equals(-1)); + expect(addedMessage!.pathBytes, isEmpty); + }); + }); +} From 4ad4a93a207dc10f79b12aff7d15ee11305747a6 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Fri, 20 Mar 2026 01:55:08 -0700 Subject: [PATCH 325/421] formatted code --- lib/connector/meshcore_connector.dart | 94 +++++++++++--------- lib/services/message_retry_service.dart | 17 ++-- lib/services/path_history_service.dart | 26 +++--- test/models/model_changes_test.dart | 36 ++++---- test/services/path_history_service_test.dart | 86 ++++++++++-------- 5 files changed, 143 insertions(+), 116 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d00d49a..ef2f9b7 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -699,43 +699,44 @@ class MeshCoreConnector extends ChangeNotifier { _loadChannelOrder(); // Initialize retry service callbacks - _retryService?.initialize(RetryServiceConfig( - sendMessage: _sendMessageDirect, - addMessage: _addMessage, - updateMessage: _updateMessage, - clearContactPath: clearContactPath, - setContactPath: setContactPath, - calculateTimeout: - (pathLength, messageBytes, {String? contactKey}) => calculateTimeout( - pathLength: pathLength, - messageBytes: messageBytes, - contactKey: contactKey, - ), - getSelfPublicKey: () => _selfPublicKey, - prepareContactOutboundText: prepareContactOutboundText, - appSettingsService: appSettingsService, - debugLogService: _appDebugLogService, - recordPathResult: _recordPathResult, - selectRetryPath: - (contactKey, attemptIndex, maxRetries, recentSelections) => - _selectAutoPathForAttempt( - contactKey, - attemptIndex: attemptIndex, - maxRetries: maxRetries, - recentSelections: recentSelections, - ), - onDeliveryObserved: - (contactKey, pathLength, messageBytes, tripTimeMs) { - final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; - _timeoutPredictionService?.recordObservation( - contactKey: contactKey, + _retryService?.initialize( + RetryServiceConfig( + sendMessage: _sendMessageDirect, + addMessage: _addMessage, + updateMessage: _updateMessage, + clearContactPath: clearContactPath, + setContactPath: setContactPath, + calculateTimeout: (pathLength, messageBytes, {String? contactKey}) => + calculateTimeout( pathLength: pathLength, messageBytes: messageBytes, - tripTimeMs: tripTimeMs, - secondsSinceLastRx: secSinceRx, - ); - }, - )); + contactKey: contactKey, + ), + getSelfPublicKey: () => _selfPublicKey, + prepareContactOutboundText: prepareContactOutboundText, + appSettingsService: appSettingsService, + debugLogService: _appDebugLogService, + recordPathResult: _recordPathResult, + selectRetryPath: + (contactKey, attemptIndex, maxRetries, recentSelections) => + _selectAutoPathForAttempt( + contactKey, + attemptIndex: attemptIndex, + maxRetries: maxRetries, + recentSelections: recentSelections, + ), + onDeliveryObserved: (contactKey, pathLength, messageBytes, tripTimeMs) { + final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds; + _timeoutPredictionService?.recordObservation( + contactKey: contactKey, + pathLength: pathLength, + messageBytes: messageBytes, + tripTimeMs: tripTimeMs, + secondsSinceLastRx: secSinceRx, + ); + }, + ), + ); final maxRetries = _appSettingsService?.settings.maxMessageRetries ?? 5; _retryService?.setMaxRetries(maxRetries); } @@ -908,7 +909,8 @@ class MeshCoreConnector extends ChangeNotifier { List recentSelections = const [], }) { final hasKnownPaths = - _pathHistoryService?.getRecentPaths(contactPubKeyHex).isNotEmpty ?? false; + _pathHistoryService?.getRecentPaths(contactPubKeyHex).isNotEmpty ?? + false; if (!hasKnownPaths) { return null; } @@ -3619,10 +3621,7 @@ class MeshCoreConnector extends ChangeNotifier { void _handleIncomingChannelMessage(Uint8List frame) { final parsed = ChannelMessage.fromFrame(frame); if (parsed != null && parsed.channelIndex != null) { - if (_shouldDropSelfChannelMessage( - parsed.senderName, - parsed.pathBytes, - )) { + if (_shouldDropSelfChannelMessage(parsed.senderName, parsed.pathBytes)) { return; } final contentHash = _computeContentHash( @@ -4246,7 +4245,9 @@ class MeshCoreConnector extends ChangeNotifier { final pathLenRaw = raw[index++]; final pathByteLen = _decodePathByteLen(pathLenRaw); if (raw.length < index + pathByteLen) return null; - final pathBytes = Uint8List.fromList(raw.sublist(index, index + pathByteLen)); + final pathBytes = Uint8List.fromList( + raw.sublist(index, index + pathByteLen), + ); index += pathByteLen; if (raw.length <= index) return null; final payload = Uint8List.fromList(raw.sublist(index)); @@ -4273,12 +4274,19 @@ class MeshCoreConnector extends ChangeNotifier { input[0] = payloadType; input.setRange(1, input.length, payload); final digest = crypto.sha256.convert(input).bytes; - return digest.sublist(0, 8).map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + return digest + .sublist(0, 8) + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); } /// Content-based dedup hash for sync queue messages (no raw payload available). /// Prefixed with 'c:' to avoid collisions with packet hashes. - String _computeContentHash(int channelIdx, int timestampSecs, String fullText) { + String _computeContentHash( + int channelIdx, + int timestampSecs, + String fullText, + ) { final textBytes = utf8.encode(fullText); final input = Uint8List(5 + textBytes.length); input[0] = channelIdx; diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 2f10511..b284425 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -22,7 +22,11 @@ class _AckHistoryEntry { } /// (messageId, timestamp, attemptIndex) — stored per ACK hash for O(1) lookup. -typedef AckHashMapping = ({String messageId, DateTime timestamp, int attemptIndex}); +typedef AckHashMapping = ({ + String messageId, + DateTime timestamp, + int attemptIndex, +}); class RetryServiceConfig { final void Function(Contact, String, int, int) sendMessage; @@ -31,7 +35,7 @@ class RetryServiceConfig { final Function(Contact)? clearContactPath; final Function(Contact, Uint8List, int)? setContactPath; final int Function(int pathLength, int messageBytes, {String? contactKey})? - calculateTimeout; + calculateTimeout; final Uint8List? Function()? getSelfPublicKey; final String Function(Contact, String)? prepareContactOutboundText; final AppSettingsService? appSettingsService; @@ -43,7 +47,8 @@ class RetryServiceConfig { int attemptIndex, int maxRetries, List recentSelections, - )? selectRetryPath; + )? + selectRetryPath; const RetryServiceConfig({ required this.sendMessage, @@ -132,7 +137,8 @@ class MessageRetryService extends ChangeNotifier { }) async { final messageId = const Uuid().v4(); final resolved = resolvePathSelection(contact); - final messagePathBytes = pathBytes ?? Uint8List.fromList(resolved.pathBytes); + final messagePathBytes = + pathBytes ?? Uint8List.fromList(resolved.pathBytes); final messagePathLength = pathLength ?? (resolved.useFlood ? -1 : resolved.hopCount); final message = Message( @@ -262,7 +268,8 @@ class MessageRetryService extends ChangeNotifier { if (config.setContactPath != null && config.clearContactPath != null) { final bool useFlood = currentSelection != null ? currentSelection.useFlood - : (effectiveMessage.pathLength != null && effectiveMessage.pathLength! < 0); + : (effectiveMessage.pathLength != null && + effectiveMessage.pathLength! < 0); final List pathBytes = currentSelection != null ? currentSelection.pathBytes : effectiveMessage.pathBytes; diff --git a/lib/services/path_history_service.dart b/lib/services/path_history_service.dart index 809f867..68a9245 100644 --- a/lib/services/path_history_service.dart +++ b/lib/services/path_history_service.dart @@ -469,17 +469,18 @@ class PathHistoryService extends ChangeNotifier { final highestRouteWeight = _getHighestKnownRouteWeight(ranked); ranked.sort((a, b) { - final scoreCompare = _scorePathRecord( - b, - fastestTripMs: fastestTripMs, - highestRouteWeight: highestRouteWeight, - ).compareTo( - _scorePathRecord( - a, - fastestTripMs: fastestTripMs, - highestRouteWeight: highestRouteWeight, - ), - ); + final scoreCompare = + _scorePathRecord( + b, + fastestTripMs: fastestTripMs, + highestRouteWeight: highestRouteWeight, + ).compareTo( + _scorePathRecord( + a, + fastestTripMs: fastestTripMs, + highestRouteWeight: highestRouteWeight, + ), + ); if (scoreCompare != 0) { return scoreCompare; } @@ -531,8 +532,7 @@ class PathHistoryService extends ChangeNotifier { (DateTime.now().difference(path.timestamp!).inMinutes / 60.0 / 24.0)); - final routeWeight = - (path.routeWeight / highestRouteWeight).clamp(0.0, 1.0); + final routeWeight = (path.routeWeight / highestRouteWeight).clamp(0.0, 1.0); return (reliability * 0.45) + (latency * 0.25) + diff --git a/test/models/model_changes_test.dart b/test/models/model_changes_test.dart index 165b91d..a80c794 100644 --- a/test/models/model_changes_test.dart +++ b/test/models/model_changes_test.dart @@ -15,7 +15,9 @@ Uint8List _buildContactFrame({ }) { final writer = BytesBuilder(); writer.addByte(respCodeContact); // 3 - writer.add(pubKey ?? Uint8List.fromList(List.generate(32, (i) => i + 1))); // valid pubkey + writer.add( + pubKey ?? Uint8List.fromList(List.generate(32, (i) => i + 1)), + ); // valid pubkey writer.addByte(1); // type writer.addByte(0); // flags writer.addByte(pathLen); @@ -239,21 +241,23 @@ void main() { expect(record.routeWeight, equals(4.0)); }); - test('fromJson with missing route_weight defaults to 1.0 (backward compat)', - () { - final json = { - 'hop_count': 1, - 'trip_time_ms': 100, - 'timestamp': DateTime(2024).toIso8601String(), - 'was_flood': false, - 'path_bytes': [], - 'success_count': 0, - 'failure_count': 0, - // 'route_weight' intentionally omitted - }; - final record = PathRecord.fromJson(json); - expect(record.routeWeight, equals(1.0)); - }); + test( + 'fromJson with missing route_weight defaults to 1.0 (backward compat)', + () { + final json = { + 'hop_count': 1, + 'trip_time_ms': 100, + 'timestamp': DateTime(2024).toIso8601String(), + 'was_flood': false, + 'path_bytes': [], + 'success_count': 0, + 'failure_count': 0, + // 'route_weight' intentionally omitted + }; + final record = PathRecord.fromJson(json); + expect(record.routeWeight, equals(1.0)); + }, + ); }); group('AppSettings — new fields', () { diff --git a/test/services/path_history_service_test.dart b/test/services/path_history_service_test.dart index 561bad3..87ae729 100644 --- a/test/services/path_history_service_test.dart +++ b/test/services/path_history_service_test.dart @@ -140,7 +140,11 @@ void main() { attemptIndex: i, maxRetries: 5, ); - expect(selection.useFlood, isFalse, reason: 'attempt $i should be path'); + expect( + selection.useFlood, + isFalse, + reason: 'attempt $i should be path', + ); expect(selection.pathBytes, equals([0x01, 0x02])); } }); @@ -400,45 +404,49 @@ void main() { expect(first.pathBytes, equals([0x02])); }); - test('higher route weight wins when other factors are effectively tied', () async { - final pubKey = _hex('rank4'); - final sharedTimestamp = - DateTime.now().subtract(const Duration(minutes: 30)); - storage._store[pubKey] = ContactPathHistory( - contactPubKeyHex: pubKey, - recentPaths: [ - PathRecord( - hopCount: 1, - tripTimeMs: 750, - timestamp: sharedTimestamp, - wasFloodDiscovery: false, - pathBytes: const [0x01], - successCount: 1, - failureCount: 0, - routeWeight: 4.0, - ), - PathRecord( - hopCount: 1, - tripTimeMs: 750, - timestamp: sharedTimestamp, - wasFloodDiscovery: false, - pathBytes: const [0x02], - successCount: 1, - failureCount: 0, - routeWeight: 1.0, - ), - ], - ); - svc.getRecentPaths(pubKey); - await _flush(); + test( + 'higher route weight wins when other factors are effectively tied', + () async { + final pubKey = _hex('rank4'); + final sharedTimestamp = DateTime.now().subtract( + const Duration(minutes: 30), + ); + storage._store[pubKey] = ContactPathHistory( + contactPubKeyHex: pubKey, + recentPaths: [ + PathRecord( + hopCount: 1, + tripTimeMs: 750, + timestamp: sharedTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x01], + successCount: 1, + failureCount: 0, + routeWeight: 4.0, + ), + PathRecord( + hopCount: 1, + tripTimeMs: 750, + timestamp: sharedTimestamp, + wasFloodDiscovery: false, + pathBytes: const [0x02], + successCount: 1, + failureCount: 0, + routeWeight: 1.0, + ), + ], + ); + svc.getRecentPaths(pubKey); + await _flush(); - final first = svc.selectPathForAttempt( - pubKey, - attemptIndex: 0, - maxRetries: 5, - ); - expect(first.pathBytes, equals([0x01])); - }); + final first = svc.selectPathForAttempt( + pubKey, + attemptIndex: 0, + maxRetries: 5, + ); + expect(first.pathBytes, equals([0x01])); + }, + ); }); // ------------------------------------------------------------------------- From cb63b48b7831a5f2f971aeed2efd9e6dfeb8d443 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Fri, 20 Mar 2026 02:24:02 -0700 Subject: [PATCH 326/421] Add comprehensive documentation for various app features - Introduced "Contacts" documentation detailing the contact management system, types, list, search, and tap actions. - Added "Map & Location" documentation covering map features, interactions, path tracing, and line-of-sight analysis. - Created "Navigation" documentation outlining app flow, QuickSwitchBar, and device screen interactions. - Developed "Notifications" documentation explaining notification types, in-app badges, settings, and rate limiting. - Established "Repeater Management" documentation for managing repeaters and room servers, including CLI access and telemetry. - Compiled "Scanner & Connection" documentation detailing BLE, USB, and TCP connection processes. - Formulated "Settings" documentation outlining access, layout, device info, app settings, node settings, actions, debug options, export features, and about section. --- documentation/README.md | 30 +++ documentation/additional-features.md | 187 ++++++++++++++++++ documentation/ble-protocol.md | 249 ++++++++++++++++++++++++ documentation/channels.md | 164 ++++++++++++++++ documentation/chat-and-messaging.md | 120 ++++++++++++ documentation/contacts.md | 118 +++++++++++ documentation/map-and-location.md | 186 ++++++++++++++++++ documentation/navigation.md | 87 +++++++++ documentation/notifications.md | 92 +++++++++ documentation/repeater-management.md | 186 ++++++++++++++++++ documentation/scanner-and-connection.md | 124 ++++++++++++ documentation/settings.md | 169 ++++++++++++++++ 12 files changed, 1712 insertions(+) create mode 100644 documentation/README.md create mode 100644 documentation/additional-features.md create mode 100644 documentation/ble-protocol.md create mode 100644 documentation/channels.md create mode 100644 documentation/chat-and-messaging.md create mode 100644 documentation/contacts.md create mode 100644 documentation/map-and-location.md create mode 100644 documentation/navigation.md create mode 100644 documentation/notifications.md create mode 100644 documentation/repeater-management.md create mode 100644 documentation/scanner-and-connection.md create mode 100644 documentation/settings.md diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 0000000..1367013 --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,30 @@ +# MeshCore Open - Feature Documentation + +MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does. + +## Table of Contents + +1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection +2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation +3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing +4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries +5. [Channels](channels.md) - Broadcast channels, communities, and channel chat +6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching +7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports +8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences +9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors +10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more +11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture + +## App Overview + +MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can: + +- **Chat** with other mesh nodes via encrypted direct messages +- **Broadcast** on shared channels (public, hashtag, private, or community-scoped) +- **View nodes on a map** with GPS locations, predicted positions, and path traces +- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings +- **Share contacts** via `meshcore://` URIs and QR codes +- **Configure radio settings** including frequency, power, bandwidth, and spreading factor +- **Cache offline maps** for use without internet connectivity +- **Analyze line-of-sight** between nodes with terrain elevation profiles diff --git a/documentation/additional-features.md b/documentation/additional-features.md new file mode 100644 index 0000000..f7b8319 --- /dev/null +++ b/documentation/additional-features.md @@ -0,0 +1,187 @@ +# Additional Features + +## GIF Picker (Giphy Integration) + +### How to Access +In any chat screen (direct or channel), tap the GIF button in the message input bar. + +### What the User Sees +A bottom sheet with a search field and a grid of GIF thumbnails. + +### Key Interactions +- On open, loads trending GIFs (G-rated, 25 results) +- Type to search and press the keyboard submit button (search triggers on submit, not on each keystroke). Clearing the search field reloads trending GIFs +- On network/API errors, a "Retry" button is shown in-place +- Tap a GIF to select it — the chat input shows an inline preview with an X button to dismiss +- Send the message to transmit the GIF reference (`g:`) +- Recipients see the GIF rendered inline via Giphy CDN +- "Powered by Giphy" attribution is always shown at the bottom of the picker +- The bottom sheet occupies 70% of screen height + +--- + +## Localization / Multi-Language Support + +### How to Access +App Settings → Appearance → Language + +### Supported Languages (15) +English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian + +### How It Works +- All UI strings go through Flutter's ARB localization system +- Language can follow the system locale or be explicitly overridden +- Changes take effect immediately + +--- + +## Discovered Contacts Screen + +### How to Access +From Contacts screen → overflow menu → "Discovered Contacts" + +### What the User Sees +A list of nodes heard passively over the air but not yet added as contacts. Each shows: +- Color-coded avatar (by type) +- Name +- Short public key +- Last-seen time + +### Key Interactions +- Search bar with debounced filtering +- Sort by last seen or name; filter by type +- **Tap**: Import the contact (adds to your contact list) +- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete +- Overflow menu → "Delete All" (with confirmation) +- Already-known contacts and your own node are filtered out + +--- + +## SMAZ Compression + +### What It Is +An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text). + +### How to Enable +- **Per contact**: Chat screen → info button → toggle "SMAZ compression" +- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression" + +### How It Works +- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed +- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data +- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text +- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages +- Disabled by default + +--- + +## Community QR Scanner + +### How to Access +From Channels screen → "+" FAB → "Scan Community QR" + +### What the User Sees +A live QR scanner view with instruction text overlay. + +### Key Interactions +- Scan a community QR code shared by another member +- On valid scan: confirmation dialog showing community name and ID +- Option to "Add public channel to device" on join +- If already a member: shows an "Already a member" dialog +- Invalid QR: shows an orange error snackbar + +--- + +## Channel Message Path Viewing + +### How to Access +In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop). + +### What the User Sees +- Summary card: sender, time, repeat count, path type, observed hops +- "Other Observed Paths" section (if multiple paths detected) +- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates + +### Actions +- **Radar icon**: Opens path trace map for live trace +- **Map icon**: Opens a map with hop markers and polyline +- **Path dropdown**: Switch between observed path variants (if multiple) + +--- + +## Debug Logging + +### BLE Debug Log +**Access**: Settings → BLE Debug Log + +Two views: +- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex. +- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary. + +### App Debug Log +**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug) + +Structured log entries with level (Info/Warning/Error), tag, message, and timestamp. + +Both logs support copy-all and clear operations. + +--- + +## Chrome Required Screen + +### When It Appears +Automatically shown on web platforms when a non-Chromium browser is detected. + +### What the User Sees +A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational. + +--- + +## Path History Service + +### What It Does (Background Service) +Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking: +- Hop count and trip time +- Success/failure counts and route weights +- Flood vs. direct discovery + +### Path Scoring +Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path. + +Used internally for: +- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths +- **Path selection**: Picks the best-scored path for each retry attempt +- **Flood statistics**: Tracks flood vs. direct discovery ratios + +--- + +## Message Retry Service + +### What It Does (Background Service) +Handles reliable delivery of outgoing direct messages: +1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued +2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]` +3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...) +4. Each retry may use a different path (via path history diversity window) +5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path +6. Reports RTT and path data for quality learning +7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs + +### Configurable Settings (App Settings → Messaging) +- Max retries (2–10, default 5) +- Clear path on max retry (on/off) +- Auto route rotation with weight parameters + +--- + +## Timeout Prediction (ML) + +### What It Does (Background Service) +An ML-based service that predicts expected delivery timeouts: +- Collects delivery observations (path length, message size, time since last RX, delivery time) in a sliding window of up to 100 observations (oldest evicted first) +- Requires **10 minimum observations** before first training. After that, retrains every 5 new observations +- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time) +- Features with zero variance are automatically excluded from training +- Blends per-contact statistics with ML predictions +- Falls back to `3000 + 3000 × pathLength` ms when insufficient data +- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost) diff --git a/documentation/ble-protocol.md b/documentation/ble-protocol.md new file mode 100644 index 0000000..9f4c1d7 --- /dev/null +++ b/documentation/ble-protocol.md @@ -0,0 +1,249 @@ +# BLE Protocol & Data Layer + +This is a technical reference for the communication protocol and data architecture. + +## Transport Layer + +The app supports three transports, all sharing the same command/response protocol: + +| Transport | Method | Implementation | +|---|---|---| +| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` | +| USB Serial | Packet-framed serial | `MeshCoreUsbManager` | +| TCP | Packet-framed socket | `MeshCoreTcpConnector` | + +### BLE (Nordic UART Service) + +- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e` +- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e` +- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e` + +Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response". + +### USB and TCP Framing + +Both use a lightweight packet framing codec: + +``` +TX (host → device): [0x3C][len_lo][len_hi][payload...] +RX (device → host): [0x3E][len_lo][len_hi][payload...] +``` + +- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming +- Length: 2-byte little-endian, payload only +- Max payload: 172 bytes +- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving +- USB: 10ms post-write delay between frames + +## Connection State Machine + +``` +enum MeshCoreConnectionState { + disconnected, + scanning, + connecting, + connected, + disconnecting, +} +``` + +## BLE Connection Lifecycle + +1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]` +2. **Connect** with 15-second timeout +3. **Request MTU** 185 bytes (non-web only) +4. **Discover services** and locate NUS +5. **Enable TX notifications** (up to 3 attempts on native) +6. **Subscribe** to TX characteristic for incoming frames +7. **Initial sync**: device info query, time sync, channel sync + +## Auto-Reconnect (BLE Only) + +On unexpected disconnection, auto-reconnect with exponential backoff: +- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s... +- Resets on successful connection +- Disabled for manual disconnects +- Not available for USB or TCP + +## Protocol Constants + +| Constant | Value | Description | +|---|---|---| +| Max frame size | 172 bytes | BLE/USB/TCP payload limit | +| Public key size | 32 bytes | Ed25519 public key | +| Max path size | 64 bytes | Maximum path data | +| Max name size | 32 bytes | Maximum node name | +| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` | +| App protocol version | 3 | Sent in device query | +| Contact frame size | 148 bytes | Fixed-size contact record | + +## Command Codes (App → Device) + +| Code | Name | Description | +|------|------|-------------| +| 1 | CMD_APP_START | Announce app connection | +| 2 | CMD_SEND_TXT_MSG | Send direct text message | +| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message | +| 4 | CMD_GET_CONTACTS | Request contact list | +| 5 | CMD_GET_DEVICE_TIME | Query device clock | +| 6 | CMD_SET_DEVICE_TIME | Set device clock | +| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement | +| 8 | CMD_SET_ADVERT_NAME | Set node name | +| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact | +| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message | +| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters | +| 12 | CMD_SET_RADIO_TX_POWER | Set TX power | +| 13 | CMD_RESET_PATH | Reset contact path | +| 14 | CMD_SET_ADVERT_LATLON | Set advertised location | +| 15 | CMD_REMOVE_CONTACT | Remove a contact | +| 16 | CMD_SHARE_CONTACT | Share contact to mesh | +| 17 | CMD_EXPORT_CONTACT | Export contact as bytes | +| 18 | CMD_IMPORT_CONTACT | Import contact from bytes | +| 19 | CMD_REBOOT | Reboot device | +| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage | +| 22 | CMD_DEVICE_QUERY | Query device info | +| 26 | CMD_SEND_LOGIN | Login to repeater/room | +| 27 | CMD_SEND_STATUS_REQ | Request repeater status | +| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key | +| 31 | CMD_GET_CHANNEL | Get channel definition | +| 32 | CMD_SET_CHANNEL | Set channel name and PSK | +| 36 | CMD_SEND_TRACE_PATH | Request path trace | +| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters | +| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry | +| 40 | CMD_GET_CUSTOM_VAR | Get custom variables | +| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable | +| 50 | CMD_SEND_BINARY_REQ | Send binary request | +| 57 | CMD_SEND_ANON_REQ | Send anonymous request | +| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration | +| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration | + +## Response / Push Codes (Device → App) + +| Code | Name | Description | +|------|------|-------------| +| 0 | RESP_CODE_OK | Generic success | +| 1 | RESP_CODE_ERR | Generic error | +| 2 | RESP_CODE_CONTACTS_START | Contact list begins | +| 3 | RESP_CODE_CONTACT | Single contact data | +| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete | +| 5 | RESP_CODE_SELF_INFO | Device self-info response | +| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [2–5]=ack_hash, [6–9]=estimated_timeout_ms` | +| 7 | RESP_CODE_CONTACT_MSG_RECV | Incoming direct message (v2) | +| 8 | RESP_CODE_CHANNEL_MSG_RECV | Incoming channel message (v2) | +| 10 | RESP_CODE_NO_MORE_MESSAGES | No more queued messages | +| 11 | RESP_CODE_EXPORT_CONTACT | Exported contact data | +| 9 | RESP_CODE_CURR_TIME | Current device time | +| 12 | RESP_CODE_BATT_AND_STORAGE | Battery mV (uint16 LE) + storage used/total (uint32 LE each) | +| 13 | RESP_CODE_DEVICE_INFO | Firmware info | +| 16 | RESP_CODE_CONTACT_MSG_RECV_V3 | Incoming direct message (v3) | +| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) | +| 18 | RESP_CODE_CHANNEL_INFO | Channel definition | +| 21 | RESP_CODE_CUSTOM_VARS | Custom variables | +| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags | +| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen | +| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact | +| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) | +| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued | +| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded | +| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed | +| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response | +| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet | +| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result | +| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered | +| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data | +| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response | + +## Data Models + +### Contact +32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout: + +``` +[0] = resp_code +[1–32] = public key (32 bytes) +[33] = type (1=chat, 2=repeater, 3=room, 4=sensor) +[34] = flags (bit 0 = favorite) +[35] = path_length +[36–99] = path (64 bytes) +[100–131] = name (32 bytes, null-padded) +[132–135] = timestamp (uint32 LE) +[136–139] = latitude (int32 LE, × 1e-6 degrees) +[140–143] = longitude (int32 LE, × 1e-6 degrees) +[144–147] = last_modified (uint32 LE) +``` + +### Message (Direct) +Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions. + +### Channel Message +Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields. + +### Channel +Index (0–7), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels. + +### Community +UUID, name, 32-byte secret, hashtag channel list. Shared via QR code. + +## Persistence + +All data is stored via `SharedPreferences` (JSON-serialized). No SQLite or other database. + +| Data | Storage Key Pattern | Scope | +|---|---|---| +| Contacts | `contacts` | Per device identity | +| Messages | `messages_` | Per device + contact | +| Channel Messages | `channel_messages_` | Per device + channel | +| Channels | `channels` | Per device identity | +| Channel Order | `channel_order_` | Per device identity | +| Contact Groups | `contact_groups` | Per device identity | +| Communities | `communities_v1` | Per device identity | +| Unread Counts | `contact_unread_count` | Per device identity | +| Discovered Contacts | `discovered_contacts` | Global | +| App Settings | `app_settings` | Global | +| Path History | `path_history_` | Per contact | + +## Auto-Add Configuration Bitmask + +Used by `CMD_SET_AUTO_ADD_CONFIG` (58) and `RESP_CODE_AUTO_ADD_CONFIG` (25): + +| Bit | Flag | Description | +|-----|------|-------------| +| 0 | 0x01 | Overwrite oldest contact when list is full | +| 1 | 0x02 | Auto-add chat users | +| 2 | 0x04 | Auto-add repeaters | +| 3 | 0x08 | Auto-add room servers | +| 4 | 0x10 | Auto-add sensors | + +## Radio Packet Payload Types + +Seen inside `PUSH_CODE_LOG_RX_DATA` raw packets: + +| Code | Type | +|------|------| +| 0x00 | REQ (request) | +| 0x01 | RESPONSE | +| 0x02 | TXTMSG (text message) | +| 0x03 | ACK | +| 0x04 | ADVERT | +| 0x05 | GRPTXT (group/channel text) | +| 0x06 | GRPDATA (group data) | +| 0x07 | ANONREQ (anonymous request) | +| 0x08 | PATH | +| 0x09 | TRACE | +| 0x0A | MULTIPART | +| 0x0B | CONTROL | +| 0x0F | RAW_CUSTOM | + +## State Management + +Uses Flutter `Provider` with `ChangeNotifier`. The central state holder is `MeshCoreConnector`, which owns all in-memory collections and fires debounced (50ms) `notifyListeners()` to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand. + +### Data Flow + +1. Raw frames arrive over BLE/USB/TCP +2. First byte is parsed as response/push code +3. Appropriate model factory (`fromFrame()`) parses the data +4. In-memory collections are updated +5. Storage stores are persisted (async) +6. `notifyListeners()` triggers UI rebuilds +7. Screens read current state via getters diff --git a/documentation/channels.md b/documentation/channels.md new file mode 100644 index 0000000..21fb52e --- /dev/null +++ b/documentation/channels.md @@ -0,0 +1,164 @@ +# Channels + +## Overview + +Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh. + +Up to 8 channels (indices 0–7) can be active simultaneously on one device. + +## How to Access + +QuickSwitchBar tab 1 (middle) from any main screen. + +## Channel Types + +| Type | Icon | Color | Description | +|---|---|---|---| +| Public | Globe | Green | Fixed well-known PSK; any device can join | +| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention | +| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key | +| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret | + +## Channels List Screen + +### What the User Sees + +- **Search bar** with live text filtering (300ms debounce) +- **Sort/filter button** +- **Scrollable list of channel cards**, each showing: + - Type icon with color coding (purple badge overlay for community channels) + - Channel name (or "Channel N" if unnamed) + - Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}" + - Unread badge (if messages are unread) + - Drag handle (when manual sort is active) +- **"+" FAB** to add a new channel +- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings + +If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown. + +Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware. + +### Sorting Options + +- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active) +- **A–Z**: Alphabetical +- **Latest messages**: Most recent first +- **Unread**: Most unread first + +## Adding a Channel + +Tap the "+" FAB to open a dialog with six options: + +1. **Create Private Channel** — Enter a name (max 31 characters); a random PSK is generated +2. **Join Private Channel** — Enter a name and a 32-hex PSK (non-hex characters like spaces and dashes are silently stripped, so pasted keys with formatting are accepted) +3. **Join Public Channel** — One tap; uses the well-known public PSK (only shown if no public channel exists) +4. **Join Hashtag Channel** — Enter a hashtag name; PSK is derived from the name. If communities exist, choose between regular hashtag (SHA-256) or community hashtag (HMAC) +5. **Scan Community QR** — Opens QR scanner to join a community +6. **Create Community** — Enter a name; generates a random 32-byte secret; optionally adds a community public channel; shows QR code for sharing + +## Channel Actions (Long-Press / Right-Click) + +| Action | Description | +|---|---| +| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) | +| Mute / Unmute | Toggle push notification suppression for this channel | +| Delete | Remove the channel from the device (confirmation required) | + +## Channel Chat + +Tap a channel card to open the channel chat screen. + +### App Bar + +- Type icon (public/private/hashtag) +- Channel name +- Subtitle: "{type} - {N} unread" + +### Message Display + +- Reverse-scrolling list (newest at bottom) +- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble +- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle) +- Automatic older-message loading on scroll-to-top +- Jump-to-bottom button when scrolled up +- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset text size +- **Message tracing mode** (when enabled in App Settings): Each bubble additionally shows path prefix bytes (`via XX,YY,...`), a timestamp, and a repeat count icon + +### Message Types in Chat + +- **Plain text** with linkified URLs +- **GIFs** (`g:{gifId}`) rendered inline via Giphy CDN +- **Location pins** (`m:{lat},{lon}|{label}|`) shown as tappable location cards +- **Reactions** displayed as emoji pills below target messages + +### Replies (Channel Chat Only) + +- **Mobile**: Swipe an **incoming** message left to trigger reply (with haptic feedback). You cannot swipe your own outgoing messages. Swipe reply is not available on desktop. +- **All platforms**: Long-press → "Reply" +- Reply banner appears above the input bar with the quoted message (tap X to cancel) +- Sent replies are prefixed `@[{senderName}] {text}` +- Received replies show a bordered quote block inside the bubble; tapping scrolls to the original. Reply previews render GIF thumbnails and location pin icons, not just text. + +### Message Path Viewing + +- **Mobile**: Tap a message bubble to view its routing path +- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop) +- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md)) + +### Context Actions (Long-Press / Right-Click) + +| Action | Availability | Description | +|---|---|---| +| Reply | All messages | Triggers reply mode | +| Path | Desktop only | Opens message path view | +| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) | +| Copy | All messages | Copies text to clipboard | +| Delete | All messages | Removes locally (not from mesh) | + +### Message Path Viewing + +Tap a message bubble to open the Channel Message Path Screen, which shows: +- Each hop in the path as a visual chain +- Known contacts identified by name at each hop +- Observed vs. declared hop counts +- Alternative path variants (if received via multiple paths) +- Map view buttons for geographic path visualization + +## Communities + +Communities are a layer above channels that provide a private namespace. + +### What is a Community? + +A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret: +- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]` +- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]` + +Outsiders who don't know the secret cannot discover or join community channels. + +### Sharing a Community + +Communities are shared via QR codes containing a JSON payload: +```json +{"v": 1, "type": "meshcore_community", "name": "...", "k": ""} +``` + +### Managing Communities + +From the channels screen overflow menu → "Manage Communities". Opens a draggable scrollable sheet (resizable 30–90% of screen height): + +- Each community shows its name and a short community ID (first 8 hex characters) +- **Tap a community** to directly show its QR code for sharing +- **Popup menu** per community: + - **Show QR** — displays the QR code for sharing with new members + - **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed) + +## How Channels Differ from Direct Messages + +| Aspect | Channels | Direct Messages | +|---|---|---| +| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact | +| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) | +| Sender identity | Plain text prefix in payload | Verified via public key | +| Replies | Supported (swipe or long-press) | Not supported | +| Retry mechanism | No automatic retry | Exponential backoff with path rotation | diff --git a/documentation/chat-and-messaging.md b/documentation/chat-and-messaging.md new file mode 100644 index 0000000..22030d5 --- /dev/null +++ b/documentation/chat-and-messaging.md @@ -0,0 +1,120 @@ +# Chat & Messaging + +## Overview + +The app supports two chat modes: +- **Direct messages**: Encrypted point-to-point messages to individual contacts +- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md)) + +This page covers direct messaging. For channel chat, see the Channels documentation. + +## How to Access + +From the Contacts screen, tap any Chat-type contact to open the ChatScreen. + +## Chat Screen Layout + +### App Bar + +- **Title**: Contact name +- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details. +- **Action buttons**: + - **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing + - **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries. + - **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle + +### Message List + +- Scrollable list with newest messages at the bottom +- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background +- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name) +- Bubble width capped at 65% of screen width +- Hyperlinks rendered as tappable green underlined text +- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset +- **Jump to bottom**: Floating button appears when scrolled away from the bottom +- **Lazy loading**: Scrolling to top loads older messages from storage + +### Input Bar + +- **GIF button** (left): Opens GIF picker bottom sheet +- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time +- **Send button** (right): Submits the message +- On desktop: Enter/Numpad Enter also submits +- When a GIF is selected, the text field shows an inline GIF preview with a dismiss button + +## Message Types + +| Type | Wire Format | Display | +|---|---|---| +| Plain text | Raw UTF-8 string | Inline text with link detection | +| GIF | `g:` | Inline GIF image from Giphy CDN | +| Location pin | `m:,\|